diff options
author | Alexander Smirnov <alex@ydb.tech> | 2024-01-31 17:22:33 +0300 |
---|---|---|
committer | Alexander Smirnov <alex@ydb.tech> | 2024-01-31 17:22:33 +0300 |
commit | 52be5dbdd420165c68e7e90ba8f1d2f00da041f6 (patch) | |
tree | 5d47f5b2ff4e6a7c8e75d33931a1e683949b7229 /contrib/python | |
parent | ea57c8867ceca391357c3c5ffcc5ba6738b49adc (diff) | |
parent | 809f0cf2fdfddfbeacc2256ffdbaaf5808ce5ed4 (diff) | |
download | ydb-52be5dbdd420165c68e7e90ba8f1d2f00da041f6.tar.gz |
Merge branch 'mergelibs12' into main
Diffstat (limited to 'contrib/python')
23 files changed, 343 insertions, 110 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index b4f00cf430..c94200ebd3 100644 --- a/contrib/python/hypothesis/py3/.dist-info/METADATA +++ b/contrib/python/hypothesis/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: hypothesis -Version: 6.92.8 +Version: 6.94.0 Summary: A library for property-based testing Home-page: https://hypothesis.works Author: David R. MacIver and Zac Hatfield-Dodds diff --git a/contrib/python/hypothesis/py3/.dist-info/top_level.txt b/contrib/python/hypothesis/py3/.dist-info/top_level.txt index 93b8370b8d..77a969c858 100644 --- a/contrib/python/hypothesis/py3/.dist-info/top_level.txt +++ b/contrib/python/hypothesis/py3/.dist-info/top_level.txt @@ -1,3 +1,4 @@ _hypothesis_ftz_detector +_hypothesis_globals _hypothesis_pytestplugin hypothesis diff --git a/contrib/python/hypothesis/py3/_hypothesis_globals.py b/contrib/python/hypothesis/py3/_hypothesis_globals.py new file mode 100644 index 0000000000..e97e091879 --- /dev/null +++ b/contrib/python/hypothesis/py3/_hypothesis_globals.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/. + +""" +Module for globals shared between plugin(s) and the main hypothesis module, without +depending on either. This file should have no imports outside of stdlib. +""" + +import os + +in_initialization = 1 +"""If nonzero, indicates that hypothesis is still initializing (importing or loading +the test environment). `import hypothesis` will cause this number to be decremented, +and the pytest plugin increments at load time, then decrements it just before the test +session starts. However, this leads to a hole in coverage if another pytest plugin +imports hypothesis before our plugin is loaded. HYPOTHESIS_EXTEND_INITIALIZATION may +be set to pre-increment the value on behalf of _hypothesis_pytestplugin, plugging the +hole.""" + +if os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"): + in_initialization += 1 diff --git a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py index 9875e067f5..944304ccdb 100644 --- a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py +++ b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py @@ -21,9 +21,12 @@ See https://github.com/HypothesisWorks/hypothesis/issues/3140 for details. import base64 import json +import os import sys +import warnings from inspect import signature +import _hypothesis_globals import pytest try: @@ -94,6 +97,19 @@ if tuple(map(int, pytest.__version__.split(".")[:2])) < (4, 6): # pragma: no co warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,), stacklevel=1) else: + # Restart side-effect detection as early as possible, to maximize coverage. We + # need balanced increment/decrement in configure/sessionstart to support nested + # pytest (e.g. runpytest_inprocess), so this early increment in effect replaces + # the first one in pytest_configure. + _configured = False + if not os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"): + _hypothesis_globals.in_initialization += 1 + if "hypothesis" in sys.modules: + # Some other plugin has imported hypothesis, so we'll check if there + # have been undetected side-effects and warn if so. + from hypothesis.configuration import notice_initialization_restarted + + notice_initialization_restarted() def pytest_addoption(parser): group = parser.getgroup("hypothesis", "Hypothesis") @@ -147,6 +163,12 @@ else: return f"hypothesis profile {settings._current_profile!r}{settings_str}" def pytest_configure(config): + global _configured + # skip first increment because we pre-incremented at import time + if _configured: + _hypothesis_globals.in_initialization += 1 + _configured = True + config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.") if not _any_hypothesis_option(config): return @@ -407,6 +429,9 @@ else: if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj): item.add_marker("hypothesis") + def pytest_sessionstart(session): + _hypothesis_globals.in_initialization -= 1 + # Monkeypatch some internals to prevent applying @pytest.fixture() to a # function which has already been decorated with @hypothesis.given(). # (the reverse case is already an explicit error in Hypothesis) diff --git a/contrib/python/hypothesis/py3/hypothesis/__init__.py b/contrib/python/hypothesis/py3/hypothesis/__init__.py index db140b8165..cfb55119f7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/__init__.py +++ b/contrib/python/hypothesis/py3/hypothesis/__init__.py @@ -15,6 +15,8 @@ It verifies your code against a wide range of input and minimizes any failing examples it finds. """ +import _hypothesis_globals + from hypothesis._settings import HealthCheck, Phase, Verbosity, settings from hypothesis.control import ( assume, @@ -54,3 +56,6 @@ __all__ = [ run() del run + +_hypothesis_globals.in_initialization -= 1 +del _hypothesis_globals diff --git a/contrib/python/hypothesis/py3/hypothesis/configuration.py b/contrib/python/hypothesis/py3/hypothesis/configuration.py index 6e6ab29516..2586c729f7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/configuration.py +++ b/contrib/python/hypothesis/py3/hypothesis/configuration.py @@ -9,8 +9,13 @@ # obtain one at https://mozilla.org/MPL/2.0/. import os +import warnings from pathlib import Path +import _hypothesis_globals + +from hypothesis.errors import HypothesisSideeffectWarning + __hypothesis_home_directory_default = Path.cwd() / ".hypothesis" __hypothesis_home_directory = None @@ -21,7 +26,12 @@ def set_hypothesis_home_dir(directory): __hypothesis_home_directory = None if directory is None else Path(directory) -def storage_directory(*names): +def storage_directory(*names, intent_to_write=True): + if intent_to_write: + check_sideeffect_during_initialization( + "accessing storage for {}", "/".join(names) + ) + global __hypothesis_home_directory if not __hypothesis_home_directory: if where := os.getenv("HYPOTHESIS_STORAGE_DIRECTORY"): @@ -29,3 +39,53 @@ def storage_directory(*names): if not __hypothesis_home_directory: __hypothesis_home_directory = __hypothesis_home_directory_default return __hypothesis_home_directory.joinpath(*names) + + +_first_postinit_what = None + + +def check_sideeffect_during_initialization( + what: str, *fmt_args: object, extra: str = "" +) -> None: + """Called from locations that should not be executed during initialization, for example + touching disk or materializing lazy/deferred strategies from plugins. If initialization + is in progress, a warning is emitted. + + Note that computing the repr can take nontrivial time or memory, so we avoid doing so + unless (and until) we're actually emitting the warning. + """ + global _first_postinit_what + # This is not a particularly hot path, but neither is it doing productive work, so we want to + # minimize the cost by returning immediately. The drawback is that we require + # notice_initialization_restarted() to be called if in_initialization changes away from zero. + if _first_postinit_what is not None: + return + elif _hypothesis_globals.in_initialization: + # Note: -Werror is insufficient under pytest, as doesn't take effect until + # test session start. + msg = what.format(*fmt_args) + warnings.warn( + f"Slow code in plugin: avoid {msg} at import time! Set PYTHONWARNINGS=error " + "to get a traceback and show which plugin is responsible." + extra, + HypothesisSideeffectWarning, + stacklevel=3, + ) + else: + _first_postinit_what = (what, fmt_args) + + +def notice_initialization_restarted(*, warn: bool = True) -> None: + """Reset _first_postinit_what, so that we don't think we're in post-init. Additionally, if it + was set that means that there has been a sideeffect that we haven't warned about, so do that + now (the warning text will be correct, and we also hint that the stacktrace can be improved). + """ + global _first_postinit_what + if _first_postinit_what is not None: + what, *fmt_args = _first_postinit_what + _first_postinit_what = None + if warn: + check_sideeffect_during_initialization( + what, + *fmt_args, + extra=" Additionally, set HYPOTHESIS_EXTEND_INITIALIZATION=1 to pinpoint the exact location.", + ) diff --git a/contrib/python/hypothesis/py3/hypothesis/control.py b/contrib/python/hypothesis/py3/hypothesis/control.py index 3a973f666f..5b662197a7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/control.py +++ b/contrib/python/hypothesis/py3/hypothesis/control.py @@ -105,16 +105,13 @@ class BuildContext: ) ) - def prep_args_kwargs_from_strategies(self, arg_strategies, kwarg_strategies): + def prep_args_kwargs_from_strategies(self, kwarg_strategies): arg_labels = {} - all_s = [(None, s) for s in arg_strategies] + list(kwarg_strategies.items()) - args = [] kwargs = {} - for i, (k, s) in enumerate(all_s): + for k, s in kwarg_strategies.items(): start_idx = self.data.index - obj = self.data.draw(s) + obj = self.data.draw(s, observe_as=f"generate:{k}") end_idx = self.data.index - assert k is not None kwargs[k] = obj # This high up the stack, we can't see or really do much with the conjecture @@ -124,10 +121,10 @@ class BuildContext: # 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 or i] = (start_idx, end_idx) + arg_labels[k] = (start_idx, end_idx) self.data.arg_slices.add((start_idx, end_idx)) - return args, kwargs, arg_labels + return kwargs, arg_labels def __enter__(self): self.assign_variable = _current_build_context.with_value(self) diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py index 86b20ea6f9..75f1cc70e9 100644 --- a/contrib/python/hypothesis/py3/hypothesis/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/core.py @@ -15,6 +15,7 @@ import contextlib import datetime import inspect import io +import math import sys import time import types @@ -605,6 +606,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s data=empty_data, how_generated="explicit example", string_repr=state._string_repr, + timing=state._timing_features, ) deliver_json_blob(tc) @@ -816,34 +818,30 @@ class StateForActualGivenExecution: self._string_repr = "" text_repr = None - if self.settings.deadline is None: + if self.settings.deadline is None and not TESTCASE_CALLBACKS: test = self.test else: @proxies(self.test) def test(*args, **kwargs): - arg_drawtime = sum(data.draw_times) - initial_draws = len(data.draw_times) + arg_drawtime = math.fsum(data.draw_times.values()) start = time.perf_counter() try: result = self.test(*args, **kwargs) finally: finish = time.perf_counter() - internal_draw_time = sum(data.draw_times[initial_draws:]) - runtime = datetime.timedelta( - seconds=finish - start - internal_draw_time - ) + in_drawtime = math.fsum(data.draw_times.values()) - arg_drawtime + runtime = datetime.timedelta(seconds=finish - start - in_drawtime) self._timing_features = { - "time_running_test": finish - start - internal_draw_time, - "time_drawing_args": arg_drawtime, - "time_interactive_draws": internal_draw_time, + "execute_test": finish - start - in_drawtime, + **data.draw_times, } - current_deadline = self.settings.deadline - if not is_final: - current_deadline = (current_deadline // 4) * 5 - if runtime >= current_deadline: - raise DeadlineExceeded(runtime, self.settings.deadline) + if (current_deadline := self.settings.deadline) is not None: + if not is_final: + current_deadline = (current_deadline // 4) * 5 + if runtime >= current_deadline: + raise DeadlineExceeded(runtime, self.settings.deadline) return result def run(data): @@ -854,10 +852,9 @@ class StateForActualGivenExecution: args = self.stuff.args kwargs = dict(self.stuff.kwargs) if example_kwargs is None: - a, kw, argslices = context.prep_args_kwargs_from_strategies( - (), self.stuff.given_kwargs + kw, argslices = context.prep_args_kwargs_from_strategies( + self.stuff.given_kwargs ) - assert not a, "strategies all moved to kwargs by now" else: kw = example_kwargs argslices = {} @@ -944,7 +941,7 @@ class StateForActualGivenExecution: if expected_failure is not None: exception, traceback = expected_failure if isinstance(exception, DeadlineExceeded) and ( - runtime_secs := self._timing_features.get("time_running_test") + runtime_secs := self._timing_features.get("execute_test") ): report( "Unreliable test timings! On an initial run, this " @@ -1066,11 +1063,12 @@ class StateForActualGivenExecution: how_generated=f"generated during {phase} phase", string_repr=self._string_repr, arguments={**self._jsonable_arguments, **data._observability_args}, - metadata=self._timing_features, + timing=self._timing_features, + metadata={}, coverage=tractable_coverage_report(trace) or None, ) deliver_json_blob(tc) - self._timing_features.clear() + self._timing_features = {} def run_engine(self): """Run the test function many times, on database input and generated @@ -1184,17 +1182,17 @@ class StateForActualGivenExecution: "status": "passed" if sys.exc_info()[0] else "failed", "status_reason": str(origin or "unexpected/flaky pass"), "representation": self._string_repr, + "arguments": self._jsonable_arguments, "how_generated": "minimal failing example", "features": { **{ - k: v + f"target:{k}".strip(":"): v for k, v in ran_example.target_observations.items() - if isinstance(k, str) }, **ran_example.events, - **self._timing_features, }, - "coverage": None, # TODO: expose this? + "timing": self._timing_features, + "coverage": None, # Not recorded when we're replaying the MFE "metadata": {"traceback": tb}, } deliver_json_blob(tc) diff --git a/contrib/python/hypothesis/py3/hypothesis/database.py b/contrib/python/hypothesis/py3/hypothesis/database.py index 4abce10c01..9fbdd31597 100644 --- a/contrib/python/hypothesis/py3/hypothesis/database.py +++ b/contrib/python/hypothesis/py3/hypothesis/database.py @@ -59,7 +59,7 @@ def _db_for_path(path=None): "https://hypothesis.readthedocs.io/en/latest/settings.html#settings-profiles" ) - path = storage_directory("examples") + path = storage_directory("examples", intent_to_write=False) if not _usable_dir(path): # pragma: no cover warnings.warn( "The database setting is not configured, and the default " @@ -495,6 +495,8 @@ class GitHubArtifactDatabase(ExampleDatabase): self._initialized = True def _initialize_db(self) -> None: + # Trigger warning that we suppressed earlier by intent_to_write=False + storage_directory(self.path.name) # Create the cache directory if it doesn't exist self.path.mkdir(exist_ok=True, parents=True) diff --git a/contrib/python/hypothesis/py3/hypothesis/errors.py b/contrib/python/hypothesis/py3/hypothesis/errors.py index 8387a87586..0d376a7493 100644 --- a/contrib/python/hypothesis/py3/hypothesis/errors.py +++ b/contrib/python/hypothesis/py3/hypothesis/errors.py @@ -117,6 +117,13 @@ class HypothesisDeprecationWarning(HypothesisWarning, FutureWarning): """ +class HypothesisSideeffectWarning(HypothesisWarning): + """A warning issued by Hypothesis when it sees actions that are + discouraged at import or initialization time because they are + slow or have user-visible side effects. + """ + + class Frozen(HypothesisException): """Raised when a mutation method has been called on a ConjectureData object after freeze() has been called.""" diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py index 57b68ec9f7..37b5f5d401 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py @@ -34,6 +34,7 @@ from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.validation import check_type +from hypothesis.strategies._internal.regex import IncompatibleWithAlphabet from hypothesis.strategies._internal.utils import cacheable, defines_strategy __all__ = ["from_lark"] @@ -59,7 +60,7 @@ class LarkStrategy(st.SearchStrategy): See ``from_lark`` for details. """ - def __init__(self, grammar, start, explicit): + def __init__(self, grammar, start, explicit, alphabet): assert isinstance(grammar, lark.lark.Lark) if start is None: start = grammar.options.start @@ -86,38 +87,66 @@ class LarkStrategy(st.SearchStrategy): t = r.origin self.names_to_symbols[t.name] = t + disallowed = set() + self.terminal_strategies = {} for t in terminals: self.names_to_symbols[t.name] = Terminal(t.name) - - self.start = st.sampled_from([self.names_to_symbols[s] for s in start]) + s = st.from_regex(t.pattern.to_regexp(), fullmatch=True, alphabet=alphabet) + try: + s.validate() + except IncompatibleWithAlphabet: + disallowed.add(t.name) + else: + self.terminal_strategies[t.name] = s self.ignored_symbols = tuple(self.names_to_symbols[n] for n in ignore_names) - self.terminal_strategies = { - t.name: st.from_regex(t.pattern.to_regexp(), fullmatch=True) - for t in terminals - } - unknown_explicit = set(explicit) - get_terminal_names( - terminals, rules, ignore_names - ) - if unknown_explicit: + all_terminals = get_terminal_names(terminals, rules, ignore_names) + if unknown_explicit := sorted(set(explicit) - all_terminals): + raise InvalidArgument( + "The following arguments were passed as explicit_strategies, but " + f"there is no {unknown_explicit} terminal production in this grammar." + ) + if missing_declared := sorted( + all_terminals - {t.name for t in terminals} - set(explicit) + ): raise InvalidArgument( - "The following arguments were passed as explicit_strategies, " - "but there is no such terminal production in this grammar: " - + repr(sorted(unknown_explicit)) + f"Undefined terminal{'s' * (len(missing_declared) > 1)} " + f"{sorted(missing_declared)!r}. Generation does not currently " + "support use of %declare unless you pass `explicit`, a dict of " + f"names-to-strategies, such as `{{{missing_declared[0]!r}: " + 'st.just("")}}`' ) self.terminal_strategies.update(explicit) nonterminals = {} for rule in rules: - nonterminals.setdefault(rule.origin.name, []).append(tuple(rule.expansion)) - - for v in nonterminals.values(): - v.sort(key=len) + if disallowed.isdisjoint(r.name for r in rule.expansion): + nonterminals.setdefault(rule.origin.name, []).append( + tuple(rule.expansion) + ) + + allowed_rules = {*self.terminal_strategies, *nonterminals} + while dict(nonterminals) != ( + nonterminals := { + k: clean + for k, v in nonterminals.items() + if (clean := [x for x in v if all(r.name in allowed_rules for r in x)]) + } + ): + allowed_rules = {*self.terminal_strategies, *nonterminals} + + if set(start).isdisjoint(allowed_rules): + raise InvalidArgument( + f"No start rule {tuple(start)} is allowed by {alphabet=}" + ) + self.start = st.sampled_from( + [self.names_to_symbols[s] for s in start if s in allowed_rules] + ) self.nonterminal_strategies = { - k: st.sampled_from(v) for k, v in nonterminals.items() + k: st.sampled_from(sorted(v, key=len)) for k, v in nonterminals.items() } self.__rule_labels = {} @@ -138,15 +167,7 @@ class LarkStrategy(st.SearchStrategy): def draw_symbol(self, data, symbol, draw_state): if isinstance(symbol, Terminal): - try: - strategy = self.terminal_strategies[symbol.name] - except KeyError: - raise InvalidArgument( - "Undefined terminal %r. Generation does not currently support " - "use of %%declare unless you pass `explicit`, a dict of " - 'names-to-strategies, such as `{%r: st.just("")}`' - % (symbol.name, symbol.name) - ) from None + strategy = self.terminal_strategies[symbol.name] draw_state.append(data.draw(strategy)) else: assert isinstance(symbol, NonTerminal) @@ -181,6 +202,7 @@ def from_lark( *, start: Optional[str] = None, explicit: Optional[Dict[str, st.SearchStrategy[str]]] = None, + alphabet: st.SearchStrategy[str] = st.characters(codec="utf-8"), ) -> st.SearchStrategy[str]: """A strategy for strings accepted by the given context-free grammar. @@ -214,4 +236,4 @@ def from_lark( k: v.map(check_explicit(f"explicit[{k!r}]={v!r}")) for k, v in explicit.items() } - return LarkStrategy(grammar, start, explicit) + return LarkStrategy(grammar, start, explicit, alphabet) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py index 7a5542b0bd..03f489fa50 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py @@ -30,6 +30,7 @@ from typing import ( Set, Tuple, Type, + TypeVar, Union, ) @@ -88,6 +89,8 @@ InterestingOrigin = Tuple[ ] TargetObservations = Dict[Optional[str], Union[int, float]] +T = TypeVar("T") + class ExtraInformation: """A class for holding shared state on a ``ConjectureData`` that should @@ -1426,7 +1429,7 @@ class ConjectureData: self.events: Dict[str, Union[str, int, float]] = {} self.forced_indices: "Set[int]" = set() self.interesting_origin: Optional[InterestingOrigin] = None - self.draw_times: "List[float]" = [] + self.draw_times: "Dict[str, float]" = {} self.max_depth = 0 self.has_discards = False self.provider = PrimitiveProvider(self) @@ -1550,6 +1553,8 @@ class ConjectureData: def draw_bytes(self, size: int, *, forced: Optional[bytes] = None) -> bytes: assert forced is None or len(forced) == size + assert size >= 0 + return self.provider.draw_bytes(size, forced=forced) def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool: @@ -1594,7 +1599,12 @@ class ConjectureData: value = repr(value) self.output += value - def draw(self, strategy: "SearchStrategy[Ex]", label: Optional[int] = None) -> "Ex": + def draw( + self, + strategy: "SearchStrategy[Ex]", + label: Optional[int] = None, + observe_as: Optional[str] = None, + ) -> "Ex": if self.is_find and not strategy.supports_find: raise InvalidArgument( f"Cannot use strategy {strategy!r} within a call to find " @@ -1631,7 +1641,8 @@ class ConjectureData: try: return strategy.do_draw(self) finally: - self.draw_times.append(time.perf_counter() - start_time) + key = observe_as or f"unlabeled_{len(self.draw_times)}" + self.draw_times[key] = time.perf_counter() - start_time finally: self.stop_example() @@ -1719,6 +1730,11 @@ class ConjectureData: self.buffer = bytes(self.buffer) self.observer.conclude_test(self.status, self.interesting_origin) + def choice(self, values: Sequence[T], *, forced: Optional[T] = None) -> T: + forced_i = None if forced is None else values.index(forced) + i = self.draw_integer(0, len(values) - 1, forced=forced_i) + return values[i] + def draw_bits(self, n: int, *, forced: Optional[int] = None) -> int: """Return an ``n``-bit integer from the underlying source of bytes. If ``forced`` is set to an integer will instead @@ -1770,15 +1786,6 @@ class ConjectureData: assert result.bit_length() <= n return result - def write(self, string: bytes) -> Optional[bytes]: - """Write ``string`` to the output buffer.""" - self.__assert_not_frozen("write") - string = bytes(string) - if not string: - return None - self.draw_bits(len(string) * 8, forced=int_from_bytes(string)) - return self.buffer[-len(string) :] - def __check_capacity(self, n: int) -> None: if self.index + n > self.max_length: self.mark_overrun() diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py index c5d33480e2..961774816f 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py @@ -53,7 +53,39 @@ class HealthCheckState: valid_examples: int = attr.ib(default=0) invalid_examples: int = attr.ib(default=0) overrun_examples: int = attr.ib(default=0) - draw_times: list = attr.ib(factory=list) + draw_times: "defaultdict[str, list[float]]" = attr.ib( + factory=lambda: defaultdict(list) + ) + + @property + def total_draw_time(self): + return math.fsum(sum(self.draw_times.values(), start=[])) + + def timing_report(self): + """Return a terminal report describing what was slow.""" + if not self.draw_times: + return "" + width = max(len(k[len("generate:") :].strip(": ")) for k in self.draw_times) + out = [f"\n {'':^{width}} count | fraction | slowest draws (seconds)"] + args_in_order = sorted(self.draw_times.items(), key=lambda kv: -sum(kv[1])) + for i, (argname, times) in enumerate(args_in_order): # pragma: no branch + # If we have very many unique keys, which can happen due to interactive + # draws with computed labels, we'll skip uninformative rows. + if ( + 5 <= i < (len(self.draw_times) - 2) + and math.fsum(times) * 20 < self.total_draw_time + ): + out.append(f" (skipped {len(self.draw_times) - i} rows of fast draws)") + break + # Compute the row to report, omitting times <1ms to focus on slow draws + reprs = [f"{t:>6.3f}," for t in sorted(times)[-5:] if t > 5e-4] + desc = " ".join(([" -- "] * 5 + reprs)[-5:]).rstrip(",") + arg = argname[len("generate:") :].strip(": ") # removeprefix in py3.9 + out.append( + f" {arg:^{width}} | {len(times):>4} | " + f"{math.fsum(times)/self.total_draw_time:>7.0%} | {desc}" + ) + return "\n".join(out) class ExitReason(Enum): @@ -205,7 +237,7 @@ class ConjectureRunner: call_stats = { "status": data.status.name.lower(), "runtime": data.finish_time - data.start_time, - "drawtime": math.fsum(data.draw_times), + "drawtime": math.fsum(data.draw_times.values()), "events": sorted( k if v == "" else f"{k}: {v}" for k, v in data.events.items() ), @@ -328,7 +360,8 @@ class ConjectureRunner: if state is None: return - state.draw_times.extend(data.draw_times) + for k, v in data.draw_times.items(): + state.draw_times[k].append(v) if data.status == Status.VALID: state.valid_examples += 1 @@ -371,7 +404,7 @@ class ConjectureRunner: HealthCheck.filter_too_much, ) - draw_time = sum(state.draw_times) + draw_time = state.total_draw_time # Allow at least the greater of one second or 5x the deadline. If deadline # is None, allow 30s - the user can disable the healthcheck too if desired. @@ -383,7 +416,8 @@ class ConjectureRunner: f"{state.valid_examples} valid examples in {draw_time:.2f} seconds " f"({state.invalid_examples} invalid ones and {state.overrun_examples} " "exceeded maximum size). Try decreasing size of the data you're " - "generating (with e.g. max_size or max_leaves parameters).", + "generating (with e.g. max_size or max_leaves parameters)." + + state.timing_report(), HealthCheck.too_slow, ) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py index 0712b2d8c8..97b913d79e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py @@ -81,14 +81,6 @@ def check_sample( return tuple(values) -def choice( - data: "ConjectureData", values: Sequence[T], *, forced: Optional[T] = None -) -> T: - forced_i = None if forced is None else values.index(forced) - i = data.draw_integer(0, len(values) - 1, forced=forced_i) - return values[i] - - class Sampler: """Sampler based on Vose's algorithm for the alias method. See http://www.keithschwarz.com/darts-dice-coins/ for a good explanation. @@ -182,8 +174,8 @@ class Sampler: if forced is None else next((b, a, a_c) for (b, a, a_c) in self.table if forced in (b, a)) ) - base, alternate, alternate_chance = choice( - data, self.table, forced=forced_choice + base, alternate, alternate_chance = data.choice( + self.table, forced=forced_choice ) use_alternate = data.draw_boolean( alternate_chance, forced=None if forced is None else forced == alternate diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py index 0da4aca764..284a2072d7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py @@ -37,6 +37,7 @@ def make_testcase( how_generated: str = "unknown", string_repr: str = "<unknown>", arguments: Optional[dict] = None, + timing: Dict[str, float], metadata: Optional[dict] = None, coverage: Optional[Dict[str, List[int]]] = None, ) -> dict: @@ -65,6 +66,7 @@ def make_testcase( }, **data.events, }, + "timing": timing, "metadata": { **(metadata or {}), "traceback": getattr(data.extra_information, "_expected_traceback", None), diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index b3a558c306..028d8405c7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -55,6 +55,7 @@ import attr from hypothesis._settings import note_deprecation from hypothesis.control import cleanup, current_build_context, note from hypothesis.errors import ( + HypothesisSideeffectWarning, HypothesisWarning, InvalidArgument, ResolutionFailed, @@ -2100,10 +2101,10 @@ class DataObject: def draw(self, strategy: SearchStrategy[Ex], label: Any = None) -> Ex: check_strategy(strategy, "strategy") - result = self.conjecture_data.draw(strategy) self.count += 1 printer = RepresentationPrinter(context=current_build_context()) desc = f"Draw {self.count}{'' if label is None else f' ({label})'}: " + result = self.conjecture_data.draw(strategy, observe_as=f"generate:{desc}") if TESTCASE_CALLBACKS: self.conjecture_data._observability_args[desc] = to_jsonable(result) @@ -2196,14 +2197,25 @@ def register_type_strategy( f"{custom_type=} is not allowed to be registered, " f"because there is no such thing as a runtime instance of {custom_type!r}" ) - elif not (isinstance(strategy, SearchStrategy) or callable(strategy)): + if not (isinstance(strategy, SearchStrategy) or callable(strategy)): raise InvalidArgument( f"{strategy=} must be a SearchStrategy, or a function that takes " "a generic type and returns a specific SearchStrategy" ) - elif isinstance(strategy, SearchStrategy) and strategy.is_empty: - raise InvalidArgument(f"{strategy=} must not be empty") - elif types.has_type_arguments(custom_type): + if isinstance(strategy, SearchStrategy): + with warnings.catch_warnings(): + warnings.simplefilter("error", HypothesisSideeffectWarning) + + # Calling is_empty forces materialization of lazy strategies. If this is done at import + # time, lazy strategies will warn about it; here, we force that warning to raise to + # avoid the materialization. Ideally, we'd just check if the strategy is lazy, but the + # lazy strategy may be wrapped underneath another strategy so that's complicated. + try: + if strategy.is_empty: + raise InvalidArgument(f"{strategy=} must not be empty") + except HypothesisSideeffectWarning: # pragma: no cover + pass + if types.has_type_arguments(custom_type): raise InvalidArgument( f"Cannot register generic type {custom_type!r}, because it has type " "arguments which would not be handled. Instead, register a function " diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py index 489b4d7b7a..f7dae9a1e5 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py @@ -10,6 +10,7 @@ import inspect +from hypothesis.configuration import check_sideeffect_during_initialization from hypothesis.errors import InvalidArgument from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.strategies import SearchStrategy, check_strategy @@ -27,6 +28,8 @@ class DeferredStrategy(SearchStrategy): @property def wrapped_strategy(self): if self.__wrapped_strategy is None: + check_sideeffect_during_initialization("deferred evaluation of {!r}", self) + if not inspect.isfunction(self.__definition): raise InvalidArgument( f"Expected definition to be a function but got {self.__definition!r} " diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py index 5e493a9099..d6bb13c7c1 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py @@ -12,6 +12,7 @@ from inspect import signature from typing import MutableMapping from weakref import WeakKeyDictionary +from hypothesis.configuration import check_sideeffect_during_initialization from hypothesis.internal.reflection import ( convert_keyword_arguments, convert_positional_arguments, @@ -100,6 +101,8 @@ class LazyStrategy(SearchStrategy): @property def wrapped_strategy(self): if self.__wrapped_strategy is None: + check_sideeffect_during_initialization("lazy evaluation of {!r}", self) + unwrapped_args = tuple(unwrap_strategies(s) for s in self.__args) unwrapped_kwargs = { k: unwrap_strategies(v) for k, v in self.__kwargs.items() diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py index 23c3aedc82..8de137a422 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py @@ -63,6 +63,10 @@ GROUP_CACHE_STRATEGY: st.SearchStrategy[dict] = st.shared( ) +class IncompatibleWithAlphabet(InvalidArgument): + pass + + @st.composite def update_group(draw, group_name, strategy): cache = draw(GROUP_CACHE_STRATEGY) @@ -176,11 +180,8 @@ class CharactersBuilder: else: raise NotImplementedError(f"Unknown character category: {category}") - def add_char(self, char, *, check=True): + def add_char(self, c): """Add given char to the whitelist.""" - c = self.code_to_char(char) - if check and chars_not_in_alphabet(self._alphabet, c): - raise InvalidArgument(f"Literal {c!r} is not in the specified alphabet") self._whitelist_chars.add(c) if ( self._ignorecase @@ -363,7 +364,7 @@ def _strategy(codes, context, is_unicode, *, alphabet): if i + 1 < j: chars = empty.join(to_char(charcode) for _, charcode in codes[i:j]) if invalid := chars_not_in_alphabet(alphabet, chars): - raise InvalidArgument( + raise IncompatibleWithAlphabet( f"Literal {chars!r} contains characters {invalid!r} " f"which are not in the specified alphabet" ) @@ -389,7 +390,9 @@ def _strategy(codes, context, is_unicode, *, alphabet): # Regex 'a' (single char) c = to_char(value) if chars_not_in_alphabet(alphabet, c): - raise InvalidArgument(f"Literal {c!r} is not in the specified alphabet") + raise IncompatibleWithAlphabet( + f"Literal {c!r} is not in the specified alphabet" + ) if ( context.flags & re.IGNORECASE and c != c.swapcase() @@ -451,12 +454,28 @@ def _strategy(codes, context, is_unicode, *, alphabet): pass elif charset_code == sre.LITERAL: # Regex '[a]' (single char) - builder.add_char(charset_value) + c = builder.code_to_char(charset_value) + if chars_not_in_alphabet(builder._alphabet, c): + raise IncompatibleWithAlphabet( + f"Literal {c!r} is not in the specified alphabet" + ) + builder.add_char(c) elif charset_code == sre.RANGE: # Regex '[a-z]' (char range) low, high = charset_value - for char_code in range(low, high + 1): - builder.add_char(char_code, check=char_code in (low, high)) + chars = empty.join(map(builder.code_to_char, range(low, high + 1))) + if len(chars) == len( + invalid := set(chars_not_in_alphabet(alphabet, chars)) + ): + raise IncompatibleWithAlphabet( + f"Charset '[{chr(low)}-{chr(high)}]' contains characters {invalid!r} " + f"which are not in the specified alphabet" + ) + for c in chars: + if isinstance(c, int): + c = int_to_byte(c) + if c not in invalid: + builder.add_char(c) elif charset_code == sre.CATEGORY: # Regex '[\w]' (char category) builder.add_category(charset_value) @@ -515,7 +534,16 @@ def _strategy(codes, context, is_unicode, *, alphabet): elif code == sre.BRANCH: # Regex 'a|b|c' (branch) - return st.one_of([recurse(branch) for branch in value[1]]) + branches = [] + errors = [] + for branch in value[1]: + try: + branches.append(recurse(branch)) + except IncompatibleWithAlphabet as e: + errors.append(str(e)) + if errors and not branches: + raise IncompatibleWithAlphabet("\n".join(errors)) + return st.one_of(branches) elif code in [sre.MIN_REPEAT, sre.MAX_REPEAT, POSSESSIVE_REPEAT]: # Regexes 'a?', 'a*', 'a+' and their non-greedy variants diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py index f2cc0925a5..d2b33f2673 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py @@ -602,7 +602,7 @@ class SampledFromStrategy(SearchStrategy): # The speculative index didn't work out, but at this point we've built # and can choose from the complete list of allowed indices and elements. if allowed: - i, element = cu.choice(data, allowed) + i, element = data.choice(allowed) data.draw_integer(0, len(self.elements) - 1, forced=i) return element # If there are no allowed indices, the filter couldn't be satisfied. diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py index b2a7661cd6..bd56d2287e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py @@ -184,5 +184,11 @@ def to_jsonable(obj: object) -> object: if (pyd := sys.modules.get("pydantic")) and isinstance(obj, pyd.BaseModel): return to_jsonable(obj.model_dump()) + # Hey, might as well try calling a .to_json() method - it works for Pandas! + try: + return to_jsonable(obj.to_json()) # type: ignore + except Exception: + pass + # If all else fails, we'll just pretty-print as a string. return pretty(obj) diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index ef8fe6a63a..fd4613cb61 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, 92, 8) +__version_info__ = (6, 94, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index 92b1d0c734..476e93730e 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.92.8) +VERSION(6.94.0) LICENSE(MPL-2.0) @@ -20,6 +20,7 @@ NO_CHECK_IMPORTS( PY_SRCS( TOP_LEVEL _hypothesis_ftz_detector.py + _hypothesis_globals.py _hypothesis_pytestplugin.py hypothesis/__init__.py hypothesis/_settings.py |