扫码登录,获取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,141 @@
# 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.strategies._internal import SearchStrategy
from hypothesis.strategies._internal.collections import tuples
from hypothesis.strategies._internal.core import (
DataObject,
DrawFn,
binary,
booleans,
builds,
characters,
complex_numbers,
composite,
data,
decimals,
deferred,
dictionaries,
emails,
fixed_dictionaries,
fractions,
from_regex,
from_type,
frozensets,
functions,
iterables,
lists,
permutations,
random_module,
randoms,
recursive,
register_type_strategy,
runner,
sampled_from,
sets,
shared,
slices,
text,
uuids,
)
from hypothesis.strategies._internal.datetime import (
dates,
datetimes,
timedeltas,
times,
timezone_keys,
timezones,
)
from hypothesis.strategies._internal.ipaddress import ip_addresses
from hypothesis.strategies._internal.misc import just, none, nothing
from hypothesis.strategies._internal.numbers import floats, integers
from hypothesis.strategies._internal.strategies import one_of
from hypothesis.strategies._internal.utils import _strategies
# The implementation of all of these lives in `_strategies.py`, but we
# re-export them via this module to avoid exposing implementation details
# to over-zealous tab completion in editors that do not respect __all__.
__all__ = [
"binary",
"booleans",
"builds",
"characters",
"complex_numbers",
"composite",
"data",
"DataObject",
"dates",
"datetimes",
"decimals",
"deferred",
"dictionaries",
"DrawFn",
"emails",
"fixed_dictionaries",
"floats",
"fractions",
"from_regex",
"from_type",
"frozensets",
"functions",
"integers",
"ip_addresses",
"iterables",
"just",
"lists",
"none",
"nothing",
"one_of",
"permutations",
"random_module",
"randoms",
"recursive",
"register_type_strategy",
"runner",
"sampled_from",
"sets",
"shared",
"slices",
"text",
"timedeltas",
"times",
"timezone_keys",
"timezones",
"tuples",
"uuids",
"SearchStrategy",
]
def _check_exports(_public):
assert set(__all__) == _public, (set(__all__) - _public, _public - set(__all__))
# Verify that all exported strategy functions were registered with
# @declares_strategy.
existing_strategies = set(_strategies) - {"_maybe_nil_uuids"}
exported_strategies = set(__all__) - {
"DataObject",
"DrawFn",
"SearchStrategy",
"composite",
"register_type_strategy",
}
assert existing_strategies == exported_strategies, (
existing_strategies - exported_strategies,
exported_strategies - existing_strategies,
)
_check_exports({n for n in dir() if n[0] not in "_@"})
del _check_exports

View File

@@ -0,0 +1,16 @@
# 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/.
"""Package defining SearchStrategy, which is the core type that Hypothesis uses
to explore data."""
from .strategies import SearchStrategy, check_strategy
__all__ = ["SearchStrategy", "check_strategy"]

View File

@@ -0,0 +1,179 @@
# 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 functools import reduce
from itertools import chain
import attr
from hypothesis import strategies as st
from hypothesis.errors import ResolutionFailed
from hypothesis.internal.compat import get_type_hints
from hypothesis.strategies._internal.core import BuildsStrategy
from hypothesis.strategies._internal.types import is_a_type, type_sorting_key
from hypothesis.utils.conventions import infer
def get_attribute_by_alias(fields, alias, *, target=None):
"""
Get an attrs attribute by its alias, rather than its name (compare
getattr(fields, name)).
``target`` is used only to provide a nicer error message, and can be safely
omitted.
"""
# attrs supports defining an alias for a field, which is the name used when
# defining __init__. The init args are what we pull from when determining
# what parameters we need to supply to the class, so it's what we need to
# match against as well, rather than the class-level attribute name.
matched_fields = [f for f in fields if f.alias == alias]
if not matched_fields:
raise TypeError(
f"Unexpected keyword argument {alias} for attrs class"
f"{f' {target}' if target else ''}. Expected one of "
f"{[f.name for f in fields]}"
)
# alias is used as an arg in __init__, so it is guaranteed to be unique, if
# it exists.
assert len(matched_fields) == 1
return matched_fields[0]
def from_attrs(target, args, kwargs, to_infer):
"""An internal version of builds(), specialised for Attrs classes."""
fields = attr.fields(target)
kwargs = {k: v for k, v in kwargs.items() if v is not infer}
for name in to_infer:
attrib = get_attribute_by_alias(fields, name, target=target)
kwargs[name] = from_attrs_attribute(attrib, target)
# We might make this strategy more efficient if we added a layer here that
# retries drawing if validation fails, for improved composition.
# The treatment of timezones in datetimes() provides a precedent.
return BuildsStrategy(target, args, kwargs)
def from_attrs_attribute(attrib, target):
"""Infer a strategy from the metadata on an attr.Attribute object."""
# Try inferring from the default argument. Note that this will only help if
# the user passed `...` to builds() for this attribute, but in that case
# we use it as the minimal example.
default = st.nothing()
if isinstance(attrib.default, attr.Factory):
if not attrib.default.takes_self:
default = st.builds(attrib.default.factory)
elif attrib.default is not attr.NOTHING:
default = st.just(attrib.default)
# Try inferring None, exact values, or type from attrs provided validators.
null = st.nothing() # updated to none() on seeing an OptionalValidator
in_collections = [] # list of in_ validator collections to sample from
validator_types = set() # type constraints to pass to types_to_strategy()
if attrib.validator is not None:
validator = attrib.validator
if isinstance(validator, attr.validators._OptionalValidator):
null = st.none()
validator = validator.validator
if isinstance(validator, attr.validators._AndValidator):
vs = validator._validators
else:
vs = [validator]
for v in vs:
if isinstance(v, attr.validators._InValidator):
if isinstance(v.options, str):
in_collections.append(list(all_substrings(v.options)))
else:
in_collections.append(v.options)
elif isinstance(v, attr.validators._InstanceOfValidator):
validator_types.add(v.type)
# This is the important line. We compose the final strategy from various
# parts. The default value, if any, is the minimal shrink, followed by
# None (again, if allowed). We then prefer to sample from values passed
# to an in_ validator if available, but infer from a type otherwise.
# Pick one because (sampled_from((1, 2)) | from_type(int)) would usually
# fail validation by generating e.g. zero!
if in_collections:
sample = st.sampled_from(list(ordered_intersection(in_collections)))
strat = default | null | sample
else:
strat = default | null | types_to_strategy(attrib, validator_types)
# Better to give a meaningful error here than an opaque "could not draw"
# when we try to get a value but have lost track of where this was created.
if strat.is_empty:
raise ResolutionFailed(
"Cannot infer a strategy from the default, validator, type, or "
f"converter for attribute={attrib!r} of class={target!r}"
)
return strat
def types_to_strategy(attrib, types):
"""Find all the type metadata for this attribute, reconcile it, and infer a
strategy from the mess."""
# If we know types from the validator(s), that's sufficient.
if len(types) == 1:
(typ,) = types
if isinstance(typ, tuple):
return st.one_of(*map(st.from_type, typ))
return st.from_type(typ)
elif types:
# We have a list of tuples of types, and want to find a type
# (or tuple of types) that is a subclass of all of of them.
type_tuples = [k if isinstance(k, tuple) else (k,) for k in types]
# Flatten the list, filter types that would fail validation, and
# sort so that ordering is stable between runs and shrinks well.
allowed = [
t
for t in set(sum(type_tuples, ()))
if all(issubclass(t, tup) for tup in type_tuples)
]
allowed.sort(key=type_sorting_key)
return st.one_of([st.from_type(t) for t in allowed])
# Otherwise, try the `type` attribute as a fallback, and finally try
# the type hints on a converter (desperate!) before giving up.
if is_a_type(getattr(attrib, "type", None)):
# The convoluted test is because variable annotations may be stored
# in string form; attrs doesn't evaluate them and we don't handle them.
# See PEP 526, PEP 563, and Hypothesis issue #1004 for details.
return st.from_type(attrib.type)
converter = getattr(attrib, "converter", None)
if isinstance(converter, type):
return st.from_type(converter)
elif callable(converter):
hints = get_type_hints(converter)
if "return" in hints:
return st.from_type(hints["return"])
return st.nothing()
def ordered_intersection(in_):
"""Set union of n sequences, ordered for reproducibility across runs."""
intersection = reduce(set.intersection, in_, set(in_[0]))
for x in chain.from_iterable(in_):
if x in intersection:
yield x
intersection.remove(x)
def all_substrings(s):
"""Generate all substrings of `s`, in order of length then occurrence.
Includes the empty string (first), and any duplicates that are present.
>>> list(all_substrings('010'))
['', '0', '1', '0', '01', '10', '010']
"""
yield s[:0]
for n, _ in enumerate(s):
for i in range(len(s) - n):
yield s[i : i + n + 1]

View File

