Skip to content

Commit

Permalink
Merge pull request #209 from jacebrowning/python-3.10
Browse files Browse the repository at this point in the history
Preliminary Python 3.10 support
  • Loading branch information
jacebrowning committed Apr 18, 2021
2 parents 1e40eb1 + 8e4d305 commit e9afaaa
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .python-version
@@ -1 +1 @@
3.9.0
3.10.0a6
17 changes: 16 additions & 1 deletion datafiles/converters/__init__.py
@@ -1,4 +1,5 @@
import dataclasses
import inspect
from enum import Enum
from inspect import isclass
from typing import Any, Dict, Mapping, Optional, Union
Expand Down Expand Up @@ -31,6 +32,18 @@ def register(cls: Union[type, str], converter: type):
register(String.TYPE, String)


def resolve(annotation, obj=None):
if isinstance(annotation, str):
log.debug(f"Attempting to eval {annotation!r} using {obj}")
annotation = annotation.replace('List', 'list').replace('Dict', 'list')
namespace = inspect.getmodule(obj).__dict__ if obj else None
try:
return eval(annotation, namespace) # pylint: disable=eval-used
except NameError as e:
log.warn(f"Unable to eval: {e}")
return annotation


@cached
def map_type(cls, *, name: str = '', item_cls: Optional[type] = None):
"""Infer the converter type from a dataclass, type, or annotation."""
Expand All @@ -47,11 +60,13 @@ def map_type(cls, *, name: str = '', item_cls: Optional[type] = None):
if dataclasses.is_dataclass(cls):
converters = {}
for field in dataclasses.fields(cls):
converters[field.name] = map_type(field.type, name=field.name) # type: ignore
converters[field.name] = map_type(resolve(field.type), name=field.name) # type: ignore
converter = Dataclass.of_mappings(cls, converters)
log.debug(f'Mapped {cls!r} to new converter: {converter}')
return converter

cls = resolve(cls)

if hasattr(cls, '__origin__'):
converter = None

Expand Down
4 changes: 2 additions & 2 deletions datafiles/mapper.py
Expand Up @@ -12,7 +12,7 @@
from cached_property import cached_property

from . import config, formats, hooks
from .converters import Converter, List, map_type
from .converters import Converter, List, map_type, resolve
from .types import Missing, Trilean
from .utils import display, get_default_field_value, recursive_update, write

Expand Down Expand Up @@ -290,7 +290,7 @@ def create_mapper(obj, root=None) -> Mapper:
for field in [field for field in dataclasses.fields(obj) if field.init]:
self_name = f'self.{field.name}'
if pattern is None or self_name not in pattern:
attrs[field.name] = map_type(field.type, name=field.name) # type: ignore
attrs[field.name] = map_type(resolve(field.type, obj), name=field.name) # type: ignore

