aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorAlexander Smirnov <alex@ydb.tech>2024-01-31 17:22:33 +0300
committerAlexander Smirnov <alex@ydb.tech>2024-01-31 17:22:33 +0300
commit52be5dbdd420165c68e7e90ba8f1d2f00da041f6 (patch)
tree5d47f5b2ff4e6a7c8e75d33931a1e683949b7229 /contrib/python
parentea57c8867ceca391357c3c5ffcc5ba6738b49adc (diff)
parent809f0cf2fdfddfbeacc2256ffdbaaf5808ce5ed4 (diff)
downloadydb-52be5dbdd420165c68e7e90ba8f1d2f00da041f6.tar.gz
Merge branch 'mergelibs12' into main
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/top_level.txt1
-rw-r--r--contrib/python/hypothesis/py3/_hypothesis_globals.py28
-rw-r--r--contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py25
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/__init__.py5
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/configuration.py62
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/control.py13
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/core.py48
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/database.py4
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/errors.py7
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/lark.py80
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py31
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py44
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py12
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/observability.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py22
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py3
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py3
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py48
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/version.py2
-rw-r--r--contrib/python/hypothesis/py3/ya.make3
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