summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-06-12 23:53:32 +0300
committerrobot-piglet <[email protected]>2026-06-13 00:16:39 +0300
commitbf1639fd14cbc553114b0b296d799fda6b1f97c8 (patch)
treecfc1b0874bd6b6fdaa20ec57afebb56840612a02 /contrib/python
parentc283ae2aba847b444692dec65fb0e9a7d043d4a0 (diff)
Intermediate changes
commit_hash:603483c94b54cb723799b5a0726871b2331f9c29
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/core.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/numpy.py23
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py37
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py4
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py13
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py29
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py159
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py23
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py9
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/version.py2
-rw-r--r--contrib/python/hypothesis/py3/ya.make2
12 files changed, 221 insertions, 88 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA
index 5765424bbb5..5f53851c0d3 100644
--- a/contrib/python/hypothesis/py3/.dist-info/METADATA
+++ b/contrib/python/hypothesis/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: hypothesis
-Version: 6.153.2
+Version: 6.155.0
Summary: The property-based testing library for Python
Author-email: "David R. MacIver and Zac Hatfield-Dodds" <[email protected]>
License-Expression: MPL-2.0
diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py
index beb28c539a3..44418c255ed 100644
--- a/contrib/python/hypothesis/py3/hypothesis/core.py
+++ b/contrib/python/hypothesis/py3/hypothesis/core.py
@@ -2328,7 +2328,11 @@ def given(
except UnsatisfiedAssumption:
status = Status.INVALID
return None
- except BaseException:
+ except BaseException as e:
+ # The engine sets data.interesting_origin in
+ # _execute_once_for_engine, but fuzz_one_input calls
+ # execute_once directly, so we replicate it here.
+ data.interesting_origin = InterestingOrigin.from_exception(e)
known = minimal_failures.get(data.interesting_origin)
if settings.database is not None and (
known is None or sort_key(data.nodes) <= sort_key(known)
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
index eee98231592..0d93bdb82bb 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
@@ -110,6 +110,19 @@ TIME_RESOLUTIONS = ("Y", "M", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs",
NP_FIXED_UNICODE = tuple(int(x) for x in np.__version__.split(".")[:2]) >= (1, 19)
+def _reject_dtype_class(dtype: object) -> None:
+ # A common mistake is to pass a dtype *class*, e.g. np.dtypes.StringDType,
+ # rather than an instance such as np.dtypes.StringDType(). numpy silently
+ # coerces such classes to the object dtype, so we reject them with a more
+ # helpful message than the resulting confusion further down the line.
+ if isinstance(dtype, type) and issubclass(dtype, np.dtype):
+ name = getattr(dtype, "__name__", repr(dtype))
+ raise InvalidArgument(
+ f"Cannot infer a strategy from the dtype class {name}; pass an "
+ f"instance instead, e.g. {name}() rather than {name}."
+ )
+
+
@defines_strategy(force_reusable_values=True)
def from_dtype(
dtype: np.dtype,
@@ -137,6 +150,7 @@ def from_dtype(
:func:`arrays` which allow a variety of numeric dtypes, as it seamlessly
handles the ``width`` or representable bounds for you.
"""
+ _reject_dtype_class(dtype)
check_type(np.dtype, dtype, "dtype")
kwargs = {k: v for k, v in locals().items() if k != "dtype" and v is not None}
@@ -214,6 +228,14 @@ def from_dtype(
result = st.text(**compat_kw("alphabet", "min_size", max_size=max_size)).filter(
lambda b: b[-1:] != "\0"
)
+ elif dtype.kind == "T":
+ # NumPy 2.0+ variable-width strings (StringDType). Unlike the fixed-width
+ # "U"/"S" dtypes, these store arbitrary Python strings with no length
+ # limit and no null-termination, so we can use st.text() directly - but
+ # the UTF-8 backing storage means we must exclude lone surrogates.
+ if "alphabet" not in kwargs:
+ kwargs["alphabet"] = st.characters(codec="utf-8")
+ result = st.text(**compat_kw("alphabet", "min_size", "max_size"))
elif dtype.kind in ("m", "M"):
if "[" in dtype.str:
res = st.just(dtype.str.split("[")[-1][:-1])
@@ -555,6 +577,7 @@ def arrays(
lambda s: arrays(dtype, s, elements=elements, fill=fill, unique=unique)
)
# From here on, we're only dealing with values and it's relatively simple.
+ _reject_dtype_class(dtype)
dtype = np.dtype(dtype) # type: ignore[arg-type]
assert isinstance(dtype, np.dtype) # help mypy out a bit...
if elements is None or isinstance(elements, Mapping):
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
index ded037098d6..6605242ba36 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
@@ -226,6 +226,7 @@ StatisticsDict = TypedDict(
"generate-phase": NotRequired[PhaseStatistics],
"reuse-phase": NotRequired[PhaseStatistics],
"shrink-phase": NotRequired[PhaseStatistics],
+ "explain-phase": NotRequired[PhaseStatistics],
"stopped-because": NotRequired[str],
"targets": NotRequired[dict[str, float]],
"nodeid": NotRequired[str],
@@ -308,6 +309,8 @@ class ConjectureRunner:
self._current_phase: str = "(not a phase)"
self.statistics: StatisticsDict = {}
self.stats_per_test_case: list[CallStats] = []
+ # Time spent in any nested phase, so the enclosing phase can exclude it.
+ self._nested_phase_seconds: float = 0.0
self.interesting_examples: dict[InterestingOrigin, ConjectureResult] = {}
# We use call_count because there may be few possible valid_examples.
@@ -379,20 +382,36 @@ class ConjectureRunner:
@contextmanager
def _log_phase_statistics(
- self, phase: Literal["reuse", "generate", "shrink"]
+ self, phase: Literal["reuse", "generate", "shrink", "explain"]
) -> Generator[None, None, None]:
- self.stats_per_test_case.clear()
+ # Phases may nest - the explain phase runs inside the shrink phase - so
+ # we save and restore the per-call stats and current phase, exclude the
+ # duration of any nested phase, and accumulate when a phase is entered
+ # more than once (the explain phase runs once per shrinking target).
+ saved_stats = self.stats_per_test_case
+ saved_phase = self._current_phase
+ saved_nested_seconds = self._nested_phase_seconds
+ self.stats_per_test_case = []
+ self._current_phase = phase
+ self._nested_phase_seconds = 0.0
start_time = time.perf_counter()
try:
- self._current_phase = phase
yield
finally:
- self.statistics[phase + "-phase"] = { # type: ignore
- "duration-seconds": time.perf_counter() - start_time,
- "test-cases": list(self.stats_per_test_case),
- "distinct-failures": len(self.interesting_examples),
- "shrinks-successful": self.shrinks,
- }
+ elapsed = time.perf_counter() - start_time
+ # A phase can be entered more than once (the explain phase runs once
+ # per shrinking target), so accumulate into any existing bucket.
+ stats = self.statistics.setdefault(
+ phase + "-phase", # type: ignore
+ {"duration-seconds": 0.0, "test-cases": []},
+ )
+ stats["duration-seconds"] += elapsed - self._nested_phase_seconds
+ stats["test-cases"] += self.stats_per_test_case
+ stats["distinct-failures"] = len(self.interesting_examples)
+ stats["shrinks-successful"] = self.shrinks
+ self.stats_per_test_case = saved_stats
+ self._current_phase = saved_phase
+ self._nested_phase_seconds = saved_nested_seconds + elapsed
@property
def should_optimise(self) -> bool:
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
index f36739f0f4e..8f8875e5188 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
@@ -491,10 +491,12 @@ class Shrinker:
self.explain()
def explain(self) -> None:
-
if not self.should_explain or not self.shrink_target.arg_slices:
return
+ with self.engine._log_phase_statistics("explain"):
+ self._explain()
+ def _explain(self) -> None:
self.max_stall = 2**100
shrink_target = self.shrink_target
nodes = self.nodes
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py
index e916fe2e3f1..9f8b41f709e 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py
@@ -77,6 +77,19 @@ def identity(v: T) -> T:
return v
+def fisher_yates_shuffle(data: "ConjectureData", ls: list[T]) -> None:
+ """Shuffle ``ls`` in place, drawing from ``data``.
+
+ Reversed Fisher-Yates shuffle: swap each element with itself or with a
+ later element. This shrinks i==j for each element, i.e. towards no change,
+ so a shuffled sequence shrinks back to its original order. We don't
+ consider the last element as it's always a no-op.
+ """
+ for i in range(len(ls) - 1):
+ j = data.draw_integer(i, len(ls) - 1)
+ ls[i], ls[j] = ls[j], ls[i]
+
+
def check_sample(
values: type[enum.Enum] | Sequence[T], strategy_name: str
) -> Sequence[T]:
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
index 97de24f249b..735145638ac 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
@@ -10,7 +10,7 @@
import copy
import math
-from collections.abc import Callable, Iterable
+from collections.abc import Callable, Iterable, Mapping
from typing import Any, TypeGuard, overload
from hypothesis import strategies as st
@@ -370,37 +370,37 @@ class UniqueSampledListStrategy(UniqueListStrategy):
return result
-class FixedDictStrategy(SearchStrategy[dict[Any, Any]]):
- """A strategy which produces dicts with a fixed set of keys, given a
+class FixedDictStrategy(SearchStrategy[Mapping[Any, Any]]):
+ """A strategy which produces mappings 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
+ e.g. {'foo' : some_int_strategy} would generate mappings with the single
key 'foo' mapping to some integer.
"""
def __init__(
self,
- mapping: dict[Any, SearchStrategy[Any]],
+ mapping: Mapping[Any, SearchStrategy[Any]],
*,
- optional: dict[Any, SearchStrategy[Any]] | None,
+ optional: Mapping[Any, SearchStrategy[Any]] | None,
):
super().__init__()
dict_type = type(mapping)
self.mapping = mapping
keys = tuple(mapping.keys())
self.fixed = st.tuples(*[mapping[k] for k in keys]).map(
- lambda value: dict_type(zip(keys, value, strict=True))
+ lambda value: dict_type(zip(keys, value, strict=True)) # type: ignore
)
self.optional = optional
- def do_draw(self, data: ConjectureData) -> dict[Any, Any]:
+ def do_draw(self, data: ConjectureData) -> Mapping[Any, Any]:
context = current_build_context()
arg_labels: ArgLabelsT = {}
- value = type(self.mapping)()
+ pairs: list[tuple[Any, Any]] = []
for key, strategy in self.mapping.items():
with context.track_arg_label(str(key)) as arg_label:
- value[key] = data.draw(strategy)
+ pairs.append((key, data.draw(strategy)))
arg_labels |= arg_label
if self.optional is not None:
@@ -416,12 +416,17 @@ class FixedDictStrategy(SearchStrategy[dict[Any, Any]]):
remaining[-1], remaining[j] = remaining[j], remaining[-1]
key = remaining.pop()
with context.track_arg_label(str(key)) as arg_label:
- value[key] = data.draw(self.optional[key])
+ pairs.append((key, data.draw(self.optional[key])))
arg_labels |= arg_label
+ # Vary the dict's iteration order (#3906). We shuffle after choosing
+ # the optional keys, so only order varies, not the set of keys.
+ cu.fisher_yates_shuffle(data, pairs)
+ value = type(self.mapping)(pairs) # type: ignore
+
if arg_labels:
context.known_object_printers[IDKey(value)].append(
- _fixeddict_pprinter(arg_labels, self.mapping)
+ _fixeddict_pprinter(arg_labels)
)
return value
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
index 9ef4a555c41..3df73ed2905 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
@@ -87,6 +87,7 @@ from hypothesis.internal.conjecture.utils import (
calc_label_from_name,
check_sample,
combine_labels,
+ fisher_yates_shuffle,
identity,
)
from hypothesis.internal.entropy import get_seeder_and_restorer
@@ -498,43 +499,59 @@ def iterables(
).map(PrettyIter)
-# this type definition is imprecise, in multiple ways:
-# * mapping and optional can be of different types:
-# s: dict[str | int, int] = st.fixed_dictionaries(
-# {"a": st.integers()}, optional={1: st.integers()}
-# )
-# * the values in either mapping or optional need not all be of the same type:
-# s: dict[str, int | bool] = st.fixed_dictionaries(
-# {"a": st.integers(), "b": st.booleans()}
-# )
-# * the arguments may be of any dict-compatible type, in which case the return
-# value will be of that type instead of dict
+# fixed_dictionaries accepts Mapping rather than the invariant dict so that
+# type-checkers can infer the value type even when the per-key strategies are
+# heterogeneous: Mapping is covariant in its value type and SearchStrategy is
+# covariant in its own, so e.g. `SearchStrategy[int] | SearchStrategy[str]` is
+# accepted as `SearchStrategy[int | str]`. The overloads let mapping and
+# optional contribute independent key and value types, which are unioned in the
+# result. See revealed_types.py for the resulting types.
#
-# Overloads may help here, but I doubt we'll be able to satisfy all these
-# constraints.
+# We use fresh typevars rather than the module-level Ex because Ex has a default
+# (PEP 696), and a defaulted typevar may not precede a bare one in a signature.
#
-# Here's some platonic ideal test cases for revealed_types.py, with the understanding
-# that some may not be achievable:
-#
-# ("fixed_dictionaries({'a': booleans()})", "dict[str, bool]"),
-# ("fixed_dictionaries({'a': booleans(), 'b': integers()})", "dict[str, bool | int]"),
-# ("fixed_dictionaries({}, optional={'a': booleans()})", "dict[str, bool]"),
-# (
-# "fixed_dictionaries({'a': booleans()}, optional={1: booleans()})",
-# "dict[str | int, bool]",
-# ),
-# (
-# "fixed_dictionaries({'a': booleans()}, optional={1: integers()})",
-# "dict[str | int, bool | int]",
-# ),
+# The remaining imprecision is that we always report a plain dict, even though
+# at runtime the result preserves the concrete (dict-subclass) type of mapping.
+K = TypeVar("K")
+V = TypeVar("V")
+K2 = TypeVar("K2")
+V2 = TypeVar("V2")
+
+
+@overload
+def fixed_dictionaries(
+ mapping: Mapping[K, SearchStrategy[V]],
+) -> SearchStrategy[dict[K, V]]: # pragma: no cover
+ ...
+
+
+@overload
+def fixed_dictionaries(
+ # Matching an empty mapping against NoReturn lets the result come solely
+ # from optional, rather than picking up a spurious `Any` from the empty
+ # mapping (whose key and value types are otherwise uninferable).
+ mapping: Mapping[NoReturn, NoReturn],
+ *,
+ optional: Mapping[K2, SearchStrategy[V2]],
+) -> SearchStrategy[dict[K2, V2]]: # pragma: no cover
+ ...
+
+
+@overload
+def fixed_dictionaries(
+ mapping: Mapping[K, SearchStrategy[V]],
+ *,
+ optional: Mapping[K2, SearchStrategy[V2]],
+) -> SearchStrategy[dict[K | K2, V | V2]]: # pragma: no cover
+ ...
@defines_strategy()
def fixed_dictionaries(
- mapping: dict[T, SearchStrategy[Ex]],
+ mapping: Mapping[Any, SearchStrategy[Any]],
*,
- optional: dict[T, SearchStrategy[Ex]] | None = None,
-) -> SearchStrategy[dict[T, Ex]]:
+ optional: Mapping[Any, SearchStrategy[Any]] | None = None,
+) -> SearchStrategy[dict[Any, Any]]:
"""Generates a dictionary of the same type as mapping with a fixed set of
keys mapping to strategies. ``mapping`` must be a dict subclass.
@@ -548,12 +565,12 @@ def fixed_dictionaries(
Examples from this strategy shrink by shrinking each individual value in
the generated dictionary, and omitting optional key-value pairs.
"""
- check_type(dict, mapping, "mapping")
+ check_type(Mapping, mapping, "mapping")
for k, v in mapping.items():
check_strategy(v, f"mapping[{k!r}]")
if optional is not None:
- check_type(dict, optional, "optional")
+ check_type(Mapping, optional, "optional")
for k, v in optional.items():
check_strategy(v, f"optional[{k!r}]")
if type(mapping) != type(optional):
@@ -568,7 +585,13 @@ def fixed_dictionaries(
f"which is invalid: {set(mapping) & set(optional)!r}"
)
- return FixedDictStrategy(mapping, optional=optional)
+ # FixedDictStrategy honestly types itself as SearchStrategy[Mapping], since
+ # type(mapping)(pairs) may return any Mapping subclass. We narrow to dict
+ # here because that's what callers almost always get and find convenient.
+ return cast(
+ "SearchStrategy[dict[Any, Any]]",
+ FixedDictStrategy(mapping, optional=optional),
+ )
_get_first_item = operator.itemgetter(0)
@@ -1303,6 +1326,7 @@ def _from_type_deferred(thing: type[Ex]) -> SearchStrategy[Ex]:
_recurse_guard: ContextVar = ContextVar("recurse_guard")
+_abstract_recurse_guard: ContextVar = ContextVar("abstract_recurse_guard")
def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]:
@@ -1552,16 +1576,20 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]:
# a subclass of `thing` and are not themselves a subtype of any other such
# type. For example, `Number -> integers() | floats()`, but bools() is
# not included because bool is a subclass of int as well as Number.
+ # Filter to matching subtypes *before* sorting, because computing the repr
+ # of every registered strategy (just to establish a deterministic order) is
+ # surprisingly expensive and usually wasted - the matching set is typically
+ # empty for user-defined types.
+ matching = [
+ (k, v)
+ for k, v in types._global_type_lookup.items()
+ if isinstance(k, type)
+ and issubclass(k, thing)
+ and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) == 1
+ ]
strategies = [
s
- for s in (
- as_strategy(v, thing)
- for k, v in sorted(types._global_type_lookup.items(), key=repr)
- if isinstance(k, type)
- and issubclass(k, thing)
- and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup)
- == 1
- )
+ for s in (as_strategy(v, thing) for _, v in sorted(matching, key=repr))
if s is not NotImplemented
]
if any(not s.is_empty for s in strategies):
@@ -1644,12 +1672,35 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]:
"type without any subclasses. Consider using register_type_strategy"
)
- subclass_strategies: SearchStrategy = nothing()
- for sc in subclasses:
- try:
- subclass_strategies |= _from_type(sc)
- except Exception:
- pass
+ # When subclasses reference `thing` (directly, or via a sibling subclass)
+ # in their own annotations, naively resolving each subclass would re-resolve
+ # the entire hierarchy once per reference - which is combinatorially
+ # expensive for mutually-recursive types. We track the abstract types we're
+ # currently resolving and defer any recursive reference back to them (by
+ # returning the cached strategy, so the references share one object - which
+ # lets recursion in e.g. is_empty checks terminate), so each type is resolved
+ # only once per pass. We use a guard separate from `_recurse_guard` because
+ # this catches references regardless of how they reach `_from_type` (e.g. as a
+ # union arg), and because it must not make `from_type_guarded` treat a
+ # subclass's required field of type `thing` as unresolvable.
+ try:
+ abstract_guard = _abstract_recurse_guard.get()
+ except LookupError:
+ _abstract_recurse_guard.set(abstract_guard := set())
+ if thing in abstract_guard:
+ return from_type(thing)
+
+ abstract_guard.add(thing)
+ try:
+ substrategies = []
+ for sc in subclasses:
+ try:
+ substrategies.append(_from_type(sc))
+ except Exception:
+ pass
+ finally:
+ abstract_guard.discard(thing)
+ subclass_strategies = one_of(substrategies)
if subclass_strategies.is_empty:
# We're unable to resolve subclasses now, but we might be able to later -
# so we'll just go back to the mixed distribution.
@@ -1840,10 +1891,13 @@ def decimals(
factor = Decimal(10) ** -places
min_num, max_num = None, None
+ # Work out the integer bounds exactly: limited-precision division can
+ # round when the bounds have more than `places` fractional digits,
+ # which would make ceil/floor over- or undershoot the true bound.
if min_value is not None:
- min_num = ceil(ctx(min_value).divide(min_value, factor))
+ min_num = ceil(Fraction(min_value) / Fraction(factor))
if max_value is not None:
- max_num = floor(ctx(max_value).divide(max_value, factor))
+ max_num = floor(Fraction(max_value) / Fraction(factor))
if min_num is not None and max_num is not None and min_num > max_num:
raise InvalidArgument(
f"There are no decimals with {places} places between "
@@ -1911,13 +1965,8 @@ class PermutationStrategy(SearchStrategy):
self.values = values
def do_draw(self, data):
- # Reversed Fisher-Yates shuffle: swap each element with itself or with
- # a later element. This shrinks i==j for each element, i.e. to no
- # change. We don't consider the last element as it's always a no-op.
result = list(self.values)
- for i in range(len(result) - 1):
- j = data.draw_integer(i, len(result) - 1)
- result[i], result[j] = result[j], result[i]
+ fisher_yates_shuffle(data, result)
return result
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
index b65fea49398..48e8913b408 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
@@ -30,6 +30,7 @@ import uuid
import warnings
import zoneinfo
from collections.abc import Iterator
+from contextvars import ContextVar
from functools import partial
from pathlib import PurePath
from types import FunctionType
@@ -220,6 +221,13 @@ def type_sorting_key(t):
return (is_container, repr(t))
+# Types whose forward references we are currently resolving, used to break the
+# recursion in self- or mutually-referential forward references such as
+# ``A = list[Union["A", str]]``. Without this we would recurse until hitting a
+# RecursionError, which makes resolution depend on the ambient stack depth.
+_forward_ref_resolution: ContextVar[list] = ContextVar("forward_ref_resolution")
+
+
def _resolve_forward_ref_in_caller(forward_arg: str) -> typing.Any:
"""Try to resolve a forward reference name by walking up the call stack.
@@ -679,7 +687,20 @@ def from_typing_type(thing):
if resolved is None: # pragma: no branch
resolved = _resolve_forward_ref_in_caller(thing.__forward_arg__)
if resolved is not None and is_a_type(resolved):
- return st.from_type(resolved)
+ try:
+ in_progress = _forward_ref_resolution.get()
+ except LookupError:
+ _forward_ref_resolution.set(in_progress := [])
+ if resolved in in_progress:
+ # We're already resolving this type higher up the stack, so this
+ # is a recursive reference; defer to break the cycle and rely on
+ # st.from_type's cache to tie the recursive knot.
+ return st.deferred(lambda r=resolved: st.from_type(r))
+ in_progress.append(resolved)
+ try:
+ return st.from_type(resolved)
+ finally:
+ in_progress.pop()
def is_maximal(t):
# For each k in the mapping, we use it if it's the most general type
diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
index 51b11433a36..79b59494cae 100644
--- a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
+++ b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
@@ -964,10 +964,7 @@ def _tuple_pprinter(arg_labels: ArgLabelsT) -> PrettyPrintFunction:
return inner
-def _fixeddict_pprinter(
- arg_labels: ArgLabelsT,
- mapping: dict[Any, Any],
-) -> PrettyPrintFunction:
+def _fixeddict_pprinter(arg_labels: ArgLabelsT) -> PrettyPrintFunction:
"""Pretty printer for fixed_dictionaries that shows sub-argument comments."""
def inner(obj: dict, p: RepresentationPrinter, cycle: bool) -> None:
@@ -975,8 +972,8 @@ def _fixeddict_pprinter(
return p.text("{...}")
get = lambda k: _get_slice_comment(p, arg_labels, k)
- # Preserve mapping key order, then any optional keys (deduped)
- keys = list(dict.fromkeys(k for k in [*mapping, *obj] if k in obj))
+ # Print in the dict's actual (possibly permuted) iteration order.
+ keys = list(obj)
has_comments = any(get(k) for k in keys)
with p.group(indent=4, open="{", close=""):
diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py
index 7ea0a18e4e5..e437edaa976 100644
--- a/contrib/python/hypothesis/py3/hypothesis/version.py
+++ b/contrib/python/hypothesis/py3/hypothesis/version.py
@@ -8,5 +8,5 @@
# 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/.
-__version_info__ = (6, 153, 2)
+__version_info__ = (6, 155, 0)
__version__ = ".".join(map(str, __version_info__))
diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make
index 3390164dd29..82e97ca0ac4 100644
--- a/contrib/python/hypothesis/py3/ya.make
+++ b/contrib/python/hypothesis/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(6.153.2)
+VERSION(6.155.0)
LICENSE(MPL-2.0)