@@ -0,0 +1,337 @@
# 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 copy
from typing import Any, Iterable, Tuple, overload
from hypothesis.errors import InvalidArgument
from hypothesis.internal.conjecture import utils as cu
from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy
from hypothesis.internal.conjecture.utils import combine_labels
from hypothesis.internal.reflection import is_identity_function
from hypothesis.strategies._internal.strategies import (
T3,
T4,
T5,
Ex,
MappedSearchStrategy,
SearchStrategy,
T,
check_strategy,
filter_not_satisfied,
)
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
class TupleStrategy(SearchStrategy):
"""A strategy responsible for fixed length tuples based on heterogeneous
strategies for each of their elements."""
def __init__(self, strategies: Iterable[SearchStrategy[Any]]):
super().__init__()
self.element_strategies = tuple(strategies)
def do_validate(self):
for s in self.element_strategies:
s.validate()
def calc_label(self):
return combine_labels(
self.class_label, *(s.label for s in self.element_strategies)
)
def __repr__(self):
tuple_string = ", ".join(map(repr, self.element_strategies))
return f"TupleStrategy(({tuple_string}))"
def calc_has_reusable_values(self, recur):
return all(recur(e) for e in self.element_strategies)
def do_draw(self, data):
return tuple(data.draw(e) for e in self.element_strategies)
def calc_is_empty(self, recur):
return any(recur(e) for e in self.element_strategies)
@overload
def tuples() -> SearchStrategy[Tuple[()]]: # pragma: no cover
...
@overload
def tuples(__a1: SearchStrategy[Ex]) -> SearchStrategy[Tuple[Ex]]: # pragma: no cover
...
@overload
def tuples(
__a1: SearchStrategy[Ex], __a2: SearchStrategy[T]
) -> SearchStrategy[Tuple[Ex, T]]: # pragma: no cover
...
@overload
def tuples(
__a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3]
) -> SearchStrategy[Tuple[Ex, T, T3]]: # pragma: no cover
...
@overload
def tuples(
__a1: SearchStrategy[Ex],
__a2: SearchStrategy[T],
__a3: SearchStrategy[T3],
__a4: SearchStrategy[T4],
) -> SearchStrategy[Tuple[Ex, T, T3, T4]]: # pragma: no cover
...
@overload
def tuples(
__a1: SearchStrategy[Ex],
__a2: SearchStrategy[T],
__a3: SearchStrategy[T3],
__a4: SearchStrategy[T4],
__a5: SearchStrategy[T5],
) -> SearchStrategy[Tuple[Ex, T, T3, T4, T5]]: # pragma: no cover
...
@overload
def tuples(
*args: SearchStrategy[Any],
) -> SearchStrategy[Tuple[Any, ...]]: # pragma: no cover
...
@cacheable
@defines_strategy()
def tuples(*args: SearchStrategy[Any]) -> SearchStrategy[Tuple[Any, ...]]:
"""Return a strategy which generates a tuple of the same length as args by
generating the value at index i from args[i].
e.g. tuples(integers(), integers()) would generate a tuple of length
two with both values an integer.
Examples from this strategy shrink by shrinking their component parts.
"""
for arg in args:
check_strategy(arg)
return TupleStrategy(args)
class ListStrategy(SearchStrategy):
"""A strategy for lists which takes a strategy for its elements and the
allowed lengths, and generates lists with the correct size and contents."""
_nonempty_filters: tuple = (bool, len, tuple, list)
def __init__(self, elements, min_size=0, max_size=float("inf")):
super().__init__()
self.min_size = min_size or 0
self.max_size = max_size if max_size is not None else float("inf")
assert 0 <= self.min_size <= self.max_size
self.average_size = min(
max(self.min_size * 2, self.min_size + 5),
0.5 * (self.min_size + self.max_size),
)
self.element_strategy = elements
def calc_label(self):
return combine_labels(self.class_label, self.element_strategy.label)
def do_validate(self):
self.element_strategy.validate()
if self.is_empty:
raise InvalidArgument(
"Cannot create non-empty lists with elements drawn from "
f"strategy {self.element_strategy!r} because it has no values."
)
if self.element_strategy.is_empty and 0 < self.max_size < float("inf"):
raise InvalidArgument(
f"Cannot create a collection of max_size={self.max_size!r}, "
"because no elements can be drawn from the element strategy "
f"{self.element_strategy!r}"
)
def calc_is_empty(self, recur):
if self.min_size == 0:
return False
else:
return recur(self.element_strategy)
def do_draw(self, data):
if self.element_strategy.is_empty:
assert self.min_size == 0
return []
elements = cu.many(
data,
min_size=self.min_size,
max_size=self.max_size,
average_size=self.average_size,
)
result = []
while elements.more():
result.append(data.draw(self.element_strategy))
return result
def __repr__(self):
return "{}({!r}, min_size={!r}, max_size={!r})".format(
self.__class__.__name__, self.element_strategy, self.min_size, self.max_size
)
def filter(self, condition):
if condition in self._nonempty_filters or is_identity_function(condition):
assert self.max_size >= 1, "Always-empty is special cased in st.lists()"
if self.min_size >= 1:
return self
new = copy.copy(self)
new.min_size = 1
return new
return super().filter(condition)
class UniqueListStrategy(ListStrategy):
def __init__(self, elements, min_size, max_size, keys, tuple_suffixes):
super().__init__(elements, min_size, max_size)
self.keys = keys
self.tuple_suffixes = tuple_suffixes
def do_draw(self, data):
if self.element_strategy.is_empty:
assert self.min_size == 0
return []
elements = cu.many(
data,
min_size=self.min_size,
max_size=self.max_size,
average_size=self.average_size,
)
seen_sets = tuple(set() for _ in self.keys)
result = []
# We construct a filtered strategy here rather than using a check-and-reject
# approach because some strategies have special logic for generation under a
# filter, and FilteredStrategy can consolidate multiple filters.
def not_yet_in_unique_list(val):
return all(key(val) not in seen for key, seen in zip(self.keys, seen_sets))
filtered = self.element_strategy._filter_for_filtered_draw(
not_yet_in_unique_list
)
while elements.more():
value = filtered.do_filtered_draw(data)
if value is filter_not_satisfied:
elements.reject(f"Aborted test because unable to satisfy {filtered!r}")
else:
for key, seen in zip(self.keys, seen_sets):
seen.add(key(value))
if self.tuple_suffixes is not None:
value = (value, *data.draw(self.tuple_suffixes))
result.append(value)
assert self.max_size >= len(result) >= self.min_size
return result
class UniqueSampledListStrategy(UniqueListStrategy):
def do_draw(self, data):
should_draw = cu.many(
data,
min_size=self.min_size,
max_size=self.max_size,
average_size=self.average_size,
)
seen_sets = tuple(set() for _ in self.keys)
result = []
remaining = LazySequenceCopy(self.element_strategy.elements)
while remaining and should_draw.more():
i = len(remaining) - 1
j = data.draw_integer(0, i)
if j != i:
remaining[i], remaining[j] = remaining[j], remaining[i]
value = self.element_strategy._transform(remaining.pop())
if value is not filter_not_satisfied and all(
key(value) not in seen for key, seen in zip(self.keys, seen_sets)
):
for key, seen in zip(self.keys, seen_sets):
seen.add(key(value))
if self.tuple_suffixes is not None:
value = (value, *data.draw(self.tuple_suffixes))
result.append(value)
else:
should_draw.reject(
"UniqueSampledListStrategy filter not satisfied or value already seen"
)
assert self.max_size >= len(result) >= self.min_size
return result
class FixedKeysDictStrategy(MappedSearchStrategy):
"""A strategy which produces dicts with a fixed set of keys, given a
strategy for each of their equivalent values.
e.g. {'foo' : some_int_strategy} would generate dicts with the single
key 'foo' mapping to some integer.
"""
def __init__(self, strategy_dict):
self.dict_type = type(strategy_dict)
self.keys = tuple(strategy_dict.keys())
super().__init__(strategy=TupleStrategy(strategy_dict[k] for k in self.keys))
def calc_is_empty(self, recur):
return recur(self.mapped_strategy)
def __repr__(self):
return f"FixedKeysDictStrategy({self.keys!r}, {self.mapped_strategy!r})"
def pack(self, value):
return self.dict_type(zip(self.keys, value))
class FixedAndOptionalKeysDictStrategy(SearchStrategy):
"""A strategy which produces dicts with a fixed set of keys, given a
strategy for each of their equivalent values.
e.g. {'foo' : some_int_strategy} would generate dicts with the single
key 'foo' mapping to some integer.
"""
def __init__(self, strategy_dict, optional):
self.required = strategy_dict
self.fixed = FixedKeysDictStrategy(strategy_dict)
self.optional = optional
def calc_is_empty(self, recur):
return recur(self.fixed)
def __repr__(self):
return f"FixedAndOptionalKeysDictStrategy({self.required!r}, {self.optional!r})"
def do_draw(self, data):
result = data.draw(self.fixed)
remaining = [k for k, v in self.optional.items() if not v.is_empty]
should_draw = cu.many(
data, min_size=0, max_size=len(remaining), average_size=len(remaining) / 2
)
while should_draw.more():
j = data.draw_integer(0, len(remaining) - 1)
remaining[-1], remaining[j] = remaining[j], remaining[-1]
key = remaining.pop()
result[key] = data.draw(self.optional[key])
return result

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,468 @@
# 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 datetime as dt
from calendar import monthrange
from functools import lru_cache
from importlib import resources
from pathlib import Path
from typing import Optional
from hypothesis.errors import InvalidArgument
from hypothesis.internal.validation import check_type, check_valid_interval
from hypothesis.strategies._internal.core import sampled_from
from hypothesis.strategies._internal.misc import just, none
from hypothesis.strategies._internal.strategies import SearchStrategy
from hypothesis.strategies._internal.utils import defines_strategy
# The zoneinfo module, required for the timezones() and timezone_keys()
# strategies, is new in Python 3.9 and the backport might be missing.
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo # type: ignore
except ImportError:
# We raise an error recommending `pip install hypothesis[zoneinfo]`
# when timezones() or timezone_keys() strategies are actually used.
zoneinfo = None # type: ignore
DATENAMES = ("year", "month", "day")
TIMENAMES = ("hour", "minute", "second", "microsecond")
def is_pytz_timezone(tz):
if not isinstance(tz, dt.tzinfo):
return False
module = type(tz).__module__
return module == "pytz" or module.startswith("pytz.")
def replace_tzinfo(value, timezone):
if is_pytz_timezone(timezone):
# Pytz timezones are a little complicated, and using the .replace method
# can cause some weird issues, so we use their special "localize" instead.
#
# We use the fold attribute as a convenient boolean for is_dst, even though
# they're semantically distinct. For ambiguous or imaginary hours, fold says
# whether you should use the offset that applies before the gap (fold=0) or
# the offset that applies after the gap (fold=1). is_dst says whether you
# should choose the side that is "DST" or "STD" (STD->STD or DST->DST
# transitions are unclear as you might expect).
#
# WARNING: this is INCORRECT for timezones with negative DST offsets such as
# "Europe/Dublin", but it's unclear what we could do instead beyond
# documenting the problem and recommending use of `dateutil` instead.
return timezone.localize(value, is_dst=not value.fold)
return value.replace(tzinfo=timezone)
def datetime_does_not_exist(value):
"""This function tests whether the given datetime can be round-tripped to and
from UTC. It is an exact inverse of (and very similar to) the dateutil method
https://dateutil.readthedocs.io/en/stable/tz.html#dateutil.tz.datetime_exists
"""
# Naive datetimes cannot be imaginary, but we need this special case because
# chaining .astimezone() ends with *the system local timezone*, not None.
# See bug report in https://github.com/HypothesisWorks/hypothesis/issues/2662
if value.tzinfo is None:
return False
try:
# Does the naive portion of the datetime change when round-tripped to
# UTC? If so, or if this overflows, we say that it does not exist.
roundtrip = value.astimezone(dt.timezone.utc).astimezone(value.tzinfo)
except OverflowError:
# Overflows at datetime.min or datetime.max boundary condition.
# Rejecting these is acceptable, because timezones are close to
# meaningless before ~1900 and subject to a lot of change by
# 9999, so it should be a very small fraction of possible values.
return True
if (
value.tzinfo is not roundtrip.tzinfo
and value.utcoffset() != roundtrip.utcoffset()
):
# This only ever occurs during imaginary (i.e. nonexistent) datetimes,
# and only for pytz timezones which do not follow PEP-495 semantics.
# (may exclude a few other edge cases, but you should use zoneinfo anyway)
return True
assert value.tzinfo is roundtrip.tzinfo, "so only the naive portions are compared"
return value != roundtrip
def draw_capped_multipart(
data, min_value, max_value, duration_names=DATENAMES + TIMENAMES
):
assert isinstance(min_value, (dt.date, dt.time, dt.datetime))
assert type(min_value) == type(max_value)
assert min_value <= max_value
result = {}
cap_low, cap_high = True, True
for name in duration_names:
low = getattr(min_value if cap_low else dt.datetime.min, name)
high = getattr(max_value if cap_high else dt.datetime.max, name)
if name == "day" and not cap_high:
_, high = monthrange(**result)
if name == "year":
val = data.draw_integer(low, high, shrink_towards=2000)
else:
val = data.draw_integer(low, high)
result[name] = val
cap_low = cap_low and val == low
cap_high = cap_high and val == high
if hasattr(min_value, "fold"):
# The `fold` attribute is ignored in comparison of naive datetimes.
# In tz-aware datetimes it would require *very* invasive changes to
# the logic above, and be very sensitive to the specific timezone
# (at the cost of efficient shrinking and mutation), so at least for
# now we stick with the status quo and generate it independently.
result["fold"] = data.draw_integer(0, 1)
return result
class DatetimeStrategy(SearchStrategy):
def __init__(self, min_value, max_value, timezones_strat, allow_imaginary):
assert isinstance(min_value, dt.datetime)
assert isinstance(max_value, dt.datetime)
assert min_value.tzinfo is None
assert max_value.tzinfo is None
assert min_value <= max_value
assert isinstance(timezones_strat, SearchStrategy)
assert isinstance(allow_imaginary, bool)
self.min_value = min_value
self.max_value = max_value
self.tz_strat = timezones_strat
self.allow_imaginary = allow_imaginary
def do_draw(self, data):
# We start by drawing a timezone, and an initial datetime.
tz = data.draw(self.tz_strat)
result = self.draw_naive_datetime_and_combine(data, tz)
# TODO: with some probability, systematically search for one of
# - an imaginary time (if allowed),
# - a time within 24hrs of a leap second (if there any are within bounds),
# - other subtle, little-known, or nasty issues as described in
# https://github.com/HypothesisWorks/hypothesis/issues/69
# If we happened to end up with a disallowed imaginary time, reject it.
if (not self.allow_imaginary) and datetime_does_not_exist(result):
data.mark_invalid("nonexistent datetime")
return result
def draw_naive_datetime_and_combine(self, data, tz):
result = draw_capped_multipart(data, self.min_value, self.max_value)
try:
return replace_tzinfo(dt.datetime(**result), timezone=tz)
except (ValueError, OverflowError):
data.mark_invalid(
f"Failed to draw a datetime between {self.min_value!r} and "
f"{self.max_value!r} with timezone from {self.tz_strat!r}."
)
@defines_strategy(force_reusable_values=True)
def datetimes(
min_value: dt.datetime = dt.datetime.min,
max_value: dt.datetime = dt.datetime.max,
*,
timezones: SearchStrategy[Optional[dt.tzinfo]] = none(),
allow_imaginary: bool = True,
) -> SearchStrategy[dt.datetime]:
"""datetimes(min_value=datetime.datetime.min, max_value=datetime.datetime.max, *, timezones=none(), allow_imaginary=True)
A strategy for generating datetimes, which may be timezone-aware.
This strategy works by drawing a naive datetime between ``min_value``
and ``max_value``, which must both be naive (have no timezone).
``timezones`` must be a strategy that generates either ``None``, for naive
datetimes, or :class:`~python:datetime.tzinfo` objects for 'aware' datetimes.
You can construct your own, though we recommend using one of these built-in
strategies:
* with Python 3.9 or newer or :pypi:`backports.zoneinfo`:
:func:`hypothesis.strategies.timezones`;
* with :pypi:`dateutil <python-dateutil>`:
:func:`hypothesis.extra.dateutil.timezones`; or
* with :pypi:`pytz`: :func:`hypothesis.extra.pytz.timezones`.
You may pass ``allow_imaginary=False`` to filter out "imaginary" datetimes
which did not (or will not) occur due to daylight savings, leap seconds,
timezone and calendar adjustments, etc. Imaginary datetimes are allowed
by default, because malformed timestamps are a common source of bugs.
Examples from this strategy shrink towards midnight on January 1st 2000,
local time.
"""
# Why must bounds be naive? In principle, we could also write a strategy
# that took aware bounds, but the API and validation is much harder.
# If you want to generate datetimes between two particular moments in
# time I suggest (a) just filtering out-of-bounds values; (b) if bounds
# are very close, draw a value and subtract its UTC offset, handling
# overflows and nonexistent times; or (c) do something customised to
# handle datetimes in e.g. a four-microsecond span which is not
# representable in UTC. Handling (d), all of the above, leads to a much
# more complex API for all users and a useful feature for very few.
check_type(bool, allow_imaginary, "allow_imaginary")
check_type(dt.datetime, min_value, "min_value")
check_type(dt.datetime, max_value, "max_value")
if min_value.tzinfo is not None:
raise InvalidArgument(f"{min_value=} must not have tzinfo")
if max_value.tzinfo is not None:
raise InvalidArgument(f"{max_value=} must not have tzinfo")
check_valid_interval(min_value, max_value, "min_value", "max_value")
if not isinstance(timezones, SearchStrategy):
raise InvalidArgument(
f"{timezones=} must be a SearchStrategy that can "
"provide tzinfo for datetimes (either None or dt.tzinfo objects)"
)
return DatetimeStrategy(min_value, max_value, timezones, allow_imaginary)
class TimeStrategy(SearchStrategy):
def __init__(self, min_value, max_value, timezones_strat):
self.min_value = min_value
self.max_value = max_value
self.tz_strat = timezones_strat
def do_draw(self, data):
result = draw_capped_multipart(data, self.min_value, self.max_value, TIMENAMES)
tz = data.draw(self.tz_strat)
return dt.time(**result, tzinfo=tz)
@defines_strategy(force_reusable_values=True)
def times(
min_value: dt.time = dt.time.min,
max_value: dt.time = dt.time.max,
*,
timezones: SearchStrategy[Optional[dt.tzinfo]] = none(),
) -> SearchStrategy[dt.time]:
"""times(min_value=datetime.time.min, max_value=datetime.time.max, *, timezones=none())
A strategy for times between ``min_value`` and ``max_value``.
The ``timezones`` argument is handled as for :py:func:`datetimes`.
Examples from this strategy shrink towards midnight, with the timezone
component shrinking as for the strategy that provided it.
"""
check_type(dt.time, min_value, "min_value")
check_type(dt.time, max_value, "max_value")
if min_value.tzinfo is not None:
raise InvalidArgument(f"{min_value=} must not have tzinfo")
if max_value.tzinfo is not None:
raise InvalidArgument(f"{max_value=} must not have tzinfo")
check_valid_interval(min_value, max_value, "min_value", "max_value")
return TimeStrategy(min_value, max_value, timezones)
class DateStrategy(SearchStrategy):
def __init__(self, min_value, max_value):
assert isinstance(min_value, dt.date)
assert isinstance(max_value, dt.date)
assert min_value < max_value
self.min_value = min_value
self.max_value = max_value
def do_draw(self, data):
return dt.date(
**draw_capped_multipart(data, self.min_value, self.max_value, DATENAMES)
)
@defines_strategy(force_reusable_values=True)
def dates(
min_value: dt.date = dt.date.min, max_value: dt.date = dt.date.max
) -> SearchStrategy[dt.date]:
"""dates(min_value=datetime.date.min, max_value=datetime.date.max)
A strategy for dates between ``min_value`` and ``max_value``.
Examples from this strategy shrink towards January 1st 2000.
"""
check_type(dt.date, min_value, "min_value")
check_type(dt.date, max_value, "max_value")
check_valid_interval(min_value, max_value, "min_value", "max_value")
if min_value == max_value:
return just(min_value)
return DateStrategy(min_value, max_value)
class TimedeltaStrategy(SearchStrategy):
def __init__(self, min_value, max_value):
assert isinstance(min_value, dt.timedelta)
assert isinstance(max_value, dt.timedelta)
assert min_value < max_value
self.min_value = min_value
self.max_value = max_value
def do_draw(self, data):
result = {}
low_bound = True
high_bound = True
for name in ("days", "seconds", "microseconds"):
low = getattr(self.min_value if low_bound else dt.timedelta.min, name)
high = getattr(self.max_value if high_bound else dt.timedelta.max, name)
val = data.draw_integer(low, high)
result[name] = val
low_bound = low_bound and val == low
high_bound = high_bound and val == high
return dt.timedelta(**result)
@defines_strategy(force_reusable_values=True)
def timedeltas(
min_value: dt.timedelta = dt.timedelta.min,
max_value: dt.timedelta = dt.timedelta.max,
) -> SearchStrategy[dt.timedelta]:
"""timedeltas(min_value=datetime.timedelta.min, max_value=datetime.timedelta.max)
A strategy for timedeltas between ``min_value`` and ``max_value``.
Examples from this strategy shrink towards zero.
"""
check_type(dt.timedelta, min_value, "min_value")
check_type(dt.timedelta, max_value, "max_value")
check_valid_interval(min_value, max_value, "min_value", "max_value")
if min_value == max_value:
return just(min_value)
return TimedeltaStrategy(min_value=min_value, max_value=max_value)
@lru_cache(maxsize=None)
def _valid_key_cacheable(tzpath, key):
assert isinstance(tzpath, tuple) # zoneinfo changed, better update this function!
for root in tzpath:
if Path(root).joinpath(key).exists(): # pragma: no branch
# No branch because most systems only have one TZPATH component.
return True
else: # pragma: no cover
# This branch is only taken for names which are known to zoneinfo
# but not present on the filesystem, i.e. on Windows with tzdata,
# and so is never executed by our coverage tests.
*package_loc, resource_name = key.split("/")
package = "tzdata.zoneinfo." + ".".join(package_loc)
try:
try:
traversable = resources.files(package) / resource_name
return traversable.exists()
except (AttributeError, ValueError):
# .files() was added in Python 3.9
return resources.is_resource(package, resource_name)
except ModuleNotFoundError:
return False
@defines_strategy(force_reusable_values=True)
def timezone_keys(
*,
# allow_alias: bool = True,
# allow_deprecated: bool = True,
allow_prefix: bool = True,
) -> SearchStrategy[str]:
"""A strategy for :wikipedia:`IANA timezone names <List_of_tz_database_time_zones>`.
As well as timezone names like ``"UTC"``, ``"Australia/Sydney"``, or
``"America/New_York"``, this strategy can generate:
- Aliases such as ``"Antarctica/McMurdo"``, which links to ``"Pacific/Auckland"``.
- Deprecated names such as ``"Antarctica/South_Pole"``, which *also* links to
``"Pacific/Auckland"``. Note that most but
not all deprecated timezone names are also aliases.
- Timezone names with the ``"posix/"`` or ``"right/"`` prefixes, unless
``allow_prefix=False``.
These strings are provided separately from Tzinfo objects - such as ZoneInfo
instances from the timezones() strategy - to facilitate testing of timezone
logic without needing workarounds to access non-canonical names.
.. note::
The :mod:`python:zoneinfo` module is new in Python 3.9, so you will need
to install the :pypi:`backports.zoneinfo` module on earlier versions.
`On Windows, you will also need to install the tzdata package
<https://docs.python.org/3/library/zoneinfo.html#data-sources>`__.
``pip install hypothesis[zoneinfo]`` will install these conditional
dependencies if and only if they are needed.
On Windows, you may need to access IANA timezone data via the :pypi:`tzdata`
package. For non-IANA timezones, such as Windows-native names or GNU TZ
strings, we recommend using :func:`~hypothesis.strategies.sampled_from` with
the :pypi:`dateutil <python-dateutil>` package, e.g.
:meth:`dateutil:dateutil.tz.tzwin.list`.
"""
# check_type(bool, allow_alias, "allow_alias")
# check_type(bool, allow_deprecated, "allow_deprecated")
check_type(bool, allow_prefix, "allow_prefix")
if zoneinfo is None: # pragma: no cover
raise ModuleNotFoundError(
"The zoneinfo module is required, but could not be imported. "
"Run `pip install hypothesis[zoneinfo]` and try again."
)
available_timezones = ("UTC", *sorted(zoneinfo.available_timezones()))
# TODO: filter out alias and deprecated names if disallowed
# When prefixes are allowed, we first choose a key and then flatmap to get our
# choice with one of the available prefixes. That in turn means that we need
# some logic to determine which prefixes are available for a given key:
def valid_key(key):
return key == "UTC" or _valid_key_cacheable(zoneinfo.TZPATH, key)
# TODO: work out how to place a higher priority on "weird" timezones
# For details see https://github.com/HypothesisWorks/hypothesis/issues/2414
strategy = sampled_from([key for key in available_timezones if valid_key(key)])
if not allow_prefix:
return strategy
def sample_with_prefixes(zone):
keys_with_prefixes = (zone, f"posix/{zone}", f"right/{zone}")
return sampled_from([key for key in keys_with_prefixes if valid_key(key)])
return strategy.flatmap(sample_with_prefixes)
@defines_strategy(force_reusable_values=True)
def timezones(*, no_cache: bool = False) -> SearchStrategy["zoneinfo.ZoneInfo"]:
"""A strategy for :class:`python:zoneinfo.ZoneInfo` objects.
If ``no_cache=True``, the generated instances are constructed using
:meth:`ZoneInfo.no_cache <python:zoneinfo.ZoneInfo.no_cache>` instead
of the usual constructor. This may change the semantics of your datetimes
in surprising ways, so only use it if you know that you need to!
.. note::
The :mod:`python:zoneinfo` module is new in Python 3.9, so you will need
to install the :pypi:`backports.zoneinfo` module on earlier versions.
`On Windows, you will also need to install the tzdata package
<https://docs.python.org/3/library/zoneinfo.html#data-sources>`__.
``pip install hypothesis[zoneinfo]`` will install these conditional
dependencies if and only if they are needed.
"""
check_type(bool, no_cache, "no_cache")
if zoneinfo is None: # pragma: no cover
raise ModuleNotFoundError(
"The zoneinfo module is required, but could not be imported. "
"Run `pip install hypothesis[zoneinfo]` and try again."
)
return timezone_keys().map(
zoneinfo.ZoneInfo.no_cache if no_cache else zoneinfo.ZoneInfo
)

