Skip to content

Instantly share code, notes, and snippets.

@schicks
Last active November 25, 2021 23:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save schicks/adc1b55a4947fb7326876119a38a9b90 to your computer and use it in GitHub Desktop.
Save schicks/adc1b55a4947fb7326876119a38a9b90 to your computer and use it in GitHub Desktop.
Pydantic Generation Strategy

Pydantic Generation Strategy

We frequently use pydantic at Pluralsight to validate data at the edge of a well typed domain. I've been trying to sell my coworkers on using hypothesis for testing, and thought it might go easier if they could generate test data from existing pydantic schemas. I found that it was almost trivial for data classes, but BaseModel subclasses (which are unfortunately much more common in our code) don't play as nicely, and deeply nested schemas can get you into trouble. If anyone has any advice on how to get around the errors that come from the last test, I'd be super greatful.

Requirements for usage

  • pydantic
  • hypothesis
  • pytest
from typing import Dict, List, Tuple
from hypothesis import HealthCheck, assume, given, settings
from hypothesis.strategies import (
SearchStrategy,
composite,
data,
deferred,
fixed_dictionaries,
from_type,
just,
one_of,
register_type_strategy,
tuples,
)
from pydantic import BaseModel
from pydantic.dataclasses import dataclass
@composite
def dataclass_instances(draw, cls=None):
if cls is None:
raise ValueError("No class provided to dataclass strategy")
def recursive_generate(t):
if hasattr(t, "__annotations__"):
return fixed_dictionaries(
{
attribute_name: recursive_generate(attribute_type)
for attribute_name, attribute_type in t.__annotations__.items()
}
)
else:
return from_type(t)
return cls(**draw(recursive_generate(cls)))
hashable_types = one_of(
just(int),
just(str),
just(float),
deferred(lambda: hashable_types.map(lambda t: Tuple[t])),
deferred(
lambda: tuples(hashable_types, hashable_types).map(
lambda t_u: Tuple[t_u[0], t_u[1]]
)
),
deferred(
lambda: tuples(hashable_types, hashable_types, hashable_types).map(
lambda t_u_r: Tuple[t_u_r[0], t_u_r[1], t_u_r[2]]
)
),
)
@composite
def pydantic_dataclasses(
draw, a_value=deferred(lambda: valid_types), b_value=deferred(lambda: valid_types)
):
@dataclass
class DataClassExample:
a: draw(a_value)
b: draw(b_value)
return DataClassExample
@composite
def pydantic_models(
draw, a_value=deferred(lambda: valid_types), b_value=deferred(lambda: valid_types)
):
class ModelExample(BaseModel):
a: draw(a_value)
b: draw(b_value)
register_type_strategy(ModelExample, dataclass_instances)
return ModelExample
valid_types: SearchStrategy = one_of(
hashable_types,
deferred(lambda: valid_types.map(lambda inner: List[inner])),
deferred(
lambda: tuples(hashable_types, valid_types).map(
lambda args: Dict[args[0], args[1]]
)
),
pydantic_dataclasses(),
pydantic_models(),
)
@settings(suppress_health_check=(HealthCheck.too_slow,))
@given(data=data(), target=one_of(pydantic_dataclasses(), pydantic_models()))
def test_pydantic_schemas_can_be_generated(data, target):
assert isinstance(data.draw(dataclass_instances(target)), target)
@settings(suppress_health_check=(HealthCheck.too_slow,))
@given(data=data(), target=pydantic_dataclasses(b_value=pydantic_models()))
def test_nested_schemas_can_be_generated(data, target):
assert isinstance(data.draw(dataclass_instances(target)), target)
@settings(suppress_health_check=(HealthCheck.too_slow,))
@given(data=data(), target=pydantic_dataclasses())
def test_lists_of_dataclasses_can_be_generated(data, target):
generated = data.draw(from_type(List[target]))
assume(generated)
assert isinstance(generated[0], target)
@settings(suppress_health_check=(HealthCheck.too_slow,))
@given(data=data(), target=pydantic_models())
def test_lists_of_models_can_be_generated(data, target):
generated = data.draw(from_type(List[target]))
assume(generated)
assert isinstance(generated[0], target)
@rsokl
Copy link

rsokl commented Jan 19, 2020

Something weird is going on with pydantic and inheritance. You need to register ModelExample

@composite
def pydantic_model(
    draw, a_value=deferred(lambda: valid_types), b_value=deferred(lambda: valid_types)
):
    class ModelExample(BaseModel):
        a: draw(a_value)
        b: draw(b_value)

    register_type_strategy(ModelExample, dataclass_instances)
    return ModelExample

@schicks
Copy link
Author

schicks commented Jan 19, 2020

That makes sense. Updating the file to have saner names and implement that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment