扫码登录,获取cookies

This commit is contained in:
2026-03-09 16:10:29 +08:00
parent 754e720ba7
commit 8229208165
7775 changed files with 1150053 additions and 208 deletions

View File

@@ -0,0 +1,30 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
from hypothesis.extra.django._fields import from_field, register_field_strategy
from hypothesis.extra.django._impl import (
LiveServerTestCase,
StaticLiveServerTestCase,
TestCase,
TransactionTestCase,
from_form,
from_model,
)
__all__ = [
"LiveServerTestCase",
"StaticLiveServerTestCase",
"TestCase",
"TransactionTestCase",
"from_field",
"from_model",
"register_field_strategy",
"from_form",
]

View File

@@ -0,0 +1,343 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
import re
import string
from datetime import timedelta
from decimal import Decimal
from functools import lru_cache
from typing import Any, Callable, Dict, Type, TypeVar, Union
import django
from django import forms as df
from django.contrib.auth.forms import UsernameField
from django.core.validators import (
validate_ipv4_address,
validate_ipv6_address,
validate_ipv46_address,
)
from django.db import models as dm
from hypothesis import strategies as st
from hypothesis.errors import InvalidArgument, ResolutionFailed
from hypothesis.internal.validation import check_type
from hypothesis.provisional import urls
from hypothesis.strategies import emails
AnyField = Union[dm.Field, df.Field]
F = TypeVar("F", bound=AnyField)
def numeric_bounds_from_validators(
field, min_value=float("-inf"), max_value=float("inf")
):
for v in field.validators:
if isinstance(v, django.core.validators.MinValueValidator):
min_value = max(min_value, v.limit_value)
elif isinstance(v, django.core.validators.MaxValueValidator):
max_value = min(max_value, v.limit_value)
return min_value, max_value
def integers_for_field(min_value, max_value):
def inner(field):
return st.integers(*numeric_bounds_from_validators(field, min_value, max_value))
return inner
@lru_cache
def timezones():
# From Django 4.0, the default is to use zoneinfo instead of pytz.
assert getattr(django.conf.settings, "USE_TZ", False)
if getattr(django.conf.settings, "USE_DEPRECATED_PYTZ", True):
from hypothesis.extra.pytz import timezones
else:
from hypothesis.strategies import timezones
return timezones()
# Mapping of field types, to strategy objects or functions of (type) -> strategy
_FieldLookUpType = Dict[
Type[AnyField],
Union[st.SearchStrategy, Callable[[Any], st.SearchStrategy]],
]
_global_field_lookup: _FieldLookUpType = {
dm.SmallIntegerField: integers_for_field(-32768, 32767),
dm.IntegerField: integers_for_field(-2147483648, 2147483647),
dm.BigIntegerField: integers_for_field(-9223372036854775808, 9223372036854775807),
dm.PositiveIntegerField: integers_for_field(0, 2147483647),
dm.PositiveSmallIntegerField: integers_for_field(0, 32767),
dm.BooleanField: st.booleans(),
dm.DateField: st.dates(),
dm.EmailField: emails(),
dm.FloatField: st.floats(),
dm.NullBooleanField: st.one_of(st.none(), st.booleans()),
dm.URLField: urls(),
dm.UUIDField: st.uuids(),
df.DateField: st.dates(),
df.DurationField: st.timedeltas(),
df.EmailField: emails(),
df.FloatField: lambda field: st.floats(
*numeric_bounds_from_validators(field), allow_nan=False, allow_infinity=False
),
df.IntegerField: integers_for_field(-2147483648, 2147483647),
df.NullBooleanField: st.one_of(st.none(), st.booleans()),
df.URLField: urls(),
df.UUIDField: st.uuids(),
}
_ipv6_strings = st.one_of(
st.ip_addresses(v=6).map(str),
st.ip_addresses(v=6).map(lambda addr: addr.exploded),
)
def register_for(field_type):
def inner(func):
_global_field_lookup[field_type] = func
return func
return inner
@register_for(dm.DateTimeField)
@register_for(df.DateTimeField)
def _for_datetime(field):
if getattr(django.conf.settings, "USE_TZ", False):
return st.datetimes(timezones=timezones())
return st.datetimes()
def using_sqlite():
try:
return (
getattr(django.conf.settings, "DATABASES", {})
.get("default", {})
.get("ENGINE", "")
.endswith(".sqlite3")
)
except django.core.exceptions.ImproperlyConfigured:
return None
@register_for(dm.TimeField)
def _for_model_time(field):
# SQLITE supports TZ-aware datetimes, but not TZ-aware times.
if getattr(django.conf.settings, "USE_TZ", False) and not using_sqlite():
return st.times(timezones=timezones())
return st.times()
@register_for(df.TimeField)
def _for_form_time(field):
if getattr(django.conf.settings, "USE_TZ", False):
return st.times(timezones=timezones())
return st.times()
@register_for(dm.DurationField)
def _for_duration(field):
# SQLite stores timedeltas as six bytes of microseconds
if using_sqlite():
delta = timedelta(microseconds=2**47 - 1)
return st.timedeltas(-delta, delta)
return st.timedeltas()
@register_for(dm.SlugField)
@register_for(df.SlugField)
def _for_slug(field):
min_size = 1
if getattr(field, "blank", False) or not getattr(field, "required", True):
min_size = 0
return st.text(
alphabet=string.ascii_letters + string.digits,
min_size=min_size,
max_size=field.max_length,
)
@register_for(dm.GenericIPAddressField)
def _for_model_ip(field):
return {
"ipv4": st.ip_addresses(v=4).map(str),
"ipv6": _ipv6_strings,
"both": st.ip_addresses(v=4).map(str) | _ipv6_strings,
}[field.protocol.lower()]
@register_for(df.GenericIPAddressField)
def _for_form_ip(field):
# the IP address form fields have no direct indication of which type
# of address they want, so direct comparison with the validator
# function has to be used instead. Sorry for the potato logic here
if validate_ipv46_address in field.default_validators:
return st.ip_addresses(v=4).map(str) | _ipv6_strings
if validate_ipv4_address in field.default_validators:
return st.ip_addresses(v=4).map(str)
if validate_ipv6_address in field.default_validators:
return _ipv6_strings
raise ResolutionFailed(f"No IP version validator on {field=}")
@register_for(dm.DecimalField)
@register_for(df.DecimalField)
def _for_decimal(field):
min_value, max_value = numeric_bounds_from_validators(field)
bound = Decimal(10**field.max_digits - 1) / (10**field.decimal_places)
return st.decimals(
min_value=max(min_value, -bound),
max_value=min(max_value, bound),
places=field.decimal_places,
)
def length_bounds_from_validators(field):
min_size = 1
max_size = field.max_length
for v in field.validators:
if isinstance(v, django.core.validators.MinLengthValidator):
min_size = max(min_size, v.limit_value)
elif isinstance(v, django.core.validators.MaxLengthValidator):
max_size = min(max_size or v.limit_value, v.limit_value)
return min_size, max_size
@register_for(dm.BinaryField)
def _for_binary(field):
min_size, max_size = length_bounds_from_validators(field)
if getattr(field, "blank", False) or not getattr(field, "required", True):
return st.just(b"") | st.binary(min_size=min_size, max_size=max_size)
return st.binary(min_size=min_size, max_size=max_size)
@register_for(dm.CharField)
@register_for(dm.TextField)
@register_for(df.CharField)
@register_for(df.RegexField)
@register_for(UsernameField)
def _for_text(field):
# We can infer a vastly more precise strategy by considering the
# validators as well as the field type. This is a minimal proof of
# concept, but we intend to leverage the idea much more heavily soon.
# See https://github.com/HypothesisWorks/hypothesis-python/issues/1116
regexes = [
re.compile(v.regex, v.flags) if isinstance(v.regex, str) else v.regex
for v in field.validators
if isinstance(v, django.core.validators.RegexValidator) and not v.inverse_match
]
if regexes:
# This strategy generates according to one of the regexes, and
# filters using the others. It can therefore learn to generate
# from the most restrictive and filter with permissive patterns.
# Not maximally efficient, but it makes pathological cases rarer.
# If you want a challenge: extend https://qntm.org/greenery to
# compute intersections of the full Python regex language.
return st.one_of(*(st.from_regex(r) for r in regexes))
# If there are no (usable) regexes, we use a standard text strategy.
min_size, max_size = length_bounds_from_validators(field)
strategy = st.text(
alphabet=st.characters(exclude_characters="\x00", exclude_categories=("Cs",)),
min_size=min_size,
max_size=max_size,
).filter(lambda s: min_size <= len(s.strip()))
if getattr(field, "blank", False) or not getattr(field, "required", True):
return st.just("") | strategy
return strategy
@register_for(df.BooleanField)
def _for_form_boolean(field):
if field.required:
return st.just(True)
return st.booleans()
def register_field_strategy(
field_type: Type[AnyField], strategy: st.SearchStrategy
) -> None:
"""Add an entry to the global field-to-strategy lookup used by
:func:`~hypothesis.extra.django.from_field`.
``field_type`` must be a subtype of :class:`django.db.models.Field` or
:class:`django.forms.Field`, which must not already be registered.
``strategy`` must be a :class:`~hypothesis.strategies.SearchStrategy`.
"""
if not issubclass(field_type, (dm.Field, df.Field)):
raise InvalidArgument(f"{field_type=} must be a subtype of Field")
check_type(st.SearchStrategy, strategy, "strategy")
if field_type in _global_field_lookup:
raise InvalidArgument(
f"{field_type=} already has a registered "
f"strategy ({_global_field_lookup[field_type]!r})"
)
if issubclass(field_type, dm.AutoField):
raise InvalidArgument("Cannot register a strategy for an AutoField")
_global_field_lookup[field_type] = strategy
def from_field(field: F) -> st.SearchStrategy[Union[F, None]]:
"""Return a strategy for values that fit the given field.
This function is used by :func:`~hypothesis.extra.django.from_form` and
:func:`~hypothesis.extra.django.from_model` for any fields that require
a value, or for which you passed ``...`` (:obj:`python:Ellipsis`) to infer
a strategy from an annotation.
It's pretty similar to the core :func:`~hypothesis.strategies.from_type`
function, with a subtle but important difference: ``from_field`` takes a
Field *instance*, rather than a Field *subtype*, so that it has access to
instance attributes such as string length and validators.
"""
check_type((dm.Field, df.Field), field, "field")
if getattr(field, "choices", False):
choices: list = []
for value, name_or_optgroup in field.choices:
if isinstance(name_or_optgroup, (list, tuple)):
choices.extend(key for key, _ in name_or_optgroup)
else:
choices.append(value)
# form fields automatically include an empty choice, strip it out
if "" in choices:
choices.remove("")
min_size = 1
if isinstance(field, (dm.CharField, dm.TextField)) and field.blank:
choices.insert(0, "")
elif isinstance(field, (df.Field)) and not field.required:
choices.insert(0, "")
min_size = 0
strategy = st.sampled_from(choices)
if isinstance(field, (df.MultipleChoiceField, df.TypedMultipleChoiceField)):
strategy = st.lists(st.sampled_from(choices), min_size=min_size)
else:
if type(field) not in _global_field_lookup:
if getattr(field, "null", False):
return st.none()
raise ResolutionFailed(f"Could not infer a strategy for {field!r}")
strategy = _global_field_lookup[type(field)] # type: ignore
if not isinstance(strategy, st.SearchStrategy):
strategy = strategy(field)
assert isinstance(strategy, st.SearchStrategy)
if field.validators:
def validate(value):
try:
field.run_validators(value)
return True
except django.core.exceptions.ValidationError:
return False
strategy = strategy.filter(validate)
if getattr(field, "null", False):
return st.none() | strategy
return strategy

