Skip to content

Commit

Permalink
More factories with takes_self
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Dec 25, 2024
1 parent 5d480f4 commit 7f2cedc
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 51 deletions.
4 changes: 4 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,3 +15,5 @@
settings.load_profile("CI")

unstructure_strats = one_of(just(s) for s in UnstructureStrategy)

FeatureFlag: TypeAlias = Literal["always", "never", "sometimes"]
10 changes: 6 additions & 4 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
):
Expand Down Expand Up @@ -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):
"""
Expand Down
6 changes: 3 additions & 3 deletions tests/test_unstructure.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -69,15 +69,15 @@ 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()
instance = nested_class[0]()
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)
Expand Down
10 changes: 7 additions & 3 deletions tests/typed.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)),
)
Expand Down Expand Up @@ -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(
Expand Down
113 changes: 72 additions & 41 deletions tests/untyped.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import keyword
import string
from collections import OrderedDict
from enum import Enum
from typing import (
Any,
Expand All @@ -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(
[
Expand Down Expand Up @@ -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.
Expand All @@ -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),
),
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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], <given
# class strategy>).
attrs_and_classes = st.tuples(lists_of_attrs(defaults=True), simple_class_strategy)
Expand All @@ -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)
)


Expand Down Expand Up @@ -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)

0 comments on commit 7f2cedc

Please sign in to comment.