View File

@@ -0,0 +1,84 @@
# 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 inspect
from hypothesis.errors import InvalidArgument
from hypothesis.internal.reflection import get_pretty_function_description
from hypothesis.strategies._internal.strategies import SearchStrategy, check_strategy
class DeferredStrategy(SearchStrategy):
"""A strategy which may be used before it is fully defined."""
def __init__(self, definition):
super().__init__()
self.__wrapped_strategy = None
self.__in_repr = False
self.__definition = definition
@property
def wrapped_strategy(self):
if self.__wrapped_strategy is None:
if not inspect.isfunction(self.__definition):
raise InvalidArgument(
f"Expected definition to be a function but got {self.__definition!r} "
f"of type {type(self.__definition).__name__} instead."
)
result = self.__definition()
if result is self:
raise InvalidArgument("Cannot define a deferred strategy to be itself")
check_strategy(result, "definition()")
self.__wrapped_strategy = result
self.__definition = None
return self.__wrapped_strategy
@property
def branches(self):
return self.wrapped_strategy.branches
@property
def supports_find(self):
return self.wrapped_strategy.supports_find
def calc_label(self):
"""Deferred strategies don't have a calculated label, because we would
end up having to calculate the fixed point of some hash function in
order to calculate it when they recursively refer to themself!
The label for the wrapped strategy will still appear because it
will be passed to draw.
"""
# This is actually the same as the parent class implementation, but we
# include it explicitly here in order to document that this is a
# deliberate decision.
return self.class_label
def calc_is_empty(self, recur):
return recur(self.wrapped_strategy)
def calc_has_reusable_values(self, recur):
return recur(self.wrapped_strategy)
def __repr__(self):
if self.__wrapped_strategy is not None:
if self.__in_repr:
return f"(deferred@{id(self)!r})"
try:
self.__in_repr = True
return repr(self.__wrapped_strategy)
finally:
self.__in_repr = False
else:
description = get_pretty_function_description(self.__definition)
return f"deferred({description})"
def do_draw(self, data):
return data.draw(self.wrapped_strategy)

View File

@@ -0,0 +1,104 @@
# 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.internal.conjecture import utils as cu
from hypothesis.strategies._internal.strategies import SearchStrategy
FEATURE_LABEL = cu.calc_label_from_name("feature flag")
class FeatureFlags:
"""Object that can be used to control a number of feature flags for a
given test run.
This enables an approach to data generation called swarm testing (
see Groce, Alex, et al. "Swarm testing." Proceedings of the 2012
International Symposium on Software Testing and Analysis. ACM, 2012), in
which generation is biased by selectively turning some features off for
each test case generated. When there are many interacting features this can
find bugs that a pure generation strategy would otherwise have missed.
FeatureFlags are designed to "shrink open", so that during shrinking they
become less restrictive. This allows us to potentially shrink to smaller
test cases that were forbidden during the generation phase because they
required disabled features.
"""
def __init__(self, data=None, enabled=(), disabled=()):
self.__data = data
self.__is_disabled = {}
for f in enabled:
self.__is_disabled[f] = False
for f in disabled:
self.__is_disabled[f] = True
# In the original swarm testing paper they turn features on or off
# uniformly at random. Instead we decide the probability with which to
# enable features up front. This can allow for scenarios where all or
# no features are enabled, which are vanishingly unlikely in the
# original model.
#
# We implement this as a single 8-bit integer and enable features which
# score >= that value. In particular when self.__baseline is 0, all
# features will be enabled. This is so that we shrink in the direction
# of more features being enabled.
if self.__data is not None:
self.__p_disabled = data.draw_integer(0, 255) / 255.0
else:
# If data is None we're in example mode so all that matters is the
# enabled/disabled lists above. We set this up so that everything
# else is enabled by default.
self.__p_disabled = 0.0
def is_enabled(self, name):
"""Tests whether the feature named ``name`` should be enabled on this
test run."""
if self.__data is None or self.__data.frozen:
# Feature set objects might hang around after data generation has
# finished. If this happens then we just report all new features as
# enabled, because that's our shrinking direction and they have no
# impact on data generation if they weren't used while it was
# running.
return not self.__is_disabled.get(name, False)
data = self.__data
data.start_example(label=FEATURE_LABEL)
# If we've already decided on this feature then we don't actually
# need to draw anything, but we do write the same decision to the
# input stream. This allows us to lazily decide whether a feature
# is enabled, because it means that if we happen to delete the part
# of the test case where we originally decided, the next point at
# which we make this decision just makes the decision it previously
# made.
is_disabled = self.__data.draw_boolean(
self.__p_disabled, forced=self.__is_disabled.get(name)
)
self.__is_disabled[name] = is_disabled
data.stop_example()
return not is_disabled
def __repr__(self):
enabled = []
disabled = []
for name, is_disabled in self.__is_disabled.items():
if is_disabled:
disabled.append(name)
else:
enabled.append(name)
return f"FeatureFlags({enabled=}, {disabled=})"
class FeatureStrategy(SearchStrategy):
def do_draw(self, data):
return FeatureFlags(data)

View File

@@ -0,0 +1,40 @@
# 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.internal.reflection import get_pretty_function_description
from hypothesis.strategies._internal.strategies import SearchStrategy, check_strategy
class FlatMapStrategy(SearchStrategy):
def __init__(self, strategy, expand):
super().__init__()
self.flatmapped_strategy = strategy
self.expand = expand
def calc_is_empty(self, recur):
return recur(self.flatmapped_strategy)
def __repr__(self):
if not hasattr(self, "_cached_repr"):
self._cached_repr = f"{self.flatmapped_strategy!r}.flatmap({get_pretty_function_description(self.expand)})"
return self._cached_repr
def do_draw(self, data):
source = data.draw(self.flatmapped_strategy)
expanded_source = self.expand(source)
check_strategy(expanded_source)
return data.draw(expanded_source)
@property
def branches(self):
return [
FlatMapStrategy(strategy=strategy, expand=self.expand)
for strategy in self.flatmapped_strategy.branches
]

View File

@@ -0,0 +1,62 @@
# 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 weakref import WeakKeyDictionary
from hypothesis.control import note
from hypothesis.errors import InvalidState
from hypothesis.internal.reflection import (
convert_positional_arguments,
nicerepr,
proxies,
repr_call,
)
from hypothesis.strategies._internal.strategies import SearchStrategy
class FunctionStrategy(SearchStrategy):
supports_find = False
def __init__(self, like, returns, pure):
super().__init__()
self.like = like
self.returns = returns
self.pure = pure
# Using wekrefs-to-generated-functions means that the cache can be
# garbage-collected at the end of each example, reducing memory use.
self._cache = WeakKeyDictionary()
def calc_is_empty(self, recur):
return recur(self.returns)
def do_draw(self, data):
@proxies(self.like)
def inner(*args, **kwargs):
if data.frozen:
raise InvalidState(
f"This generated {nicerepr(self.like)} function can only "
"be called within the scope of the @given that created it."
)
if self.pure:
args, kwargs = convert_positional_arguments(self.like, args, kwargs)
key = (args, frozenset(kwargs.items()))
cache = self._cache.setdefault(inner, {})
if key not in cache:
cache[key] = data.draw(self.returns)
rep = repr_call(self.like, args, kwargs, reorder=False)
note(f"Called function: {rep} -> {cache[key]!r}")
return cache[key]
else:
val = data.draw(self.returns)
rep = repr_call(self.like, args, kwargs, reorder=False)
note(f"Called function: {rep} -> {val!r}")
return val
return inner

View File

@@ -0,0 +1,118 @@
# 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 ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network
from typing import Literal, Optional, Union
from hypothesis.errors import InvalidArgument
from hypothesis.internal.validation import check_type
from hypothesis.strategies._internal.core import binary, sampled_from
from hypothesis.strategies._internal.numbers import integers
from hypothesis.strategies._internal.strategies import SearchStrategy
from hypothesis.strategies._internal.utils import defines_strategy
# See https://www.iana.org/assignments/iana-ipv4-special-registry/
SPECIAL_IPv4_RANGES = (
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.0.0/29",
"192.0.0.8/32",
"192.0.0.9/32",
"192.0.0.10/32",
"192.0.0.170/32",
"192.0.0.171/32",
"192.0.2.0/24",
"192.31.196.0/24",
"192.52.193.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"192.175.48.0/24",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"240.0.0.0/4",
"255.255.255.255/32",
)
# and https://www.iana.org/assignments/iana-ipv6-special-registry/
SPECIAL_IPv6_RANGES = (
"::1/128",
"::/128",
"::ffff:0:0/96",
"64:ff9b::/96",
"64:ff9b:1::/48",
"100::/64",
"2001::/23",
"2001::/32",
"2001:1::1/128",
"2001:1::2/128",
"2001:2::/48",
"2001:3::/32",
"2001:4:112::/48",
"2001:10::/28",
"2001:20::/28",
"2001:db8::/32",
"2002::/16",
"2620:4f:8000::/48",
"fc00::/7",
"fe80::/10",
)
@defines_strategy(force_reusable_values=True)
def ip_addresses(
*,
v: Optional[Literal[4, 6]] = None,
network: Optional[Union[str, IPv4Network, IPv6Network]] = None,
) -> SearchStrategy[Union[IPv4Address, IPv6Address]]:
r"""Generate IP addresses - ``v=4`` for :class:`~python:ipaddress.IPv4Address`\ es,
``v=6`` for :class:`~python:ipaddress.IPv6Address`\ es, or leave unspecified
to allow both versions.
``network`` may be an :class:`~python:ipaddress.IPv4Network` or
:class:`~python:ipaddress.IPv6Network`, or a string representing a network such as
``"127.0.0.0/24"`` or ``"2001:db8::/32"``. As well as generating addresses within
a particular routable network, this can be used to generate addresses from a
reserved range listed in the
`IANA <https://www.iana.org/assignments/iana-ipv4-special-registry/>`__
`registries <https://www.iana.org/assignments/iana-ipv6-special-registry/>`__.
If you pass both ``v`` and ``network``, they must be for the same version.
"""
if v is not None:
check_type(int, v, "v")
if v not in (4, 6):
raise InvalidArgument(f"{v=}, but only v=4 or v=6 are valid")
if network is None:
# We use the reserved-address registries to boost the chance
# of generating one of the various special types of address.
four = binary(min_size=4, max_size=4).map(IPv4Address) | sampled_from(
SPECIAL_IPv4_RANGES
).flatmap(lambda network: ip_addresses(network=network))
six = binary(min_size=16, max_size=16).map(IPv6Address) | sampled_from(
SPECIAL_IPv6_RANGES
).flatmap(lambda network: ip_addresses(network=network))
if v == 4:
return four
if v == 6:
return six
return four | six
if isinstance(network, str):
network = ip_network(network)
check_type((IPv4Network, IPv6Network), network, "network")
assert isinstance(network, (IPv4Network, IPv6Network)) # for Mypy
if v not in (None, network.version):
raise InvalidArgument(f"{v=} is incompatible with {network=}")
addr_type = IPv4Address if network.version == 4 else IPv6Address
return integers(int(network[0]), int(network[-1])).map(addr_type) # type: ignore

View File

@@ -0,0 +1,164 @@
# 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 inspect import signature
from typing import MutableMapping
from weakref import WeakKeyDictionary
from hypothesis.internal.reflection import (
convert_keyword_arguments,
convert_positional_arguments,
get_pretty_function_description,
repr_call,
)
from hypothesis.strategies._internal.strategies import SearchStrategy
unwrap_cache: MutableMapping[SearchStrategy, SearchStrategy] = WeakKeyDictionary()
unwrap_depth = 0
def unwrap_strategies(s):
global unwrap_depth
if not isinstance(s, SearchStrategy):
return s
try:
return unwrap_cache[s]
except KeyError:
pass
unwrap_cache[s] = s
try:
unwrap_depth += 1
try:
result = unwrap_strategies(s.wrapped_strategy)
unwrap_cache[s] = result
try:
assert result.force_has_reusable_values == s.force_has_reusable_values
except AttributeError:
pass
try:
result.force_has_reusable_values = s.force_has_reusable_values
except AttributeError:
pass
return result
except AttributeError:
return s
finally:
unwrap_depth -= 1
if unwrap_depth <= 0:
unwrap_cache.clear()
assert unwrap_depth >= 0
def _repr_filter(condition):
return f".filter({get_pretty_function_description(condition)})"
class LazyStrategy(SearchStrategy):
"""A strategy which is defined purely by conversion to and from another
strategy.
Its parameter and distribution come from that other strategy.
"""
def __init__(self, function, args, kwargs, filters=(), *, force_repr=None):
super().__init__()
self.__wrapped_strategy = None
self.__representation = force_repr
self.function = function
self.__args = args
self.__kwargs = kwargs
self.__filters = filters
@property
def supports_find(self):
return self.wrapped_strategy.supports_find
def calc_is_empty(self, recur):
return recur(self.wrapped_strategy)
def calc_has_reusable_values(self, recur):
return recur(self.wrapped_strategy)
def calc_is_cacheable(self, recur):
for source in (self.__args, self.__kwargs.values()):
for v in source:
if isinstance(v, SearchStrategy) and not v.is_cacheable:
return False
return True
@property
def wrapped_strategy(self):
if self.__wrapped_strategy is None:
unwrapped_args = tuple(unwrap_strategies(s) for s in self.__args)
unwrapped_kwargs = {
k: unwrap_strategies(v) for k, v in self.__kwargs.items()
}
base = self.function(*self.__args, **self.__kwargs)
if unwrapped_args == self.__args and unwrapped_kwargs == self.__kwargs:
self.__wrapped_strategy = base
else:
self.__wrapped_strategy = self.function(
*unwrapped_args, **unwrapped_kwargs
)
for f in self.__filters:
self.__wrapped_strategy = self.__wrapped_strategy.filter(f)
return self.__wrapped_strategy
def filter(self, condition):
try:
repr_ = f"{self!r}{_repr_filter(condition)}"
except Exception:
repr_ = None
return LazyStrategy(
self.function,
self.__args,
self.__kwargs,
(*self.__filters, condition),
force_repr=repr_,
)
def do_validate(self):
w = self.wrapped_strategy
assert isinstance(w, SearchStrategy), f"{self!r} returned non-strategy {w!r}"
w.validate()
def __repr__(self):
if self.__representation is None:
sig = signature(self.function)
pos = [p for p in sig.parameters.values() if "POSITIONAL" in p.kind.name]
if len(pos) > 1 or any(p.default is not sig.empty for p in pos):
_args, _kwargs = convert_positional_arguments(
self.function, self.__args, self.__kwargs
)
else:
_args, _kwargs = convert_keyword_arguments(
self.function, self.__args, self.__kwargs
)
kwargs_for_repr = {
k: v
for k, v in _kwargs.items()
if k not in sig.parameters or v is not sig.parameters[k].default
}
self.__representation = repr_call(
self.function, _args, kwargs_for_repr, reorder=False
) + "".join(map(_repr_filter, self.__filters))
return self.__representation
def do_draw(self, data):
return data.draw(self.wrapped_strategy)
@property
def label(self):
return self.wrapped_strategy.label

View File

@@ -0,0 +1,118 @@
# 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.internal.reflection import get_pretty_function_description
from hypothesis.strategies._internal.strategies import (
SampledFromStrategy,
SearchStrategy,
T,
is_simple_data,
)
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
class JustStrategy(SampledFromStrategy):
"""A strategy which always returns a single fixed value.
It's implemented as a length-one SampledFromStrategy so that all our
special-case logic for filtering and sets applies also to just(x).
The important difference from a SampledFromStrategy with only one
element to choose is that JustStrategy *never* touches the underlying
choice sequence, i.e. drawing neither reads from nor writes to `data`.
This is a reasonably important optimisation (or semantic distinction!)
for both JustStrategy and SampledFromStrategy.
"""
@property
def value(self):
return self.elements[0]
def __repr__(self):
suffix = "".join(
f".{name}({get_pretty_function_description(f)})"
for name, f in self._transformations
)
if self.value is None:
return "none()" + suffix
return f"just({get_pretty_function_description(self.value)}){suffix}"
def calc_is_cacheable(self, recur):
return is_simple_data(self.value)
def do_filtered_draw(self, data):
# The parent class's `do_draw` implementation delegates directly to
# `do_filtered_draw`, which we can greatly simplify in this case since
# we have exactly one value. (This also avoids drawing any data.)
return self._transform(self.value)
@defines_strategy(never_lazy=True)
def just(value: T) -> SearchStrategy[T]:
"""Return a strategy which only generates ``value``.
Note: ``value`` is not copied. Be wary of using mutable values.
If ``value`` is the result of a callable, you can use
:func:`builds(callable) <hypothesis.strategies.builds>` instead
of ``just(callable())`` to get a fresh value each time.
Examples from this strategy do not shrink (because there is only one).
"""
return JustStrategy([value])
@defines_strategy(force_reusable_values=True)
def none() -> SearchStrategy[None]:
"""Return a strategy which only generates None.
Examples from this strategy do not shrink (because there is only
one).
"""
return just(None)
class Nothing(SearchStrategy):
def calc_is_empty(self, recur):
return True
def do_draw(self, data):
# This method should never be called because draw() will mark the
# data as invalid immediately because is_empty is True.
raise NotImplementedError("This should never happen")
def calc_has_reusable_values(self, recur):
return True
def __repr__(self):
return "nothing()"
def map(self, f):
return self
def filter(self, f):
return self
def flatmap(self, f):
return self
NOTHING = Nothing()
@cacheable
@defines_strategy(never_lazy=True)
def nothing() -> SearchStrategy:
"""This strategy never successfully draws a value and will always reject on
an attempt to draw.
Examples from this strategy do not shrink (because there are none).
"""
return NOTHING

