From 2270761c35be24bac2dff0c189313dfa061e980f Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 7 Jul 2025 22:41:03 +0200 Subject: [PATCH 01/11] Bump some test dependencies --- pdm.lock | 45 +++++++++++++++++++++++---------------------- pyproject.toml | 10 +++++----- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/pdm.lock b/pdm.lock index 8ecc46c5..d0b38e31 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "bench", "bson", "cbor2", "docs", "lint", "msgpack", "msgspec", "orjson", "pyyaml", "test", "tomlkit", "ujson"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:9c9f6594d0354bd9648f6134de8ff7a1b77ac6e4f437ac10ddee15a757889bf7" +content_hash = "sha256:e890c0f97bdebbd110292c1b0e0ad4c49629188af9a72dc6b0f29d04c880760c" [[metadata.targets]] requires_python = ">=3.9" @@ -375,8 +375,8 @@ files = [ [[package]] name = "hypothesis" -version = "6.111.2" -requires_python = ">=3.8" +version = "6.135.26" +requires_python = ">=3.9" summary = "A library for property-based testing" dependencies = [ "attrs>=22.2.0", @@ -384,8 +384,8 @@ dependencies = [ "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"}, - {file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"}, + {file = "hypothesis-6.135.26-py3-none-any.whl", hash = "sha256:fa237cbe2ae2c31d65f7230dcb866139ace635dcfec6c30dddf25974dd8ff4b9"}, + {file = "hypothesis-6.135.26.tar.gz", hash = "sha256:73af0e46cd5039c6806f514fed6a3c185d91ef88b5a1577477099ddbd1a2e300"}, ] [[package]] @@ -1026,50 +1026,51 @@ files = [ [[package]] name = "pytest" -version = "8.3.2" -requires_python = ">=3.8" +version = "8.4.1" +requires_python = ">=3.9" summary = "pytest: simple powerful testing with Python" dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=1", + "packaging>=20", "pluggy<2,>=1.5", + "pygments>=2.7.2", "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [[package]] name = "pytest-benchmark" -version = "4.0.0" -requires_python = ">=3.7" +version = "5.1.0" +requires_python = ">=3.9" summary = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." dependencies = [ "pathlib2; python_version < \"3.4\"", "py-cpuinfo", - "pytest>=3.8", + "pytest>=8.1", "statistics; python_version < \"3.4\"", ] files = [ - {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, - {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, + {file = "pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105"}, + {file = "pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89"}, ] [[package]] name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" +version = "3.8.0" +requires_python = ">=3.9" summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" dependencies = [ "execnet>=2.1", "pytest>=7.0.0", ] files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index d084100c..ccf0a587 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,12 @@ lint = [ "ruff>=0.12.2", ] test = [ - "hypothesis>=6.111.2", - "pytest>=8.3.2", - "pytest-benchmark>=4.0.0", - "immutables>=0.20", + "hypothesis>=6.135.26", + "pytest>=8.4.1", + "pytest-benchmark>=5.1.0", + "immutables>=0.21", "coverage>=7.6.1", - "pytest-xdist>=3.6.1", + "pytest-xdist>=3.8.0", ] docs = [ "sphinx>=5.3.0", From f5533897af892644714a225ee5cc39e750691307 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 7 Jul 2025 23:16:54 +0200 Subject: [PATCH 02/11] Tweak tests for coverage --- src/cattrs/converters.py | 6 +++- tests/test_converter.py | 62 ++++++++++++++++++++++++++-------------- tests/test_gen_dict.py | 10 +++++-- tests/untyped.py | 14 ++++----- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 81c75996..864a8b9d 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -281,7 +281,11 @@ def __init__( (is_tuple, self._structure_tuple), (is_namedtuple, namedtuple_structure_factory, "extended"), (is_mapping, self._structure_dict), - (is_supported_union, self._gen_attrs_union_structure, True), + *( + [(is_supported_union, self._gen_attrs_union_structure, True)] + if unstruct_strat is UnstructureStrategy.AS_DICT + else [] + ), (is_optional, self._structure_optional), ( lambda t: is_union_type(t) and t in self._union_struct_registry, diff --git a/tests/test_converter.py b/tests/test_converter.py index 614cb876..a64d62f3 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -267,24 +267,20 @@ def test_nested_roundtrip_tuple(cls_and_vals, omit_if_default: bool): assert inst == converter.structure(unstructured, cl) -@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) +@settings(suppress_health_check=[HealthCheck.too_slow]) @given( - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), - unstructure_strats, + simple_typed_classes(defaults=False, newtypes=False, allow_nan=False, min_attrs=2), + simple_typed_classes(defaults=False, newtypes=False, allow_nan=False, min_attrs=1), ) -def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): +def test_union_field_roundtrip_dict(cl_and_vals_a, cl_and_vals_b): """ Classes with union fields can be unstructured and structured. """ - converter = Converter(unstruct_strat=strat) + converter = Converter() 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) - assume(b_field_names) common_names = a_field_names & b_field_names assume(len(a_field_names) > len(common_names)) @@ -295,20 +291,44 @@ class C: inst = C(a=cl_a(*vals_a, **kwargs_a)) - if strat is UnstructureStrategy.AS_DICT: - unstructured = converter.unstructure(inst) - assert inst == converter.structure(converter.unstructure(unstructured), C) - else: - # Our disambiguation functions only support dictionaries for now. - with pytest.raises(ValueError): - converter.structure(converter.unstructure(inst), C) + unstructured = converter.unstructure(inst) + assert inst == converter.structure(converter.unstructure(unstructured), C) - def handler(obj, _): - return converter.structure(obj, cl_a) - converter.register_structure_hook(Union[cl_a, cl_b], handler) - unstructured = converter.unstructure(inst) - assert inst == converter.structure(unstructured, C) +@settings(suppress_health_check=[HealthCheck.too_slow]) +@given( + simple_typed_classes( + defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=2 + ), + simple_typed_classes( + defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=1 + ), +) +def test_union_field_roundtrip_tuple(cl_and_vals_a, cl_and_vals_b): + """ + Classes with union fields can be unstructured and structured. + """ + converter = Converter(unstruct_strat=UnstructureStrategy.AS_TUPLE) + cl_a, vals_a, _ = cl_and_vals_a + cl_b, _, _ = cl_and_vals_b + + @define + class C: + a: Union[cl_a, cl_b] + + inst = C(a=cl_a(*vals_a)) + + # Our disambiguation functions only support dictionaries for now. + raw = converter.unstructure(inst) + with pytest.raises(StructureHandlerNotFoundError): + converter.structure(raw, C) + + def handler(obj, _): + return converter.structure(obj, cl_a) + + converter.register_structure_hook(Union[cl_a, cl_b], handler) + unstructured = converter.unstructure(inst) + assert inst == converter.structure(unstructured, C) @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") diff --git a/tests/test_gen_dict.py b/tests/test_gen_dict.py index 7cf638a2..ef1e5e62 100644 --- a/tests/test_gen_dict.py +++ b/tests/test_gen_dict.py @@ -108,7 +108,9 @@ def test_nodefs_generated_unstructuring_cl( @given( one_of(just(BaseConverter), just(Converter)), - nested_classes() | simple_classes() | simple_typed_dataclasses(), + nested_classes() + | simple_classes(min_attrs=1) + | simple_typed_dataclasses(min_attrs=1), ) def test_individual_overrides(converter_cls, cl_and_vals): """ @@ -118,7 +120,9 @@ def test_individual_overrides(converter_cls, cl_and_vals): converter = converter_cls() cl, vals, kwargs = cl_and_vals - for attr in adapted_fields(cl): + fields = adapted_fields(cl) + + for attr in fields: if attr.default is not NOTHING: break else: @@ -142,7 +146,7 @@ def test_individual_overrides(converter_cls, cl_and_vals): assert "Hyp" not in repr(res) assert "Factory" not in repr(res) - for attr, val in zip(adapted_fields(cl), vals): + for attr, val in zip(fields, vals): if attr.name == chosen_name: assert attr.name in res elif attr.default is not NOTHING: diff --git a/tests/untyped.py b/tests/untyped.py index 7b0dab95..0a2df142 100644 --- a/tests/untyped.py +++ b/tests/untyped.py @@ -2,12 +2,12 @@ import keyword import string +from collections.abc import Iterable from enum import Enum from typing import ( Any, Deque, Dict, - Iterable, List, Mapping, MutableMapping, @@ -25,8 +25,6 @@ from hypothesis.strategies import SearchStrategy, booleans from typing_extensions import TypeAlias -from . import FeatureFlag - PosArg = Any PosArgs = tuple[PosArg] KwArgs = dict[str, Any] @@ -206,7 +204,7 @@ def key(t): def just_class(tup): nested_cl = tup[1][0] - default = attr.Factory(nested_cl) + default = Factory(nested_cl) combined_attrs = list(tup[0]) combined_attrs.append((attr.ib(default=default), st.just(nested_cl()))) return _create_hyp_class(combined_attrs) @@ -288,7 +286,7 @@ def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]: def dict_of_class(tup): nested_cl = tup[1][0] - default = attr.Factory(lambda: {"cls": nested_cl()}) + default = Factory(lambda: {"cls": nested_cl()}) combined_attrs = list(tup[0]) combined_attrs.append((attr.ib(default=default), st.just({"cls": nested_cl()}))) return _create_hyp_class(combined_attrs) @@ -404,7 +402,7 @@ def dict_attrs(draw, defaults=None, kw_only=None): val_strat = st.dictionaries(keys=st.text(), values=st.integers()) if defaults is True or (defaults is None and draw(st.booleans())): default_val = draw(val_strat) - default = attr.Factory(lambda: default_val) + default = Factory(lambda: default_val) return ( attr.ib( default=default, kw_only=draw(st.booleans()) if kw_only is None else kw_only @@ -460,9 +458,7 @@ def simple_classes(defaults=None, min_attrs=0, frozen=None, kw_only=None): ) -def nested_classes( - takes_self: FeatureFlag = "sometimes", -) -> SearchStrategy[AttrsAndArgs]: +def nested_classes() -> SearchStrategy[AttrsAndArgs]: # Ok, so st.recursive works by taking a base strategy (in this case, # simple_classes) and a special function. This function receives a strategy, # and returns another strategy (building on top of the base strategy). From 66eda5b7d86755f2fa1e5a315d61ab703231dacf Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 7 Jul 2025 23:32:24 +0200 Subject: [PATCH 03/11] Small test refactor --- tests/test_baseconverter.py | 14 ++--- tests/test_converter.py | 32 ++++++----- tests/typed.py | 105 +++++++++++++++++++++++------------- 3 files changed, 95 insertions(+), 56 deletions(-) diff --git a/tests/test_baseconverter.py b/tests/test_baseconverter.py index 56045518..9b622aae 100644 --- a/tests/test_baseconverter.py +++ b/tests/test_baseconverter.py @@ -28,7 +28,7 @@ def test_simple_roundtrip(cls_and_vals, strat): @given( - simple_typed_attrs(defaults=True, newtypes=False, allow_nan=False), + simple_typed_attrs(defaults="always", newtypes=False, allow_nan=False), unstructure_strats, ) def test_simple_roundtrip_defaults(attr_and_strat, strat): @@ -73,8 +73,8 @@ def test_nested_roundtrip_tuple(cls_and_vals): @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), unstructure_strats, ) def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -116,8 +116,8 @@ def handler(obj, _): @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), unstructure_strats, ) def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -156,7 +156,7 @@ def handler(obj, _): assert inst == converter.structure(converter.unstructure(inst), C) -@given(simple_typed_classes(defaults=False, newtypes=False, allow_nan=False)) +@given(simple_typed_classes(defaults="never", newtypes=False, allow_nan=False)) def test_optional_field_roundtrip(cl_and_vals): """ Classes with optional fields can be unstructured and structured. @@ -178,7 +178,7 @@ class C: @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") -@given(simple_typed_classes(defaults=False, newtypes=False, allow_nan=False)) +@given(simple_typed_classes(defaults="never", newtypes=False, allow_nan=False)) def test_310_optional_field_roundtrip(cl_and_vals): """ Classes with optional fields can be unstructured and structured. diff --git a/tests/test_converter.py b/tests/test_converter.py index a64d62f3..844a1bab 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -77,7 +77,7 @@ def test_simple_roundtrip_tuple(cls_and_vals, dv: bool): assert inst == converter.structure(unstructured, cl) -@given(simple_typed_attrs(defaults=True, allow_nan=False)) +@given(simple_typed_attrs(defaults="always", allow_nan=False)) def test_simple_roundtrip_defaults(attr_and_vals): """ Simple classes with metadata can be unstructured and restructured. @@ -93,7 +93,9 @@ def test_simple_roundtrip_defaults(attr_and_vals): @given( - simple_typed_attrs(defaults=True, kw_only=False, newtypes=False, allow_nan=False) + simple_typed_attrs( + defaults="always", kw_only=False, newtypes=False, allow_nan=False + ) ) def test_simple_roundtrip_defaults_tuple(attr_and_vals): """ @@ -173,7 +175,7 @@ class C: converter.structure(unstructured, C) -@given(simple_typed_attrs(defaults=True)) +@given(simple_typed_attrs(defaults="always")) def test_forbid_extra_keys_defaults(attr_and_vals): """ Restructuring fails when a dict key is renamed (if forbid_extra_keys set) @@ -234,7 +236,9 @@ class A: assert cve.value.exceptions[0].extra_fields == {"b"} -@given(nested_typed_classes(defaults=True, min_attrs=1, allow_nan=False), booleans()) +@given( + nested_typed_classes(defaults="always", min_attrs=1, allow_nan=False), booleans() +) def test_nested_roundtrip(cls_and_vals, omit_if_default): """ Nested classes with metadata can be unstructured and restructured. @@ -249,7 +253,7 @@ def test_nested_roundtrip(cls_and_vals, omit_if_default): @given( nested_typed_classes( - defaults=True, min_attrs=1, kw_only=False, newtypes=False, allow_nan=False + defaults="always", min_attrs=1, kw_only=False, newtypes=False, allow_nan=False ), booleans(), ) @@ -269,8 +273,12 @@ def test_nested_roundtrip_tuple(cls_and_vals, omit_if_default: bool): @settings(suppress_health_check=[HealthCheck.too_slow]) @given( - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False, min_attrs=2), - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False, min_attrs=1), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=2 + ), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), ) def test_union_field_roundtrip_dict(cl_and_vals_a, cl_and_vals_b): """ @@ -334,8 +342,8 @@ def handler(obj, _): @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), - simple_typed_classes(defaults=False, newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), unstructure_strats, ) def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -398,7 +406,7 @@ class C: @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") -@given(simple_typed_classes(defaults=False, allow_nan=False)) +@given(simple_typed_classes(defaults="never", allow_nan=False)) def test_310_optional_field_roundtrip(cl_and_vals): """ Classes with optional fields can be unstructured and structured. @@ -419,7 +427,7 @@ class C: assert inst == converter.structure(unstructured, C) -@given(simple_typed_classes(defaults=True, allow_nan=False)) +@given(simple_typed_classes(defaults="always", allow_nan=False)) def test_omit_default_roundtrip(cl_and_vals): """ Omit default on the converter works. @@ -460,7 +468,7 @@ class C: assert converter.structure(unstructured, C) == inst -@given(simple_typed_classes(defaults=True)) +@given(simple_typed_classes(defaults="always")) def test_type_overrides(cl_and_vals): """ Type overrides on the GenConverter work. diff --git a/tests/typed.py b/tests/typed.py index 989b5394..d0e556cf 100644 --- a/tests/typed.py +++ b/tests/typed.py @@ -49,6 +49,7 @@ tuples, ) +from . import FeatureFlag from .untyped import gen_attr_names, make_class PosArg = Any @@ -58,7 +59,7 @@ def simple_typed_classes( - defaults=None, + defaults: FeatureFlag = "sometimes", min_attrs=0, frozen=False, kw_only=None, @@ -81,7 +82,11 @@ def simple_typed_classes( def simple_typed_dataclasses( - defaults=None, min_attrs=0, frozen=False, newtypes=True, allow_nan=True + defaults: FeatureFlag = "sometimes", + min_attrs=0, + frozen=False, + newtypes=True, + allow_nan=True, ): """Yield tuples of (class, values).""" return lists_of_typed_attrs( @@ -95,7 +100,11 @@ def simple_typed_dataclasses( def simple_typed_classes_and_strats( - defaults=None, min_attrs=0, kw_only=None, newtypes=True, allow_nan=True + defaults: FeatureFlag = "sometimes", + min_attrs=0, + kw_only=None, + newtypes=True, + allow_nan=True, ) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: """Yield tuples of (class, (strategies)).""" return lists_of_typed_attrs( @@ -108,7 +117,7 @@ def simple_typed_classes_and_strats( def lists_of_typed_attrs( - defaults=None, + defaults: FeatureFlag = "sometimes", min_size=0, for_frozen=False, allow_mutable_defaults=True, @@ -140,7 +149,7 @@ def lists_of_typed_attrs( def simple_typed_attrs( - defaults=None, + defaults: FeatureFlag = "sometimes", for_frozen=False, allow_mutable_defaults=True, kw_only=None, @@ -301,11 +310,11 @@ def key(t): @composite def any_typed_attrs( - draw: DrawFn, defaults=None, kw_only=None + draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None ) -> tuple[_CountingAttr, SearchStrategy[None]]: """Attributes typed as `Any`, having values of `None`.""" default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = None return ( field( @@ -318,13 +327,13 @@ def any_typed_attrs( @composite -def int_typed_attrs(draw, defaults=None, kw_only=None): +def int_typed_attrs(draw, defaults: FeatureFlag = "sometimes", kw_only=None): """ Generate a tuple of an attribute and a strategy that yields ints for that attribute. """ default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(integers()) return ( field( @@ -337,13 +346,15 @@ def int_typed_attrs(draw, defaults=None, kw_only=None): @composite -def str_typed_attrs(draw, defaults=None, kw_only=None, codec: str = "utf8"): +def str_typed_attrs( + draw, defaults: FeatureFlag = "sometimes", kw_only=None, codec: str = "utf8" +): """ Generate a tuple of an attribute and a strategy that yields strs for that attribute. """ default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(text()) return ( field( @@ -357,14 +368,18 @@ def str_typed_attrs(draw, defaults=None, kw_only=None, codec: str = "utf8"): @composite def float_typed_attrs( - draw, defaults=None, kw_only=None, allow_infinity=None, allow_nan=True + draw, + defaults: FeatureFlag = "sometimes", + kw_only=None, + allow_infinity=None, + allow_nan=True, ): """ Generate a tuple of an attribute and a strategy that yields floats for that attribute. """ default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(floats(allow_infinity=allow_infinity, allow_nan=allow_nan)) return ( field( @@ -378,7 +393,7 @@ def float_typed_attrs( @composite def path_typed_attrs( - draw: DrawFn, defaults: Optional[bool] = None, kw_only: Optional[bool] = None + draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only: Optional[bool] = None ) -> tuple[_CountingAttr, SearchStrategy[Path]]: """ Generate a tuple of an attribute and a strategy that yields paths for that @@ -386,7 +401,7 @@ def path_typed_attrs( """ default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = Path(draw(text(ascii_lowercase, min_size=1))) return ( field( @@ -400,7 +415,10 @@ def path_typed_attrs( @composite def dict_typed_attrs( - draw: DrawFn, defaults=None, allow_mutable_defaults=True, kw_only=None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + allow_mutable_defaults=True, + kw_only=None, ) -> tuple[_CountingAttr, SearchStrategy[dict[str, int]]]: """ Generate a tuple of an attribute and a strategy that yields dictionaries @@ -409,7 +427,7 @@ def dict_typed_attrs( """ default = NOTHING val_strat = dictionaries(keys=text(), values=integers()) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -428,7 +446,7 @@ def dict_typed_attrs( @composite def new_dict_typed_attrs( - draw, defaults=None, allow_mutable_defaults=True, kw_only=None + draw, defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, kw_only=None ): """ Generate a tuple of an attribute and a strategy that yields dictionaries @@ -438,7 +456,7 @@ def new_dict_typed_attrs( """ default_val = NOTHING val_strat = dictionaries(keys=text(), values=integers()) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -460,7 +478,7 @@ def new_dict_typed_attrs( @composite def set_typed_attrs( draw: DrawFn, - defaults=None, + defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, kw_only=None, @@ -471,7 +489,7 @@ def set_typed_attrs( """ default_val = NOTHING val_strat = sets(integers()) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -499,7 +517,10 @@ def set_typed_attrs( @composite def frozenset_typed_attrs( - draw: DrawFn, defaults=None, legacy_types_only=False, kw_only=None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + legacy_types_only=False, + kw_only=None, ): """ Generate a tuple of an attribute and a strategy that yields frozensets @@ -507,7 +528,7 @@ def frozenset_typed_attrs( """ default = NOTHING val_strat = frozensets(integers()) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(val_strat) type = draw( sampled_from( @@ -529,7 +550,7 @@ def frozenset_typed_attrs( @composite def list_typed_attrs( draw: DrawFn, - defaults=None, + defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, kw_only=None, @@ -540,7 +561,7 @@ def list_typed_attrs( """ default_val = NOTHING val_strat = lists(floats(allow_infinity=False, allow_nan=False)) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -567,7 +588,7 @@ def list_typed_attrs( @composite def seq_typed_attrs( draw, - defaults=None, + defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, kw_only=None, @@ -581,7 +602,7 @@ def seq_typed_attrs( """ default_val = NOTHING val_strat = lists(integers()).map(tuple) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -603,7 +624,7 @@ def seq_typed_attrs( @composite def mutable_seq_typed_attrs( draw, - defaults=None, + defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, kw_only=None, @@ -614,7 +635,7 @@ def mutable_seq_typed_attrs( """ default_val = NOTHING val_strat = lists(floats(allow_infinity=False, allow_nan=False)) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default_val = draw(val_strat) if not allow_mutable_defaults or draw(booleans()): default = Factory(lambda: default_val) @@ -638,14 +659,16 @@ def mutable_seq_typed_attrs( @composite -def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False, kw_only=None): +def homo_tuple_typed_attrs( + draw, defaults: FeatureFlag = "sometimes", 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. """ default = NOTHING val_strat = tuples(text(), text(), text()) - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(val_strat) return ( field( @@ -664,13 +687,15 @@ def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False, kw_only @composite -def newtype_int_typed_attrs(draw: DrawFn, defaults=None, kw_only=None): +def newtype_int_typed_attrs( + draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None +): """ Generate a tuple of an attribute and a strategy that yields ints for that attribute. """ default = NOTHING - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = draw(integers()) NewInt = NewType("NewInt", int) return ( @@ -684,7 +709,9 @@ def newtype_int_typed_attrs(draw: DrawFn, defaults=None, kw_only=None): @composite -def newtype_attrs_typed_attrs(draw: DrawFn, defaults=None, kw_only=None): +def newtype_attrs_typed_attrs( + draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None +): """ Generate a tuple of an attribute and a strategy that yields values for that attribute. @@ -695,7 +722,7 @@ def newtype_attrs_typed_attrs(draw: DrawFn, defaults=None, kw_only=None): class NewTypeAttrs: a: int - if defaults is True or (defaults is None and draw(booleans())): + if defaults == "always" or (defaults == "sometimes" and draw(booleans())): default = NewTypeAttrs(draw(integers())) NewAttrs = NewType("NewAttrs", NewTypeAttrs) @@ -840,7 +867,11 @@ def nested_classes( def nested_typed_classes_and_strat( - defaults=None, min_attrs=0, kw_only=None, newtypes=True, allow_nan=True + defaults: FeatureFlag = "sometimes", + min_attrs=0, + kw_only=None, + newtypes=True, + allow_nan=True, ) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs]]]: return recursive( simple_typed_classes_and_strats( @@ -863,7 +894,7 @@ def nested_typed_classes_and_strat( @composite def nested_typed_classes( draw: DrawFn, - defaults=None, + defaults: FeatureFlag = "sometimes", min_attrs=0, kw_only=None, newtypes=True, From f0c1309c5c2f8b2aa6af948cef67b4b9d8d88df2 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 7 Jul 2025 23:46:12 +0200 Subject: [PATCH 04/11] Rebump coverage, pin Hypothesis --- pdm.lock | 153 +++++++++++++++++------------------- pyproject.toml | 4 +- tests/test_baseconverter.py | 13 +-- tests/test_converter.py | 12 +-- 4 files changed, 91 insertions(+), 91 deletions(-) diff --git a/pdm.lock b/pdm.lock index d0b38e31..1affa16c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "bench", "bson", "cbor2", "docs", "lint", "msgpack", "msgspec", "orjson", "pyyaml", "test", "tomlkit", "ujson"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:e890c0f97bdebbd110292c1b0e0ad4c49629188af9a72dc6b0f29d04c880760c" +content_hash = "sha256:d317bca9bc2ecd6e03c6d6015ccd9be078c0c8f3b89b2930b7b286a36deae596" [[metadata.targets]] requires_python = ">=3.9" @@ -239,82 +239,77 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" -requires_python = ">=3.8" +version = "7.9.2" +requires_python = ">=3.9" summary = "Code coverage measurement for Python" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912"}, + {file = "coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e"}, + {file = "coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e"}, + {file = "coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c"}, + {file = "coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba"}, + {file = "coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74"}, + {file = "coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6"}, + {file = "coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7"}, + {file = "coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62"}, + {file = "coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0"}, + {file = "coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d"}, + {file = "coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355"}, + {file = "coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0"}, + {file = "coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b"}, + {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"}, + {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"}, + {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"}, + {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"}, + {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"}, + {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"}, + {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"}, + {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"}, + {file = "coverage-7.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddc39510ac922a5c4c27849b739f875d3e1d9e590d1e7b64c98dadf037a16cce"}, + {file = "coverage-7.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a535c0c7364acd55229749c2b3e5eebf141865de3a8f697076a3291985f02d30"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df0f9ef28e0f20c767ccdccfc5ae5f83a6f4a2fbdfbcbcc8487a8a78771168c8"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f3da12e0ccbcb348969221d29441ac714bbddc4d74e13923d3d5a7a0bebef7a"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a17eaf46f56ae0f870f14a3cbc2e4632fe3771eab7f687eda1ee59b73d09fe4"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:669135a9d25df55d1ed56a11bf555f37c922cf08d80799d4f65d77d7d6123fcf"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d3a700304d01a627df9db4322dc082a0ce1e8fc74ac238e2af39ced4c083193"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71ae8b53855644a0b1579d4041304ddc9995c7b21c8a1f16753c4d8903b4dfed"}, + {file = "coverage-7.9.2-cp39-cp39-win32.whl", hash = "sha256:dd7a57b33b5cf27acb491e890720af45db05589a80c1ffc798462a765be6d4d7"}, + {file = "coverage-7.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f65bb452e579d5540c8b37ec105dd54d8b9307b07bcaa186818c104ffda22441"}, + {file = "coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050"}, + {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"}, + {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"}, ] [[package]] @@ -375,8 +370,8 @@ files = [ [[package]] name = "hypothesis" -version = "6.135.26" -requires_python = ">=3.9" +version = "6.111.2" +requires_python = ">=3.8" summary = "A library for property-based testing" dependencies = [ "attrs>=22.2.0", @@ -384,8 +379,8 @@ dependencies = [ "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.135.26-py3-none-any.whl", hash = "sha256:fa237cbe2ae2c31d65f7230dcb866139ace635dcfec6c30dddf25974dd8ff4b9"}, - {file = "hypothesis-6.135.26.tar.gz", hash = "sha256:73af0e46cd5039c6806f514fed6a3c185d91ef88b5a1577477099ddbd1a2e300"}, + {file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"}, + {file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index ccf0a587..1da12856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,11 @@ lint = [ "ruff>=0.12.2", ] test = [ - "hypothesis>=6.135.26", + "hypothesis==6.111.2", "pytest>=8.4.1", "pytest-benchmark>=5.1.0", "immutables>=0.21", - "coverage>=7.6.1", + "coverage>=7.9.2", "pytest-xdist>=3.8.0", ] docs = [ diff --git a/tests/test_baseconverter.py b/tests/test_baseconverter.py index 9b622aae..42f0e7e8 100644 --- a/tests/test_baseconverter.py +++ b/tests/test_baseconverter.py @@ -8,6 +8,7 @@ from hypothesis.strategies import just, one_of from cattrs import BaseConverter, UnstructureStrategy +from cattrs.errors import StructureHandlerNotFoundError from ._compat import is_py310_plus from .typed import nested_typed_classes, simple_typed_attrs, simple_typed_classes @@ -116,8 +117,12 @@ def handler(obj, _): @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), unstructure_strats, ) def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -130,8 +135,6 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): 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) - assume(b_field_names) common_names = a_field_names & b_field_names assume(len(a_field_names) > len(common_names)) @@ -146,7 +149,7 @@ class C: assert inst == converter.structure(converter.unstructure(inst), C) else: # Our disambiguation functions only support dictionaries for now. - with pytest.raises(ValueError): + with pytest.raises(StructureHandlerNotFoundError): converter.structure(converter.unstructure(inst), C) def handler(obj, _): diff --git a/tests/test_converter.py b/tests/test_converter.py index 844a1bab..a0bbaa4d 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -342,8 +342,12 @@ def handler(obj, _): @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), unstructure_strats, ) def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -356,8 +360,6 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): 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) - assume(b_field_names) common_names = a_field_names & b_field_names assume(len(a_field_names) > len(common_names)) @@ -373,7 +375,7 @@ class C: assert inst == converter.structure(converter.unstructure(unstructured), C) else: # Our disambiguation functions only support dictionaries for now. - with pytest.raises(ValueError): + with pytest.raises(StructureHandlerNotFoundError): converter.structure(converter.unstructure(inst), C) def handler(obj, _): From ed2719251524099c87db8c4b68b9524f241eb20f Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 7 Jul 2025 23:51:09 +0200 Subject: [PATCH 05/11] Fix tests --- tests/test_baseconverter.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_baseconverter.py b/tests/test_baseconverter.py index 42f0e7e8..79f3e348 100644 --- a/tests/test_baseconverter.py +++ b/tests/test_baseconverter.py @@ -74,8 +74,12 @@ def test_nested_roundtrip_tuple(cls_and_vals): @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), - simple_typed_classes(defaults="never", newtypes=False, allow_nan=False), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), + simple_typed_classes( + defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + ), unstructure_strats, ) def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): @@ -85,11 +89,9 @@ def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): converter = BaseConverter(unstruct_strat=strat) 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 + 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) - assume(b_field_names) common_names = a_field_names & b_field_names assume(len(a_field_names) > len(common_names)) @@ -104,7 +106,7 @@ class C: assert inst == converter.structure(converter.unstructure(inst), C) else: # Our disambiguation functions only support dictionaries for now. - with pytest.raises(ValueError): + with pytest.raises(StructureHandlerNotFoundError): converter.structure(converter.unstructure(inst), C) def handler(obj, _): From 2c7591c2094bb0aade5c5e7cde921f4c571ff1d3 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 00:07:18 +0200 Subject: [PATCH 06/11] Docs and test --- HISTORY.md | 3 +++ tests/test_disambiguators.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 382dfbcd..708319ac 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -32,6 +32,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python - For {class}`cattrs.errors.StructureHandlerNotFoundError` and {class}`cattrs.errors.ForbiddenExtraKeysError` correctly set {attr}`BaseException.args` in `super()` and hence make them pickable. ([#666](https://github.com/python-attrs/cattrs/pull/666)) +- The default disambiguation hook factory is now only enabled for converters with `unstructure_strat=AS_DICT` (the default). + Since the strategy doesn't support tuples, it is skipped for `unstructure_strat=AS_TUPLE` converters. + ([#673](https://github.com/python-attrs/cattrs/pull/673)) ## 25.1.1 (2025-06-04) diff --git a/tests/test_disambiguators.py b/tests/test_disambiguators.py index 19bc7e7a..dbc90e0b 100644 --- a/tests/test_disambiguators.py +++ b/tests/test_disambiguators.py @@ -113,6 +113,30 @@ class L: fn([]) +def test_input_not_mapping(): + """Correct errors are raised when the raw payload isn't a mapping.""" + c = Converter() + + @define + class A: + a: int + + @define + class B: + b: int + + @define + class C: + pass + + with pytest.raises(ValueError): + c.structure([], Union[A, B]) + + # Also test the fallback case + with pytest.raises(ValueError): + c.structure([], Union[A, B, C]) + + @given(simple_classes(defaults=False)) def test_fallback(cl_and_vals): """The fallback case works.""" From 1b49d4ff0df0db5ed14a8edb4875696829982601 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 00:13:12 +0200 Subject: [PATCH 07/11] Bump Hypothesis, rework `FAST` --- pdm.lock | 10 +++++----- pyproject.toml | 2 +- tests/conftest.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pdm.lock b/pdm.lock index 1affa16c..ac6927cc 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "bench", "bson", "cbor2", "docs", "lint", "msgpack", "msgspec", "orjson", "pyyaml", "test", "tomlkit", "ujson"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:d317bca9bc2ecd6e03c6d6015ccd9be078c0c8f3b89b2930b7b286a36deae596" +content_hash = "sha256:b18f05d0d3bad30caebab3d0bf0c9fc98ea29232ce336c6203a34aae1bd78ed0" [[metadata.targets]] requires_python = ">=3.9" @@ -370,8 +370,8 @@ files = [ [[package]] name = "hypothesis" -version = "6.111.2" -requires_python = ">=3.8" +version = "6.135.26" +requires_python = ">=3.9" summary = "A library for property-based testing" dependencies = [ "attrs>=22.2.0", @@ -379,8 +379,8 @@ dependencies = [ "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"}, - {file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"}, + {file = "hypothesis-6.135.26-py3-none-any.whl", hash = "sha256:fa237cbe2ae2c31d65f7230dcb866139ace635dcfec6c30dddf25974dd8ff4b9"}, + {file = "hypothesis-6.135.26.tar.gz", hash = "sha256:73af0e46cd5039c6806f514fed6a3c185d91ef88b5a1577477099ddbd1a2e300"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 1da12856..7a0e83e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ lint = [ "ruff>=0.12.2", ] test = [ - "hypothesis==6.111.2", + "hypothesis>=6.135.26", "pytest>=8.4.1", "pytest-benchmark>=5.1.0", "immutables>=0.21", diff --git a/tests/conftest.py b/tests/conftest.py index d295990e..e3504ea7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,7 @@ def converter_cls(request): ) settings.register_profile("fast", settings.get_profile("tests"), max_examples=10) -settings.load_profile("fast" if "FAST" in environ else "tests") +settings.load_profile("fast" if environ.get("FAST") == "1" else "tests") collect_ignore_glob = [] if sys.version_info < (3, 10): From d7a198a0a3a3800184130dd4bba367f5b1b46547 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 01:12:52 +0200 Subject: [PATCH 08/11] Move tests around --- tests/test_baseconverter.py | 60 +++++++++++++------ tests/test_converter.py | 115 ------------------------------------ 2 files changed, 42 insertions(+), 133 deletions(-) diff --git a/tests/test_baseconverter.py b/tests/test_baseconverter.py index 79f3e348..ff9d9925 100644 --- a/tests/test_baseconverter.py +++ b/tests/test_baseconverter.py @@ -72,23 +72,21 @@ def test_nested_roundtrip_tuple(cls_and_vals): assert inst == converter.structure(converter.unstructure(inst), cl) -@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) +@settings(suppress_health_check=[HealthCheck.too_slow]) @given( simple_typed_classes( - defaults="never", newtypes=False, allow_nan=False, min_attrs=1 + defaults="never", newtypes=False, allow_nan=False, min_attrs=2 ), simple_typed_classes( defaults="never", newtypes=False, allow_nan=False, min_attrs=1 ), - unstructure_strats, ) -def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): +def test_union_field_roundtrip_dict(cl_and_vals_a, cl_and_vals_b): """ Classes with union fields can be unstructured and structured. """ - converter = BaseConverter(unstruct_strat=strat) + converter = BaseConverter() cl_a, vals_a, kwargs_a = cl_and_vals_a - assume(strat is UnstructureStrategy.AS_DICT or not kwargs_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)} @@ -102,22 +100,48 @@ class C: inst = C(a=cl_a(*vals_a, **kwargs_a)) - if strat is UnstructureStrategy.AS_DICT: - assert inst == converter.structure(converter.unstructure(inst), C) - else: - # Our disambiguation functions only support dictionaries for now. - with pytest.raises(StructureHandlerNotFoundError): - converter.structure(converter.unstructure(inst), C) + unstructured = converter.unstructure(inst) + assert inst == converter.structure(converter.unstructure(unstructured), C) - def handler(obj, _): - return converter.structure(obj, cl_a) - converter.register_structure_hook(Union[cl_a, cl_b], handler) - assert inst == converter.structure(converter.unstructure(inst), C) +@settings(suppress_health_check=[HealthCheck.too_slow]) +@given( + simple_typed_classes( + defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=2 + ), + simple_typed_classes( + defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=1 + ), +) +def test_union_field_roundtrip_tuple(cl_and_vals_a, cl_and_vals_b): + """ + Classes with union fields can be unstructured and structured. + """ + converter = BaseConverter(unstruct_strat=UnstructureStrategy.AS_TUPLE) + cl_a, vals_a, _ = cl_and_vals_a + cl_b, _, _ = cl_and_vals_b + + @define + class C: + a: Union[cl_a, cl_b] + + inst = C(a=cl_a(*vals_a)) + + # Our disambiguation functions only support dictionaries for now. + raw = converter.unstructure(inst) + with pytest.raises(StructureHandlerNotFoundError): + converter.structure(raw, C) + + def handler(obj, _): + return converter.structure(obj, cl_a) + + converter.register_structure_hook(Union[cl_a, cl_b], handler) + unstructured = converter.unstructure(inst) + assert inst == converter.structure(unstructured, C) @pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") -@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) +@settings(suppress_health_check=[HealthCheck.too_slow]) @given( simple_typed_classes( defaults="never", newtypes=False, allow_nan=False, min_attrs=1 @@ -133,7 +157,7 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): """ converter = BaseConverter(unstruct_strat=strat) cl_a, vals_a, kwargs_a = cl_and_vals_a - cl_b, vals_b, _ = cl_and_vals_b + 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)} diff --git a/tests/test_converter.py b/tests/test_converter.py index a0bbaa4d..4a0c8492 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -271,121 +271,6 @@ def test_nested_roundtrip_tuple(cls_and_vals, omit_if_default: bool): assert inst == converter.structure(unstructured, cl) -@settings(suppress_health_check=[HealthCheck.too_slow]) -@given( - simple_typed_classes( - defaults="never", newtypes=False, allow_nan=False, min_attrs=2 - ), - simple_typed_classes( - defaults="never", newtypes=False, allow_nan=False, min_attrs=1 - ), -) -def test_union_field_roundtrip_dict(cl_and_vals_a, cl_and_vals_b): - """ - Classes with union fields can be unstructured and structured. - """ - converter = Converter() - 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)} - - common_names = a_field_names & b_field_names - assume(len(a_field_names) > len(common_names)) - - @define - class C: - a: Union[cl_a, cl_b] - - inst = C(a=cl_a(*vals_a, **kwargs_a)) - - unstructured = converter.unstructure(inst) - assert inst == converter.structure(converter.unstructure(unstructured), C) - - -@settings(suppress_health_check=[HealthCheck.too_slow]) -@given( - simple_typed_classes( - defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=2 - ), - simple_typed_classes( - defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=1 - ), -) -def test_union_field_roundtrip_tuple(cl_and_vals_a, cl_and_vals_b): - """ - Classes with union fields can be unstructured and structured. - """ - converter = Converter(unstruct_strat=UnstructureStrategy.AS_TUPLE) - cl_a, vals_a, _ = cl_and_vals_a - cl_b, _, _ = cl_and_vals_b - - @define - class C: - a: Union[cl_a, cl_b] - - inst = C(a=cl_a(*vals_a)) - - # Our disambiguation functions only support dictionaries for now. - raw = converter.unstructure(inst) - with pytest.raises(StructureHandlerNotFoundError): - converter.structure(raw, C) - - def handler(obj, _): - return converter.structure(obj, cl_a) - - converter.register_structure_hook(Union[cl_a, cl_b], handler) - unstructured = converter.unstructure(inst) - assert inst == converter.structure(unstructured, C) - - -@pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax") -@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) -@given( - simple_typed_classes( - defaults="never", newtypes=False, allow_nan=False, min_attrs=1 - ), - simple_typed_classes( - defaults="never", newtypes=False, allow_nan=False, min_attrs=1 - ), - unstructure_strats, -) -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, 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)} - - common_names = a_field_names & b_field_names - assume(len(a_field_names) > len(common_names)) - - @define - class C: - a: cl_a | cl_b - - inst = C(a=cl_a(*vals_a, **kwargs_a)) - - if strat is UnstructureStrategy.AS_DICT: - unstructured = converter.unstructure(inst) - assert inst == converter.structure(converter.unstructure(unstructured), C) - else: - # Our disambiguation functions only support dictionaries for now. - with pytest.raises(StructureHandlerNotFoundError): - converter.structure(converter.unstructure(inst), C) - - def handler(obj, _): - return converter.structure(obj, cl_a) - - converter.register_structure_hook(Union[cl_a, cl_b], handler) - unstructured = converter.unstructure(inst) - assert inst == converter.structure(unstructured, C) - - @given(simple_typed_classes(defaults=False, allow_nan=False)) def test_optional_field_roundtrip(cl_and_vals): """ From e70eedeb0bdbd4b7f99dc1413b9e843cc2c7285e Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 01:17:13 +0200 Subject: [PATCH 09/11] Remove unused imports --- tests/test_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 4a0c8492..69b6b262 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -18,7 +18,7 @@ import pytest from attrs import Factory, define, field, fields, has, make_class -from hypothesis import HealthCheck, assume, given, settings +from hypothesis import assume, given from hypothesis.strategies import booleans, just, lists, one_of, sampled_from from cattrs import BaseConverter, Converter, UnstructureStrategy From 230c00abc83ab3393dac2bc7862a2f9332040cc6 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 01:28:13 +0200 Subject: [PATCH 10/11] Refactor `kw_only` to a FeatureFlag --- tests/test_baseconverter.py | 6 +- tests/test_converter.py | 6 +- tests/typed.py | 126 +++++++++++++++++++++++++----------- 3 files changed, 95 insertions(+), 43 deletions(-) diff --git a/tests/test_baseconverter.py b/tests/test_baseconverter.py index ff9d9925..617cd360 100644 --- a/tests/test_baseconverter.py +++ b/tests/test_baseconverter.py @@ -59,7 +59,7 @@ def test_nested_roundtrip(cls_and_vals): assert inst == converter.structure(converter.unstructure(inst), cl) -@given(nested_typed_classes(kw_only=False, newtypes=False, allow_nan=False)) +@given(nested_typed_classes(kw_only="never", newtypes=False, allow_nan=False)) def test_nested_roundtrip_tuple(cls_and_vals): """ Nested classes with metadata can be unstructured and restructured. @@ -107,10 +107,10 @@ class C: @settings(suppress_health_check=[HealthCheck.too_slow]) @given( simple_typed_classes( - defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=2 + defaults="never", newtypes=False, allow_nan=False, kw_only="never", min_attrs=2 ), simple_typed_classes( - defaults=False, newtypes=False, allow_nan=False, kw_only=False, min_attrs=1 + defaults="never", newtypes=False, allow_nan=False, kw_only="never", min_attrs=1 ), ) def test_union_field_roundtrip_tuple(cl_and_vals_a, cl_and_vals_b): diff --git a/tests/test_converter.py b/tests/test_converter.py index 69b6b262..76162aab 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -59,7 +59,7 @@ def test_simple_roundtrip(cls_and_vals, detailed_validation): @given( - simple_typed_classes(kw_only=False, newtypes=False, allow_nan=False) + simple_typed_classes(kw_only="never", newtypes=False, allow_nan=False) | simple_typed_dataclasses(newtypes=False, allow_nan=False), booleans(), ) @@ -94,7 +94,7 @@ def test_simple_roundtrip_defaults(attr_and_vals): @given( simple_typed_attrs( - defaults="always", kw_only=False, newtypes=False, allow_nan=False + defaults="always", kw_only="never", newtypes=False, allow_nan=False ) ) def test_simple_roundtrip_defaults_tuple(attr_and_vals): @@ -253,7 +253,7 @@ def test_nested_roundtrip(cls_and_vals, omit_if_default): @given( nested_typed_classes( - defaults="always", min_attrs=1, kw_only=False, newtypes=False, allow_nan=False + defaults="always", min_attrs=1, kw_only="never", newtypes=False, allow_nan=False ), booleans(), ) diff --git a/tests/typed.py b/tests/typed.py index d0e556cf..5b78cc60 100644 --- a/tests/typed.py +++ b/tests/typed.py @@ -62,7 +62,7 @@ def simple_typed_classes( defaults: FeatureFlag = "sometimes", min_attrs=0, frozen=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, text_codec: str = "utf8", allow_infinity=None, @@ -102,7 +102,7 @@ def simple_typed_dataclasses( def simple_typed_classes_and_strats( defaults: FeatureFlag = "sometimes", min_attrs=0, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, allow_nan=True, ) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: @@ -121,7 +121,7 @@ def lists_of_typed_attrs( min_size=0, for_frozen=False, allow_mutable_defaults=True, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, text_codec="utf8", allow_infinity=None, @@ -152,7 +152,7 @@ def simple_typed_attrs( defaults: FeatureFlag = "sometimes", for_frozen=False, allow_mutable_defaults=True, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, text_codec="utf8", allow_infinity=None, @@ -310,7 +310,9 @@ def key(t): @composite def any_typed_attrs( - draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + kw_only: FeatureFlag = "sometimes", ) -> tuple[_CountingAttr, SearchStrategy[None]]: """Attributes typed as `Any`, having values of `None`.""" default = NOTHING @@ -320,14 +322,18 @@ def any_typed_attrs( field( type=Any, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), just(None), ) @composite -def int_typed_attrs(draw, defaults: FeatureFlag = "sometimes", kw_only=None): +def int_typed_attrs( + draw, defaults: FeatureFlag = "sometimes", kw_only: FeatureFlag = "sometimes" +): """ Generate a tuple of an attribute and a strategy that yields ints for that attribute. @@ -339,7 +345,9 @@ def int_typed_attrs(draw, defaults: FeatureFlag = "sometimes", kw_only=None): field( type=int, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), integers(), ) @@ -347,7 +355,10 @@ def int_typed_attrs(draw, defaults: FeatureFlag = "sometimes", kw_only=None): @composite def str_typed_attrs( - draw, defaults: FeatureFlag = "sometimes", kw_only=None, codec: str = "utf8" + draw, + defaults: FeatureFlag = "sometimes", + kw_only: FeatureFlag = "sometimes", + codec: str = "utf8", ): """ Generate a tuple of an attribute and a strategy that yields strs for that @@ -360,7 +371,9 @@ def str_typed_attrs( field( type=str, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), text(characters(codec=codec)), ) @@ -370,7 +383,7 @@ def str_typed_attrs( def float_typed_attrs( draw, defaults: FeatureFlag = "sometimes", - kw_only=None, + kw_only: FeatureFlag = "sometimes", allow_infinity=None, allow_nan=True, ): @@ -385,7 +398,9 @@ def float_typed_attrs( field( type=float, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), floats(allow_infinity=allow_infinity, allow_nan=allow_nan), ) @@ -393,7 +408,9 @@ def float_typed_attrs( @composite def path_typed_attrs( - draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only: Optional[bool] = None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + kw_only: FeatureFlag = "sometimes", ) -> tuple[_CountingAttr, SearchStrategy[Path]]: """ Generate a tuple of an attribute and a strategy that yields paths for that @@ -407,7 +424,9 @@ def path_typed_attrs( field( type=Path, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), text(ascii_lowercase, min_size=1).map(Path), ) @@ -418,7 +437,7 @@ def dict_typed_attrs( draw: DrawFn, defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ) -> tuple[_CountingAttr, SearchStrategy[dict[str, int]]]: """ Generate a tuple of an attribute and a strategy that yields dictionaries @@ -438,7 +457,9 @@ def dict_typed_attrs( field( type=type, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -446,7 +467,10 @@ def dict_typed_attrs( @composite def new_dict_typed_attrs( - draw, defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, kw_only=None + draw, + defaults: FeatureFlag = "sometimes", + allow_mutable_defaults=True, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields dictionaries @@ -469,7 +493,9 @@ def new_dict_typed_attrs( field( type=dict[str, int], default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -481,7 +507,7 @@ def set_typed_attrs( defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields sets @@ -509,7 +535,9 @@ def set_typed_attrs( field( type=type, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -520,7 +548,7 @@ def frozenset_typed_attrs( draw: DrawFn, defaults: FeatureFlag = "sometimes", legacy_types_only=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields frozensets @@ -541,7 +569,9 @@ def frozenset_typed_attrs( field( type=type, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -553,7 +583,7 @@ def list_typed_attrs( defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ) -> tuple[_CountingAttr, SearchStrategy[list[float]]]: """ Generate a tuple of an attribute and a strategy that yields lists @@ -579,7 +609,9 @@ def list_typed_attrs( ) ), default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -591,7 +623,7 @@ def seq_typed_attrs( defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields tuples @@ -615,7 +647,9 @@ def seq_typed_attrs( field( 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, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -627,7 +661,7 @@ def mutable_seq_typed_attrs( defaults: FeatureFlag = "sometimes", allow_mutable_defaults=True, legacy_types_only=False, - kw_only=None, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields lists @@ -652,7 +686,9 @@ def mutable_seq_typed_attrs( else MutableSequence[float] ), default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -660,7 +696,10 @@ def mutable_seq_typed_attrs( @composite def homo_tuple_typed_attrs( - draw, defaults: FeatureFlag = "sometimes", legacy_types_only=False, kw_only=None + draw, + defaults: FeatureFlag = "sometimes", + legacy_types_only=False, + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields homogenous @@ -680,7 +719,9 @@ def homo_tuple_typed_attrs( ) ), default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), val_strat, ) @@ -688,7 +729,9 @@ def homo_tuple_typed_attrs( @composite def newtype_int_typed_attrs( - draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields ints for that @@ -702,7 +745,9 @@ def newtype_int_typed_attrs( field( type=NewInt, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), integers(), ) @@ -710,7 +755,9 @@ def newtype_int_typed_attrs( @composite def newtype_attrs_typed_attrs( - draw: DrawFn, defaults: FeatureFlag = "sometimes", kw_only=None + draw: DrawFn, + defaults: FeatureFlag = "sometimes", + kw_only: FeatureFlag = "sometimes", ): """ Generate a tuple of an attribute and a strategy that yields values for that @@ -730,7 +777,9 @@ class NewTypeAttrs: field( type=NewAttrs, default=default, - kw_only=draw(booleans()) if kw_only is None else kw_only, + kw_only=( + draw(booleans()) if kw_only == "sometimes" else (kw_only == "always") + ), ), integers().map(NewTypeAttrs), ) @@ -818,7 +867,10 @@ def dict_of_class( def _create_hyp_nested_strategy( - simple_class_strategy: SearchStrategy, kw_only=None, newtypes=True, allow_nan=True + simple_class_strategy: SearchStrategy, + kw_only: FeatureFlag = "sometimes", + newtypes=True, + allow_nan=True, ) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: """ Create a recursive attrs class. @@ -869,7 +921,7 @@ def nested_classes( def nested_typed_classes_and_strat( defaults: FeatureFlag = "sometimes", min_attrs=0, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, allow_nan=True, ) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs]]]: @@ -896,7 +948,7 @@ def nested_typed_classes( draw: DrawFn, defaults: FeatureFlag = "sometimes", min_attrs=0, - kw_only=None, + kw_only: FeatureFlag = "sometimes", newtypes=True, allow_nan=True, ): From ae8ce87d79c77240c4ca62c99520a587214b48b6 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 8 Jul 2025 12:05:17 +0200 Subject: [PATCH 11/11] Remove unused import --- tests/typed.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/typed.py b/tests/typed.py index 5b78cc60..791c4833 100644 --- a/tests/typed.py +++ b/tests/typed.py @@ -18,7 +18,6 @@ MutableSequence, MutableSet, NewType, - Optional, Sequence, Set, Tuple,