From 26eeb3a736192df526ff7cb30247b10d7aa5fd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Mon, 2 Dec 2024 23:20:40 +0100 Subject: [PATCH 1/8] Improve cols coverage --- tests/test_defaultdicts.py | 14 ++++++++++++++ tests/test_tuples.py | 14 ++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_defaultdicts.py b/tests/test_defaultdicts.py index 02a34637..00539445 100644 --- a/tests/test_defaultdicts.py +++ b/tests/test_defaultdicts.py @@ -4,6 +4,7 @@ from typing import DefaultDict from cattrs import Converter +from cattrs.cols import defaultdict_structure_factory def test_typing_defaultdicts(genconverter: Converter): @@ -30,3 +31,16 @@ def test_collection_defaultdicts(genconverter: Converter): genconverter.register_unstructure_hook(int, str) assert genconverter.unstructure(res) == {"a": "1", "b": "0"} + + +def test_factory(genconverter: Converter): + """Explicit factories work.""" + genconverter.register_structure_hook_func( + lambda t: t == defaultdict[str, int], + defaultdict_structure_factory(defaultdict[str, int], genconverter, lambda: 2), + ) + res = genconverter.structure({"a": 1}, defaultdict[str, int]) + + assert isinstance(res, defaultdict) + assert res["a"] == 1 + assert res["b"] == 2 diff --git a/tests/test_tuples.py b/tests/test_tuples.py index a6729abc..4fcfd85e 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -69,19 +69,25 @@ class Test(NamedTuple): def test_simple_dict_nametuples(genconverter: Converter): """Namedtuples can be un/structured to/from dicts.""" + class TestInner(NamedTuple): + a: int + class Test(NamedTuple): a: int b: str = "test" + c: TestInner = TestInner(1) genconverter.register_unstructure_hook_factory( - lambda t: t is Test, namedtuple_dict_unstructure_factory + lambda t: t in (Test, TestInner), namedtuple_dict_unstructure_factory ) genconverter.register_structure_hook_factory( - lambda t: t is Test, namedtuple_dict_structure_factory + lambda t: t in (Test, TestInner), namedtuple_dict_structure_factory ) - assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test"} - assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test(1, "2") + assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test", "c": {"a": 1}} + assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test( + 1, "2", TestInner(1) + ) # Defaults work. assert genconverter.structure({"a": 1}, Test) == Test(1, "test") From d17d1e3ef6500b60e58e19985a5dc28d66e09968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 3 Dec 2024 14:46:44 +0100 Subject: [PATCH 2/8] Test the tests --- tests/test_tests.py | 9 +++++++++ tests/test_typeddicts.py | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/test_tests.py diff --git a/tests/test_tests.py b/tests/test_tests.py new file mode 100644 index 00000000..bae1ac8f --- /dev/null +++ b/tests/test_tests.py @@ -0,0 +1,9 @@ +from .untyped import gen_attr_names + + +def test_gen_attr_names(): + """We can generate a lot of attribute names.""" + assert len(list(gen_attr_names())) == 697 + + # No duplicates! + assert len(list(gen_attr_names())) == len(set(gen_attr_names())) diff --git a/tests/test_typeddicts.py b/tests/test_typeddicts.py index 492750c8..da0cf109 100644 --- a/tests/test_typeddicts.py +++ b/tests/test_typeddicts.py @@ -27,12 +27,21 @@ from ._compat import is_py311_plus from .typeddicts import ( + gen_typeddict_attr_names, generic_typeddicts, simple_typeddicts, simple_typeddicts_with_extra_keys, ) +def test_gen_attr_names(): + """We can generate a lot of attribute names.""" + assert len(list(gen_typeddict_attr_names())) == 697 + + # No duplicates! + assert len(list(gen_typeddict_attr_names())) == len(set(gen_typeddict_attr_names())) + + def mk_converter(detailed_validation: bool = True) -> Converter: """We can't use function-scoped fixtures with Hypothesis strats.""" c = Converter(detailed_validation=detailed_validation) From c1b39c10a796ae56e3f98043f8d89798a16ccd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 3 Dec 2024 14:55:40 +0100 Subject: [PATCH 3/8] Improve typeddict coverage --- tests/typeddicts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/typeddicts.py b/tests/typeddicts.py index f44d8bf9..21dcfe1f 100644 --- a/tests/typeddicts.py +++ b/tests/typeddicts.py @@ -280,8 +280,7 @@ def make_typeddict( bases_snippet = ", ".join(f"_base{ix}" for ix in range(len(bases))) for ix, base in enumerate(bases): globs[f"_base{ix}"] = base - if bases_snippet: - bases_snippet = f", {bases_snippet}" + bases_snippet = f", {bases_snippet}" lines.append(f"class {cls_name}(TypedDict{bases_snippet}, total={total}):") for n, t in attrs.items(): From 73af88697f7952255c55759dece8dbc6c4e2d819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 3 Dec 2024 15:06:38 +0100 Subject: [PATCH 4/8] Improve disambiguators coverage --- tests/__init__.py | 2 +- tests/test_disambiguators.py | 5 +---- tests/untyped.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 9d678465..ab305117 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,7 +9,7 @@ "CI", settings(suppress_health_check=[HealthCheck.too_slow]), deadline=None ) -if "CI" in os.environ: +if "CI" in os.environ: # pragma: nocover settings.load_profile("CI") unstructure_strats = one_of(just(s) for s in UnstructureStrategy) diff --git a/tests/test_disambiguators.py b/tests/test_disambiguators.py index 772d2ed4..6f549ce0 100644 --- a/tests/test_disambiguators.py +++ b/tests/test_disambiguators.py @@ -130,10 +130,7 @@ class A: assert fn({}) is A assert fn(asdict(cl(*vals, **kwargs))) is cl - attr_names = {a.name for a in fields(cl)} - - if "xyz" not in attr_names: - assert fn({"xyz": 1}) is A # Uses the fallback. + assert fn({"xyz": 1}) is A # Uses the fallback. @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) diff --git a/tests/untyped.py b/tests/untyped.py index 23f39c8c..d90ce1b1 100644 --- a/tests/untyped.py +++ b/tests/untyped.py @@ -167,7 +167,7 @@ def gen_attr_names() -> Iterable[str]: def _create_hyp_class( attrs_and_strategy: list[tuple[_CountingAttr, st.SearchStrategy[PosArgs]]], frozen=None, -): +) -> SearchStrategy[tuple]: """ A helper function for Hypothesis to generate attrs classes. From fd223af6f36b9e50c43d060cebc75c83929a17a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 3 Dec 2024 17:17:13 +0100 Subject: [PATCH 5/8] preconf: test bare dicts --- src/cattrs/preconf/bson.py | 2 +- src/cattrs/preconf/json.py | 2 +- src/cattrs/preconf/msgpack.py | 2 +- src/cattrs/preconf/msgspec.py | 2 +- src/cattrs/preconf/orjson.py | 12 ++++++------ src/cattrs/preconf/pyyaml.py | 2 +- src/cattrs/preconf/ujson.py | 2 +- tests/test_preconf.py | 2 ++ 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/cattrs/preconf/bson.py b/src/cattrs/preconf/bson.py index 7d398b4a..49574893 100644 --- a/src/cattrs/preconf/bson.py +++ b/src/cattrs/preconf/bson.py @@ -61,7 +61,7 @@ def configure_converter(converter: BaseConverter): * a deserialization hook is registered for bson.ObjectId by default * string and int enums are passed through when unstructuring - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ diff --git a/src/cattrs/preconf/json.py b/src/cattrs/preconf/json.py index b6c0ecc2..08851a75 100644 --- a/src/cattrs/preconf/json.py +++ b/src/cattrs/preconf/json.py @@ -36,7 +36,7 @@ def configure_converter(converter: BaseConverter): * union passthrough is configured for unions of strings, bools, ints, floats and None - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ converter.register_unstructure_hook( diff --git a/src/cattrs/preconf/msgpack.py b/src/cattrs/preconf/msgpack.py index 4e1bddd5..da01418e 100644 --- a/src/cattrs/preconf/msgpack.py +++ b/src/cattrs/preconf/msgpack.py @@ -31,7 +31,7 @@ def configure_converter(converter: BaseConverter): * sets are serialized as lists * string and int enums are passed through when unstructuring - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ converter.register_unstructure_hook(datetime, lambda v: v.timestamp()) diff --git a/src/cattrs/preconf/msgspec.py b/src/cattrs/preconf/msgspec.py index 62673c27..3fa1a3b6 100644 --- a/src/cattrs/preconf/msgspec.py +++ b/src/cattrs/preconf/msgspec.py @@ -75,7 +75,7 @@ def configure_converter(converter: Converter) -> None: * union passthrough configured for str, bool, int, float and None * bare, string and int enums are passed through when unstructuring - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ configure_passthroughs(converter) diff --git a/src/cattrs/preconf/orjson.py b/src/cattrs/preconf/orjson.py index 6e0b6b80..112534eb 100644 --- a/src/cattrs/preconf/orjson.py +++ b/src/cattrs/preconf/orjson.py @@ -11,7 +11,7 @@ from .._compat import is_subclass from ..cols import is_mapping, is_namedtuple, namedtuple_unstructure_factory -from ..converters import BaseConverter, Converter +from ..converters import Converter from ..fns import identity from ..literals import is_literal_containing_enums from ..strategies import configure_union_passthrough @@ -28,7 +28,7 @@ def loads(self, data: Union[bytes, bytearray, memoryview, str], cl: type[T]) -> return self.structure(loads(data), cl) -def configure_converter(converter: BaseConverter): +def configure_converter(converter: Converter): """ Configure the converter for use with the orjson library. @@ -40,9 +40,9 @@ def configure_converter(converter: BaseConverter): * mapping keys are coerced into strings when unstructuring * bare, string and int enums are passed through when unstructuring - .. versionchanged: 24.1.0 + .. versionchanged:: 24.1.0 Add support for typed namedtuples. - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ converter.register_unstructure_hook( @@ -53,7 +53,7 @@ def configure_converter(converter: BaseConverter): converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) - def gen_unstructure_mapping(cl: Any, unstructure_to=None): + def unstructure_mapping_factory(cl: Any, unstructure_to=None): key_handler = str args = getattr(cl, "__args__", None) if args: @@ -77,7 +77,7 @@ def key_handler(v): converter._unstructure_func.register_func_list( [ - (is_mapping, gen_unstructure_mapping, True), + (is_mapping, unstructure_mapping_factory, True), ( is_namedtuple, partial(namedtuple_unstructure_factory, unstructure_to=tuple), diff --git a/src/cattrs/preconf/pyyaml.py b/src/cattrs/preconf/pyyaml.py index 9c0ca99b..a6a4bfa9 100644 --- a/src/cattrs/preconf/pyyaml.py +++ b/src/cattrs/preconf/pyyaml.py @@ -38,7 +38,7 @@ def configure_converter(converter: BaseConverter): * datetimes and dates are validated * typed namedtuples are serialized as lists - .. versionchanged: 24.1.0 + .. versionchanged:: 24.1.0 Add support for typed namedtuples. """ converter.register_unstructure_hook( diff --git a/src/cattrs/preconf/ujson.py b/src/cattrs/preconf/ujson.py index bc9b1084..afb79b98 100644 --- a/src/cattrs/preconf/ujson.py +++ b/src/cattrs/preconf/ujson.py @@ -33,7 +33,7 @@ def configure_converter(converter: BaseConverter): * sets are serialized as lists * string and int enums are passed through when unstructuring - .. versionchanged: 24.2.0 + .. versionchanged:: 24.2.0 Enums are left to the library to unstructure, speeding them up. """ converter.register_unstructure_hook( diff --git a/tests/test_preconf.py b/tests/test_preconf.py index fec750ff..91610a3f 100644 --- a/tests/test_preconf.py +++ b/tests/test_preconf.py @@ -88,6 +88,7 @@ class ABareEnum(Enum): an_int: int a_float: float a_dict: Dict[str, int] + a_bare_dict: dict a_list: List[int] a_homogenous_tuple: TupleSubscriptable[int, ...] a_hetero_tuple: TupleSubscriptable[str, int, float] @@ -160,6 +161,7 @@ def everythings( draw(ints), draw(fs), draw(dictionaries(key_text, ints)), + draw(dictionaries(key_text, strings)), draw(lists(ints)), tuple(draw(lists(ints))), (draw(strings), draw(ints), draw(fs)), From 5d480f4d64440867aaa9032cf11d058f57ce46f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 3 Dec 2024 17:25:36 +0100 Subject: [PATCH 6/8] test_preconf: always include bools and ints --- tests/test_preconf.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_preconf.py b/tests/test_preconf.py index 91610a3f..0197ad0f 100644 --- a/tests/test_preconf.py +++ b/tests/test_preconf.py @@ -198,8 +198,6 @@ def everythings( def native_unions( draw: DrawFn, include_strings=True, - include_bools=True, - include_ints=True, include_floats=True, include_nones=True, include_bytes=True, @@ -207,17 +205,11 @@ def native_unions( include_objectids=False, include_literals=True, ) -> tuple[Any, Any]: - types = [] - strats = {} + types = [bool, int] + strats = {bool: booleans(), int: integers()} if include_strings: types.append(str) strats[str] = text() - if include_bools: - types.append(bool) - strats[bool] = booleans() - if include_ints: - types.append(int) - strats[int] = integers() if include_floats: types.append(float) strats[float] = floats(allow_nan=False) From 7f2cedce2a7a1a478e1dde3a57d62a2adbb0e89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Wed, 25 Dec 2024 17:43:35 +0100 Subject: [PATCH 7/8] More factories with `takes_self` --- tests/__init__.py | 4 ++ tests/test_gen_dict.py | 10 ++-- tests/test_unstructure.py | 6 +- tests/typed.py | 10 +++- tests/untyped.py | 113 ++++++++++++++++++++++++-------------- 5 files changed, 92 insertions(+), 51 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index ab305117..01b82519 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,9 @@ import os +from typing import Literal from hypothesis import HealthCheck, settings from hypothesis.strategies import just, one_of +from typing_extensions import TypeAlias from cattrs import UnstructureStrategy @@ -13,3 +15,5 @@ settings.load_profile("CI") unstructure_strats = one_of(just(s) for s in UnstructureStrategy) + +FeatureFlag: TypeAlias = Literal["always", "never", "sometimes"] diff --git a/tests/test_gen_dict.py b/tests/test_gen_dict.py index d9ae4666..6dd68503 100644 --- a/tests/test_gen_dict.py +++ b/tests/test_gen_dict.py @@ -16,7 +16,7 @@ from .untyped import nested_classes, simple_classes -@given(nested_classes | simple_classes()) +@given(nested_classes() | simple_classes()) def test_unmodified_generated_unstructuring(cl_and_vals): converter = BaseConverter() cl, vals, kwargs = cl_and_vals @@ -33,7 +33,7 @@ def test_unmodified_generated_unstructuring(cl_and_vals): assert res_expected == res_actual -@given(nested_classes | simple_classes()) +@given(nested_classes() | simple_classes()) def test_nodefs_generated_unstructuring(cl_and_vals): """Test omitting default values on a per-attribute basis.""" converter = BaseConverter() @@ -61,7 +61,9 @@ def test_nodefs_generated_unstructuring(cl_and_vals): assert attr.name not in res -@given(one_of(just(BaseConverter), just(Converter)), nested_classes | simple_classes()) +@given( + one_of(just(BaseConverter), just(Converter)), nested_classes() | simple_classes() +) def test_nodefs_generated_unstructuring_cl( converter_cls: Type[BaseConverter], cl_and_vals ): @@ -105,7 +107,7 @@ def test_nodefs_generated_unstructuring_cl( @given( one_of(just(BaseConverter), just(Converter)), - nested_classes | simple_classes() | simple_typed_dataclasses(), + nested_classes() | simple_classes() | simple_typed_dataclasses(), ) def test_individual_overrides(converter_cls, cl_and_vals): """ diff --git a/tests/test_unstructure.py b/tests/test_unstructure.py index d290e66a..40830ec8 100644 --- a/tests/test_unstructure.py +++ b/tests/test_unstructure.py @@ -1,6 +1,6 @@ """Tests for dumping.""" -from attr import asdict, astuple +from attrs import asdict, astuple from hypothesis import given from hypothesis.strategies import data, just, lists, one_of, sampled_from @@ -69,7 +69,7 @@ def test_enum_unstructure(enum, dump_strat, data): assert converter.unstructure(member) == member.value -@given(nested_classes) +@given(nested_classes()) def test_attrs_asdict_unstructure(nested_class): """Our dumping should be identical to `attrs`.""" converter = BaseConverter() @@ -77,7 +77,7 @@ def test_attrs_asdict_unstructure(nested_class): assert converter.unstructure(instance) == asdict(instance) -@given(nested_classes) +@given(nested_classes()) def test_attrs_astuple_unstructure(nested_class): """Our dumping should be identical to `attrs`.""" converter = BaseConverter(unstruct_strat=UnstructureStrategy.AS_TUPLE) diff --git a/tests/typed.py b/tests/typed.py index 7c88dd34..27a3ea52 100644 --- a/tests/typed.py +++ b/tests/typed.py @@ -1,6 +1,5 @@ """Strategies for attributes with types and classes using them.""" -from collections import OrderedDict from collections.abc import MutableSequence as AbcMutableSequence from collections.abc import MutableSet as AbcMutableSet from collections.abc import Sequence as AbcSequence @@ -293,7 +292,7 @@ def key(t): 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(make_class("HypClass", dict(zip(gen_attr_names(), attrs)))), just(tuples(*vals)), just(fixed_dictionaries(kwarg_strats)), ) @@ -860,7 +859,12 @@ def nested_typed_classes_and_strat( @composite def nested_typed_classes( - draw, defaults=None, min_attrs=0, kw_only=None, newtypes=True, allow_nan=True + draw: DrawFn, + defaults=None, + min_attrs=0, + kw_only=None, + newtypes=True, + allow_nan=True, ): cl, strat, kwarg_strat = draw( nested_typed_classes_and_strat( diff --git a/tests/untyped.py b/tests/untyped.py index d90ce1b1..7b0dab95 100644 --- a/tests/untyped.py +++ b/tests/untyped.py @@ -2,7 +2,6 @@ import keyword import string -from collections import OrderedDict from enum import Enum from typing import ( Any, @@ -23,11 +22,15 @@ from attr._make import _CountingAttr from attrs import NOTHING, AttrsInstance, Factory, make_class from hypothesis import strategies as st -from hypothesis.strategies import SearchStrategy +from hypothesis.strategies import SearchStrategy, booleans +from typing_extensions import TypeAlias + +from . import FeatureFlag PosArg = Any PosArgs = tuple[PosArg] KwArgs = dict[str, Any] +AttrsAndArgs: TypeAlias = tuple[type[AttrsInstance], PosArgs, KwArgs] primitive_strategies = st.sampled_from( [ @@ -167,7 +170,7 @@ def gen_attr_names() -> Iterable[str]: def _create_hyp_class( attrs_and_strategy: list[tuple[_CountingAttr, st.SearchStrategy[PosArgs]]], frozen=None, -) -> SearchStrategy[tuple]: +) -> SearchStrategy[AttrsAndArgs]: """ A helper function for Hypothesis to generate attrs classes. @@ -192,7 +195,7 @@ def key(t): return st.tuples( st.builds( lambda f: make_class( - "HypClass", OrderedDict(zip(gen_attr_names(), attrs)), frozen=f + "HypClass", dict(zip(gen_attr_names(), attrs)), frozen=f ), st.booleans() if frozen is None else st.just(frozen), ), @@ -209,26 +212,28 @@ def just_class(tup): return _create_hyp_class(combined_attrs) -def just_class_with_type(tup): +def just_class_with_type(tup: tuple) -> SearchStrategy[AttrsAndArgs]: nested_cl = tup[1][0] - default = attr.Factory(nested_cl) - combined_attrs = list(tup[0]) - combined_attrs.append( - (attr.ib(default=default, type=nested_cl), st.just(nested_cl())) - ) - return _create_hyp_class(combined_attrs) + def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]: + combined_attrs = list(tup[0]) + combined_attrs.append( + ( + attr.ib( + default=( + Factory( + nested_cl if not takes_self else lambda _: nested_cl(), + takes_self=takes_self, + ) + ), + type=nested_cl, + ), + st.just(nested_cl()), + ) + ) + return _create_hyp_class(combined_attrs) -def just_class_with_type_takes_self( - tup: tuple[list[tuple[_CountingAttr, SearchStrategy]], tuple[type[AttrsInstance]]] -) -> SearchStrategy[tuple[type[AttrsInstance]]]: - nested_cl = tup[1][0] - default = Factory(lambda _: nested_cl(), takes_self=True) - combined_attrs = list(tup[0]) - combined_attrs.append( - (attr.ib(default=default, type=nested_cl), st.just(nested_cl())) - ) - return _create_hyp_class(combined_attrs) + return booleans().flatmap(make_with_default) def just_frozen_class_with_type(tup): @@ -240,22 +245,45 @@ def just_frozen_class_with_type(tup): return _create_hyp_class(combined_attrs) -def list_of_class(tup): +def list_of_class(tup: tuple) -> SearchStrategy[AttrsAndArgs]: nested_cl = tup[1][0] - default = attr.Factory(lambda: [nested_cl()]) - combined_attrs = list(tup[0]) - combined_attrs.append((attr.ib(default=default), st.just([nested_cl()]))) - return _create_hyp_class(combined_attrs) + def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]: + combined_attrs = list(tup[0]) + combined_attrs.append( + ( + attr.ib( + default=( + Factory(lambda: [nested_cl()]) + if not takes_self + else Factory(lambda _: [nested_cl()], takes_self=True) + ), + type=list[nested_cl], + ), + st.just([nested_cl()]), + ) + ) + return _create_hyp_class(combined_attrs) + + return booleans().flatmap(make_with_default) -def list_of_class_with_type(tup): + +def list_of_class_with_type(tup: tuple) -> SearchStrategy[AttrsAndArgs]: nested_cl = tup[1][0] - default = attr.Factory(lambda: [nested_cl()]) - combined_attrs = list(tup[0]) - combined_attrs.append( - (attr.ib(default=default, type=List[nested_cl]), st.just([nested_cl()])) - ) - return _create_hyp_class(combined_attrs) + + def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]: + default = ( + Factory(lambda: [nested_cl()]) + if not takes_self + else Factory(lambda _: [nested_cl()], takes_self=True) + ) + combined_attrs = list(tup[0]) + combined_attrs.append( + (attr.ib(default=default, type=List[nested_cl]), st.just([nested_cl()])) + ) + return _create_hyp_class(combined_attrs) + + return booleans().flatmap(make_with_default) def dict_of_class(tup): @@ -266,7 +294,9 @@ def dict_of_class(tup): return _create_hyp_class(combined_attrs) -def _create_hyp_nested_strategy(simple_class_strategy): +def _create_hyp_nested_strategy( + simple_class_strategy: SearchStrategy, +) -> SearchStrategy: """ Create a recursive attrs class. Given a strategy for building (simpler) classes, create and return @@ -275,6 +305,7 @@ def _create_hyp_nested_strategy(simple_class_strategy): * a list of simpler classes * a dict mapping the string "cls" to a simpler class. """ + # A strategy producing tuples of the form ([list of attributes], ). attrs_and_classes = st.tuples(lists_of_attrs(defaults=True), simple_class_strategy) @@ -286,7 +317,6 @@ def _create_hyp_nested_strategy(simple_class_strategy): | attrs_and_classes.flatmap(list_of_class_with_type) | attrs_and_classes.flatmap(dict_of_class) | attrs_and_classes.flatmap(just_frozen_class_with_type) - | attrs_and_classes.flatmap(just_class_with_type_takes_self) ) @@ -430,9 +460,10 @@ def simple_classes(defaults=None, min_attrs=0, frozen=None, kw_only=None): ) -# 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). -nested_classes = st.recursive( - simple_classes(defaults=True), _create_hyp_nested_strategy -) +def nested_classes( + takes_self: FeatureFlag = "sometimes", +) -> 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). + return st.recursive(simple_classes(defaults=True), _create_hyp_nested_strategy) From 89edc2662723297b5899fcdb40128bc85cd7bd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Thu, 26 Dec 2024 00:26:28 +0100 Subject: [PATCH 8/8] Fix return type annotations --- tests/typed.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/typed.py b/tests/typed.py index 27a3ea52..5ff4ea6f 100644 --- a/tests/typed.py +++ b/tests/typed.py @@ -26,7 +26,7 @@ ) from attr._make import _CountingAttr -from attrs import NOTHING, Factory, field, frozen +from attrs import NOTHING, AttrsInstance, Factory, field, frozen from hypothesis import note from hypothesis.strategies import ( DrawFn, @@ -400,8 +400,8 @@ def path_typed_attrs( @composite def dict_typed_attrs( - draw, defaults=None, allow_mutable_defaults=True, kw_only=None -) -> SearchStrategy[tuple[_CountingAttr, SearchStrategy]]: + draw: DrawFn, defaults=None, 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 for that attribute. The dictionaries map strings to integers. @@ -819,7 +819,7 @@ def nested_classes( tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]], ] ], -) -> SearchStrategy[tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]: +) -> tuple[type[AttrsInstance], SearchStrategy[PosArgs], SearchStrategy[KwArgs]]: attrs, class_and_strat = draw(attrs_and_classes) cls, strat, kw_strat = class_and_strat pos_defs = tuple(draw(strat))