View File

@@ -0,0 +1,520 @@
# 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 math
from decimal import Decimal
from fractions import Fraction
from typing import Literal, Optional, Union
from hypothesis.control import reject
from hypothesis.errors import InvalidArgument
from hypothesis.internal.filtering import (
get_float_predicate_bounds,
get_integer_predicate_bounds,
)
from hypothesis.internal.floats import (
SMALLEST_SUBNORMAL,
float_of,
float_to_int,
int_to_float,
is_negative,
next_down,
next_down_normal,
next_up,
next_up_normal,
width_smallest_normals,
)
from hypothesis.internal.validation import (
check_type,
check_valid_bound,
check_valid_interval,
)
from hypothesis.strategies._internal.misc import nothing
from hypothesis.strategies._internal.strategies import (
SampledFromStrategy,
SearchStrategy,
)
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
# See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong!
Real = Union[int, float, Fraction, Decimal]
class IntegersStrategy(SearchStrategy):
def __init__(self, start, end):
assert isinstance(start, int) or start is None
assert isinstance(end, int) or end is None
assert start is None or end is None or start <= end
self.start = start
self.end = end
def __repr__(self):
if self.start is None and self.end is None:
return "integers()"
if self.end is None:
return f"integers(min_value={self.start})"
if self.start is None:
return f"integers(max_value={self.end})"
return f"integers({self.start}, {self.end})"
def do_draw(self, data):
# For bounded integers, make the bounds and near-bounds more likely.
forced = None
if (
self.end is not None
and self.start is not None
and self.end - self.start > 127
):
bits = data.draw_integer(0, 127)
forced = {
122: self.start,
123: self.start,
124: self.end,
125: self.end,
126: self.start + 1,
127: self.end - 1,
}.get(bits)
return data.draw_integer(
min_value=self.start, max_value=self.end, forced=forced
)
def filter(self, condition):
if condition is math.isfinite:
return self
if condition in [math.isinf, math.isnan]:
return nothing()
kwargs, pred = get_integer_predicate_bounds(condition)
start, end = self.start, self.end
if "min_value" in kwargs:
start = max(kwargs["min_value"], -math.inf if start is None else start)
if "max_value" in kwargs:
end = min(kwargs["max_value"], math.inf if end is None else end)
if start != self.start or end != self.end:
if start is not None and end is not None and start > end:
return nothing()
self = type(self)(start, end)
if pred is None:
return self
return super().filter(pred)
@cacheable
@defines_strategy(force_reusable_values=True)
def integers(
min_value: Optional[int] = None,
max_value: Optional[int] = None,
) -> SearchStrategy[int]:
"""Returns a strategy which generates integers.
If min_value is not None then all values will be >= min_value. If
max_value is not None then all values will be <= max_value
Examples from this strategy will shrink towards zero, and negative values
will also shrink towards positive (i.e. -n may be replaced by +n).
"""
check_valid_bound(min_value, "min_value")
check_valid_bound(max_value, "max_value")
check_valid_interval(min_value, max_value, "min_value", "max_value")
if min_value is not None:
if min_value != int(min_value):
raise InvalidArgument(
"min_value=%r of type %r cannot be exactly represented as an integer."
% (min_value, type(min_value))
)
min_value = int(min_value)
if max_value is not None:
if max_value != int(max_value):
raise InvalidArgument(
"max_value=%r of type %r cannot be exactly represented as an integer."
% (max_value, type(max_value))
)
max_value = int(max_value)
return IntegersStrategy(min_value, max_value)
class FloatStrategy(SearchStrategy):
"""A strategy for floating point numbers."""
def __init__(
self,
*,
min_value: float,
max_value: float,
allow_nan: bool,
# The smallest nonzero number we can represent is usually a subnormal, but may
# be the smallest normal if we're running in unsafe denormals-are-zero mode.
# While that's usually an explicit error, we do need to handle the case where
# the user passes allow_subnormal=False.
smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
):
super().__init__()
assert isinstance(allow_nan, bool)
assert smallest_nonzero_magnitude >= 0.0, "programmer error if this is negative"
if smallest_nonzero_magnitude == 0.0: # pragma: no cover
raise FloatingPointError(
"Got allow_subnormal=True, but we can't represent subnormal floats "
"right now, in violation of the IEEE-754 floating-point "
"specification. This is usually because something was compiled with "
"-ffast-math or a similar option, which sets global processor state. "
"See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
"writeup - and good luck!"
)
self.min_value = min_value
self.max_value = max_value
self.allow_nan = allow_nan
self.smallest_nonzero_magnitude = smallest_nonzero_magnitude
def __repr__(self):
return "{}(min_value={}, max_value={}, allow_nan={}, smallest_nonzero_magnitude={})".format(
self.__class__.__name__,
self.min_value,
self.max_value,
self.allow_nan,
self.smallest_nonzero_magnitude,
)
def do_draw(self, data):
return data.draw_float(
min_value=self.min_value,
max_value=self.max_value,
allow_nan=self.allow_nan,
smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
)
def filter(self, condition):
# Handle a few specific weird cases.
if condition is math.isfinite:
return FloatStrategy(
min_value=max(self.min_value, next_up(float("-inf"))),
max_value=min(self.max_value, next_down(float("inf"))),
allow_nan=False,
smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
)
if condition is math.isinf:
if permitted_infs := [
x
for x in (-math.inf, math.inf)
if self.min_value <= x <= self.max_value
]:
return SampledFromStrategy(permitted_infs)
return nothing()
if condition is math.isnan:
if not self.allow_nan:
return nothing()
return NanStrategy()
kwargs, pred = get_float_predicate_bounds(condition)
if not kwargs:
return super().filter(pred)
min_bound = max(kwargs.get("min_value", -math.inf), self.min_value)
max_bound = min(kwargs.get("max_value", math.inf), self.max_value)
# Adjustments for allow_subnormal=False, if any need to be made
if -self.smallest_nonzero_magnitude < min_bound < 0:
min_bound = -0.0
elif 0 < min_bound < self.smallest_nonzero_magnitude:
min_bound = self.smallest_nonzero_magnitude
if -self.smallest_nonzero_magnitude < max_bound < 0:
max_bound = -self.smallest_nonzero_magnitude
elif 0 < max_bound < self.smallest_nonzero_magnitude:
max_bound = 0.0
if min_bound > max_bound:
return nothing()
if (
min_bound > self.min_value
or self.max_value > max_bound
or (self.allow_nan and (-math.inf < min_bound or max_bound < math.inf))
):
self = type(self)(
min_value=min_bound,
max_value=max_bound,
allow_nan=False,
smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
)
if pred is None:
return self
return super().filter(pred)
@cacheable
@defines_strategy(force_reusable_values=True)
def floats(
min_value: Optional[Real] = None,
max_value: Optional[Real] = None,
*,
allow_nan: Optional[bool] = None,
allow_infinity: Optional[bool] = None,
allow_subnormal: Optional[bool] = None,
width: Literal[16, 32, 64] = 64,
exclude_min: bool = False,
exclude_max: bool = False,
) -> SearchStrategy[float]:
"""Returns a strategy which generates floats.
- If min_value is not None, all values will be ``>= min_value``
(or ``> min_value`` if ``exclude_min``).
- If max_value is not None, all values will be ``<= max_value``
(or ``< max_value`` if ``exclude_max``).
- If min_value or max_value is not None, it is an error to enable
allow_nan.
- If both min_value and max_value are not None, it is an error to enable
allow_infinity.
- If inferred values range does not include subnormal values, it is an error
to enable allow_subnormal.
Where not explicitly ruled out by the bounds,
:wikipedia:`subnormals <Subnormal_number>`, infinities, and NaNs are possible
values generated by this strategy.
The width argument specifies the maximum number of bits of precision
required to represent the generated float. Valid values are 16, 32, or 64.
Passing ``width=32`` will still use the builtin 64-bit :class:`~python:float` class,
but always for values which can be exactly represented as a 32-bit float.
The exclude_min and exclude_max argument can be used to generate numbers
from open or half-open intervals, by excluding the respective endpoints.
Excluding either signed zero will also exclude the other.
Attempting to exclude an endpoint which is None will raise an error;
use ``allow_infinity=False`` to generate finite floats. You can however
use e.g. ``min_value=-math.inf, exclude_min=True`` to exclude only
one infinite endpoint.
Examples from this strategy have a complicated and hard to explain
shrinking behaviour, but it tries to improve "human readability". Finite
numbers will be preferred to infinity and infinity will be preferred to
NaN.
"""
check_type(bool, exclude_min, "exclude_min")
check_type(bool, exclude_max, "exclude_max")
if allow_nan is None:
allow_nan = bool(min_value is None and max_value is None)
elif allow_nan and (min_value is not None or max_value is not None):
raise InvalidArgument(f"Cannot have {allow_nan=}, with min_value or max_value")
if width not in (16, 32, 64):
raise InvalidArgument(
f"Got {width=}, but the only valid values "
"are the integers 16, 32, and 64."
)
check_valid_bound(min_value, "min_value")
check_valid_bound(max_value, "max_value")
if math.copysign(1.0, -0.0) == 1.0: # pragma: no cover
raise FloatingPointError(
"Your Python install can't represent -0.0, which is required by the "
"IEEE-754 floating-point specification. This is probably because it was "
"compiled with an unsafe option like -ffast-math; for a more detailed "
"explanation see https://simonbyrne.github.io/notes/fastmath/"
)
if allow_subnormal and next_up(0.0, width=width) == 0: # pragma: no cover
# Not worth having separate CI envs and dependencies just to cover this branch;
# discussion in https://github.com/HypothesisWorks/hypothesis/issues/3092
#
# Erroring out here ensures that the database contents are interpreted
# consistently - which matters for such a foundational strategy, even if it's
# not always true for all user-composed strategies further up the stack.
from _hypothesis_ftz_detector import identify_ftz_culprits
try:
ftz_pkg = identify_ftz_culprits()
except Exception:
ftz_pkg = None
if ftz_pkg:
ftz_msg = (
f"This seems to be because the `{ftz_pkg}` package was compiled with "
f"-ffast-math or a similar option, which sets global processor state "
f"- see https://simonbyrne.github.io/notes/fastmath/ for details. "
f"If you don't know why {ftz_pkg} is installed, `pipdeptree -rp "
f"{ftz_pkg}` will show which packages depend on it."
)
else:
ftz_msg = (
"This is usually because something was compiled with -ffast-math "
"or a similar option, which sets global processor state. See "
"https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
"writeup - and good luck!"
)
raise FloatingPointError(
f"Got {allow_subnormal=}, but we can't represent "
f"subnormal floats right now, in violation of the IEEE-754 floating-point "
f"specification. {ftz_msg}"
)
min_arg, max_arg = min_value, max_value
if min_value is not None:
min_value = float_of(min_value, width)
assert isinstance(min_value, float)
if max_value is not None:
max_value = float_of(max_value, width)
assert isinstance(max_value, float)
if min_value != min_arg:
raise InvalidArgument(
f"min_value={min_arg!r} cannot be exactly represented as a float "
f"of width {width} - use {min_value=} instead."
)
if max_value != max_arg:
raise InvalidArgument(
f"max_value={max_arg!r} cannot be exactly represented as a float "
f"of width {width} - use {max_value=} instead."
)
if exclude_min and (min_value is None or min_value == math.inf):
raise InvalidArgument(f"Cannot exclude {min_value=}")
if exclude_max and (max_value is None or max_value == -math.inf):
raise InvalidArgument(f"Cannot exclude {max_value=}")
assumed_allow_subnormal = allow_subnormal is None or allow_subnormal
if min_value is not None and (
exclude_min or (min_arg is not None and min_value < min_arg)
):
min_value = next_up_normal(min_value, width, assumed_allow_subnormal)
if min_value == min_arg:
assert min_value == min_arg == 0
assert is_negative(min_arg)
assert not is_negative(min_value)
min_value = next_up_normal(min_value, width, assumed_allow_subnormal)
assert min_value > min_arg # type: ignore
if max_value is not None and (
exclude_max or (max_arg is not None and max_value > max_arg)
):
max_value = next_down_normal(max_value, width, assumed_allow_subnormal)
if max_value == max_arg:
assert max_value == max_arg == 0
assert is_negative(max_value)
assert not is_negative(max_arg)
max_value = next_down_normal(max_value, width, assumed_allow_subnormal)
assert max_value < max_arg # type: ignore
if min_value == -math.inf:
min_value = None
if max_value == math.inf:
max_value = None
bad_zero_bounds = (
min_value == max_value == 0
and is_negative(max_value)
and not is_negative(min_value)
)
if (
min_value is not None
and max_value is not None
and (min_value > max_value or bad_zero_bounds)
):
# This is a custom alternative to check_valid_interval, because we want
# to include the bit-width and exclusion information in the message.
msg = (
"There are no %s-bit floating-point values between min_value=%r "
"and max_value=%r" % (width, min_arg, max_arg)
)
if exclude_min or exclude_max:
msg += f", {exclude_min=} and {exclude_max=}"
raise InvalidArgument(msg)
if allow_infinity is None:
allow_infinity = bool(min_value is None or max_value is None)
elif allow_infinity:
if min_value is not None and max_value is not None:
raise InvalidArgument(
f"Cannot have {allow_infinity=}, with both min_value and max_value"
)
elif min_value == math.inf:
if min_arg == math.inf:
raise InvalidArgument("allow_infinity=False excludes min_value=inf")
raise InvalidArgument(
f"exclude_min=True turns min_value={min_arg!r} into inf, "
"but allow_infinity=False"
)
elif max_value == -math.inf:
if max_arg == -math.inf:
raise InvalidArgument("allow_infinity=False excludes max_value=-inf")
raise InvalidArgument(
f"exclude_max=True turns max_value={max_arg!r} into -inf, "
"but allow_infinity=False"
)
smallest_normal = width_smallest_normals[width]
if allow_subnormal is None:
if min_value is not None and max_value is not None:
if min_value == max_value:
allow_subnormal = -smallest_normal < min_value < smallest_normal
else:
allow_subnormal = (
min_value < smallest_normal and max_value > -smallest_normal
)
elif min_value is not None:
allow_subnormal = min_value < smallest_normal
elif max_value is not None:
allow_subnormal = max_value > -smallest_normal
else:
allow_subnormal = True
if allow_subnormal:
if min_value is not None and min_value >= smallest_normal:
raise InvalidArgument(
f"allow_subnormal=True, but minimum value {min_value} "
f"excludes values below float{width}'s "
f"smallest positive normal {smallest_normal}"
)
if max_value is not None and max_value <= -smallest_normal:
raise InvalidArgument(
f"allow_subnormal=True, but maximum value {max_value} "
f"excludes values above float{width}'s "
f"smallest negative normal {-smallest_normal}"
)
if min_value is None:
min_value = float("-inf")
if max_value is None:
max_value = float("inf")
if not allow_infinity:
min_value = max(min_value, next_up(float("-inf")))
max_value = min(max_value, next_down(float("inf")))
assert isinstance(min_value, float)
assert isinstance(max_value, float)
smallest_nonzero_magnitude = (
SMALLEST_SUBNORMAL if allow_subnormal else smallest_normal
)
result: SearchStrategy = FloatStrategy(
min_value=min_value,
max_value=max_value,
allow_nan=allow_nan,
smallest_nonzero_magnitude=smallest_nonzero_magnitude,
)
if width < 64:
def downcast(x):
try:
return float_of(x, width)
except OverflowError: # pragma: no cover
reject()
result = result.map(downcast)
return result
class NanStrategy(SearchStrategy):
"""Strategy for sampling the space of nan float values."""
def do_draw(self, data):
# Nans must have all exponent bits and the first mantissa bit set, so
# we generate by taking 64 random bits and setting the required ones.
sign_bit = int(data.draw_boolean()) << 63
nan_bits = float_to_int(math.nan)
mantissa_bits = data.draw_integer(0, 2**52 - 1)
return int_to_float(sign_bit | nan_bits | mantissa_bits)

View File