View File

@@ -0,0 +1,217 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
import sys
import unittest
from functools import partial
from typing import TYPE_CHECKING, Optional, Type, TypeVar, Union
from django import forms as df, test as dt
from django.contrib.staticfiles import testing as dst
from django.core.exceptions import ValidationError
from django.db import IntegrityError, models as dm
from hypothesis import reject, strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.extra.django._fields import from_field
from hypothesis.strategies._internal.utils import defines_strategy
if sys.version_info >= (3, 10):
from types import EllipsisType as EllipsisType
elif TYPE_CHECKING:
from builtins import ellipsis as EllipsisType
else:
EllipsisType = type(Ellipsis)
ModelT = TypeVar("ModelT", bound=dm.Model)
class HypothesisTestCase:
def setup_example(self):
self._pre_setup()
def teardown_example(self, example):
self._post_teardown()
def __call__(self, result=None):
testMethod = getattr(self, self._testMethodName)
if getattr(testMethod, "is_hypothesis_test", False):
return unittest.TestCase.__call__(self, result)
else:
return dt.SimpleTestCase.__call__(self, result)
class TestCase(HypothesisTestCase, dt.TestCase):
pass
class TransactionTestCase(HypothesisTestCase, dt.TransactionTestCase):
pass
class LiveServerTestCase(HypothesisTestCase, dt.LiveServerTestCase):
pass
class StaticLiveServerTestCase(HypothesisTestCase, dst.StaticLiveServerTestCase):
pass
@defines_strategy()
def from_model(
model: Type[ModelT], /, **field_strategies: Union[st.SearchStrategy, EllipsisType]
) -> st.SearchStrategy[ModelT]:
"""Return a strategy for examples of ``model``.
.. warning::
Hypothesis creates saved models. This will run inside your testing
transaction when using the test runner, but if you use the dev console
this will leave debris in your database.
``model`` must be an subclass of :class:`~django:django.db.models.Model`.
Strategies for fields may be passed as keyword arguments, for example
``is_staff=st.just(False)``. In order to support models with fields named
"model", this is a positional-only parameter.
Hypothesis can often infer a strategy based the field type and validators,
and will attempt to do so for any required fields. No strategy will be
inferred for an :class:`~django:django.db.models.AutoField`, nullable field,
foreign key, or field for which a keyword
argument is passed to ``from_model()``. For example,
a Shop type with a foreign key to Company could be generated with::
shop_strategy = from_model(Shop, company=from_model(Company))
Like for :func:`~hypothesis.strategies.builds`, you can pass
``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for
a field which has a default value instead of using the default.
"""
if not issubclass(model, dm.Model):
raise InvalidArgument(f"{model=} must be a subtype of Model")
fields_by_name = {f.name: f for f in model._meta.concrete_fields}
for name, value in sorted(field_strategies.items()):
if value is ...:
field_strategies[name] = from_field(fields_by_name[name])
for name, field in sorted(fields_by_name.items()):
if (
name not in field_strategies
and not field.auto_created
and field.default is dm.fields.NOT_PROVIDED
):
field_strategies[name] = from_field(field)
for field in field_strategies:
if model._meta.get_field(field).primary_key:
# The primary key is generated as part of the strategy. We
# want to find any existing row with this primary key and
# overwrite its contents.
kwargs = {field: field_strategies.pop(field)}
kwargs["defaults"] = st.fixed_dictionaries(field_strategies) # type: ignore
return _models_impl(st.builds(model.objects.update_or_create, **kwargs))
# The primary key is not generated as part of the strategy, so we
# just match against any row that has the same value for all
# fields.
return _models_impl(st.builds(model.objects.get_or_create, **field_strategies))
@st.composite
def _models_impl(draw, strat):
"""Handle the nasty part of drawing a value for models()"""
try:
return draw(strat)[0]
except IntegrityError:
reject()
@defines_strategy()
def from_form(
form: Type[df.Form],
form_kwargs: Optional[dict] = None,
**field_strategies: Union[st.SearchStrategy, EllipsisType],
) -> st.SearchStrategy[df.Form]:
"""Return a strategy for examples of ``form``.
``form`` must be an subclass of :class:`~django:django.forms.Form`.
Strategies for fields may be passed as keyword arguments, for example
``is_staff=st.just(False)``.
Hypothesis can often infer a strategy based the field type and validators,
and will attempt to do so for any required fields. No strategy will be
inferred for a disabled field or field for which a keyword argument
is passed to ``from_form()``.
This function uses the fields of an unbound ``form`` instance to determine
field strategies, any keyword arguments needed to instantiate the unbound
``form`` instance can be passed into ``from_form()`` as a dict with the
keyword ``form_kwargs``. E.g.::
shop_strategy = from_form(Shop, form_kwargs={"company_id": 5})
Like for :func:`~hypothesis.strategies.builds`, you can pass
``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for
a field which has a default value instead of using the default.
"""
# currently unsupported:
# ComboField
# FilePathField
# FileField
# ImageField
form_kwargs = form_kwargs or {}
if not issubclass(form, df.BaseForm):
raise InvalidArgument(f"{form=} must be a subtype of Form")
# Forms are a little bit different from models. Model classes have
# all their fields defined, whereas forms may have different fields
# per-instance. So, we ought to instantiate the form and get the
# fields from the instance, thus we need to accept the kwargs for
# instantiation as well as the explicitly defined strategies
unbound_form = form(**form_kwargs)
fields_by_name = {}
for name, field in unbound_form.fields.items():
if isinstance(field, df.MultiValueField):
# PS: So this is a little strange, but MultiValueFields must
# have their form data encoded in a particular way for the
# values to actually be picked up by the widget instances'
# ``value_from_datadict``.
# E.g. if a MultiValueField named 'mv_field' has 3
# sub-fields then the ``value_from_datadict`` will look for
# 'mv_field_0', 'mv_field_1', and 'mv_field_2'. Here I'm
# decomposing the individual sub-fields into the names that
# the form validation process expects
for i, _field in enumerate(field.fields):
fields_by_name[f"{name}_{i}"] = _field
else:
fields_by_name[name] = field
for name, value in sorted(field_strategies.items()):
if value is ...:
field_strategies[name] = from_field(fields_by_name[name])
for name, field in sorted(fields_by_name.items()):
if name not in field_strategies and not field.disabled:
field_strategies[name] = from_field(field)
return _forms_impl(
st.builds(
partial(form, **form_kwargs),
data=st.fixed_dictionaries(field_strategies), # type: ignore
)
)
@st.composite
def _forms_impl(draw, strat):
"""Handle the nasty part of drawing a value for from_form()"""
try:
return draw(strat)
except ValidationError:
reject()