about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py391
1 files changed, 391 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py b/.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py
new file mode 100644
index 00000000..b62234d5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py
@@ -0,0 +1,391 @@
+"""
+Register Hypothesis strategies for Pydantic custom types.
+
+This enables fully-automatic generation of test data for most Pydantic classes.
+
+Note that this module has *no* runtime impact on Pydantic itself; instead it
+is registered as a setuptools entry point and Hypothesis will import it if
+Pydantic is installed.  See also:
+
+https://hypothesis.readthedocs.io/en/latest/strategies.html#registering-strategies-via-setuptools-entry-points
+https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.register_type_strategy
+https://hypothesis.readthedocs.io/en/latest/strategies.html#interaction-with-pytest-cov
+https://docs.pydantic.dev/usage/types/#pydantic-types
+
+Note that because our motivation is to *improve user experience*, the strategies
+are always sound (never generate invalid data) but sacrifice completeness for
+maintainability (ie may be unable to generate some tricky but valid data).
+
+Finally, this module makes liberal use of `# type: ignore[<code>]` pragmas.
+This is because Hypothesis annotates `register_type_strategy()` with
+`(T, SearchStrategy[T])`, but in most cases we register e.g. `ConstrainedInt`
+to generate instances of the builtin `int` type which match the constraints.
+"""
+
+import contextlib
+import datetime
+import ipaddress
+import json
+import math
+from fractions import Fraction
+from typing import Callable, Dict, Type, Union, cast, overload
+
+import hypothesis.strategies as st
+
+import pydantic
+import pydantic.color
+import pydantic.types
+from pydantic.v1.utils import lenient_issubclass
+
+# FilePath and DirectoryPath are explicitly unsupported, as we'd have to create
+# them on-disk, and that's unsafe in general without being told *where* to do so.
+#
+# URLs are unsupported because it's easy for users to define their own strategy for
+# "normal" URLs, and hard for us to define a general strategy which includes "weird"
+# URLs but doesn't also have unpredictable performance problems.
+#
+# conlist() and conset() are unsupported for now, because the workarounds for
+# Cython and Hypothesis to handle parametrized generic types are incompatible.
+# We are rethinking Hypothesis compatibility in Pydantic v2.
+
+# Emails
+try:
+    import email_validator
+except ImportError:  # pragma: no cover
+    pass
+else:
+
+    def is_valid_email(s: str) -> bool:
+        # Hypothesis' st.emails() occasionally generates emails like 0@A0--0.ac
+        # that are invalid according to email-validator, so we filter those out.
+        try:
+            email_validator.validate_email(s, check_deliverability=False)
+            return True
+        except email_validator.EmailNotValidError:  # pragma: no cover
+            return False
+
+    # Note that these strategies deliberately stay away from any tricky Unicode
+    # or other encoding issues; we're just trying to generate *something* valid.
+    st.register_type_strategy(pydantic.EmailStr, st.emails().filter(is_valid_email))  # type: ignore[arg-type]
+    st.register_type_strategy(
+        pydantic.NameEmail,
+        st.builds(
+            '{} <{}>'.format,  # type: ignore[arg-type]
+            st.from_regex('[A-Za-z0-9_]+( [A-Za-z0-9_]+){0,5}', fullmatch=True),
+            st.emails().filter(is_valid_email),
+        ),
+    )
+
+# PyObject - dotted names, in this case taken from the math module.
+st.register_type_strategy(
+    pydantic.PyObject,  # type: ignore[arg-type]
+    st.sampled_from(
+        [cast(pydantic.PyObject, f'math.{name}') for name in sorted(vars(math)) if not name.startswith('_')]
+    ),
+)
+
+# CSS3 Colors; as name, hex, rgb(a) tuples or strings, or hsl strings
+_color_regexes = (
+    '|'.join(
+        (
+            pydantic.color.r_hex_short,
+            pydantic.color.r_hex_long,
+            pydantic.color.r_rgb,
+            pydantic.color.r_rgba,
+            pydantic.color.r_hsl,
+            pydantic.color.r_hsla,
+        )
+    )
+    # Use more precise regex patterns to avoid value-out-of-range errors
+    .replace(pydantic.color._r_sl, r'(?:(\d\d?(?:\.\d+)?|100(?:\.0+)?)%)')
+    .replace(pydantic.color._r_alpha, r'(?:(0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,2}%))')
+    .replace(pydantic.color._r_255, r'(?:((?:\d|\d\d|[01]\d\d|2[0-4]\d|25[0-4])(?:\.\d+)?|255(?:\.0+)?))')
+)
+st.register_type_strategy(
+    pydantic.color.Color,
+    st.one_of(
+        st.sampled_from(sorted(pydantic.color.COLORS_BY_NAME)),
+        st.tuples(
+            st.integers(0, 255),
+            st.integers(0, 255),
+            st.integers(0, 255),
+            st.none() | st.floats(0, 1) | st.floats(0, 100).map('{}%'.format),
+        ),
+        st.from_regex(_color_regexes, fullmatch=True),
+    ),
+)
+
+
+# Card numbers, valid according to the Luhn algorithm
+
+
+def add_luhn_digit(card_number: str) -> str:
+    # See https://en.wikipedia.org/wiki/Luhn_algorithm
+    for digit in '0123456789':
+        with contextlib.suppress(Exception):
+            pydantic.PaymentCardNumber.validate_luhn_check_digit(card_number + digit)
+            return card_number + digit
+    raise AssertionError('Unreachable')  # pragma: no cover
+
+
+card_patterns = (
+    # Note that these patterns omit the Luhn check digit; that's added by the function above
+    '4[0-9]{14}',  # Visa
+    '5[12345][0-9]{13}',  # Mastercard
+    '3[47][0-9]{12}',  # American Express
+    '[0-26-9][0-9]{10,17}',  # other (incomplete to avoid overlap)
+)
+st.register_type_strategy(
+    pydantic.PaymentCardNumber,
+    st.from_regex('|'.join(card_patterns), fullmatch=True).map(add_luhn_digit),  # type: ignore[arg-type]
+)
+
+# UUIDs
+st.register_type_strategy(pydantic.UUID1, st.uuids(version=1))
+st.register_type_strategy(pydantic.UUID3, st.uuids(version=3))
+st.register_type_strategy(pydantic.UUID4, st.uuids(version=4))
+st.register_type_strategy(pydantic.UUID5, st.uuids(version=5))
+
+# Secrets
+st.register_type_strategy(pydantic.SecretBytes, st.binary().map(pydantic.SecretBytes))
+st.register_type_strategy(pydantic.SecretStr, st.text().map(pydantic.SecretStr))
+
+# IP addresses, networks, and interfaces
+st.register_type_strategy(pydantic.IPvAnyAddress, st.ip_addresses())  # type: ignore[arg-type]
+st.register_type_strategy(
+    pydantic.IPvAnyInterface,
+    st.from_type(ipaddress.IPv4Interface) | st.from_type(ipaddress.IPv6Interface),  # type: ignore[arg-type]
+)
+st.register_type_strategy(
+    pydantic.IPvAnyNetwork,
+    st.from_type(ipaddress.IPv4Network) | st.from_type(ipaddress.IPv6Network),  # type: ignore[arg-type]
+)
+
+# We hook into the con***() functions and the ConstrainedNumberMeta metaclass,
+# so here we only have to register subclasses for other constrained types which
+# don't go via those mechanisms.  Then there are the registration hooks below.
+st.register_type_strategy(pydantic.StrictBool, st.booleans())
+st.register_type_strategy(pydantic.StrictStr, st.text())
+
+
+# FutureDate, PastDate
+st.register_type_strategy(pydantic.FutureDate, st.dates(min_value=datetime.date.today() + datetime.timedelta(days=1)))
+st.register_type_strategy(pydantic.PastDate, st.dates(max_value=datetime.date.today() - datetime.timedelta(days=1)))
+
+
+# Constrained-type resolver functions
+#
+# For these ones, we actually want to inspect the type in order to work out a
+# satisfying strategy.  First up, the machinery for tracking resolver functions:
+
+RESOLVERS: Dict[type, Callable[[type], st.SearchStrategy]] = {}  # type: ignore[type-arg]
+
+
+@overload
+def _registered(typ: Type[pydantic.types.T]) -> Type[pydantic.types.T]:
+    pass
+
+
+@overload
+def _registered(typ: pydantic.types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta:
+    pass
+
+
+def _registered(
+    typ: Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]
+) -> Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]:
+    # This function replaces the version in `pydantic.types`, in order to
+    # effect the registration of new constrained types so that Hypothesis
+    # can generate valid examples.
+    pydantic.types._DEFINED_TYPES.add(typ)
+    for supertype, resolver in RESOLVERS.items():
+        if issubclass(typ, supertype):
+            st.register_type_strategy(typ, resolver(typ))  # type: ignore
+            return typ
+    raise NotImplementedError(f'Unknown type {typ!r} has no resolver to register')  # pragma: no cover
+
+
+def resolves(
+    typ: Union[type, pydantic.types.ConstrainedNumberMeta]
+) -> Callable[[Callable[..., st.SearchStrategy]], Callable[..., st.SearchStrategy]]:  # type: ignore[type-arg]
+    def inner(f):  # type: ignore
+        assert f not in RESOLVERS
+        RESOLVERS[typ] = f
+        return f
+
+    return inner
+
+
+# Type-to-strategy resolver functions
+
+
+@resolves(pydantic.JsonWrapper)
+def resolve_json(cls):  # type: ignore[no-untyped-def]
+    try:
+        inner = st.none() if cls.inner_type is None else st.from_type(cls.inner_type)
+    except Exception:  # pragma: no cover
+        finite = st.floats(allow_infinity=False, allow_nan=False)
+        inner = st.recursive(
+            base=st.one_of(st.none(), st.booleans(), st.integers(), finite, st.text()),
+            extend=lambda x: st.lists(x) | st.dictionaries(st.text(), x),  # type: ignore
+        )
+    inner_type = getattr(cls, 'inner_type', None)
+    return st.builds(
+        cls.inner_type.json if lenient_issubclass(inner_type, pydantic.BaseModel) else json.dumps,
+        inner,
+        ensure_ascii=st.booleans(),
+        indent=st.none() | st.integers(0, 16),
+        sort_keys=st.booleans(),
+    )
+
+
+@resolves(pydantic.ConstrainedBytes)
+def resolve_conbytes(cls):  # type: ignore[no-untyped-def]  # pragma: no cover
+    min_size = cls.min_length or 0
+    max_size = cls.max_length
+    if not cls.strip_whitespace:
+        return st.binary(min_size=min_size, max_size=max_size)
+    # Fun with regex to ensure we neither start nor end with whitespace
+    repeats = '{{{},{}}}'.format(
+        min_size - 2 if min_size > 2 else 0,
+        max_size - 2 if (max_size or 0) > 2 else '',
+    )
+    if min_size >= 2:
+        pattern = rf'\W.{repeats}\W'
+    elif min_size == 1:
+        pattern = rf'\W(.{repeats}\W)?'
+    else:
+        assert min_size == 0
+        pattern = rf'(\W(.{repeats}\W)?)?'
+    return st.from_regex(pattern.encode(), fullmatch=True)
+
+
+@resolves(pydantic.ConstrainedDecimal)
+def resolve_condecimal(cls):  # type: ignore[no-untyped-def]
+    min_value = cls.ge
+    max_value = cls.le
+    if cls.gt is not None:
+        assert min_value is None, 'Set `gt` or `ge`, but not both'
+        min_value = cls.gt
+    if cls.lt is not None:
+        assert max_value is None, 'Set `lt` or `le`, but not both'
+        max_value = cls.lt
+    s = st.decimals(min_value, max_value, allow_nan=False, places=cls.decimal_places)
+    if cls.lt is not None:
+        s = s.filter(lambda d: d < cls.lt)
+    if cls.gt is not None:
+        s = s.filter(lambda d: cls.gt < d)
+    return s
+
+
+@resolves(pydantic.ConstrainedFloat)
+def resolve_confloat(cls):  # type: ignore[no-untyped-def]
+    min_value = cls.ge
+    max_value = cls.le
+    exclude_min = False
+    exclude_max = False
+
+    if cls.gt is not None:
+        assert min_value is None, 'Set `gt` or `ge`, but not both'
+        min_value = cls.gt
+        exclude_min = True
+    if cls.lt is not None:
+        assert max_value is None, 'Set `lt` or `le`, but not both'
+        max_value = cls.lt
+        exclude_max = True
+
+    if cls.multiple_of is None:
+        return st.floats(min_value, max_value, exclude_min=exclude_min, exclude_max=exclude_max, allow_nan=False)
+
+    if min_value is not None:
+        min_value = math.ceil(min_value / cls.multiple_of)
+        if exclude_min:
+            min_value = min_value + 1
+    if max_value is not None:
+        assert max_value >= cls.multiple_of, 'Cannot build model with max value smaller than multiple of'
+        max_value = math.floor(max_value / cls.multiple_of)
+        if exclude_max:
+            max_value = max_value - 1
+
+    return st.integers(min_value, max_value).map(lambda x: x * cls.multiple_of)
+
+
+@resolves(pydantic.ConstrainedInt)
+def resolve_conint(cls):  # type: ignore[no-untyped-def]
+    min_value = cls.ge
+    max_value = cls.le
+    if cls.gt is not None:
+        assert min_value is None, 'Set `gt` or `ge`, but not both'
+        min_value = cls.gt + 1
+    if cls.lt is not None:
+        assert max_value is None, 'Set `lt` or `le`, but not both'
+        max_value = cls.lt - 1
+
+    if cls.multiple_of is None or cls.multiple_of == 1:
+        return st.integers(min_value, max_value)
+
+    # These adjustments and the .map handle integer-valued multiples, while the
+    # .filter handles trickier cases as for confloat.
+    if min_value is not None:
+        min_value = math.ceil(Fraction(min_value) / Fraction(cls.multiple_of))
+    if max_value is not None:
+        max_value = math.floor(Fraction(max_value) / Fraction(cls.multiple_of))
+    return st.integers(min_value, max_value).map(lambda x: x * cls.multiple_of)
+
+
+@resolves(pydantic.ConstrainedDate)
+def resolve_condate(cls):  # type: ignore[no-untyped-def]
+    if cls.ge is not None:
+        assert cls.gt is None, 'Set `gt` or `ge`, but not both'
+        min_value = cls.ge
+    elif cls.gt is not None:
+        min_value = cls.gt + datetime.timedelta(days=1)
+    else:
+        min_value = datetime.date.min
+    if cls.le is not None:
+        assert cls.lt is None, 'Set `lt` or `le`, but not both'
+        max_value = cls.le
+    elif cls.lt is not None:
+        max_value = cls.lt - datetime.timedelta(days=1)
+    else:
+        max_value = datetime.date.max
+    return st.dates(min_value, max_value)
+
+
+@resolves(pydantic.ConstrainedStr)
+def resolve_constr(cls):  # type: ignore[no-untyped-def]  # pragma: no cover
+    min_size = cls.min_length or 0
+    max_size = cls.max_length
+
+    if cls.regex is None and not cls.strip_whitespace:
+        return st.text(min_size=min_size, max_size=max_size)
+
+    if cls.regex is not None:
+        strategy = st.from_regex(cls.regex)
+        if cls.strip_whitespace:
+            strategy = strategy.filter(lambda s: s == s.strip())
+    elif cls.strip_whitespace:
+        repeats = '{{{},{}}}'.format(
+            min_size - 2 if min_size > 2 else 0,
+            max_size - 2 if (max_size or 0) > 2 else '',
+        )
+        if min_size >= 2:
+            strategy = st.from_regex(rf'\W.{repeats}\W')
+        elif min_size == 1:
+            strategy = st.from_regex(rf'\W(.{repeats}\W)?')
+        else:
+            assert min_size == 0
+            strategy = st.from_regex(rf'(\W(.{repeats}\W)?)?')
+
+    if min_size == 0 and max_size is None:
+        return strategy
+    elif max_size is None:
+        return strategy.filter(lambda s: min_size <= len(s))
+    return strategy.filter(lambda s: min_size <= len(s) <= max_size)
+
+
+# Finally, register all previously-defined types, and patch in our new function
+for typ in list(pydantic.types._DEFINED_TYPES):
+    _registered(typ)
+pydantic.types._registered = _registered
+st.register_type_strategy(pydantic.Json, resolve_json)