@@ -0,0 +1,443 @@
# 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 inspect
import math
from random import Random
from typing import Any, Dict
import attr
from hypothesis.control import should_note
from hypothesis.internal.reflection import define_function_signature
from hypothesis.reporting import report
from hypothesis.strategies._internal.core import (
binary,
lists,
permutations,
sampled_from,
)
from hypothesis.strategies._internal.numbers import floats, integers
from hypothesis.strategies._internal.strategies import SearchStrategy
class HypothesisRandom(Random):
"""A subclass of Random designed to expose the seed it was initially
provided with."""
def __init__(self, note_method_calls):
self.__note_method_calls = note_method_calls
def __deepcopy__(self, table):
return self.__copy__()
def __repr__(self):
raise NotImplementedError
def seed(self, seed):
raise NotImplementedError
def getstate(self):
raise NotImplementedError
def setstate(self, state):
raise NotImplementedError
def _hypothesis_log_random(self, method, kwargs, result):
if not (self.__note_method_calls and should_note()):
return
args, kwargs = convert_kwargs(method, kwargs)
argstr = ", ".join(
list(map(repr, args)) + [f"{k}={v!r}" for k, v in kwargs.items()]
)
report(f"{self!r}.{method}({argstr}) -> {result!r}")
def _hypothesis_do_random(self, method, kwargs):
raise NotImplementedError
RANDOM_METHODS = [
name
for name in [
"_randbelow",
"betavariate",
"binomialvariate",
"choice",
"choices",
"expovariate",
"gammavariate",
"gauss",
"getrandbits",
"lognormvariate",
"normalvariate",
"paretovariate",
"randint",
"random",
"randrange",
"sample",
"shuffle",
"triangular",
"uniform",
"vonmisesvariate",
"weibullvariate",
"randbytes",
]
if hasattr(Random, name)
]
# Fake shims to get a good signature
def getrandbits(self, n: int) -> int: # type: ignore
raise NotImplementedError
def random(self) -> float: # type: ignore
raise NotImplementedError
def _randbelow(self, n: int) -> int: # type: ignore
raise NotImplementedError
STUBS = {f.__name__: f for f in [getrandbits, random, _randbelow]}
SIGNATURES: Dict[str, inspect.Signature] = {}
def sig_of(name):
try:
return SIGNATURES[name]
except KeyError:
pass
target = getattr(Random, name)
result = inspect.signature(STUBS.get(name, target))
SIGNATURES[name] = result
return result
def define_copy_method(name):
target = getattr(Random, name)
def implementation(self, **kwargs):
result = self._hypothesis_do_random(name, kwargs)
self._hypothesis_log_random(name, kwargs, result)
return result
sig = inspect.signature(STUBS.get(name, target))
result = define_function_signature(target.__name__, target.__doc__, sig)(
implementation
)
result.__module__ = __name__
result.__qualname__ = "HypothesisRandom." + result.__name__
setattr(HypothesisRandom, name, result)
for r in RANDOM_METHODS:
define_copy_method(r)
@attr.s(slots=True)
class RandomState:
next_states: dict = attr.ib(factory=dict)
state_id: Any = attr.ib(default=None)
def state_for_seed(data, seed):
try:
seeds_to_states = data.seeds_to_states
except AttributeError:
seeds_to_states = {}
data.seeds_to_states = seeds_to_states
try:
state = seeds_to_states[seed]
except KeyError:
state = RandomState()
seeds_to_states[seed] = state
return state
UNIFORM = floats(0, 1)
def normalize_zero(f: float) -> float:
if f == 0.0:
return 0.0
else:
return f
class ArtificialRandom(HypothesisRandom):
VERSION = 10**6
def __init__(self, note_method_calls, data):
super().__init__(note_method_calls=note_method_calls)
self.__data = data
self.__state = RandomState()
def __repr__(self):
return "HypothesisRandom(generated data)"
def __copy__(self):
result = ArtificialRandom(
note_method_calls=self._HypothesisRandom__note_method_calls,
data=self.__data,
)
result.setstate(self.getstate())
return result
def __convert_result(self, method, kwargs, result):
if method == "choice":
return kwargs.get("seq")[result]
if method in ("choices", "sample"):
seq = kwargs["population"]
return [seq[i] for i in result]
if method == "shuffle":
seq = kwargs["x"]
original = list(seq)
for i, i2 in enumerate(result):
seq[i] = original[i2]
return None
return result
def _hypothesis_do_random(self, method, kwargs):
if method == "choices":
key = (method, len(kwargs["population"]), kwargs.get("k"))
elif method == "choice":
key = (method, len(kwargs["seq"]))
elif method == "shuffle":
key = (method, len(kwargs["x"]))
else:
key = (method, *sorted(kwargs))
try:
result, self.__state = self.__state.next_states[key]
except KeyError:
pass
else:
return self.__convert_result(method, kwargs, result)
if method == "_randbelow":
result = self.__data.draw_integer(0, kwargs["n"] - 1)
elif method in ("betavariate", "random"):
result = self.__data.draw(UNIFORM)
elif method == "uniform":
a = normalize_zero(kwargs["a"])
b = normalize_zero(kwargs["b"])
result = self.__data.draw(floats(a, b))
elif method in ("weibullvariate", "gammavariate"):
result = self.__data.draw(floats(min_value=0.0, allow_infinity=False))
elif method in ("gauss", "normalvariate"):
mu = kwargs["mu"]
result = mu + self.__data.draw(
floats(allow_nan=False, allow_infinity=False)
)
elif method == "vonmisesvariate":
result = self.__data.draw(floats(0, 2 * math.pi))
elif method == "randrange":
if kwargs["stop"] is None:
stop = kwargs["start"]
start = 0
else:
start = kwargs["start"]
stop = kwargs["stop"]
step = kwargs["step"]
if start == stop:
raise ValueError(f"empty range for randrange({start}, {stop}, {step})")
if step != 1:
endpoint = (stop - start) // step
if (start - stop) % step == 0:
endpoint -= 1
i = self.__data.draw_integer(0, endpoint)
result = start + i * step
else:
result = self.__data.draw_integer(start, stop - 1)
elif method == "randint":
result = self.__data.draw_integer(kwargs["a"], kwargs["b"])
# New in Python 3.12, so not taken by our coverage job
elif method == "binomialvariate": # pragma: no cover
result = self.__data.draw_integer(0, kwargs["n"])
elif method == "choice":
seq = kwargs["seq"]
result = self.__data.draw_integer(0, len(seq) - 1)
elif method == "choices":
k = kwargs["k"]
result = self.__data.draw(
lists(
integers(0, len(kwargs["population"]) - 1),
min_size=k,
max_size=k,
)
)
elif method == "sample":
k = kwargs["k"]
seq = kwargs["population"]
if k > len(seq) or k < 0:
raise ValueError(
f"Sample size {k} not in expected range 0 <= k <= {len(seq)}"
)
if k == 0:
result = []
else:
result = self.__data.draw(
lists(
sampled_from(range(len(seq))),
min_size=k,
max_size=k,
unique=True,
)
)
elif method == "getrandbits":
result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1)
elif method == "triangular":
low = normalize_zero(kwargs["low"])
high = normalize_zero(kwargs["high"])
mode = normalize_zero(kwargs["mode"])
if mode is None:
result = self.__data.draw(floats(low, high))
elif self.__data.draw_boolean(0.5):
result = self.__data.draw(floats(mode, high))
else:
result = self.__data.draw(floats(low, mode))
elif method in ("paretovariate", "expovariate", "lognormvariate"):
result = self.__data.draw(floats(min_value=0.0))
elif method == "shuffle":
result = self.__data.draw(permutations(range(len(kwargs["x"]))))
elif method == "randbytes":
n = kwargs["n"]
result = self.__data.draw(binary(min_size=n, max_size=n))
else:
raise NotImplementedError(method)
new_state = RandomState()
self.__state.next_states[key] = (result, new_state)
self.__state = new_state
return self.__convert_result(method, kwargs, result)
def seed(self, seed):
self.__state = state_for_seed(self.__data, seed)
def getstate(self):
if self.__state.state_id is not None:
return self.__state.state_id
try:
states_for_ids = self.__data.states_for_ids
except AttributeError:
states_for_ids = {}
self.__data.states_for_ids = states_for_ids
self.__state.state_id = len(states_for_ids)
states_for_ids[self.__state.state_id] = self.__state
return self.__state.state_id
def setstate(self, state):
self.__state = self.__data.states_for_ids[state]
DUMMY_RANDOM = Random(0)
def convert_kwargs(name, kwargs):
kwargs = dict(kwargs)
signature = sig_of(name)
bound = signature.bind(DUMMY_RANDOM, **kwargs)
bound.apply_defaults()
for k in list(kwargs):
if (
kwargs[k] is signature.parameters[k].default
or signature.parameters[k].kind != inspect.Parameter.KEYWORD_ONLY
):
kwargs.pop(k)
arg_names = list(signature.parameters)[1:]
args = []
for a in arg_names:
if signature.parameters[a].kind == inspect.Parameter.KEYWORD_ONLY:
break
args.append(bound.arguments[a])
kwargs.pop(a, None)
while args:
name = arg_names[len(args) - 1]
if args[-1] is signature.parameters[name].default:
args.pop()
else:
break
return (args, kwargs)
class TrueRandom(HypothesisRandom):
def __init__(self, seed, note_method_calls):
super().__init__(note_method_calls)
self.__seed = seed
self.__random = Random(seed)
def _hypothesis_do_random(self, method, kwargs):
args, kwargs = convert_kwargs(method, kwargs)
return getattr(self.__random, method)(*args, **kwargs)
def __copy__(self):
result = TrueRandom(
seed=self.__seed,
note_method_calls=self._HypothesisRandom__note_method_calls,
)
result.setstate(self.getstate())
return result
def __repr__(self):
return f"Random({self.__seed!r})"
def seed(self, seed):
self.__random.seed(seed)
self.__seed = seed
def getstate(self):
return self.__random.getstate()
def setstate(self, state):
self.__random.setstate(state)
class RandomStrategy(SearchStrategy):
def __init__(self, note_method_calls, use_true_random):
self.__note_method_calls = note_method_calls
self.__use_true_random = use_true_random
def do_draw(self, data):
if self.__use_true_random:
seed = data.draw_integer(0, 2**64 - 1)
return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls)
else:
return ArtificialRandom(
note_method_calls=self.__note_method_calls, data=data
)

View File

@@ -0,0 +1,117 @@
# 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 threading
from contextlib import contextmanager
from hypothesis.errors import InvalidArgument
from hypothesis.internal.reflection import get_pretty_function_description
from hypothesis.internal.validation import check_type
from hypothesis.strategies._internal.strategies import (
OneOfStrategy,
SearchStrategy,
check_strategy,
)
class LimitReached(BaseException):
pass
class LimitedStrategy(SearchStrategy):
def __init__(self, strategy):
super().__init__()
self.base_strategy = strategy
self._threadlocal = threading.local()
@property
def marker(self):
return getattr(self._threadlocal, "marker", 0)
@marker.setter
def marker(self, value):
self._threadlocal.marker = value
@property
def currently_capped(self):
return getattr(self._threadlocal, "currently_capped", False)
@currently_capped.setter
def currently_capped(self, value):
self._threadlocal.currently_capped = value
def __repr__(self):
return f"LimitedStrategy({self.base_strategy!r})"
def do_validate(self):
self.base_strategy.validate()
def do_draw(self, data):
assert self.currently_capped
if self.marker <= 0:
raise LimitReached
self.marker -= 1
return data.draw(self.base_strategy)
@contextmanager
def capped(self, max_templates):
try:
was_capped = self.currently_capped
self.currently_capped = True
self.marker = max_templates
yield
finally:
self.currently_capped = was_capped
class RecursiveStrategy(SearchStrategy):
def __init__(self, base, extend, max_leaves):
self.max_leaves = max_leaves
self.base = base
self.limited_base = LimitedStrategy(base)
self.extend = extend
strategies = [self.limited_base, self.extend(self.limited_base)]
while 2 ** (len(strategies) - 1) <= max_leaves:
strategies.append(extend(OneOfStrategy(tuple(strategies))))
self.strategy = OneOfStrategy(strategies)
def __repr__(self):
if not hasattr(self, "_cached_repr"):
self._cached_repr = "recursive(%r, %s, max_leaves=%d)" % (
self.base,
get_pretty_function_description(self.extend),
self.max_leaves,
)
return self._cached_repr
def do_validate(self):
check_strategy(self.base, "base")
extended = self.extend(self.limited_base)
check_strategy(extended, f"extend({self.limited_base!r})")
self.limited_base.validate()
extended.validate()
check_type(int, self.max_leaves, "max_leaves")
if self.max_leaves <= 0:
raise InvalidArgument(
f"max_leaves={self.max_leaves!r} must be greater than zero"
)
def do_draw(self, data):
count = 0
while True:
try:
with self.limited_base.capped(self.max_leaves):
return data.draw(self.strategy)
except LimitReached:
if count == 0:
msg = f"Draw for {self!r} exceeded max_leaves and had to be retried"
data.events[msg] = ""
count += 1

View File

