# 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 inspect import Parameter, signature
from typing import TYPE_CHECKING, Optional, Type, 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.internal.reflection import define_function_signature
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)
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[dm.Model], **field_strategies: Union[st.SearchStrategy, EllipsisType]
) -> st.SearchStrategy:
"""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 len(model) == 1:
m_type = model[0]
elif len(model) > 1:
raise TypeError("Too many positional arguments")
else:
raise TypeError("Missing required positional argument `model`")
if not issubclass(m_type, dm.Model):
raise InvalidArgument(f"model={model!r} must be a subtype of Model")
fields_by_name = {f.name: f for f in m_type._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 m_type._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(m_type.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(m_type.objects.get_or_create, **field_strategies))
if sys.version_info[:2] >= (3, 8):
# See notes above definition of st.builds() - this signature is compatible
# and better matches the semantics of the function. Great for documentation!
sig = signature(from_model)
params = list(sig.parameters.values())
params[0] = params[0].replace(kind=Parameter.POSITIONAL_ONLY)
from_model = define_function_signature(
name=from_model.__name__,
docstring=from_model.__doc__,
signature=sig.replace(parameters=params),
)(from_model)
@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()
@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()