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__/_pathcapture.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/_inputter.py",
"ba_data/python/efro/dataclassio/_outputter.py",
"ba_data/python/efro/dataclassio/_pathcapture.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/__pycache__/__init__.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/_pathcapture.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/_base.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__/_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__/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__/_base.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 -->
<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,
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>

View File

@ -750,3 +750,22 @@ def test_datetime_limits() -> None:
out['tval'][-1] += 1
with pytest.raises(ValueError):
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
# but should fail at runtime since their subfield arrangement differs.
# reveal_type(ent1.grp.blah)
with pytest.raises(ValueError):
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._inputter import _Inputter
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
if TYPE_CHECKING:
from typing import Any, Dict, Type, Tuple, Optional, List, Set
__all__ = [
'Codec', 'IOAttrs', 'ioprepped', 'is_ioprepped_dataclass',
'Codec', 'IOAttrs', 'ioprep', 'ioprepped', 'is_ioprepped_dataclass',
'FieldStoragePathCapture', 'dataclass_to_dict', 'dataclass_to_json',
'dataclass_from_dict', 'dataclass_from_json', 'dataclass_validate'
]

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for importing, exporting, and validating 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.
"""
"""Core components of dataclassio."""
from __future__ import annotations

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for importing, exporting, and validating 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.
"""
"""Functionality for dataclassio related to pulling data into dataclasses."""
# Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged).

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for importing, exporting, and validating 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.
"""
"""Functionality for dataclassio related to exporting data from dataclasses."""
# Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged).

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for importing, exporting, and validating 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.
"""
"""Functionality related to capturing nested dataclass paths."""
from __future__ import annotations

View File

@ -1,12 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for importing, exporting, and validating 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.
"""
"""Functionality for prepping types for use with dataclassio."""
# Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged).
@ -127,17 +121,20 @@ class PrepSession:
' @efro.dataclassio.ioprepped decorator).', cls)
try:
# NOTE: perhaps we want to expose the globalns/localns args
# to this?
# NOTE: Now passing the class' __dict__ (vars()) as locals
# which allows us to pick up nested classes, etc.
# 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
except Exception as exc:
raise RuntimeError(
print('GOT', cls.__dict__)
raise TypeError(
f'dataclassio prep for {cls} failed with error: {exc}.'
f' Make sure all types used in annotations are defined'
f' at the module level or add them as part of an explicit'
f' prep call.') from exc
f' at the module or class level or add them as part of an'
f' explicit prep call.') from exc
# noinspection PyDataclass
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)