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 ..utils.subclass_with_meta import SubclassWithMeta
from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta
from ..utils.trim_docstring import trim_docstring
@ -26,6 +26,9 @@ class BaseOptions:
return f"<{self.__class__.__name__} name={repr(self.name)}>"
BaseTypeMeta = SubclassWithMeta_Meta
class BaseType(SubclassWithMeta):
@classmethod
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 .interface import Interface
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
MYPY = False
if MYPY:
@ -14,7 +19,34 @@ class ObjectTypeOptions(BaseOptions):
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
@ -127,44 +159,3 @@ class ObjectType(BaseType):
super(ObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
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():
class MyObjectType(ObjectType):
def __init__(self, _private_state=None, **kwargs):
self._private_state = _private_state
super().__init__(**kwargs)
_private_state = None
assert "_private_state" not in MyObjectType._meta.fields
@ -155,6 +159,20 @@ def test_objecttype_as_container_only_args():
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():
container = Container("1", field2="2")
assert container.field1 == "1"
@ -173,17 +191,19 @@ def test_objecttype_as_container_all_kwargs():
def test_objecttype_as_container_extra_args():
with raises(IndexError) as excinfo:
with raises(TypeError) as excinfo:
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():
with raises(TypeError) as excinfo:
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
)