Skip to content

Commit

Permalink
add tests for other libs
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag committed Nov 27, 2024
1 parent 9b9a256 commit 7ba05e7
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 138 deletions.
143 changes: 70 additions & 73 deletions benchmarks/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import pytest
from dataclasses_json import DataClassJsonMixin, config
from jsons import JsonSerializable
from dacite import from_dict as dacite_from_dict
from pydantic import BaseModel
import attr
import mashumaro

from dataclass_wizard import JSONWizard
from dataclass_wizard.class_helper import create_new_class
Expand All @@ -19,10 +23,8 @@

log = logging.getLogger(__name__)


@dataclass
class MyClass:

my_ledger: Dict[str, Any]
the_answer_to_life: Optional[int]
people: List['Person']
Expand All @@ -37,50 +39,49 @@ class MyClassDJ(DataClassJsonMixin):
is_enabled: bool = True


# New Mashumaro Model
@dataclass
class MyClassMashumaro(mashumaro.DataClassDictMixin):
my_ledger: Dict[str, Any]
the_answer_to_life: Optional[int]
people: List['Person']
is_enabled: bool = True


@dataclass
class Person:
name: 'Name'
age: int
birthdate: datetime
# dataclass-factory doesn't support Literals
# gender: Literal['M', 'F', 'N/A']
gender: str
occupation: Union[str, List[str]]
# dataclass-factory doesn't support DefaultDict
# hobbies: DefaultDict[str, List[str]] = field(
# default_factory=lambda: defaultdict(list))
hobbies: Dict[str, List[str]] = field(
default_factory=lambda: defaultdict(list))


class Name(NamedTuple):
"""A person's name"""
first: str
last: str
salutation: Optional[str] = 'Mr.'

@dataclass
class NameDataclass:
first: str
last: str
salutation: Optional[str] = 'Mr.'


@dataclass
class PersonDJ:
# spent a long time debugging the issue: `dataclasses-json` doesn't
# seem to support Named Tuples like `Name` (unsure how to fix)
#
# I also can't annotate it as just a `Tuple` either... I have no idea
# why. So unsure what to do, now I'm just declaring it as `Any` type -
# which of course is a bit unfair on other test cases that handle
# Named Tuples properly, but it's the only I could get this to work.
name: Any
name: NameDataclass
age: int
birthdate: datetime = field(metadata=config(
encoder=datetime.isoformat,
decoder=as_datetime,
mm_field=marshmallow.fields.DateTime(format='iso')
encoder=datetime.isoformat,
decoder=as_datetime,
mm_field=marshmallow.fields.DateTime(format='iso')
))
gender: str
occupation: Union[str, List[str]]
# dataclass-factory doesn't support DefaultDict
# hobbies: DefaultDict[str, List[str]] = field(
# default_factory=lambda: defaultdict(list))
hobbies: Dict[str, List[str]] = field(
default_factory=lambda: defaultdict(list))

Expand All @@ -91,6 +92,8 @@ class PersonDJ:
JsonsType = TypeVar('JsonsType', MyClass, JsonSerializable)
# Model for `dataclasses-json`
DJType = TypeVar('DJType', MyClass, DataClassJsonMixin)
# Model for `mashumaro`
MashumaroType = TypeVar('MashumaroType', MyClass, mashumaro.DataClassDictMixin)
# Factory for `dataclass-factory`
factory = dataclass_factory.Factory()

Expand All @@ -103,6 +106,14 @@ class PersonDJ:
MyClassJsons: JsonsType = create_new_class(
MyClass, (MyClass, JsonSerializable), 'Jsons',
attr_dict=vars(MyClass).copy())
MyClassMashumaro: MashumaroType = create_new_class(
MyClass, (MyClass, mashumaro.DataClassDictMixin), 'Mashumaro',
attr_dict=vars(MyClass).copy())



def custom_name_decoder(value):
return Name(**value)


@pytest.fixture(scope='session')
Expand All @@ -115,20 +126,16 @@ def data():
'the_answer_to_life': '42',
'people': [
{
# I want to make this into a Tuple - ('Roberto', 'Fuirron') -
# but `dataclass-factory` doesn't seem to like that.
'name': {'first': 'Roberto', 'last': 'Fuirron'},
'name': ('Roberto', 'Fuirron'),
'age': 21,
'birthdate': '1950-02-28T17:35:20Z',
'gender': 'M',
'occupation': ['sailor', 'fisher'],
'hobbies': {'M-F': ('chess', '123', 'reading'), 'Sat-Sun': ['parasailing']}
},
{
'name': {'first': 'Janice', 'last': 'Darr', 'salutation': 'Dr.'},
'name': ('Janice', 'Darr', 'Dr.'),
'age': 45,
# `jsons` doesn't support this format (not sure how to fix?)
# 'birthdate': '1971-11-05 05:10:59',
'birthdate': '1971-11-05T05:10:59Z',
'gender': 'F',
'occupation': 'Dentist'
Expand All @@ -137,10 +144,25 @@ def data():
}


@pytest.fixture(scope='session')
def data_2(data):
"""data for `dataclasses-factory`, which has issue with tuple -> NamedTuple"""

d = data.copy()
d['people'] = [p.copy() for p in data['people']]

# I want to make this into a Tuple - ('Roberto', 'Fuirron') -
# but `dataclass-factory` doesn't seem to like that.

d['people'][0]['name'] = {'first': 'Roberto', 'last': 'Fuirron'}
d['people'][1]['name'] = {'first': 'Janice', 'last': 'Darr', 'salutation': 'Dr.'}

return d


def parse_iso_format(data):
return as_datetime(data)


iso_format_schema = dataclass_factory.Schema(
parser=parse_iso_format,
serializer=datetime.isoformat
Expand All @@ -150,101 +172,76 @@ def parse_iso_format(data):
datetime: iso_format_schema
}


def test_load(request, data, n):
def test_load(request, data, data_2, n):
g = globals().copy()
g.update(locals())

# Result: 0.790
log.info('dataclass-wizard %f',
timeit('MyClassWizard.from_dict(data)', globals=g, number=n))

# Result: 0.774
log.info('dataclass-factory %f',
timeit('factory.load(data, MyClass)', globals=g, number=n))
timeit('factory.load(data_2, MyClass)', globals=g, number=n))

# Result: 23.40
# NOTE: This likely is not an entirely fair comparison, since the
# rest load `Person.name` as a `Name` (which is a NamedTuple sub-class),
# but in this case we just load it as an `Any` type.
log.info('dataclasses-json %f',
timeit('MyClassDJ.from_dict(data)', globals=g, number=n))
timeit('MyClassDJ.from_dict(data_2)', globals=g, number=n))

log.info('mashumaro %f',
timeit('MyClassMashumaro.from_dict(data)', globals=g, number=n))

if not request.config.getoption("--all"):
pytest.skip("Skipping benchmarks for the rest by default, unless --all is specified.")

# these ones took a long time xD
# Result: 70.752
log.info('jsons %f',
timeit('MyClassJsons.load(data)', globals=g, number=n))

# Result: 118.775
log.info('jsons (strict) %f',
timeit('MyClassJsons.load(data, strict=True)', globals=g, number=n))

# Assert the dataclass instances have the same values for all fields.

c1 = MyClassWizard.from_dict(data)
c2 = factory.load(data, MyClass)
c2 = factory.load(data_2, MyClass)
c3 = MyClassDJ.from_dict(data)
c4 = MyClassJsons.load(data)
c5 = MyClassMashumaro.from_dict(data)

# Really can't do a direct equality check because it's all over the place.
# For example, `dataclass-factory` de-serializes NamedTuple sub-classes as
# tuples. That's a bit odd, because our annotated type is clearly a NamedTuple
# subclass (Name).
# assert c1.__dict__ == c2.__dict__ == c3.__dict__ == c4.__dict__


def test_dump(request, data, n):
# Since these models might differ slightly, we can skip exact equality checks
# assert c1.__dict__ == c2.__dict__ == c3.__dict__ == c4.__dict__ == c5.__dict__

def test_dump(request, data, data_2, n):
c1 = MyClassWizard.from_dict(data)
c2 = factory.load(data, MyClass)
c3 = MyClassDJ.from_dict(data)
c2 = factory.load(data_2, MyClass)
c3 = MyClassDJ.from_dict(data_2)
c4 = MyClassJsons.load(data)
c5 = MyClassMashumaro.from_dict(data)

g = globals().copy()
g.update(locals())

# Result: 1.394
log.info('dataclass-wizard %f',
timeit('c1.to_dict()', globals=g, number=n))

# Result: 1.804
log.info('asdict (dataclasses) %f',
timeit('asdict(c1)', globals=g, number=n))

# Result: 0.862
log.info('dataclass-factory %f',
timeit('factory.dump(c2, MyClass)', globals=g, number=n))

# Result: 9.872
log.info('dataclasses-json %f',
timeit('c3.to_dict()', globals=g, number=n))

log.info('mashumaro %f',
timeit('c5.to_dict()', globals=g, number=n))

if not request.config.getoption("--all"):
pytest.skip("Skipping benchmarks for the rest by default, unless --all is specified.")

# Result: 53.686
log.info('jsons %f',
timeit('c4.dump()', globals=g, number=n))

# Result: 48.100
log.info('jsons (strict) %f',
timeit('c4.dump(strict=True)', globals=g, number=n))

# Assert the dict objects which are the result of `to_dict` are all equal.

# Need this step because our lib converts field names to camel-case
# by default.
c1_dict = {to_snake_case(f): fval for f, fval in c1.to_dict().items()}
# I tried to make the formats equal, but then I gave up midway. Probably not
# worth the effort tbh. The other important difference is how NamedTuple's
# are converted. `dataclass-factory` already loads them as tuples, so it
# also dumps them as tuples. But in our case we dump as NamedTuple, because
# technically NamedTuple is still JSON-serializable (its a tuple in the end)
#
# for person in c1_dict['people']:
# person['birthdate'] = person['birthdate'].replace('Z', '+00:00', 1)

# assert c1_dict == factory.dump(c2, MyClass) == c3.to_dict() == c4.dump()

# assert c1_dict == factory.dump(c2, MyClass) == c3.to_dict() == c4.dump() == c5.to_dict()
Loading

0 comments on commit 7ba05e7

Please sign in to comment.