From b7d93df0c840371679fd3a7a267cfef8f395e92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Thu, 7 Apr 2022 13:20:12 +0200 Subject: [PATCH 1/6] Initial kw_only support for attrs classes --- poetry.lock | 272 ++++++++++----------- pyproject.toml | 4 +- src/cattrs/converters.py | 2 +- src/cattrs/gen.py | 13 +- tests/__init__.py | 106 ++++++--- tests/metadata/__init__.py | 353 +++++++++++++++++++--------- tests/metadata/test_genconverter.py | 108 ++++++--- tests/metadata/test_roundtrips.py | 46 ++-- tests/test_disambigutors.py | 10 +- tests/test_gen_dict.py | 47 ++-- tests/test_structure_attrs.py | 36 +-- tests/test_unstructure.py | 14 +- 12 files changed, 633 insertions(+), 378 deletions(-) diff --git a/poetry.lock b/poetry.lock index 58e24524..9af28d24 100644 --- a/poetry.lock +++ b/poetry.lock @@ -102,11 +102,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.2" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -203,7 +203,7 @@ doc = ["myst-parser", "sphinx-copybutton", "sphinx-design", "sphinx-inline-tabs" [[package]] name = "hypothesis" -version = "6.37.0" +version = "6.41.0" description = "A library for property-based testing" category = "dev" optional = false @@ -214,7 +214,7 @@ attrs = ">=19.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.1)"] cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] @@ -227,7 +227,7 @@ pandas = ["pandas (>=0.25)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.1)"] [[package]] name = "idna" @@ -258,7 +258,7 @@ test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)"] [[package]] name = "importlib-metadata" -version = "4.11.1" +version = "4.11.3" description = "Read metadata from Python packages" category = "dev" optional = false @@ -269,7 +269,7 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] @@ -297,11 +297,11 @@ plugins = ["setuptools"] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.1" description = "A very fast and expressive template engine." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -311,7 +311,7 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "2.1.0" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false @@ -478,11 +478,11 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -493,10 +493,10 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-benchmark" @@ -528,7 +528,7 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2021.3" +version = "2022.1" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -594,7 +594,7 @@ python-versions = "*" [[package]] name = "soupsieve" -version = "2.3.1" +version = "2.3.2" description = "A modern CSS selector implementation for Beautiful Soup." category = "dev" optional = false @@ -602,7 +602,7 @@ python-versions = ">=3.6" [[package]] name = "sphinx" -version = "4.4.0" +version = "4.5.0" description = "Python documentation generator" category = "dev" optional = false @@ -768,7 +768,7 @@ python-versions = ">=3.6" [[package]] name = "ujson" -version = "5.1.0" +version = "5.2.0" description = "Ultra fast JSON encoder and decoder for Python" category = "dev" optional = false @@ -776,20 +776,20 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.9" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.1" +version = "20.14.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -808,15 +808,15 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" @@ -857,8 +857,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, + {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -932,8 +932,8 @@ furo = [ {file = "furo-2021.11.23.tar.gz", hash = "sha256:54cecac5f3b688b5c7370d72ecdf1cd91a6c53f0f42751f4a719184b562cde70"}, ] hypothesis = [ - {file = "hypothesis-6.37.0-py3-none-any.whl", hash = "sha256:0f407e8e87644e0c6bd2dad6c2df02892172b923065de9d302ea6ce002e6f345"}, - {file = "hypothesis-6.37.0.tar.gz", hash = "sha256:f4fd3c11d997aea3675d67f3613bccbbb3ef897a96968ec483d472f5d8aa410d"}, + {file = "hypothesis-6.41.0-py3-none-any.whl", hash = "sha256:ca931c5a6414f3f9636fdaf978a216ee9b5c4a6b4415adf628e9d5e5003dcd99"}, + {file = "hypothesis-6.41.0.tar.gz", hash = "sha256:de48abb676fc76e4397cd002926e5747cef518570d132221244d27e1075c0bec"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -961,8 +961,8 @@ immutables = [ {file = "immutables-0.15.tar.gz", hash = "sha256:3713ab1ebbb6946b7ce1387bb9d1d7f5e09c45add58c2a2ee65f963c171e746b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.11.1-py3-none-any.whl", hash = "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094"}, - {file = "importlib_metadata-4.11.1.tar.gz", hash = "sha256:175f4ee440a0317f6e8d81b7f8d4869f93316170a65ad2b007d2929186c8052c"}, + {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, + {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -973,50 +973,50 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, + {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, - {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -1268,8 +1268,8 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, ] pytest-benchmark = [ {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, @@ -1280,8 +1280,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] pytz = [ - {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, - {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, ] pytzdata = [ {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, @@ -1335,12 +1335,12 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] soupsieve = [ - {file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"}, - {file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"}, + {file = "soupsieve-2.3.2-py3-none-any.whl", hash = "sha256:a714129d3021ec17ce5be346b1007300558b378332c289a1a20e7d4de6ff18a5"}, + {file = "soupsieve-2.3.2.tar.gz", hash = "sha256:0bcc6d7432153063e3df09c3ac9442af3eba488715bfcad6a4c38ccb2a523124"}, ] sphinx = [ - {file = "Sphinx-4.4.0-py3-none-any.whl", hash = "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe"}, - {file = "Sphinx-4.4.0.tar.gz", hash = "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"}, + {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, + {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1413,66 +1413,66 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] ujson = [ - {file = "ujson-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:644552d1e89983c08d0c24358fbcb5829ae5b5deee9d876e16d20085cfa7dc81"}, - {file = "ujson-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0cae4a9c141856f7ad1a79c17ff1aaebf7fd8faa2f2c2614c37d6f82ed261d96"}, - {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba63b789d83ca92237dbc72041a268d91559f981c01763a107105878bae442e"}, - {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4e8f71e2fd42dce245bace7e2aa97dabef13926750a351eadca89a1e0f1abd"}, - {file = "ujson-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f73946c047a38640b1f5a2a459237b7bdc417ab028a76c796e4eea984b359b9"}, - {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afe91153c2046fa8210b92def513124e0ea5b87ad8fa4c14fef8197204b980f1"}, - {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b1ef400fc73ab0cb61b74a662ad4207917223aba6f933a9fea9b0fbe75de2361"}, - {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c8a884d60dd2eed2fc95a9474d57ead82adf254f54caffb3d9e8ed185c49aba"}, - {file = "ujson-5.1.0-cp310-cp310-win32.whl", hash = "sha256:173b90a2c2836ee42f708df88ecfe3efbc4d868df73c9fcea8cb8f6f3ab93892"}, - {file = "ujson-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c45ad95e82155372d9908774db46e0ef7880af28a734d0b14eaa4f505e64982"}, - {file = "ujson-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4155a7c29bf330329519027c815e15e381c1fff22f50d26f135584d482bbd95d"}, - {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa616d0d3c594785c6e9b7f42686bb1c86f9e64aa0f30a72c86d8eb315f54194"}, - {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a48efcb5d3695b295c26835ed81048da8cd40e76c4fde2940c807aa452b560c9"}, - {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838d35eb9006d36f9241e95958d9f4819bcf1ea2ec155daf92d5751c31bcc62b"}, - {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:05aa6c7297a22081f65497b6f586de6b7060ea47c3ecda80896f47200e9dbf04"}, - {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce441ab7ad1db592e2db95b6c2a1eb882123532897340afac1342c28819e9833"}, - {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9937e819196b894ffd00801b24f1042dabda142f355313c3f20410993219bc4f"}, - {file = "ujson-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:06bed66ae62d517f67a61cf53c056800b35ef364270723168a1db62702e2d30c"}, - {file = "ujson-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:74e41a0222e6e8136e38f103d6cc228e4e20f1c35cc80224a42804fd67fb35c8"}, - {file = "ujson-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7bbb87f040e618bebe8c6257b3e4e8ae2f708dcbff3270c84718b3360a152799"}, - {file = "ujson-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68e38122115a8097fbe1cfe52979a797eaff91c10c1bf4b27774e5f30e7f723a"}, - {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b09843123425337d2efee5c8ff6519e4dfc7b044db66c8bd560517fc1070a157"}, - {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dca10174a3bd482d969a2d12d0aec2fdd63fb974e255ec0147e36a516a2d68a"}, - {file = "ujson-5.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202ae52f4a53f03c42ead6d046b1a146517e93bd757f517bdeef0a26228e0260"}, - {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7a4bed7bd7b288cf73ba47bda27fdd1d78ef6906831489e7f296aef9e786eccb"}, - {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d423956f8dfd98a075c9338b886414b6e3c2817dbf67935797466c998af39936"}, - {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:083c1078e4de3a39019e590c43865b17e07a763fee25b012e650bb4f42c89703"}, - {file = "ujson-5.1.0-cp38-cp38-win32.whl", hash = "sha256:31671ad99f0395eb881d698f2871dc64ff00fbd4380c5d9bfd8bff3d4c8f8d88"}, - {file = "ujson-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:994eaf4369e6bc24258f59fe8c6345037abcf24557571814e27879851c4353aa"}, - {file = "ujson-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00d6ea9702c2eaeaf1a826934eaba1b4c609c873379bf54e36ba7b7e128edf94"}, - {file = "ujson-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a53c4fe8e1c067e6c98b4526e982ed9486f08578ad8eb5f0e94f8cadf0c1d911"}, - {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:368f855779fded560724a6448838304621f498113a116d66bc5ed5ad5ad3ca92"}, - {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd97e45a0f450ba2c43cda18147e54b8e41e886c22e3506c62f7d61e9e53b0d"}, - {file = "ujson-5.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caeadbf95ce277f1f8f4f71913bc20c01f49fc9228f238920f9ff6f7645d2a5f"}, - {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:681fed63c948f757466eeb3aea98873e2ab8b2b18e9020c96a97479a513e2018"}, - {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6fc4376266ae67f6d8f9e69386ab950eb84ba345c6fdbeb1884fa5b773c8c76b"}, - {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:585271d6ad545a2ccfc237582f70c160e627735c89d0ca2bde24afa321bc0750"}, - {file = "ujson-5.1.0-cp39-cp39-win32.whl", hash = "sha256:b631af423e6d5d35f9f37fbcc4fbdb6085abc1c441cf864c64b7fbb5b150faf7"}, - {file = "ujson-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:08265db5ccff8b521ff68aee13a417d68cca784d7e711d961b92fda6ccffcc4f"}, - {file = "ujson-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e2b1c372583eb4363b42e21222d3a18116a41973781d502d61e1b0daf4b8352f"}, - {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51142c9d40439f299594e399bef8892a16586ded54c88d3af926865ca221a177"}, - {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ba8be1717b1867a85b2413a8585bad0e4507a22d6af2c244e1c74151f6d5cc0"}, - {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b26d9d6eb9a0979d37f28c715e717a409c9e03163e5cd8fa73aab806351ab5"}, - {file = "ujson-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b2c7e4afde0d36926b091fa9613b18b65e911fcaa60024e8721f2dcfedc25329"}, - {file = "ujson-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:110633a8dda6c8ca78090292231e15381f8b2423e998399d4bc5f135149c722b"}, - {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdac161127ef8e0889180a4c07475457c55fe0bbd644436d8f4c7ef07565d653"}, - {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:452990c2b18445a7379a45873527d2ec47789b9289c13a17a3c1cc76b9641126"}, - {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5304ad25d100d50b5bc8513ef110335df678f66c7ccf3d4728c0c3aa69e08e0c"}, - {file = "ujson-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ce620a6563b21aa3fbb1658bc1bfddb484a6dad542de1efb5121eb7bb4f2b93a"}, - {file = "ujson-5.1.0.tar.gz", hash = "sha256:a88944d2f99db71a3ca0c63d81f37e55b660edde0b07216fb65a3e46403ef004"}, + {file = "ujson-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:754e9da96a24535ae5ab2a52e1d1dfc65a6a717c14063855b83f327fdf2173ea"}, + {file = "ujson-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6227597d0201ceadc902d1a8edaffaeb244050b197368ed25e6f6be0df170a6f"}, + {file = "ujson-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be909514a47b6272e34cd1213feee324ca35a354e07f1ae3aba12d3694a5279f"}, + {file = "ujson-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d495431c80dc0048c4551a0d6cdbf1209e2d274f47c3f72415c91842eeb68"}, + {file = "ujson-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d1ed3897e45477b2a4a1371186df299b13938d4d44d850953a4bb0ea4cb38f3"}, + {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:102b8eb5e15e6c5537426414d180c28dbf0489e51f7c22b706511ac84aae4458"}, + {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2c04456de1fc92cc7062904c176c74e6ea220469b949508be42e819646a28457"}, + {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a3c6798035b574ceba747de83f3223a622622b7ab77a24f8b4fbea2cb92f14b0"}, + {file = "ujson-5.2.0-cp310-cp310-win32.whl", hash = "sha256:27a254a150e46980608b16ef3b609e703173492cfa738f4644c81d7e7d77494c"}, + {file = "ujson-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c3f7578a62d9255650ef32e78d3345e98262e064c9ba3f205311b4c9eb507a6"}, + {file = "ujson-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:584c558c23ddc21f5b07d2c54ee527731bd9716101c27829023ab7f3ffbaa8fc"}, + {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57a87bbc77d66b8a2b74bab66357c3bb6194f5d248f1053fb8044787abde73f"}, + {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4555df1fe018806ba14cc38786269c8e213930103c6d0ac81e506d09d1de7e"}, + {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d38c2a58c892c680080b22b59eebd77b7c6f4ae24361111fba115f9ed3651dcf"}, + {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a6f3ad3b11578bc4e25d5bd256c938fe2c7c015d8f504bc7835f127ed26a0818"}, + {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b5fcbaabf3d115cb816eb165f3fa5de5c5bc795473a554ae55620d134ddf2d36"}, + {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a1a55b3310632661a03ce68ccfb92264031aea21626d6fa5c8f6c32e769be7b6"}, + {file = "ujson-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:04a8c388b2d16316df3365c81f368955662581f6a4ff033e9aba2dd1ffc9e05e"}, + {file = "ujson-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d2357ce7d93eadd29b6efbe72228809948cc59ec6682c20fa6de08aeef1703f8"}, + {file = "ujson-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e53388fb092197cb8f956673792aca994872917d897ca42a0abf7a35e293575a"}, + {file = "ujson-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc71ead5706e81fdf1054c8c11e4aaab43527da450a2701213c20717852d1a51"}, + {file = "ujson-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:350a3010db0045e1306bbdf889d1bdaee9bb095856c317716f0a74108cf4afe9"}, + {file = "ujson-5.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acc874128baddeff908736db251597e4cbd007a384730377a59a61b08886599"}, + {file = "ujson-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c549d5a7652c3a0dd00ef6ff910fb01878bc116c66c94ac455a55cffa32cc229"}, + {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ed78a5b169ece75a1e1368935ce6ab051dcbcd5c158b9796b2f1fa6cc467a651"}, + {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468d7d8dcbafc3fd40cc73e4a533a7a1d4f935f605c15ae6cac32c6d53c4c6aa"}, + {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dc5fd1d5b48edd3cc64e89ea94abe231509fdc938bdeafafe9aef3a05810159f"}, + {file = "ujson-5.2.0-cp38-cp38-win32.whl", hash = "sha256:49ce8521b0cdf210481bd89887fd1bd0a975f66088b1256dafc77c67c8ccb89d"}, + {file = "ujson-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:c519743a53bbe8aac6b743bcf50eb83057d1e0341e1ca8f8491f729a885af640"}, + {file = "ujson-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3671e1dfc49a4b4453d89fd7438aa9d7cca28afe329c70eba84e2a5778dbf3f"}, + {file = "ujson-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f735870f189bff1841c720115226894415ab6a7796dee8ab46bc767ea2e743"}, + {file = "ujson-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90de04391916c5adc7bbcc69bd778e263ed45cc83c070099cb07ed25068d6a12"}, + {file = "ujson-5.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c7712da662b92f80442a8efc0df09cea3a5efb42b0dd6a642e36b1b40a260d4"}, + {file = "ujson-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b1c3d2b22c040a81ff4e5927ce307919f7ac8bf888afded714d925edc8d0a4"}, + {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c5bbe6de6c9a5fe8dca56e36fb5c4a42e1a01d4aae1ac20cd8d7d82ccff9430"}, + {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:940f35e9a0969440621445dbb6adffaa2cea77d0262abc74fce78704120c4534"}, + {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6677bee8690c71f5e6cf519a6d8400f04fbd3ff9f6c50f35f1b664bc94546f84"}, + {file = "ujson-5.2.0-cp39-cp39-win32.whl", hash = "sha256:0b47a138203bb06bdac03b2a89ac9b2993fd32cb7daded06c966dd84300a5786"}, + {file = "ujson-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e991b7b3a08ac9e9d3a51589ef1c359c8d44ece730351cfac055684bf3787372"}, + {file = "ujson-5.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d1e5c635b7c3465ab8d2e3dc97c341ef1801c53a378f1d1d4cb934f6c90ec66c"}, + {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef868bf01851869a26c0ca5f88036903836c3a6b463c74d96b37f294f6bdeea4"}, + {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5e374e793b0a3c7df20ee4c8234e89859ddb2b2821cc3300ae94ab5b08fa6d0"}, + {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:080da13f81740c076e5f16c254a10d0e32f45d225a5e6b0687a86493cfcfbafb"}, + {file = "ujson-5.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:75a886bd89d8e5a004a39a6c5dc8a43bb7fcf05129d2dccd16a59602a612823a"}, + {file = "ujson-5.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:54ee7c46615b42f7ae9dca90f54d204a4d2041a4c926b08fffa953aa3a246e54"}, + {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b455a62bd20e890b2124a65df45313b4292dbea851ef38574e5e2de94691ad5"}, + {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1a619bad9894dad144184b735c98179c7d92d7b40fbda28eb8b0857bdfdf52"}, + {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:729af63e4de30c54b527b54b4100266f79833c1e8ba35e784f01b44c2aca88d8"}, + {file = "ujson-5.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25522c674b35c33f375586ac98d92ce731e79059424507ecbccbfcbce832d597"}, + {file = "ujson-5.2.0.tar.gz", hash = "sha256:163191b88842d874e081707d35de2e205e0e396e70fd068d1038879bca8b17ad"}, ] urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] virtualenv = [ - {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, - {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, + {file = "virtualenv-20.14.0-py2.py3-none-any.whl", hash = "sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66"}, + {file = "virtualenv-20.14.0.tar.gz", hash = "sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, ] diff --git a/pyproject.toml b/pyproject.toml index 5f398d19..36e2c8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,9 +31,9 @@ pymongo = "^3.12.1" flake8 = "^3.9.0" tox = "^3.23.0" Sphinx = "^4.1.2" -pytest = "^6.2.3" +pytest = "^7.1.1" pytest-benchmark = "^3.2.3" -hypothesis = "^6.9.2" +hypothesis = "^6.41.0" pendulum = "^2.1.2" isort = "^5.8.0" black = "^21.12-beta.0" diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index a2c998a1..10163e1e 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -398,7 +398,7 @@ def _structure_enum_literal(val, type): def structure_attrs_fromtuple(self, obj: Tuple[Any, ...], cl: Type[T]) -> T: """Load an attrs class from a sequence (tuple).""" conv_obj = [] # A list of converter parameters. - for a, value in zip(fields(cl), obj): # type: ignore + for a, value in zip(fields(cl), obj): # We detect the type by the metadata. converted = self._structure_attribute(a, value) conv_obj.append(converted) diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index f36dfe97..75b958f9 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -395,15 +395,18 @@ def make_dict_structure_fn( internal_arg_parts[struct_handler_name] = handler if handler == converter._structure_call: internal_arg_parts[struct_handler_name] = t - invocation_lines.append(f"{struct_handler_name}(o['{kn}']),") + invocation_line = f"{struct_handler_name}(o['{kn}'])," else: type_name = f"__c_type_{an}" internal_arg_parts[type_name] = t - invocation_lines.append( - f"{struct_handler_name}(o['{kn}'], {type_name})," - ) + invocation_line = f"{struct_handler_name}(o['{kn}'], {type_name})," else: - invocation_lines.append(f"o['{kn}'],") + invocation_line = f"o['{kn}']," + + if a.kw_only: + ian = an if (is_dc or an[0] != "_") else an[1:] + invocation_line = f"{ian}={invocation_line}" + invocation_lines.append(invocation_line) # The second loop is for optional args. if non_required: diff --git a/tests/__init__.py b/tests/__init__.py index 807ea623..52bc22e5 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -18,6 +18,7 @@ import attr from attr import NOTHING, make_class +from attr._make import _CountingAttr from hypothesis import HealthCheck, settings from hypothesis import strategies as st @@ -28,6 +29,9 @@ if "CI" in os.environ: settings.load_profile("CI") +PosArg = Any +PosArgs = Tuple[Any] +KwArgs = Dict[str, Any] primitive_strategies = st.sampled_from( [ @@ -152,22 +156,31 @@ def gen_attr_names(): yield outer + inner -def _create_hyp_class(attrs_and_strategy, frozen=None): +def _create_hyp_class( + attrs_and_strategy: List[Tuple[_CountingAttr, st.SearchStrategy[PosArgs]]], + frozen=None, +): """ A helper function for Hypothesis to generate attrs classes. - The result is a tuple: an attrs class, and a tuple of values to - instantiate it. + The result is a tuple: an attrs class, a tuple of values to + instantiate it, and a kwargs dict for kw-only attributes. """ def key(t): - return t[0].default is not NOTHING + return (t[0].default is not NOTHING, t[0].kw_only) attrs_and_strat = sorted(attrs_and_strategy, key=key) attrs = [a[0] for a in attrs_and_strat] for i, a in enumerate(attrs): a.counter = i - vals = tuple((a[1]) for a in attrs_and_strat) + vals = tuple((a[1]) for a in attrs_and_strat if not a[0].kw_only) + kwargs = {} + for attr_name, attr_and_strat in zip(gen_attr_names(), attrs_and_strat): + if attr_and_strat[0].kw_only: + if attr_name.startswith("_"): + attr_name = attr_name[1:] + kwargs[attr_name] = attr_and_strat[1] return st.tuples( st.builds( lambda f: make_class( @@ -176,6 +189,7 @@ def key(t): st.booleans() if frozen is None else st.just(frozen), ), st.tuples(*vals), + st.fixed_dictionaries(kwargs), ) @@ -267,7 +281,7 @@ def _create_hyp_nested_strategy(simple_class_strategy): @st.composite -def bare_attrs(draw, defaults=None): +def bare_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields values appropriate for that attribute. @@ -275,11 +289,16 @@ def bare_attrs(draw, defaults=None): default = NOTHING if defaults is True or (defaults is None and draw(st.booleans())): default = None - return (attr.ib(default=default), st.just(None)) + return ( + attr.ib( + default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only + ), + st.just(None), + ) @st.composite -def int_attrs(draw, defaults=None): +def int_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields ints for that attribute. @@ -287,11 +306,16 @@ def int_attrs(draw, defaults=None): default = NOTHING if defaults is True or (defaults is None and draw(st.booleans())): default = draw(st.integers()) - return (attr.ib(default=default), st.integers()) + return ( + attr.ib( + default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only + ), + st.integers(), + ) @st.composite -def str_attrs(draw, defaults=None, type_annotations=None): +def str_attrs(draw, defaults=None, type_annotations=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields strs for that attribute. @@ -303,11 +327,18 @@ def str_attrs(draw, defaults=None, type_annotations=None): type = str else: type = None - return (attr.ib(default=default, type=type), st.text()) + return ( + attr.ib( + default=default, + type=type, + kw_only=draw(st.booleans()) if kw_only is None else kw_only, + ), + st.text(), + ) @st.composite -def float_attrs(draw, defaults=None): +def float_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields floats for that attribute. @@ -315,11 +346,16 @@ def float_attrs(draw, defaults=None): default = NOTHING if defaults is True or (defaults is None and draw(st.booleans())): default = draw(st.floats()) - return (attr.ib(default=default), st.floats(allow_nan=False)) + return ( + attr.ib( + default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only + ), + st.floats(allow_nan=False), + ) @st.composite -def dict_attrs(draw, defaults=None): +def dict_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields dictionaries for that attribute. The dictionaries map strings to integers. @@ -329,11 +365,16 @@ def dict_attrs(draw, defaults=None): if defaults is True or (defaults is None and draw(st.booleans())): default_val = draw(val_strat) default = attr.Factory(lambda: default_val) - return (attr.ib(default=default), val_strat) + return ( + attr.ib( + default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only + ), + val_strat, + ) @st.composite -def optional_attrs(draw, defaults=None): +def optional_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields values for that attribute. The strategy generates optional integers. @@ -343,33 +384,38 @@ def optional_attrs(draw, defaults=None): if defaults is True or (defaults is None and draw(st.booleans())): default = draw(val_strat) - return (attr.ib(default=default), val_strat) + return ( + attr.ib( + default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only + ), + val_strat, + ) -def simple_attrs(defaults=None): +def simple_attrs(defaults=None, kw_only=None): return ( - bare_attrs(defaults) - | int_attrs(defaults) - | str_attrs(defaults) - | float_attrs(defaults) - | dict_attrs(defaults) - | optional_attrs(defaults) + bare_attrs(defaults, kw_only=kw_only) + | int_attrs(defaults, kw_only=kw_only) + | str_attrs(defaults, kw_only=kw_only) + | float_attrs(defaults, kw_only=kw_only) + | dict_attrs(defaults, kw_only=kw_only) + | optional_attrs(defaults, kw_only=kw_only) ) -def lists_of_attrs(defaults=None, min_size=0): +def lists_of_attrs(defaults=None, min_size=0, kw_only=None): # Python functions support up to 255 arguments. - return st.lists(simple_attrs(defaults), min_size=min_size, max_size=10).map( - lambda l: sorted(l, key=lambda t: t[0]._default is not NOTHING) - ) + return st.lists( + simple_attrs(defaults, kw_only), min_size=min_size, max_size=10 + ).map(lambda l: sorted(l, key=lambda t: t[0]._default is not NOTHING)) -def simple_classes(defaults=None, min_attrs=0, frozen=None): +def simple_classes(defaults=None, min_attrs=0, frozen=None, kw_only=None): """ Return a strategy that yields tuples of simple classes and values to instantiate them. """ - return lists_of_attrs(defaults, min_size=min_attrs).flatmap( + return lists_of_attrs(defaults, min_size=min_attrs, kw_only=kw_only).flatmap( lambda attrs_and_strategy: _create_hyp_class(attrs_and_strategy, frozen=frozen) ) diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index 68716d42..eb4366c2 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -32,6 +32,7 @@ booleans, composite, dictionaries, + fixed_dictionaries, floats, frozensets, integers, @@ -49,13 +50,16 @@ is_39_or_later = sys.version_info[:2] >= (3, 9) PosArg = Any PosArgs = Tuple[Any] +KwArgs = Dict[str, Any] T = TypeVar("T") -def simple_typed_classes(defaults=None, min_attrs=0, frozen=False): +def simple_typed_classes( + defaults=None, min_attrs=0, frozen=False, kw_only=None +) -> SearchStrategy[Tuple[Type, PosArgs, KwArgs]]: """Yield tuples of (class, values).""" return lists_of_typed_attrs( - defaults, min_size=min_attrs, for_frozen=frozen + defaults, min_size=min_attrs, for_frozen=frozen, kw_only=kw_only ).flatmap(partial(_create_hyp_class, frozen=frozen)) @@ -67,16 +71,20 @@ def simple_typed_dataclasses(defaults=None, min_attrs=0, frozen=False): def simple_typed_classes_and_strats( - defaults=None, min_attrs=0 -) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: + defaults=None, min_attrs=0, kw_only=None +) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: """Yield tuples of (class, (strategies)).""" - return lists_of_typed_attrs(defaults, min_size=min_attrs).flatmap( + return lists_of_typed_attrs(defaults, min_size=min_attrs, kw_only=kw_only).flatmap( _create_hyp_class_and_strat ) def lists_of_typed_attrs( - defaults=None, min_size=0, for_frozen=False, allow_mutable_defaults=True + defaults=None, + min_size=0, + for_frozen=False, + allow_mutable_defaults=True, + kw_only=None, ) -> SearchStrategy[List[Tuple[_CountingAttr, SearchStrategy[PosArg]]]]: # Python functions support up to 255 arguments. return lists( @@ -84,60 +92,77 @@ def lists_of_typed_attrs( defaults, for_frozen=for_frozen, allow_mutable_defaults=allow_mutable_defaults, + kw_only=kw_only, ), min_size=min_size, max_size=50, - ).map(lambda l: sorted(l, key=lambda t: t[0]._default is not NOTHING)) + ).map( + lambda l: sorted(l, key=lambda t: (t[0]._default is not NOTHING, t[0].kw_only)) + ) def simple_typed_attrs( - defaults=None, for_frozen=False, allow_mutable_defaults=True + defaults=None, for_frozen=False, allow_mutable_defaults=True, kw_only=None ) -> SearchStrategy[Tuple[_CountingAttr, SearchStrategy[PosArgs]]]: if not is_39_or_later: res = ( - bare_typed_attrs(defaults) - | int_typed_attrs(defaults) - | str_typed_attrs(defaults) - | float_typed_attrs(defaults) - | frozenset_typed_attrs(defaults, legacy_types_only=True) - | homo_tuple_typed_attrs(defaults, legacy_types_only=True) + bare_typed_attrs(defaults, kw_only) + | int_typed_attrs(defaults, kw_only) + | str_typed_attrs(defaults, kw_only) + | float_typed_attrs(defaults, kw_only) + | frozenset_typed_attrs(defaults, legacy_types_only=True, kw_only=kw_only) + | homo_tuple_typed_attrs(defaults, legacy_types_only=True, kw_only=kw_only) ) if not for_frozen: res = ( res - | dict_typed_attrs(defaults, allow_mutable_defaults) + | dict_typed_attrs(defaults, allow_mutable_defaults, kw_only) | mutable_seq_typed_attrs( - defaults, allow_mutable_defaults, legacy_types_only=True + defaults, + allow_mutable_defaults, + legacy_types_only=True, + kw_only=kw_only, ) | seq_typed_attrs( - defaults, allow_mutable_defaults, legacy_types_only=True + defaults, + allow_mutable_defaults, + legacy_types_only=True, + kw_only=kw_only, ) | list_typed_attrs( - defaults, allow_mutable_defaults, legacy_types_only=True + defaults, + allow_mutable_defaults, + legacy_types_only=True, + kw_only=kw_only, ) | set_typed_attrs( - defaults, allow_mutable_defaults, legacy_types_only=True + defaults, + allow_mutable_defaults, + legacy_types_only=True, + kw_only=kw_only, ) ) else: res = ( - bare_typed_attrs(defaults) - | int_typed_attrs(defaults) - | str_typed_attrs(defaults) - | float_typed_attrs(defaults) - | frozenset_typed_attrs(defaults) - | homo_tuple_typed_attrs(defaults) + bare_typed_attrs(defaults, kw_only) + | int_typed_attrs(defaults, kw_only) + | str_typed_attrs(defaults, kw_only) + | float_typed_attrs(defaults, kw_only) + | frozenset_typed_attrs(defaults, kw_only=kw_only) + | homo_tuple_typed_attrs(defaults, kw_only=kw_only) ) if not for_frozen: res = ( res - | dict_typed_attrs(defaults, allow_mutable_defaults) - | new_dict_typed_attrs(defaults, allow_mutable_defaults) - | set_typed_attrs(defaults, allow_mutable_defaults) - | list_typed_attrs(defaults, allow_mutable_defaults) - | mutable_seq_typed_attrs(defaults, allow_mutable_defaults) - | seq_typed_attrs(defaults, allow_mutable_defaults) + | dict_typed_attrs(defaults, allow_mutable_defaults, kw_only) + | new_dict_typed_attrs(defaults, allow_mutable_defaults, kw_only) + | set_typed_attrs(defaults, allow_mutable_defaults, kw_only=kw_only) + | list_typed_attrs(defaults, allow_mutable_defaults, kw_only=kw_only) + | mutable_seq_typed_attrs( + defaults, allow_mutable_defaults, kw_only=kw_only + ) + | seq_typed_attrs(defaults, allow_mutable_defaults, kw_only=kw_only) ) return res @@ -146,44 +171,47 @@ def simple_typed_attrs( def _create_hyp_class( attrs_and_strategy: List[Tuple[_CountingAttr, SearchStrategy[PosArgs]]], frozen=False, -) -> SearchStrategy[Tuple[Type, PosArgs]]: +) -> SearchStrategy[Tuple[Type, PosArgs, KwArgs]]: """ A helper function for Hypothesis to generate attrs classes. - The result is a tuple: an attrs class, and a tuple of values to - instantiate it. + The result is a tuple: an attrs class, tuple of values to + instantiate it, and a kwargs dict for kw_only args. """ def key(t): - return t[0]._default is not attr.NOTHING + return (t[0]._default is not attr.NOTHING, t[0].kw_only) attrs_and_strat = sorted(attrs_and_strategy, key=key) attrs = [a[0] for a in attrs_and_strat] for i, a in enumerate(attrs): a.counter = i - vals = tuple((a[1]) for a in attrs_and_strat) + vals = tuple((a[1]) for a in attrs_and_strat if not a[0].kw_only) note(f"Class fields: {attrs}") + attrs_dict = OrderedDict(zip(gen_attr_names(), attrs)) + kwarg_strats = {} + for attr_name, attr_and_strat in zip(gen_attr_names(), attrs_and_strat): + if attr_and_strat[0].kw_only: + if attr_name.startswith("_"): + attr_name = attr_name[1:] + kwarg_strats[attr_name] = attr_and_strat[1] + return tuples( - just( - make_class( - "HypAttrsClass", - OrderedDict(zip(gen_attr_names(), attrs)), - frozen=frozen, - ) - ), + just(make_class("HypAttrsClass", attrs_dict, frozen=frozen)), tuples(*vals), + fixed_dictionaries(kwarg_strats), ) def _create_dataclass( attrs_and_strategy: List[Tuple[_CountingAttr, SearchStrategy[PosArgs]]], frozen=False, -) -> SearchStrategy[Tuple[Type, PosArgs]]: +) -> SearchStrategy[Tuple[Type, PosArgs, KwArgs]]: """ A helper function for Hypothesis to generate dataclasses. - The result is a tuple: a dataclass, and a tuple of values to - instantiate it. + The result is a tuple: a dataclass, a tuple of values to + instantiate it, and an empty dict (usually used for kw-only attrs attributes). """ def key(t): @@ -212,28 +240,36 @@ def key(t): ) ), tuples(*vals), + just({}), ) def _create_hyp_class_and_strat( attrs_and_strategy: List[Tuple[_CountingAttr, SearchStrategy[PosArg]]] -) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: +) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: def key(t): - return t[0].default is not attr.NOTHING + return (t[0].default is not attr.NOTHING, t[0].kw_only) attrs_and_strat = sorted(attrs_and_strategy, key=key) attrs = [a[0] for a in attrs_and_strat] for i, a in enumerate(attrs): a.counter = i - vals = tuple((a[1]) for a in attrs_and_strat) + vals = tuple((a[1]) for a in attrs_and_strat if not a[0].kw_only) + kwarg_strats = {} + for attr_name, attr_and_strat in zip(gen_attr_names(), attrs_and_strat): + if attr_and_strat[0].kw_only: + if attr_name.startswith("_"): + attr_name = attr_name[1:] + kwarg_strats[attr_name] = attr_and_strat[1] return tuples( just(make_class("HypClass", OrderedDict(zip(gen_attr_names(), attrs)))), just(tuples(*vals)), + just(fixed_dictionaries(kwarg_strats)), ) @composite -def bare_typed_attrs(draw, defaults=None): +def bare_typed_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields values appropriate for that attribute. @@ -241,11 +277,18 @@ def bare_typed_attrs(draw, defaults=None): default = attr.NOTHING if defaults is True or (defaults is None and draw(booleans())): default = None - return (attr.ib(type=Any, default=default), just(None)) + return ( + attr.ib( + type=Any, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + just(None), + ) @composite -def int_typed_attrs(draw, defaults=None): +def int_typed_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields ints for that attribute. @@ -253,11 +296,18 @@ def int_typed_attrs(draw, defaults=None): default = attr.NOTHING if defaults is True or (defaults is None and draw(booleans())): default = draw(integers()) - return (attr.ib(type=int, default=default), integers()) + return ( + attr.ib( + type=int, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + integers(), + ) @composite -def str_typed_attrs(draw, defaults=None): +def str_typed_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields strs for that attribute. @@ -265,11 +315,18 @@ def str_typed_attrs(draw, defaults=None): default = NOTHING if defaults is True or (defaults is None and draw(booleans())): default = draw(text()) - return (attr.ib(type=str, default=default), text()) + return ( + attr.ib( + type=str, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + text(), + ) @composite -def float_typed_attrs(draw, defaults=None): +def float_typed_attrs(draw, defaults=None, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields floats for that attribute. @@ -277,12 +334,19 @@ def float_typed_attrs(draw, defaults=None): default = attr.NOTHING if defaults is True or (defaults is None and draw(booleans())): default = draw(floats()) - return (attr.ib(type=float, default=default), floats()) + return ( + attr.ib( + type=float, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + floats(), + ) @composite def dict_typed_attrs( - draw, defaults=None, allow_mutable_defaults=True + draw, defaults=None, allow_mutable_defaults=True, kw_only=None ) -> SearchStrategy[Tuple[_CountingAttr, SearchStrategy]]: """ Generate a tuple of an attribute and a strategy that yields dictionaries @@ -298,11 +362,20 @@ def dict_typed_attrs( else: default = default_val type = draw(sampled_from([Dict[str, int], Dict, dict])) - return (attr.ib(type=type, default=default), val_strat) + return ( + attr.ib( + type=type, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + val_strat, + ) @composite -def new_dict_typed_attrs(draw, defaults=None, allow_mutable_defaults=True): +def new_dict_typed_attrs( + draw, defaults=None, allow_mutable_defaults=True, kw_only=None +): """ Generate a tuple of an attribute and a strategy that yields dictionaries for that attribute. The dictionaries map strings to integers. @@ -320,12 +393,23 @@ def new_dict_typed_attrs(draw, defaults=None, allow_mutable_defaults=True): else: default = default_val - return (attr.ib(type=dict[str, int], default=default), val_strat) + return ( + attr.ib( + type=dict[str, int], + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + val_strat, + ) @composite def set_typed_attrs( - draw: DrawFn, defaults=None, allow_mutable_defaults=True, legacy_types_only=False + draw: DrawFn, + defaults=None, + allow_mutable_defaults=True, + legacy_types_only=False, + kw_only=None, ): """ Generate a tuple of an attribute and a strategy that yields sets @@ -349,11 +433,20 @@ def set_typed_attrs( else [set, Set[int], MutableSet[int]] ) ) - return (attr.ib(type=type, default=default), val_strat) + return ( + attr.ib( + type=type, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + val_strat, + ) @composite -def frozenset_typed_attrs(draw: DrawFn, defaults=None, legacy_types_only=False): +def frozenset_typed_attrs( + draw: DrawFn, defaults=None, legacy_types_only=False, kw_only=None +): """ Generate a tuple of an attribute and a strategy that yields frozensets for that attribute. The frozensets contain integers. @@ -369,12 +462,23 @@ def frozenset_typed_attrs(draw: DrawFn, defaults=None, legacy_types_only=False): else [frozenset, FrozenSet[int], FrozenSet] ) ) - return (attr.ib(type=type, default=default), val_strat) + return ( + attr.ib( + type=type, + default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, + ), + val_strat, + ) @composite def list_typed_attrs( - draw: DrawFn, defaults=None, allow_mutable_defaults=True, legacy_types_only=False + draw: DrawFn, + defaults=None, + allow_mutable_defaults=True, + legacy_types_only=False, + kw_only=None, ): """ Generate a tuple of an attribute and a strategy that yields lists @@ -400,6 +504,7 @@ def list_typed_attrs( ) ), default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, ), val_strat, ) @@ -407,7 +512,11 @@ def list_typed_attrs( @composite def seq_typed_attrs( - draw, defaults=None, allow_mutable_defaults=True, legacy_types_only=False + draw, + defaults=None, + allow_mutable_defaults=True, + legacy_types_only=False, + kw_only=None, ): """ Generate a tuple of an attribute and a strategy that yields lists @@ -428,6 +537,7 @@ def seq_typed_attrs( attr.ib( type=AbcSequence[int] if not legacy_types_only else Sequence[int], default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, ), val_strat, ) @@ -435,7 +545,11 @@ def seq_typed_attrs( @composite def mutable_seq_typed_attrs( - draw, defaults=None, allow_mutable_defaults=True, legacy_types_only=False + draw, + defaults=None, + allow_mutable_defaults=True, + legacy_types_only=False, + kw_only=None, ): """ Generate a tuple of an attribute and a strategy that yields lists @@ -458,13 +572,14 @@ def mutable_seq_typed_attrs( if not legacy_types_only else MutableSequence[float], default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, ), val_strat, ) @composite -def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False): +def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False, kw_only=None): """ Generate a tuple of an attribute and a strategy that yields homogenous tuples for that attribute. The tuples contain strings. @@ -483,80 +598,96 @@ def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False): ) ), default=default, + kw_only=draw(booleans()) if kw_only is None else kw_only, ), val_strat, ) def just_class( - tup: Tuple[List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs]], - defaults: PosArgs, + tup: Tuple[ + List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] + ], + defaults: tuple[PosArgs, KwArgs], ): nested_cl = tup[1][0] nested_cl_args = tup[1][1] - default = attr.Factory(lambda: nested_cl(*defaults)) + nested_cl_kwargs = tup[1][2] + default = attr.Factory(lambda: nested_cl(*defaults[0], **defaults[1])) combined_attrs = list(tup[0]) combined_attrs.append( - (attr.ib(type=nested_cl, default=default), just(nested_cl(*nested_cl_args))) + ( + attr.ib(type=nested_cl, default=default), + just(nested_cl(*nested_cl_args, **nested_cl_kwargs)), + ) ) return _create_hyp_class_and_strat(combined_attrs) def list_of_class( - tup: Tuple[List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs]], - defaults: PosArgs, -) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: + tup: Tuple[ + List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] + ], + defaults: tuple[PosArgs, KwArgs], +) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: nested_cl = tup[1][0] nested_cl_args = tup[1][1] - default = attr.Factory(lambda: [nested_cl(*defaults)]) + nested_cl_kwargs = tup[1][2] + default = attr.Factory(lambda: [nested_cl(*defaults[0], **defaults[1])]) combined_attrs = list(tup[0]) combined_attrs.append( ( attr.ib(type=List[nested_cl], default=default), - just([nested_cl(*nested_cl_args)]), + just([nested_cl(*nested_cl_args, **nested_cl_kwargs)]), ) ) return _create_hyp_class_and_strat(combined_attrs) def new_list_of_class( - tup: Tuple[List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs]], - defaults: PosArgs, + tup: Tuple[ + List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] + ], + defaults: tuple[PosArgs, KwArgs], ): """Uses the new 3.9 list type annotation.""" nested_cl = tup[1][0] nested_cl_args = tup[1][1] - default = attr.Factory(lambda: [nested_cl(*defaults)]) + nested_cl_kwargs = tup[1][2] + default = attr.Factory(lambda: [nested_cl(*defaults[0], **defaults[1])]) combined_attrs = list(tup[0]) combined_attrs.append( ( attr.ib(type=list[nested_cl], default=default), - just([nested_cl(*nested_cl_args)]), + just([nested_cl(*nested_cl_args, **nested_cl_kwargs)]), ) ) return _create_hyp_class_and_strat(combined_attrs) def dict_of_class( - tup: Tuple[List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs]], - defaults: PosArgs, + tup: Tuple[ + List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] + ], + defaults: tuple[PosArgs, KwArgs], ): nested_cl = tup[1][0] nested_cl_args = tup[1][1] - default = attr.Factory(lambda: {"cls": nested_cl(*defaults)}) + nested_cl_kwargs = tup[1][2] + default = attr.Factory(lambda: {"cls": nested_cl(*defaults[0], **defaults[1])}) combined_attrs = list(tup[0]) combined_attrs.append( ( attr.ib(type=Dict[str, nested_cl], default=default), - just({"cls": nested_cl(*nested_cl_args)}), + just({"cls": nested_cl(*nested_cl_args, **nested_cl_kwargs)}), ) ) return _create_hyp_class_and_strat(combined_attrs) def _create_hyp_nested_strategy( - simple_class_strategy: SearchStrategy, -) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: + simple_class_strategy: SearchStrategy, kw_only=None +) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: """ Create a recursive attrs class. Given a strategy for building (simpler) classes, create and return @@ -571,52 +702,58 @@ def _create_hyp_nested_strategy( Tuple[ List[Tuple[_CountingAttr, PosArgs]], Tuple[Type, SearchStrategy[PosArgs]], ] - ] = tuples(lists_of_typed_attrs(), simple_class_strategy) + ] = tuples(lists_of_typed_attrs(kw_only=kw_only), simple_class_strategy) return nested_classes(attrs_and_classes) @composite def nested_classes( - draw: Callable[[SearchStrategy[T]], T], + draw: DrawFn, attrs_and_classes: SearchStrategy[ Tuple[ List[Tuple[_CountingAttr, SearchStrategy]], - Tuple[Type, SearchStrategy[PosArgs]], + Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]], ] ], -) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: +) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: attrs, class_and_strat = draw(attrs_and_classes) - cls, strat = class_and_strat - defaults = tuple(draw(strat)) + cls, strat, kw_strat = class_and_strat + pos_defs = tuple(draw(strat)) + kwarg_defs = draw(kw_strat) init_vals = tuple(draw(strat)) + init_kwargs = draw(kw_strat) if is_39_or_later: return draw( - list_of_class((attrs, (cls, init_vals)), defaults) - | new_list_of_class((attrs, (cls, init_vals)), defaults) - | dict_of_class((attrs, (cls, init_vals)), defaults) - | just_class((attrs, (cls, init_vals)), defaults) + list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + | new_list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + | dict_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + | just_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) ) else: return draw( - list_of_class((attrs, (cls, init_vals)), defaults) - | dict_of_class((attrs, (cls, init_vals)), defaults) - | just_class((attrs, (cls, init_vals)), defaults) + list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + | dict_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + | just_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) ) def nested_typed_classes_and_strat( - defaults=None, min_attrs=0 + defaults=None, min_attrs=0, kw_only=None ) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs]]]: return recursive( - simple_typed_classes_and_strats(defaults=defaults, min_attrs=min_attrs), - _create_hyp_nested_strategy, + simple_typed_classes_and_strats( + defaults=defaults, min_attrs=min_attrs, kw_only=kw_only + ), + partial(_create_hyp_nested_strategy, kw_only=kw_only), ) @composite -def nested_typed_classes(draw, defaults=None, min_attrs=0): - cl, strat = draw( - nested_typed_classes_and_strat(defaults=defaults, min_attrs=min_attrs) +def nested_typed_classes(draw, defaults=None, min_attrs=0, kw_only=None): + cl, strat, kwarg_strat = draw( + nested_typed_classes_and_strat( + defaults=defaults, min_attrs=min_attrs, kw_only=kw_only + ) ) - return cl, draw(strat) + return cl, draw(strat), draw(kwarg_strat) diff --git a/tests/metadata/test_genconverter.py b/tests/metadata/test_genconverter.py index 8a7a5b52..72fb8ac5 100644 --- a/tests/metadata/test_genconverter.py +++ b/tests/metadata/test_genconverter.py @@ -32,29 +32,58 @@ unstructure_strats = sampled_from(list(UnstructureStrategy)) -@given( - simple_typed_classes() | simple_typed_dataclasses(), unstructure_strats, booleans() -) -def test_simple_roundtrip(cls_and_vals, strat, detailed_validation): +@given(simple_typed_classes() | simple_typed_dataclasses(), booleans()) +def test_simple_roundtrip(cls_and_vals, detailed_validation): """ Simple classes with metadata can be unstructured and restructured. """ - converter = Converter(unstruct_strat=strat, detailed_validation=detailed_validation) - cl, vals = cls_and_vals + converter = Converter(detailed_validation=detailed_validation) + cl, vals, kwargs = cls_and_vals + inst = cl(*vals, **kwargs) + unstructured = converter.unstructure(inst) + assert "Hyp" not in repr(unstructured) + assert inst == converter.structure(unstructured, cl) + + +@given(simple_typed_classes(kw_only=False) | simple_typed_dataclasses(), booleans()) +def test_simple_roundtrip_tuple(cls_and_vals, detailed_validation): + """ + Simple classes with metadata can be unstructured and restructured. + """ + converter = Converter( + unstruct_strat=UnstructureStrategy.AS_TUPLE, + detailed_validation=detailed_validation, + ) + cl, vals, _ = cls_and_vals inst = cl(*vals) unstructured = converter.unstructure(inst) assert "Hyp" not in repr(unstructured) assert inst == converter.structure(unstructured, cl) -@given(simple_typed_attrs(defaults=True), unstructure_strats) -def test_simple_roundtrip_defaults(attr_and_vals, strat): +@given(simple_typed_attrs(defaults=True)) +def test_simple_roundtrip_defaults(attr_and_vals): """ Simple classes with metadata can be unstructured and restructured. """ a, _ = attr_and_vals cl = make_class("HypClass", {"a": a}) - converter = Converter(unstruct_strat=strat) + converter = Converter() + inst = cl() + assert converter.unstructure(converter.structure({}, cl)) == converter.unstructure( + inst + ) + assert inst == converter.structure(converter.unstructure(inst), cl) + + +@given(simple_typed_attrs(defaults=True, kw_only=False)) +def test_simple_roundtrip_defaults_tuple(attr_and_vals): + """ + Simple classes with metadata can be unstructured and restructured. + """ + a, _ = attr_and_vals + cl = make_class("HypClass", {"a": a}) + converter = Converter(unstruct_strat=UnstructureStrategy.AS_TUPLE) inst = cl() assert converter.unstructure(converter.structure({}, cl)) == converter.unstructure( inst @@ -68,8 +97,9 @@ def test_simple_roundtrip_with_extra_keys_forbidden(cls_and_vals, strat): Simple classes can be unstructured and restructured with forbid_extra_keys=True. """ converter = Converter(unstruct_strat=strat, forbid_extra_keys=True) - cl, vals = cls_and_vals - inst = cl(*vals) + cl, vals, kwargs = cls_and_vals + assume(strat is UnstructureStrategy.AS_DICT or not kwargs) + inst = cl(*vals, **kwargs) unstructured = converter.unstructure(inst) assert "Hyp" not in repr(unstructured) assert inst == converter.structure(unstructured, cl) @@ -81,8 +111,8 @@ def test_forbid_extra_keys(cls_and_vals): Restructuring fails when extra keys are present (when configured) """ converter = Converter(forbid_extra_keys=True) - cl, vals = cls_and_vals - inst = cl(*vals) + cl, vals, kwargs = cls_and_vals + inst = cl(*vals, **kwargs) unstructured = converter.unstructure(inst) bad_key = list(unstructured)[0] + "A" if unstructured else "Hyp" while bad_key in unstructured: @@ -158,13 +188,28 @@ class A: assert cve.value.exceptions[0].extra_fields == {"b"} -@given(nested_typed_classes(defaults=True, min_attrs=1), unstructure_strats, booleans()) -def test_nested_roundtrip(cls_and_vals, strat, omit_if_default): +@given(nested_typed_classes(defaults=True, min_attrs=1), booleans()) +def test_nested_roundtrip(cls_and_vals, omit_if_default): """ Nested classes with metadata can be unstructured and restructured. """ - converter = Converter(unstruct_strat=strat, omit_if_default=omit_if_default) - cl, vals = cls_and_vals + converter = Converter(omit_if_default=omit_if_default) + cl, vals, kwargs = cls_and_vals + # Vals are a tuple, convert into a dictionary. + inst = cl(*vals, **kwargs) + unstructured = converter.unstructure(inst) + assert inst == converter.structure(unstructured, cl) + + +@given(nested_typed_classes(defaults=True, min_attrs=1, kw_only=False), booleans()) +def test_nested_roundtrip_tuple(cls_and_vals, omit_if_default): + """ + Nested classes with metadata can be unstructured and restructured. + """ + converter = Converter( + unstruct_strat=UnstructureStrategy.AS_TUPLE, omit_if_default=omit_if_default + ) + cl, vals, _ = cls_and_vals # Vals are a tuple, convert into a dictionary. inst = cl(*vals) unstructured = converter.unstructure(inst) @@ -182,8 +227,9 @@ def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): Classes with union fields can be unstructured and structured. """ converter = Converter(unstruct_strat=strat) - cl_a, vals_a = cl_and_vals_a - cl_b, _ = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, _, _ = cl_and_vals_b + assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a) a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -196,7 +242,7 @@ def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): class C(object): a = attr.ib(type=Union[cl_a, cl_b]) - inst = C(a=cl_a(*vals_a)) + inst = C(a=cl_a(*vals_a, **kwargs_a)) if strat is UnstructureStrategy.AS_DICT: unstructured = converter.unstructure(inst) @@ -226,8 +272,8 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): Classes with union fields can be unstructured and structured. """ converter = Converter(unstruct_strat=strat) - cl_a, vals_a = cl_and_vals_a - cl_b, _ = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, _, _ = cl_and_vals_b a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -240,7 +286,7 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): class C: a: cl_a | cl_b - inst = C(a=cl_a(*vals_a)) + inst = C(a=cl_a(*vals_a, **kwargs_a)) if strat is UnstructureStrategy.AS_DICT: unstructured = converter.unstructure(inst) @@ -264,13 +310,13 @@ def test_optional_field_roundtrip(cl_and_vals): Classes with optional fields can be unstructured and structured. """ converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals @attr.s class C(object): a = attr.ib(type=Optional[cl]) - inst = C(a=cl(*vals)) + inst = C(a=cl(*vals, **kwargs)) assert inst == converter.structure(converter.unstructure(inst), C) inst = C(a=None) @@ -307,12 +353,12 @@ def test_omit_default_roundtrip(cl_and_vals): Omit default on the converter works. """ converter = Converter(omit_if_default=True) - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals @attr.s class C(object): a: int = attr.ib(default=1) - b: cl = attr.ib(factory=lambda: cl(*vals)) + b: cl = attr.ib(factory=lambda: cl(*vals, **kwargs)) inst = C() unstructured = converter.unstructure(inst) @@ -331,9 +377,9 @@ def test_type_overrides(cl_and_vals): Type overrides on the GenConverter work. """ converter = Converter(type_overrides={int: override(omit_if_default=True)}) - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals - inst = cl(*vals) + inst = cl(*vals, **kwargs) unstructured = converter.unstructure(inst) for field, val in zip(fields(cl), vals): @@ -493,12 +539,12 @@ def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type_and_annotation test_val = ("test", 1) - for cl, _ in cls_and_vals: + for cl, _, _ in cls_and_vals: converter.register_unstructure_hook(cl, lambda _: test_val) break # Just register the first class. seq_type, annotation = seq_type_and_annotation - inputs = seq_type(cl(*vals) for cl, vals in cls_and_vals) + inputs = seq_type(cl(*vals, **kwargs) for cl, vals, kwargs in cls_and_vals) outputs = converter.unstructure( inputs, unstructure_as=annotation[cl] diff --git a/tests/metadata/test_roundtrips.py b/tests/metadata/test_roundtrips.py index 177939e0..846457b2 100644 --- a/tests/metadata/test_roundtrips.py +++ b/tests/metadata/test_roundtrips.py @@ -5,14 +5,14 @@ import pytest from attr import define, fields, make_class from hypothesis import HealthCheck, assume, given, settings -from hypothesis.strategies import sampled_from +from hypothesis.strategies import just, one_of from cattrs import Converter, UnstructureStrategy from cattrs._compat import is_py310_plus from . import nested_typed_classes, simple_typed_attrs, simple_typed_classes -unstructure_strats = sampled_from(list(UnstructureStrategy)) +unstructure_strats = one_of(just(s) for s in UnstructureStrategy) @given(simple_typed_classes(), unstructure_strats) @@ -21,17 +21,19 @@ def test_simple_roundtrip(cls_and_vals, strat): Simple classes with metadata can be unstructured and restructured. """ converter = Converter(unstruct_strat=strat) - cl, vals = cls_and_vals - inst = cl(*vals) + cl, vals, kwargs = cls_and_vals + assume(strat is UnstructureStrategy.AS_DICT or not kwargs) + inst = cl(*vals, **kwargs) assert inst == converter.structure(converter.unstructure(inst), cl) @given(simple_typed_attrs(defaults=True), unstructure_strats) -def test_simple_roundtrip_defaults(cls_and_vals, strat): +def test_simple_roundtrip_defaults(attr_and_strat, strat): """ Simple classes with metadata can be unstructured and restructured. """ - a, _ = cls_and_vals + a, _ = attr_and_strat + assume(strat is UnstructureStrategy.AS_DICT or not a.kw_only) cl = make_class("HypClass", {"a": a}) converter = Converter(unstruct_strat=strat) inst = cl() @@ -41,13 +43,26 @@ def test_simple_roundtrip_defaults(cls_and_vals, strat): assert inst == converter.structure(converter.unstructure(inst), cl) -@given(nested_typed_classes(), unstructure_strats) -def test_nested_roundtrip(cls_and_vals, strat): +@given(nested_typed_classes()) +def test_nested_roundtrip(cls_and_vals): """ Nested classes with metadata can be unstructured and restructured. """ - converter = Converter(unstruct_strat=strat) - cl, vals = cls_and_vals + converter = Converter() + cl, vals, kwargs = cls_and_vals + # Vals are a tuple, convert into a dictionary. + inst = cl(*vals, **kwargs) + assert inst == converter.structure(converter.unstructure(inst), cl) + + +@given(nested_typed_classes(kw_only=False)) +def test_nested_roundtrip_tuple(cls_and_vals): + """ + Nested classes with metadata can be unstructured and restructured. + """ + converter = Converter(unstruct_strat=UnstructureStrategy.AS_TUPLE) + cl, vals, kwargs = cls_and_vals + assert not kwargs # Vals are a tuple, convert into a dictionary. inst = cl(*vals) assert inst == converter.structure(converter.unstructure(inst), cl) @@ -64,8 +79,9 @@ def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): Classes with union fields can be unstructured and structured. """ converter = Converter(unstruct_strat=strat) - cl_a, vals_a = cl_and_vals_a - cl_b, vals_b = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a) + cl_b, vals_b, _ = cl_and_vals_b a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -78,7 +94,7 @@ def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): class C(object): a = attr.ib(type=Union[cl_a, cl_b]) - inst = C(a=cl_a(*vals_a)) + inst = C(a=cl_a(*vals_a, **kwargs_a)) if strat is UnstructureStrategy.AS_DICT: assert inst == converter.structure(converter.unstructure(inst), C) @@ -142,13 +158,13 @@ def test_optional_field_roundtrip(cl_and_vals): Classes with optional fields can be unstructured and structured. """ converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals @attr.s class C(object): a = attr.ib(type=Optional[cl]) - inst = C(a=cl(*vals)) + inst = C(a=cl(*vals, **kwargs)) assert inst == converter.structure(converter.unstructure(inst), C) inst = C(a=None) diff --git a/tests/test_disambigutors.py b/tests/test_disambigutors.py index d86abeac..464f8165 100644 --- a/tests/test_disambigutors.py +++ b/tests/test_disambigutors.py @@ -58,7 +58,7 @@ class F: @given(simple_classes(defaults=False)) def test_fallback(cl_and_vals): """The fallback case works.""" - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals assume(attr.fields(cl)) # At least one field. @@ -69,7 +69,7 @@ class A(object): fn = create_uniq_field_dis_func(A, cl) assert fn({}) is A - assert fn(attr.asdict(cl(*vals))) is cl + assert fn(attr.asdict(cl(*vals, **kwargs))) is cl attr_names = {a.name for a in attr.fields(cl)} @@ -81,8 +81,8 @@ class A(object): @given(simple_classes(), simple_classes()) def test_disambiguation(cl_and_vals_a, cl_and_vals_b): """Disambiguation should work when there are unique required fields.""" - cl_a, vals_a = cl_and_vals_a - cl_b, vals_b = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, vals_b, kwargs_b = cl_and_vals_b req_a = {a.name for a in attr.fields(cl_a)} req_b = {a.name for a in attr.fields(cl_b)} @@ -98,4 +98,4 @@ def test_disambiguation(cl_and_vals_a, cl_and_vals_b): fn = create_uniq_field_dis_func(cl_a, cl_b) - assert fn(attr.asdict(cl_a(*vals_a))) is cl_a + assert fn(attr.asdict(cl_a(*vals_a, **kwargs_a))) is cl_a diff --git a/tests/test_gen_dict.py b/tests/test_gen_dict.py index 91a6aa72..1e83e7e0 100644 --- a/tests/test_gen_dict.py +++ b/tests/test_gen_dict.py @@ -1,11 +1,13 @@ """Tests for generated dict functions.""" +from typing import Type + import pytest from attr import Factory, define, field from attr._make import NOTHING from hypothesis import assume, given -from hypothesis.strategies._internal.core import data, sampled_from +from hypothesis.strategies import data, just, one_of, sampled_from -from cattrs import Converter +from cattrs import Converter, GenConverter from cattrs._compat import adapted_fields, fields from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override @@ -21,10 +23,10 @@ @given(nested_classes | simple_classes()) def test_unmodified_generated_unstructuring(cl_and_vals): converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals fn = make_dict_unstructure_fn(cl, converter) - inst = cl(*vals) + inst = cl(*vals, **kwargs) res_expected = converter.unstructure(inst) @@ -39,10 +41,10 @@ def test_unmodified_generated_unstructuring(cl_and_vals): def test_nodefs_generated_unstructuring(cl_and_vals): """Test omitting default values on a per-attribute basis.""" converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals attr_is_default = False - for attr, val in zip(cl.__attrs_attrs__, vals): + for attr, val in zip([a for a in cl.__attrs_attrs__ if not a.kw_only], vals): if attr.default is not NOTHING: fn = make_dict_unstructure_fn( cl, converter, **{attr.name: override(omit_if_default=True)} @@ -55,7 +57,7 @@ def test_nodefs_generated_unstructuring(cl_and_vals): converter.register_unstructure_hook(cl, fn) - inst = cl(*vals) + inst = cl(*vals, **kwargs) res = converter.unstructure(inst) @@ -63,11 +65,11 @@ def test_nodefs_generated_unstructuring(cl_and_vals): assert attr.name not in res -@given(nested_classes | simple_classes()) -def test_nodefs_generated_unstructuring_cl(cl_and_vals): +@given(one_of(just(Converter), just(GenConverter)), nested_classes | simple_classes()) +def test_nodefs_generated_unstructuring_cl(converter_cls: Type[Converter], cl_and_vals): """Test omitting default values on a per-class basis.""" - converter = Converter() - cl, vals = cl_and_vals + converter = converter_cls() + cl, vals, kwargs = cl_and_vals for attr, val in zip(cl.__attrs_attrs__, vals): if attr.default is not NOTHING: @@ -79,7 +81,7 @@ def test_nodefs_generated_unstructuring_cl(cl_and_vals): cl, make_dict_unstructure_fn(cl, converter, _cattrs_omit_if_default=True) ) - inst = cl(*vals) + inst = cl(*vals, **kwargs) res = converter.unstructure(inst) @@ -104,14 +106,17 @@ def test_nodefs_generated_unstructuring_cl(cl_and_vals): assert attr.name in res -@given(nested_classes | simple_classes() | simple_typed_dataclasses()) -def test_individual_overrides(cl_and_vals): +@given( + one_of(just(Converter), just(GenConverter)), + nested_classes | simple_classes() | simple_typed_dataclasses(), +) +def test_individual_overrides(converter_cls, cl_and_vals): """ Test omitting default values on a per-class basis, but with individual overrides. """ - converter = Converter() - cl, vals = cl_and_vals + converter = converter_cls() + cl, vals, kwargs = cl_and_vals for attr, val in zip(adapted_fields(cl), vals): if attr.default is not NOTHING: @@ -131,7 +136,7 @@ def test_individual_overrides(cl_and_vals): ), ) - inst = cl(*vals) + inst = cl(*vals, **kwargs) res = converter.unstructure(inst) assert "Hyp" not in repr(res) @@ -162,10 +167,10 @@ def test_individual_overrides(cl_and_vals): @given(nested_typed_classes() | simple_typed_classes() | simple_typed_dataclasses()) def test_unmodified_generated_structuring(cl_and_vals): converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals fn = make_dict_structure_fn(cl, converter) - inst = cl(*vals) + inst = cl(*vals, **kwargs) unstructured = converter.unstructure(inst) @@ -183,7 +188,7 @@ def test_unmodified_generated_structuring(cl_and_vals): ) def test_renaming(cl_and_vals, data): converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals attrs = fields(cl) to_replace = data.draw(sampled_from(attrs)) @@ -198,7 +203,7 @@ def test_renaming(cl_and_vals, data): converter.register_structure_hook(cl, s_fn) converter.register_unstructure_hook(cl, u_fn) - inst = cl(*vals) + inst = cl(*vals, **kwargs) raw = converter.unstructure(inst) diff --git a/tests/test_structure_attrs.py b/tests/test_structure_attrs.py index e5f89a7c..8fc62a2a 100644 --- a/tests/test_structure_attrs.py +++ b/tests/test_structure_attrs.py @@ -19,8 +19,8 @@ def test_structure_simple_from_dict(cl_and_vals): """Test structuring non-nested attrs classes dumped with asdict.""" converter = Converter() - cl, vals = cl_and_vals - obj = cl(*vals) + cl, vals, kwargs = cl_and_vals + obj = cl(*vals, **kwargs) dumped = asdict(obj) loaded = converter.structure(dumped, cl) @@ -32,8 +32,8 @@ def test_structure_simple_from_dict(cl_and_vals): def test_structure_simple_from_dict_default(cl_and_vals, data): """Test structuring non-nested attrs classes with default value.""" converter = Converter() - cl, vals = cl_and_vals - obj = cl(*vals) + cl, vals, kwargs = cl_and_vals + obj = cl(*vals, **kwargs) attrs_with_defaults = [a for a in fields(cl) if a.default is not NOTHING] to_remove = data.draw( lists(elements=sampled_from(attrs_with_defaults), unique=True) @@ -57,8 +57,8 @@ def test_structure_simple_from_dict_default(cl_and_vals, data): def test_roundtrip(cl_and_vals): """We dump the class, then we load it.""" converter = Converter() - cl, vals = cl_and_vals - obj = cl(*vals) + cl, vals, kwargs = cl_and_vals + obj = cl(*vals, **kwargs) dumped = converter.unstructure(obj) loaded = converter.structure(dumped, cl) @@ -66,13 +66,13 @@ def test_roundtrip(cl_and_vals): assert obj == loaded -@given(simple_classes()) +@given(simple_classes(kw_only=False)) def test_structure_tuple(cl_and_vals): """Test loading from a tuple, by registering the loader.""" converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals converter.register_structure_hook(cl, converter.structure_attrs_fromtuple) - obj = cl(*vals) + obj = cl(*vals, **kwargs) dumped = astuple(obj) loaded = converter.structure(dumped, cl) @@ -84,8 +84,8 @@ def test_structure_tuple(cl_and_vals): def test_structure_union(cl_and_vals_a, cl_and_vals_b): """Structuring of automatically-disambiguable unions works.""" converter = Converter() - cl_a, vals_a = cl_and_vals_a - cl_b, vals_b = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, vals_b, kwargs_b = cl_and_vals_b a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -93,7 +93,7 @@ def test_structure_union(cl_and_vals_a, cl_and_vals_b): common_names = a_field_names & b_field_names if len(a_field_names) > len(common_names): - obj = cl_a(*vals_a) + obj = cl_a(*vals_a, **kwargs_a) dumped = asdict(obj) res = converter.structure(dumped, Union[cl_a, cl_b]) assert isinstance(res, cl_a) @@ -104,8 +104,8 @@ def test_structure_union(cl_and_vals_a, cl_and_vals_b): def test_structure_union_none(cl_and_vals_a, cl_and_vals_b): """Structuring of automatically-disambiguable unions works.""" converter = Converter() - cl_a, vals_a = cl_and_vals_a - cl_b, _ = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, _, _ = cl_and_vals_b a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -113,7 +113,7 @@ def test_structure_union_none(cl_and_vals_a, cl_and_vals_b): common_names = a_field_names & b_field_names if len(a_field_names) > len(common_names): - obj = cl_a(*vals_a) + obj = cl_a(*vals_a, **kwargs_a) dumped = asdict(obj) res = converter.structure(dumped, Union[cl_a, cl_b, None]) assert isinstance(res, cl_a) @@ -124,15 +124,15 @@ def test_structure_union_none(cl_and_vals_a, cl_and_vals_b): def test_structure_union_explicit(cl_and_vals_a, cl_and_vals_b): """Structuring of manually-disambiguable unions works.""" converter = Converter() - cl_a, vals_a = cl_and_vals_a - cl_b, vals_b = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, vals_b, kwargs_b = cl_and_vals_b def dis(obj, _): return converter.structure(obj, cl_a) converter.register_structure_hook(Union[cl_a, cl_b], dis) - inst = cl_a(*vals_a) + inst = cl_a(*vals_a, **kwargs_a) assert inst == converter.structure(converter.unstructure(inst), Union[cl_a, cl_b]) diff --git a/tests/test_unstructure.py b/tests/test_unstructure.py index 6fb97d0d..a6afac36 100644 --- a/tests/test_unstructure.py +++ b/tests/test_unstructure.py @@ -1,7 +1,9 @@ """Tests for dumping.""" +from typing import Type + from attr import asdict, astuple from hypothesis import given -from hypothesis.strategies import data, lists, sampled_from +from hypothesis.strategies import data, just, lists, one_of, sampled_from from cattr.converters import Converter, UnstructureStrategy @@ -90,8 +92,8 @@ def test_unstructure_hooks(cl_and_vals): Unstructure hooks work. """ converter = Converter() - cl, vals = cl_and_vals - inst = cl(*vals) + cl, vals, kwargs = cl_and_vals + inst = cl(*vals, **kwargs) converter.register_unstructure_hook(cl, lambda _: "test") @@ -122,12 +124,12 @@ class Bar(object): assert converter.unstructure(b) is b -@given(lists(simple_classes()), sampled_from([tuple, list])) -def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type): +@given(lists(simple_classes()), one_of(just(tuple), just(list))) +def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type: Type): """Dumping a sequence of primitives is a simple copy operation.""" converter = Converter() - inputs = seq_type(cl(*vals) for cl, vals in cls_and_vals) + inputs = seq_type(cl(*vals, **kwargs) for cl, vals, kwargs in cls_and_vals) outputs = converter.unstructure(inputs) assert type(outputs) == seq_type assert all(type(e) is dict for e in outputs) From 09c733a6058dd58f55b7e16e768e066465ef7bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 8 Apr 2022 01:49:57 +0200 Subject: [PATCH 2/6] Tweaks --- HISTORY.rst | 3 ++- Makefile | 2 +- tests/metadata/__init__.py | 9 ++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 89b48a95..6e98d4b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,8 @@ History ======= 22.2.0 (UNRELEASED) ------------------- - +* cattrs now supports un/structuring `kw_only` fields on attrs classes into/from dictionaries. + (`#247 `_) 22.1.0 (2022-04-03) ------------------- diff --git a/Makefile b/Makefile index 6776a79e..eee5ab35 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ clean-test: ## remove test and coverage artifacts rm -fr htmlcov/ lint: ## check style with flake8 - poetry run flake8 src/cattr tests + poetry run flake8 src/ tests poetry run black --check src tests docs/conf.py test: ## run tests quickly with the default Python diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index eb4366c2..acbe7851 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -9,7 +9,6 @@ from functools import partial from typing import ( Any, - Callable, Dict, FrozenSet, List, @@ -608,7 +607,7 @@ def just_class( tup: Tuple[ List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] ], - defaults: tuple[PosArgs, KwArgs], + defaults: Tuple[PosArgs, KwArgs], ): nested_cl = tup[1][0] nested_cl_args = tup[1][1] @@ -628,7 +627,7 @@ def list_of_class( tup: Tuple[ List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] ], - defaults: tuple[PosArgs, KwArgs], + defaults: Tuple[PosArgs, KwArgs], ) -> SearchStrategy[Tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: nested_cl = tup[1][0] nested_cl_args = tup[1][1] @@ -648,7 +647,7 @@ def new_list_of_class( tup: Tuple[ List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] ], - defaults: tuple[PosArgs, KwArgs], + defaults: Tuple[PosArgs, KwArgs], ): """Uses the new 3.9 list type annotation.""" nested_cl = tup[1][0] @@ -669,7 +668,7 @@ def dict_of_class( tup: Tuple[ List[Tuple[_CountingAttr, SearchStrategy]], Tuple[Type, PosArgs, KwArgs] ], - defaults: tuple[PosArgs, KwArgs], + defaults: Tuple[PosArgs, KwArgs], ): nested_cl = tup[1][0] nested_cl_args = tup[1][1] From 4a4dc21568243aa6ee52941993e9e6f4cead1064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 8 Apr 2022 03:01:44 +0200 Subject: [PATCH 3/6] Update black --- poetry.lock | 55 +++++++++++++++++++++++++++++++++----------------- pyproject.toml | 2 +- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9af28d24..53f267f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -41,11 +41,11 @@ pytz = ">=2015.7" [[package]] name = "beautifulsoup4" -version = "4.10.0" +version = "4.11.0" description = "Screen-scraping library" category = "dev" optional = false -python-versions = ">3.0.0" +python-versions = ">=3.6.0" [package.dependencies] soupsieve = ">1.2" @@ -56,29 +56,25 @@ lxml = ["lxml"] [[package]] name = "black" -version = "21.12b0" +version = "22.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -713,11 +709,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "tomlkit" @@ -841,12 +837,33 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] beautifulsoup4 = [ - {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, - {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, + {file = "beautifulsoup4-4.11.0-py3-none-any.whl", hash = "sha256:577b9e1c36d2ada780d807c5622e889d43172466658c2eb239e97296965cdddb"}, + {file = "beautifulsoup4-4.11.0.tar.gz", hash = "sha256:ac98f868e1cb8eb9932a61be75b4f7018a118a490e7fdb424a74a982430cfcbd"}, ] black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -1371,8 +1388,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, diff --git a/pyproject.toml b/pyproject.toml index 36e2c8ce..f3d99e4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ pytest-benchmark = "^3.2.3" hypothesis = "^6.41.0" pendulum = "^2.1.2" isort = "^5.8.0" -black = "^21.12-beta.0" +black = "^22.3.0" immutables = "^0.15" ujson = "^5.1.0" orjson = "^3.5.2" From ecc8d7743ffc14cd54bb87733bd34f1607de9f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 8 Apr 2022 03:19:45 +0200 Subject: [PATCH 4/6] Reformat --- docs/conf.py | 14 +++++++------- tests/metadata/__init__.py | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a8816aec..bba67b90 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # Get the project root dir, which is the parent dir of this cwd = os.getcwd() -project_root = os.path.join(os.path.dirname(cwd), u"src") +project_root = os.path.join(os.path.dirname(cwd), "src") # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its @@ -60,8 +60,8 @@ master_doc = "index" # General information about the project. -project = u"cattrs" -copyright = u"2020, Tin Tvrtković" +project = "cattrs" +copyright = "2020, Tin Tvrtković" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -212,7 +212,7 @@ # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ("index", "cattrs.tex", u"cattrs Documentation", u"Tin Tvrtković", "manual") + ("index", "cattrs.tex", "cattrs Documentation", "Tin Tvrtković", "manual") ] # The name of an image file (relative to this directory) to place at @@ -240,7 +240,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "cattrs", u"cattrs Documentation", [u"Tin Tvrtković"], 1)] +man_pages = [("index", "cattrs", "cattrs Documentation", ["Tin Tvrtković"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -255,8 +255,8 @@ ( "index", "cattrs", - u"cattrs Documentation", - u"Tin Tvrtković", + "cattrs Documentation", + "Tin Tvrtković", "cattrs", "Composable complex class support for attrs.", "Miscellaneous", diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index acbe7851..faeecd4b 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -724,15 +724,25 @@ def nested_classes( init_kwargs = draw(kw_strat) if is_39_or_later: return draw( - list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) - | new_list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) - | dict_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + list_of_class( + (attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs) + ) + | new_list_of_class( + (attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs) + ) + | dict_of_class( + (attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs) + ) | just_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) ) else: return draw( - list_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) - | dict_of_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) + list_of_class( + (attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs) + ) + | dict_of_class( + (attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs) + ) | just_class((attrs, (cls, init_vals, init_kwargs)), (pos_defs, kwarg_defs)) ) From cad51703dd43a38cd32efb9a5ddee7b8e0394013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 8 Apr 2022 12:33:14 +0200 Subject: [PATCH 5/6] 3.10 fixes --- tests/metadata/test_genconverter.py | 5 +++-- tests/metadata/test_roundtrips.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/metadata/test_genconverter.py b/tests/metadata/test_genconverter.py index 72fb8ac5..04ec36fb 100644 --- a/tests/metadata/test_genconverter.py +++ b/tests/metadata/test_genconverter.py @@ -274,6 +274,7 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): converter = Converter(unstruct_strat=strat) cl_a, vals_a, kwargs_a = cl_and_vals_a cl_b, _, _ = cl_and_vals_b + assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a) a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -332,13 +333,13 @@ def test_310_optional_field_roundtrip(cl_and_vals): Classes with optional fields can be unstructured and structured. """ converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals @define class C: a: cl | None - inst = C(a=cl(*vals)) + inst = C(a=cl(*vals, **kwargs)) assert inst == converter.structure(converter.unstructure(inst), C) inst = C(a=None) diff --git a/tests/metadata/test_roundtrips.py b/tests/metadata/test_roundtrips.py index 846457b2..0ef7ca89 100644 --- a/tests/metadata/test_roundtrips.py +++ b/tests/metadata/test_roundtrips.py @@ -122,8 +122,9 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): Classes with union fields can be unstructured and structured. """ converter = Converter(unstruct_strat=strat) - cl_a, vals_a = cl_and_vals_a - cl_b, vals_b = cl_and_vals_b + cl_a, vals_a, kwargs_a = cl_and_vals_a + cl_b, vals_b, _ = cl_and_vals_b + assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a) a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) @@ -180,13 +181,13 @@ def test_310_optional_field_roundtrip(cl_and_vals): Classes with optional fields can be unstructured and structured. """ converter = Converter() - cl, vals = cl_and_vals + cl, vals, kwargs = cl_and_vals @define class C: a: cl | None - inst = C(a=cl(*vals)) + inst = C(a=cl(*vals, **kwargs)) assert inst == converter.structure(converter.unstructure(inst), C) inst = C(a=None) From 944f74099669e5ac8a11cb8f8614064bb84f9a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 8 Apr 2022 13:16:15 +0200 Subject: [PATCH 6/6] Tweak test --- tests/metadata/test_roundtrips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/metadata/test_roundtrips.py b/tests/metadata/test_roundtrips.py index 0ef7ca89..ea81dc82 100644 --- a/tests/metadata/test_roundtrips.py +++ b/tests/metadata/test_roundtrips.py @@ -137,7 +137,7 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): class C: a: cl_a | cl_b - inst = C(a=cl_a(*vals_a)) + inst = C(a=cl_a(*vals_a, **kwargs_a)) if strat is UnstructureStrategy.AS_DICT: assert inst == converter.structure(converter.unstructure(inst), C)