Source code for hypothesis.extra.django._fields

# coding=utf-8
#
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Most of this work is copyright (C) 2013-2019 David R. MacIver
# (david@drmaciver.com), but it contains contributions by others. See
# CONTRIBUTING.rst for a full list of people who may hold copyright, and
# consult the git log if you need to determine who owns an individual
# contribution.
#
# 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/.
#
# END HEADER

from __future__ import absolute_import, division, print_function

import re
import string
from datetime import timedelta
from decimal import Decimal

import django
import django.db.models as dm
import django.forms as df
from django.core.validators import (
    validate_ipv4_address,
    validate_ipv6_address,
    validate_ipv46_address,
)

import hypothesis.strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.extra.pytz import timezones
from hypothesis.internal.validation import check_type
from hypothesis.provisional import ip4_addr_strings, ip6_addr_strings, urls
from hypothesis.strategies import emails

if False:
    from datetime import tzinfo  # noqa
    from typing import Any, Dict, Type, Optional, List, Text, Callable, Union  # noqa


# Mapping of field types, to strategy objects or functions of (type) -> strategy
_global_field_lookup = {
    dm.SmallIntegerField: st.integers(-32768, 32767),
    dm.IntegerField: st.integers(-2147483648, 2147483647),
    dm.BigIntegerField: st.integers(-9223372036854775808, 9223372036854775807),
    dm.PositiveIntegerField: st.integers(0, 2147483647),
    dm.PositiveSmallIntegerField: st.integers(0, 32767),
    dm.BinaryField: st.binary(),
    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: st.floats(allow_nan=False, allow_infinity=False),
    df.IntegerField: st.integers(-2147483648, 2147483647),
    df.NullBooleanField: st.one_of(st.none(), st.booleans()),
    df.URLField: urls(),
    df.UUIDField: st.uuids(),
}  # type: Dict[Any, Union[st.SearchStrategy, Callable[[Any], st.SearchStrategy]]]


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 dict(
        ipv4=ip4_addr_strings(),
        ipv6=ip6_addr_strings(),
        both=ip4_addr_strings() | ip6_addr_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 ip4_addr_strings() | ip6_addr_strings()
    if validate_ipv4_address in field.default_validators:
        return ip4_addr_strings()
    if validate_ipv6_address in field.default_validators:
        return ip6_addr_strings()
    raise InvalidArgument("No IP version validator on field=%r" % field)


@register_for(dm.DecimalField)
@register_for(df.DecimalField)
def _for_decimal(field):
    bound = Decimal(10 ** field.max_digits - 1) / (10 ** field.decimal_places)
    return st.decimals(min_value=-bound, max_value=bound, places=field.decimal_places)


@register_for(dm.CharField)
@register_for(dm.TextField)
@register_for(df.CharField)
@register_for(df.RegexField)
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 = 1
    if getattr(field, "blank", False) or not getattr(field, "required", True):
        min_size = 0
    strategy = st.text(
        alphabet=st.characters(
            blacklist_characters=u"\x00", blacklist_categories=("Cs",)
        ),
        min_size=min_size,
        max_size=field.max_length,
    )
    if getattr(field, "required", True):
        strategy = strategy.filter(lambda s: s.strip())
    return strategy


@register_for(df.BooleanField)
def _for_form_boolean(field):
    if field.required:
        return st.just(True)
    return st.booleans()


[docs]def register_field_strategy(field_type, strategy): # type: (Type[dm.Field], st.SearchStrategy) -> None """Add an entry to the global field-to-strategy lookup used by from_field. ``field_type`` must be a subtype of django.db.models.Field, which must not already be registered. ``strategy`` must be a SearchStrategy. """ if not issubclass(field_type, (dm.Field, df.Field)): raise InvalidArgument( "field_type=%r must be a subtype of Field" % (field_type,) ) check_type(st.SearchStrategy, strategy, "strategy") if field_type in _global_field_lookup: raise InvalidArgument( "field_type=%r already has a registered strategy (%r)" % (field_type, _global_field_lookup[field_type]) ) 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): # type: (Type[dm.Field]) -> st.SearchStrategy[dm.Field] """Return a strategy for values that fit the given field. This is pretty similar to the core `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 = [] # type: 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 u"" in choices: choices.remove(u"") min_size = 1 if isinstance(field, (dm.CharField, dm.TextField)) and field.blank: choices.insert(0, u"") elif isinstance(field, (df.Field)) and not field.required: choices.insert(0, u"") 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 InvalidArgument("Could not infer a strategy for %r", (field,)) 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