diff --git a/.efrocachemap b/.efrocachemap
index e6d351ca..01b08fcf 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -3971,26 +3971,26 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
- "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3a/d3/38075453348d9d2abd102554a937",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/1c/1cd7a078b292c0d01d634f97f222",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fa/20/4f69c5c8a3e9bf9afabff8a7118f",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3a/3a/d23f5fabe309b6e8694aff471b9b",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4d/6f/12942dd120a3ec0963249b596989",
- "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4d/90/3b11a947cd2befe576c12dc58a9c",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/77/2c01655f3144cd40c9ee252c7226",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f2/62/a0ba340093f8c143ab39149244e9",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/67/a9/4b48db661bc7b1f6338ee0389e87",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8e/44/c087c45ca09e5a8e4284076d4663",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d8/ee/452c5c9164807e82d97214f7fc5b",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7a/f0/2a4003966c6e7ec531bcfb19f013",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ec/be/637f05ea13de731126047d88a6c2",
- "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/53/c6/2503d6a33f1a86d3504732091540",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/14/ba5b7d47bec90c93af50fe344a1e",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/89/79/e08d502d6b980db9c9506b6614ca",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/62/a7/c88cf55d6242a7b28467b193cb7f",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/36/7e/6f55132fea94bbca6b41cc764a28",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/f4/4a/a7aa2270ca1bd8cc15cb5204d089",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/86/df/dd3bef3462b735d85243e24540f9",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5e/57/faba903f766084e7e4e5b6eebb73",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/05/95/792c416c8a4a399f4ad8a90cda06",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/68/606372da26157d45e396eda4a6fc",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/00/87f70c8158a37abbbfb6e9226aa4",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f3/52/700948881e3ebcacebc842eb8e25",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/11/f1/e4eb196828af3b9b53bfbc8c1b14",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6d/75/b8a6efdc2a5a8d0aa45a151c133c",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5b/9e/b4f1b05d0af9389501057b52141b",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e7/bf/7ce1b7cb02b9589a3d3aa24fb309",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fa/39/3affaf5911a076b45d51d33b9068",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/03/72/5bd95cc26e648c9a53ded3e7c3f7",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cd/a4/4bc45cda2a7777e7f9738895e4bb",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/01/1c/a1973631978d7b7a7d7f85ed6e7c",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e0/1c/c1f316bb1334145f691686c804b6",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7f/81/6543391def25718bf9ed0da8b9f3",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9b/06/5e1bd19506b1806f793637b73325",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/1c/9b/994c9aff7a533da605d3c90bb93f",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/dd/41/fd523388e077ca84d37a4fd912ad",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/4b/9f/2f280864e003e275a4f7f324c4ee",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/0b/a7/9c93f87f6e2430e1af483b2269ef",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f9/81/e339e45d6c650df8217acbfb5f29",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/de/d8/ce0ae67d8fa1bc4afb931c2e8a0f",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7b/0a/4907ea64f8492164a84ab9503aaf",
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 08e9b907..9571f4eb 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -2214,6 +2214,7 @@
sockaddr
socketmodule
socketserver
+ softdefault
somevar
sortname
soundtrackname
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1efefb07..90ddb742 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.6.12 (20561, 2022-04-21)
+### 1.6.12 (20563, 2022-04-21)
- More internal work on V2 master-server communication
### 1.6.11 (20539, 2022-03-23)
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index b2418ae6..f580f813 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -1102,6 +1102,7 @@
snorm
sockaddr
soffs
+ softdefault
solaris
sortname
sourcenode
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index ea5169d1..4c44390b 100644
--- a/src/ballistica/ballistica.cc
+++ b/src/ballistica/ballistica.cc
@@ -21,7 +21,7 @@
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kAppBuildNumber = 20561;
+const int kAppBuildNumber = 20563;
const char* kAppVersion = "1.6.12";
// Our standalone globals.
diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py
index 83384620..c169ccfc 100644
--- a/tests/test_efro/test_dataclassio.py
+++ b/tests/test_efro/test_dataclassio.py
@@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details.
#
"""Testing dataclasses functionality."""
+# pylint: disable=too-many-lines
from __future__ import annotations
@@ -877,6 +878,7 @@ def test_extended_data() -> None:
def test_soft_default() -> None:
"""Test soft_default IOAttr value."""
+ # pylint: disable=too-many-locals
# Try both of these with and without storage_name to make sure
# soft_default interacts correctly with both cases.
@@ -946,7 +948,16 @@ def test_soft_default() -> None:
assert dataclass_to_dict(_TestClassC3(0)) == {}
- # we should disallow passing a few mutable types as soft_defaults
+ @ioprepped
+ @dataclass
+ class _TestClassC3b:
+ ival: Annotated[
+ int,
+ IOAttrs(store_default=False, soft_default_factory=lambda: 0)]
+
+ assert dataclass_to_dict(_TestClassC3b(0)) == {}
+
+ # We disallow passing a few mutable types as soft_defaults
# just as dataclass does with regular defaults.
with pytest.raises(TypeError):
@@ -954,3 +965,73 @@ def test_soft_default() -> None:
@dataclass
class _TestClassD:
lval: Annotated[list, IOAttrs(soft_default=[])]
+
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class _TestClassD2:
+ lval: Annotated[set, IOAttrs(soft_default=set())]
+
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class _TestClassD3:
+ lval: Annotated[dict, IOAttrs(soft_default={})]
+
+ # soft_defaults are not static-type-checked, but we do try to
+ # catch basic type mismatches at prep time. Make sure that's working.
+ # (we also do full value validation during input, but the more we catch
+ # early the better)
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class _TestClassE:
+ lval: Annotated[int, IOAttrs(soft_default='')]
+
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class _TestClassE2:
+ lval: Annotated[str, IOAttrs(soft_default=45)]
+
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class _TestClassE3:
+ lval: Annotated[list, IOAttrs(soft_default_factory=set)]
+
+ # Make sure Unions/Optionals go through ok.
+ # (note that mismatches currently aren't caught at prep time; just
+ # checking the negative case here).
+ @ioprepped
+ @dataclass
+ class _TestClassE4:
+ lval: Annotated[Optional[str], IOAttrs(soft_default=None)]
+
+ @ioprepped
+ @dataclass
+ class _TestClassE5:
+ lval: Annotated[Optional[str], IOAttrs(soft_default='foo')]
+
+ # Now try more in-depth examples: nested type mismatches like this
+ # are currently not caught at prep-time but ARE caught during inputting.
+ @ioprepped
+ @dataclass
+ class _TestClassE6:
+ lval: Annotated[tuple[int, int], IOAttrs(soft_default=('foo', 'bar'))]
+
+ with pytest.raises(TypeError):
+ dataclass_from_dict(_TestClassE6, {})
+
+ @ioprepped
+ @dataclass
+ class _TestClassE7:
+ lval: Annotated[Optional[bool], IOAttrs(soft_default=12)]
+
+ with pytest.raises(TypeError):
+ dataclass_from_dict(_TestClassE7, {})
diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py
index d43b0fcf..02934ba1 100644
--- a/tools/efro/dataclassio/_base.py
+++ b/tools/efro/dataclassio/_base.py
@@ -163,6 +163,9 @@ class IOAttrs:
self.soft_default = soft_default
if soft_default_factory is not cls.soft_default_factory:
self.soft_default_factory = soft_default_factory
+ if self.soft_default is not cls.soft_default:
+ raise ValueError('Cannot set both soft_default'
+ ' and soft_default_factory')
def validate_for_field(self, cls: type, field: dataclasses.Field) -> None:
"""Ensure the IOAttrs instance is ok to use with the provided field."""
diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py
index 92e2c502..6948cbf8 100644
--- a/tools/efro/dataclassio/_inputter.py
+++ b/tools/efro/dataclassio/_inputter.py
@@ -23,7 +23,9 @@ from efro.dataclassio._prep import PrepSession
if TYPE_CHECKING:
from typing import Any, Optional
+
from efro.dataclassio._base import IOAttrs
+ from efro.dataclassio._outputter import _Outputter
T = TypeVar('T')
@@ -41,6 +43,7 @@ class _Inputter(Generic[T]):
self._coerce_to_float = coerce_to_float
self._allow_unknown_attrs = allow_unknown_attrs
self._discard_unknown_attrs = discard_unknown_attrs
+ self._soft_default_validator: Optional[_Outputter] = None
if not allow_unknown_attrs and discard_unknown_attrs:
raise ValueError('discard_unknown_attrs cannot be True'
@@ -216,18 +219,28 @@ class _Inputter(Generic[T]):
# Go through all fields looking for any not yet present in our data.
# If we find any such fields with a soft-default value or factory
- # defined, inject that value into our args.
+ # defined, inject that soft value into our args.
for key, aparsed in parsed_field_annotations.items():
- if key not in args:
- ioattrs = aparsed[1]
- if (ioattrs is not None and
- (ioattrs.soft_default is not ioattrs.MISSING
- or ioattrs.soft_default_factory is not ioattrs.MISSING)):
- if ioattrs.soft_default is not ioattrs.MISSING:
- args[key] = ioattrs.soft_default
- else:
- assert callable(ioattrs.soft_default_factory)
- args[key] = ioattrs.soft_default_factory()
+ if key in args:
+ continue
+ ioattrs = aparsed[1]
+ if (ioattrs is not None and
+ (ioattrs.soft_default is not ioattrs.MISSING
+ or ioattrs.soft_default_factory is not ioattrs.MISSING)):
+ if ioattrs.soft_default is not ioattrs.MISSING:
+ soft_default = ioattrs.soft_default
+ else:
+ assert callable(ioattrs.soft_default_factory)
+ soft_default = ioattrs.soft_default_factory()
+ args[key] = soft_default
+
+ # Make sure these values are valid since we didn't run
+ # them through our normal input type checking.
+
+ self._type_check_soft_default(
+ value=soft_default,
+ anntype=aparsed[0],
+ fieldpath=(f'{fieldpath}.{key}' if fieldpath else key))
try:
out = cls(**args)
@@ -238,6 +251,23 @@ class _Inputter(Generic[T]):
setattr(out, EXTRA_ATTRS_ATTR, extra_attrs)
return out
+ def _type_check_soft_default(self, value: Any, anntype: Any,
+ fieldpath: str) -> None:
+ from efro.dataclassio._outputter import _Outputter
+
+ # Counter-intuitively, we create an outputter as part of
+ # our inputter. Soft-default values are already internal types;
+ # we need to make sure they can go out from there.
+ if self._soft_default_validator is None:
+ self._soft_default_validator = _Outputter(
+ obj=None,
+ create=False,
+ codec=self._codec,
+ coerce_to_float=self._coerce_to_float)
+ self._soft_default_validator.soft_default_check(value=value,
+ anntype=anntype,
+ fieldpath=fieldpath)
+
def _dict_from_input(self, cls: type, fieldpath: str, anntype: Any,
value: Any, ioattrs: Optional[IOAttrs]) -> Any:
# pylint: disable=too-many-branches
diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py
index 03bb74be..11dc87cf 100644
--- a/tools/efro/dataclassio/_outputter.py
+++ b/tools/efro/dataclassio/_outputter.py
@@ -39,12 +39,23 @@ class _Outputter:
def run(self) -> Any:
"""Do the thing."""
+ assert dataclasses.is_dataclass(self._obj)
+
# For special extended data types, call their 'will_output' callback.
if isinstance(self._obj, IOExtendedData):
self._obj.will_output()
return self._process_dataclass(type(self._obj), self._obj, '')
+ def soft_default_check(self, value: Any, anntype: Any,
+ fieldpath: str) -> None:
+ """(internal)"""
+ self._process_value(type(value),
+ fieldpath=fieldpath,
+ anntype=anntype,
+ value=value,
+ ioattrs=None)
+
def _process_dataclass(self, cls: type, obj: Any, fieldpath: str) -> Any:
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
diff --git a/tools/efro/dataclassio/_prep.py b/tools/efro/dataclassio/_prep.py
index 57785b13..dc576909 100644
--- a/tools/efro/dataclassio/_prep.py
+++ b/tools/efro/dataclassio/_prep.py
@@ -16,10 +16,12 @@ import datetime
from typing import TYPE_CHECKING, TypeVar, get_type_hints
# noinspection PyProtectedMember
-from efro.dataclassio._base import _parse_annotated, _get_origin, SIMPLE_TYPES
+from efro.dataclassio._base import (_parse_annotated, _get_origin,
+ SIMPLE_TYPES)
if TYPE_CHECKING:
from typing import Any, Optional
+ from efro.dataclassio._base import IOAttrs
T = TypeVar('T')
@@ -214,6 +216,7 @@ class PrepSession:
self.prep_type(cls,
attrname,
anntype,
+ ioattrs=ioattrs,
recursion_level=recursion_level + 1)
# Success! Store our resolved stuff with the class and we're done.
@@ -228,13 +231,12 @@ class PrepSession:
return prepdata
def prep_type(self, cls: type, attrname: str, anntype: Any,
- recursion_level: int) -> None:
+ ioattrs: Optional[IOAttrs], recursion_level: int) -> None:
"""Run prep on a dataclass."""
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
- # If we run into classes containing themselves, we may have
- # to do something smarter to handle it.
if recursion_level > MAX_RECURSION:
raise RuntimeError('Max recursion exceeded.')
@@ -257,6 +259,32 @@ class PrepSession:
f'Unsupported type found for \'{attrname}\' on {cls}:'
f' {anntype}')
+ # If a soft_default value/factory was passed, we do some basic
+ # type checking on the top-level value here. We also run full
+ # recursive validation on values later during inputting, but this
+ # should catch at least some errors early on, which can be
+ # useful since soft_defaults are not static type checked.
+ if ioattrs is not None:
+ have_soft_default = False
+ soft_default: Any = None
+ if ioattrs.soft_default is not ioattrs.MISSING:
+ have_soft_default = True
+ soft_default = ioattrs.soft_default
+ elif ioattrs.soft_default_factory is not ioattrs.MISSING:
+ assert callable(ioattrs.soft_default_factory)
+ have_soft_default = True
+ soft_default = ioattrs.soft_default_factory()
+
+ # Do a simple type check for the top level to catch basic
+ # soft_default mismatches early; full check will happen at
+ # input time.
+ if have_soft_default:
+ if not isinstance(soft_default, origin):
+ raise TypeError(
+ f'{cls} attr {attrname} has type {origin}'
+ f' but soft_default value is type {type(soft_default)}'
+ )
+
if origin in SIMPLE_TYPES:
return
@@ -273,6 +301,7 @@ class PrepSession:
self.prep_type(cls,
attrname,
childtypes[0],
+ ioattrs=None,
recursion_level=recursion_level + 1)
return
@@ -304,6 +333,7 @@ class PrepSession:
self.prep_type(cls,
attrname,
childtypes[1],
+ ioattrs=None,
recursion_level=recursion_level + 1)
return
@@ -325,6 +355,7 @@ class PrepSession:
self.prep_type(cls,
attrname,
childtype,
+ ioattrs=None,
recursion_level=recursion_level + 1)
return
@@ -362,6 +393,7 @@ class PrepSession:
self.prep_type(cls,
attrname,
childtype,
+ None,
recursion_level=recursion_level + 1)
def prep_enum(self, enumtype: type[Enum]) -> None: