dataclassio now supports nested types

This commit is contained in:
Eric Froemling 2021-09-29 11:14:19 -05:00
parent 329e254246
commit 82515efbc5
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
12 changed files with 106 additions and 45 deletions

View File

@ -510,11 +510,13 @@
"ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-38.opt-1.pyc", "ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-38.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-38.opt-1.pyc", "ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-38.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-38.opt-1.pyc", "ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-38.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/extras.cpython-38.opt-1.pyc",
"ba_data/python/efro/dataclassio/_base.py", "ba_data/python/efro/dataclassio/_base.py",
"ba_data/python/efro/dataclassio/_inputter.py", "ba_data/python/efro/dataclassio/_inputter.py",
"ba_data/python/efro/dataclassio/_outputter.py", "ba_data/python/efro/dataclassio/_outputter.py",
"ba_data/python/efro/dataclassio/_pathcapture.py", "ba_data/python/efro/dataclassio/_pathcapture.py",
"ba_data/python/efro/dataclassio/_prep.py", "ba_data/python/efro/dataclassio/_prep.py",
"ba_data/python/efro/dataclassio/extras.py",
"ba_data/python/efro/entity/__init__.py", "ba_data/python/efro/entity/__init__.py",
"ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc", "ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc", "ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc",

View File

@ -649,6 +649,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/_outputter.py \ build/ba_data/python/efro/dataclassio/_outputter.py \
build/ba_data/python/efro/dataclassio/_pathcapture.py \ build/ba_data/python/efro/dataclassio/_pathcapture.py \
build/ba_data/python/efro/dataclassio/_prep.py \ build/ba_data/python/efro/dataclassio/_prep.py \
build/ba_data/python/efro/dataclassio/extras.py \
build/ba_data/python/efro/entity/__init__.py \ build/ba_data/python/efro/entity/__init__.py \
build/ba_data/python/efro/entity/_base.py \ build/ba_data/python/efro/entity/_base.py \
build/ba_data/python/efro/entity/_entity.py \ build/ba_data/python/efro/entity/_entity.py \
@ -676,6 +677,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-38.opt-1.pyc \ build/ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-38.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-38.opt-1.pyc \ build/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-38.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-38.opt-1.pyc \ build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-38.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-38.opt-1.pyc \
build/ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc \ build/ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc \
build/ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc \ build/ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc \
build/ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc \ build/ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc \

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-09-28 for Ballistica version 1.6.5 build 20393</em></h4> <h4><em>last updated on 2021-09-29 for Ballistica version 1.6.5 build 20393</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p> which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr> <hr>

View File

@ -750,3 +750,22 @@ def test_datetime_limits() -> None:
out['tval'][-1] += 1 out['tval'][-1] += 1
with pytest.raises(ValueError): with pytest.raises(ValueError):
dataclass_from_dict(_TestClass2, out) dataclass_from_dict(_TestClass2, out)
def test_nested() -> None:
"""Test nesting dataclasses."""
@ioprepped
@dataclass
class _TestClass:
class _TestEnum(Enum):
VAL1 = 'val1'
VAL2 = 'val2'
@dataclass
class _TestSubClass:
ival: int = 0
subval: _TestSubClass = field(default_factory=_TestSubClass)
enval: _TestEnum = _TestEnum.VAL1

View File

@ -263,7 +263,6 @@ def test_field_copies() -> None:
# Type-checker currently allows this because both are Compounds # Type-checker currently allows this because both are Compounds
# but should fail at runtime since their subfield arrangement differs. # but should fail at runtime since their subfield arrangement differs.
# reveal_type(ent1.grp.blah)
with pytest.raises(ValueError): with pytest.raises(ValueError):
ent2.grp = ent1.grp2 ent2.grp = ent1.grp2

View File

@ -15,14 +15,14 @@ from typing import TYPE_CHECKING, TypeVar
from efro.dataclassio._outputter import _Outputter from efro.dataclassio._outputter import _Outputter
from efro.dataclassio._inputter import _Inputter from efro.dataclassio._inputter import _Inputter
from efro.dataclassio._base import Codec, IOAttrs from efro.dataclassio._base import Codec, IOAttrs
from efro.dataclassio._prep import ioprepped, is_ioprepped_dataclass from efro.dataclassio._prep import ioprep, ioprepped, is_ioprepped_dataclass
from efro.dataclassio._pathcapture import FieldStoragePathCapture from efro.dataclassio._pathcapture import FieldStoragePathCapture
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Dict, Type, Tuple, Optional, List, Set from typing import Any, Dict, Type, Tuple, Optional, List, Set
__all__ = [ __all__ = [
'Codec', 'IOAttrs', 'ioprepped', 'is_ioprepped_dataclass', 'Codec', 'IOAttrs', 'ioprep', 'ioprepped', 'is_ioprepped_dataclass',
'FieldStoragePathCapture', 'dataclass_to_dict', 'dataclass_to_json', 'FieldStoragePathCapture', 'dataclass_to_dict', 'dataclass_to_json',
'dataclass_from_dict', 'dataclass_from_json', 'dataclass_validate' 'dataclass_from_dict', 'dataclass_from_json', 'dataclass_validate'
] ]

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality for importing, exporting, and validating dataclasses. """Core components of dataclassio."""
This allows complex nested dataclasses to be flattened to json-compatible
data and restored from said data. It also gracefully handles and preserves
unrecognized attribute data, allowing older clients to interact with newer
data formats in a nondestructive manner.
"""
from __future__ import annotations from __future__ import annotations

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality for importing, exporting, and validating dataclasses. """Functionality for dataclassio related to pulling data into dataclasses."""
This allows complex nested dataclasses to be flattened to json-compatible
data and restored from said data. It also gracefully handles and preserves
unrecognized attribute data, allowing older clients to interact with newer
data formats in a nondestructive manner.
"""
# Note: We do lots of comparing of exact types here which is normally # Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged). # frowned upon (stuff like isinstance() is usually encouraged).

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality for importing, exporting, and validating dataclasses. """Functionality for dataclassio related to exporting data from dataclasses."""
This allows complex nested dataclasses to be flattened to json-compatible
data and restored from said data. It also gracefully handles and preserves
unrecognized attribute data, allowing older clients to interact with newer
data formats in a nondestructive manner.
"""
# Note: We do lots of comparing of exact types here which is normally # Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged). # frowned upon (stuff like isinstance() is usually encouraged).

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality for importing, exporting, and validating dataclasses. """Functionality related to capturing nested dataclass paths."""
This allows complex nested dataclasses to be flattened to json-compatible
data and restored from said data. It also gracefully handles and preserves
unrecognized attribute data, allowing older clients to interact with newer
data formats in a nondestructive manner.
"""
from __future__ import annotations from __future__ import annotations

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality for importing, exporting, and validating dataclasses. """Functionality for prepping types for use with dataclassio."""
This allows complex nested dataclasses to be flattened to json-compatible
data and restored from said data. It also gracefully handles and preserves
unrecognized attribute data, allowing older clients to interact with newer
data formats in a nondestructive manner.
"""
# Note: We do lots of comparing of exact types here which is normally # Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged). # frowned upon (stuff like isinstance() is usually encouraged).
@ -127,17 +121,20 @@ class PrepSession:
' @efro.dataclassio.ioprepped decorator).', cls) ' @efro.dataclassio.ioprepped decorator).', cls)
try: try:
# NOTE: perhaps we want to expose the globalns/localns args # NOTE: Now passing the class' __dict__ (vars()) as locals
# to this? # which allows us to pick up nested classes, etc.
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
resolved_annotations = get_type_hints(cls, include_extras=True) resolved_annotations = get_type_hints(cls,
localns=vars(cls),
include_extras=True)
# pylint: enable=unexpected-keyword-arg # pylint: enable=unexpected-keyword-arg
except Exception as exc: except Exception as exc:
raise RuntimeError( print('GOT', cls.__dict__)
raise TypeError(
f'dataclassio prep for {cls} failed with error: {exc}.' f'dataclassio prep for {cls} failed with error: {exc}.'
f' Make sure all types used in annotations are defined' f' Make sure all types used in annotations are defined'
f' at the module level or add them as part of an explicit' f' at the module or class level or add them as part of an'
f' prep call.') from exc f' explicit prep call.') from exc
# noinspection PyDataclass # noinspection PyDataclass
fields = dataclasses.fields(cls) fields = dataclasses.fields(cls)

View File

@ -0,0 +1,66 @@
# Released under the MIT License. See LICENSE for details.
#
"""Extra rarely-needed functionality related to dataclasses."""
from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Dict, Type, Tuple, Optional, List, Set
def dataclass_diff(obj1: Any, obj2: Any) -> str:
"""Generate a string showing differences between two dataclass instances.
Both must be of the exact same type.
"""
diff = _diff(obj1, obj2, 2)
return ' <no differences>' if diff == '' else diff
class DataclassDiff:
"""Wraps dataclass_diff() in an object for efficiency.
It is preferable to pass this to logging calls instead of the
final diff string since the diff will never be generated if
the associated logging level is not being emitted.
"""
def __init__(self, obj1: Any, obj2: Any):
self._obj1 = obj1
self._obj2 = obj2
def __repr__(self) -> str:
return dataclass_diff(self._obj1, self._obj2)
def _diff(obj1: Any, obj2: Any, indent: int) -> str:
assert dataclasses.is_dataclass(obj1)
assert dataclasses.is_dataclass(obj2)
if type(obj1) is not type(obj2):
raise TypeError(f'Passed objects are not of the same'
f' type ({type(obj1)} and {type(obj2)}).')
bits: List[str] = []
indentstr = ' ' * indent
fields = dataclasses.fields(obj1)
for field in fields:
fieldname = field.name
val1 = getattr(obj1, fieldname)
val2 = getattr(obj2, fieldname)
# For nested dataclasses, dive in and do nice piecewise compares.
if (dataclasses.is_dataclass(val1) and dataclasses.is_dataclass(val2)
and type(val1) is type(val2)):
diff = _diff(val1, val2, indent + 2)
if diff != '':
bits.append(f'{indentstr}{fieldname}:')
bits.append(diff)
# For all else just do a single line
# (perhaps we could improve on this for other complex types)
else:
if val1 != val2:
bits.append(f'{indentstr}{fieldname}: {val1} -> {val2}')
return '\n'.join(bits)