@@ -0,0 +1,548 @@
# 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 operator
import re
from hypothesis.errors import InvalidArgument
from hypothesis.internal import charmap
from hypothesis.strategies._internal.lazy import unwrap_strategies
from hypothesis.strategies._internal.strings import OneCharStringStrategy
try: # pragma: no cover
import re._constants as sre
import re._parser as sre_parse
ATOMIC_GROUP = sre.ATOMIC_GROUP
POSSESSIVE_REPEAT = sre.POSSESSIVE_REPEAT
except ImportError: # Python < 3.11
import sre_constants as sre
import sre_parse
ATOMIC_GROUP = object()
POSSESSIVE_REPEAT = object()
from hypothesis import reject, strategies as st
from hypothesis.internal.charmap import as_general_categories, categories
from hypothesis.internal.compat import add_note, int_to_byte
UNICODE_CATEGORIES = set(categories())
SPACE_CHARS = set(" \t\n\r\f\v")
UNICODE_SPACE_CHARS = SPACE_CHARS | set("\x1c\x1d\x1e\x1f\x85")
UNICODE_DIGIT_CATEGORIES = {"Nd"}
UNICODE_SPACE_CATEGORIES = set(as_general_categories("Z"))
UNICODE_LETTER_CATEGORIES = set(as_general_categories("L"))
UNICODE_WORD_CATEGORIES = set(as_general_categories(["L", "N"]))
# This is verbose, but correct on all versions of Python
BYTES_ALL = {int_to_byte(i) for i in range(256)}
BYTES_DIGIT = {b for b in BYTES_ALL if re.match(b"\\d", b)}
BYTES_SPACE = {b for b in BYTES_ALL if re.match(b"\\s", b)}
BYTES_WORD = {b for b in BYTES_ALL if re.match(b"\\w", b)}
BYTES_LOOKUP = {
sre.CATEGORY_DIGIT: BYTES_DIGIT,
sre.CATEGORY_SPACE: BYTES_SPACE,
sre.CATEGORY_WORD: BYTES_WORD,
sre.CATEGORY_NOT_DIGIT: BYTES_ALL - BYTES_DIGIT,
sre.CATEGORY_NOT_SPACE: BYTES_ALL - BYTES_SPACE,
sre.CATEGORY_NOT_WORD: BYTES_ALL - BYTES_WORD,
}
GROUP_CACHE_STRATEGY: st.SearchStrategy[dict] = st.shared(
st.builds(dict), key="hypothesis.regex.group_cache"
)
@st.composite
def update_group(draw, group_name, strategy):
cache = draw(GROUP_CACHE_STRATEGY)
result = draw(strategy)
cache[group_name] = result
return result
@st.composite
def reuse_group(draw, group_name):
cache = draw(GROUP_CACHE_STRATEGY)
try:
return cache[group_name]
except KeyError:
reject()
@st.composite
def group_conditional(draw, group_name, if_yes, if_no):
cache = draw(GROUP_CACHE_STRATEGY)
if group_name in cache:
return draw(if_yes)
else:
return draw(if_no)
@st.composite
def clear_cache_after_draw(draw, base_strategy):
cache = draw(GROUP_CACHE_STRATEGY)
result = draw(base_strategy)
cache.clear()
return result
def chars_not_in_alphabet(alphabet, string):
# Given a string, return a tuple of the characters which are not in alphabet
if alphabet is None:
return ()
intset = unwrap_strategies(alphabet).intervals
return tuple(c for c in string if c not in intset)
class Context:
__slots__ = ["flags"]
def __init__(self, flags):
self.flags = flags
class CharactersBuilder:
"""Helper object that allows to configure `characters` strategy with
various unicode categories and characters. Also allows negation of
configured set.
:param negate: If True, configure :func:`hypothesis.strategies.characters`
to match anything other than configured character set
:param flags: Regex flags. They affect how and which characters are matched
"""
def __init__(self, *, negate=False, flags=0, alphabet):
self._categories = set()
self._whitelist_chars = set()
self._blacklist_chars = set()
self._negate = negate
self._ignorecase = flags & re.IGNORECASE
self.code_to_char = chr
self._alphabet = unwrap_strategies(alphabet)
if flags & re.ASCII:
self._alphabet = OneCharStringStrategy(
self._alphabet.intervals & charmap.query(max_codepoint=127)
)
@property
def strategy(self):
"""Returns resulting strategy that generates configured char set."""
# Start by getting the set of all characters allowed by the pattern
white_chars = self._whitelist_chars - self._blacklist_chars
multi_chars = {c for c in white_chars if len(c) > 1}
intervals = charmap.query(
categories=self._categories,
exclude_characters=self._blacklist_chars,
include_characters=white_chars - multi_chars,
)
# Then take the complement if this is from a negated character class
if self._negate:
intervals = charmap.query() - intervals
multi_chars.clear()
# and finally return the intersection with our alphabet
return OneCharStringStrategy(intervals & self._alphabet.intervals) | (
st.sampled_from(sorted(multi_chars)) if multi_chars else st.nothing()
)
def add_category(self, category):
"""Update unicode state to match sre_parse object ``category``."""
if category == sre.CATEGORY_DIGIT:
self._categories |= UNICODE_DIGIT_CATEGORIES
elif category == sre.CATEGORY_NOT_DIGIT:
self._categories |= UNICODE_CATEGORIES - UNICODE_DIGIT_CATEGORIES
elif category == sre.CATEGORY_SPACE:
self._categories |= UNICODE_SPACE_CATEGORIES
self._whitelist_chars |= UNICODE_SPACE_CHARS
elif category == sre.CATEGORY_NOT_SPACE:
self._categories |= UNICODE_CATEGORIES - UNICODE_SPACE_CATEGORIES
self._blacklist_chars |= UNICODE_SPACE_CHARS
elif category == sre.CATEGORY_WORD:
self._categories |= UNICODE_WORD_CATEGORIES
self._whitelist_chars.add("_")
elif category == sre.CATEGORY_NOT_WORD:
self._categories |= UNICODE_CATEGORIES - UNICODE_WORD_CATEGORIES
self._blacklist_chars.add("_")
else:
raise NotImplementedError(f"Unknown character category: {category}")
def add_char(self, char, *, check=True):
"""Add given char to the whitelist."""
c = self.code_to_char(char)
if check and chars_not_in_alphabet(self._alphabet, c):
raise InvalidArgument(f"Literal {c!r} is not in the specified alphabet")
self._whitelist_chars.add(c)
if (
self._ignorecase
and re.match(re.escape(c), c.swapcase(), flags=re.IGNORECASE) is not None
):
# Note that it is possible that `len(c.swapcase()) > 1`
self._whitelist_chars.add(c.swapcase())
class BytesBuilder(CharactersBuilder):
def __init__(self, *, negate=False, flags=0):
self._whitelist_chars = set()
self._blacklist_chars = set()
self._negate = negate
self._alphabet = None
self._ignorecase = flags & re.IGNORECASE
self.code_to_char = int_to_byte
@property
def strategy(self):
"""Returns resulting strategy that generates configured char set."""
allowed = self._whitelist_chars
if self._negate:
allowed = BYTES_ALL - allowed
return st.sampled_from(sorted(allowed))
def add_category(self, category):
"""Update characters state to match sre_parse object ``category``."""
self._whitelist_chars |= BYTES_LOOKUP[category]
@st.composite
def maybe_pad(draw, regex, strategy, left_pad_strategy, right_pad_strategy):
"""Attempt to insert padding around the result of a regex draw while
preserving the match."""
result = draw(strategy)
left_pad = draw(left_pad_strategy)
if left_pad and regex.search(left_pad + result):
result = left_pad + result
right_pad = draw(right_pad_strategy)
if right_pad and regex.search(result + right_pad):
result += right_pad
return result
def base_regex_strategy(regex, parsed=None, alphabet=None):
if parsed is None:
parsed = sre_parse.parse(regex.pattern, flags=regex.flags)
try:
s = _strategy(
parsed,
context=Context(flags=regex.flags),
is_unicode=isinstance(regex.pattern, str),
alphabet=alphabet,
)
except Exception as err:
add_note(err, f"{alphabet=} {regex=}")
raise
return clear_cache_after_draw(s)
def regex_strategy(
regex, fullmatch, *, alphabet, _temp_jsonschema_hack_no_end_newline=False
):
if not hasattr(regex, "pattern"):
regex = re.compile(regex)
is_unicode = isinstance(regex.pattern, str)
parsed = sre_parse.parse(regex.pattern, flags=regex.flags)
if fullmatch:
if not parsed:
return st.just("" if is_unicode else b"")
return base_regex_strategy(regex, parsed, alphabet).filter(regex.fullmatch)
if not parsed:
if is_unicode:
return st.text(alphabet=alphabet)
else:
return st.binary()
if is_unicode:
base_padding_strategy = st.text(alphabet=alphabet)
empty = st.just("")
newline = st.just("\n")
else:
base_padding_strategy = st.binary()
empty = st.just(b"")
newline = st.just(b"\n")
right_pad = base_padding_strategy
left_pad = base_padding_strategy
if parsed[-1][0] == sre.AT:
if parsed[-1][1] == sre.AT_END_STRING:
right_pad = empty
elif parsed[-1][1] == sre.AT_END:
if regex.flags & re.MULTILINE:
right_pad = st.one_of(
empty, st.builds(operator.add, newline, right_pad)
)
else:
right_pad = st.one_of(empty, newline)
# This will be removed when a regex-syntax-translation library exists.
# It's a pretty nasty hack, but means that we can match the semantics
# of JSONschema's compatible subset of ECMA regex, which is important
# for hypothesis-jsonschema and Schemathesis. See e.g.
# https://github.com/schemathesis/schemathesis/issues/1241
if _temp_jsonschema_hack_no_end_newline:
right_pad = empty
if parsed[0][0] == sre.AT:
if parsed[0][1] == sre.AT_BEGINNING_STRING:
left_pad = empty
elif parsed[0][1] == sre.AT_BEGINNING:
if regex.flags & re.MULTILINE:
left_pad = st.one_of(empty, st.builds(operator.add, left_pad, newline))
else:
left_pad = empty
base = base_regex_strategy(regex, parsed, alphabet).filter(regex.search)
return maybe_pad(regex, base, left_pad, right_pad)
def _strategy(codes, context, is_unicode, *, alphabet):
"""Convert SRE regex parse tree to strategy that generates strings matching
that regex represented by that parse tree.
`codes` is either a list of SRE regex elements representations or a
particular element representation. Each element is a tuple of element code
(as string) and parameters. E.g. regex 'ab[0-9]+' compiles to following
elements:
[
(LITERAL, 97),
(LITERAL, 98),
(MAX_REPEAT, (1, 4294967295, [
(IN, [
(RANGE, (48, 57))
])
]))
]
The function recursively traverses regex element tree and converts each
element to strategy that generates strings that match that element.
Context stores
1. List of groups (for backreferences)
2. Active regex flags (e.g. IGNORECASE, DOTALL, UNICODE, they affect
behavior of various inner strategies)
"""
def recurse(codes):
return _strategy(codes, context, is_unicode, alphabet=alphabet)
if is_unicode:
empty = ""
to_char = chr
else:
empty = b""
to_char = int_to_byte
binary_char = st.binary(min_size=1, max_size=1)
if not isinstance(codes, tuple):
# List of codes
strategies = []
i = 0
while i < len(codes):
if codes[i][0] == sre.LITERAL and not context.flags & re.IGNORECASE:
# Merge subsequent "literals" into one `just()` strategy
# that generates corresponding text if no IGNORECASE
j = i + 1
while j < len(codes) and codes[j][0] == sre.LITERAL:
j += 1
if i + 1 < j:
chars = empty.join(to_char(charcode) for _, charcode in codes[i:j])
if invalid := chars_not_in_alphabet(alphabet, chars):
raise InvalidArgument(
f"Literal {chars!r} contains characters {invalid!r} "
f"which are not in the specified alphabet"
)
strategies.append(st.just(chars))
i = j
continue
strategies.append(recurse(codes[i]))
i += 1
# We handle this separately at the top level, but some regex can
# contain empty lists internally, so we need to handle this here too.
if not strategies:
return st.just(empty)
if len(strategies) == 1:
return strategies[0]
return st.tuples(*strategies).map(empty.join)
else:
# Single code
code, value = codes
if code == sre.LITERAL:
# Regex 'a' (single char)
c = to_char(value)
if chars_not_in_alphabet(alphabet, c):
raise InvalidArgument(f"Literal {c!r} is not in the specified alphabet")
if (
context.flags & re.IGNORECASE
and c != c.swapcase()
and re.match(re.escape(c), c.swapcase(), re.IGNORECASE) is not None
and not chars_not_in_alphabet(alphabet, c.swapcase())
):
# We do the explicit check for swapped-case matching because
# eg 'ß'.upper() == 'SS' and ignorecase doesn't match it.
return st.sampled_from([c, c.swapcase()])
return st.just(c)
elif code == sre.NOT_LITERAL:
# Regex '[^a]' (negation of a single char)
c = to_char(value)
blacklist = {c}
if (
context.flags & re.IGNORECASE
and re.match(re.escape(c), c.swapcase(), re.IGNORECASE) is not None
):
# There are a few cases where .swapcase() returns two characters,
# but is still a case-insensitive match. In such cases we add *both*
# characters to our blacklist, to avoid doing the wrong thing for
# patterns such as r"[^\u0130]+" where "i\u0307" matches.
#
# (that's respectively 'Latin letter capital I with dot above' and
# 'latin latter i' + 'combining dot above'; see issue #2657)
#
# As a final additional wrinkle, "latin letter capital I" *also*
# case-insensitive-matches, with or without combining dot character.
# We therefore have to chain .swapcase() calls until a fixpoint.
stack = [c.swapcase()]
while stack:
for char in stack.pop():
blacklist.add(char)
stack.extend(set(char.swapcase()) - blacklist)
if is_unicode:
return OneCharStringStrategy(
unwrap_strategies(alphabet).intervals
& charmap.query(exclude_characters=blacklist)
)
else:
return binary_char.filter(lambda c: c not in blacklist)
elif code == sre.IN:
# Regex '[abc0-9]' (set of characters)
negate = value[0][0] == sre.NEGATE
if is_unicode:
builder = CharactersBuilder(
flags=context.flags, negate=negate, alphabet=alphabet
)
else:
builder = BytesBuilder(flags=context.flags, negate=negate)
for charset_code, charset_value in value:
if charset_code == sre.NEGATE:
# Regex '[^...]' (negation)
# handled by builder = CharactersBuilder(...) above
pass
elif charset_code == sre.LITERAL:
# Regex '[a]' (single char)
builder.add_char(charset_value)
elif charset_code == sre.RANGE:
# Regex '[a-z]' (char range)
low, high = charset_value
for char_code in range(low, high + 1):
builder.add_char(char_code, check=char_code in (low, high))
elif charset_code == sre.CATEGORY:
# Regex '[\w]' (char category)
builder.add_category(charset_value)
else:
# Currently there are no known code points other than
# handled here. This code is just future proofing
raise NotImplementedError(f"Unknown charset code: {charset_code}")
return builder.strategy
elif code == sre.ANY:
# Regex '.' (any char)
if is_unicode:
assert alphabet is not None
if context.flags & re.DOTALL:
return alphabet
return OneCharStringStrategy(
unwrap_strategies(alphabet).intervals
& charmap.query(exclude_characters="\n")
)
else:
if context.flags & re.DOTALL:
return binary_char
return binary_char.filter(lambda c: c != b"\n")
elif code == sre.AT:
# Regexes like '^...', '...$', '\bfoo', '\Bfoo'
# An empty string (or newline) will match the token itself, but
# we don't and can't check the position (eg '%' at the end)
return st.just(empty)
elif code == sre.SUBPATTERN:
# Various groups: '(...)', '(:...)' or '(?P<name>...)'
old_flags = context.flags
context.flags = (context.flags | value[1]) & ~value[2]
strat = _strategy(value[-1], context, is_unicode, alphabet=alphabet)
context.flags = old_flags
if value[0]:
strat = update_group(value[0], strat)
return strat
elif code == sre.GROUPREF:
# Regex '\\1' or '(?P=name)' (group reference)
return reuse_group(value)
elif code == sre.ASSERT:
# Regex '(?=...)' or '(?<=...)' (positive lookahead/lookbehind)
return recurse(value[1])
elif code == sre.ASSERT_NOT:
# Regex '(?!...)' or '(?<!...)' (negative lookahead/lookbehind)
return st.just(empty)
elif code == sre.BRANCH:
# Regex 'a|b|c' (branch)
return st.one_of([recurse(branch) for branch in value[1]])
elif code in [sre.MIN_REPEAT, sre.MAX_REPEAT, POSSESSIVE_REPEAT]:
# Regexes 'a?', 'a*', 'a+' and their non-greedy variants
# (repeaters)
at_least, at_most, subregex = value
if at_most == sre.MAXREPEAT:
at_most = None
if at_least == 0 and at_most == 1:
return st.just(empty) | recurse(subregex)
return st.lists(recurse(subregex), min_size=at_least, max_size=at_most).map(
empty.join
)
elif code == sre.GROUPREF_EXISTS:
# Regex '(?(id/name)yes-pattern|no-pattern)'
# (if group exists choice)
return group_conditional(
value[0],
recurse(value[1]),
recurse(value[2]) if value[2] else st.just(empty),
)
elif code == ATOMIC_GROUP: # pragma: no cover # new in Python 3.11
return _strategy(value, context, is_unicode, alphabet=alphabet)
else:
# Currently there are no known code points other than handled here.
# This code is just future proofing
raise NotImplementedError(
f"Unknown code point: {code!r}. Please open an issue."
)

View File

@@ -0,0 +1,38 @@
# 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.strategies._internal import SearchStrategy
SHARED_STRATEGY_ATTRIBUTE = "_hypothesis_shared_strategies"
class SharedStrategy(SearchStrategy):
def __init__(self, base, key=None):
self.key = key
self.base = base
@property
def supports_find(self):
return self.base.supports_find
def __repr__(self):
if self.key is not None:
return f"shared({self.base!r}, key={self.key!r})"
else:
return f"shared({self.base!r})"
def do_draw(self, data):
if not hasattr(data, SHARED_STRATEGY_ATTRIBUTE):
setattr(data, SHARED_STRATEGY_ATTRIBUTE, {})
sharing = getattr(data, SHARED_STRATEGY_ATTRIBUTE)
key = self.key or self
if key not in sharing:
sharing[key] = data.draw(self.base)
return sharing[key]

View File

