Skip to content

Commit

Permalink
Merge pull request #269 from jacebrowning/handle-typeddict
Browse files Browse the repository at this point in the history
Fix exception when TypedDict is used
  • Loading branch information
jacebrowning authored Jul 28, 2022
2 parents 06b123a + 1fdd8fd commit 7503a4c
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes

## 1.4.1 (2022-07-28)

- Fixed exception when `TypedDict` is used, but schema is not yet supported.

## 1.4 (2022-06-03)

- Added support for accessing `Dict` keys as attributes.
Expand Down
6 changes: 6 additions & 0 deletions datafiles/converters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,10 @@ def map_type(cls, *, name: str = "", item_cls: Optional[type] = None):
if issubclass(cls, Enum):
return Enumeration.of_type(cls)

if issubclass(cls, dict):
log.warn("Schema enforcement not possible with 'TypedDict' annotation")
key = map_type(str)
value = Any
return Dictionary.of_mapping(key, value)

raise TypeError(f"Could not map type: {cls}")
7 changes: 5 additions & 2 deletions datafiles/converters/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ class Dictionary(Converter):

@classmethod
def of_mapping(cls, key: type, value: type):
name = f"{key.__name__}{value.__name__}Dict"
try:
name = f"{key.__name__}{value.__name__}Dict"
except AttributeError: # Python < 3.10
name = "UntypedDict"
bases = (cls,)
return type(name, bases, {})

Expand All @@ -151,7 +154,7 @@ def to_python_value(cls, deserialized_data, *, target_object):

@classmethod
def to_preserialization_data(cls, python_value, *, default_to_skip=None):
data = dict(python_value)
data = dict(python_value) if python_value else {}

if data == default_to_skip:
data.clear()
Expand Down
5 changes: 5 additions & 0 deletions datafiles/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@
sys.version_info < (3, 10),
reason="Union types (PEP 604) are not available in Python 3.9 and earlier",
)

xfail_without_type_names = pytest.mark.xfail(
sys.version_info < (3, 10),
reason="Types lack a __name__ in Python 3.9 and earlier",
)
21 changes: 20 additions & 1 deletion datafiles/tests/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

import pytest
from ruamel.yaml.scalarstring import LiteralScalarString
from typing_extensions import TypedDict

from datafiles import converters, settings

from . import xfail_without_pep_604
from . import xfail_without_pep_604, xfail_without_type_names


@dataclass
Expand All @@ -24,6 +25,11 @@ class MyNestedDataclass:
dc: MyDataclass


class MyTypedDict(TypedDict):
title: str
salary: int


class MyNonDataclass:
pass

Expand All @@ -45,6 +51,7 @@ class Color(Enum):
StringSet = converters.Set.of_type(converters.String)
MyDataclassConverter = converters.map_type(MyDataclass)
MyDataclassConverterList = converters.map_type(List[MyDataclass])
MyTypedDictConverter = converters.map_type(MyTypedDict)


def describe_map_type():
Expand Down Expand Up @@ -88,6 +95,11 @@ def it_requires_dict_annotations_to_have_types(expect):
with expect.raises(TypeError, "Types are required with 'Dict' annotation"):
converters.map_type(Dict)

@xfail_without_type_names
def it_handles_typed_dict_annotations(expect):
converter = converters.map_type(MyTypedDict)
expect(converter.__name__) == "StringAnyDict"

def it_handles_abstract_mapping_types(expect):
converter = converters.map_type(Mapping[str, int])
expect(converter.__name__) == "StringIntegerDict"
Expand Down Expand Up @@ -211,6 +223,9 @@ def when_immutable(expect, converter, data, value):
(MyDataclassConverter, MyDataclass(42), MyDataclass(foobar=42)),
(MyDataclassConverterList, None, []),
(MyDataclassConverterList, 42, [MyDataclass(foobar=0)]),
(MyTypedDictConverter, None, {}),
(MyTypedDictConverter, {}, {}),
(MyTypedDictConverter, {"a": 1}, {"a": 1}),
],
)
def when_mutable(expect, converter, data, value):
Expand Down Expand Up @@ -293,6 +308,10 @@ def describe_to_preserialization_data():
(StringList, [123, True, False], ["123", "True", "False"]),
(StringList, [], [None]),
(StringList, None, [None]),
# Dicts
(MyTypedDictConverter, None, {}),
(MyTypedDictConverter, {}, {}),
(MyTypedDictConverter, {"a": 1}, {"a": 1}),
# Sets
(StringSet, "ab", ["ab"]),
(StringSet, ("b", 1, "A"), ["b", "1", "A"]),
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]

name = "datafiles"
version = "1.4"
version = "1.4.1"
description = "File-based ORM for dataclasses."

license = "MIT"
Expand Down Expand Up @@ -56,7 +56,7 @@ classproperties = "^0.2"
minilog = "^2.1"

# Typing
typing-extensions = "^3.7"
typing-extensions = "^3.7" # remove this when Python 3.7 support is dropped

[tool.poetry.dev-dependencies]

Expand Down

0 comments on commit 7503a4c

Please sign in to comment.