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..ceeee544 100644 --- a/tests/untyped.py +++ b/tests/untyped.py @@ -2,7 +2,7 @@ import keyword import string -from collections import OrderedDict +from collections.abc import Callable from enum import Enum from typing import ( Any, @@ -23,11 +23,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 +171,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 +196,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 +213,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 +246,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 +295,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 +306,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 +318,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 +461,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)