return Mapper(
obj,
Expand Down
52 changes: 25 additions & 27 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -66,6 +66,7 @@ isort = "=5.5.1"

# Linters
mypy = "~0.790"
typed_ast = "^1.4.3" # updated mypy dependency for Python 3.10
pylint = "~2.7.4"
pydocstyle = "*"

Expand Down
10 changes: 10 additions & 0 deletions tests/__init__.py
@@ -1 +1,11 @@
"""Integration tests for the package."""

import sys

import pytest


xfail_on_latest = pytest.mark.xfail(
sys.version_info >= (3, 10),
reason="Python 3.10+ cannot annotations for locally-defined types",
)
1 change: 0 additions & 1 deletion tests/test_custom_converters_future.py
Expand Up @@ -10,7 +10,6 @@
from datafiles import datafile


@pytest.mark.xfail(reason='https://github.com/jacebrowning/datafiles/issues/131')
def test_optional_type(expect):
@datafile("../tmp/sample.yml")
class MyObject:
Expand Down
3 changes: 3 additions & 0 deletions tests/test_generic_converters.py
Expand Up @@ -5,7 +5,10 @@
from datafiles import Missing, converters, datafile
from datafiles.utils import dedent

from . import xfail_on_latest


@xfail_on_latest
def test_generic_converters(expect):
S = TypeVar("S")
T = TypeVar("T")
Expand Down
3 changes: 3 additions & 0 deletions tests/test_instantiation.py
Expand Up @@ -8,6 +8,8 @@
from datafiles import Missing, datafile
from datafiles.utils import logbreak, write

from . import xfail_on_latest


@datafile('../tmp/sample.yml', manual=True)
class SampleWithDefaults:
Expand Down Expand Up @@ -150,6 +152,7 @@ def when_file_exists(expect):


def describe_missing_attributes():
@xfail_on_latest
def when_dataclass(expect):
@dataclass
class Name:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_loading.py
Expand Up @@ -9,6 +9,7 @@
from datafiles import datafile
from datafiles.utils import dedent, logbreak, read, write

from . import xfail_on_latest
from .samples import (
Sample,
SampleAsJSON,
Expand Down Expand Up @@ -249,6 +250,7 @@ def with_extra_attributes(sample, expect):
expect(sample.nested.score) == 3.4
expect(hasattr(sample.nested, 'extra')).is_(False)

@xfail_on_latest
def with_multiple_levels(expect):
@dataclass
class Bottom:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_orm_usage.py
Expand Up @@ -8,6 +8,8 @@
from datafiles import datafile
from datafiles.utils import logbreak, write

from . import xfail_on_latest


# This model is based on the example dataclass from:
# https://docs.python.org/3/library/dataclasses.html
Expand Down Expand Up @@ -48,6 +50,7 @@ def test_multiple_instances_are_distinct(expect):
}


@xfail_on_latest
def test_classes_can_share_a_nested_dataclass(expect):
@datafile
class Nested:
Expand Down Expand Up @@ -95,6 +98,7 @@ def test_partial_load_from_disk(expect):
expect(items[0].quantity_on_hand) == 0


@xfail_on_latest
def test_missing_optional_fields_are_loaded(expect):
@datafile
class Name:
Expand Down
5 changes: 5 additions & 0 deletions tests/test_profiling.py
Expand Up @@ -6,6 +6,8 @@
from datafiles import datafile
from datafiles.utils import logbreak

from . import xfail_on_latest


def get_sample():
@dataclass
Expand All @@ -29,16 +31,19 @@ class Sample:
return Sample('profiling', Item(2, None), None, [Item(3, None)])


@xfail_on_latest
def test_init():
get_sample()


@xfail_on_latest
def test_load():
sample = get_sample()
logbreak("Loading")
sample.datafile.load() # pylint: disable=no-member


@xfail_on_latest
def test_save():
sample = get_sample()
logbreak("Saving")
Expand Down
2 changes: 2 additions & 0 deletions tests/test_saving.py
Expand Up @@ -9,6 +9,7 @@
from datafiles import datafile
from datafiles.utils import dedent, logbreak, read, write

from . import xfail_on_latest
from .samples import (
Sample,
SampleWithCustomFields,
Expand Down Expand Up @@ -276,6 +277,7 @@ def with_nones(expect):
"""
)

@xfail_on_latest
def when_nested_dataclass_is_none(expect):
@datafile
class Name:
Expand Down
3 changes: 3 additions & 0 deletions tests/test_setup.py
Expand Up @@ -11,6 +11,8 @@
import datafiles
from datafiles import datafile

from . import xfail_on_latest


def describe_automatic():
"""Test creating a datafile using the decorator."""
Expand Down Expand Up @@ -76,6 +78,7 @@ class Sample:

return Sample(1, Nested(name="b"), "a")

@xfail_on_latest
def it_converts_attributes(expect, sample):
expect(sample.datafile.data) == {
'name': "a",
Expand Down

0 comments on commit e9afaaa

Please sign in to comment.