diff options
| author | robot-piglet <[email protected]> | 2026-01-20 15:06:25 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-01-20 15:39:58 +0300 |
| commit | b91838abbe7ab48ede4a5cff56e811aaf525a67b (patch) | |
| tree | fe780697b7e0760c5099edc6fdb7db85021373ed /contrib/python/hypothesis | |
| parent | 31211be095bdda84ca0e0e6f404561c7faff92f3 (diff) | |
Intermediate changes
commit_hash:bb091210500d5d8a7c7263bd9e6946718a164752
Diffstat (limited to 'contrib/python/hypothesis')
20 files changed, 351 insertions, 148 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index e716851be16..00267a4c202 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.148.12 +Version: 6.149.1 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/_settings.py b/contrib/python/hypothesis/py3/hypothesis/_settings.py index 13c90373ed0..0d66eb63368 100644 --- a/contrib/python/hypothesis/py3/hypothesis/_settings.py +++ b/contrib/python/hypothesis/py3/hypothesis/_settings.py @@ -18,7 +18,6 @@ import contextlib import datetime import inspect import os -import warnings from collections.abc import Collection, Generator, Sequence from enum import Enum, EnumMeta, unique from functools import total_ordering @@ -31,13 +30,13 @@ from typing import ( ) from hypothesis.errors import ( - HypothesisDeprecationWarning, InvalidArgument, ) from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type, try_convert from hypothesis.utils.conventions import not_set +from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.dynamicvariables import DynamicVariable if TYPE_CHECKING: @@ -622,7 +621,7 @@ class settings(metaclass=settingsMeta): "default", max_examples=100, derandomize=False, - database=not_set, # see settings.database for the default database + database=not_set, # see settings.database for default behavior verbosity=Verbosity.normal, phases=tuple(Phase), stateful_step_count=50, @@ -1191,20 +1190,6 @@ def local_settings(s: settings) -> Generator[settings, None, None]: yield s -def note_deprecation( - message: str, *, since: str, has_codemod: bool, stacklevel: int = 0 -) -> None: - if since != "RELEASEDAY": - date = datetime.date.fromisoformat(since) - assert datetime.date(2021, 1, 1) <= date - if has_codemod: - message += ( - "\n The `hypothesis codemod` command-line tool can automatically " - "refactor your code to fix this warning." - ) - warnings.warn(HypothesisDeprecationWarning(message), stacklevel=2 + stacklevel) - - default = settings( max_examples=100, derandomize=False, diff --git a/contrib/python/hypothesis/py3/hypothesis/control.py b/contrib/python/hypothesis/py3/hypothesis/control.py index 11f82232311..d0aee23d2f7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/control.py +++ b/contrib/python/hypothesis/py3/hypothesis/control.py @@ -12,13 +12,12 @@ import inspect import math import random from collections import defaultdict -from collections.abc import Callable, Sequence +from collections.abc import Callable, Generator, Sequence from contextlib import contextmanager from typing import Any, Literal, NoReturn, Optional, overload from weakref import WeakKeyDictionary from hypothesis import Verbosity, settings -from hypothesis._settings import note_deprecation from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.internal.conjecture.data import ConjectureData @@ -26,8 +25,9 @@ from hypothesis.internal.observability import observability_enabled from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type from hypothesis.reporting import report, verbose_report +from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.dynamicvariables import DynamicVariable -from hypothesis.vendor.pretty import IDKey, PrettyPrintFunction, pretty +from hypothesis.vendor.pretty import ArgLabelsT, IDKey, PrettyPrintFunction, pretty def _calling_function_location(what: str, frame: Any) -> str: @@ -157,6 +157,33 @@ class BuildContext: defaultdict(list) ) + # Track nested strategy calls for explain-phase label paths + self._label_path: list[str] = [] + + @contextmanager + def track_arg_label(self, label: str) -> Generator[ArgLabelsT, None, None]: + start = len(self.data.nodes) + self._label_path.append(label) + arg_labels: ArgLabelsT = {} + try: + yield arg_labels + finally: + self._label_path.pop() + + # This high up the stack, we can't see or really do much with + # Span / SpanRecord - not least because they're only materialized + # after the test case is completed. + # + # Instead, we'll stash the (start_idx, end_idx) pair on our data object + # for the ConjectureRunner engine to deal with, and mutate the arg_labels + # dict so that the pretty-printer knows where to place the + # which-parts-matter comments later. + end = len(self.data.nodes) + assert start <= end + if start != end: + arg_labels[label] = (start, end) + self.data.arg_slices.add((start, end)) + def record_call( self, obj: object, @@ -164,34 +191,33 @@ class BuildContext: *, args: Sequence[object], kwargs: dict[str, object], + arg_labels: ArgLabelsT | None = None, ) -> None: self.known_object_printers[IDKey(obj)].append( - # _func=func prevents mypy from inferring lambda type. Would need - # paramspec I think - not worth it. - lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call( # type: ignore - obj, cycle, get_pretty_function_description(_func), args, kwargs + lambda obj, p, cycle, *, _func=func, _arg_labels=arg_labels: p.maybe_repr_known_object_as_call( # type: ignore + obj, + cycle, + get_pretty_function_description(_func), + args, + kwargs, + arg_labels=_arg_labels, ) ) - def prep_args_kwargs_from_strategies(self, kwarg_strategies): - arg_labels = {} - kwargs = {} - for k, s in kwarg_strategies.items(): - start_idx = len(self.data.nodes) - with deprecate_random_in_strategy("from {}={!r}", k, s): - obj = self.data.draw(s, observe_as=f"generate:{k}") - end_idx = len(self.data.nodes) - kwargs[k] = obj + def prep_args_kwargs_from_strategies( + self, + kwarg_strategies: dict[str, Any], + ) -> tuple[dict[str, Any], ArgLabelsT]: + arg_labels: ArgLabelsT = {} + kwargs: dict[str, Any] = {} - # This high up the stack, we can't see or really do much with the conjecture - # Example objects - not least because they're only materialized after the - # test case is completed. Instead, we'll stash the (start_idx, end_idx) - # pair on our data object for the ConjectureRunner engine to deal with, and - # pass a dict of such out so that the pretty-printer knows where to place - # the which-parts-matter comments later. - if start_idx != end_idx: - arg_labels[k] = (start_idx, end_idx) - self.data.arg_slices.add((start_idx, end_idx)) + for k, s in kwarg_strategies.items(): + with ( + self.track_arg_label(k) as arg_label, + deprecate_random_in_strategy("from {}={!r}", k, s), + ): + kwargs[k] = self.data.draw(s, observe_as=f"generate:{k}") + arg_labels |= arg_label return kwargs, arg_labels diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py index ba59d9e4b63..d7d1b39e598 100644 --- a/contrib/python/hypothesis/py3/hypothesis/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/core.py @@ -916,7 +916,14 @@ def unwrap_markers_from_group() -> Generator[None, None, None]: class StateForActualGivenExecution: def __init__( - self, stuff, test, settings, random, wrapped_test, *, thread_overlap=None + self, + stuff: Stuff, + test: Callable[..., Any], + settings: Settings, + random: Random, + wrapped_test: Any, + *, + thread_overlap: dict[int, bool] | None = None, ): self.stuff = stuff self.test = test @@ -933,15 +940,18 @@ class StateForActualGivenExecution: self.last_exception = None self.falsifying_examples = () self.ever_executed = False - self.xfail_example_reprs = set() - self.files_to_propagate = set() + self.xfail_example_reprs: set[str] = set() self.failed_normally = False self.failed_due_to_deadline = False - self.explain_traces = defaultdict(set) + self.explain_traces: dict[None | InterestingOrigin, set[Trace]] = defaultdict( + set + ) self._start_timestamp = time.time() self._string_repr = "" - self._timing_features = {} + self._timing_features: dict[str, float] = {} + + self._runner: ConjectureRunner | None = None @property def test_identifier(self) -> str: @@ -1172,6 +1182,7 @@ class StateForActualGivenExecution: def _flaky_replay_to_failure( self, err: FlakyReplay, context: BaseException ) -> FlakyFailure: + assert self._runner is not None # Note that in the mark_interesting case, _context_ itself # is part of err._interesting_examples - but it's not in # _runner.interesting_examples - this is fine, as the context @@ -1193,7 +1204,7 @@ class StateForActualGivenExecution: This allows the engine to assume that any exception other than ``StopTest`` must be a fatal error, and should stop the entire engine. """ - trace: Trace = set() + trace: Trace = frozenset() try: with Tracer(should_trace=self._should_trace()) as tracer: try: @@ -1203,7 +1214,7 @@ class StateForActualGivenExecution: ): # pragma: no cover # This is in fact covered by our *non-coverage* tests, but due # to the settrace() contention *not* by our coverage tests. - self.explain_traces[None].add(frozenset(tracer.branches)) + self.explain_traces[None].add(tracer.branches) finally: trace = tracer.branches if result is not None: @@ -1287,7 +1298,7 @@ class StateForActualGivenExecution: interesting_origin = InterestingOrigin.from_exception(e) if trace: # pragma: no cover # Trace collection is explicitly disabled under coverage. - self.explain_traces[interesting_origin].add(frozenset(trace)) + self.explain_traces[interesting_origin].add(trace) if interesting_origin.exc_type == DeadlineExceeded: self.failed_due_to_deadline = True self.explain_traces.clear() @@ -1381,13 +1392,14 @@ class StateForActualGivenExecution: else: database_key = None - runner = self._runner = ConjectureRunner( + runner = ConjectureRunner( self._execute_once_for_engine, settings=self.settings, random=self.random, database_key=database_key, thread_overlap=self.thread_overlap, ) + self._runner = runner # Use the Conjecture engine to run the test function many times # on different inputs. runner.run() diff --git a/contrib/python/hypothesis/py3/hypothesis/database.py b/contrib/python/hypothesis/py3/hypothesis/database.py index 9106a2d2193..68568675f32 100644 --- a/contrib/python/hypothesis/py3/hypothesis/database.py +++ b/contrib/python/hypothesis/py3/hypothesis/database.py @@ -37,11 +37,11 @@ from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from zipfile import BadZipFile, ZipFile -from hypothesis._settings import note_deprecation from hypothesis.configuration import storage_directory from hypothesis.errors import HypothesisException, HypothesisWarning from hypothesis.internal.conjecture.choice import ChoiceT from hypothesis.utils.conventions import UniqueIdentifier, not_set +from hypothesis.utils.deprecation import note_deprecation __all__ = [ "DirectoryBasedExampleDatabase", diff --git a/contrib/python/hypothesis/py3/hypothesis/errors.py b/contrib/python/hypothesis/py3/hypothesis/errors.py index 0e8fa889df8..daf81828c97 100644 --- a/contrib/python/hypothesis/py3/hypothesis/errors.py +++ b/contrib/python/hypothesis/py3/hypothesis/errors.py @@ -222,8 +222,8 @@ class Frozen(HypothesisException): def __getattr__(name: str) -> Any: if name == "MultipleFailures": - from hypothesis._settings import note_deprecation from hypothesis.internal.compat import BaseExceptionGroup + from hypothesis.utils.deprecation import note_deprecation note_deprecation( "MultipleFailures is deprecated; use the builtin `BaseExceptionGroup` type " diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py index e78b0dd15d2..0cf3efaea3a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py @@ -27,7 +27,6 @@ from typing import ( import numpy as np from hypothesis import strategies as st -from hypothesis._settings import note_deprecation from hypothesis.errors import HypothesisException, InvalidArgument from hypothesis.extra._array_helpers import ( _BIE, @@ -60,6 +59,7 @@ from hypothesis.strategies._internal.strategies import ( check_strategy, ) from hypothesis.strategies._internal.utils import defines_strategy +from hypothesis.utils.deprecation import note_deprecation def _try_import(mod_name: str, attr_name: str) -> Any: diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py index 4d4c00f1277..12da7370a68 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py @@ -19,7 +19,6 @@ import numpy as np import pandas from hypothesis import strategies as st -from hypothesis._settings import note_deprecation from hypothesis.control import reject from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as npst @@ -33,6 +32,7 @@ from hypothesis.internal.validation import ( ) from hypothesis.strategies._internal.strategies import Ex, check_strategy from hypothesis.strategies._internal.utils import cacheable, defines_strategy +from hypothesis.utils.deprecation import note_deprecation try: from pandas.core.arrays.integer import IntegerDtype diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py index 4ac53e9dd92..9ac26e1cbaf 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py @@ -70,6 +70,7 @@ from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import PredicateCounts from hypothesis.reporting import debug_report from hypothesis.utils.conventions import not_set +from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.threading import ThreadLocal if TYPE_CHECKING: @@ -81,7 +82,6 @@ if TYPE_CHECKING: def __getattr__(name: str) -> Any: if name == "AVAILABLE_PROVIDERS": - from hypothesis._settings import note_deprecation from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS note_deprecation( diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py index 85b34f474fb..3a1c1944b4c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py @@ -23,7 +23,7 @@ from random import Random from typing import Literal, NoReturn, cast from hypothesis import HealthCheck, Phase, Verbosity, settings as Settings -from hypothesis._settings import local_settings, note_deprecation +from hypothesis._settings import local_settings from hypothesis.database import ExampleDatabase, choices_from_bytes, choices_to_bytes from hypothesis.errors import ( BackendCannotProceed, @@ -70,6 +70,7 @@ from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import Observation, with_observability_callback from hypothesis.reporting import base_report, report, verbose_report +from hypothesis.utils.deprecation import note_deprecation # In most cases, the following constants are all Final. However, we do allow users # to monkeypatch all of these variables, which means we cannot annotate them as diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py index 8b5af7f6138..50e1ff89f4c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py @@ -490,6 +490,17 @@ class Shrinker: ): continue + # Skip slices that are subsets of already-explained slices. + # If a larger slice can vary freely, so can its sub-slices. + # Note: (0, 0) is a special marker for the "together" comment that + # applies to the whole test, not a specific slice, so we exclude it. + if any( + s <= start and end <= e + for s, e in self.shrink_target.slice_comments + if (s, e) != (0, 0) + ): + continue + # Run our experiments n_same_failures = 0 note = "or any other generated value" @@ -569,7 +580,10 @@ class Shrinker: if len(self.shrink_target.slice_comments) <= 1: return n_same_failures_together = 0 - chunks_by_start_index = sorted(chunks.items()) + # Only include slices that were actually added to slice_comments + chunks_by_start_index = sorted( + (k, v) for k, v in chunks.items() if k in self.shrink_target.slice_comments + ) for _ in range(500): # pragma: no branch # no-branch here because we don't coverage-test the abort-at-500 logic. new_choices: list[ChoiceT] = [] diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py index 84d5b51bf49..ad674f22651 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py @@ -51,6 +51,7 @@ from hypothesis.internal.conjecture.choice import ( from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.floats import float_to_int from hypothesis.internal.intervalsets import IntervalSet +from hypothesis.utils.deprecation import note_deprecation if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData, Spans, Status @@ -355,8 +356,6 @@ class _TestcaseCallbacks: return bool(_callbacks) def _note_deprecation(self): - from hypothesis._settings import note_deprecation - note_deprecation( "hypothesis.internal.observability.TESTCASE_CALLBACKS is deprecated. " "Replace TESTCASE_CALLBACKS.append with add_observability_callback, " diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py index a4665d13752..d1429110695 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py @@ -21,15 +21,18 @@ from enum import IntEnum from functools import lru_cache, reduce from os import sep from pathlib import Path -from typing import TypeAlias +from typing import TYPE_CHECKING, TypeAlias from hypothesis._settings import Phase, Verbosity from hypothesis.internal.compat import PYPY from hypothesis.internal.escalation import is_hypothesis_file +if TYPE_CHECKING: + from typing_extensions import Self + Location: TypeAlias = tuple[str, int] Branch: TypeAlias = tuple[Location | None, Location] -Trace: TypeAlias = set[Branch] +Trace: TypeAlias = frozenset[Branch] @functools.cache @@ -52,14 +55,14 @@ class Tracer: """A super-simple branch coverage tracer.""" __slots__ = ( + "_branches", "_previous_location", "_should_trace", "_tried_and_failed_to_trace", - "branches", ) def __init__(self, *, should_trace: bool) -> None: - self.branches: Trace = set() + self._branches: set[Branch] = set() self._previous_location: Location | None = None self._tried_and_failed_to_trace = False self._should_trace = should_trace and self.can_trace() @@ -72,6 +75,10 @@ class Tracer: return sys.monitoring.get_tool(MONITORING_TOOL_ID) is None return sys.gettrace() is None + @property + def branches(self) -> Trace: + return frozenset(self._branches) + def trace(self, frame, event, arg): try: if event == "call": @@ -80,7 +87,7 @@ class Tracer: fname = frame.f_code.co_filename if should_trace_file(fname): current_location = (fname, frame.f_lineno) - self.branches.add((self._previous_location, current_location)) + self._branches.add((self._previous_location, current_location)) self._previous_location = current_location except RecursionError: pass @@ -93,10 +100,10 @@ class Tracer: return sys.monitoring.DISABLE # type: ignore current_location = (fname, line_number) - self.branches.add((self._previous_location, current_location)) + self._branches.add((self._previous_location, current_location)) self._previous_location = current_location - def __enter__(self): + def __enter__(self) -> "Self": self._tried_and_failed_to_trace = False if not self._should_trace: diff --git a/contrib/python/hypothesis/py3/hypothesis/stateful.py b/contrib/python/hypothesis/py3/hypothesis/stateful.py index 8026d2d1672..2103fdcb2c5 100644 --- a/contrib/python/hypothesis/py3/hypothesis/stateful.py +++ b/contrib/python/hypothesis/py3/hypothesis/stateful.py @@ -28,12 +28,7 @@ from typing import Any, ClassVar, TypeVar, overload from unittest import TestCase from hypothesis import strategies as st -from hypothesis._settings import ( - HealthCheck, - Verbosity, - note_deprecation, - settings as Settings, -) +from hypothesis._settings import HealthCheck, Verbosity, settings as Settings from hypothesis.control import _current_build_context, current_build_context from hypothesis.core import TestFunc, given from hypothesis.errors import ( @@ -62,6 +57,7 @@ from hypothesis.strategies._internal.strategies import ( SearchStrategy, check_strategy, ) +from hypothesis.utils.deprecation import note_deprecation from hypothesis.vendor.pretty import RepresentationPrinter T = TypeVar("T") diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py index d585d296f71..7a604bb8833 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py @@ -14,6 +14,7 @@ from collections.abc import Callable, Iterable from typing import Any, overload from hypothesis import strategies as st +from hypothesis.control import current_build_context from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData @@ -37,6 +38,12 @@ from hypothesis.strategies._internal.strategies import ( ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.conventions import UniqueIdentifier +from hypothesis.vendor.pretty import ( + ArgLabelsT, + IDKey, + _fixeddict_pprinter, + _tuple_pprinter, +) class TupleStrategy(SearchStrategy[tuple[Ex, ...]]): @@ -64,7 +71,20 @@ class TupleStrategy(SearchStrategy[tuple[Ex, ...]]): return all(recur(e) for e in self.element_strategies) def do_draw(self, data: ConjectureData) -> tuple[Ex, ...]: - return tuple(data.draw(e) for e in self.element_strategies) + context = current_build_context() + arg_labels: ArgLabelsT = {} + result = [] + for i, strategy in enumerate(self.element_strategies): + with context.track_arg_label(f"arg[{i}]") as arg_label: + result.append(data.draw(strategy)) + arg_labels |= arg_label + + result = tuple(result) + if arg_labels: + context.known_object_printers[IDKey(result)].append( + _tuple_pprinter(arg_labels) + ) + return result def calc_is_empty(self, recur: RecurT) -> bool: return any(recur(e) for e in self.element_strategies) @@ -366,19 +386,35 @@ class FixedDictStrategy(SearchStrategy[dict[Any, Any]]): self.optional = optional def do_draw(self, data: ConjectureData) -> dict[Any, Any]: - value = data.draw(self.fixed) - if self.optional is None: - return value + context = current_build_context() + arg_labels: ArgLabelsT = {} + value = type(self.mapping)() - 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() - value[key] = data.draw(self.optional[key]) + for key, strategy in self.mapping.items(): + with context.track_arg_label(str(key)) as arg_label: + value[key] = data.draw(strategy) + arg_labels |= arg_label + + if self.optional is not None: + 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() + with context.track_arg_label(str(key)) as arg_label: + value[key] = data.draw(self.optional[key]) + arg_labels |= arg_label + + if arg_labels: + context.known_object_printers[IDKey(value)].append( + _fixeddict_pprinter(arg_labels, self.mapping) + ) return value def calc_is_empty(self, recur: RecurT) -> bool: diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 5aaf0fe497e..ac48b450cd3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -45,7 +45,6 @@ from typing import ( ) from uuid import UUID -from hypothesis._settings import note_deprecation from hypothesis.control import ( cleanup, current_build_context, @@ -138,7 +137,8 @@ from hypothesis.strategies._internal.strings import ( ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.conventions import not_set -from hypothesis.vendor.pretty import RepresentationPrinter +from hypothesis.utils.deprecation import note_deprecation +from hypothesis.vendor.pretty import ArgLabelsT, RepresentationPrinter @cacheable @@ -1053,8 +1053,20 @@ class BuildsStrategy(SearchStrategy[Ex]): ) def do_draw(self, data: ConjectureData) -> Ex: - args = [data.draw(s) for s in self.args] - kwargs = {k: data.draw(v) for k, v in self.kwargs.items()} + context = current_build_context() + arg_labels: ArgLabelsT = {} + + args = [] + for i, s in enumerate(self.args): + with context.track_arg_label(f"arg[{i}]") as arg_label: + args.append(data.draw(s)) + arg_labels |= arg_label + + kwargs = {} + for k, v in self.kwargs.items(): + with context.track_arg_label(k) as arg_label: + kwargs[k] = data.draw(v) + arg_labels |= arg_label try: obj = self.target(*args, **kwargs) except TypeError as err: @@ -1086,7 +1098,9 @@ class BuildsStrategy(SearchStrategy[Ex]): ) from err raise - current_build_context().record_call(obj, self.target, args=args, kwargs=kwargs) + context.record_call( + obj, self.target, args=args, kwargs=kwargs, arg_labels=arg_labels + ) return obj def do_validate(self) -> None: diff --git a/contrib/python/hypothesis/py3/hypothesis/utils/deprecation.py b/contrib/python/hypothesis/py3/hypothesis/utils/deprecation.py new file mode 100644 index 00000000000..e27e2cf9c1e --- /dev/null +++ b/contrib/python/hypothesis/py3/hypothesis/utils/deprecation.py @@ -0,0 +1,28 @@ +# 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 +import warnings + +from hypothesis.errors import HypothesisDeprecationWarning + + +def note_deprecation( + message: str, *, since: str, has_codemod: bool, stacklevel: int = 0 +) -> None: + if since != "RELEASEDAY": + date = datetime.date.fromisoformat(since) + assert datetime.date(2021, 1, 1) <= date + if has_codemod: + message += ( + "\n The `hypothesis codemod` command-line tool can automatically " + "refactor your code to fix this warning." + ) + warnings.warn(HypothesisDeprecationWarning(message), stacklevel=2 + stacklevel) diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py index af790bbab2c..41315694d1e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py @@ -83,10 +83,13 @@ if TYPE_CHECKING: T = TypeVar("T") PrettyPrintFunction: TypeAlias = Callable[[Any, "RepresentationPrinter", bool], None] +ArgLabelsT: TypeAlias = dict[str, tuple[int, int]] __all__ = [ "IDKey", "RepresentationPrinter", + "_fixeddict_pprinter", + "_tuple_pprinter", "pretty", ] @@ -186,6 +189,9 @@ class RepresentationPrinter: self.known_object_printers = context.known_object_printers self.slice_comments = context.data.slice_comments assert all(isinstance(k, IDKey) for k in self.known_object_printers) + # Track which slices we've already printed comments for, to avoid + # duplicating comments when nested objects share the same slice range. + self._commented_slices: set[tuple[int, int]] = set() def pretty(self, obj: object, *, cycle: bool = False) -> None: """Pretty print the given object.""" @@ -212,6 +218,25 @@ class RepresentationPrinter: if callable(pretty_method): return pretty_method(self, cycle) + # Check for object-specific printers which show how this + # object was constructed (a Hypothesis special feature). + # This must come before type_pprinters so that sub-argument + # comments are shown for tuples/dicts/etc. + printers = self.known_object_printers[IDKey(obj)] + if len(printers) == 1: + return printers[0](obj, self, cycle) + if printers: + # Multiple registered functions for the same object (due to + # caching, small ints, etc). Use the first if all produce + # the same string; otherwise pretend none were registered. + strs = set() + for f in printers: + p = RepresentationPrinter() + f(obj, p, cycle) + strs.add(p.getvalue()) + if len(strs) == 1: + return printers[0](obj, self, cycle) + # Next walk the mro and check for either: # 1) a registered printer # 2) a _repr_pretty_ method @@ -250,26 +275,6 @@ class RepresentationPrinter: if v.init ], ) - # Now check for object-specific printers which show how this - # object was constructed (a Hypothesis special feature). - printers = self.known_object_printers[IDKey(obj)] - if len(printers) == 1: - return printers[0](obj, self, cycle) - elif printers: - # We've ended up with multiple registered functions for the same - # object, which must have been returned from multiple calls due to - # e.g. memoization. If they all return the same string, we'll use - # the first; otherwise we'll pretend that *none* were registered. - # - # It's annoying, but still seems to be the best option for which- - # parts-matter too, as unreportable results aren't very useful. - strs = set() - for f in printers: - p = RepresentationPrinter() - f(obj, p, cycle) - strs.add(p.getvalue()) - if len(strs) == 1: - return printers[0](obj, self, cycle) # A user-provided repr. Find newlines and replace them with p.break_() return _repr_pprint(obj, self, cycle) @@ -411,28 +416,34 @@ class RepresentationPrinter: name: str, args: Sequence[object], kwargs: dict[str, object], + arg_labels: ArgLabelsT | None = None, ) -> None: # pprint this object as a call, _unless_ the call would be invalid syntax # and the repr would be valid and there are not comments on arguments. if cycle: return self.text("<...>") - # Since we don't yet track comments for sub-argument parts, we omit the - # "if no comments" condition here for now. Add it when we revive - # https://github.com/HypothesisWorks/hypothesis/pull/3624/ - with suppress(Exception): - # Check whether the repr is valid syntax: - ast.parse(repr(obj)) - # Given that the repr is valid syntax, check the call: - p = RepresentationPrinter() - p.stack = self.stack.copy() - p.known_object_printers = self.known_object_printers - p.repr_call(name, args, kwargs) - # If the call is not valid syntax, use the repr - try: - ast.parse(p.getvalue()) - except Exception: - return _repr_pprint(obj, self, cycle) - return self.repr_call(name, args, kwargs) + # Look up comments from slice_comments if we have arg_labels + comments = {} + if arg_labels: + for key, sr in arg_labels.items(): + if sr in self.slice_comments: + comments[key] = self.slice_comments[sr] + # If there are comments, we must use our call-style repr regardless of syntax + if not comments: + with suppress(Exception): + # Check whether the repr is valid syntax: + ast.parse(repr(obj)) + # Given that the repr is valid syntax, check the call: + p = RepresentationPrinter() + p.stack = self.stack.copy() + p.known_object_printers = self.known_object_printers + p.repr_call(name, args, kwargs) + # If the call is not valid syntax, use the repr + try: + ast.parse(p.getvalue()) + except Exception: + return _repr_pprint(obj, self, cycle) + return self.repr_call(name, args, kwargs, arg_slices=arg_labels) def repr_call( self, @@ -441,7 +452,7 @@ class RepresentationPrinter: kwargs: dict[str, object], *, force_split: bool | None = None, - arg_slices: dict[str, tuple[int, int]] | None = None, + arg_slices: ArgLabelsT | None = None, leading_comment: str | None = None, avoid_realization: bool = False, ) -> None: @@ -457,14 +468,15 @@ class RepresentationPrinter: if func_name.startswith(("lambda:", "lambda ")): func_name = f"({func_name})" self.text(func_name) - all_args = [(None, v) for v in args] + list(kwargs.items()) - # int indicates the position of a positional argument, rather than a keyword - # argument. Currently no callers use this; see #3624. - comments: dict[int | str, object] = { - k: self.slice_comments[v] - for k, v in (arg_slices or {}).items() - if v in self.slice_comments - } + # Build list of (label, value) pairs. Labels are "arg[i]" for positional + # args, or the keyword name. Skip slices already commented at a higher level. + all_args = [(f"arg[{i}]", v) for i, v in enumerate(args)] + all_args += list(kwargs.items()) + arg_slices = arg_slices or {} + comments: dict[str, tuple[str, tuple[int, int]]] = {} + for label, sr in arg_slices.items(): + if sr in self.slice_comments and sr not in self._commented_slices: + comments[label] = (self.slice_comments[sr], sr) if leading_comment or any(k in comments for k, _ in all_args): # We have to split one arg per line in order to leave comments on them. @@ -480,7 +492,7 @@ class RepresentationPrinter: force_split = "\n" in s with self.group(indent=4, open="(", close=""): - for i, (k, v) in enumerate(all_args): + for i, (label, v) in enumerate(all_args): if force_split: if i == 0 and leading_comment: self.break_() @@ -489,19 +501,20 @@ class RepresentationPrinter: else: assert leading_comment is None # only passed by top-level report self.breakable(" " if i else "") - if k: - self.text(f"{k}=") + if not label.startswith("arg["): + self.text(f"{label}=") + # Mark slice as commented BEFORE printing value, so nested printers skip it + entry = comments.get(label) + if entry: + self._commented_slices.add(entry[1]) if avoid_realization: self.text("<symbolic>") else: self.pretty(v) if force_split or i + 1 < len(all_args): self.text(",") - comment = None - if k is not None: - comment = comments.get(i) or comments.get(k) - if comment: - self.text(f" # {comment}") + if entry: + self.text(f" # {entry[0]}") if all_args and force_split: self.break_() self.text(")") # after dedent @@ -801,6 +814,77 @@ def pprint_fields( p.pretty(getattr(obj, field)) +def _get_slice_comment( + p: RepresentationPrinter, + arg_labels: ArgLabelsT, + key: Any, +) -> tuple[str, tuple[int, int]] | None: + """Look up a comment for a slice, if not already printed at a higher level.""" + if (sr := arg_labels.get(key)) and sr in p.slice_comments: + if sr not in p._commented_slices: + return (p.slice_comments[sr], sr) + return None + + +def _tuple_pprinter(arg_labels: ArgLabelsT) -> PrettyPrintFunction: + """Pretty printer for tuples that shows sub-argument comments.""" + + def inner(obj: tuple, p: RepresentationPrinter, cycle: bool) -> None: + if cycle: + return p.text("(...)") + + get = lambda i: _get_slice_comment(p, arg_labels, f"arg[{i}]") + has_comments = any(get(i) for i in range(len(obj))) + + with p.group(indent=4, open="(", close=""): + for idx, x in p._enumerate(obj): + p.break_() if has_comments else (p.breakable() if idx else None) + p.pretty(x) + if has_comments or idx + 1 < len(obj) or len(obj) == 1: + p.text(",") + if entry := get(idx): + p._commented_slices.add(entry[1]) + p.text(f" # {entry[0]}") + if has_comments and obj: + p.break_() + p.text(")") + + return inner + + +def _fixeddict_pprinter( + arg_labels: ArgLabelsT, + mapping: dict[Any, Any], +) -> PrettyPrintFunction: + """Pretty printer for fixed_dictionaries that shows sub-argument comments.""" + + def inner(obj: dict, p: RepresentationPrinter, cycle: bool) -> None: + if cycle: + 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)) + has_comments = any(get(k) for k in keys) + + with p.group(indent=4, open="{", close=""): + for idx, key in p._enumerate(keys): + p.break_() if has_comments else (p.breakable() if idx else None) + p.pretty(key) + p.text(": ") + p.pretty(obj[key]) + if has_comments or idx + 1 < len(keys): + p.text(",") + if entry := get(key): + p._commented_slices.add(entry[1]) + p.text(f" # {entry[0]}") + if has_comments and obj: + p.break_() + p.text("}") + + return inner + + def _function_pprint( obj: types.FunctionType | types.BuiltinFunctionType | types.MethodType, p: RepresentationPrinter, diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index 1b8e5a60276..41812a39d47 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, 148, 12) +__version_info__ = (6, 149, 1) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index 9e7117c2ebf..89ea7a2df29 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.148.12) +VERSION(6.149.1) LICENSE(MPL-2.0) @@ -119,6 +119,7 @@ PY_SRCS( hypothesis/strategies/_internal/utils.py hypothesis/utils/__init__.py hypothesis/utils/conventions.py + hypothesis/utils/deprecation.py hypothesis/utils/dynamicvariables.py hypothesis/utils/terminal.py hypothesis/utils/threading.py |
