diff options
| author | robot-piglet <[email protected]> | 2026-06-03 11:48:18 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-06-03 13:01:08 +0300 |
| commit | 8f985d25514540edfb1474af9eeb9f0d5c420d55 (patch) | |
| tree | 6d0af8185f26c264f661316b63b389373257cde3 /contrib/python/hypothesis | |
| parent | b62b275069b9c743d3da207efbc6e03206212f47 (diff) | |
Intermediate changes
commit_hash:d2da6ed53d161b48478eb9496a3b244ce234b972
Diffstat (limited to 'contrib/python/hypothesis')
23 files changed, 401 insertions, 111 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index 21754a6e162..053754adaf7 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.152.7 +Version: 6.152.9 Summary: The property-based testing library for Python Author-email: "David R. MacIver and Zac Hatfield-Dodds" <[email protected]> License-Expression: MPL-2.0 @@ -60,21 +60,21 @@ Requires-Dist: dpcontracts>=0.4; extra == "dpcontracts" Provides-Extra: redis Requires-Dist: redis>=3.0.0; extra == "redis" Provides-Extra: crosshair -Requires-Dist: hypothesis-crosshair>=0.0.27; extra == "crosshair" -Requires-Dist: crosshair-tool>=0.0.102; extra == "crosshair" +Requires-Dist: hypothesis-crosshair>=0.0.28; extra == "crosshair" +Requires-Dist: crosshair-tool>=0.0.104; extra == "crosshair" Provides-Extra: zoneinfo -Requires-Dist: tzdata>=2026.1; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "zoneinfo" +Requires-Dist: tzdata>=2026.2; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "zoneinfo" Provides-Extra: django -Requires-Dist: django>=4.2; extra == "django" +Requires-Dist: django>=5.2; extra == "django" Provides-Extra: watchdog Requires-Dist: watchdog>=4.0.0; extra == "watchdog" Provides-Extra: all Requires-Dist: black>=20.8b0; extra == "all" Requires-Dist: click>=7.0; extra == "all" -Requires-Dist: crosshair-tool>=0.0.102; extra == "all" -Requires-Dist: django>=4.2; extra == "all" +Requires-Dist: crosshair-tool>=0.0.104; extra == "all" +Requires-Dist: django>=5.2; extra == "all" Requires-Dist: dpcontracts>=0.4; extra == "all" -Requires-Dist: hypothesis-crosshair>=0.0.27; extra == "all" +Requires-Dist: hypothesis-crosshair>=0.0.28; extra == "all" Requires-Dist: lark>=0.10.1; extra == "all" Requires-Dist: libcst>=0.3.16; extra == "all" Requires-Dist: numpy>=1.21.6; extra == "all" @@ -84,7 +84,7 @@ Requires-Dist: python-dateutil>=1.4; extra == "all" Requires-Dist: pytz>=2014.1; extra == "all" Requires-Dist: redis>=3.0.0; extra == "all" Requires-Dist: rich>=9.0.0; extra == "all" -Requires-Dist: tzdata>=2026.1; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "all" +Requires-Dist: tzdata>=2026.2; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "all" Requires-Dist: watchdog>=4.0.0; extra == "all" Dynamic: license-file diff --git a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py index 5eaadadd512..2a5ae3915a4 100644 --- a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py +++ b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py @@ -83,8 +83,8 @@ def modules_imported_by(mod: str) -> FTZCulprits: # We don't want to redo all the expensive process-spawning checks when we've already # done them, so we cache known-good packages and a known-FTZ result if we have one. -KNOWN_FTZ = None -CHECKED_CACHE = set() +KNOWN_FTZ: str | None = None +CHECKED_CACHE: set[str] = set() def identify_ftz_culprits() -> str: diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py index ca8048ee876..0417703c213 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py @@ -129,20 +129,20 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand): else node.args ), ) - via: cst.BaseExpression = cst.Call( + expr: cst.BaseExpression = cst.Call( func=cst.Attribute(node, cst.Name("via")), args=[cst.Arg(cst.SimpleString(repr(via)))], ) if black: # pragma: no branch try: pretty = black.format_str( - cst.Module([]).code_for_node(via), + cst.Module([]).code_for_node(expr), mode=black.Mode(line_length=self.line_length), ) except (ImportError, AttributeError): # pragma: no cover return None # See https://github.com/psf/black/pull/4224 - via = cst.parse_expression(pretty.strip()) - return cst.Decorator(via) + expr = cst.parse_expression(pretty.strip()) + return cst.Decorator(expr) def leave_FunctionDef( self, _original_node: cst.FunctionDef, updated_node: cst.FunctionDef diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py b/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py index a41f7f48241..debc7af7750 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py @@ -21,7 +21,7 @@ and :func:`~hypothesis.strategies.times` produce timezone-aware values. import datetime as dt -from dateutil import tz, zoneinfo # type: ignore +from dateutil import tz, zoneinfo from hypothesis import strategies as st from hypothesis.strategies._internal.utils import cacheable, defines_strategy diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py index 960b52be75f..2dece58af29 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py @@ -368,6 +368,7 @@ def from_field(field: F) -> st.SearchStrategy[F | None]: """ check_type((dm.Field, df.Field), field, "field") + strategy: st.SearchStrategy[Any] # The following isinstance check must occur *before* the getattr # check. In the case of ModelChoicesField, evaluating # field.choices causes database access, which we want to avoid if diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py index ef793634e9a..73216946ce3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py @@ -1806,7 +1806,7 @@ def _make_binop_body( try: identity = find(operands, lambda x: True, settings=_quietly_settings) except Exception: - identity = "identity element here" # type: ignore + identity = "identity element here" # If the repr of this element is invalid Python, stringify it - this # can't be executed as-is, but at least makes it clear what should # happen. E.g. type(None) -> <class 'NoneType'> -> quoted. @@ -1817,7 +1817,7 @@ def _make_binop_body( # executed; so you still shouldn't ghostwrite for hostile code. compile(repr(identity), "<string>", "exec") except SyntaxError: - identity = repr(identity) # type: ignore + identity = repr(identity) identity_parts = [ f"{identity = }", _assert_eq( diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py index ad554e222ee..75702b4abc8 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py @@ -69,17 +69,17 @@ class LarkStrategy(st.SearchStrategy): ) -> None: super().__init__() assert isinstance(grammar, lark.lark.Lark) - start: list[str] = grammar.options.start if start is None else [start] + starts = grammar.options.start if start is None else [start] # This is a total hack, but working around the changes is a nicer user # experience than breaking for anyone who doesn't instantly update their # installation of Lark alongside Hypothesis. compile_args = signature(grammar.grammar.compile).parameters if "terminals_to_keep" in compile_args: - terminals, rules, ignore_names = grammar.grammar.compile(start, ()) + terminals, rules, ignore_names = grammar.grammar.compile(starts, ()) elif "start" in compile_args: # pragma: no cover # Support lark <= 0.10.0, without the terminals_to_keep argument. - terminals, rules, ignore_names = grammar.grammar.compile(start) # type: ignore + terminals, rules, ignore_names = grammar.grammar.compile(starts) # type: ignore else: # pragma: no cover # This branch is to support lark <= 0.7.1, without the start argument. terminals, rules, ignore_names = grammar.grammar.compile() # type: ignore @@ -140,12 +140,12 @@ class LarkStrategy(st.SearchStrategy): ): allowed_rules = {*self.terminal_strategies, *nonterminals} - if set(start).isdisjoint(allowed_rules): + if set(starts).isdisjoint(allowed_rules): raise InvalidArgument( - f"No start rule {tuple(start)} is allowed by {alphabet=}" + f"No start rule {tuple(starts)} is allowed by {alphabet=}" ) self.start = st.sampled_from( - [self.names_to_symbols[s] for s in start if s in allowed_rules] + [self.names_to_symbols[s] for s in starts if s in allowed_rules] ) self.nonterminal_strategies = { diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py index 6f356706def..0463b9b9b09 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py @@ -287,7 +287,7 @@ def stack_depth_of_caller() -> int: frame = sys._getframe(2) size = 1 while frame: - frame = frame.f_back # type: ignore[assignment] + frame = frame.f_back size += 1 return size diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py index be8bbeebf89..a8483a3650a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py @@ -45,8 +45,6 @@ from hypothesis.internal.conjecture.choice import ( from hypothesis.internal.conjecture.floats import lex_to_float from hypothesis.internal.conjecture.junkdrawer import bits_to_bytes from hypothesis.internal.conjecture.utils import ( - INT_SIZES, - INT_SIZES_SAMPLER, Sampler, many, ) @@ -57,6 +55,7 @@ from hypothesis.internal.constants_ast import ( ) from hypothesis.internal.floats import ( SIGNALING_NAN, + clamp, float_to_int, make_float_clamper, next_down, @@ -64,6 +63,11 @@ from hypothesis.internal.floats import ( ) from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import InfoObservationType, TestCaseObservation +from hypothesis.internal.statistics import ( + LogStudentTDistribution, + PiecewiseDistribution, + UniformDistribution, +) if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData @@ -73,6 +77,13 @@ T = TypeVar("T") LifetimeT: TypeAlias = Literal["test_case", "test_function"] COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large" +# see https://github.com/HypothesisWorks/hypothesis/pull/4728 for details +INTEGERS_DISTRIBUTION = PiecewiseDistribution( + inner=UniformDistribution(half_width=256), + outer=LogStudentTDistribution(scale_bits=13, df=2), + switchover=256, +) + #: Registered Hypothesis backends. This is a dictionary where keys are the name #: to be used in |settings.backend|. The value of a key can be either: @@ -845,12 +856,6 @@ class HypothesisProvider(PrimitiveProvider): assert isinstance(constant, int) return constant - center = 0 - if min_value is not None: - center = max(min_value, center) - if max_value is not None: - center = min(max_value, center) - if weights is not None: assert min_value is not None assert max_value is not None @@ -871,28 +876,81 @@ class HypothesisProvider(PrimitiveProvider): idx = sampler.sample(self._cd) if idx == 0: - return self._draw_bounded_integer(min_value, max_value) + return self._draw_integer_from_distribution(min_value, max_value) # implicit reliance on dicts being sorted for determinism return list(weights)[idx - 1] - if min_value is None and max_value is None: - return self._draw_unbounded_integer() + return self._draw_integer_from_distribution(min_value, max_value) + + def _draw_integer_from_distribution( + self, min_value: int | None, max_value: int | None + ) -> int: + assert self._random is not None + dist = INTEGERS_DISTRIBUTION - if min_value is None: + # Our integers distribution is defined over the full float64 range. If the user + # has not placed a lower and/or upper bound, we don't actually want to sample + # from this full range, because we might otherwise generate integers + # that are so large as to cause problems. For example python errors when passing + # a too-large integer to c APIs (common and implicit in some python stdlib APIs). + # + # * If the user asks for an unbounded range, we bound at [-2**128, 2**128]. + # * If the user asks for a half-bounded range starting at n, we bound at + # [n, 2**128] for n < 2**127, and [n, 2n] otherwise, so we always support + # generating large integers if a user explicitly asks for integers around that + # magnitude. The choice of a factor of two here is arbitrary. + if min_value is None and max_value is None: + min_value = -(2**128) + max_value = 2**128 + elif min_value is None: assert max_value is not None - probe = max_value + 1 - while max_value < probe: - probe = center + self._draw_unbounded_integer() - return probe + min_value = -max(2**128, 2 * abs(max_value)) + elif max_value is None: + max_value = max(2**128, 2 * abs(min_value)) - if max_value is None: - assert min_value is not None - probe = min_value - 1 - while probe < min_value: - probe = center + self._draw_unbounded_integer() - return probe + lo = 0.0 + hi = 1.0 + safe_bounds = True + try: + cdf_min = min_value - 0.5 + cdf_max = max_value + 0.5 + except OverflowError: + safe_bounds = False + + if safe_bounds: + lo = dist.cdf(cdf_min) + hi = dist.cdf(cdf_max) + + # if the bounds in CDF-space are too close together, there isn't enough room + # to stably sample between hi and lo. For example, in the extreme case of + # hi = lo (which can happen even if min_value != max_value due to either float + # imprecision or numerical instability), our sampling algorithm would collapse + # to always return the sample value. + # + # The choice of 1e-13 here is somewhat arbitrary. At 1.0, epsilon in float64 + # is 2^-53 ~= 1e-16. We want some breathing room from epsilon, because we need + # some variation to actually sample in. I haven't put much thought into where + # the correct tradeoff lies. The downside risk to choosing a too-aggressive + # bound is some integers may literally not be representable in the `hi - lo` + # sampling space. The downside risk to a too-conservative bound is switching + # off our good distribution too early. + if hi - lo < 1e-13: + safe_bounds = False - return self._draw_bounded_integer(min_value, max_value) + if safe_bounds: + # inverse_cdf requires strictly 0 < p < 1. Resample until we get it. + while (p := lo + self._random.random() * (hi - lo)) in {0, 1}: + pass # pragma: no cover + n = round(dist.inverse_cdf(p)) + # As an extra measure of safety, clamp our generated value to the requested + # range. It would of course be nice if we provably satisfied this by + # construction - I'm just not 100% confident that we do. + return clamp(min_value, n, max_value) + + # We have determined the bounds [min_value, max_value] are not numerically safe + # to use. Fall back to a simpler and safer distribution: a uniform distribution + # on [min_value, max_value]. + return self._random.randint(min_value, max_value) def draw_float( self, @@ -1048,45 +1106,6 @@ class HypothesisProvider(PrimitiveProvider): sign = 1 if self._random.getrandbits(1) else -1 return sign * f - def _draw_unbounded_integer(self) -> int: - assert self._cd is not None - assert self._random is not None - - size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd)] - - r = self._random.getrandbits(size) - sign = r & 1 - r >>= 1 - if sign: - r = -r - return r - - def _draw_bounded_integer( - self, - lower: int, - upper: int, - *, - vary_size: bool = True, - ) -> int: - assert lower <= upper - assert self._cd is not None - assert self._random is not None - - if lower == upper: - return lower - - bits = (upper - lower).bit_length() - if bits > 24 and vary_size and self._random.random() < 7 / 8: - # For large ranges, we combine the uniform random distribution - # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our - # choice of unicode characters is uniform but the 32bit distribution is not. - idx = INT_SIZES_SAMPLER.sample(self._cd) - cap_bits = min(bits, INT_SIZES[idx]) - upper = min(upper, lower + 2**cap_bits - 1) - return self._random.randint(lower, upper) - - return self._random.randint(lower, upper) - # Masks for masking off the first byte of an n-bit buffer. # The appropriate mask is stored at position n % 8. diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py index d27df88c83d..e916fe2e3f1 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py @@ -245,10 +245,6 @@ class Sampler: return base -INT_SIZES = (8, 16, 32, 64, 128) -INT_SIZES_SAMPLER = Sampler((4.0, 8.0, 1.0, 1.0, 0.5), observe=False) - - class many: """Utility class for collections. Bundles up the logic we use for "should I keep drawing more values?" and handles starting and stopping examples in diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py b/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py index 98cffed7b4a..b4d148b5f74 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py @@ -49,7 +49,7 @@ def pretty_file_name(f): IN_COVERAGE_TESTS = os.getenv("HYPOTHESIS_INTERNAL_COVERAGE") == "true" -description_stack = [] +description_stack: list[str] = [] if IN_COVERAGE_TESTS: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py b/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py index cf1f2f4f5d4..10414ed096a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py @@ -272,7 +272,7 @@ def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate: # parsing it to an abstract syntax tree; if this fails for any reason we bail out # and fall back to standard rejection sampling (a running theme). try: - if predicate.__name__ == "<lambda>": + if predicate.__name__ == "<lambda>": # type: ignore source = lambda_description(predicate) else: source = inspect.getsource(predicate) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py index 93f09de185e..e5449199911 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py @@ -12,7 +12,7 @@ import math import struct from collections.abc import Callable from sys import float_info -from typing import Literal, SupportsFloat, TypeAlias +from typing import Literal, SupportsFloat, TypeAlias, overload SignedIntFormat: TypeAlias = Literal["!h", "!i", "!q"] UnsignedIntFormat: TypeAlias = Literal["!H", "!I", "!Q"] @@ -191,6 +191,10 @@ def sign_aware_lte(x: float | int, y: float | int) -> bool: return x <= y +@overload +def clamp(lower: int, value: int, upper: int) -> int: ... +@overload +def clamp(lower: float, value: float, upper: float) -> float: ... def clamp(lower: float | int, value: float | int, upper: float | int) -> float | int: """Given a value and lower/upper bounds, 'clamp' the value so that it satisfies lower <= value <= upper. NaN is mapped to lower.""" diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py index 1c29eff7636..06c6de18941 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py @@ -24,6 +24,7 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import date, timedelta from functools import lru_cache +from pathlib import Path from threading import Lock from typing import ( TYPE_CHECKING, @@ -477,7 +478,7 @@ def make_testcase( ) -_WROTE_TO = set() +_WROTE_TO: set[Path] = set() _deliver_to_file_lock = Lock() diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/statistics.py b/contrib/python/hypothesis/py3/hypothesis/internal/statistics.py new file mode 100644 index 00000000000..43c7159b820 --- /dev/null +++ b/contrib/python/hypothesis/py3/hypothesis/internal/statistics.py @@ -0,0 +1,260 @@ +# 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 abc +import math + +from hypothesis.internal.floats import clamp + +# Liam: please be aware that an LLM wrote the stdtr and stdtrit functions, with some +# guidance from me on comparing to scipy's existing implementation. +# +# This code is strongly in-distribution for AI, and is tested against a strong +# oracle (scipy's implementation), which are the only reasons I am willing to +# accept this code without deeply understanding it. + + +def stdtr(df: int, t: float) -> float: # pragma: no cover # covered by tests/scipy + """Student's t CDF for integer df >= 1, evaluated at t. + + Closed-form finite sum from Abramowitz & Stegun 26.7.7-8. + """ + assert isinstance(df, int) + if df < 1: + raise ValueError(f"stdtr requires integer df >= 1, got {df}") + if t == 0.0: + return 0.5 + abs_t = abs(t) + z = 1.0 + abs_t * abs_t / df + if df % 2 == 1: + # odd: includes an arctan term + u = abs_t / math.sqrt(df) + p = math.atan(u) + if df > 1: + f = 1.0 + tz = 1.0 + j = 3 + while j <= df - 2: + tz *= (j - 1) / (z * j) + f += tz + j += 2 + p += f * u / z + p *= 2.0 / math.pi + else: + # even: simple finite sum, no arctan + f = 1.0 + tz = 1.0 + j = 2 + while j <= df - 2: + tz *= (j - 1) / (z * j) + f += tz + j += 2 + p = f * abs_t / math.sqrt(z * df) + return 0.5 - 0.5 * p if t < 0 else 0.5 + 0.5 * p + + +def stdtrit( + df: int, p: float, *, eps: float = 1e-10, max_iter: int = 50 +) -> float: # pragma: no cover # covered by tests/scipy + """Inverse Student's t CDF (quantile) for integer df >= 1. + + df ∈ {1, 2}: closed-form analytic quantile (Shaw 2006, eq 35-36). + + df >= 3: bracketed Newton iteration on stdtr — doubling search from t=1 + to bracket the target, then Newton steps with bisection fallback. Always + converges; ~5-10 iterations typical. + """ + if df < 1: + raise ValueError(f"stdtrit requires integer df >= 1, got {df}") + if p == 0.5: + return 0.0 + if not 0.0 < p < 1.0: + raise ValueError(f"stdtrit requires 0 < p < 1, got {p}") + if df == 1: + # Cauchy: F^{-1}(p) = -cot(pi p). Reflect via 1-p when p > 0.5 because + # sin(pi p) near pi suffers cancellation; sin(pi (1-p)) near 0 is exact. + if p > 0.5: + return math.cos(math.pi * (1 - p)) / math.sin(math.pi * (1 - p)) + return -math.cos(math.pi * p) / math.sin(math.pi * p) + if df == 2: + return (2 * p - 1) / math.sqrt(2 * p * (1 - p)) + + sign = 1.0 if p > 0.5 else -1.0 + q = p if p > 0.5 else 1 - p + + lo, hi = 0.0, 1.0 + while stdtr(df, hi) < q: + hi *= 2 + + log_norm = ( + math.lgamma(0.5 * (df + 1)) + - 0.5 * math.log(df * math.pi) + - math.lgamma(0.5 * df) + ) + t = 0.5 * (lo + hi) + for _ in range(max_iter): + F = stdtr(df, t) + if F < q: + lo = t + else: + hi = t + log_f = log_norm - 0.5 * (df + 1) * math.log1p(t * t / df) + f = math.exp(log_f) + if f == 0.0: + t = 0.5 * (lo + hi) + else: + t_newton = t - (F - q) / f + t = t_newton if lo <= t_newton <= hi else 0.5 * (lo + hi) + if hi - lo < eps * (1 + abs(t)): + break + return sign * t + + +# Liam: I'm not convinced this abstraction pays for itself. If it's a hindrance in the +# future, feel free to refactor. This class is mainly here to concretize the minimal set +# of abstractions our integer sampling code expects a distribution to define, and to +# provide stronger guardrails around composition. +# +# Note that our code makes the explicit assumption that any _Distribution +# implementation is symmetric around 0. This implies inverse_cdf(0.5) = 0, for example. +# Do not break this assumption without verifying carefully against callers. +class _Distribution(abc.ABC): + @abc.abstractmethod + def cdf(self, x: float) -> float: + raise NotImplementedError + + @abc.abstractmethod + def inverse_cdf(self, u: float) -> float: + raise NotImplementedError + + @abc.abstractmethod + def pdf(self, x: float) -> float: + raise NotImplementedError + + +class UniformDistribution(_Distribution): + """Uniform distribution on [-half_width, half_width].""" + + def __init__(self, *, half_width: float) -> None: + self.half_width = half_width + + def cdf(self, x: float) -> float: + if x < -self.half_width: + return 0.0 # pragma: no cover + if x > self.half_width: + return 1.0 # pragma: no cover + return (x + self.half_width) / (2 * self.half_width) + + def inverse_cdf(self, u: float) -> float: + return -self.half_width + 2 * self.half_width * u + + def pdf(self, x: float) -> float: + if -self.half_width <= x <= self.half_width: + return 1 / (2 * self.half_width) + return 0.0 # pragma: no cover + + +class LogStudentTDistribution(_Distribution): + """Student's t distribution, in the transformed domain of log_2(x). + + Y = sign(x) * log_2(1 + |x|) ~ scale_bits * t(df). + + Note that we only support integer df. This is to reduce surface area in the vendored + mathematical functions, which are simpler when df is an integer. There is no + fundamental difficulty to supporting float df, other than the necessary due diligence + for the vendored code. + """ + + LN2 = math.log(2) + + def __init__(self, *, scale_bits: float, df: int) -> None: + self.scale_bits = scale_bits + self.df = df + self._t_coef = math.gamma((df + 1) / 2) / ( + math.sqrt(df * math.pi) * math.gamma(df / 2) + ) + + def cdf(self, x: float) -> float: + y = math.copysign(math.log2(1 + abs(x)), x) / self.scale_bits + return float(stdtr(self.df, y)) + + def inverse_cdf(self, u: float) -> float: + y = self.scale_bits * float(stdtrit(self.df, u)) + # 2^1023 is the largest power of 2 below sys.float_info.max. This makes y=1023 + # the largest value we can turn into a float to return here without overflowing. + # + # In the rare case that we draw such an extreme value, clamp to the bounds. + y = clamp(-1023, y, 1023) + return math.copysign(math.expm1(abs(y) * self.LN2), y) + + def pdf(self, x: float) -> float: + y = math.copysign(math.log2(1 + abs(x)), x) / self.scale_bits + f_t = self._t_coef * (1 + y * y / self.df) ** (-(self.df + 1) / 2) + return f_t / (self.scale_bits * (1 + abs(x)) * self.LN2) + + +class PiecewiseDistribution: + """Two-region splice: `inner` on (-switchover, switchover), + `outer` on |x| >= switchover. + + Each region is normalized so that the resulting density is continuous at + ±switchover and integrates to 1. Both inner and outer must be symmetric around 0. + """ + + def __init__( + self, *, inner: _Distribution, outer: _Distribution, switchover: float + ) -> None: + # Note that this code could be made simpler by taking advantage of our assumption that + # `inner` and `outer` are symmetric around 0. The code is currently generic enough + # to support asymmetric distributions, but doesn't need to be, + self.inner = inner + self.outer = outer + self.switchover = switchover + self._inner_g_neg = inner.cdf(-switchover) + self._inner_g_pos = inner.cdf(switchover) + self._outer_g_neg = outer.cdf(-switchover) + self._outer_g_pos = outer.cdf(switchover) + outer_outer_mass = 1 - (self._outer_g_pos - self._outer_g_neg) + inner_inner_mass = self._inner_g_pos - self._inner_g_neg + # density continuity: beta * inner.pdf(c) = alpha * outer.pdf(c) + # plus total mass 1; solve for (alpha, beta). + inner_pdf = inner.pdf(switchover) + outer_pdf = outer.pdf(switchover) + assert inner_pdf != 0, ( + f"inner.pdf(switchover={switchover}) == 0; cannot density-match. inner " + "must have support at the boundary." + ) + self._alpha = 1 / (outer_pdf * inner_inner_mass / inner_pdf + outer_outer_mass) + self._beta = self._alpha * outer_pdf / inner_pdf + self._inner_mass = self._beta * inner_inner_mass + self._left_mass = self._alpha * self._outer_g_neg + + def cdf(self, x: float) -> float: + if x <= -self.switchover: + return self._alpha * self.outer.cdf(x) + if x < self.switchover: + return self._left_mass + self._beta * ( + self.inner.cdf(x) - self._inner_g_neg + ) + return ( + self._left_mass + + self._inner_mass + + self._alpha * (self.outer.cdf(x) - self._outer_g_pos) + ) + + def inverse_cdf(self, u: float) -> float: + if u <= self._left_mass: + return self.outer.inverse_cdf(u / self._alpha) + if u < self._left_mass + self._inner_mass: + target = self._inner_g_neg + (u - self._left_mass) / self._beta + return self.inner.inverse_cdf(target) + return self.outer.inverse_cdf( + (u - self._left_mass - self._inner_mass) / self._alpha + self._outer_g_pos + ) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py index 77d3b7f180e..5b8572ec6e9 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py @@ -78,14 +78,16 @@ def from_attrs( ) -> SearchStrategy: """An internal version of builds(), specialised for Attrs classes.""" attributes: tuple[Attribute, ...] = attr.fields(target) - kwargs = {k: v for k, v in kwargs.items() if v is not infer} + new_kwargs: dict[str, SearchStrategy[Any]] = { + k: v for k, v in kwargs.items() if v is not infer + } for name in to_infer: attrib = get_attribute_by_alias(attributes, name, target=target) - kwargs[name] = from_attrs_attribute(attrib, target) + new_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) + return BuildsStrategy(target, args, new_kwargs) def from_attrs_attribute( diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py index 7f395e08cd6..97de24f249b 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py @@ -329,7 +329,7 @@ class UniqueListStrategy(ListStrategy[Ex]): for key, seen in zip(self.keys, seen_sets, strict=True): seen.add(key(value)) if self.tuple_suffixes is not None: - value = (value, *data.draw(self.tuple_suffixes)) # type: ignore + value = (value, *data.draw(self.tuple_suffixes)) result.append(value) assert self.max_size >= len(result) >= self.min_size return result diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 444417a9c60..3a8e45ad0a2 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -18,7 +18,14 @@ import string import sys import typing import warnings -from collections.abc import Callable, Collection, Hashable, Iterable, Sequence +from collections.abc import ( + Callable, + Collection, + Hashable, + Iterable, + Mapping, + Sequence, +) from contextvars import ContextVar from decimal import Context, Decimal, localcontext from fractions import Fraction @@ -364,7 +371,7 @@ def lists( and (elements.end - elements.start) <= 255 ): elements = SampledFromStrategy( - sorted(range(elements.start, elements.end + 1), key=abs) # type: ignore + sorted(range(elements.start, elements.end + 1), key=abs) if elements.end < 0 or elements.start > 0 else ( list(range(elements.end + 1)) @@ -1580,9 +1587,9 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: ) try: hints = get_type_hints(thing) - params = get_signature(thing).parameters + params: Mapping[str, Parameter] = get_signature(thing).parameters except Exception: - params = {} # type: ignore + params = {} posonly_args = [] kwargs = {} diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py index de6a9cb7ffb..cbeac0c42e5 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py @@ -1220,11 +1220,9 @@ class FilteredStrategy(SearchStrategy[Ex]): condition = self.flat_conditions[0] elif len(self.flat_conditions) == 0: # Possible, if unlikely, due to filter predicate rewriting - condition = lambda _: True # type: ignore # covariant type param + condition = lambda _: True else: - condition = lambda x: all( # type: ignore # covariant type param - cond(x) for cond in self.flat_conditions - ) + condition = lambda x: all(cond(x) for cond in self.flat_conditions) self.__condition = condition return condition diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py index 84d73a2d203..b2b3eab417a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py @@ -164,7 +164,7 @@ try: extended_get_origin = typing_extensions.get_origin except AttributeError: # pragma: no cover # `typing_extensions` might not be installed, in this case - fallback: - extended_get_origin = get_origin # type: ignore + extended_get_origin = get_origin # Used on `TypeVar` objects with no default: diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt index f15ce33c96e..6ec5a70f1d6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt @@ -1,4 +1,4 @@ -# Version 2026021400, Last Updated Sat Feb 14 07:07:01 2026 UTC +# Version 2026051600, Last Updated Sat May 16 07:07:02 2026 UTC AAA AARP ABB @@ -741,6 +741,7 @@ MEME MEMORIAL MEN MENU +MERCK MERCKMSD MG MH diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index 38877a283a1..a37af144e24 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, 152, 7) +__version_info__ = (6, 152, 9) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index e40304836ab..6b8398b4059 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.152.7) +VERSION(6.152.9) LICENSE(MPL-2.0) @@ -88,6 +88,7 @@ PY_SRCS( hypothesis/internal/observability.py hypothesis/internal/reflection.py hypothesis/internal/scrutineer.py + hypothesis/internal/statistics.py hypothesis/internal/validation.py hypothesis/provisional.py hypothesis/reporting.py |
