Allow fast ObjectType creation based on dataclasses (#1157)

* Allow fast ObjectType creation based on dataclasses

* Fixed Python 3.8 integration

* Added repr and eq methods to ObjectType containers

* Reformatted code

* Fixed mypy issue

* Removed unused __init__ for ObjectType containers

* Use black in dataclasses

* Use latest black verison on precommit
This commit is contained in:
Syrus Akbary 2020-04-12 17:45:46 -07:00 committed by GitHub
parent 37d6eaea46
commit 49fcf9f2e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 1321 additions and 47 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
from typing import Type from typing import Type
from ..utils.subclass_with_meta import SubclassWithMeta from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta
from ..utils.trim_docstring import trim_docstring from ..utils.trim_docstring import trim_docstring
@ -26,6 +26,9 @@ class BaseOptions:
return f"<{self.__class__.__name__} name={repr(self.name)}>" return f"<{self.__class__.__name__} name={repr(self.name)}>"
BaseTypeMeta = SubclassWithMeta_Meta
class BaseType(SubclassWithMeta): class BaseType(SubclassWithMeta):
@classmethod @classmethod
def create_type(cls, class_name, **options): def create_type(cls, class_name, **options):

View File

@ -1,8 +1,13 @@
from .base import BaseOptions, BaseType from .base import BaseOptions, BaseType, BaseTypeMeta
from .field import Field from .field import Field
from .interface import Interface from .interface import Interface
from .utils import yank_fields_from_attrs from .utils import yank_fields_from_attrs
try:
from dataclasses import make_dataclass, field
except ImportError:
from ..pyutils.dataclasses import make_dataclass, field # type: ignore
# For static type checking with Mypy # For static type checking with Mypy
MYPY = False MYPY = False
if MYPY: if MYPY:
@ -14,7 +19,34 @@ class ObjectTypeOptions(BaseOptions):
interfaces = () # type: Iterable[Type[Interface]] interfaces = () # type: Iterable[Type[Interface]]
class ObjectType(BaseType): class ObjectTypeMeta(BaseTypeMeta):
def __new__(cls, name, bases, namespace):
# We create this type, to then overload it with the dataclass attrs
class InterObjectType:
pass
base_cls = super().__new__(cls, name, (InterObjectType,) + bases, namespace)
if base_cls._meta:
fields = [
(
key,
"typing.Any",
field(
default=field_value.default_value
if isinstance(field_value, Field)
else None
),
)
for key, field_value in base_cls._meta.fields.items()
]
dataclass = make_dataclass(name, fields, bases=())
InterObjectType.__init__ = dataclass.__init__
InterObjectType.__eq__ = dataclass.__eq__
InterObjectType.__repr__ = dataclass.__repr__
return base_cls
class ObjectType(BaseType, metaclass=ObjectTypeMeta):
""" """
Object Type Definition Object Type Definition
@ -127,44 +159,3 @@ class ObjectType(BaseType):
super(ObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) super(ObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
is_type_of = None is_type_of = None
def __init__(self, *args, **kwargs):
# ObjectType acting as container
args_len = len(args)
fields = self._meta.fields.items()
if args_len > len(fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")
fields_iter = iter(fields)
if not kwargs:
for val, (name, field) in zip(args, fields_iter):
setattr(self, name, val)
else:
for val, (name, field) in zip(args, fields_iter):
setattr(self, name, val)
kwargs.pop(name, None)
for name, field in fields_iter:
try:
val = kwargs.pop(
name, field.default_value if isinstance(field, Field) else None
)
setattr(self, name, val)
except KeyError:
pass
if kwargs:
for prop in list(kwargs):
try:
if isinstance(
getattr(self.__class__, prop), property
) or prop.startswith("_"):
setattr(self, prop, kwargs.pop(prop))
except AttributeError:
pass
if kwargs:
raise TypeError(
f"'{list(kwargs)[0]}' is an invalid keyword argument"
f" for {self.__class__.__name__}"
)

View File

@ -83,6 +83,10 @@ def test_generate_objecttype_with_fields():
def test_generate_objecttype_with_private_attributes(): def test_generate_objecttype_with_private_attributes():
class MyObjectType(ObjectType): class MyObjectType(ObjectType):
def __init__(self, _private_state=None, **kwargs):
self._private_state = _private_state
super().__init__(**kwargs)
_private_state = None _private_state = None
assert "_private_state" not in MyObjectType._meta.fields assert "_private_state" not in MyObjectType._meta.fields
@ -155,6 +159,20 @@ def test_objecttype_as_container_only_args():
assert container.field2 == "2" assert container.field2 == "2"
def test_objecttype_repr():
container = Container("1", "2")
assert repr(container) == "Container(field1='1', field2='2')"
def test_objecttype_eq():
container1 = Container("1", "2")
container2 = Container("1", "2")
container3 = Container("2", "3")
assert container1 == container1
assert container1 == container2
assert container2 != container3
def test_objecttype_as_container_args_kwargs(): def test_objecttype_as_container_args_kwargs():
container = Container("1", field2="2") container = Container("1", field2="2")
assert container.field1 == "1" assert container.field1 == "1"
@ -173,17 +191,19 @@ def test_objecttype_as_container_all_kwargs():
def test_objecttype_as_container_extra_args(): def test_objecttype_as_container_extra_args():
with raises(IndexError) as excinfo: with raises(TypeError) as excinfo:
Container("1", "2", "3") Container("1", "2", "3")
assert "Number of args exceeds number of fields" == str(excinfo.value) assert "__init__() takes from 1 to 3 positional arguments but 4 were given" == str(
excinfo.value
)
def test_objecttype_as_container_invalid_kwargs(): def test_objecttype_as_container_invalid_kwargs():
with raises(TypeError) as excinfo: with raises(TypeError) as excinfo:
Container(unexisting_field="3") Container(unexisting_field="3")
assert "'unexisting_field' is an invalid keyword argument for Container" == str( assert "__init__() got an unexpected keyword argument 'unexisting_field'" == str(
excinfo.value excinfo.value
) )