Added bytes, datetime, and firestore format support to dataclassio

This commit is contained in:
Eric Froemling 2021-05-25 12:14:42 -05:00
parent 157be291f8
commit 7f1458304d
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
7 changed files with 214 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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