diff options
| author | robot-piglet <[email protected]> | 2026-06-12 23:53:32 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-06-13 00:16:39 +0300 |
| commit | bf1639fd14cbc553114b0b296d799fda6b1f97c8 (patch) | |
| tree | cfc1b0874bd6b6fdaa20ec57afebb56840612a02 /contrib/python | |
| parent | c283ae2aba847b444692dec65fb0e9a7d043d4a0 (diff) | |
Intermediate changes
commit_hash:603483c94b54cb723799b5a0726871b2331f9c29
Diffstat (limited to 'contrib/python')
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) |
