better soft_default type checking

This commit is contained in:
Eric Froemling 2022-04-21 20:17:19 -07:00
parent 9529c7cf32
commit 87460145db
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
10 changed files with 197 additions and 38 deletions

View File

@ -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",

View File

@ -2214,6 +2214,7 @@
<w>sockaddr</w>
<w>socketmodule</w>
<w>socketserver</w>
<w>softdefault</w>
<w>somevar</w>
<w>sortname</w>
<w>soundtrackname</w>

View File

@ -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)

View File

@ -1102,6 +1102,7 @@
<w>snorm</w>
<w>sockaddr</w>
<w>soffs</w>
<w>softdefault</w>
<w>solaris</w>
<w>sortname</w>
<w>sourcenode</w>

View File

@ -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.

View File

@ -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, {})

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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: