diff --git a/benchmarks/requirements.txt b/benchmarks/requirements.txt index 59ec22f..76530f7 100644 --- a/benchmarks/requirements.txt +++ b/benchmarks/requirements.txt @@ -4,3 +4,5 @@ django # pyup: ignore djangorestframework # pyup: ignore #marshmallow # pyup: ignore toastedmarshmallow # pyup: ignore +attr # pyup: ignore +cattrs # pyup: ignore diff --git a/benchmarks/run.py b/benchmarks/run.py index 43aa350..b36d211 100644 --- a/benchmarks/run.py +++ b/benchmarks/run.py @@ -33,6 +33,11 @@ try: except Exception: TestToastedMarshmallow = None +try: + from test_cattr import TestCAttr +except Exception: + TestCAttr = None + PUNCTUATION = ' \t\n!"#$%&\'()*+,-./' LETTERS = string.ascii_letters UNICODE = '\xa0\xad¡¢£¤¥¦§¨©ª«¬ ®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' @@ -42,7 +47,7 @@ random = random.SystemRandom() # in order of performance for csv other_tests = [ t for t in - [TestToastedMarshmallow, TestMarshmallow, TestTrafaret, TestDRF] + [TestCAttr, TestToastedMarshmallow, TestMarshmallow, TestTrafaret, TestDRF] if t is not None ] diff --git a/benchmarks/test_cattr.py b/benchmarks/test_cattr.py new file mode 100644 index 0000000..625a249 --- /dev/null +++ b/benchmarks/test_cattr.py @@ -0,0 +1,101 @@ +from datetime import datetime +from typing import List, Optional + +import attr +import cattr +from dateutil.parser import parse + + +class TestCAttr: + package = 'cattr' + version = attr.__version__ + + def __init__(self, allow_extra): + # cf. https://github.com/Tinche/cattrs/issues/26 why at least structure_str is needed + def structure_str(s, _): + if not isinstance(s, str): + raise ValueError() + return s + + def structure_int(i, _): + if not isinstance(i, int): + raise ValueError() + return i + + class PositiveInt(int): + ... + + def structure_posint(i, x): + i = PositiveInt(i) + if not isinstance(i, PositiveInt): + raise ValueError() + if i <= 0: + raise ValueError() + return i + + cattr.register_structure_hook(datetime, lambda isostring, _: parse(isostring)) + cattr.register_structure_hook(str, structure_str) + cattr.register_structure_hook(int, structure_int) + cattr.register_structure_hook(PositiveInt, structure_posint) + + def str_len_val(max_len: int, min_len: int = 0, required: bool = False): + # validate the max len of a string and optionally its min len and whether None is + # an acceptable value + def _check_str_len(self, attribute, value): + if value is None: + if required: + raise ValueError("") + else: + return + if len(value) > max_len: + raise ValueError("") + if min_len and len(value) < min_len: + raise ValueError("") + + return _check_str_len + + def pos_int(self, attribute, value): + # Validate that value is a positive >0 integer; None is allowed + if value is None: + return + if value <= 0: + raise ValueError("") + + @attr.s(auto_attribs=True, frozen=True, kw_only=True) + class Skill: + subject: str + subject_id: int + category: str + qual_level: str + qual_level_id: int + qual_level_ranking: float = 0 + + @attr.s(auto_attribs=True, frozen=True, kw_only=True) + class Location: + latitude: float = None + longitude: float = None + + @attr.s(auto_attribs=True, frozen=True, kw_only=True) + class Model: + id: int + sort_index: float + client_name: str = attr.ib(validator=str_len_val(255)) + # client_email: EmailStr = None + client_phone: Optional[str] = attr.ib(default=None, validator=str_len_val(255)) + location: Optional[Location] = None + + contractor: Optional[PositiveInt] + upstream_http_referrer: Optional[str] = attr.ib(default=None, validator=str_len_val(1023)) + grecaptcha_response: str = attr.ib(validator=str_len_val(1000, 20, required=True)) + last_updated: Optional[datetime] = None + skills: List[Skill] = [] + + self.model = Model + + def validate(self, data): + try: + return True, cattr.structure(data, self.model) + except ValueError as e: + return False, str(e) + except TypeError as e: + return False, str(e) diff --git a/changes/513-sebastianmika.md b/changes/513-sebastianmika.md new file mode 100644 index 0000000..f3c2482 --- /dev/null +++ b/changes/513-sebastianmika.md @@ -0,0 +1 @@ +Add benchmarks for `cattrs` diff --git a/docs/.benchmarks_table.md b/docs/.benchmarks_table.md index 398baf4..8535f5f 100644 --- a/docs/.benchmarks_table.md +++ b/docs/.benchmarks_table.md @@ -2,8 +2,9 @@ Package | Version | Relative Performance | Mean validation time --- | --- | --- | --- -pydantic | `1.0b1` | | 46.5μs -marshmallow | `2.15.1` | 2.9x slower | 136.1μs -trafaret | `1.2.0` | 3.3x slower | 154.2μs -toasted-marshmallow | `2.15.2post1` | 3.4x slower | 159.4μs -django-restful-framework | `3.10.3` | 12.4x slower | 577.0μs +pydantic | `1.1` | | 46.1μs +cattr | `19.3.0` | 1.3x slower | 62.1μs +marshmallow | `2.15.1` | 2.9x slower | 132.7μs +toasted-marshmallow | `2.15.2post1` | 2.9x slower | 134.1μs +trafaret | `2.0.0` | 3.3x slower | 153.5μs +django-restful-framework | `3.10.3` | 12.7x slower | 586.1μs