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: