pydantic icon indicating copy to clipboard operation
pydantic copied to clipboard

Hypothesis plugin does not work for ConstrainedStr

Open lsorber opened this issue 4 years ago • 10 comments

@Zac-HD's Hypothesis plugin https://github.com/samuelcolvin/pydantic/pull/2097 works great for some constrained type (e.g. ConstrainedInt), but not for others (e.g. ConstrainedStr):

from pydantic import ConstrainedInt, ConstrainedStr
import hypothesis.strategies as st

class MyInt(ConstrainedInt):
    gt = 10
    lt = 15

class MyStr(ConstrainedStr):
    regex = "[A-Z][0-9]{10}"

st.from_type(MyInt).example()  # As expected, values are between `gt` and `lt`

st.from_type(MyStr).example()  # Returns empty str :-(

Thanks for the awesome work by the way @Zac-HD!


Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())":

             pydantic version: 1.8.1
            pydantic compiled: True
                 install path: /Users/.../lib/python3.8/site-packages/pydantic
               python version: 3.8.8 (default, Feb 24 2021, 13:46:16)  [Clang 10.0.0 ]
                     platform: macOS-10.16-x86_64-i386-64bit
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']

lsorber avatar Mar 04 '21 13:03 lsorber

Ah, right - that's because the registration hook (_registered()) for strings is in the constr() helper function, whereas for ConstrainedInt I was able to attach it to the ConstrainedNumberMeta metaclass.

So MyStr = constr(regex="[A-Z][0-9]{10}") is your workaround.

If explicit inheritance from ConstrainedStr et al is supported as part of the public API, I think the best option is to add a new metaclass for all constrained types which ConstrainedNumberMeta can then inherit from.

Zac-HD avatar Mar 04 '21 13:03 Zac-HD

I believe subclassing Constrained* is preferable over using con* because of https://github.com/samuelcolvin/pydantic/issues/975: mypy will complain that T = con*(...) is not a valid type.

I tried using constr instead of ConstrainedStr and Hypothesis can indeed generate valid examples of it, but not when it's used inside of a BaseModel:

from pydantic import BaseModel, constr
import hypothesis.strategies as st

MyStr = constr(regex="[A-Z][0-9]{10}")

class MyModel(BaseModel):
    foo: MyStr

st.from_type(MyStr).example()  # Generates valid strings
st.from_type(MyModel).example()  # Fails with a ValidationError :-(

It also appears that other constrained types such as ConstrainedList and ConstrainedSet don't work yet either, probably for the same reasons:

from pydantic import ConstrainedList, ConstrainedSet
import hypothesis.strategies as st

class NonEmptyList(ConstrainedList):
    __args__ = [int]
    item_type = int
    min_items = 1

class NonEmptySet(ConstrainedSet):
    __args__ = [int]
    item_type = int
    min_items = 1

st.from_type(NonEmptyList).example()  # Returns [] :-(
st.from_type(NonEmptySet).example()  # Returns NonEmptySet() :-(

lsorber avatar Mar 04 '21 20:03 lsorber

I believe subclassing Constrained* is preferable over using con* because of #975: mypy will complain that T = con*(...) is not a valid type.

I don't have any objection to this, but so far as I know all the docs show the latter T = con*(...) style and it's not clear that explicit inheritance is meant to be supported. Either way, expanding the docs to say would be good.

I tried using constr instead of ConstrainedStr and Hypothesis can indeed generate valid examples of it, but not when it's used inside of a BaseModel:

This is bizzare, and apparently also under-tested 😥
Fixing this would have to start with some detailed investigation that I don't really have time for this week...


It also appears that other constrained types such as ConstrainedList and ConstrainedSet don't work yet either, probably for the same reasons

Different reasons, actually - a particular subtype of e.g. ConstrainedList is a parametrized generic type, and Hypothesis' type-to-strategy resolution machinery only deals in non-parametrized generics (by registering a function which takes a parametrized type object and returns a strategy).

Which would be fine, except that per https://github.com/samuelcolvin/pydantic/pull/2097#issuecomment-753603121 we're waiting on https://github.com/cython/cython/issues/3537!

At present, you just have to provide (or register) a strategy for each model that uses a constrained list or set field. Maybe the docs should explicitly list what the plugin doesn't handle, too?

Zac-HD avatar Mar 05 '21 01:03 Zac-HD

Also bumped into this. As already mentioned, the problem is that mypy doesn't approve of = constr() (dynamic type assignment). So you need to sub-class types. Which then doesn't work with hypothesis, currently.

This should be mentioned in the docs, at least.

tuukkamustonen avatar May 31 '21 14:05 tuukkamustonen

I was able to workaround this by manually calling pydantic.types._registered() for my sub-types, like:

class Str1(StrictStr):
    min_length = 1

pydantic.types._registered(Str1)

Alternative would be to register them via a decorator of a metaclass, but nah, this is the simplest solution to begin with.

tuukkamustonen avatar Jun 01 '21 07:06 tuukkamustonen

I'm seeing the same issue with conlist. Hypothesis is unaware of the min/max length.

mdavis-xyz avatar Jun 03 '21 04:06 mdavis-xyz

A year later, can we have an update on this ? Is this going to be solved ? Or should we go with the workaround ? I think it's a critical feature to be developed

PS : What @tuukkamonsten is proposing works partially. It takes into account the min_lenght constraint for example. However it still does not work for regex constraints

VianneyMI avatar May 10 '22 08:05 VianneyMI

Still blocked on https://github.com/cython/cython/issues/3537

Zac-HD avatar May 10 '22 15:05 Zac-HD

Still blocked on cython/cython#3537

Looks like that is fixed now, or?

biopv avatar May 28 '22 14:05 biopv

Still blocked on cython/cython#3537

Looks like that is fixed now, or?

Unfortunately while the fix has been merged, it still hasn't been released yet and therefore doesn't help us. This is definitely one of the design considerations for Pydantic v2, but I think probably can't be fixed before then 😥

Zac-HD avatar Aug 06 '22 21:08 Zac-HD

We’re no longer actively developing Pydantic V1 (although it will continue to receive security fixes for the next year or so), so we’re closing issues solely related to V1. If you really think this shouldn’t be closed, please comment or create a new issue 🚀.

sydney-runkle avatar Dec 06 '23 19:12 sydney-runkle