@@ -0,0 +1,981 @@
# 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 warnings
from collections import abc, defaultdict
from random import shuffle
from typing import (
Any,
Callable,
ClassVar,
Dict,
Generic,
List,
Sequence,
TypeVar,
Union,
cast,
overload,
)
from hypothesis._settings import HealthCheck, Phase, Verbosity, settings
from hypothesis.control import _current_build_context, assume
from hypothesis.errors import (
HypothesisException,
HypothesisWarning,
InvalidArgument,
NonInteractiveExampleWarning,
UnsatisfiedAssumption,
)
from hypothesis.internal.conjecture import utils as cu
from hypothesis.internal.conjecture.data import ConjectureData
from hypothesis.internal.conjecture.utils import (
calc_label_from_cls,
calc_label_from_name,
combine_labels,
)
from hypothesis.internal.coverage import check_function
from hypothesis.internal.reflection import (
get_pretty_function_description,
is_identity_function,
)
from hypothesis.strategies._internal.utils import defines_strategy
from hypothesis.utils.conventions import UniqueIdentifier
Ex = TypeVar("Ex", covariant=True)
Ex_Inv = TypeVar("Ex_Inv")
T = TypeVar("T")
T3 = TypeVar("T3")
T4 = TypeVar("T4")
T5 = TypeVar("T5")
calculating = UniqueIdentifier("calculating")
MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name(
"another attempted draw in MappedSearchStrategy"
)
FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name(
"single loop iteration in FilteredStrategy"
)
def recursive_property(name, default):
"""Handle properties which may be mutually recursive among a set of
strategies.
These are essentially lazily cached properties, with the ability to set
an override: If the property has not been explicitly set, we calculate
it on first access and memoize the result for later.
The problem is that for properties that depend on each other, a naive
calculation strategy may hit infinite recursion. Consider for example
the property is_empty. A strategy defined as x = st.deferred(lambda: x)
is certainly empty (in order to draw a value from x we would have to
draw a value from x, for which we would have to draw a value from x,
...), but in order to calculate it the naive approach would end up
calling x.is_empty in order to calculate x.is_empty in order to etc.
The solution is one of fixed point calculation. We start with a default
value that is the value of the property in the absence of evidence to
the contrary, and then update the values of the property for all
dependent strategies until we reach a fixed point.
The approach taken roughly follows that in section 4.2 of Adams,
Michael D., Celeste Hollenbeck, and Matthew Might. "On the complexity
and performance of parsing with derivatives." ACM SIGPLAN Notices 51.6
(2016): 224-236.
"""
cache_key = "cached_" + name
calculation = "calc_" + name
force_key = "force_" + name
def forced_value(target):
try:
return getattr(target, force_key)
except AttributeError:
return getattr(target, cache_key)
def accept(self):
try:
return forced_value(self)
except AttributeError:
pass
mapping = {}
sentinel = object()
hit_recursion = [False]
# For a first pass we do a direct recursive calculation of the
# property, but we block recursively visiting a value in the
# computation of its property: When that happens, we simply
# note that it happened and return the default value.
def recur(strat):
try:
return forced_value(strat)
except AttributeError:
pass
result = mapping.get(strat, sentinel)
if result is calculating:
hit_recursion[0] = True
return default
elif result is sentinel:
mapping[strat] = calculating
mapping[strat] = getattr(strat, calculation)(recur)
return mapping[strat]
return result
recur(self)
# If we hit self-recursion in the computation of any strategy
# value, our mapping at the end is imprecise - it may or may
# not have the right values in it. We now need to proceed with
# a more careful fixed point calculation to get the exact
# values. Hopefully our mapping is still pretty good and it
# won't take a large number of updates to reach a fixed point.
if hit_recursion[0]:
needs_update = set(mapping)
# We track which strategies use which in the course of
# calculating their property value. If A ever uses B in
# the course of calculating its value, then whenever the
# value of B changes we might need to update the value of
# A.
listeners = defaultdict(set)
else:
needs_update = None
def recur2(strat):
def recur_inner(other):
try:
return forced_value(other)
except AttributeError:
pass
listeners[other].add(strat)
result = mapping.get(other, sentinel)
if result is sentinel:
needs_update.add(other)
mapping[other] = default
return default
return result
return recur_inner
count = 0
seen = set()
while needs_update:
count += 1
# If we seem to be taking a really long time to stabilize we
# start tracking seen values to attempt to detect an infinite
# loop. This should be impossible, and most code will never
# hit the count, but having an assertion for it means that
# testing is easier to debug and we don't just have a hung
# test.
# Note: This is actually covered, by test_very_deep_deferral
# in tests/cover/test_deferred_strategies.py. Unfortunately it
# runs into a coverage bug. See
# https://github.com/nedbat/coveragepy/issues/605
# for details.
if count > 50: # pragma: no cover
key = frozenset(mapping.items())
assert key not in seen, (key, name)
seen.add(key)
to_update = needs_update
needs_update = set()
for strat in to_update:
new_value = getattr(strat, calculation)(recur2(strat))
if new_value != mapping[strat]:
needs_update.update(listeners[strat])
mapping[strat] = new_value
# We now have a complete and accurate calculation of the
# property values for everything we have seen in the course of
# running this calculation. We simultaneously update all of
# them (not just the strategy we started out with).
for k, v in mapping.items():
setattr(k, cache_key, v)
return getattr(self, cache_key)
accept.__name__ = name
return property(accept)
class SearchStrategy(Generic[Ex]):
"""A SearchStrategy is an object that knows how to explore data of a given
type.
Except where noted otherwise, methods on this class are not part of
the public API and their behaviour may change significantly between
minor version releases. They will generally be stable between patch
releases.
"""
supports_find = True
validate_called = False
__label = None
__module__ = "hypothesis.strategies"
def available(self, data):
"""Returns whether this strategy can *currently* draw any
values. This typically useful for stateful testing where ``Bundle``
grows over time a list of value to choose from.
Unlike ``empty`` property, this method's return value may change
over time.
Note: ``data`` parameter will only be used for introspection and no
value drawn from it.
"""
return not self.is_empty
# Returns True if this strategy can never draw a value and will always
# result in the data being marked invalid.
# The fact that this returns False does not guarantee that a valid value
# can be drawn - this is not intended to be perfect, and is primarily
# intended to be an optimisation for some cases.
is_empty = recursive_property("is_empty", True)
# Returns True if values from this strategy can safely be reused without
# this causing unexpected behaviour.
# True if values from this strategy can be implicitly reused (e.g. as
# background values in a numpy array) without causing surprising
# user-visible behaviour. Should be false for built-in strategies that
# produce mutable values, and for strategies that have been mapped/filtered
# by arbitrary user-provided functions.
has_reusable_values = recursive_property("has_reusable_values", True)
# Whether this strategy is suitable for holding onto in a cache.
is_cacheable = recursive_property("is_cacheable", True)
def calc_is_cacheable(self, recur):
return True
def calc_is_empty(self, recur):
# Note: It is correct and significant that the default return value
# from calc_is_empty is False despite the default value for is_empty
# being true. The reason for this is that strategies should be treated
# as empty absent evidence to the contrary, but most basic strategies
# are trivially non-empty and it would be annoying to have to override
# this method to show that.
return False
def calc_has_reusable_values(self, recur):
return False
def example(self) -> Ex:
"""Provide an example of the sort of value that this strategy
generates. This is biased to be slightly simpler than is typical for
values from this strategy, for clarity purposes.
This method shouldn't be taken too seriously. It's here for interactive
exploration of the API, not for any sort of real testing.
This method is part of the public API.
"""
if getattr(sys, "ps1", None) is None: # pragma: no branch
# The other branch *is* covered in cover/test_examples.py; but as that
# uses `pexpect` for an interactive session `coverage` doesn't see it.
warnings.warn(
"The `.example()` method is good for exploring strategies, but should "
"only be used interactively. We recommend using `@given` for tests - "
"it performs better, saves and replays failures to avoid flakiness, "
"and reports minimal examples. (strategy: %r)" % (self,),
NonInteractiveExampleWarning,
stacklevel=2,
)
context = _current_build_context.value
if context is not None:
if context.data is not None and context.data.depth > 0:
raise HypothesisException(
"Using example() inside a strategy definition is a bad "
"idea. Instead consider using hypothesis.strategies.builds() "
"or @hypothesis.strategies.composite to define your strategy."
" See https://hypothesis.readthedocs.io/en/latest/data.html"
"#hypothesis.strategies.builds or "
"https://hypothesis.readthedocs.io/en/latest/data.html"
"#composite-strategies for more details."
)
else:
raise HypothesisException(
"Using example() inside a test function is a bad "
"idea. Instead consider using hypothesis.strategies.data() "
"to draw more examples during testing. See "
"https://hypothesis.readthedocs.io/en/latest/data.html"
"#drawing-interactively-in-tests for more details."
)
try:
return self.__examples.pop()
except (AttributeError, IndexError):
self.__examples: List[Ex] = []
from hypothesis.core import given
# Note: this function has a weird name because it might appear in
# tracebacks, and we want users to know that they can ignore it.
@given(self)
@settings(
database=None,
max_examples=100,
deadline=None,
verbosity=Verbosity.quiet,
phases=(Phase.generate,),
suppress_health_check=list(HealthCheck),
)
def example_generating_inner_function(ex):
self.__examples.append(ex)
example_generating_inner_function()
shuffle(self.__examples)
return self.__examples.pop()
def map(self, pack: Callable[[Ex], T]) -> "SearchStrategy[T]":
"""Returns a new strategy that generates values by generating a value
from this strategy and then calling pack() on the result, giving that.
This method is part of the public API.
"""
if is_identity_function(pack):
return self # type: ignore # Mypy has no way to know that `Ex == T`
return MappedSearchStrategy(pack=pack, strategy=self)
def flatmap(
self, expand: Callable[[Ex], "SearchStrategy[T]"]
) -> "SearchStrategy[T]":
"""Returns a new strategy that generates values by generating a value
from this strategy, say x, then generating a value from
strategy(expand(x))
This method is part of the public API.
"""
from hypothesis.strategies._internal.flatmapped import FlatMapStrategy
return FlatMapStrategy(expand=expand, strategy=self)
def filter(self, condition: Callable[[Ex], Any]) -> "SearchStrategy[Ex]":
"""Returns a new strategy that generates values from this strategy
which satisfy the provided condition. Note that if the condition is too
hard to satisfy this might result in your tests failing with
Unsatisfiable.
This method is part of the public API.
"""
return FilteredStrategy(conditions=(condition,), strategy=self)
def _filter_for_filtered_draw(self, condition):
# Hook for parent strategies that want to perform fallible filtering
# on one of their internal strategies (e.g. UniqueListStrategy).
# The returned object must have a `.do_filtered_draw(data)` method
# that behaves like `do_draw`, but returns the sentinel object
# `filter_not_satisfied` if the condition could not be satisfied.
# This is separate from the main `filter` method so that strategies
# can override `filter` without having to also guarantee a
# `do_filtered_draw` method.
return FilteredStrategy(conditions=(condition,), strategy=self)
@property
def branches(self) -> List["SearchStrategy[Ex]"]:
return [self]
def __or__(self, other: "SearchStrategy[T]") -> "SearchStrategy[Union[Ex, T]]":
"""Return a strategy which produces values by randomly drawing from one
of this strategy or the other strategy.
This method is part of the public API.
"""
if not isinstance(other, SearchStrategy):
raise ValueError(f"Cannot | a SearchStrategy with {other!r}")
return OneOfStrategy((self, other))
def __bool__(self) -> bool:
warnings.warn(
f"bool({self!r}) is always True, did you mean to draw a value?",
HypothesisWarning,
stacklevel=2,
)
return True
def validate(self) -> None:
"""Throw an exception if the strategy is not valid.
This can happen due to lazy construction
"""
if self.validate_called:
return
try:
self.validate_called = True
self.do_validate()
self.is_empty
self.has_reusable_values
except Exception:
self.validate_called = False
raise
LABELS: ClassVar[Dict[type, int]] = {}
@property
def class_label(self):
cls = self.__class__
try:
return cls.LABELS[cls]
except KeyError:
pass
result = calc_label_from_cls(cls)
cls.LABELS[cls] = result
return result
@property
def label(self) -> int:
if self.__label is calculating:
return 0
if self.__label is None:
self.__label = calculating
self.__label = self.calc_label()
return cast(int, self.__label)
def calc_label(self):
return self.class_label
def do_validate(self):
pass
def do_draw(self, data: ConjectureData) -> Ex:
raise NotImplementedError(f"{type(self).__name__}.do_draw")
def __init__(self):
pass
def is_simple_data(value):
try:
hash(value)
return True
except TypeError:
return False
class SampledFromStrategy(SearchStrategy):
"""A strategy which samples from a set of elements. This is essentially
equivalent to using a OneOfStrategy over Just strategies but may be more
efficient and convenient.
The conditional distribution chooses uniformly at random from some
non-empty subset of the elements.
"""
def __init__(self, elements, repr_=None, transformations=()):
super().__init__()
self.elements = cu.check_sample(elements, "sampled_from")
assert self.elements
self.repr_ = repr_
self._transformations = transformations
def map(self, pack):
return type(self)(
self.elements,
repr_=self.repr_,
transformations=(*self._transformations, ("map", pack)),
)
def filter(self, condition):
return type(self)(
self.elements,
repr_=self.repr_,
transformations=(*self._transformations, ("filter", condition)),
)
def __repr__(self):
return (
self.repr_
or "sampled_from(["
+ ", ".join(map(get_pretty_function_description, self.elements))
+ "])"
) + "".join(
f".{name}({get_pretty_function_description(f)})"
for name, f in self._transformations
)
def calc_has_reusable_values(self, recur):
# Because our custom .map/.filter implementations skip the normal
# wrapper strategies (which would automatically return False for us),
# we need to manually return False here if any transformations have
# been applied.
return not self._transformations
def calc_is_cacheable(self, recur):
return is_simple_data(self.elements)
def _transform(self, element):
# Used in UniqueSampledListStrategy
for name, f in self._transformations:
if name == "map":
element = f(element)
else:
assert name == "filter"
if not f(element):
return filter_not_satisfied
return element
def do_draw(self, data):
result = self.do_filtered_draw(data)
if result is filter_not_satisfied:
data.mark_invalid(f"Aborted test because unable to satisfy {self!r}")
return result
def get_element(self, i):
return self._transform(self.elements[i])
def do_filtered_draw(self, data):
# Set of indices that have been tried so far, so that we never test
# the same element twice during a draw.
known_bad_indices = set()
# Start with ordinary rejection sampling. It's fast if it works, and
# if it doesn't work then it was only a small amount of overhead.
for _ in range(3):
i = data.draw_integer(0, len(self.elements) - 1)
if i not in known_bad_indices:
element = self.get_element(i)
if element is not filter_not_satisfied:
return element
if not known_bad_indices:
data.events[f"Retried draw from {self!r} to satisfy filter"] = ""
known_bad_indices.add(i)
# If we've tried all the possible elements, give up now.
max_good_indices = len(self.elements) - len(known_bad_indices)
if not max_good_indices:
return filter_not_satisfied
# Impose an arbitrary cutoff to prevent us from wasting too much time
# on very large element lists.
cutoff = 10000
max_good_indices = min(max_good_indices, cutoff)
# Before building the list of allowed indices, speculatively choose
# one of them. We don't yet know how many allowed indices there will be,
# so this choice might be out-of-bounds, but that's OK.
speculative_index = data.draw_integer(0, max_good_indices - 1)
# Calculate the indices of allowed values, so that we can choose one
# of them at random. But if we encounter the speculatively-chosen one,
# just use that and return immediately. Note that we also track the
# allowed elements, in case of .map(some_stateful_function)
allowed = []
for i in range(min(len(self.elements), cutoff)):
if i not in known_bad_indices:
element = self.get_element(i)
if element is not filter_not_satisfied:
allowed.append((i, element))
if len(allowed) > speculative_index:
# Early-exit case: We reached the speculative index, so
# we just return the corresponding element.
data.draw_integer(0, len(self.elements) - 1, forced=i)
return element
# The speculative index didn't work out, but at this point we've built
# and can choose from the complete list of allowed indices and elements.
if allowed:
i, element = cu.choice(data, allowed)
data.draw_integer(0, len(self.elements) - 1, forced=i)
return element
# If there are no allowed indices, the filter couldn't be satisfied.
return filter_not_satisfied
class OneOfStrategy(SearchStrategy[Ex]):
"""Implements a union of strategies. Given a number of strategies this
generates values which could have come from any of them.
The conditional distribution draws uniformly at random from some
non-empty subset of these strategies and then draws from the
conditional distribution of that strategy.
"""
def __init__(self, strategies):
super().__init__()
strategies = tuple(strategies)
self.original_strategies = list(strategies)
self.__element_strategies = None
self.__in_branches = False
def calc_is_empty(self, recur):
return all(recur(e) for e in self.original_strategies)
def calc_has_reusable_values(self, recur):
return all(recur(e) for e in self.original_strategies)
def calc_is_cacheable(self, recur):
return all(recur(e) for e in self.original_strategies)
@property
def element_strategies(self):
if self.__element_strategies is None:
# While strategies are hashable, they use object.__hash__ and are
# therefore distinguished only by identity.
#
# In principle we could "just" define a __hash__ method
# (and __eq__, but that's easy in terms of type() and hash())
# to make this more powerful, but this is harder than it sounds:
#
# 1. Strategies are often distinguished by non-hashable attributes,
# or by attributes that have the same hash value ("^.+" / b"^.+").
# 2. LazyStrategy: can't reify the wrapped strategy without breaking
# laziness, so there's a hash each for the lazy and the nonlazy.
#
# Having made several attempts, the minor benefits of making strategies
# hashable are simply not worth the engineering effort it would take.
# See also issues #2291 and #2327.
seen = {self}
strategies = []
for arg in self.original_strategies:
check_strategy(arg)
if not arg.is_empty:
for s in arg.branches:
if s not in seen and not s.is_empty:
seen.add(s)
strategies.append(s)
self.__element_strategies = strategies
return self.__element_strategies
def calc_label(self):
return combine_labels(
self.class_label, *(p.label for p in self.original_strategies)
)
def do_draw(self, data: ConjectureData) -> Ex:
strategy = data.draw(
SampledFromStrategy(self.element_strategies).filter(
lambda s: s.available(data)
)
)
return data.draw(strategy)
def __repr__(self):
return "one_of(%s)" % ", ".join(map(repr, self.original_strategies))
def do_validate(self):
for e in self.element_strategies:
e.validate()
@property
def branches(self):
if not self.__in_branches:
try:
self.__in_branches = True
return self.element_strategies
finally:
self.__in_branches = False
else:
return [self]
def filter(self, condition):
return FilteredStrategy(
OneOfStrategy([s.filter(condition) for s in self.original_strategies]),
conditions=(),
)
@overload
def one_of(
__args: Sequence[SearchStrategy[Any]],
) -> SearchStrategy[Any]: # pragma: no cover
...
@overload
def one_of(__a1: SearchStrategy[Ex]) -> SearchStrategy[Ex]: # pragma: no cover
...
@overload
def one_of(
__a1: SearchStrategy[Ex], __a2: SearchStrategy[T]
) -> SearchStrategy[Union[Ex, T]]: # pragma: no cover
...
@overload
def one_of(
__a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3]
) -> SearchStrategy[Union[Ex, T, T3]]: # pragma: no cover
...
@overload
def one_of(
__a1: SearchStrategy[Ex],
__a2: SearchStrategy[T],
__a3: SearchStrategy[T3],
__a4: SearchStrategy[T4],
) -> SearchStrategy[Union[Ex, T, T3, T4]]: # pragma: no cover
...
@overload
def one_of(
__a1: SearchStrategy[Ex],
__a2: SearchStrategy[T],
__a3: SearchStrategy[T3],
__a4: SearchStrategy[T4],
__a5: SearchStrategy[T5],
) -> SearchStrategy[Union[Ex, T, T3, T4, T5]]: # pragma: no cover
...
@overload
def one_of(*args: SearchStrategy[Any]) -> SearchStrategy[Any]: # pragma: no cover
...
@defines_strategy(never_lazy=True)
def one_of(
*args: Union[Sequence[SearchStrategy[Any]], SearchStrategy[Any]]
) -> SearchStrategy[Any]:
# Mypy workaround alert: Any is too loose above; the return parameter
# should be the union of the input parameters. Unfortunately, Mypy <=0.600
# raises errors due to incompatible inputs instead. See #1270 for links.
# v0.610 doesn't error; it gets inference wrong for 2+ arguments instead.
"""Return a strategy which generates values from any of the argument
strategies.
This may be called with one iterable argument instead of multiple
strategy arguments, in which case ``one_of(x)`` and ``one_of(*x)`` are
equivalent.
Examples from this strategy will generally shrink to ones that come from
strategies earlier in the list, then shrink according to behaviour of the
strategy that produced them. In order to get good shrinking behaviour,
try to put simpler strategies first. e.g. ``one_of(none(), text())`` is
better than ``one_of(text(), none())``.
This is especially important when using recursive strategies. e.g.
``x = st.deferred(lambda: st.none() | st.tuples(x, x))`` will shrink well,
but ``x = st.deferred(lambda: st.tuples(x, x) | st.none())`` will shrink
very badly indeed.
"""
if len(args) == 1 and not isinstance(args[0], SearchStrategy):
try:
args = tuple(args[0])
except TypeError:
pass
if len(args) == 1 and isinstance(args[0], SearchStrategy):
# This special-case means that we can one_of over lists of any size
# without incurring any performance overhead when there is only one
# strategy, and keeps our reprs simple.
return args[0]
if args and not any(isinstance(a, SearchStrategy) for a in args):
# And this special case is to give a more-specific error message if it
# seems that the user has confused `one_of()` for `sampled_from()`;
# the remaining validation is left to OneOfStrategy. See PR #2627.
raise InvalidArgument(
f"Did you mean st.sampled_from({list(args)!r})? st.one_of() is used "
"to combine strategies, but all of the arguments were of other types."
)
return OneOfStrategy(args)
class MappedSearchStrategy(SearchStrategy[Ex]):
"""A strategy which is defined purely by conversion to and from another
strategy.
Its parameter and distribution come from that other strategy.
"""
def __init__(self, strategy, pack=None):
super().__init__()
self.mapped_strategy = strategy
if pack is not None:
self.pack = pack
def calc_is_empty(self, recur):
return recur(self.mapped_strategy)
def calc_is_cacheable(self, recur):
return recur(self.mapped_strategy)
def __repr__(self):
if not hasattr(self, "_cached_repr"):
self._cached_repr = f"{self.mapped_strategy!r}.map({get_pretty_function_description(self.pack)})"
return self._cached_repr
def do_validate(self):
self.mapped_strategy.validate()
def pack(self, x):
"""Take a value produced by the underlying mapped_strategy and turn it
into a value suitable for outputting from this strategy."""
raise NotImplementedError(f"{self.__class__.__name__}.pack()")
def do_draw(self, data: ConjectureData) -> Any:
with warnings.catch_warnings():
if isinstance(self.pack, type) and issubclass(
self.pack, (abc.Mapping, abc.Set)
):
warnings.simplefilter("ignore", BytesWarning)
for _ in range(3):
try:
data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL)
x = data.draw(self.mapped_strategy)
result = self.pack(x) # type: ignore
data.stop_example()
_current_build_context.value.record_call(result, self.pack, [x], {})
return result
except UnsatisfiedAssumption:
data.stop_example(discard=True)
raise UnsatisfiedAssumption
@property
def branches(self) -> List[SearchStrategy[Ex]]:
return [
MappedSearchStrategy(pack=self.pack, strategy=strategy)
for strategy in self.mapped_strategy.branches
]
filter_not_satisfied = UniqueIdentifier("filter not satisfied")
class FilteredStrategy(SearchStrategy[Ex]):
def __init__(self, strategy, conditions):
super().__init__()
if isinstance(strategy, FilteredStrategy):
# Flatten chained filters into a single filter with multiple conditions.
self.flat_conditions = strategy.flat_conditions + conditions
self.filtered_strategy = strategy.filtered_strategy
else:
self.flat_conditions = conditions
self.filtered_strategy = strategy
assert isinstance(self.flat_conditions, tuple)
assert not isinstance(self.filtered_strategy, FilteredStrategy)
self.__condition = None
def calc_is_empty(self, recur):
return recur(self.filtered_strategy)
def calc_is_cacheable(self, recur):
return recur(self.filtered_strategy)
def __repr__(self):
if not hasattr(self, "_cached_repr"):
self._cached_repr = "{!r}{}".format(
self.filtered_strategy,
"".join(
f".filter({get_pretty_function_description(cond)})"
for cond in self.flat_conditions
),
)
return self._cached_repr
def do_validate(self):
# Start by validating our inner filtered_strategy. If this was a LazyStrategy,
# validation also reifies it so that subsequent calls to e.g. `.filter()` will
# be passed through.
self.filtered_strategy.validate()
# So now we have a reified inner strategy, we'll replay all our saved
# predicates in case some or all of them can be rewritten. Note that this
# replaces the `fresh` strategy too!
fresh = self.filtered_strategy
for cond in self.flat_conditions:
fresh = fresh.filter(cond)
if isinstance(fresh, FilteredStrategy):
# In this case we have at least some non-rewritten filter predicates,
# so we just re-initialize the strategy.
FilteredStrategy.__init__(
self, fresh.filtered_strategy, fresh.flat_conditions
)
else:
# But if *all* the predicates were rewritten... well, do_validate() is
# an in-place method so we still just re-initialize the strategy!
FilteredStrategy.__init__(self, fresh, ())
def filter(self, condition):
# If we can, it's more efficient to rewrite our strategy to satisfy the
# condition. We therefore exploit the fact that the order of predicates
# doesn't matter (`f(x) and g(x) == g(x) and f(x)`) by attempting to apply
# condition directly to our filtered strategy as the inner-most filter.
out = self.filtered_strategy.filter(condition)
# If it couldn't be rewritten, we'll get a new FilteredStrategy - and then
# combine the conditions of each in our expected newest=last order.
if isinstance(out, FilteredStrategy):
return FilteredStrategy(
out.filtered_strategy, self.flat_conditions + out.flat_conditions
)
# But if it *could* be rewritten, we can return the more efficient form!
return FilteredStrategy(out, self.flat_conditions)
@property
def condition(self):
if self.__condition is None:
if len(self.flat_conditions) == 1:
# Avoid an extra indirection in the common case of only one condition.
self.__condition = self.flat_conditions[0]
elif len(self.flat_conditions) == 0:
# Possible, if unlikely, due to filter predicate rewriting
self.__condition = lambda _: True
else:
self.__condition = lambda x: all(
cond(x) for cond in self.flat_conditions
)
return self.__condition
def do_draw(self, data: ConjectureData) -> Ex:
result = self.do_filtered_draw(data)
if result is not filter_not_satisfied:
return result
data.mark_invalid(f"Aborted test because unable to satisfy {self!r}")
raise NotImplementedError("Unreachable, for Mypy")
def do_filtered_draw(self, data):
for i in range(3):
start_index = data.index
data.start_example(FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL)
value = data.draw(self.filtered_strategy)
if self.condition(value):
data.stop_example()
return value
else:
data.stop_example(discard=True)
if i == 0:
data.events[f"Retried draw from {self!r} to satisfy filter"] = ""
# This is to guard against the case where we consume no data.
# As long as we consume data, we'll eventually pass or raise.
# But if we don't this could be an infinite loop.
assume(data.index > start_index)
return filter_not_satisfied
@property
def branches(self) -> List[SearchStrategy[Ex]]:
return [
FilteredStrategy(strategy=strategy, conditions=self.flat_conditions)
for strategy in self.filtered_strategy.branches
]
@check_function
def check_strategy(arg, name=""):
assert isinstance(name, str)
if not isinstance(arg, SearchStrategy):
hint = ""
if isinstance(arg, (list, tuple)):
hint = ", such as st.sampled_from({}),".format(name or "...")
if name:
name += "="
raise InvalidArgument(
"Expected a SearchStrategy%s but got %s%r (type=%s)"
% (hint, name, arg, type(arg).__name__)
)

View File

@@ -0,0 +1,237 @@
# 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 copy
import re
import warnings
from functools import lru_cache
from hypothesis.errors import HypothesisWarning, InvalidArgument
from hypothesis.internal import charmap
from hypothesis.internal.intervalsets import IntervalSet
from hypothesis.strategies._internal.collections import ListStrategy
from hypothesis.strategies._internal.lazy import unwrap_strategies
from hypothesis.strategies._internal.strategies import SearchStrategy
class OneCharStringStrategy(SearchStrategy):
"""A strategy which generates single character strings of text type."""
def __init__(self, intervals, force_repr=None):
assert isinstance(intervals, IntervalSet)
self.intervals = intervals
self._force_repr = force_repr
@classmethod
def from_characters_args(
cls,
*,
codec=None,
min_codepoint=None,
max_codepoint=None,
categories=None,
exclude_characters=None,
include_characters=None,
):
assert set(categories or ()).issubset(charmap.categories())
intervals = charmap.query(
min_codepoint=min_codepoint,
max_codepoint=max_codepoint,
categories=categories,
exclude_characters=exclude_characters,
include_characters=include_characters,
)
if codec is not None:
intervals &= charmap.intervals_from_codec(codec)
_arg_repr = ", ".join(
f"{k}={v!r}"
for k, v in [
("codec", codec),
("min_codepoint", min_codepoint),
("max_codepoint", max_codepoint),
("categories", categories),
("exclude_characters", exclude_characters),
("include_characters", include_characters),
]
if v not in (None, "", set(charmap.categories()) - {"Cs"})
)
if not intervals:
raise InvalidArgument(
"No characters are allowed to be generated by this "
f"combination of arguments: {_arg_repr}"
)
return cls(intervals, force_repr=f"characters({_arg_repr})")
def __repr__(self):
return self._force_repr or f"OneCharStringStrategy({self.intervals!r})"
def do_draw(self, data):
return data.draw_string(self.intervals, min_size=1, max_size=1)
class TextStrategy(ListStrategy):
def do_draw(self, data):
# if our element strategy is OneCharStringStrategy, we can skip the
# ListStrategy draw and jump right to our nice IR string draw.
# Doing so for user-provided element strategies is not correct in
# general, as they may define a different distribution than our IR.
elems = unwrap_strategies(self.element_strategy)
if isinstance(elems, OneCharStringStrategy):
return data.draw_string(
elems.intervals, min_size=self.min_size, max_size=self.max_size
)
return "".join(super().do_draw(data))
def __repr__(self):
args = []
if repr(self.element_strategy) != "characters()":
args.append(repr(self.element_strategy))
if self.min_size:
args.append(f"min_size={self.min_size}")
if self.max_size < float("inf"):
args.append(f"max_size={self.max_size}")
return f"text({', '.join(args)})"
# See https://docs.python.org/3/library/stdtypes.html#string-methods
# These methods always return Truthy values for any nonempty string.
_nonempty_filters = (
*ListStrategy._nonempty_filters,
str,
str.capitalize,
str.casefold,
str.encode,
str.expandtabs,
str.join,
str.lower,
str.rsplit,
str.split,
str.splitlines,
str.swapcase,
str.title,
str.upper,
)
_nonempty_and_content_filters = (
str.isidentifier,
str.islower,
str.isupper,
str.isalnum,
str.isalpha,
str.isascii,
str.isdecimal,
str.isdigit,
str.isnumeric,
str.isspace,
str.istitle,
str.lstrip,
str.rstrip,
str.strip,
)
def filter(self, condition):
if condition in (str.lower, str.title, str.upper):
warnings.warn(
f"You applied str.{condition.__name__} as a filter, but this allows "
f"all nonempty strings! Did you mean str.is{condition.__name__}?",
HypothesisWarning,
stacklevel=2,
)
elems = unwrap_strategies(self.element_strategy)
if (
condition is str.isidentifier
and self.max_size >= 1
and isinstance(elems, OneCharStringStrategy)
):
from hypothesis.strategies import builds, nothing
id_start, id_continue = _identifier_characters()
if not (elems.intervals & id_start):
return nothing()
return builds(
"{}{}".format,
OneCharStringStrategy(elems.intervals & id_start),
TextStrategy(
OneCharStringStrategy(elems.intervals & id_continue),
min_size=max(0, self.min_size - 1),
max_size=self.max_size - 1,
),
# Filter to ensure that NFKC normalization keeps working in future
).filter(str.isidentifier)
# We use ListStrategy filter logic for the conditions that *only* imply
# the string is nonempty. Here, we increment the min_size but still apply
# the filter for conditions that imply nonempty *and specific contents*.
if condition in self._nonempty_and_content_filters:
assert self.max_size >= 1, "Always-empty is special cased in st.text()"
self = copy.copy(self)
self.min_size = max(1, self.min_size)
return ListStrategy.filter(self, condition)
return super().filter(condition)
# Excerpted from https://www.unicode.org/Public/15.0.0/ucd/PropList.txt
# Python updates it's Unicode version between minor releases, but fortunately
# these properties do not change between the Unicode versions in question.
_PROPLIST = """
# ================================================
1885..1886 ; Other_ID_Start # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA
2118 ; Other_ID_Start # Sm SCRIPT CAPITAL P
212E ; Other_ID_Start # So ESTIMATED SYMBOL
309B..309C ; Other_ID_Start # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
# Total code points: 6
# ================================================
00B7 ; Other_ID_Continue # Po MIDDLE DOT
0387 ; Other_ID_Continue # Po GREEK ANO TELEIA
1369..1371 ; Other_ID_Continue # No [9] ETHIOPIC DIGIT ONE..ETHIOPIC DIGIT NINE
19DA ; Other_ID_Continue # No NEW TAI LUE THAM DIGIT ONE
# Total code points: 12
"""
@lru_cache
def _identifier_characters():
"""See https://docs.python.org/3/reference/lexical_analysis.html#identifiers"""
# Start by computing the set of special characters
chars = {"Other_ID_Start": "", "Other_ID_Continue": ""}
for line in _PROPLIST.splitlines():
if m := re.match(r"([0-9A-F.]+) +; (\w+) # ", line):
codes, prop = m.groups()
span = range(int(codes[:4], base=16), int(codes[-4:], base=16) + 1)
chars[prop] += "".join(chr(x) for x in span)
# Then get the basic set by Unicode category and known extras
id_start = charmap.query(
categories=("Lu", "Ll", "Lt", "Lm", "Lo", "Nl"),
include_characters="_" + chars["Other_ID_Start"],
)
id_start -= IntervalSet.from_string(
# Magic value: the characters which NFKC-normalize to be invalid identifiers.
# Conveniently they're all in `id_start`, so we only need to do this once.
"\u037a\u0e33\u0eb3\u2e2f\u309b\u309c\ufc5e\ufc5f\ufc60\ufc61\ufc62\ufc63"
"\ufdfa\ufdfb\ufe70\ufe72\ufe74\ufe76\ufe78\ufe7a\ufe7c\ufe7e\uff9e\uff9f"
)
id_continue = id_start | charmap.query(
categories=("Mn", "Mc", "Nd", "Pc"),
include_characters=chars["Other_ID_Continue"],
)
return id_start, id_continue
class FixedSizeBytes(SearchStrategy):
def __init__(self, size):
self.size = size
def do_draw(self, data):
return bytes(data.draw_bytes(self.size))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
# 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 threading
from inspect import signature
from typing import TYPE_CHECKING, Callable, Dict
import attr
from hypothesis.internal.cache import LRUReusedCache
from hypothesis.internal.compat import dataclass_asdict
from hypothesis.internal.floats import float_to_int
from hypothesis.internal.reflection import proxies
from hypothesis.vendor.pretty import pretty
if TYPE_CHECKING:
from hypothesis.strategies._internal.strategies import SearchStrategy, T
_strategies: Dict[str, Callable[..., "SearchStrategy"]] = {}
class FloatKey:
def __init__(self, f):
self.value = float_to_int(f)
def __eq__(self, other):
return isinstance(other, FloatKey) and (other.value == self.value)
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.value)
def convert_value(v):
if isinstance(v, float):
return FloatKey(v)
return (type(v), v)
_CACHE = threading.local()
def get_cache() -> LRUReusedCache:
try:
return _CACHE.STRATEGY_CACHE
except AttributeError:
_CACHE.STRATEGY_CACHE = LRUReusedCache(1024)
return _CACHE.STRATEGY_CACHE
def clear_cache() -> None:
cache = get_cache()
cache.clear()
def cacheable(fn: "T") -> "T":
from hypothesis.strategies._internal.strategies import SearchStrategy
@proxies(fn)
def cached_strategy(*args, **kwargs):
try:
kwargs_cache_key = {(k, convert_value(v)) for k, v in kwargs.items()}
except TypeError:
return fn(*args, **kwargs)
cache_key = (fn, tuple(map(convert_value, args)), frozenset(kwargs_cache_key))
cache = get_cache()
try:
if cache_key in cache:
return cache[cache_key]
except TypeError:
return fn(*args, **kwargs)
else:
result = fn(*args, **kwargs)
if not isinstance(result, SearchStrategy) or result.is_cacheable:
cache[cache_key] = result
return result
cached_strategy.__clear_cache = clear_cache # type: ignore
return cached_strategy
def defines_strategy(
*,
force_reusable_values: bool = False,
try_non_lazy: bool = False,
never_lazy: bool = False,
) -> Callable[["T"], "T"]:
"""Returns a decorator for strategy functions.
If ``force_reusable_values`` is True, the returned strategy will be marked
with ``.has_reusable_values == True`` even if it uses maps/filters or
non-reusable strategies internally. This tells our numpy/pandas strategies
that they can implicitly use such strategies as background values.
If ``try_non_lazy`` is True, attempt to execute the strategy definition
function immediately, so that a LazyStrategy is only returned if this
raises an exception.
If ``never_lazy`` is True, the decorator performs no lazy-wrapping at all,
and instead returns the original function.
"""
def decorator(strategy_definition):
"""A decorator that registers the function as a strategy and makes it
lazily evaluated."""
_strategies[strategy_definition.__name__] = signature(strategy_definition)
if never_lazy:
assert not try_non_lazy
# We could potentially support never_lazy + force_reusable_values
# with a suitable wrapper, but currently there are no callers that
# request this combination.
assert not force_reusable_values
return strategy_definition
from hypothesis.strategies._internal.lazy import LazyStrategy
@proxies(strategy_definition)
def accept(*args, **kwargs):
if try_non_lazy:
# Why not try this unconditionally? Because we'd end up with very
# deep nesting of recursive strategies - better to be lazy unless we
# *know* that eager evaluation is the right choice.
try:
return strategy_definition(*args, **kwargs)
except Exception:
# If invoking the strategy definition raises an exception,
# wrap that up in a LazyStrategy so it happens again later.
pass
result = LazyStrategy(strategy_definition, args, kwargs)
if force_reusable_values:
# Setting `force_has_reusable_values` here causes the recursive
# property code to set `.has_reusable_values == True`.
result.force_has_reusable_values = True
assert result.has_reusable_values
return result
accept.is_hypothesis_strategy_function = True
return accept
return decorator
def to_jsonable(obj: object) -> object:
"""Recursively convert an object to json-encodable form.
This is not intended to round-trip, but rather provide an analysis-ready
format for observability. To avoid side affects, we pretty-print all but
known types.
"""
if isinstance(obj, (str, int, float, bool, type(None))):
if isinstance(obj, int) and abs(obj) >= 2**63:
return float(obj)
return obj
if isinstance(obj, (list, tuple, set, frozenset)):
if isinstance(obj, tuple) and hasattr(obj, "_asdict"):
return to_jsonable(obj._asdict()) # treat namedtuples as dicts
return [to_jsonable(x) for x in obj]
if isinstance(obj, dict):
return {
k if isinstance(k, str) else pretty(k): to_jsonable(v)
for k, v in obj.items()
}
# Special handling for dataclasses, attrs, and pydantic classes
if (
(dcs := sys.modules.get("dataclasses"))
and dcs.is_dataclass(obj)
and not isinstance(obj, type)
):
return to_jsonable(dataclass_asdict(obj))
if attr.has(type(obj)):
return to_jsonable(attr.asdict(obj, recurse=False)) # type: ignore
if (pyd := sys.modules.get("pydantic")) and isinstance(obj, pyd.BaseModel):
return to_jsonable(obj.model_dump())
# If all else fails, we'll just pretty-print as a string.
return pretty(obj)