Added FieldStoragePathCapture to dataclassio

This commit is contained in:
Eric Froemling 2021-05-26 13:29:20 -05:00
parent f2c0067ee1
commit dc7d7fab1d
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
4 changed files with 79 additions and 2 deletions

View File

@ -1426,6 +1426,7 @@
<w>mythingie</w>
<w>myweakcall</w>
<w>mywidget</w>
<w>namecap</w>
<w>namedarg</w>
<w>nametext</w>
<w>nameval</w>
@ -2108,6 +2109,7 @@
<w>strt</w>
<w>strval</w>
<w>subargs</w>
<w>subc</w>
<w>subclassof</w>
<w>subcontainer</w>
<w>subcontainerheight</w>

View File

@ -626,6 +626,7 @@
<w>mynode</w>
<w>mystatspage</w>
<w>mywidget</w>
<w>namecap</w>
<w>nameval</w>
<w>ndebug</w>
<w>nearbytab</w>
@ -950,6 +951,7 @@
<w>strs</w>
<w>strtof</w>
<w>subargs</w>
<w>subc</w>
<w>subclsssing</w>
<w>subentities</w>
<w>subfieldpath</w>

View File

@ -9,13 +9,14 @@ import datetime
from dataclasses import field, dataclass
from typing import (TYPE_CHECKING, Optional, List, Set, Any, Dict, Sequence,
Union, Tuple)
from typing_extensions import Annotated
from typing_extensions import Annotated
import pytest
from efro.util import utc_now
from efro.dataclassio import (dataclass_validate, dataclass_from_dict,
dataclass_to_dict, ioprepped, IOAttrs, Codec)
dataclass_to_dict, ioprepped, IOAttrs, Codec,
FieldStoragePathCapture)
if TYPE_CHECKING:
pass
@ -651,3 +652,34 @@ def test_name_clashes() -> None:
class _TestClass3:
ival: Annotated[int, IOAttrs(store_default=False)] = 4
ival2: Annotated[int, IOAttrs('ival')] = 5
@ioprepped
@dataclass
class _SPTestClass1:
barf: int = 5
eep: str = 'blah'
barf2: Annotated[int, IOAttrs('b')] = 5
@ioprepped
@dataclass
class _SPTestClass2:
rah: bool = False
subc: _SPTestClass1 = field(default_factory=_SPTestClass1)
subc2: Annotated[_SPTestClass1,
IOAttrs('s')] = field(default_factory=_SPTestClass1)
def test_field_storage_path_capture() -> None:
"""Test FieldStoragePathCapture functionality."""
obj = _SPTestClass2()
namecap = FieldStoragePathCapture(obj)
assert namecap.subc.barf == 'subc.barf'
assert namecap.subc2.barf == 's.barf'
assert namecap.subc2.barf2 == 's.b'
with pytest.raises(AttributeError):
assert namecap.nonexistent.barf == 's.barf'

View File

@ -90,6 +90,47 @@ class IOAttrs:
f' store_default=False cannot be set for it.')
class FieldStoragePathCapture:
"""Utility for obtaining dataclass storage paths in a type safe way.
Given dataclass instance foo, FieldStoragePathCapture(foo).bar.eep
will return 'bar.eep' (or something like 'b.e' if storagenames are
overrridden). This can be combined with type-checking tricks that
return foo in the type-checker's eyes while returning
FieldStoragePathCapture(foo) at runtime in order to grant a measure
of type safety to specifying field paths for things such as db
queries. Be aware, however, that the type-checker will incorrectly
think these lookups are returning actual attr values when they
are actually returning strings.
"""
def __init__(self, obj: Any, path: List[str] = None):
if path is None:
path = []
if not dataclasses.is_dataclass(obj):
raise TypeError(f'Expected a dataclass type/instance;'
f' got {type(obj)}.')
self._cls = obj if isinstance(obj, type) else type(obj)
self._path = path
def __getattr__(self, name: str) -> Any:
prep = PrepSession(explicit=False).prep_dataclass(self._cls,
recursion_level=0)
try:
anntype = prep.annotations[name]
except KeyError as exc:
raise AttributeError(f'{type(self)} has no {name} field.') from exc
anntype, ioattrs = _parse_annotated(anntype)
storagename = (name if (ioattrs is None or ioattrs.storagename is None)
else ioattrs.storagename)
origin = _get_origin(anntype)
path = self._path + [storagename]
if dataclasses.is_dataclass(origin):
return FieldStoragePathCapture(origin, path=path)
return '.'.join(path)
def dataclass_to_dict(obj: Any,
codec: Codec = Codec.JSON,
coerce_to_float: bool = True) -> dict: