扫码登录,获取cookies
This commit is contained in:
141
backend/venv/Lib/site-packages/hypothesis/strategies/__init__.py
Normal file
141
backend/venv/Lib/site-packages/hypothesis/strategies/__init__.py
Normal 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
|
||||
Binary file not shown.
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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]
|
||||
@@ -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__)
|
||||
)
|
||||
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user