mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-30 11:13:17 +08:00
Added bytes, datetime, and firestore format support to dataclassio
This commit is contained in:
parent
157be291f8
commit
7f1458304d
@ -3932,26 +3932,26 @@
|
||||
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450",
|
||||
"assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e",
|
||||
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
|
||||
"build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2b/e1/628191b4c0b260036e0d7abc6ee3",
|
||||
"build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/83/51/0511414b7d61f636b55bb6e63b00",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/a1/3046fc4c86fb31655da6b2d076cb",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ba/e7/d0c78aaf3c6982eee87471ff5637",
|
||||
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a3/43/5e2dd209bedf7f3c5550f416fb6e",
|
||||
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7b/e0/5c94fd10dfb247529548a76ddd75",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/13/54/1e3e4532fa176cca029fc448ac6e",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d9/5b/d8a9b7d2cda3eaff208944273b08",
|
||||
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3b/37/1055f167fbab28003f7129e5aee6",
|
||||
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/25/aa/6f6c4ef8ab7d39f46b8bf69fa18b",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/c4/619b03c16992b06d8162cdb96126",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2f/1b/6ec4f564ea77d42a30917c8434c9",
|
||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b5/73/194f8c35a467b775eebe24de90a2",
|
||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b0/a7/7848042fbcd5764af31020e53ae1",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/28/a0/d2d53829a074e5c06cf5785af1cd",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4d/53/3027bbbab3c19fa2b8d0fa718d96",
|
||||
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f9/c7/b04b34fcdc0e0e0f9976b5639c0c",
|
||||
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/87/a9/e295e82a1f3b423fb2f8f2eb462f",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1c/bd/4afc4c92dcaf095928f66d5c61d3",
|
||||
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/ca/56/0923f65bb7c80211f614c4816ab5",
|
||||
"build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/93/85/ceb5bcda0a0a8b9cde91e599f505",
|
||||
"build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/76/38/49d10532844bfe7df52b53216d2b",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/28/8a/26ac9d06cc08010e5ef47fe1e14b",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b5/71/06235b13926864357c4b6cff841c",
|
||||
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2e/c5/61ca27f1d369d2e11025c9b41c13",
|
||||
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d9/58/78661f5aecd0da126b10201d0c5e",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/36/be/d706c897e778fa1b0ba439eff8c1",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/eb/15/84f0a16a78fee48970391300b957",
|
||||
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/60/eb/4f65457cf792f721bbad617d9ad0",
|
||||
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/71/f1/4f36fb59372af667d2fcfc16c6d8",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7f/e6/13d77d7939fa00e57f16ed440f92",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/44/2d/1132879368551cc7bd7fb407f0fd",
|
||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/77/6e/d9920eeaa9b9adba4cb3f5e655f0",
|
||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b9/02/ed3c91a260a476a66baab752e75c",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/56/ca/d159a3a9dcb1f266413578651e16",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c9/5e/9cc46cf58951c133917a04f04cd0",
|
||||
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/90/84/390b6916bbf2b6710729a923ab7c",
|
||||
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3e/f8/36ae7dc2ac79f3838999f66e1c87",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/fe/a1/b217231f196dc175663d7b70b4a9",
|
||||
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/95/7a/a961123e1b9bcd78a563759fd2cf",
|
||||
"build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/54/e0/8c89d241ca37ae32c9b46ea71456",
|
||||
"build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e0/05/94ac67cbf2e665eeecfc77246caf",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/e1/be897ef0e73b0e52c59f3396e0cd",
|
||||
@ -3960,12 +3960,12 @@
|
||||
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1f/b3/80375870a9ab83bbb63b50b03a73",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5c/2e/a1b1f2126b3150c6f11e799c3805",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b6/6b/37174b3a0d72a05f1bb3d904b78b",
|
||||
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/43/b7/8f87aa950891250923c7cb617d32",
|
||||
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/e9/a0c9c846c00b5ed9645ddc70e44a",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8a/e5/1f93415fb55842479f5ce0d375af",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e7/36/2475ad00f41cf75d63c786f3db39",
|
||||
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6c/52/ff241eeae0d43f433f66a8b2dcbb",
|
||||
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/40/a3/a2c2c741e551c40d0700888737a7",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a1/ef/fa977c1c8a05781f6af8321b31f2",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/15/336d51f7894aeedba54f5aabb04a"
|
||||
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c4/c3/de4c713a14762b156cbc9f1dbacc",
|
||||
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/a5/7341744d46bf83683f6d9113019a",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3b/c7/76236792620e2f0a7a5551b32a45",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/86/6d/2f7220ea5ab89b23ffacc7497694",
|
||||
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9f/46/d0cd8a310edfa84f124d11a295d7",
|
||||
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d7/7d/5196bbacb6efbfd4fe70cc042553",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/73/63/38d1af71edf49e29f802533058a0",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d9/08/502699c51e7a36c1b60acec6be36"
|
||||
}
|
||||
1
.idea/dictionaries/ericf.xml
generated
1
.idea/dictionaries/ericf.xml
generated
@ -1088,6 +1088,7 @@
|
||||
<w>intp</w>
|
||||
<w>introspectable</w>
|
||||
<w>intstr</w>
|
||||
<w>ioattrs</w>
|
||||
<w>iobj</w>
|
||||
<w>iometa</w>
|
||||
<w>ioprep</w>
|
||||
|
||||
@ -488,6 +488,7 @@
|
||||
<w>interuptions</w>
|
||||
<w>intstr</w>
|
||||
<w>invote</w>
|
||||
<w>ioattrs</w>
|
||||
<w>iobj</w>
|
||||
<w>iometa</w>
|
||||
<w>ioprep</w>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2021-05-24 for Ballistica version 1.6.3 build 20367</em></h4>
|
||||
<h4><em>last updated on 2021-05-25 for Ballistica version 1.6.4 build 20369</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>
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't change here.
|
||||
const int kAppBuildNumber = 20368;
|
||||
const char* kAppVersion = "1.6.3";
|
||||
const int kAppBuildNumber = 20369;
|
||||
const char* kAppVersion = "1.6.4";
|
||||
|
||||
// Our standalone globals.
|
||||
// These are separated out for easy access.
|
||||
|
||||
@ -15,7 +15,7 @@ import pytest
|
||||
|
||||
from efro.util import utc_now
|
||||
from efro.dataclassio import (dataclass_validate, dataclass_from_dict,
|
||||
dataclass_to_dict, ioprepped, IOMeta)
|
||||
dataclass_to_dict, ioprepped, IOAttrs, Codec)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@ -461,13 +461,13 @@ def test_extra_data() -> None:
|
||||
assert 'nonexistent' not in out
|
||||
|
||||
|
||||
def test_meta() -> None:
|
||||
"""Testing iometa annotations."""
|
||||
def test_ioattrs() -> None:
|
||||
"""Testing ioattrs annotations."""
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestClass:
|
||||
dval: Annotated[Dict, IOMeta('d')]
|
||||
dval: Annotated[Dict, IOAttrs('d')]
|
||||
|
||||
obj = _TestClass(dval={'foo': 'bar'})
|
||||
|
||||
@ -481,14 +481,14 @@ def test_meta() -> None:
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestClass2:
|
||||
dval: Annotated[Dict, IOMeta('d', store_default=False)]
|
||||
dval: Annotated[Dict, IOAttrs('d', store_default=False)]
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestClass3:
|
||||
dval: Annotated[Dict, IOMeta('d', store_default=False)] = field(
|
||||
dval: Annotated[Dict, IOAttrs('d', store_default=False)] = field(
|
||||
default_factory=dict)
|
||||
ival: Annotated[int, IOMeta('i', store_default=False)] = 123
|
||||
ival: Annotated[int, IOAttrs('i', store_default=False)] = 123
|
||||
|
||||
# Both attrs are default; should get stripped out.
|
||||
obj3 = _TestClass3()
|
||||
@ -513,6 +513,51 @@ def test_meta() -> None:
|
||||
assert obj3.ival == 125
|
||||
|
||||
|
||||
def test_codecs() -> None:
|
||||
"""Test differences with codecs."""
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestClass:
|
||||
bval: bytes
|
||||
|
||||
# bytes to/from JSON (goes through base64)
|
||||
obj = _TestClass(bval=b'foo')
|
||||
out = dataclass_to_dict(obj, codec=Codec.JSON)
|
||||
assert isinstance(out['bval'], str) and out['bval'] == 'Zm9v'
|
||||
obj = dataclass_from_dict(_TestClass, out, codec=Codec.JSON)
|
||||
assert obj.bval == b'foo'
|
||||
|
||||
# bytes to/from FIRESTORE (passed as-is)
|
||||
obj = _TestClass(bval=b'foo')
|
||||
out = dataclass_to_dict(obj, codec=Codec.FIRESTORE)
|
||||
assert isinstance(out['bval'], bytes) and out['bval'] == b'foo'
|
||||
obj = dataclass_from_dict(_TestClass, out, codec=Codec.FIRESTORE)
|
||||
assert obj.bval == b'foo'
|
||||
|
||||
now = utc_now()
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestClass2:
|
||||
dval: datetime.datetime
|
||||
|
||||
# datetime to/from JSON (turns into a list of values)
|
||||
obj2 = _TestClass2(dval=now)
|
||||
out = dataclass_to_dict(obj2, codec=Codec.JSON)
|
||||
assert (isinstance(out['dval'], list) and len(out['dval']) == 7
|
||||
and all(isinstance(val, int) for val in out['dval']))
|
||||
obj2 = dataclass_from_dict(_TestClass2, out, codec=Codec.JSON)
|
||||
assert obj2.dval == now
|
||||
|
||||
# datetime to/from FIRESTORE (passed through as-is)
|
||||
obj2 = _TestClass2(dval=now)
|
||||
out = dataclass_to_dict(obj2, codec=Codec.FIRESTORE)
|
||||
assert isinstance(out['dval'], datetime.datetime)
|
||||
obj2 = dataclass_from_dict(_TestClass2, out, codec=Codec.FIRESTORE)
|
||||
assert obj2.dval == now
|
||||
|
||||
|
||||
def test_dict() -> None:
|
||||
"""Test various dict related bits."""
|
||||
|
||||
|
||||
@ -57,15 +57,21 @@ PREP_ATTR = '_DCIOPREP'
|
||||
EXTRA_ATTRS_ATTR = '_DCIOEXATTRS'
|
||||
|
||||
|
||||
class IOMeta:
|
||||
"""Metadata for specifying io behavior."""
|
||||
class Codec(Enum):
|
||||
"""Influences format used for input/output."""
|
||||
JSON = 'json'
|
||||
FIRESTORE = 'firestore'
|
||||
|
||||
|
||||
class IOAttrs:
|
||||
"""For specifying io behavior in annotations."""
|
||||
|
||||
def __init__(self, storagename: str = None, store_default: bool = True):
|
||||
self.storagename = storagename
|
||||
self.store_default = store_default
|
||||
|
||||
def validate_for_field(self, cls: Type, field: dataclasses.Field) -> None:
|
||||
"""Ensure the IOMeta instance is ok to use with the provided field."""
|
||||
"""Ensure the IOAttrs instance is ok to use with the provided field."""
|
||||
|
||||
# Turning off store_default requires the field to have either
|
||||
# a default_factory or a default
|
||||
@ -78,7 +84,9 @@ class IOMeta:
|
||||
f' store_default=False cannot be set for it.')
|
||||
|
||||
|
||||
def dataclass_to_dict(obj: Any, coerce_to_float: bool = True) -> dict:
|
||||
def dataclass_to_dict(obj: Any,
|
||||
codec: Codec = Codec.JSON,
|
||||
coerce_to_float: bool = True) -> dict:
|
||||
"""Given a dataclass object, return a json-friendly dict.
|
||||
|
||||
All values will be checked to ensure they match the types specified
|
||||
@ -95,7 +103,10 @@ def dataclass_to_dict(obj: Any, coerce_to_float: bool = True) -> dict:
|
||||
will be triggered.
|
||||
"""
|
||||
|
||||
out = _Outputter(obj, create=True, coerce_to_float=coerce_to_float).run()
|
||||
out = _Outputter(obj,
|
||||
create=True,
|
||||
codec=codec,
|
||||
coerce_to_float=coerce_to_float).run()
|
||||
assert isinstance(out, dict)
|
||||
return out
|
||||
|
||||
@ -107,21 +118,24 @@ def dataclass_to_json(obj: Any, coerce_to_float: bool = True) -> str:
|
||||
"""
|
||||
import json
|
||||
return json.dumps(
|
||||
dataclass_to_dict(obj=obj, coerce_to_float=coerce_to_float),
|
||||
dataclass_to_dict(obj=obj,
|
||||
coerce_to_float=coerce_to_float,
|
||||
codec=Codec.JSON),
|
||||
separators=(',', ':'),
|
||||
)
|
||||
|
||||
|
||||
def dataclass_from_dict(cls: Type[T],
|
||||
values: dict,
|
||||
codec: Codec = Codec.JSON,
|
||||
coerce_to_float: bool = True,
|
||||
allow_unknown_attrs: bool = True,
|
||||
discard_unknown_attrs: bool = False) -> T:
|
||||
"""Given a dict, return a dataclass of a given type.
|
||||
|
||||
The dict must be in the json-friendly format as emitted from
|
||||
dataclass_to_dict. This means that sequence values such as tuples or
|
||||
sets should be passed as lists, enums should be passed as their
|
||||
The dict must be formatted to match the specified codec (generally
|
||||
json-friendly object types). This means that sequence values such as
|
||||
tuples or sets should be passed as lists, enums should be passed as their
|
||||
associated values, nested dataclasses should be passed as dicts, etc.
|
||||
|
||||
All values are checked to ensure their types/values are valid.
|
||||
@ -141,6 +155,7 @@ def dataclass_from_dict(cls: Type[T],
|
||||
case they will simply be discarded.
|
||||
"""
|
||||
return _Inputter(cls,
|
||||
codec=codec,
|
||||
coerce_to_float=coerce_to_float,
|
||||
allow_unknown_attrs=allow_unknown_attrs,
|
||||
discard_unknown_attrs=discard_unknown_attrs).run(values)
|
||||
@ -163,12 +178,15 @@ def dataclass_from_json(cls: Type[T],
|
||||
discard_unknown_attrs=discard_unknown_attrs)
|
||||
|
||||
|
||||
def dataclass_validate(obj: Any, coerce_to_float: bool = True) -> None:
|
||||
def dataclass_validate(obj: Any,
|
||||
coerce_to_float: bool = True,
|
||||
codec: Codec = Codec.JSON) -> None:
|
||||
"""Ensure that values in a dataclass instance are the correct types."""
|
||||
|
||||
# Simply run an output pass but tell it not to generate data;
|
||||
# only run validation.
|
||||
_Outputter(obj, create=False, coerce_to_float=coerce_to_float).run()
|
||||
_Outputter(obj, create=False, codec=codec,
|
||||
coerce_to_float=coerce_to_float).run()
|
||||
|
||||
|
||||
def ioprep(cls: Type) -> None:
|
||||
@ -186,10 +204,7 @@ def ioprep(cls: Type) -> None:
|
||||
Prepping a dataclass involves evaluating its type annotations, which,
|
||||
as of PEP 563, are stored simply as strings. This evaluation is done
|
||||
in the module namespace containing the class, so all referenced types
|
||||
must be defined at that level. The exception is Typing types (Optional,
|
||||
Union, etc.) which are often defined under an 'if TYPE_CHECKING'
|
||||
conditional and thus not available at runtime, so are explicitly made
|
||||
available during annotation evaluation.
|
||||
must be defined at that level.
|
||||
"""
|
||||
PrepSession(explicit=True).prep_dataclass(cls, recursion_level=0)
|
||||
|
||||
@ -200,7 +215,8 @@ def ioprepped(cls: Type[T]) -> Type[T]:
|
||||
Note that in some cases it may not be possible to prep a dataclass
|
||||
immediately (such as when its type annotations refer to forward-declared
|
||||
types). In these cases, dataclass_prep() should be explicitly called for
|
||||
the class once it is safe to do so.
|
||||
the class as soon as possible; ideally at module import time to expose any
|
||||
errors as early as possible in execution.
|
||||
"""
|
||||
ioprep(cls)
|
||||
return cls
|
||||
@ -278,14 +294,14 @@ class PrepSession:
|
||||
# types and prepping any contained dataclass types.
|
||||
for attrname, anntype in resolved_annotations.items():
|
||||
|
||||
anntype, iometa = _parse_annotated(anntype)
|
||||
anntype, ioattrs = _parse_annotated(anntype)
|
||||
|
||||
# If we found attached IOMeta data, make sure it contains
|
||||
# If we found attached IOAttrs data, make sure it contains
|
||||
# valid values for the field it is attached to.
|
||||
if iometa is not None:
|
||||
iometa.validate_for_field(cls, fields_by_name[attrname])
|
||||
if iometa.storagename is not None:
|
||||
storage_names_to_attr_names[iometa.storagename] = attrname
|
||||
if ioattrs is not None:
|
||||
ioattrs.validate_for_field(cls, fields_by_name[attrname])
|
||||
if ioattrs.storagename is not None:
|
||||
storage_names_to_attr_names[ioattrs.storagename] = attrname
|
||||
|
||||
self.prep_type(cls,
|
||||
attrname,
|
||||
@ -325,15 +341,10 @@ class PrepSession:
|
||||
# Everything below this point assumes the annotation type resolves
|
||||
# to a concrete type.
|
||||
if not isinstance(origin, type):
|
||||
print('ORIGIN IS', origin, type(origin))
|
||||
raise TypeError(
|
||||
f'Unsupported type found for \'{attrname}\' on {cls}:'
|
||||
f' {anntype}')
|
||||
|
||||
# extras = get_args(anntype)
|
||||
# if extras:
|
||||
# print('FOUND EXTRAS FOR', anntype)
|
||||
|
||||
if origin in SIMPLE_TYPES:
|
||||
return
|
||||
|
||||
@ -417,6 +428,9 @@ class PrepSession:
|
||||
self.prep_dataclass(origin, recursion_level=recursion_level + 1)
|
||||
return
|
||||
|
||||
if origin is bytes:
|
||||
return
|
||||
|
||||
raise TypeError(f"Attr '{attrname}' on {cls} contains type '{anntype}'"
|
||||
f' which is not supported by dataclassio.')
|
||||
|
||||
@ -508,9 +522,11 @@ def _get_origin(anntype: Any) -> Any:
|
||||
class _Outputter:
|
||||
"""Validates or exports data contained in a dataclass instance."""
|
||||
|
||||
def __init__(self, obj: Any, create: bool, coerce_to_float: bool) -> None:
|
||||
def __init__(self, obj: Any, create: bool, codec: Codec,
|
||||
coerce_to_float: bool) -> None:
|
||||
self._obj = obj
|
||||
self._create = create
|
||||
self._codec = codec
|
||||
self._coerce_to_float = coerce_to_float
|
||||
|
||||
def run(self) -> Any:
|
||||
@ -533,11 +549,11 @@ class _Outputter:
|
||||
anntype = prep.annotations[fieldname]
|
||||
value = getattr(obj, fieldname)
|
||||
|
||||
anntype, iometa = _parse_annotated(anntype)
|
||||
anntype, ioattrs = _parse_annotated(anntype)
|
||||
|
||||
# If we're not storing default values for this fella,
|
||||
# we can skip all output processing if we've got a default value.
|
||||
if iometa is not None and not iometa.store_default:
|
||||
if ioattrs is not None and not ioattrs.store_default:
|
||||
default_factory: Any = field.default_factory # type: ignore
|
||||
if default_factory is not dataclasses.MISSING:
|
||||
if default_factory() == value:
|
||||
@ -556,8 +572,8 @@ class _Outputter:
|
||||
if self._create:
|
||||
assert out is not None
|
||||
storagename = (fieldname if
|
||||
(iometa is None or iometa.storagename is None)
|
||||
else iometa.storagename)
|
||||
(ioattrs is None or ioattrs.storagename is None)
|
||||
else ioattrs.storagename)
|
||||
out[storagename] = outvalue
|
||||
|
||||
# If there's extra-attrs stored on us, check/include them.
|
||||
@ -712,19 +728,37 @@ class _Outputter:
|
||||
if not isinstance(value, origin):
|
||||
raise TypeError(f'Expected a {origin} for {fieldpath};'
|
||||
f' found a {type(value)}.')
|
||||
# We only support timezone-aware utc times.
|
||||
if (value.tzinfo is not datetime.timezone.utc
|
||||
and (_pytz_utc is None or value.tzinfo is not _pytz_utc)):
|
||||
raise ValueError(
|
||||
'datetime values must have timezone set as timezone.utc')
|
||||
_ensure_datetime_is_timezone_aware(value)
|
||||
if self._codec is Codec.FIRESTORE:
|
||||
return value
|
||||
assert self._codec is Codec.JSON
|
||||
return [
|
||||
value.year, value.month, value.day, value.hour, value.minute,
|
||||
value.second, value.microsecond
|
||||
] if self._create else None
|
||||
|
||||
if origin is bytes:
|
||||
return self._process_bytes(cls, fieldpath, value)
|
||||
|
||||
raise TypeError(
|
||||
f"Field '{fieldpath}' of type '{anntype}' is unsupported here.")
|
||||
|
||||
def _process_bytes(self, cls: Type, fieldpath: str, value: bytes) -> Any:
|
||||
import base64
|
||||
if not isinstance(value, bytes):
|
||||
raise TypeError(f'Expected bytes for {fieldpath} on {cls};'
|
||||
f' found a {type(value)}.')
|
||||
|
||||
if not self._create:
|
||||
return None
|
||||
|
||||
# In JSON we convert to base64, but firestore directly supports bytes.
|
||||
if self._codec is Codec.JSON:
|
||||
return base64.b64encode(value).decode()
|
||||
|
||||
assert self._codec is Codec.FIRESTORE
|
||||
return value
|
||||
|
||||
def _process_dict(self, cls: Type, fieldpath: str, anntype: Any,
|
||||
value: dict) -> Any:
|
||||
# pylint: disable=too-many-branches
|
||||
@ -792,10 +826,12 @@ class _Inputter(Generic[T]):
|
||||
|
||||
def __init__(self,
|
||||
cls: Type[T],
|
||||
codec: Codec,
|
||||
coerce_to_float: bool,
|
||||
allow_unknown_attrs: bool = True,
|
||||
discard_unknown_attrs: bool = False):
|
||||
self._cls = cls
|
||||
self._codec = codec
|
||||
self._coerce_to_float = coerce_to_float
|
||||
self._allow_unknown_attrs = allow_unknown_attrs
|
||||
self._discard_unknown_attrs = discard_unknown_attrs
|
||||
@ -871,9 +907,32 @@ class _Inputter(Generic[T]):
|
||||
if issubclass(origin, datetime.datetime):
|
||||
return self._datetime_from_input(cls, fieldpath, value)
|
||||
|
||||
if origin is bytes:
|
||||
return self._bytes_from_input(origin, fieldpath, value)
|
||||
|
||||
raise TypeError(
|
||||
f"Field '{fieldpath}' of type '{anntype}' is unsupported here.")
|
||||
|
||||
def _bytes_from_input(self, cls: Type, fieldpath: str,
|
||||
value: Any) -> bytes:
|
||||
"""Given input data, returns bytes."""
|
||||
import base64
|
||||
|
||||
# For firestore, bytes are passed as-is. Otherwise they're encoded
|
||||
# as base64.
|
||||
if self._codec is Codec.FIRESTORE:
|
||||
if not isinstance(value, bytes):
|
||||
raise TypeError(f'Expected a bytes object for {fieldpath}'
|
||||
f' on {cls}; got a {type(value)}.')
|
||||
|
||||
return value
|
||||
|
||||
assert self._codec is Codec.JSON
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f'Expected a string object for {fieldpath}'
|
||||
f' on {cls}; got a {type(value)}.')
|
||||
return base64.b64decode(value)
|
||||
|
||||
def _dataclass_from_input(self, cls: Type, fieldpath: str,
|
||||
values: dict) -> Any:
|
||||
"""Given a dict, instantiates a dataclass of the given type.
|
||||
@ -885,7 +944,8 @@ class _Inputter(Generic[T]):
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
if not isinstance(values, dict):
|
||||
raise TypeError("Expected a dict for 'values' arg.")
|
||||
raise TypeError(f'Expected a dict for {fieldpath} on {cls};'
|
||||
f' got a {type(values)}.')
|
||||
|
||||
prep = PrepSession(explicit=False).prep_dataclass(cls,
|
||||
recursion_level=0)
|
||||
@ -920,7 +980,7 @@ class _Inputter(Generic[T]):
|
||||
else:
|
||||
fieldname = field.name
|
||||
anntype = prep.annotations[fieldname]
|
||||
anntype, _iometa = _parse_annotated(anntype)
|
||||
anntype, _ioattrs = _parse_annotated(anntype)
|
||||
|
||||
subfieldpath = (f'{fieldpath}.{fieldname}'
|
||||
if fieldpath else fieldname)
|
||||
@ -1058,6 +1118,19 @@ class _Inputter(Generic[T]):
|
||||
def _datetime_from_input(self, cls: Type, fieldpath: str,
|
||||
value: Any) -> Any:
|
||||
|
||||
# For firestore we expect a datetime object.
|
||||
if self._codec is Codec.FIRESTORE:
|
||||
# Don't compare exact type here, as firestore can give us
|
||||
# a subclass with extended precision.
|
||||
if not isinstance(value, datetime.datetime):
|
||||
raise TypeError(
|
||||
f'Invalid input value for "{fieldpath}" on "{cls}";'
|
||||
f' expected a datetime, got a {type(value).__name__}')
|
||||
_ensure_datetime_is_timezone_aware(value)
|
||||
return value
|
||||
|
||||
assert self._codec is Codec.JSON
|
||||
|
||||
# We expect a list of 7 ints.
|
||||
if type(value) is not list:
|
||||
raise TypeError(
|
||||
@ -1109,20 +1182,28 @@ class _Inputter(Generic[T]):
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def _parse_annotated(anntype: Any) -> Tuple[Any, Optional[IOMeta]]:
|
||||
"""Parse Annotated() constructs, returning annotated type & IOMeta data."""
|
||||
def _ensure_datetime_is_timezone_aware(value: datetime.datetime) -> None:
|
||||
# We only support timezone-aware utc times.
|
||||
if (value.tzinfo is not datetime.timezone.utc
|
||||
and (_pytz_utc is None or value.tzinfo is not _pytz_utc)):
|
||||
raise ValueError(
|
||||
'datetime values must have timezone set as timezone.utc')
|
||||
|
||||
|
||||
def _parse_annotated(anntype: Any) -> Tuple[Any, Optional[IOAttrs]]:
|
||||
"""Parse Annotated() constructs, returning annotated type & IOAttrs."""
|
||||
# If we get an Annotated[foo, bar, eep] we take
|
||||
# foo as the actual type and we look for IOMeta instances in
|
||||
# foo as the actual type and we look for IOAttrs instances in
|
||||
# bar/eep to affect our behavior.
|
||||
iometa: Optional[IOMeta] = None
|
||||
ioattrs: Optional[IOAttrs] = None
|
||||
if isinstance(anntype, _AnnotatedAlias):
|
||||
annargs = get_args(anntype)
|
||||
for annarg in annargs[1:]:
|
||||
if isinstance(annarg, IOMeta):
|
||||
if iometa is not None:
|
||||
if isinstance(annarg, IOAttrs):
|
||||
if ioattrs is not None:
|
||||
raise RuntimeError(
|
||||
'Multiple IOMeta instances found for a'
|
||||
'Multiple IOAttrs instances found for a'
|
||||
' single annotation; this is not supported.')
|
||||
iometa = annarg
|
||||
ioattrs = annarg
|
||||
anntype = annargs[0]
|
||||
return anntype, iometa
|
||||
return anntype, ioattrs
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user