summaryrefslogtreecommitdiffstats
path: root/contrib/python/hypothesis
diff options
context:
space:
mode:
authorrobot-contrib <[email protected]>2025-11-15 20:06:29 +0300
committerrobot-contrib <[email protected]>2025-11-15 20:35:05 +0300
commit9a07a28dc0d44861b027fe30ef3b3a607af319b4 (patch)
treea7659c7794683dbb38d8dde41b7eda2df8d8c23b /contrib/python/hypothesis
parent97cbfc98cf9020034203704aab66ecb6a0f4624d (diff)
Update contrib/python/hypothesis/py3 to 6.136.2
commit_hash:aaf72f1c1b3aa7cf161d27d86c119860b25c0b21
Diffstat (limited to 'contrib/python/hypothesis')
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/METADATA10
-rw-r--r--contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py2
-rw-r--r--contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py16
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/__init__.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/_settings.py1310
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/control.py73
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/core.py567
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/database.py262
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/errors.py39
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py11
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/_patching.py125
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/array_api.py37
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py4
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/django/__init__.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py9
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py15
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py15
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/lark.py1
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/numpy.py21
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py10
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/pytz.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/extra/redis.py8
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/cache.py26
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/charmap.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/compat.py39
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py10
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py216
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py127
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py209
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py191
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py23
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py487
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py978
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py335
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py8
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py273
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/detection.py32
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/entropy.py79
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/escalation.py7
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py7
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/observability.py374
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/reflection.py26
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py38
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/provisional.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/stateful.py346
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py71
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py1
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py111
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py8
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py59
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py13
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py1
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/recursive.py1
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py25
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py205
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py14
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py1
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py15
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/utils/threading.py51
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py5
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/version.py2
-rw-r--r--contrib/python/hypothesis/py3/patches/01-fix-crash-with-pydebug.patch14
-rw-r--r--contrib/python/hypothesis/py3/ya.make4
65 files changed, 4767 insertions, 2222 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA
index 228af8a8c57..de8da2ed2bd 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.130.13
+Version: 6.136.2
Summary: A library for property-based testing
Author-email: "David R. MacIver and Zac Hatfield-Dodds" <[email protected]>
License-Expression: MPL-2.0
@@ -61,8 +61,8 @@ Requires-Dist: dpcontracts>=0.4; extra == "dpcontracts"
Provides-Extra: redis
Requires-Dist: redis>=3.0.0; extra == "redis"
Provides-Extra: crosshair
-Requires-Dist: hypothesis-crosshair>=0.0.20; extra == "crosshair"
-Requires-Dist: crosshair-tool>=0.0.85; extra == "crosshair"
+Requires-Dist: hypothesis-crosshair>=0.0.23; extra == "crosshair"
+Requires-Dist: crosshair-tool>=0.0.88; extra == "crosshair"
Provides-Extra: zoneinfo
Requires-Dist: tzdata>=2025.2; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "zoneinfo"
Provides-Extra: django
@@ -72,10 +72,10 @@ Requires-Dist: watchdog>=4.0.0; extra == "watchdog"
Provides-Extra: all
Requires-Dist: black>=19.10b0; extra == "all"
Requires-Dist: click>=7.0; extra == "all"
-Requires-Dist: crosshair-tool>=0.0.85; extra == "all"
+Requires-Dist: crosshair-tool>=0.0.88; extra == "all"
Requires-Dist: django>=4.2; extra == "all"
Requires-Dist: dpcontracts>=0.4; extra == "all"
-Requires-Dist: hypothesis-crosshair>=0.0.20; extra == "all"
+Requires-Dist: hypothesis-crosshair>=0.0.23; extra == "all"
Requires-Dist: lark>=0.10.1; extra == "all"
Requires-Dist: libcst>=0.3.16; extra == "all"
Requires-Dist: numpy>=1.19.3; extra == "all"
diff --git a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py
index 2ee203429b1..19fa31e735d 100644
--- a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py
+++ b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py
@@ -52,7 +52,7 @@ def run_in_process(fn: Callable[..., FTZCulprits], *args: object) -> FTZCulprits
import multiprocessing as mp
mp.set_start_method("spawn", force=True)
- q: "Queue[FTZCulprits]" = mp.Queue()
+ q: Queue[FTZCulprits] = mp.Queue()
p = mp.Process(target=target, args=(q, fn, *args))
p.start()
retval = q.get()
diff --git a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
index 98c70b08de0..f7a89268452 100644
--- a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
+++ b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
@@ -203,8 +203,7 @@ else:
yield
return
- from hypothesis import core
- from hypothesis.internal.detection import is_hypothesis_test
+ from hypothesis import core, is_hypothesis_test
# See https://github.com/pytest-dev/pytest/issues/9159
core.pytest_shows_exceptiongroups = (
@@ -306,6 +305,9 @@ else:
item.hypothesis_statistics = describe_statistics(stats)
with collector.with_value(note_statistics):
+ # NOTE: For compatibility with Python 3.9's LL(1)
+ # parser, this is written as a nested with-statement,
+ # instead of a compound one.
with with_reporter(store):
with current_pytest_item.with_value(item):
yield
@@ -383,9 +385,9 @@ else:
stats = report.__dict__.get(STATS_KEY)
if stats and print_stats:
terminalreporter.write_line(stats + "\n\n")
- fex = report.__dict__.get(FAILING_EXAMPLES_KEY)
- if fex:
- failing_examples.append(json.loads(fex))
+ examples = report.__dict__.get(FAILING_EXAMPLES_KEY)
+ if examples:
+ failing_examples.append(json.loads(examples))
from hypothesis.internal.observability import _WROTE_TO
@@ -415,7 +417,7 @@ else:
if "hypothesis" not in sys.modules:
return
- from hypothesis.internal.detection import is_hypothesis_test
+ from hypothesis import is_hypothesis_test
for item in items:
if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj):
@@ -433,7 +435,7 @@ else:
def _ban_given_call(self, function):
if "hypothesis" in sys.modules:
- from hypothesis.internal.detection import is_hypothesis_test
+ from hypothesis import is_hypothesis_test
if is_hypothesis_test(function):
raise RuntimeError(
diff --git a/contrib/python/hypothesis/py3/hypothesis/__init__.py b/contrib/python/hypothesis/py3/hypothesis/__init__.py
index 0b62ac8b1dd..0efe43985de 100644
--- a/contrib/python/hypothesis/py3/hypothesis/__init__.py
+++ b/contrib/python/hypothesis/py3/hypothesis/__init__.py
@@ -28,6 +28,7 @@ from hypothesis.control import (
)
from hypothesis.core import example, find, given, reproduce_failure, seed
from hypothesis.entry_points import run
+from hypothesis.internal.detection import is_hypothesis_test
from hypothesis.internal.entropy import register_random
from hypothesis.utils.conventions import infer
from hypothesis.version import __version__, __version_info__
@@ -45,6 +46,7 @@ __all__ = [
"find",
"given",
"infer",
+ "is_hypothesis_test",
"note",
"register_random",
"reject",
diff --git a/contrib/python/hypothesis/py3/hypothesis/_settings.py b/contrib/python/hypothesis/py3/hypothesis/_settings.py
index ba26925c7dd..e361dacfd1a 100644
--- a/contrib/python/hypothesis/py3/hypothesis/_settings.py
+++ b/contrib/python/hypothesis/py3/hypothesis/_settings.py
@@ -24,20 +24,15 @@ from enum import Enum, EnumMeta, IntEnum, unique
from typing import (
TYPE_CHECKING,
Any,
- Callable,
ClassVar,
- NoReturn,
Optional,
TypeVar,
Union,
)
-import attr
-
from hypothesis.errors import (
HypothesisDeprecationWarning,
InvalidArgument,
- InvalidState,
)
from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS
from hypothesis.internal.reflection import get_pretty_function_description
@@ -46,429 +41,94 @@ from hypothesis.utils.conventions import not_set
from hypothesis.utils.dynamicvariables import DynamicVariable
if TYPE_CHECKING:
- from typing import TypeAlias
-
from hypothesis.database import ExampleDatabase
__all__ = ["settings"]
-ValidatorT: "TypeAlias" = Callable[[Any], object]
-all_settings: dict[str, "Setting"] = {}
-
T = TypeVar("T")
+all_settings: list[str] = [
+ "max_examples",
+ "derandomize",
+ "database",
+ "verbosity",
+ "phases",
+ "stateful_step_count",
+ "report_multiple_bugs",
+ "suppress_health_check",
+ "deadline",
+ "print_blob",
+ "backend",
+]
-class settingsProperty:
- def __init__(self, name: str, *, show_default: bool) -> None:
- self.name = name
- self.show_default = show_default
-
- def __get__(self, obj, type=None):
- if obj is None:
- return self
- else:
- try:
- result = obj.__dict__[self.name]
- # This is a gross hack, but it preserves the old behaviour that
- # you can change the storage directory and it will be reflected
- # in the default database.
- if self.name == "database" and result is not_set:
- from hypothesis.database import _db_for_path
-
- result = _db_for_path(not_set)
- assert result is not not_set
- return result
- except KeyError:
- raise AttributeError(self.name) from None
-
- def __set__(self, obj, value):
- obj.__dict__[self.name] = value
-
- def __delete__(self, obj):
- raise AttributeError(f"Cannot delete attribute {self.name}")
-
- @property
- def __doc__(self):
- description = all_settings[self.name].description
- default = (
- repr(getattr(settings.default, self.name))
- if self.show_default
- else "(dynamically calculated)"
- )
- return f"{description}\n\ndefault value: ``{default}``"
-
-
-default_variable = DynamicVariable[Optional["settings"]](None)
-
-
-class settingsMeta(type):
- def __init__(cls, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- @property
- def default(cls) -> Optional["settings"]:
- v = default_variable.value
- if v is not None:
- return v
- if getattr(settings, "_current_profile", None) is not None:
- assert settings._current_profile is not None
- settings.load_profile(settings._current_profile)
- assert default_variable.value is not None
- return default_variable.value
-
- def _assign_default_internal(cls, value: "settings") -> None:
- default_variable.value = value
-
- def __setattr__(cls, name: str, value: object) -> None:
- if name == "default":
- raise AttributeError(
- "Cannot assign to the property settings.default - "
- "consider using settings.load_profile instead."
- )
- elif not (isinstance(value, settingsProperty) or name.startswith("_")):
- raise AttributeError(
- f"Cannot assign hypothesis.settings.{name}={value!r} - the settings "
- "class is immutable. You can change the global default "
- "settings with settings.load_profile, or use @settings(...) "
- "to decorate your test instead."
- )
- super().__setattr__(name, value)
-
-
-class settings(metaclass=settingsMeta):
- """A settings object configures options including verbosity, runtime controls,
- persistence, determinism, and more.
+@unique
+class Verbosity(IntEnum):
+ """Options for the |settings.verbosity| argument to |@settings|."""
- Default values are picked up from the settings.default object and
- changes made there will be picked up in newly created settings.
+ quiet = 0
+ """
+ Hypothesis will not print any output, not even the final falsifying example.
"""
- __definitions_are_locked = False
- _profiles: ClassVar[dict[str, "settings"]] = {}
- __module__ = "hypothesis"
- _current_profile = None
-
- def __getattr__(self, name):
- if name in all_settings:
- return all_settings[name].default
- else:
- raise AttributeError(f"settings has no attribute {name}")
-
- def __init__(
- self,
- parent: Optional["settings"] = None,
- *,
- # This looks pretty strange, but there's good reason: we want Mypy to detect
- # bad calls downstream, but not to freak out about the `= not_set` part even
- # though it's not semantically valid to pass that as an argument value.
- # The intended use is "like **kwargs, but more tractable for tooling".
- max_examples: int = not_set, # type: ignore
- derandomize: bool = not_set, # type: ignore
- database: Optional["ExampleDatabase"] = not_set, # type: ignore
- verbosity: "Verbosity" = not_set, # type: ignore
- phases: Collection["Phase"] = not_set, # type: ignore
- stateful_step_count: int = not_set, # type: ignore
- report_multiple_bugs: bool = not_set, # type: ignore
- suppress_health_check: Collection["HealthCheck"] = not_set, # type: ignore
- deadline: Union[int, float, datetime.timedelta, None] = not_set, # type: ignore
- print_blob: bool = not_set, # type: ignore
- backend: str = not_set, # type: ignore
- ) -> None:
- if parent is not None:
- check_type(settings, parent, "parent")
- if derandomize not in (not_set, False):
- if database not in (not_set, None): # type: ignore
- raise InvalidArgument(
- "derandomize=True implies database=None, so passing "
- f"{database=} too is invalid."
- )
- database = None
-
- defaults = parent or settings.default
- if defaults is not None:
- for setting in all_settings.values():
- value = locals()[setting.name]
- if value is not_set:
- object.__setattr__(
- self, setting.name, getattr(defaults, setting.name)
- )
- else:
- object.__setattr__(self, setting.name, setting.validator(value))
-
- def __call__(self, test: T) -> T:
- """Make the settings object (self) an attribute of the test.
-
- The settings are later discovered by looking them up on the test itself.
- """
- # Aliasing as Any avoids mypy errors (attr-defined) when accessing and
- # setting custom attributes on the decorated function or class.
- _test: Any = test
-
- # Using the alias here avoids a mypy error (return-value) later when
- # ``test`` is returned, because this check results in type refinement.
- if not callable(_test):
- raise InvalidArgument(
- "settings objects can be called as a decorator with @given, "
- f"but decorated {test=} is not callable."
- )
- if inspect.isclass(test):
- from hypothesis.stateful import RuleBasedStateMachine
-
- if issubclass(_test, RuleBasedStateMachine):
- attr_name = "_hypothesis_internal_settings_applied"
- if getattr(test, attr_name, False):
- raise InvalidArgument(
- "Applying the @settings decorator twice would "
- "overwrite the first version; merge their arguments "
- "instead."
- )
- setattr(test, attr_name, True)
- _test.TestCase.settings = self
- return test # type: ignore
- else:
- raise InvalidArgument(
- "@settings(...) can only be used as a decorator on "
- "functions, or on subclasses of RuleBasedStateMachine."
- )
- if hasattr(_test, "_hypothesis_internal_settings_applied"):
- # Can't use _hypothesis_internal_use_settings as an indicator that
- # @settings was applied, because @given also assigns that attribute.
- descr = get_pretty_function_description(test)
- raise InvalidArgument(
- f"{descr} has already been decorated with a settings object.\n"
- f" Previous: {_test._hypothesis_internal_use_settings!r}\n"
- f" This: {self!r}"
- )
-
- _test._hypothesis_internal_use_settings = self
- _test._hypothesis_internal_settings_applied = True
- return test
-
- @classmethod
- def _define_setting(
- cls,
- name: str,
- description: str,
- *,
- default: object,
- options: Optional[Sequence[object]] = None,
- validator: Optional[ValidatorT] = None,
- show_default: bool = True,
- ) -> None:
- """Add a new setting.
-
- - name is the name of the property that will be used to access the
- setting. This must be a valid python identifier.
- - description will appear in the property's docstring
- - default is the default value. This may be a zero argument
- function in which case it is evaluated and its result is stored
- the first time it is accessed on any given settings object.
- """
- if settings.__definitions_are_locked:
- raise InvalidState(
- "settings have been locked and may no longer be defined."
- )
- if options is not None:
- options = tuple(options)
- assert default in options
-
- def validator(value):
- if value not in options:
- msg = f"Invalid {name}, {value!r}. Valid options: {options!r}"
- raise InvalidArgument(msg)
- return value
-
- else:
- assert validator is not None
-
- all_settings[name] = Setting(
- name=name,
- description=description.strip(),
- default=default,
- validator=validator,
- )
- setattr(settings, name, settingsProperty(name, show_default=show_default))
+ normal = 1
+ """
+ Standard verbosity. Hypothesis will print the falsifying example, alongside
+ any notes made with |note| (only for the falsfying example).
+ """
- @classmethod
- def lock_further_definitions(cls) -> None:
- settings.__definitions_are_locked = True
+ verbose = 2
+ """
+ Increased verbosity. In addition to everything in |Verbosity.normal|, Hypothesis
+ will print each example as it tries it, as well as any notes made with |note|
+ for every example. Hypothesis will also print shrinking attempts.
+ """
- def __setattr__(self, name: str, value: object) -> NoReturn:
- raise AttributeError("settings objects are immutable")
+ debug = 3
+ """
+ Even more verbosity. Useful for debugging Hypothesis internals. You probably
+ don't want this.
+ """
def __repr__(self) -> str:
- bits = sorted(
- f"{name}={getattr(self, name)!r}"
- for name in all_settings
- if (name != "backend" or len(AVAILABLE_PROVIDERS) > 1) # experimental
- )
- return "settings({})".format(", ".join(bits))
-
- def show_changed(self) -> str:
- bits = []
- for name, setting in all_settings.items():
- value = getattr(self, name)
- if value != setting.default:
- bits.append(f"{name}={value!r}")
- return ", ".join(sorted(bits, key=len))
-
- @staticmethod
- def register_profile(
- name: str,
- parent: Optional["settings"] = None,
- **kwargs: Any,
- ) -> None:
- """Registers a collection of values to be used as a settings profile.
-
- Settings profiles can be loaded by name - for example, you might
- create a 'fast' profile which runs fewer examples, keep the 'default'
- profile, and create a 'ci' profile that increases the number of
- examples and uses a different database to store failures.
-
- The arguments to this method are exactly as for
- :class:`~hypothesis.settings`: optional ``parent`` settings, and
- keyword arguments for each setting that will be set differently to
- parent (or settings.default, if parent is None).
-
- If you register a profile that has already been defined and that profile
- is the currently loaded profile, the new changes will take effect immediately,
- and do not require reloading the profile.
- """
- check_type(str, name, "name")
- settings._profiles[name] = settings(parent=parent, **kwargs)
- if settings._current_profile == name:
- settings.load_profile(name)
-
- @staticmethod
- def get_profile(name: str) -> "settings":
- """Return the profile with the given name."""
- check_type(str, name, "name")
- try:
- return settings._profiles[name]
- except KeyError:
- raise InvalidArgument(f"Profile {name!r} is not registered") from None
-
- @staticmethod
- def load_profile(name: str) -> None:
- """Loads in the settings defined in the profile provided.
-
- If the profile does not exist, InvalidArgument will be raised.
- Any setting not defined in the profile will be the library
- defined default for that setting.
- """
- check_type(str, name, "name")
- settings._current_profile = name
- settings._assign_default_internal(settings.get_profile(name))
-
-
-def local_settings(s: settings) -> Generator[settings, None, None]:
- with default_variable.with_value(s):
- yield s
-
-
-class Setting:
- name: str = attr.ib()
- description: str = attr.ib()
- default: object = attr.ib()
- validator: ValidatorT = attr.ib()
-
-
-def _max_examples_validator(x: int) -> int:
- check_type(int, x, name="max_examples")
- if x < 1:
- raise InvalidArgument(
- f"max_examples={x!r} should be at least one. You can disable "
- "example generation with the `phases` setting instead."
- )
- return x
-
-
-settings._define_setting(
- "max_examples",
- default=100,
- validator=_max_examples_validator,
- description="""
-Once this many satisfying examples have been considered without finding any
-counter-example, Hypothesis will stop looking.
-
-Note that we might call your test function fewer times if we find a bug early
-or can tell that we've exhausted the search space; or more if we discard some
-examples due to use of .filter(), assume(), or a few other things that can
-prevent the test case from completing successfully.
-
-The default value is chosen to suit a workflow where the test will be part of
-a suite that is regularly executed locally or on a CI server, balancing total
-running time against the chance of missing a bug.
-
-If you are writing one-off tests, running tens of thousands of examples is
-quite reasonable as Hypothesis may miss uncommon bugs with default settings.
-For very complex code, we have observed Hypothesis finding novel bugs after
-*several million* examples while testing :pypi:`SymPy <sympy>`.
-If you are running more than 100k examples for a test, consider using our
-:ref:`integration for coverage-guided fuzzing <fuzz_one_input>` - it really
-shines when given minutes or hours to run.
-""",
-)
-
-
-settings._define_setting(
- "derandomize",
- default=False,
- options=(True, False),
- description="""
-If True, seed Hypothesis' random number generator using a hash of the test
-function, so that every run will test the same set of examples until you
-update Hypothesis, Python, or the test function.
-
-This allows you to `check for regressions and look for bugs
-<https://blog.nelhage.com/post/two-kinds-of-testing/>`__ using
-:ref:`separate settings profiles <settings_profiles>` - for example running
-quick deterministic tests on every commit, and a longer non-deterministic
-nightly testing run.
+ return f"Verbosity.{self.name}"
-By default when running on CI, this will be set to True.
-""",
-)
+@unique
+class Phase(IntEnum):
+ """Options for the |settings.phases| argument to |@settings|."""
-def _validate_database(db: "ExampleDatabase") -> "ExampleDatabase":
- from hypothesis.database import ExampleDatabase
+ explicit = 0
+ """
+ Controls whether explicit examples are run.
+ """
- if db is None or isinstance(db, ExampleDatabase):
- return db
- raise InvalidArgument(
- "Arguments to the database setting must be None or an instance of "
- "ExampleDatabase. Try using one of the specific subclasses in "
- "hypothesis.database"
- )
+ reuse = 1
+ """
+ Controls whether previous examples will be reused.
+ """
+ generate = 2
+ """
+ Controls whether new examples will be generated.
+ """
-settings._define_setting(
- "database",
- default=not_set,
- show_default=False,
- description="""
-An instance of :class:`~hypothesis.database.ExampleDatabase` that will be
-used to save examples to and load previous examples from. May be ``None``
-in which case no storage will be used.
+ target = 3
+ """
+ Controls whether examples will be mutated for targeting.
+ """
-See the :ref:`example database documentation <database>` for a list of built-in
-example database implementations, and how to define custom implementations.
-""",
- validator=_validate_database,
-)
+ shrink = 4
+ """
+ Controls whether examples will be shrunk.
+ """
+ explain = 5
+ """
+ Controls whether Hypothesis attempts to explain test failures.
-@unique
-class Phase(IntEnum):
- explicit = 0 #: controls whether explicit examples are run.
- reuse = 1 #: controls whether previous examples will be reused.
- generate = 2 #: controls whether new examples will be generated.
- target = 3 #: controls whether examples will be mutated for targeting.
- shrink = 4 #: controls whether examples will be shrunk.
- explain = 5 #: controls whether Hypothesis attempts to explain test failures.
+ The explain phase has two parts, each of which is best-effort - if Hypothesis
+ can't find a useful explanation, we'll just print the minimal failing example.
+ """
def __repr__(self) -> str:
return f"Phase.{self.name}"
@@ -484,7 +144,16 @@ class HealthCheckMeta(EnumMeta):
class HealthCheck(Enum, metaclass=HealthCheckMeta):
"""Arguments for :attr:`~hypothesis.settings.suppress_health_check`.
- Each member of this enum is a type of health check to suppress.
+ Each member of this enum is a specific health check to suppress.
+
+ Hypothesis' health checks are designed to detect and warn you about performance
+ problems where your tests are slow, inefficient, or generating very large examples.
+
+ If this is expected, e.g. when generating large arrays or dataframes, you can selectively
+ disable them with the :obj:`~hypothesis.settings.suppress_health_check` setting.
+ The argument for this parameter is a list with elements drawn from any of
+ the class-level attributes of the HealthCheck class.
+ Using a value of ``list(HealthCheck)`` will disable all health checks.
"""
def __repr__(self) -> str:
@@ -513,8 +182,8 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta):
filter_too_much = 2
"""Check for when the test is filtering out too many examples, either
- through use of :func:`~hypothesis.assume()` or |strategy.filter|,
- or occasionally for Hypothesis internal reasons."""
+ through use of |assume| or |.filter|, or occasionally for Hypothesis
+ internal reasons."""
too_slow = 3
"""Check for when your data generation is extremely slow and likely to hurt
@@ -527,11 +196,11 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta):
"""Checks if the natural example to shrink towards is very large."""
not_a_test_method = 8
- """Deprecated; we always error if :func:`@given <hypothesis.given>` is applied
+ """Deprecated; we always error if |@given| is applied
to a method defined by :class:`python:unittest.TestCase` (i.e. not a test)."""
function_scoped_fixture = 9
- """Checks if :func:`@given <hypothesis.given>` has been applied to a test
+ """Checks if |@given| has been applied to a test
with a pytest function-scoped fixture. Function-scoped fixtures run once
for the whole function, not once per example, and this is usually not what
you want.
@@ -549,7 +218,7 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta):
which is enabled by default when running Hypothesis inside pytest."""
differing_executors = 10
- """Checks if :func:`@given <hypothesis.given>` has been applied to a test
+ """Checks if |@given| has been applied to a test
which is executed by different :ref:`executors<custom-function-execution>`.
If your test function is defined as a method on a class, that class will be
your executor, and subclasses executing an inherited test is a common way
@@ -557,126 +226,104 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta):
The correct fix is often to bring the executor instance under the control
of hypothesis by explicit parametrization over, or sampling from,
- subclasses, or to refactor so that :func:`@given <hypothesis.given>` is
+ subclasses, or to refactor so that |@given| is
specified on leaf subclasses."""
nested_given = 11
- """Checks if :func:`@given <hypothesis.given>` is used inside another
- :func:`@given <hypothesis.given>`. This results in quadratic generation and
+ """Checks if |@given| is used inside another
+ |@given|. This results in quadratic generation and
shrinking behavior, and can usually be expressed more cleanly by using
:func:`~hypothesis.strategies.data` to replace the inner
- :func:`@given <hypothesis.given>`.
+ |@given|.
Nesting @given can be appropriate if you set appropriate limits for the
quadratic behavior and cannot easily reexpress the inner function with
:func:`~hypothesis.strategies.data`. To suppress this health check, set
``suppress_health_check=[HealthCheck.nested_given]`` on the outer
- :func:`@given <hypothesis.given>`. Setting it on the inner
- :func:`@given <hypothesis.given>` has no effect. If you have more than one
+ |@given|. Setting it on the inner
+ |@given| has no effect. If you have more than one
level of nesting, add a suppression for this health check to every
- :func:`@given <hypothesis.given>` except the innermost one.
+ |@given| except the innermost one.
"""
-@unique
-class Verbosity(IntEnum):
- """Verbosity levels for |@settings|."""
+class duration(datetime.timedelta):
+ """A timedelta specifically measured in milliseconds."""
- quiet = 0
- """
- Hypothesis will not print any output, not even the final falsifying example.
- """
+ def __repr__(self) -> str:
+ ms = self.total_seconds() * 1000
+ return f"timedelta(milliseconds={int(ms) if ms == int(ms) else ms!r})"
- normal = 1
- """
- Standard verbosity. Hypothesis will print the falsifying example, alongside
- any notes made with |note| (only for the falsfying example).
- """
- verbose = 2
- """
- Increased verbosity. In addition to everything in |Verbosity.normal|, Hypothesis
- will print each example as it tries it, as well as any notes made with |note|
- for every example. Hypothesis will also print shrinking attempts.
- """
+def is_in_ci() -> bool:
+ # GitHub Actions, Travis CI and AppVeyor have "CI"
+ # Azure Pipelines has "TF_BUILD"
+ # GitLab CI has "GITLAB_CI"
+ return "CI" in os.environ or "TF_BUILD" in os.environ or "GITLAB_CI" in os.environ
- debug = 3
- """
- Even more verbosity. Useful for debugging Hypothesis internals. You probably
- don't want this.
- """
- def __repr__(self) -> str:
- return f"Verbosity.{self.name}"
+default_variable = DynamicVariable[Optional["settings"]](None)
-settings._define_setting(
- "verbosity",
- options=tuple(Verbosity),
- default=Verbosity.normal,
- description="Control the verbosity level of Hypothesis messages",
-)
+def _validate_choices(name: str, value: T, *, choices: Sequence[object]) -> T:
+ if value not in choices:
+ msg = f"Invalid {name}, {value!r}. Valid choices: {choices!r}"
+ raise InvalidArgument(msg)
+ return value
-def _validate_phases(phases: Sequence[Phase]) -> Sequence[Phase]:
- phases = tuple(phases)
- for a in phases:
- if not isinstance(a, Phase):
- raise InvalidArgument(f"{a!r} is not a valid phase")
- return tuple(p for p in list(Phase) if p in phases)
+def _validate_max_examples(max_examples: int) -> int:
+ check_type(int, max_examples, name="max_examples")
+ if max_examples < 1:
+ raise InvalidArgument(
+ f"max_examples={max_examples!r} must be at least one. If you want "
+ "to disable generation entirely, use phases=[Phase.explicit] instead."
+ )
+ return max_examples
-settings._define_setting(
- "phases",
- default=tuple(Phase),
- description=(
- "Control which phases should be run. "
- "See :ref:`the full documentation for more details <phases>`"
- ),
- validator=_validate_phases,
-)
+def _validate_database(
+ database: Optional["ExampleDatabase"],
+) -> Optional["ExampleDatabase"]:
+ from hypothesis.database import ExampleDatabase
+ if database is None or isinstance(database, ExampleDatabase):
+ return database
+ raise InvalidArgument(
+ "Arguments to the database setting must be None or an instance of "
+ "ExampleDatabase. Use one of the database classes in "
+ "hypothesis.database"
+ )
-def _validate_stateful_step_count(x: int) -> int:
- check_type(int, x, name="stateful_step_count")
- if x < 1:
- raise InvalidArgument(f"stateful_step_count={x!r} must be at least one.")
- return x
+def _validate_phases(phases: Collection[Phase]) -> Sequence[Phase]:
+ phases = tuple(phases)
+ for phase in phases:
+ if not isinstance(phase, Phase):
+ raise InvalidArgument(f"{phase!r} is not a valid phase")
+ return tuple(phase for phase in list(Phase) if phase in phases)
-settings._define_setting(
- name="stateful_step_count",
- default=50,
- validator=_validate_stateful_step_count,
- description="""
-Number of steps to run a stateful program for before giving up on it breaking.
-""",
-)
-settings._define_setting(
- name="report_multiple_bugs",
- default=True,
- options=(True, False),
- description="""
-Because Hypothesis runs the test many times, it can sometimes find multiple
-bugs in a single run. Reporting all of them at once is usually very useful,
-but replacing the exceptions can occasionally clash with debuggers.
-If disabled, only the exception with the smallest minimal example is raised.
-""",
-)
+def _validate_stateful_step_count(stateful_step_count: int) -> int:
+ check_type(int, stateful_step_count, name="stateful_step_count")
+ if stateful_step_count < 1:
+ raise InvalidArgument(
+ f"stateful_step_count={stateful_step_count!r} must be at least one."
+ )
+ return stateful_step_count
-def validate_health_check_suppressions(suppressions):
- suppressions = try_convert(list, suppressions, "suppress_health_check")
- for s in suppressions:
- if not isinstance(s, HealthCheck):
+def _validate_suppress_health_check(suppressions):
+ suppressions = try_convert(tuple, suppressions, "suppress_health_check")
+ for health_check in suppressions:
+ if not isinstance(health_check, HealthCheck):
raise InvalidArgument(
- f"Non-HealthCheck value {s!r} of type {type(s).__name__} "
+ f"Non-HealthCheck value {health_check!r} of type {type(health_check).__name__} "
"is invalid in suppress_health_check."
)
- if s in (HealthCheck.return_value, HealthCheck.not_a_test_method):
+ if health_check in (HealthCheck.return_value, HealthCheck.not_a_test_method):
note_deprecation(
- f"The {s.name} health check is deprecated, because this is always an error.",
+ f"The {health_check.name} health check is deprecated, because this is always an error.",
since="2023-03-15",
has_codemod=False,
stacklevel=2,
@@ -684,22 +331,6 @@ def validate_health_check_suppressions(suppressions):
return suppressions
-settings._define_setting(
- "suppress_health_check",
- default=(),
- description="""A list of :class:`~hypothesis.HealthCheck` items to disable.""",
- validator=validate_health_check_suppressions,
-)
-
-
-class duration(datetime.timedelta):
- """A timedelta specifically measured in milliseconds."""
-
- def __repr__(self) -> str:
- ms = self.total_seconds() * 1000
- return f"timedelta(milliseconds={int(ms) if ms == int(ms) else ms!r})"
-
-
def _validate_deadline(
x: Union[int, float, datetime.timedelta, None],
) -> Optional[duration]:
@@ -730,67 +361,609 @@ def _validate_deadline(
raise invalid_deadline_error
-settings._define_setting(
- "deadline",
- default=duration(milliseconds=200),
- validator=_validate_deadline,
- description="""
-If set, a duration (as timedelta, or integer or float number of milliseconds)
-that each individual example (i.e. each time your test
-function is called, not the whole decorated test) within a test is not
-allowed to exceed. Tests which take longer than that may be converted into
-errors (but will not necessarily be if close to the deadline, to allow some
-variability in test run time).
+def _validate_backend(backend: str) -> str:
+ if backend not in AVAILABLE_PROVIDERS:
+ if backend == "crosshair": # pragma: no cover
+ install = '`pip install "hypothesis[crosshair]"` and try again.'
+ raise InvalidArgument(f"backend={backend!r} is not available. {install}")
+ raise InvalidArgument(
+ f"backend={backend!r} is not available - maybe you need to install a plugin?"
+ f"\n Installed backends: {sorted(AVAILABLE_PROVIDERS)!r}"
+ )
+ return backend
-Set this to ``None`` to disable this behaviour entirely.
-By default when running on CI, this will be set to None.
-""",
-)
+class settingsMeta(type):
+ def __init__(cls, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ @property
+ def default(cls) -> Optional["settings"]:
+ v = default_variable.value
+ if v is not None:
+ return v
+ if getattr(settings, "_current_profile", None) is not None:
+ assert settings._current_profile is not None
+ settings.load_profile(settings._current_profile)
+ assert default_variable.value is not None
+ return default_variable.value
-def is_in_ci() -> bool:
- # GitHub Actions, Travis CI and AppVeyor have "CI"
- # Azure Pipelines has "TF_BUILD"
- return "CI" in os.environ or "TF_BUILD" in os.environ
+ def __setattr__(cls, name: str, value: object) -> None:
+ if name == "default":
+ raise AttributeError(
+ "Cannot assign to the property settings.default - "
+ "consider using settings.load_profile instead."
+ )
+ elif not name.startswith("_"):
+ raise AttributeError(
+ f"Cannot assign hypothesis.settings.{name}={value!r} - the settings "
+ "class is immutable. You can change the global default "
+ "settings with settings.load_profile, or use @settings(...) "
+ "to decorate your test instead."
+ )
+ super().__setattr__(name, value)
+ def __repr__(cls):
+ return "hypothesis.settings"
-settings._define_setting(
- "print_blob",
- default=False,
- options=(True, False),
- description="""
-If set to ``True``, Hypothesis will print code for failing examples that can be used with
-:func:`@reproduce_failure <hypothesis.reproduce_failure>` to reproduce the failing example.
-""",
-)
+class settings(metaclass=settingsMeta):
+ """
+ A settings object controls the following aspects of test behavior:
+ |~settings.max_examples|, |~settings.derandomize|, |~settings.database|,
+ |~settings.verbosity|, |~settings.phases|, |~settings.stateful_step_count|,
+ |~settings.report_multiple_bugs|, |~settings.suppress_health_check|,
+ |~settings.deadline|, |~settings.print_blob|, and |~settings.backend|.
-def _backend_validator(value: str) -> str:
- if value not in AVAILABLE_PROVIDERS:
- if value == "crosshair": # pragma: no cover
- install = '`pip install "hypothesis[crosshair]"` and try again.'
- raise InvalidArgument(f"backend={value!r} is not available. {install}")
- raise InvalidArgument(
- f"backend={value!r} is not available - maybe you need to install a plugin?"
- f"\n Installed backends: {sorted(AVAILABLE_PROVIDERS)!r}"
+ A settings object can be applied as a decorator to a test function, in which
+ case that test function will use those settings. A test may only have one
+ settings object applied to it. A settings object can also be passed to
+ |settings.register_profile| or as a parent to another |settings|.
+
+ Attribute inheritance
+ ---------------------
+
+ Settings objects are immutable once created. When a settings object is created,
+ it uses the value specified for each attribute. Any attribute which is
+ not specified will inherit from its value in the ``parent`` settings object.
+ If ``parent`` is not passed, any attributes which are not specified will inherit
+ from the currently active settings profile instead.
+
+ For instance, ``settings(max_examples=10)`` will have a ``max_examples`` of ``10``,
+ and the value of all other attributes will be equal to its value in the
+ currently active settings profile.
+
+ A settings object is immutable once created. Changes made from activating a new
+ settings profile with |settings.load_profile| will be reflected in
+ settings objects created after the profile was made active, but not in existing
+ settings objects.
+
+ Built-in profiles
+ -----------------
+
+ While you can register additional profiles with |settings.register_profile|,
+ Hypothesis comes with two built-in profiles: ``default`` and ``ci``.
+
+ The ``default`` profile is active by default, unless one of the ``CI``,
+ ``TF_BUILD``, or ``GITLAB_CI`` environment variables are set (to any value),
+ in which case the ``CI`` profile will be active by default.
+
+ The attributes of the currently active settings profile can be retrieved with
+ ``settings()`` (so ``settings().max_examples`` is the currently active default
+ for |settings.max_examples|).
+
+ The settings attributes for the built-in profiles are as follows:
+
+ .. code-block:: python
+
+ default = settings.register_profile(
+ "default",
+ max_examples=100,
+ derandomize=False,
+ database=not_set, # see settings.database for details
+ verbosity=Verbosity.normal,
+ phases=tuple(Phase),
+ stateful_step_count=50,
+ report_multiple_bugs=True,
+ suppress_health_check=(),
+ deadline=duration(milliseconds=200),
+ print_blob=False,
+ backend="hypothesis",
)
- return value
+ ci = settings.register_profile(
+ "ci",
+ parent=default,
+ derandomize=True,
+ deadline=None,
+ database=None,
+ print_blob=True,
+ suppress_health_check=[HealthCheck.too_slow],
+ )
-settings._define_setting(
- "backend",
- default="hypothesis",
- show_default=False,
- validator=_backend_validator,
- description="""
-EXPERIMENTAL AND UNSTABLE - see :ref:`alternative-backends`.
-The importable name of a backend which Hypothesis should use to generate primitive
-types. We aim to support heuristic-random, solver-based, and fuzzing-based backends.
-""",
-)
+ You can configure either of the built-in profiles with |settings.register_profile|:
+
+ .. code-block:: python
+
+ # run more examples in CI
+ settings.register_profile(
+ "ci",
+ settings.get_profile("ci"),
+ max_examples=1000,
+ )
+ """
+
+ _profiles: ClassVar[dict[str, "settings"]] = {}
+ _current_profile: ClassVar[Optional[str]] = None
+
+ def __init__(
+ self,
+ parent: Optional["settings"] = None,
+ *,
+ # This looks pretty strange, but there's good reason: we want Mypy to detect
+ # bad calls downstream, but not to freak out about the `= not_set` part even
+ # though it's not semantically valid to pass that as an argument value.
+ # The intended use is "like **kwargs, but more tractable for tooling".
+ max_examples: int = not_set, # type: ignore
+ derandomize: bool = not_set, # type: ignore
+ database: Optional["ExampleDatabase"] = not_set, # type: ignore
+ verbosity: "Verbosity" = not_set, # type: ignore
+ phases: Collection["Phase"] = not_set, # type: ignore
+ stateful_step_count: int = not_set, # type: ignore
+ report_multiple_bugs: bool = not_set, # type: ignore
+ suppress_health_check: Collection["HealthCheck"] = not_set, # type: ignore
+ deadline: Union[int, float, datetime.timedelta, None] = not_set, # type: ignore
+ print_blob: bool = not_set, # type: ignore
+ backend: str = not_set, # type: ignore
+ ) -> None:
+ self._in_definition = True
+
+ if parent is not None:
+ check_type(settings, parent, "parent")
+ if derandomize not in (not_set, False):
+ if database not in (not_set, None): # type: ignore
+ raise InvalidArgument(
+ "derandomize=True implies database=None, so passing "
+ f"{database=} too is invalid."
+ )
+ database = None
+
+ # fallback is None if we're creating the default settings object, and
+ # the parent (or default settings object) otherwise
+ self._fallback = parent or settings.default
+ self._max_examples = (
+ self._fallback.max_examples # type: ignore
+ if max_examples is not_set # type: ignore
+ else _validate_max_examples(max_examples)
+ )
+ self._derandomize = (
+ self._fallback.derandomize # type: ignore
+ if derandomize is not_set # type: ignore
+ else _validate_choices("derandomize", derandomize, choices=[True, False])
+ )
+ if database is not not_set: # type: ignore
+ database = _validate_database(database)
+ self._database = database
+ self._cached_database = None
+ self._verbosity = (
+ self._fallback.verbosity # type: ignore
+ if verbosity is not_set # type: ignore
+ else _validate_choices("verbosity", verbosity, choices=tuple(Verbosity))
+ )
+ self._phases = (
+ self._fallback.phases # type: ignore
+ if phases is not_set # type: ignore
+ else _validate_phases(phases)
+ )
+ self._stateful_step_count = (
+ self._fallback.stateful_step_count # type: ignore
+ if stateful_step_count is not_set # type: ignore
+ else _validate_stateful_step_count(stateful_step_count)
+ )
+ self._report_multiple_bugs = (
+ self._fallback.report_multiple_bugs # type: ignore
+ if report_multiple_bugs is not_set # type: ignore
+ else _validate_choices(
+ "report_multiple_bugs", report_multiple_bugs, choices=[True, False]
+ )
+ )
+ self._suppress_health_check = (
+ self._fallback.suppress_health_check # type: ignore
+ if suppress_health_check is not_set # type: ignore
+ else _validate_suppress_health_check(suppress_health_check)
+ )
+ self._deadline = (
+ self._fallback.deadline # type: ignore
+ if deadline is not_set
+ else _validate_deadline(deadline)
+ )
+ self._print_blob = (
+ self._fallback.print_blob # type: ignore
+ if print_blob is not_set # type: ignore
+ else _validate_choices("print_blob", print_blob, choices=[True, False])
+ )
+ self._backend = (
+ self._fallback.backend # type: ignore
+ if backend is not_set # type: ignore
+ else _validate_backend(backend)
+ )
+
+ self._in_definition = False
+
+ @property
+ def max_examples(self):
+ """
+ Once this many satisfying examples have been considered without finding any
+ counter-example, Hypothesis will stop looking.
+
+ Note that we might call your test function fewer times if we find a bug early
+ or can tell that we've exhausted the search space; or more if we discard some
+ examples due to use of .filter(), assume(), or a few other things that can
+ prevent the test case from completing successfully.
+
+ The default value is chosen to suit a workflow where the test will be part of
+ a suite that is regularly executed locally or on a CI server, balancing total
+ running time against the chance of missing a bug.
+
+ If you are writing one-off tests, running tens of thousands of examples is
+ quite reasonable as Hypothesis may miss uncommon bugs with default settings.
+ For very complex code, we have observed Hypothesis finding novel bugs after
+ *several million* examples while testing :pypi:`SymPy <sympy>`.
+ If you are running more than 100k examples for a test, consider using our
+ :ref:`integration for coverage-guided fuzzing <fuzz_one_input>` - it really
+ shines when given minutes or hours to run.
+
+ The default max examples is ``100``.
+ """
+ return self._max_examples
+
+ @property
+ def derandomize(self):
+ """
+ If True, seed Hypothesis' random number generator using a hash of the test
+ function, so that every run will test the same set of examples until you
+ update Hypothesis, Python, or the test function.
+
+ This allows you to `check for regressions and look for bugs
+ <https://blog.nelhage.com/post/two-kinds-of-testing/>`__ using separate
+ settings profiles - for example running
+ quick deterministic tests on every commit, and a longer non-deterministic
+ nightly testing run.
+
+ The default is ``False``. If running on CI, the default is ``True`` instead.
+ """
+ return self._derandomize
+
+ @property
+ def database(self):
+ """
+ An instance of |ExampleDatabase| that will be used to save examples to
+ and load previous examples from.
+
+ If not set, a |DirectoryBasedExampleDatabase| is created in the current
+ working directory under ``.hypothesis/examples``. If this location is
+ unusable, e.g. due to the lack of read or write permissions, Hypothesis
+ will emit a warning and fall back to an |InMemoryExampleDatabase|.
+
+ If ``None``, no storage will be used.
+
+ See the :ref:`database documentation <database>` for a list of database
+ classes, and how to define custom database classes.
+ """
+ from hypothesis.database import _db_for_path
+
+ # settings.database has two conflicting requirements:
+ # * The default settings should respect changes to set_hypothesis_home_dir
+ # in-between accesses
+ # * `s.database is s.database` should be true, except for the default settings
+ #
+ # We therefore cache s.database for everything except the default settings,
+ # which always recomputes dynamically.
+ if self._fallback is None:
+ # if self._fallback is None, we are the default settings, at which point
+ # we should recompute the database dynamically
+ assert self._database is not_set
+ return _db_for_path(not_set)
+
+ # otherwise, we cache the database
+ if self._cached_database is None:
+ self._cached_database = (
+ self._fallback.database if self._database is not_set else self._database
+ )
+ return self._cached_database
+
+ @property
+ def verbosity(self):
+ """
+ Control the verbosity level of Hypothesis messages.
+
+ To see what's going on while Hypothesis runs your tests, you can turn
+ up the verbosity setting.
+
+ .. code-block:: pycon
+
+ >>> from hypothesis import settings, Verbosity
+ >>> from hypothesis.strategies import lists, integers
+ >>> @given(lists(integers()))
+ ... @settings(verbosity=Verbosity.verbose)
+ ... def f(x):
+ ... assert not any(x)
+ ... f()
+ Trying example: []
+ Falsifying example: [-1198601713, -67, 116, -29578]
+ Shrunk example to [-1198601713]
+ Shrunk example to [-128]
+ Shrunk example to [32]
+ Shrunk example to [1]
+ [1]
-settings.lock_further_definitions()
+ The four levels are |Verbosity.quiet|, |Verbosity.normal|,
+ |Verbosity.verbose|, and |Verbosity.debug|. |Verbosity.normal| is the
+ default. For |Verbosity.quiet|, Hypothesis will not print anything out,
+ not even the final falsifying example. |Verbosity.debug| is basically
+ |Verbosity.verbose| but a bit more so. You probably don't want it.
+
+ If you are using :pypi:`pytest`, you may also need to :doc:`disable
+ output capturing for passing tests <pytest:how-to/capture-stdout-stderr>`
+ to see verbose output as tests run.
+ """
+ return self._verbosity
+
+ @property
+ def phases(self):
+ """
+ Control which phases should be run.
+
+ Hypothesis divides tests into logically distinct phases.
+
+ - |Phase.explicit|: Running explicit examples from |@example|.
+ - |Phase.reuse|: Running examples from the database which previously failed.
+ - |Phase.generate|: Generating new random examples.
+ - |Phase.target|: Mutating examples for :ref:`targeted property-based
+ testing <targeted>`. Requires |Phase.generate|.
+ - |Phase.shrink|: Shrinking failing examples.
+ - |Phase.explain|: Attempting to explain why a failure occurred.
+ Requires |Phase.shrink|.
+
+ Following the first failure, Hypothesis will (usually, depending on
+ which |Phase| is enabled) track which lines of code are always run on
+ failing but never on passing inputs. On 3.12+, this uses
+ :mod:`sys.monitoring`, while 3.11 and earlier uses :func:`python:sys.settrace`.
+ For python 3.11 and earlier, we therefore automatically disable the explain
+ phase on PyPy, or if you are using :pypi:`coverage` or a debugger. If
+ there are no clearly suspicious lines of code, :pep:`we refuse the
+ temptation to guess <20>`.
+
+ After shrinking to a minimal failing example, Hypothesis will try to find
+ parts of the example -- e.g. separate args to |@given|
+ -- which can vary freely without changing the result
+ of that minimal failing example. If the automated experiments run without
+ finding a passing variation, we leave a comment in the final report:
+
+ .. code-block:: python
+
+ test_x_divided_by_y(
+ x=0, # or any other generated value
+ y=0,
+ )
+
+ Just remember that the *lack* of an explanation sometimes just means that
+ Hypothesis couldn't efficiently find one, not that no explanation (or
+ simpler failing example) exists.
+
+
+ The phases setting provides you with fine grained control over which of
+ these run, with each phase corresponding to a value on the |Phase| enum.
+
+ The phases argument accepts a collection with any subset of these. e.g.
+ ``settings(phases=[Phase.generate, Phase.shrink])`` will generate new examples
+ and shrink them, but will not run explicit examples or reuse previous failures,
+ while ``settings(phases=[Phase.explicit])`` will only run the explicit
+ examples.
+ """
+
+ return self._phases
+
+ @property
+ def stateful_step_count(self):
+ """
+ The maximum number of times to call an additional |@rule| method in
+ :ref:`stateful testing <stateful>` before we give up on finding a bug.
+
+ Note that this setting is effectively multiplicative with max_examples,
+ as each example will run for a maximum of ``stateful_step_count`` steps.
+
+ The default stateful step count is ``50``.
+ """
+ return self._stateful_step_count
+
+ @property
+ def report_multiple_bugs(self):
+ """
+ Because Hypothesis runs the test many times, it can sometimes find multiple
+ bugs in a single run. Reporting all of them at once is usually very useful,
+ but replacing the exceptions can occasionally clash with debuggers.
+ If disabled, only the exception with the smallest minimal example is raised.
+
+ The default value is ``True``.
+ """
+ return self._report_multiple_bugs
+
+ @property
+ def suppress_health_check(self):
+ """
+ A list of |HealthCheck| items to disable.
+ """
+ return self._suppress_health_check
+
+ @property
+ def deadline(self):
+ """
+ The maximum allowed duration of an individual test case, in milliseconds.
+ You can pass an integer, float, or timedelta. If ``None``, the deadline
+ is disabled entirely.
+
+ We treat the deadline as a soft limit in some cases, where that would
+ avoid flakiness due to timing variability.
+
+ The default deadline is 200 milliseconds. If running on CI, the default is
+ ``None`` instead.
+ """
+ return self._deadline
+
+ @property
+ def print_blob(self):
+ """
+ If set to ``True``, Hypothesis will print code for failing examples that
+ can be used with |@reproduce_failure| to reproduce the failing example.
+
+ The default value is ``False``. If running on CI, the default is ``True`` instead.
+ """
+ return self._print_blob
+
+ @property
+ def backend(self):
+ """
+ .. warning::
+
+ EXPERIMENTAL AND UNSTABLE - see :ref:`alternative-backends`.
+
+ The importable name of a backend which Hypothesis should use to generate
+ primitive types. We support heuristic-random, solver-based, and fuzzing-based
+ backends.
+ """
+ return self._backend
+
+ def __call__(self, test: T) -> T:
+ """Make the settings object (self) an attribute of the test.
+
+ The settings are later discovered by looking them up on the test itself.
+ """
+ # Aliasing as Any avoids mypy errors (attr-defined) when accessing and
+ # setting custom attributes on the decorated function or class.
+ _test: Any = test
+
+ # Using the alias here avoids a mypy error (return-value) later when
+ # ``test`` is returned, because this check results in type refinement.
+ if not callable(_test):
+ raise InvalidArgument(
+ "settings objects can be called as a decorator with @given, "
+ f"but decorated {test=} is not callable."
+ )
+ if inspect.isclass(test):
+ from hypothesis.stateful import RuleBasedStateMachine
+
+ if issubclass(_test, RuleBasedStateMachine):
+ attr_name = "_hypothesis_internal_settings_applied"
+ if getattr(test, attr_name, False):
+ raise InvalidArgument(
+ "Applying the @settings decorator twice would "
+ "overwrite the first version; merge their arguments "
+ "instead."
+ )
+ setattr(test, attr_name, True)
+ _test.TestCase.settings = self
+ return test # type: ignore
+ else:
+ raise InvalidArgument(
+ "@settings(...) can only be used as a decorator on "
+ "functions, or on subclasses of RuleBasedStateMachine."
+ )
+ if hasattr(_test, "_hypothesis_internal_settings_applied"):
+ # Can't use _hypothesis_internal_use_settings as an indicator that
+ # @settings was applied, because @given also assigns that attribute.
+ descr = get_pretty_function_description(test)
+ raise InvalidArgument(
+ f"{descr} has already been decorated with a settings object.\n"
+ f" Previous: {_test._hypothesis_internal_use_settings!r}\n"
+ f" This: {self!r}"
+ )
+
+ _test._hypothesis_internal_use_settings = self
+ _test._hypothesis_internal_settings_applied = True
+ return test
+
+ def __setattr__(self, name: str, value: object) -> None:
+ if not name.startswith("_") and not self._in_definition:
+ raise AttributeError("settings objects are immutable")
+ return super().__setattr__(name, value)
+
+ def __repr__(self) -> str:
+ bits = sorted(
+ f"{name}={getattr(self, name)!r}"
+ for name in all_settings
+ if (name != "backend" or len(AVAILABLE_PROVIDERS) > 1) # experimental
+ )
+ return "settings({})".format(", ".join(bits))
+
+ def show_changed(self) -> str:
+ bits = []
+ for name in all_settings:
+ value = getattr(self, name)
+ if value != getattr(default, name):
+ bits.append(f"{name}={value!r}")
+ return ", ".join(sorted(bits, key=len))
+
+ @staticmethod
+ def register_profile(
+ name: str,
+ parent: Optional["settings"] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Register a settings object as a settings profile, under the name ``name``.
+ The ``parent`` and ``kwargs`` arguments to this method are as for
+ |settings|.
+
+ If a settings profile already exists under ``name``, it will be overwritten.
+ Registering a profile with the same name as the currently active profile
+ will cause those changes to take effect in the active profile immediately,
+ and do not require reloading the profile.
+
+ Registered settings profiles can be retrieved later by name with
+ |settings.get_profile|.
+ """
+ check_type(str, name, "name")
+ # if we just pass the parent and no kwargs, like
+ # settings.register_profile(settings(max_examples=10))
+ # then optimize out the pointless intermediate settings object which
+ # would just forward everything to the parent.
+ settings._profiles[name] = (
+ parent
+ if parent is not None and not kwargs
+ else settings(parent=parent, **kwargs)
+ )
+ if settings._current_profile == name:
+ settings.load_profile(name)
+
+ @staticmethod
+ def get_profile(name: str) -> "settings":
+ """
+ Returns the settings profile registered under ``name``. If no settings
+ profile is registered under ``name``, raises |InvalidArgument|.
+ """
+ check_type(str, name, "name")
+ try:
+ return settings._profiles[name]
+ except KeyError:
+ raise InvalidArgument(f"Profile {name!r} is not registered") from None
+
+ @staticmethod
+ def load_profile(name: str) -> None:
+ """
+ Makes the settings profile registered under ``name`` the active profile.
+
+ If no settings profile is registered under ``name``, raises |InvalidArgument|.
+ """
+ check_type(str, name, "name")
+ settings._current_profile = name
+ default_variable.value = settings.get_profile(name)
+
+
+def local_settings(s: settings) -> Generator[settings, None, None]:
+ with default_variable.with_value(s):
+ yield s
def note_deprecation(
@@ -807,7 +980,20 @@ def note_deprecation(
warnings.warn(HypothesisDeprecationWarning(message), stacklevel=2 + stacklevel)
-settings.register_profile("default", settings())
+default = settings(
+ max_examples=100,
+ derandomize=False,
+ database=not_set, # type: ignore
+ verbosity=Verbosity.normal,
+ phases=tuple(Phase),
+ stateful_step_count=50,
+ report_multiple_bugs=True,
+ suppress_health_check=(),
+ deadline=duration(milliseconds=200),
+ print_blob=False,
+ backend="hypothesis",
+)
+settings.register_profile("default", default)
settings.load_profile("default")
assert settings.default is not None
@@ -823,7 +1009,7 @@ CI = settings(
settings.register_profile("ci", CI)
-if is_in_ci():
+if is_in_ci(): # pragma: no cover # covered in ci, but not locally
settings.load_profile("ci")
assert settings.default is not None
diff --git a/contrib/python/hypothesis/py3/hypothesis/control.py b/contrib/python/hypothesis/py3/hypothesis/control.py
index 9e76b443fe1..27679c662ed 100644
--- a/contrib/python/hypothesis/py3/hypothesis/control.py
+++ b/contrib/python/hypothesis/py3/hypothesis/control.py
@@ -44,8 +44,8 @@ def reject() -> NoReturn:
)
where = _calling_function_location("reject", inspect.currentframe())
if currently_in_test_context():
- count = current_build_context().data._observability_predicates[where]
- count["unsatisfied"] += 1
+ counts = current_build_context().data._observability_predicates[where]
+ counts.update_count(condition=False)
raise UnsatisfiedAssumption(where)
@@ -65,8 +65,8 @@ def assume(condition: object) -> bool:
if TESTCASE_CALLBACKS or not condition:
where = _calling_function_location("assume", inspect.currentframe())
if TESTCASE_CALLBACKS and currently_in_test_context():
- predicates = current_build_context().data._observability_predicates
- predicates[where]["satisfied" if condition else "unsatisfied"] += 1
+ counts = current_build_context().data._observability_predicates[where]
+ counts.update_count(condition=bool(condition))
if not condition:
raise UnsatisfiedAssumption(f"failed to satisfy {where}")
return True
@@ -77,12 +77,11 @@ _current_build_context = DynamicVariable[Optional["BuildContext"]](None)
def currently_in_test_context() -> bool:
"""Return ``True`` if the calling code is currently running inside an
- :func:`@given <hypothesis.given>` or :ref:`stateful <stateful>` test,
- ``False`` otherwise.
+ |@given| or :ref:`stateful <stateful>` test, and ``False`` otherwise.
This is useful for third-party integrations and assertion helpers which
- may be called from traditional or property-based tests, but can only use
- :func:`~hypothesis.assume` or :func:`~hypothesis.target` in the latter case.
+ may be called from either traditional or property-based tests, and can only
+ use e.g. |assume| or |target| in the latter case.
"""
return _current_build_context.value is not None
@@ -94,32 +93,38 @@ def current_build_context() -> "BuildContext":
return context
-class RandomSeeder:
- def __init__(self, seed):
- self.seed = seed
-
- def __repr__(self):
- return f"RandomSeeder({self.seed!r})"
-
-
-class _Checker:
- def __init__(self) -> None:
- self.saw_global_random = False
-
- def __call__(self, x):
- self.saw_global_random |= isinstance(x, RandomSeeder)
- return x
-
-
@contextmanager
def deprecate_random_in_strategy(fmt, *args):
- _global_rand_state = random.getstate()
- yield (checker := _Checker())
- if _global_rand_state != random.getstate() and not checker.saw_global_random:
- # raise InvalidDefinition
+ from hypothesis.internal import entropy
+
+ state_before = random.getstate()
+ yield
+ state_after = random.getstate()
+ if (
+ # there is a threading race condition here with deterministic_PRNG. Say
+ # we have two threads 1 and 2. We start in global random state A, and
+ # deterministic_PRNG sets to global random state B (which is constant across
+ # threads since we seed to 0 unconditionally). Then we might have state
+ # transitions:
+ #
+ # [1] [2]
+ # A -> B deterministic_PRNG().__enter__
+ # B ->B deterministic_PRNG().__enter__
+ # state_before = B deprecate_random_in_strategy.__enter__
+ # B -> A deterministic_PRNG().__exit__
+ # state_after = A deprecate_random_in_strategy.__exit__
+ #
+ # where state_before != state_after because a different thread has reset
+ # the global random state.
+ #
+ # To fix this, we track the known random states set by deterministic_PRNG,
+ # and will not note a deprecation if it matches one of those.
+ state_after != state_before
+ and hash(state_after) not in entropy._known_random_state_hashes
+ ):
note_deprecation(
"Do not use the `random` module inside strategies; instead "
- "consider `st.randoms()`, `st.sampled_from()`, etc. " + fmt.format(*args),
+ "consider `st.randoms()`, `st.sampled_from()`, etc. " + fmt.format(*args),
since="2024-02-05",
has_codemod=False,
stacklevel=1,
@@ -132,10 +137,12 @@ class BuildContext:
data: ConjectureData,
*,
is_final: bool = False,
+ wrapped_test: Callable,
) -> None:
self.data = data
self.tasks: list[Callable[[], Any]] = []
self.is_final = is_final
+ self.wrapped_test = wrapped_test
# Use defaultdict(list) here to handle the possibility of having multiple
# functions registered for the same object (due to caching, small ints, etc).
@@ -164,8 +171,8 @@ class BuildContext:
kwargs = {}
for k, s in kwarg_strategies.items():
start_idx = len(self.data.nodes)
- with deprecate_random_in_strategy("from {}={!r}", k, s) as check:
- obj = check(self.data.draw(s, observe_as=f"generate:{k}"))
+ 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
@@ -287,7 +294,7 @@ def target(observation: Union[int, float], *, label: str = "") -> Union[int, flo
``target()`` with any label more than once per test case.
.. note::
- **The more examples you run, the better this technique works.**
+ The more examples you run, the better this technique works.
As a rule of thumb, the targeting effect is noticeable above
:obj:`max_examples=1000 <hypothesis.settings.max_examples>`,
diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py
index 0498522d7ce..da5f5ea81d9 100644
--- a/contrib/python/hypothesis/py3/hypothesis/core.py
+++ b/contrib/python/hypothesis/py3/hypothesis/core.py
@@ -11,10 +11,12 @@
"""This module provides the core primitives of Hypothesis, such as given."""
import base64
import contextlib
+import dataclasses
import datetime
import inspect
import io
import math
+import os
import sys
import time
import traceback
@@ -24,11 +26,11 @@ import warnings
import zlib
from collections import defaultdict
from collections.abc import Coroutine, Generator, Hashable, Iterable, Sequence
+from dataclasses import dataclass, field
from functools import partial
from inspect import Parameter
from random import Random
from typing import (
- TYPE_CHECKING,
Any,
BinaryIO,
Callable,
@@ -39,8 +41,6 @@ from typing import (
)
from unittest import TestCase
-import attr
-
from hypothesis import strategies as st
from hypothesis._settings import (
HealthCheck,
@@ -69,9 +69,11 @@ from hypothesis.errors import (
Unsatisfiable,
UnsatisfiedAssumption,
)
+from hypothesis.internal import observability
from hypothesis.internal.compat import (
PYPY,
BaseExceptionGroup,
+ EllipsisType,
add_note,
bad_django_TestCase,
get_type_hints,
@@ -99,10 +101,10 @@ from hypothesis.internal.escalation import (
)
from hypothesis.internal.healthcheck import fail_health_check
from hypothesis.internal.observability import (
- OBSERVABILITY_COLLECT_COVERAGE,
TESTCASE_CALLBACKS,
- _system_metadata,
- deliver_json_blob,
+ InfoObservation,
+ InfoObservationType,
+ deliver_observation,
make_testcase,
)
from hypothesis.internal.reflection import (
@@ -138,38 +140,86 @@ from hypothesis.strategies._internal.strategies import (
SearchStrategy,
check_strategy,
)
-from hypothesis.strategies._internal.utils import to_jsonable
+from hypothesis.utils.threading import ThreadLocal
from hypothesis.vendor.pretty import RepresentationPrinter
from hypothesis.version import __version__
-if sys.version_info >= (3, 10):
- from types import EllipsisType as EllipsisType
-elif TYPE_CHECKING:
- from builtins import ellipsis as EllipsisType
-else: # pragma: no cover
- EllipsisType = type(Ellipsis)
-
-
TestFunc = TypeVar("TestFunc", bound=Callable)
running_under_pytest = False
pytest_shows_exceptiongroups = True
global_force_seed = None
-_hypothesis_global_random = None
+# `threadlocal` stores "engine-global" constants, which are global relative to a
+# ConjectureRunner instance (roughly speaking). Since only one conjecture runner
+# instance can be active per thread, making engine constants thread-local prevents
+# the ConjectureRunner instances of concurrent threads from treading on each other.
+threadlocal = ThreadLocal(_hypothesis_global_random=lambda: None)
+@dataclass
class Example:
- args = attr.ib()
- kwargs = attr.ib()
+ args: Any
+ kwargs: Any
# Plus two optional arguments for .xfail()
- raises = attr.ib(default=None)
- reason = attr.ib(default=None)
+ raises: Any = field(default=None)
+ reason: Any = field(default=None)
+
+
+# TODO_DOCS link to not-yet-existent patch-dumping docs
class example:
- """A decorator which ensures a specific example is always tested."""
+ """
+ Add an explicit input to a Hypothesis test, which Hypothesis will always
+ try before generating random inputs. This combines the randomized nature of
+ Hypothesis generation with a traditional parametrized test.
+
+ For example:
+
+ .. code-block:: python
+
+ @example("Hello world")
+ @example("some string with special significance")
+ @given(st.text())
+ def test_strings(s):
+ pass
+
+ will call ``test_strings("Hello World")`` and
+ ``test_strings("some string with special significance")`` before generating
+ any random inputs. |@example| may be placed in any order relative to |@given|
+ and |@settings|.
+
+ Explicit inputs from |@example| are run in the |Phase.explicit| phase.
+ Explicit inputs do not count towards |settings.max_examples|. Note that
+ explicit inputs added by |@example| do not shrink. If an explicit input
+ fails, Hypothesis will stop and report the failure without generating any
+ random inputs.
+
+ |@example| can also be used to easily reproduce a failure. For instance, if
+ Hypothesis reports that ``f(n=[0, math.nan])`` fails, you can add
+ ``@example(n=[0, math.nan])`` to your test to quickly reproduce that failure.
+
+ Arguments to ``@example``
+ -------------------------
+
+ Arguments to |@example| have the same behavior and restrictions as arguments
+ to |@given|. This means they may be either positional or keyword arguments
+ (but not both in the same |@example|):
+
+ .. code-block:: python
+
+ @example(1, 2)
+ @example(x=1, y=2)
+ @given(st.integers(), st.integers())
+ def test(x, y):
+ pass
+
+ Noting that while arguments to |@given| are strategies (like |st.integers|),
+ arguments to |@example| are values instead (like ``1``).
+
+ See the :ref:`given-arguments` section for full details.
+ """
def __init__(self, *args: Any, **kwargs: Any) -> None:
if args and kwargs:
@@ -245,30 +295,34 @@ class example:
f"{raises=} must be an exception type or tuple of exception types"
)
if condition:
- self._this_example = attr.evolve(
+ self._this_example = dataclasses.replace(
self._this_example, raises=raises, reason=reason
)
return self
def via(self, whence: str, /) -> "example":
- """Attach a machine-readable label noting whence this example came.
+ """Attach a machine-readable label noting what the origin of this example
+ was. |example.via| is completely optional and does not change runtime
+ behavior.
- The idea is that tools will be able to add ``@example()`` cases for you, e.g.
- to maintain a high-coverage set of explicit examples, but also *remove* them
- if they become redundant - without ever deleting manually-added examples:
+ |example.via| is intended to support self-documenting behavior, as well as
+ tooling which might add (or remove) |@example| decorators automatically.
+ For example:
.. code-block:: python
- # You can choose to annotate examples, or not, as you prefer
+ # Annotating examples is optional and does not change runtime behavior
@example(...)
@example(...).via("regression test for issue #42")
-
- # The `hy-` prefix is reserved for automated tooling
- @example(...).via("hy-failing")
- @example(...).via("hy-coverage")
- @example(...).via("hy-target-$label")
+ @example(...).via("discovered failure")
def test(x):
pass
+
+ .. note::
+
+ `HypoFuzz <https://hypofuzz.com/>`_ uses |example.via| to tag examples
+ in the patch of its high-coverage set of explicit inputs, on
+ `the patches page <https://hypofuzz.com/example-dashboard/#/patches>`_.
"""
if not isinstance(whence, str):
raise InvalidArgument(".via() must be passed a string")
@@ -277,16 +331,34 @@ class example:
def seed(seed: Hashable) -> Callable[[TestFunc], TestFunc]:
- """seed: Start the test execution from a specific seed.
+ """
+ Seed the randomness for this test.
+
+ ``seed`` may be any hashable object. No exact meaning for ``seed`` is provided
+ other than that for a fixed seed value Hypothesis will produce the same
+ examples (assuming that there are no other sources of nondeterminisim, such
+ as timing, hash randomization, or external state).
+
+ For example, the following test function and |RuleBasedStateMachine| will
+ each generate the same series of examples each time they are executed:
+
+ .. code-block:: python
+
+ @seed(1234)
+ @given(st.integers())
+ def test(n): ...
- May be any hashable object. No exact meaning for seed is provided
- other than that for a fixed seed value Hypothesis will try the same
- actions (insofar as it can given external sources of non-
- determinism. e.g. timing and hash randomization).
+ @seed(6789)
+ class MyMachine(RuleBasedStateMachine): ...
- Overrides the derandomize setting, which is designed to enable
- deterministic builds rather than reproducing observed failures.
+ If using pytest, you can alternatively pass ``--hypothesis-seed`` on the
+ command line.
+ Setting a seed overrides |settings.derandomize|, which is designed to enable
+ deterministic CI tests rather than reproducing observed failures.
+
+ Hypothesis will only print the seed which would reproduce a failure if a test
+ fails in an unexpected way, for instance inside Hypothesis internals.
"""
def accept(test):
@@ -300,19 +372,31 @@ def seed(seed: Hashable) -> Callable[[TestFunc], TestFunc]:
return accept
+# TODO_DOCS: link to /explanation/choice-sequence
+
+
def reproduce_failure(version: str, blob: bytes) -> Callable[[TestFunc], TestFunc]:
- """Run the example that corresponds to this data blob in order to reproduce
- a failure.
+ """
+ Run the example corresponding to the binary ``blob`` in order to reproduce a
+ failure. ``blob`` is a serialized version of the internal input representation
+ of Hypothesis.
+
+ A test decorated with |@reproduce_failure| always runs exactly one example,
+ which is expected to cause a failure. If the provided ``blob`` does not
+ cause a failure, Hypothesis will raise |DidNotReproduce|.
- A test with this decorator *always* runs only one example and always fails.
- If the provided example does not cause a failure, or is in some way invalid
- for this test, then this will fail with a DidNotReproduce error.
+ Hypothesis will print an |@reproduce_failure| decorator if
+ |settings.print_blob| is ``True`` (which is the default in CI).
- This decorator is not intended to be a permanent addition to your test
- suite. It's simply some code you can add to ease reproduction of a problem
- in the event that you don't have access to the test database. Because of
- this, *no* compatibility guarantees are made between different versions of
- Hypothesis - its API may change arbitrarily from version to version.
+ |@reproduce_failure| is intended to be temporarily added to your test suite in
+ order to reproduce a failure. It is not intended to be a permanent addition to
+ your test suite. Because of this, no compatibility guarantees are made across
+ Hypothesis versions, and |@reproduce_failure| will error if used on a different
+ Hypothesis version than it was created for.
+
+ .. seealso::
+
+ See also the :doc:`/tutorial/replaying-failures` tutorial.
"""
def accept(test):
@@ -322,6 +406,10 @@ def reproduce_failure(version: str, blob: bytes) -> Callable[[TestFunc], TestFun
return accept
+def reproduction_decorator(choices: Iterable[ChoiceT]) -> str:
+ return f"@reproduce_failure({__version__!r}, {encode_failure(choices)!r})"
+
+
def encode_failure(choices: Iterable[ChoiceT]) -> bytes:
blob = choices_to_bytes(choices)
compressed = zlib.compress(blob)
@@ -368,8 +456,8 @@ def _invalid(message, *, exc=InvalidArgument, test, given_kwargs):
wrapped_test.is_hypothesis_test = True
wrapped_test.hypothesis = HypothesisHandle(
inner_test=test,
- get_fuzz_target=wrapped_test,
- given_kwargs=given_kwargs,
+ _get_fuzz_target=wrapped_test,
+ _given_kwargs=given_kwargs,
)
return wrapped_test
@@ -568,7 +656,8 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
new = HypothesisWarning(
"The @example() decorator expects to be passed values, but "
"you passed strategies instead. See https://hypothesis."
- "readthedocs.io/en/latest/reproducing.html for details."
+ "readthedocs.io/en/latest/reference/api.html#hypothesis"
+ ".example for details."
)
new.__cause__ = err
err = new
@@ -591,15 +680,16 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
"Falsifying example", "Falsifying explicit example", 1
)
+ empty_data.freeze()
tc = make_testcase(
- start_timestamp=state._start_timestamp,
- test_name_or_nodeid=state.test_identifier,
+ run_start=state._start_timestamp,
+ property=state.test_identifier,
data=empty_data,
how_generated="explicit example",
- string_repr=state._string_repr,
+ representation=state._string_repr,
timing=state._timing_features,
)
- deliver_json_blob(tc)
+ deliver_observation(tc)
if fragments_reported:
verbose_report(fragments_reported[0].replace("Falsifying", "Trying", 1))
@@ -618,20 +708,19 @@ def get_random_for_wrapped_test(test, wrapped_test):
elif global_force_seed is not None:
return Random(global_force_seed)
else:
- global _hypothesis_global_random
- if _hypothesis_global_random is None: # pragma: no cover
- _hypothesis_global_random = Random()
- seed = _hypothesis_global_random.getrandbits(128)
+ if threadlocal._hypothesis_global_random is None: # pragma: no cover
+ threadlocal._hypothesis_global_random = Random()
+ seed = threadlocal._hypothesis_global_random.getrandbits(128)
wrapped_test._hypothesis_internal_use_generated_seed = seed
return Random(seed)
+@dataclass
class Stuff:
- selfy: Any = attr.ib(default=None)
- args: tuple = attr.ib(factory=tuple)
- kwargs: dict = attr.ib(factory=dict)
- given_kwargs: dict = attr.ib(factory=dict)
+ selfy: Any
+ args: tuple
+ kwargs: dict
+ given_kwargs: dict
def process_arguments_to_given(
@@ -689,8 +778,8 @@ def skip_exceptions_to_reraise():
exceptions.add(sys.modules["unittest2"].SkipTest)
if "nose" in sys.modules:
exceptions.add(sys.modules["nose"].SkipTest)
- if "_pytest" in sys.modules:
- exceptions.add(sys.modules["_pytest"].outcomes.Skipped)
+ if "_pytest.outcomes" in sys.modules:
+ exceptions.add(sys.modules["_pytest.outcomes"].Skipped)
return tuple(sorted(exceptions, key=str))
@@ -705,8 +794,8 @@ def failure_exceptions_to_catch() -> tuple[type[BaseException], ...]:
# them as standard exceptions, check for flakiness, etc.
# See https://github.com/HypothesisWorks/hypothesis/issues/2223 for details.
exceptions = [Exception, SystemExit, GeneratorExit]
- if "_pytest" in sys.modules:
- exceptions.append(sys.modules["_pytest"].outcomes.Failed)
+ if "_pytest.outcomes" in sys.modules:
+ exceptions.append(sys.modules["_pytest.outcomes"].Failed)
return tuple(exceptions)
@@ -853,7 +942,9 @@ class StateForActualGivenExecution:
) or get_pretty_function_description(self.wrapped_test)
def _should_trace(self):
- _trace_obs = TESTCASE_CALLBACKS and OBSERVABILITY_COLLECT_COVERAGE
+ # NOTE: we explicitly support monkeypatching this. Keep the namespace
+ # access intact.
+ _trace_obs = TESTCASE_CALLBACKS and observability.OBSERVABILITY_COLLECT_COVERAGE
_trace_failure = (
self.failed_normally
and not self.failed_due_to_deadline
@@ -890,8 +981,12 @@ class StateForActualGivenExecution:
@proxies(self.test)
def test(*args, **kwargs):
- with unwrap_markers_from_group(), ensure_free_stackframes():
- return self.test(*args, **kwargs)
+ with unwrap_markers_from_group():
+ # NOTE: For compatibility with Python 3.9's LL(1)
+ # parser, this is written as a nested with-statement,
+ # instead of a compound one.
+ with ensure_free_stackframes():
+ return self.test(*args, **kwargs)
else:
@@ -902,8 +997,12 @@ class StateForActualGivenExecution:
arg_gctime = gc_cumulative_time()
start = time.perf_counter()
try:
- with unwrap_markers_from_group(), ensure_free_stackframes():
- result = self.test(*args, **kwargs)
+ with unwrap_markers_from_group():
+ # NOTE: For compatibility with Python 3.9's LL(1)
+ # parser, this is written as a nested with-statement,
+ # instead of a compound one.
+ with ensure_free_stackframes():
+ result = self.test(*args, **kwargs)
finally:
finish = time.perf_counter()
in_drawtime = math.fsum(data.draw_times.values()) - arg_drawtime
@@ -928,7 +1027,7 @@ class StateForActualGivenExecution:
)
return result
- def run(data):
+ def run(data: ConjectureData) -> None:
# Set up dynamic context needed by a single test run.
if self.stuff.selfy is not None:
data.hypothesis_runner = self.stuff.selfy
@@ -987,10 +1086,6 @@ class StateForActualGivenExecution:
avoid_realization=data.provider.avoid_realization,
)
self._string_repr = printer.getvalue()
- data._observability_arguments = {
- k: to_jsonable(v, avoid_realization=data.provider.avoid_realization)
- for k, v in [*enumerate(args), *kwargs.items()]
- }
try:
return test(*args, **kwargs)
@@ -1008,20 +1103,36 @@ class StateForActualGivenExecution:
if parts := getattr(data, "_stateful_repr_parts", None):
self._string_repr = "\n".join(parts)
+ if TESTCASE_CALLBACKS:
+ printer = RepresentationPrinter(context=context)
+ for name, value in data._observability_args.items():
+ if name.startswith("generate:Draw "):
+ try:
+ value = data.provider.realize(value)
+ except BackendCannotProceed: # pragma: no cover
+ value = "<backend failed to realize symbolic>"
+ printer.text(f"\n{name.removeprefix('generate:')}: ")
+ printer.pretty(value)
+
+ self._string_repr += printer.getvalue()
+
# self.test_runner can include the execute_example method, or setup/teardown
# _example, so it's important to get the PRNG and build context in place first.
- with (
- local_settings(self.settings),
- deterministic_PRNG(),
- BuildContext(data, is_final=is_final) as context,
- ):
- # providers may throw in per_case_context_fn, and we'd like
- # `result` to still be set in these cases.
- result = None
- with data.provider.per_test_case_context_manager():
- # Run the test function once, via the executor hook.
- # In most cases this will delegate straight to `run(data)`.
- result = self.test_runner(data, run)
+ #
+ # NOTE: For compatibility with Python 3.9's LL(1) parser, this is written as
+ # three nested with-statements, instead of one compound statement.
+ with local_settings(self.settings):
+ with deterministic_PRNG():
+ with BuildContext(
+ data, is_final=is_final, wrapped_test=self.wrapped_test
+ ) as context:
+ # providers may throw in per_case_context_fn, and we'd like
+ # `result` to still be set in these cases.
+ result = None
+ with data.provider.per_test_case_context_manager():
+ # Run the test function once, via the executor hook.
+ # In most cases this will delegate straight to `run(data)`.
+ result = self.test_runner(data, run)
# If a failure was expected, it should have been raised already, so
# instead raise an appropriate diagnostic error.
@@ -1036,16 +1147,13 @@ class StateForActualGivenExecution:
):
report(
"Unreliable test timings! On an initial run, this "
- "test took %.2fms, which exceeded the deadline of "
- "%.2fms, but on a subsequent run it took %.2f ms, "
+ f"test took {exception.runtime.total_seconds() * 1000:.2f}ms, "
+ "which exceeded the deadline of "
+ f"{self.settings.deadline.total_seconds() * 1000:.2f}ms, but "
+ f"on a subsequent run it took {runtime_secs * 1000:.2f} ms, "
"which did not. If you expect this sort of "
"variability in your test timings, consider turning "
"deadlines off for this test by setting deadline=None."
- % (
- exception.runtime.total_seconds() * 1000,
- self.settings.deadline.total_seconds() * 1000,
- runtime_secs * 1000,
- )
)
else:
report("Failed to reproduce exception. Expected: \n" + traceback)
@@ -1064,11 +1172,11 @@ class StateForActualGivenExecution:
# _runner.interesting_examples - this is fine, as the context
# (i.e., immediate exception) is appended.
interesting_examples = [
- self._runner.interesting_examples[io]
- for io in err._interesting_origins
- if io in self._runner.interesting_examples
+ self._runner.interesting_examples[origin]
+ for origin in err._interesting_origins
+ if origin in self._runner.interesting_examples
]
- exceptions = [ie.expected_exception for ie in interesting_examples]
+ exceptions = [result.expected_exception for result in interesting_examples]
exceptions.append(context) # the immediate exception
return FlakyFailure(err.reason, exceptions)
@@ -1141,7 +1249,17 @@ class StateForActualGivenExecution:
else:
tb = e.__traceback__
filepath = traceback.extract_tb(tb)[-1][0]
- if is_hypothesis_file(filepath) and not isinstance(e, HypothesisException):
+ if (
+ is_hypothesis_file(filepath)
+ and not isinstance(e, HypothesisException)
+ # We expect backend authors to use the provider_conformance test
+ # to test their backends. If an error occurs there, it is probably
+ # from their backend, and we would like to treat it as a standard
+ # error, not a hypothesis-internal error.
+ and not filepath.endswith(
+ f"internal{os.sep}conjecture{os.sep}provider_conformance.py"
+ )
+ ):
raise
if data.frozen:
@@ -1197,19 +1315,20 @@ class StateForActualGivenExecution:
data._observability_args = {}
self._string_repr = "<backend failed to realize symbolic arguments>"
+ data.freeze()
tc = make_testcase(
- start_timestamp=self._start_timestamp,
- test_name_or_nodeid=self.test_identifier,
+ run_start=self._start_timestamp,
+ property=self.test_identifier,
data=data,
how_generated=f"during {phase} phase{backend_desc}",
- string_repr=self._string_repr,
+ representation=self._string_repr,
arguments=data._observability_args,
timing=self._timing_features,
coverage=tractable_coverage_report(trace) or None,
phase=phase,
backend_metadata=data.provider.observe_test_case(),
)
- deliver_json_blob(tc)
+ deliver_observation(tc)
for msg in data.provider.observe_information_messages(
lifetime="test_case"
):
@@ -1217,16 +1336,16 @@ class StateForActualGivenExecution:
self._timing_features = {}
def _deliver_information_message(
- self, *, type: str, title: str, content: Union[str, dict]
+ self, *, type: InfoObservationType, title: str, content: Union[str, dict]
) -> None:
- deliver_json_blob(
- {
- "type": type,
- "run_start": self._start_timestamp,
- "property": self.test_identifier,
- "title": title,
- "content": content,
- }
+ deliver_observation(
+ InfoObservation(
+ type=type,
+ run_start=self._start_timestamp,
+ property=self.test_identifier,
+ title=title,
+ content=content,
+ )
)
def run_engine(self):
@@ -1393,55 +1512,48 @@ class StateForActualGivenExecution:
# execute_once() will always raise either the expected error, or Flaky.
raise NotImplementedError("This should be unreachable")
finally:
+ ran_example.freeze()
# log our observability line for the final failing example
- tc = {
- "type": "test_case",
- "run_start": self._start_timestamp,
- "property": self.test_identifier,
- "status": "passed" if sys.exc_info()[0] else "failed",
- "status_reason": str(origin or "unexpected/flaky pass"),
- "representation": self._string_repr,
- "arguments": ran_example._observability_args,
- "how_generated": "minimal failing example",
- "features": {
- **{
- f"target:{k}".strip(":"): v
- for k, v in ran_example.target_observations.items()
- },
- **ran_example.events,
- },
- "timing": self._timing_features,
- "coverage": None, # Not recorded when we're replaying the MFE
- "metadata": {
- "traceback": tb,
- "predicates": dict(ran_example._observability_predicates),
- **_system_metadata(),
- },
- }
- deliver_json_blob(tc)
+ tc = make_testcase(
+ run_start=self._start_timestamp,
+ property=self.test_identifier,
+ data=ran_example,
+ how_generated="minimal failing example",
+ representation=self._string_repr,
+ arguments=ran_example._observability_args,
+ timing=self._timing_features,
+ coverage=None, # Not recorded when we're replaying the MFE
+ status="passed" if sys.exc_info()[0] else "failed",
+ status_reason=str(origin or "unexpected/flaky pass"),
+ metadata={"traceback": tb},
+ )
+ deliver_observation(tc)
# Whether or not replay actually raised the exception again, we want
# to print the reproduce_failure decorator for the failing example.
if self.settings.print_blob:
fragments.append(
"\nYou can reproduce this example by temporarily adding "
- "@reproduce_failure(%r, %r) as a decorator on your test case"
- % (__version__, encode_failure(falsifying_example.choices))
+ f"{reproduction_decorator(falsifying_example.choices)} "
+ "as a decorator on your test case"
)
- # Mostly useful for ``find`` and ensuring that objects that
- # hold on to a reference to ``data`` know that it's now been
- # finished and they can't draw more data from it.
- ran_example.freeze() # pragma: no branch
- # No branch is possible here because we never have an active exception.
+
_raise_to_user(
errors_to_report,
self.settings,
report_lines,
- verified_by=runner._verified_by,
+ # A backend might report a failure and then report verified afterwards,
+ # which is to be interpreted as "there are no more failures *other
+ # than what we already reported*". Do not report this as unsound.
+ unsound_backend=(
+ runner._verified_by
+ if runner._verified_by and not runner._backend_found_failure
+ else None
+ ),
)
def _raise_to_user(
- errors_to_report, settings, target_lines, trailer="", verified_by=None
+ errors_to_report, settings, target_lines, trailer="", *, unsound_backend=None
):
"""Helper function for attaching notes and grouping multiple errors."""
failing_prefix = "Falsifying example: "
@@ -1467,8 +1579,8 @@ def _raise_to_user(
for line in target_lines:
add_note(the_error_hypothesis_found, line)
- if verified_by:
- msg = f"backend={verified_by!r} claimed to verify this test passes - please send them a bug report!"
+ if unsound_backend:
+ msg = f"backend={unsound_backend!r} claimed to verify this test passes - please send them a bug report!"
add_note(err, msg)
raise the_error_hypothesis_found
@@ -1493,7 +1605,7 @@ def fake_subTest(self, msg=None, **__):
yield
+@dataclass
class HypothesisHandle:
"""This object is provided as the .hypothesis attribute on @given tests.
@@ -1509,9 +1621,9 @@ class HypothesisHandle:
information.
"""
- inner_test = attr.ib()
- _get_fuzz_target = attr.ib()
- _given_kwargs = attr.ib()
+ inner_test: Any
+ _get_fuzz_target: Any
+ _given_kwargs: Any
@property
def fuzz_one_input(
@@ -1566,10 +1678,98 @@ def given(
) -> Callable[
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]:
- """A decorator for turning a test function that accepts arguments into a
- randomized test.
+ """
+ The |@given| decorator turns a function into a Hypothesis test. This is the
+ main entry point to Hypothesis.
+
+ .. seealso::
+
+ See also the :doc:`/tutorial/introduction` tutorial, which introduces
+ defining Hypothesis tests with |@given|.
+
+ .. _given-arguments:
+
+ Arguments to ``@given``
+ -----------------------
+
+ Arguments to |@given| may be either positional or keyword arguments:
+
+ .. code-block:: python
+
+ @given(st.integers(), st.floats())
+ def test_one(x, y):
+ pass
+
+ @given(x=st.integers(), y=st.floats())
+ def test_two(x, y):
+ pass
+
+ If using keyword arguments, the arguments may appear in any order, as with
+ standard Python functions:
+
+ .. code-block:: python
+
+ # different order, but still equivalent to before
+ @given(y=st.floats(), x=st.integers())
+ def test(x, y):
+ assert isinstance(x, int)
+ assert isinstance(y, float)
+
+ If |@given| is provided fewer positional arguments than the decorated test,
+ the test arguments are filled in on the right side, leaving the leftmost
+ positional arguments unfilled:
+
+ .. code-block:: python
+
+ @given(st.integers(), st.floats())
+ def test(manual_string, y, z):
+ assert manual_string == "x"
+ assert isinstance(y, int)
+ assert isinstance(z, float)
- This is the main entry point to Hypothesis.
+ # `test` is now a callable which takes one argument `manual_string`
+
+ test("x")
+ # or equivalently:
+ test(manual_string="x")
+
+ The reason for this "from the right" behavior is to support using |@given|
+ with instance methods, by passing through ``self``:
+
+ .. code-block:: python
+
+ class MyTest(TestCase):
+ @given(st.integers())
+ def test(self, x):
+ assert isinstance(self, MyTest)
+ assert isinstance(x, int)
+
+ If (and only if) using keyword arguments, |@given| may be combined with
+ ``**kwargs`` or ``*args``:
+
+ .. code-block:: python
+
+ @given(x=integers(), y=integers())
+ def test(x, **kwargs):
+ assert "y" in kwargs
+
+ @given(x=integers(), y=integers())
+ def test(x, *args, **kwargs):
+ assert args == ()
+ assert "x" not in kwargs
+ assert "y" in kwargs
+
+ It is an error to:
+
+ * Mix positional and keyword arguments to |@given|.
+ * Use |@given| with a function that has a default value for an argument.
+ * Use |@given| with positional arguments with a function that uses ``*args``,
+ ``**kwargs``, or keyword-only arguments.
+
+ The function returned by given has all the same arguments as the original
+ test, minus those that are filled in by |@given|. See the :ref:`notes on
+ framework compatibility <framework-compatibility>` for how this interacts
+ with features of other testing libraries, such as :pypi:`pytest` fixtures.
"""
if currently_in_test_context():
@@ -1585,7 +1785,21 @@ def given(
if inspect.isclass(test):
# Provide a meaningful error to users, instead of exceptions from
# internals that assume we're dealing with a function.
- raise InvalidArgument("@given cannot be applied to a class.")
+ raise InvalidArgument("@given cannot be applied to a class")
+
+ if (
+ "_pytest" in sys.modules
+ and "_pytest.fixtures" in sys.modules
+ and (
+ tuple(map(int, sys.modules["_pytest"].__version__.split(".")[:2]))
+ >= (8, 4)
+ )
+ and isinstance(
+ test, sys.modules["_pytest.fixtures"].FixtureFunctionDefinition
+ )
+ ): # pragma: no cover # covered by pytest/test_fixtures, but not by cover/
+ raise InvalidArgument("@given cannot be applied to a pytest fixture")
+
given_arguments = tuple(_given_arguments)
given_kwargs = dict(_given_kwargs)
@@ -1730,10 +1944,9 @@ def given(
if expected_version != __version__:
raise InvalidArgument(
"Attempting to reproduce a failure from a different "
- "version of Hypothesis. This failure is from %s, but "
- "you are currently running %r. Please change your "
+ f"version of Hypothesis. This failure is from {expected_version}, but "
+ f"you are currently running {__version__!r}. Please change your "
"Hypothesis version to a matching one."
- % (expected_version, __version__)
)
try:
state.execute_once(
@@ -1888,7 +2101,12 @@ def given(
)
try:
state.execute_once(data)
- except (StopTest, UnsatisfiedAssumption):
+ status = Status.VALID
+ except StopTest:
+ status = data.status
+ return None
+ except UnsatisfiedAssumption:
+ status = Status.INVALID
return None
except BaseException:
known = minimal_failures.get(data.interesting_origin)
@@ -1899,7 +2117,26 @@ def given(
database_key, choices_to_bytes(data.choices)
)
minimal_failures[data.interesting_origin] = data.nodes
+ status = Status.INTERESTING
raise
+ finally:
+ if TESTCASE_CALLBACKS:
+ data.freeze()
+ tc = make_testcase(
+ run_start=state._start_timestamp,
+ property=state.test_identifier,
+ data=data,
+ how_generated="fuzz_one_input",
+ representation=state._string_repr,
+ arguments=data._observability_args,
+ timing=state._timing_features,
+ coverage=None,
+ status=status,
+ backend_metadata=data.provider.observe_test_case(),
+ )
+ deliver_observation(tc)
+ state._timing_features = {}
+
assert isinstance(data.provider, BytestringProvider)
return bytes(data.provider.drawn)
diff --git a/contrib/python/hypothesis/py3/hypothesis/database.py b/contrib/python/hypothesis/py3/hypothesis/database.py
index 53d5e14684f..5772e6c200b 100644
--- a/contrib/python/hypothesis/py3/hypothesis/database.py
+++ b/contrib/python/hypothesis/py3/hypothesis/database.py
@@ -9,6 +9,7 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import abc
+import errno
import json
import os
import struct
@@ -145,10 +146,62 @@ if "sphinx" in sys.modules:
class ExampleDatabase(metaclass=_EDMeta):
- """An abstract base class for storing examples in Hypothesis' internal format.
+ """
+ A Hypothesis database, for use in |settings.database|.
+
+ Hypothesis automatically saves failures to the database set in
+ |settings.database|. The next time the test is run, Hypothesis will replay
+ any failures from the database in |settings.database| for that test (in
+ |Phase.reuse|).
+
+ The database is best thought of as a cache that you never need to invalidate.
+ Entries may be transparently dropped when upgrading your Hypothesis version
+ or changing your test. Do not rely on the database for correctness; to ensure
+ Hypothesis always tries an input, use |@example|.
+
+ A Hypothesis database is a simple mapping of bytes to sets of bytes. Hypothesis
+ provides several concrete database subclasses. To write your own database class,
+ see :doc:`/how-to/custom-database`.
+
+ Change listening
+ ----------------
+
+ An optional extension to |ExampleDatabase| is change listening. On databases
+ which support change listening, calling |ExampleDatabase.add_listener| adds
+ a function as a change listener, which will be called whenever a value is
+ added, deleted, or moved inside the database. See |ExampleDatabase.add_listener|
+ for details.
+
+ All databases in Hypothesis support change listening. Custom database classes
+ are not required to support change listening, though they will not be compatible
+ with features that require change listening until they do so.
+
+ .. note::
+
+ While no Hypothesis features currently require change listening, change
+ listening is required by `HypoFuzz <https://hypofuzz.com/>`_.
+
+ Database methods
+ ----------------
+
+ Required methods:
+
+ * |ExampleDatabase.save|
+ * |ExampleDatabase.fetch|
+ * |ExampleDatabase.delete|
- An ExampleDatabase maps each ``bytes`` key to many distinct ``bytes``
- values, like a ``Mapping[bytes, set[bytes]]``.
+ Optional methods:
+
+ * |ExampleDatabase.move|
+
+ Change listening methods:
+
+ * |ExampleDatabase.add_listener|
+ * |ExampleDatabase.remove_listener|
+ * |ExampleDatabase.clear_listeners|
+ * |ExampleDatabase._start_listening|
+ * |ExampleDatabase._stop_listening|
+ * |ExampleDatabase._broadcast_change|
"""
def __init__(self) -> None:
@@ -158,7 +211,7 @@ class ExampleDatabase(metaclass=_EDMeta):
def save(self, key: bytes, value: bytes) -> None:
"""Save ``value`` under ``key``.
- If this value is already present for this key, silently do nothing.
+ If ``value`` is already present in ``key``, silently do nothing.
"""
raise NotImplementedError(f"{type(self).__name__}.save")
@@ -169,16 +222,18 @@ class ExampleDatabase(metaclass=_EDMeta):
@abc.abstractmethod
def delete(self, key: bytes, value: bytes) -> None:
- """Remove this value from this key.
+ """Remove ``value`` from ``key``.
- If this value is not present, silently do nothing.
+ If ``value`` is not present in ``key``, silently do nothing.
"""
raise NotImplementedError(f"{type(self).__name__}.delete")
def move(self, src: bytes, dest: bytes, value: bytes) -> None:
- """Move ``value`` from key ``src`` to key ``dest``. Equivalent to
- ``delete(src, value)`` followed by ``save(src, value)``, but may
- have a more efficient implementation.
+ """
+ Move ``value`` from key ``src`` to key ``dest``.
+
+ Equivalent to ``delete(src, value)`` followed by ``save(src, value)``,
+ but may have a more efficient implementation.
Note that ``value`` will be inserted at ``dest`` regardless of whether
it is currently present at ``src``.
@@ -190,7 +245,24 @@ class ExampleDatabase(metaclass=_EDMeta):
self.save(dest, value)
def add_listener(self, f: ListenerT, /) -> None:
- """Add a change listener."""
+ """
+ Add a change listener. ``f`` will be called whenever a value is saved,
+ deleted, or moved in the database.
+
+ ``f`` can be called with two different event values:
+
+ * ``("save", (key, value))``
+ * ``("delete", (key, value))``
+
+ where ``key`` and ``value`` are both ``bytes``.
+
+ There is no ``move`` event. Instead, a move is broadcasted as a
+ ``delete`` event followed by a ``save`` event.
+
+ For the ``delete`` event, ``value`` may be ``None``. This might occur if
+ the database knows that a deletion has occurred in ``key``, but does not
+ know what value was deleted.
+ """
had_listeners = bool(self._listeners)
self._listeners.append(f)
if not had_listeners:
@@ -198,8 +270,9 @@ class ExampleDatabase(metaclass=_EDMeta):
def remove_listener(self, f: ListenerT, /) -> None:
"""
- Remove a change listener. If the listener is not present, silently do
- nothing.
+ Removes ``f`` from the list of change listeners.
+
+ If ``f`` is not in the list of change listeners, silently do nothing.
"""
if f not in self._listeners:
return
@@ -217,16 +290,20 @@ class ExampleDatabase(metaclass=_EDMeta):
def _broadcast_change(self, event: ListenerEventT) -> None:
"""
Called when a value has been either added to or deleted from a key in
- the underlying database store. event_type is one of "save" or "delete".
+ the underlying database store. The possible values for ``event`` are:
- ``value`` may be ``None`` for ``event_type == "delete"``, which indicates
- we don't know what value was deleted from the database.
+ * ``("save", (key, value))``
+ * ``("delete", (key, value))``
- Note that you should not assume you are the only reference to the underlying
- database store. For example, if two DirectoryBasedExampleDatabase reference
- the same directory, _broadcast_change should be called whenever a file is
- added or removed from the directory, even if that database was not responsible
- for changing the file.
+ ``value`` may be ``None`` for the ``delete`` event, indicating we know
+ that some value was deleted under this key, but not its exact value.
+
+ Note that you should not assume your instance is the only reference to
+ the underlying database store. For example, if two instances of
+ |DirectoryBasedExampleDatabase| reference the same directory,
+ _broadcast_change should be called whenever a file is added or removed
+ from the directory, even if that database was not responsible for
+ changing the file.
"""
for listener in self._listeners:
listener(event)
@@ -237,9 +314,10 @@ class ExampleDatabase(metaclass=_EDMeta):
have any change listeners. Intended to allow databases to wait to start
expensive listening operations until necessary.
- _start_listening and _stop_listening are guaranteed to alternate, so you
- do not need to handle the case of multiple consecutive _start_listening
- calls without an intermediate _stop_listening call.
+ ``_start_listening`` and ``_stop_listening`` are guaranteed to alternate,
+ so you do not need to handle the case of multiple consecutive
+ ``_start_listening`` calls without an intermediate ``_stop_listening``
+ call.
"""
warnings.warn(
f"{self.__class__} does not support listening for changes",
@@ -251,9 +329,10 @@ class ExampleDatabase(metaclass=_EDMeta):
"""
Called whenever no change listeners remain on the database.
- _stop_listening and _start_listening are guaranteed to alternate, so you
- do not need to handle the case of multiple consecutive _stop_listening
- calls without an intermediate _start_listening call.
+ ``_stop_listening`` and ``_start_listening`` are guaranteed to alternate,
+ so you do not need to handle the case of multiple consecutive
+ ``_stop_listening`` calls without an intermediate ``_start_listening``
+ call.
"""
warnings.warn(
f"{self.__class__} does not support stopping listening for changes",
@@ -263,7 +342,8 @@ class ExampleDatabase(metaclass=_EDMeta):
class InMemoryExampleDatabase(ExampleDatabase):
- """A non-persistent example database, implemented in terms of a dict of sets.
+ """A non-persistent example database, implemented in terms of an in-memory
+ dictionary.
This can be useful if you call a test function several times in a single
session, or for testing other database implementations, but because it
@@ -277,6 +357,9 @@ class InMemoryExampleDatabase(ExampleDatabase):
def __repr__(self) -> str:
return f"InMemoryExampleDatabase({self.data!r})"
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, InMemoryExampleDatabase) and self.data is other.data
+
def fetch(self, key: bytes) -> Iterable[bytes]:
yield from self.data.get(key, ())
@@ -317,7 +400,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
Each test corresponds to a directory, and each example to a file within that
directory. While the contents are fairly opaque, a
- ``DirectoryBasedExampleDatabase`` can be shared by checking the directory
+ |DirectoryBasedExampleDatabase| can be shared by checking the directory
into version control, for example with the following ``.gitignore``::
# Ignore files cached by Hypothesis...
@@ -327,8 +410,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
Note however that this only makes sense if you also pin to an exact version of
Hypothesis, and we would usually recommend implementing a shared database with
- a network datastore - see :class:`~hypothesis.database.ExampleDatabase`, and
- the :class:`~hypothesis.database.MultiplexedDatabase` helper.
+ a network datastore - see |ExampleDatabase|, and the |MultiplexedDatabase| helper.
"""
# we keep a database entry of the full values of all the database keys.
@@ -345,6 +427,11 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
def __repr__(self) -> str:
return f"DirectoryBasedExampleDatabase({self.path!r})"
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, DirectoryBasedExampleDatabase) and self.path == other.path
+ )
+
def _key_path(self, key: bytes) -> Path:
try:
return self.keypaths[key]
@@ -360,11 +447,16 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
kp = self._key_path(key)
if not kp.is_dir():
return
- for path in os.listdir(kp):
- try:
- yield (kp / path).read_bytes()
- except OSError:
- pass
+
+ try:
+ for path in os.listdir(kp):
+ try:
+ yield (kp / path).read_bytes()
+ except OSError:
+ pass
+ except OSError: # pragma: no cover
+ # the `kp` directory might have been deleted in the meantime
+ pass
def save(self, key: bytes, value: bytes) -> None:
key_path = self._key_path(key)
@@ -390,7 +482,14 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
os.close(fd)
try:
tmppath.rename(path)
- except OSError: # pragma: no cover
+ except OSError as err: # pragma: no cover
+ if err.errno == errno.EXDEV:
+ # Can't rename across filesystem boundaries, see e.g.
+ # https://github.com/HypothesisWorks/hypothesis/issues/4335
+ try:
+ path.write_bytes(tmppath.read_bytes())
+ except OSError:
+ pass
tmppath.unlink()
assert not tmppath.exists()
except OSError: # pragma: no cover
@@ -418,7 +517,19 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
try:
self._value_path(key, value).unlink()
except OSError:
+ return
+
+ # try deleting the key dir, which will only succeed if the dir is empty
+ # (i.e. ``value`` was the last value in this key).
+ try:
+ self._key_path(key).rmdir()
+ except OSError:
pass
+ else:
+ # if the deletion succeeded, also delete this key entry from metakeys.
+ # (if this key happens to be the metakey itself, this deletion will
+ # fail; that's ok and faster than checking for this rare case.)
+ self.delete(self._metakeys_name, key)
def _start_listening(self) -> None:
try:
@@ -461,7 +572,13 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
key_hash = value_path.parent.name
if key_hash == _metakeys_hash:
- hash_to_key[value_path.name] = value_path.read_bytes()
+ try:
+ hash_to_key[value_path.name] = value_path.read_bytes()
+ except OSError: # pragma: no cover
+ # this might occur if all the values in a key have been
+ # deleted and DirectoryBasedExampleDatabase removes its
+ # metakeys entry (which is `value_path` here`).
+ pass
return
key = hash_to_key.get(key_hash)
@@ -511,6 +628,12 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
_broadcast_change(("delete", (k1, value)))
_broadcast_change(("save", (k2, value)))
+ # If we add a listener to a DirectoryBasedExampleDatabase whose database
+ # directory doesn't yet exist, the watchdog observer will not fire any
+ # events, even after the directory gets created.
+ #
+ # Ensure the directory exists before starting the observer.
+ self.path.mkdir(exist_ok=True, parents=True)
self._observer = Observer()
self._observer.schedule(
Handler(),
@@ -548,6 +671,9 @@ class ReadOnlyDatabase(ExampleDatabase):
def __repr__(self) -> str:
return f"ReadOnlyDatabase({self._wrapped!r})"
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, ReadOnlyDatabase) and self._wrapped == other._wrapped
+
def fetch(self, key: bytes) -> Iterable[bytes]:
yield from self._wrapped.fetch(key)
@@ -599,6 +725,11 @@ class MultiplexedDatabase(ExampleDatabase):
def __repr__(self) -> str:
return "MultiplexedDatabase({})".format(", ".join(map(repr, self._wrapped)))
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, MultiplexedDatabase) and self._wrapped == other._wrapped
+ )
+
def fetch(self, key: bytes) -> Iterable[bytes]:
seen = set()
for db in self._wrapped:
@@ -746,6 +877,15 @@ class GitHubArtifactDatabase(ExampleDatabase):
f"repo={self.repo!r}, artifact_name={self.artifact_name!r})"
)
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, GitHubArtifactDatabase)
+ and self.owner == other.owner
+ and self.repo == other.repo
+ and self.artifact_name == other.artifact_name
+ and self.path == other.path
+ )
+
def _prepare_for_io(self) -> None:
assert self._artifact is not None, "Artifact not loaded."
@@ -970,15 +1110,22 @@ class BackgroundWriteDatabase(ExampleDatabase):
super().__init__()
self._db = db
self._queue: Queue[tuple[str, tuple[bytes, ...]]] = Queue()
- self._thread = Thread(target=self._worker, daemon=True)
- self._thread.start()
- # avoid an unbounded timeout during gc. 0.1 should be plenty for most
- # use cases.
- weakref.finalize(self, self._join, 0.1)
+ self._thread: Optional[Thread] = None
+
+ def _ensure_thread(self):
+ if self._thread is None:
+ self._thread = Thread(target=self._worker, daemon=True)
+ self._thread.start()
+ # avoid an unbounded timeout during gc. 0.1 should be plenty for most
+ # use cases.
+ weakref.finalize(self, self._join, 0.1)
def __repr__(self) -> str:
return f"BackgroundWriteDatabase({self._db!r})"
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, BackgroundWriteDatabase) and self._db == other._db
+
def _worker(self) -> None:
while True:
method, args = self._queue.get()
@@ -996,12 +1143,15 @@ class BackgroundWriteDatabase(ExampleDatabase):
return self._db.fetch(key)
def save(self, key: bytes, value: bytes) -> None:
+ self._ensure_thread()
self._queue.put(("save", (key, value)))
def delete(self, key: bytes, value: bytes) -> None:
+ self._ensure_thread()
self._queue.put(("delete", (key, value)))
def move(self, src: bytes, dest: bytes, value: bytes) -> None:
+ self._ensure_thread()
self._queue.put(("move", (src, dest, value)))
def _start_listening(self) -> None:
@@ -1050,8 +1200,8 @@ def _unpack_uleb128(buffer: bytes) -> tuple[int, int]:
return (i + 1, value)
-def choices_to_bytes(ir: Iterable[ChoiceT], /) -> bytes:
- """Serialize a list of IR elements to a bytestring. Inverts choices_from_bytes."""
+def choices_to_bytes(choices: Iterable[ChoiceT], /) -> bytes:
+ """Serialize a list of choices to a bytestring. Inverts choices_from_bytes."""
# We use a custom serialization format for this, which might seem crazy - but our
# data is a flat sequence of elements, and standard tools like protobuf or msgpack
# don't deal well with e.g. nonstandard bit-pattern-NaNs, or invalid-utf8 unicode.
@@ -1059,33 +1209,33 @@ def choices_to_bytes(ir: Iterable[ChoiceT], /) -> bytes:
# We simply encode each element with a metadata byte, if needed a uint16 size, and
# then the payload bytes. For booleans, the payload is inlined into the metadata.
parts = []
- for elem in ir:
- if isinstance(elem, bool):
+ for choice in choices:
+ if isinstance(choice, bool):
# `000_0000v` - tag zero, low bit payload.
- parts.append(b"\1" if elem else b"\0")
+ parts.append(b"\1" if choice else b"\0")
continue
# `tag_ssss [uint16 size?] [payload]`
- if isinstance(elem, float):
+ if isinstance(choice, float):
tag = 1 << 5
- elem = struct.pack("!d", elem)
- elif isinstance(elem, int):
+ choice = struct.pack("!d", choice)
+ elif isinstance(choice, int):
tag = 2 << 5
- elem = elem.to_bytes(1 + elem.bit_length() // 8, "big", signed=True)
- elif isinstance(elem, bytes):
+ choice = choice.to_bytes(1 + choice.bit_length() // 8, "big", signed=True)
+ elif isinstance(choice, bytes):
tag = 3 << 5
else:
- assert isinstance(elem, str)
+ assert isinstance(choice, str)
tag = 4 << 5
- elem = elem.encode(errors="surrogatepass")
+ choice = choice.encode(errors="surrogatepass")
- size = len(elem)
+ size = len(choice)
if size < 0b11111:
parts.append((tag | size).to_bytes(1, "big"))
else:
parts.append((tag | 0b11111).to_bytes(1, "big"))
parts.append(_pack_uleb128(size))
- parts.append(elem)
+ parts.append(choice)
return b"".join(parts)
diff --git a/contrib/python/hypothesis/py3/hypothesis/errors.py b/contrib/python/hypothesis/py3/hypothesis/errors.py
index 18e71f5928d..4c64ca001f6 100644
--- a/contrib/python/hypothesis/py3/hypothesis/errors.py
+++ b/contrib/python/hypothesis/py3/hypothesis/errors.py
@@ -122,6 +122,23 @@ class FlakyFailure(ExceptionGroup, Flaky):
group[i] = err
return ExceptionGroup.__new__(cls, msg, group)
+ # defining `derive` is required for `split` to return an instance of FlakyFailure
+ # instead of ExceptionGroup. See https://github.com/python/cpython/issues/119287
+ # and https://docs.python.org/3/library/exceptions.html#BaseExceptionGroup.derive
+ def derive(self, excs):
+ return type(self)(self.message, excs)
+
+
+class FlakyBackendFailure(FlakyFailure):
+ """
+ A failure was reported by an :ref:`alternative backend <alternative-backends>`,
+ but this failure did not reproduce when replayed under the Hypothesis backend.
+
+ When an alternative backend reports a failure, Hypothesis first replays it
+ under the standard Hypothesis backend to check for flakiness. If the failure
+ does not reproduce, Hypothesis raises this exception.
+ """
+
class InvalidArgument(_Trimmable, TypeError):
"""Used to indicate that the arguments to a Hypothesis function were in
@@ -202,12 +219,15 @@ def __getattr__(name: str) -> Any:
class DeadlineExceeded(_Trimmable):
- """Raised when an individual test body has taken too long to run."""
+ """
+ Raised when an input takes too long to run, relative to the |settings.deadline|
+ setting.
+ """
def __init__(self, runtime: timedelta, deadline: timedelta) -> None:
super().__init__(
- "Test took %.2fms, which exceeds the deadline of %.2fms"
- % (runtime.total_seconds() * 1000, deadline.total_seconds() * 1000)
+ f"Test took {runtime.total_seconds() * 1000:.2f}ms, which exceeds "
+ f"the deadline of {deadline.total_seconds() * 1000:.2f}ms"
)
self.runtime = runtime
self.deadline = deadline
@@ -252,16 +272,15 @@ CannotProceedScopeT = Literal["verified", "exhausted", "discard_test_case", "oth
class BackendCannotProceed(HypothesisException):
- """UNSTABLE API
-
- Raised by alternative backends when the PrimitiveProvider cannot proceed.
- This is expected to occur inside one of the `.draw_*()` methods, or for
- symbolic execution perhaps in `.realize(...)`.
+ """
+ Raised by alternative backends when a |PrimitiveProvider| cannot proceed.
+ This is expected to occur inside one of the ``.draw_*()`` methods, or for
+ symbolic execution perhaps in |PrimitiveProvider.realize|.
- The optional `scope` argument can enable smarter integration:
+ The optional ``scope`` argument can enable smarter integration:
verified:
- Do not request further test cases from this backend. We _may_
+ Do not request further test cases from this backend. We *may*
generate more test cases with other backends; if one fails then
Hypothesis will report unsound verification in the backend too.
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py
index 78e821a2473..0ff33b1c815 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py
@@ -13,6 +13,7 @@ from typing import NamedTuple, Optional, Union
from hypothesis import assume, strategies as st
from hypothesis.errors import InvalidArgument
+from hypothesis.internal.compat import EllipsisType
from hypothesis.internal.conjecture.utils import _calc_p_continue
from hypothesis.internal.coverage import check_function
from hypothesis.internal.validation import check_type, check_valid_interval
@@ -37,8 +38,7 @@ __all__ = [
Shape = tuple[int, ...]
-# We silence flake8 here because it disagrees with mypy about `ellipsis` (`type(...)`)
-BasicIndex = tuple[Union[int, slice, None, "ellipsis"], ...] # noqa: F821
+BasicIndex = tuple[Union[int, slice, None, EllipsisType], ...]
class BroadcastableShapes(NamedTuple):
@@ -328,15 +328,15 @@ def _hypothesis_parse_gufunc_signature(signature):
raise InvalidArgument(
f"Got dimension {name!r}, but handling of frozen optional dimensions "
"is ambiguous. If you known how this should work, please "
- "contact us to get this fixed and documented ({signature=})."
+ f"contact us to get this fixed and documented ({signature=})."
)
except ValueError:
names_in = {n.strip("?") for shp in input_shapes for n in shp}
names_out = {n.strip("?") for n in result_shape}
if name.strip("?") in (names_out - names_in):
raise InvalidArgument(
- "The {name!r} dimension only appears in the output shape, and is "
- "not frozen, so the size is not determined ({signature=})."
+ f"The {name!r} dimension only appears in the output shape, and is "
+ f"not frozen, so the size is not determined ({signature=})."
) from None
return _GUfuncSig(input_shapes=input_shapes, result_shape=result_shape)
@@ -632,6 +632,7 @@ class BasicIndexStrategy(st.SearchStrategy):
allow_newaxis,
allow_fewer_indices_than_dims,
):
+ super().__init__()
self.shape = shape
self.min_dims = min_dims
self.max_dims = max_dims
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py
index 0532a354a9b..4027d90df42 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py
@@ -26,9 +26,11 @@ import re
import sys
import types
from ast import literal_eval
+from collections.abc import Sequence
from contextlib import suppress
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
+from typing import Any, Optional
import libcst as cst
from libcst import matchers as m
@@ -43,11 +45,11 @@ try:
except ImportError:
black = None # type: ignore
-HEADER = f"""\
+HEADER = """\
From HEAD Mon Sep 17 00:00:00 2001
-From: Hypothesis {__version__} <[email protected]>
-Date: {{when:%a, %d %b %Y %H:%M:%S}}
-Subject: [PATCH] {{msg}}
+From: {author}
+Date: {when:%a, %d %b %Y %H:%M:%S}
+Subject: [PATCH] {msg}
---
"""
@@ -56,7 +58,7 @@ _space_only_re = re.compile("^ +$", re.MULTILINE)
_leading_space_re = re.compile("(^[ ]*)(?:[^ \n])", re.MULTILINE)
-def dedent(text):
+def dedent(text: str) -> tuple[str, str]:
# Simplified textwrap.dedent, for valid Python source code only
text = _space_only_re.sub("", text)
prefix = min(_leading_space_re.findall(text), key=len)
@@ -70,7 +72,14 @@ def indent(text: str, prefix: str) -> str:
class AddExamplesCodemod(VisitorBasedCodemodCommand):
DESCRIPTION = "Add explicit examples to failing tests."
- def __init__(self, context, fn_examples, strip_via=(), dec="example", width=88):
+ def __init__(
+ self,
+ context: CodemodContext,
+ fn_examples: dict[str, list[tuple[cst.Call, str]]],
+ strip_via: tuple[str, ...] = (),
+ decorator: str = "example",
+ width: int = 88,
+ ):
"""Add @example() decorator(s) for failing test(s).
`code` is the source code of the module where the test functions are defined.
@@ -79,9 +88,11 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand):
assert fn_examples, "This codemod does nothing without fn_examples."
super().__init__(context)
- self.decorator_func = cst.parse_expression(dec)
+ self.decorator_func = cst.parse_expression(decorator)
self.line_length = width
- value_in_strip_via = m.MatchIfTrue(lambda x: literal_eval(x.value) in strip_via)
+ value_in_strip_via: Any = m.MatchIfTrue(
+ lambda x: literal_eval(x.value) in strip_via
+ )
self.strip_matching = m.Call(
m.Attribute(m.Call(), m.Name("via")),
[m.Arg(m.SimpleString() & value_in_strip_via)],
@@ -89,11 +100,17 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand):
# Codemod the failing examples to Call nodes usable as decorators
self.fn_examples = {
- k: tuple(d for x in nodes if (d := self.__call_node_to_example_dec(*x)))
+ k: tuple(
+ d
+ for (node, via) in nodes
+ if (d := self.__call_node_to_example_dec(node, via))
+ )
for k, nodes in fn_examples.items()
}
- def __call_node_to_example_dec(self, node, via):
+ def __call_node_to_example_dec(
+ self, node: cst.Call, via: str
+ ) -> Optional[cst.Decorator]:
# If we have black installed, remove trailing comma, _unless_ there's a comment
node = node.with_changes(
func=self.decorator_func,
@@ -112,7 +129,7 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand):
else node.args
),
)
- via = cst.Call(
+ via: cst.BaseExpression = cst.Call(
func=cst.Attribute(node, cst.Name("via")),
args=[cst.Arg(cst.SimpleString(repr(via)))],
)
@@ -127,7 +144,9 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand):
via = cst.parse_expression(pretty.strip())
return cst.Decorator(via)
- def leave_FunctionDef(self, _, updated_node):
+ def leave_FunctionDef(
+ self, _original_node: cst.FunctionDef, updated_node: cst.FunctionDef
+ ) -> cst.FunctionDef:
return updated_node.with_changes(
# TODO: improve logic for where in the list to insert this decorator
decorators=tuple(
@@ -140,30 +159,59 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand):
)
-def get_patch_for(func, failing_examples, *, strip_via=()):
- # Skip this if we're unable to find the location or source of this function.
+def get_patch_for(
+ func: Any,
+ examples: Sequence[tuple[str, str]],
+ *,
+ strip_via: tuple[str, ...] = (),
+) -> Optional[tuple[str, str, str]]:
+ # Skip this if we're unable to find the location of this function.
try:
module = sys.modules[func.__module__]
- fname = Path(module.__file__).relative_to(Path.cwd())
- before = inspect.getsource(func)
+ file_path = Path(module.__file__) # type: ignore
except Exception:
return None
+ fname = (
+ file_path.relative_to(Path.cwd())
+ if file_path.is_relative_to(Path.cwd())
+ else file_path
+ )
+ patch = _get_patch_for(
+ func, examples, strip_via=strip_via, namespace=module.__dict__
+ )
+ if patch is None:
+ return None
+
+ (before, after) = patch
+ return (str(fname), before, after)
+
+
+# split out for easier testing of patches in hypofuzz, where the function to
+# apply the patch to may not be loaded in sys.modules.
+def _get_patch_for(
+ func: Any,
+ examples: Sequence[tuple[str, str]],
+ *,
+ strip_via: tuple[str, ...] = (),
+ namespace: dict[str, Any],
+) -> Optional[tuple[str, str]]:
+ try:
+ before = inspect.getsource(func)
+ except Exception: # pragma: no cover
+ return None
+
modules_in_test_scope = sorted(
- (
- (k, v)
- for (k, v) in module.__dict__.items()
- if isinstance(v, types.ModuleType)
- ),
+ ((k, v) for (k, v) in namespace.items() if isinstance(v, types.ModuleType)),
key=lambda kv: len(kv[1].__name__),
)
# The printed examples might include object reprs which are invalid syntax,
# so we parse here and skip over those. If _none_ are valid, there's no patch.
- call_nodes = []
- for ex, via in set(failing_examples):
+ call_nodes: list[tuple[cst.Call, str]] = []
+ for ex, via in set(examples):
with suppress(Exception):
- node = cst.parse_module(ex)
+ node: Any = cst.parse_module(ex)
the_call = node.body[0].body[0].value
assert isinstance(the_call, cst.Call), the_call
# Check for st.data(), which doesn't support explicit examples
@@ -180,7 +228,7 @@ def get_patch_for(func, failing_examples, *, strip_via=()):
isinstance(anode, ast.Name)
and isinstance(anode.ctx, ast.Load)
and anode.id not in names
- and anode.id not in module.__dict__
+ and anode.id not in namespace
):
for k, v in modules_in_test_scope:
if anode.id in v.__dict__:
@@ -194,14 +242,15 @@ def get_patch_for(func, failing_examples, *, strip_via=()):
with suppress(Exception):
wrapper = cst.metadata.MetadataWrapper(node)
kwarg_names = {
- a.keyword for a in m.findall(wrapper, m.Arg(keyword=m.Name()))
+ node.keyword # type: ignore
+ for node in m.findall(wrapper, m.Arg(keyword=m.Name()))
}
node = m.replace(
wrapper,
m.Name(value=m.MatchIfTrue(names.__contains__))
& m.MatchMetadata(ExpressionContextProvider, ExpressionContext.LOAD)
- & m.MatchIfTrue(lambda n, k=kwarg_names: n not in k),
- replacement=lambda node, _, ns=names: ns[node.value],
+ & m.MatchIfTrue(lambda n, k=kwarg_names: n not in k), # type: ignore
+ replacement=lambda node, _, ns=names: ns[node.value], # type: ignore
)
node = node.body[0].body[0].value
assert isinstance(node, cst.Call), node
@@ -211,8 +260,8 @@ def get_patch_for(func, failing_examples, *, strip_via=()):
return None
if (
- module.__dict__.get("hypothesis") is sys.modules["hypothesis"]
- and "given" not in module.__dict__ # more reliably present than `example`
+ namespace.get("hypothesis") is sys.modules["hypothesis"]
+ and "given" not in namespace # more reliably present than `example`
):
decorator_func = "hypothesis.example"
else:
@@ -229,22 +278,28 @@ def get_patch_for(func, failing_examples, *, strip_via=()):
CodemodContext(),
fn_examples={func.__name__: call_nodes},
strip_via=strip_via,
- dec=decorator_func,
+ decorator=decorator_func,
width=88 - len(prefix), # to match Black's default formatting
).transform_module(node)
- return (str(fname), before, indent(after.code, prefix=prefix))
+ return (before, indent(after.code, prefix=prefix))
-def make_patch(triples, *, msg="Hypothesis: add explicit examples", when=None):
+def make_patch(
+ triples: Sequence[tuple[str, str, str]],
+ *,
+ msg: str = "Hypothesis: add explicit examples",
+ when: Optional[datetime] = None,
+ author: str = f"Hypothesis {__version__} <[email protected]>",
+) -> str:
"""Create a patch for (fname, before, after) triples."""
assert triples, "attempted to create empty patch"
when = when or datetime.now(tz=timezone.utc)
- by_fname = {}
+ by_fname: dict[Path, list[tuple[str, str]]] = {}
for fname, before, after in triples:
by_fname.setdefault(Path(fname), []).append((before, after))
- diffs = [HEADER.format(msg=msg, when=when)]
+ diffs = [HEADER.format(msg=msg, when=when, author=author)]
for fname, changes in sorted(by_fname.items()):
source_before = source_after = fname.read_text(encoding="utf-8")
for before, after in changes:
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py
index 8c192c0069c..30ca2b46527 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py
@@ -313,6 +313,7 @@ class ArrayStrategy(st.SearchStrategy):
def __init__(
self, *, xp, api_version, elements_strategy, dtype, shape, fill, unique
):
+ super().__init__()
self.xp = xp
self.elements_strategy = elements_strategy
self.dtype = dtype
@@ -1034,24 +1035,24 @@ def make_strategies_namespace(
f_args += f", api_version='{self.api_version}'"
return f"make_strategies_namespace({f_args})"
- kwargs = dict(
- name=xp.__name__,
- api_version=api_version,
- from_dtype=from_dtype,
- arrays=arrays,
- array_shapes=array_shapes,
- scalar_dtypes=scalar_dtypes,
- boolean_dtypes=boolean_dtypes,
- real_dtypes=real_dtypes,
- numeric_dtypes=numeric_dtypes,
- integer_dtypes=integer_dtypes,
- unsigned_integer_dtypes=unsigned_integer_dtypes,
- floating_dtypes=floating_dtypes,
- valid_tuple_axes=valid_tuple_axes,
- broadcastable_shapes=broadcastable_shapes,
- mutually_broadcastable_shapes=mutually_broadcastable_shapes,
- indices=indices,
- )
+ kwargs = {
+ "name": xp.__name__,
+ "api_version": api_version,
+ "from_dtype": from_dtype,
+ "arrays": arrays,
+ "array_shapes": array_shapes,
+ "scalar_dtypes": scalar_dtypes,
+ "boolean_dtypes": boolean_dtypes,
+ "real_dtypes": real_dtypes,
+ "numeric_dtypes": numeric_dtypes,
+ "integer_dtypes": integer_dtypes,
+ "unsigned_integer_dtypes": unsigned_integer_dtypes,
+ "floating_dtypes": floating_dtypes,
+ "valid_tuple_axes": valid_tuple_axes,
+ "broadcastable_shapes": broadcastable_shapes,
+ "mutually_broadcastable_shapes": mutually_broadcastable_shapes,
+ "indices": indices,
+ }
if api_version > "2021.12":
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py b/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py
index 14d1003666c..a41f7f48241 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/dateutil.py
@@ -9,10 +9,6 @@
# obtain one at https://mozilla.org/MPL/2.0/.
"""
---------------------
-hypothesis[dateutil]
---------------------
-
This module provides :pypi:`dateutil <python-dateutil>` timezones.
You can use this strategy to make :func:`~hypothesis.strategies.datetimes`
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/__init__.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/__init__.py
index cb49b7a5096..465eaf3fa24 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/django/__init__.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/__init__.py
@@ -11,6 +11,7 @@
from hypothesis.extra.django._fields import from_field, register_field_strategy
from hypothesis.extra.django._impl import (
LiveServerTestCase,
+ SimpleTestCase,
StaticLiveServerTestCase,
TestCase,
TransactionTestCase,
@@ -20,6 +21,7 @@ from hypothesis.extra.django._impl import (
__all__ = [
"LiveServerTestCase",
+ "SimpleTestCase",
"StaticLiveServerTestCase",
"TestCase",
"TransactionTestCase",
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py
index 36b43dffe61..357a0e6a576 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py
@@ -17,7 +17,7 @@ from typing import Any, Callable, TypeVar, Union
import django
from django import forms as df
-from django.contrib.auth.forms import UsernameField
+from django.conf import settings
from django.core.validators import (
validate_ipv4_address,
validate_ipv6_address,
@@ -231,7 +231,6 @@ def _for_binary(field):
@register_for(dm.TextField)
@register_for(df.CharField)
@register_for(df.RegexField)
-@register_for(UsernameField)
def _for_text(field):
# We can infer a vastly more precise strategy by considering the
# validators as well as the field type. This is a minimal proof of
@@ -262,6 +261,12 @@ def _for_text(field):
return strategy
+if "django.contrib.auth" in settings.INSTALLED_APPS:
+ from django.contrib.auth.forms import UsernameField
+
+ register_for(UsernameField)(_for_text)
+
+
@register_for(df.BooleanField)
def _for_form_boolean(field):
if field.required:
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py
index b1aa59744c6..a3be6fccb1d 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py
@@ -8,10 +8,9 @@
# 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 sys
import unittest
from functools import partial
-from typing import TYPE_CHECKING, Optional, TypeVar, Union
+from typing import Optional, TypeVar, Union
from django import forms as df, test as dt
from django.contrib.staticfiles import testing as dst
@@ -21,15 +20,9 @@ from django.db import IntegrityError, models as dm
from hypothesis import reject, strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.extra.django._fields import from_field
+from hypothesis.internal.compat import EllipsisType
from hypothesis.strategies._internal.utils import defines_strategy
-if sys.version_info >= (3, 10):
- from types import EllipsisType as EllipsisType
-elif TYPE_CHECKING:
- from builtins import ellipsis as EllipsisType
-else:
- EllipsisType = type(Ellipsis)
-
ModelT = TypeVar("ModelT", bound=dm.Model)
@@ -48,6 +41,10 @@ class HypothesisTestCase:
return dt.SimpleTestCase.__call__(self, result)
+class SimpleTestCase(HypothesisTestCase, dt.SimpleTestCase):
+ pass
+
+
class TestCase(HypothesisTestCase, dt.TestCase):
pass
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py
index 2008a3d3090..c2046561d9c 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py
@@ -88,7 +88,6 @@ from keyword import iskeyword as _iskeyword
from string import ascii_lowercase
from textwrap import dedent, indent
from typing import (
- TYPE_CHECKING,
Any,
Callable,
DefaultDict,
@@ -105,7 +104,7 @@ import black
from hypothesis import Verbosity, find, settings, strategies as st
from hypothesis.errors import InvalidArgument, SmallSearchSpaceWarning
-from hypothesis.internal.compat import get_type_hints
+from hypothesis.internal.compat import EllipsisType, get_type_hints
from hypothesis.internal.reflection import get_signature, is_mock
from hypothesis.internal.validation import check_type
from hypothesis.provisional import domains
@@ -122,14 +121,6 @@ from hypothesis.strategies._internal.strategies import (
)
from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type
-if sys.version_info >= (3, 10):
- from types import EllipsisType as EllipsisType
-elif TYPE_CHECKING:
- from builtins import ellipsis as EllipsisType
-else:
- EllipsisType = type(Ellipsis)
-
-
IMPORT_SECTION = """
# This test code was written by the `hypothesis.extra.ghostwriter` module
# and is provided under the Creative Commons Zero public domain dedication.
@@ -696,9 +687,11 @@ def _valid_syntax_repr(strategy):
elems.append(s)
seen.add(repr(s))
strategy = st.one_of(elems or st.nothing())
- # Trivial special case because the wrapped repr for text() is terrible.
+ # hardcode some special cases for nicer reprs
if strategy == st.text().wrapped_strategy:
return set(), "text()"
+ if strategy == st.from_type(type):
+ return set(), "from_type(type)"
# Remove any typevars; we don't exploit them so they're just clutter here
if (
isinstance(strategy, LazyStrategy)
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py
index 04d01812ac4..6762f15e54a 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py
@@ -68,6 +68,7 @@ class LarkStrategy(st.SearchStrategy):
explicit: dict[str, st.SearchStrategy[str]],
alphabet: st.SearchStrategy[str],
) -> None:
+ super().__init__()
assert isinstance(grammar, lark.lark.Lark)
start: list[str] = grammar.options.start if start is None else [start]
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
index 1ab322aec89..7a81d373857 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py
@@ -220,6 +220,7 @@ def from_dtype(
class ArrayStrategy(st.SearchStrategy):
def __init__(self, element_strategy, shape, dtype, fill, unique):
+ super().__init__()
self.shape = tuple(shape)
self.fill = fill
self.array_size = int(np.prod(shape))
@@ -248,8 +249,8 @@ class ArrayStrategy(st.SearchStrategy):
# This branch only exists to help debug weird behaviour in Numpy,
# such as the string problems we had a while back.
raise HypothesisException(
- "Internal error when checking element=%r of %r to array of %r"
- % (val, val.dtype, result.dtype)
+ f"Internal error when checking element={val!r} of {val.dtype!r} "
+ f"to array of {result.dtype!r}"
) from err
if elem_changed:
strategy = self.fill if fill else self.element_strategy
@@ -272,11 +273,11 @@ class ArrayStrategy(st.SearchStrategy):
"allow_subnormal=False."
)
raise InvalidArgument(
- "Generated array element %r from %r cannot be represented as "
- "dtype %r - instead it becomes %r (type %r). Consider using a more "
- "precise strategy, for example passing the `width` argument to "
- "`floats()`."
- % (val, strategy, self.dtype, result[idx], type(result[idx]))
+ f"Generated array element {val!r} from {strategy!r} cannot be "
+ f"represented as dtype {self.dtype!r} - instead it becomes "
+ f"{result[idx]!r} (type {type(result[idx])!r}). Consider using "
+ "a more precise strategy, for example passing the `width` argument "
+ "to `floats()`."
)
def do_draw(self, data):
@@ -388,10 +389,10 @@ class ArrayStrategy(st.SearchStrategy):
mismatch = out != result
if mismatch.any():
raise InvalidArgument(
- "Array elements %r cannot be represented as dtype %r - instead "
- "they become %r. Use a more precise strategy, e.g. without "
+ f"Array elements {result[mismatch]!r} cannot be represented "
+ f"as dtype {self.dtype!r} - instead they become "
+ f"{out[mismatch]!r}. Use a more precise strategy, e.g. without "
"trailing null bytes, as this will be an error future versions."
- % (result[mismatch], self.dtype, out[mismatch])
)
result = out
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py
index 9cbf580c734..64e3d055c76 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py
@@ -130,8 +130,8 @@ def elements_and_dtype(elements, dtype, source=None):
return np.array([value], dtype=dtype)[0]
except (TypeError, ValueError, OverflowError):
raise InvalidArgument(
- "Cannot convert %s=%r of type %s to dtype %s"
- % (name, value, type(value).__name__, dtype.str)
+ f"Cannot convert {name}={value!r} of type "
+ f"{type(value).__name__} to dtype {dtype.str}"
) from None
elements = elements.map(convert_element)
@@ -638,7 +638,7 @@ def data_frames(
else:
value = draw(c.elements)
try:
- data[c.name][i] = value
+ data[c.name].iloc[i] = value
except ValueError as err: # pragma: no cover
# This just works in Pandas 1.4 and later, but gives
# a confusing error on previous versions.
@@ -719,8 +719,8 @@ def data_frames(
for k in row:
if k not in column_names:
raise InvalidArgument(
- "Row %r contains column %r not in columns %r)"
- % (row, k, [c.name for c in rewritten_columns])
+ f"Row {row!r} contains column {k!r} not in "
+ f"columns {[c.name for c in rewritten_columns]!r})"
)
row = as_list
if any_unique:
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/pytz.py b/contrib/python/hypothesis/py3/hypothesis/extra/pytz.py
index 574d8e9b41f..3b902a5dad9 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/pytz.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/pytz.py
@@ -16,8 +16,10 @@ If you are unable to use the stdlib :mod:`zoneinfo` module, e.g. via the
strategy with :py:func:`hypothesis.strategies.datetimes` and
:py:func:`hypothesis.strategies.times` to produce timezone-aware values.
-.. deprecated:: :mod:`zoneinfo` was added
- we intend to remove ``hypothesis.extra.pytz``, after libraries
+.. warning::
+
+ Since :mod:`zoneinfo` was added in Python 3.9, this extra
+ is deprecated. We intend to remove it after libraries
such as Pandas and Django complete their own migrations.
"""
diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/redis.py b/contrib/python/hypothesis/py3/hypothesis/extra/redis.py
index 9ec1b67e180..272eb6dc6ac 100644
--- a/contrib/python/hypothesis/py3/hypothesis/extra/redis.py
+++ b/contrib/python/hypothesis/py3/hypothesis/extra/redis.py
@@ -57,6 +57,14 @@ class RedisExampleDatabase(ExampleDatabase):
f"RedisExampleDatabase({self.redis!r}, expire_after={self._expire_after!r})"
)
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, RedisExampleDatabase)
+ and self.redis == other.redis
+ and self._prefix == other._prefix
+ and self.listener_channel == other.listener_channel
+ )
+
@contextmanager
def _pipeline(
self,
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/cache.py b/contrib/python/hypothesis/py3/hypothesis/internal/cache.py
index 57b3116d618..7905fc93b3f 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/cache.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/cache.py
@@ -117,7 +117,15 @@ class GenericCache(Generic[K, V]):
raise ValueError(
"Cannot increase size of cache where all keys have been pinned."
) from None
- del self.keys_to_indices[evicted.key]
+
+ # it's not clear to me how this can occur with a thread-local
+ # cache, but we've seen failures here before (specifically under
+ # the windows ci tests).
+ try:
+ del self.keys_to_indices[evicted.key]
+ except KeyError: # pragma: no cover
+ pass
+
i = 0
self.data[0] = entry
else:
@@ -289,7 +297,7 @@ class LRUReusedCache(GenericCache[K, V]):
return (2, self.tick())
-class LRUCache:
+class LRUCache(Generic[K, V]):
"""
This is a drop-in replacement for a GenericCache (despite the lack of inheritance)
in performance critical environments. It turns out that GenericCache's heap
@@ -306,7 +314,7 @@ class LRUCache:
# https://discuss.python.org/t/simplify-lru-cache/18192/6
#
# We use OrderedDict here because it is unclear to me we can provide the same
- # api as GenericCache without messing with @lru_cache internals.
+ # api as GenericCache using @lru_cache without messing with lru_cache internals.
#
# Anecdotally, OrderedDict seems quite competitive with lru_cache, but perhaps
# that is localized to our access patterns.
@@ -317,21 +325,21 @@ class LRUCache:
self._threadlocal = threading.local()
@property
- def cache(self) -> dict[Any, Any]:
+ def cache(self) -> OrderedDict[K, V]:
try:
return self._threadlocal.cache
except AttributeError:
self._threadlocal.cache = OrderedDict()
return self._threadlocal.cache
- def __setitem__(self, key, value):
+ def __setitem__(self, key: K, value: V) -> None:
self.cache[key] = value
self.cache.move_to_end(key)
while len(self.cache) > self.max_size:
self.cache.popitem(last=False)
- def __getitem__(self, key):
+ def __getitem__(self, key: K) -> V:
val = self.cache[key]
self.cache.move_to_end(key)
return val
@@ -339,12 +347,12 @@ class LRUCache:
def __iter__(self):
return iter(self.cache)
- def __len__(self):
+ def __len__(self) -> int:
return len(self.cache)
- def __contains__(self, key):
+ def __contains__(self, key: K) -> bool:
return key in self.cache
# implement GenericCache interface, for tests
- def check_valid(self):
+ def check_valid(self) -> None:
pass
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py b/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py
index bf6bb59fb2d..35fdb8d7b19 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py
@@ -16,7 +16,7 @@ import sys
import tempfile
import unicodedata
from collections.abc import Iterable
-from functools import lru_cache
+from functools import cache
from pathlib import Path
from typing import TYPE_CHECKING, Literal, Optional
@@ -133,7 +133,7 @@ def charmap() -> dict[CategoryName, IntervalsT]:
k: tuple(tuple(pair) for pair in pairs) for k, pairs in tmp_charmap.items()
}
# each value is a tuple of 2-tuples (that is, tuples of length 2)
- # and that both elements of that tuple are integers.
+ # and both elements of that tuple are integers.
for vs in _charmap.values():
ints = list(sum(vs, ()))
assert all(isinstance(x, int) for x in ints)
@@ -144,7 +144,7 @@ def charmap() -> dict[CategoryName, IntervalsT]:
return _charmap
-@lru_cache(maxsize=None)
+@cache
def intervals_from_codec(codec_name: str) -> IntervalSet: # pragma: no cover
"""Return an IntervalSet of characters which are part of this codec."""
assert codec_name == codecs.lookup(codec_name).name
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/compat.py b/contrib/python/hypothesis/py3/hypothesis/internal/compat.py
index 7d9bbe99bfe..136dac0275f 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/compat.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/compat.py
@@ -12,12 +12,21 @@ import codecs
import copy
import dataclasses
import inspect
+import itertools
import platform
import sys
import sysconfig
import typing
from functools import partial
-from typing import Any, ForwardRef, Optional, TypedDict as TypedDict, get_args
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ ForwardRef,
+ Optional,
+ TypedDict as TypedDict,
+ Union,
+ get_args,
+)
try:
BaseExceptionGroup = BaseExceptionGroup
@@ -27,7 +36,7 @@ except NameError:
BaseExceptionGroup as BaseExceptionGroup,
ExceptionGroup as ExceptionGroup,
)
-if typing.TYPE_CHECKING: # pragma: no cover
+if TYPE_CHECKING:
from typing_extensions import (
Concatenate as Concatenate,
NotRequired as NotRequired,
@@ -76,6 +85,13 @@ else:
TypeAlias = None
override = lambda f: f
+if sys.version_info >= (3, 10):
+ from types import EllipsisType as EllipsisType
+elif TYPE_CHECKING:
+ from builtins import ellipsis as EllipsisType
+else: # pragma: no cover
+ EllipsisType = type(Ellipsis)
+
PYPY = platform.python_implementation() == "PyPy"
GRAALPY = platform.python_implementation() == "GraalVM"
@@ -100,7 +116,7 @@ def escape_unicode_characters(s: str) -> str:
return codecs.encode(s, "unicode_escape").decode("ascii")
-def int_from_bytes(data: typing.Union[bytes, bytearray]) -> int:
+def int_from_bytes(data: Union[bytes, bytearray]) -> int:
return int.from_bytes(data, "big")
@@ -292,3 +308,20 @@ def _asdict_inner(obj, dict_factory):
)
else:
return copy.deepcopy(obj)
+
+
+if sys.version_info[:2] < (3, 13):
+ # batched was added in 3.12, strict flag in 3.13
+ # copied from 3.13 docs reference implementation
+
+ def batched(iterable, n, *, strict=False):
+ if n < 1:
+ raise ValueError("n must be at least one")
+ iterator = iter(iterable)
+ while batch := tuple(itertools.islice(iterator, n)):
+ if strict and len(batch) != n: # pragma: no cover
+ raise ValueError("batched(): incomplete batch")
+ yield batch
+
+else: # pragma: no cover
+ batched = itertools.batched
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py
index 9cdd278ec0d..500d8f20709 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py
@@ -9,7 +9,7 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import math
-from collections.abc import Iterable, Sequence
+from collections.abc import Hashable, Iterable, Sequence
from typing import (
TYPE_CHECKING,
Callable,
@@ -601,8 +601,11 @@ def choice_constraints_equal(
)
-def choice_constraints_key(choice_type, constraints):
+def choice_constraints_key(
+ choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT
+) -> tuple[Hashable, ...]:
if choice_type == "float":
+ constraints = cast(FloatConstraints, constraints)
return (
float_to_int(constraints["min_value"]),
float_to_int(constraints["max_value"]),
@@ -610,13 +613,14 @@ def choice_constraints_key(choice_type, constraints):
constraints["smallest_nonzero_magnitude"],
)
if choice_type == "integer":
+ constraints = cast(IntegerConstraints, constraints)
return (
constraints["min_value"],
constraints["max_value"],
None if constraints["weights"] is None else tuple(constraints["weights"]),
constraints["shrink_towards"],
)
- return tuple(constraints[key] for key in sorted(constraints))
+ return tuple(constraints[key] for key in sorted(constraints)) # type: ignore
def choices_size(choices: Iterable[ChoiceT]) -> int:
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py
index 2f87e84f70b..5e3b51eabc1 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py
@@ -15,7 +15,17 @@ from collections.abc import Hashable, Iterable, Iterator, Sequence
from enum import IntEnum
from functools import cached_property
from random import Random
-from typing import TYPE_CHECKING, Any, NoReturn, Optional, TypeVar, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Literal,
+ NoReturn,
+ Optional,
+ TypeVar,
+ Union,
+ cast,
+ overload,
+)
import attr
@@ -59,7 +69,10 @@ from hypothesis.internal.floats import (
sign_aware_lte,
)
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.threading import ThreadLocal
if TYPE_CHECKING:
from typing import TypeAlias
@@ -95,6 +108,9 @@ MisalignedAt: "TypeAlias" = tuple[
]
TOP_LABEL = calc_label_from_name("top")
+MAX_DEPTH = 100
+
+threadlocal = ThreadLocal(global_test_counter=int)
class ExtraInformation:
@@ -137,7 +153,7 @@ def structural_coverage(label: int) -> StructuralCoverageTag:
# This cache can be quite hot and so we prefer LRUCache over LRUReusedCache for
# performance. We lose scan resistance, but that's probably fine here.
-POOLED_CONSTRAINTS_CACHE = LRUCache(4096)
+POOLED_CONSTRAINTS_CACHE: LRUCache[tuple[Any, ...], ChoiceConstraintsT] = LRUCache(4096)
class Span:
@@ -260,16 +276,16 @@ class SpanProperty:
"""Rerun the test case with this visitor and return the
results of ``self.finish()``."""
for record in self.spans.trail:
- if record == TrailType.CHOICE:
+ if record == TrailType.STOP_SPAN_DISCARD:
+ self.__pop(discarded=True)
+ elif record == TrailType.STOP_SPAN_NO_DISCARD:
+ self.__pop(discarded=False)
+ elif record == TrailType.CHOICE:
self.choice_count += 1
- elif record >= TrailType.START_SPAN:
- self.__push(record - TrailType.START_SPAN)
else:
- assert record in (
- TrailType.STOP_SPAN_DISCARD,
- TrailType.STOP_SPAN_NO_DISCARD,
- )
- self.__pop(discarded=record == TrailType.STOP_SPAN_DISCARD)
+ # everything after TrailType.CHOICE is the label of a span start.
+ self.__push(record - TrailType.CHOICE - 1)
+
return self.finish()
def __push(self, label_index: int) -> None:
@@ -300,8 +316,10 @@ class SpanProperty:
class TrailType(IntEnum):
STOP_SPAN_DISCARD = 1
STOP_SPAN_NO_DISCARD = 2
- START_SPAN = 3
- CHOICE = calc_label_from_name("ir draw record")
+ CHOICE = 3
+ # every trail element larger than TrailType.CHOICE is the label of a span
+ # start, offset by its index. So the first span label is stored as 4, the
+ # second as 5, etc, regardless of its actual integer label.
class SpanRecord:
@@ -334,7 +352,7 @@ class SpanRecord:
except KeyError:
i = self.__index_of_labels.setdefault(label, len(self.labels))
self.labels.append(label)
- self.trail.append(TrailType.START_SPAN + i)
+ self.trail.append(TrailType.CHOICE + 1 + i)
def stop_span(self, *, discard: bool) -> None:
if discard:
@@ -520,11 +538,6 @@ class _Overrun:
Overrun = _Overrun()
-global_test_counter = 0
-
-
-MAX_DEPTH = 100
-
class DataObserver:
"""Observer class for recording the behaviour of a
@@ -657,9 +670,8 @@ class ConjectureData:
self.output = ""
self.status = Status.VALID
self.frozen = False
- global global_test_counter
- self.testcounter = global_test_counter
- global_test_counter += 1
+ self.testcounter = threadlocal.global_test_counter
+ threadlocal.global_test_counter += 1
self.start_time = time.perf_counter()
self.gc_start_time = gc_cumulative_time()
self.events: dict[str, Union[str, int, float]] = {}
@@ -700,13 +712,14 @@ class ConjectureData:
self.arg_slices: set[tuple[int, int]] = set()
self.slice_comments: dict[tuple[int, int], str] = {}
self._observability_args: dict[str, Any] = {}
- self._observability_predicates: defaultdict = defaultdict(
- lambda: {"satisfied": 0, "unsatisfied": 0}
+ self._observability_predicates: defaultdict[str, PredicateCounts] = defaultdict(
+ PredicateCounts
)
self._sampled_from_all_strategies_elements_message: Optional[
tuple[str, object]
] = None
- self._shared_strategy_draws: dict[Hashable, Any] = {}
+ self._shared_strategy_draws: dict[Hashable, tuple[int, Any]] = {}
+ self.hypothesis_runner = not_set
self.expected_exception: Optional[BaseException] = None
self.expected_traceback: Optional[str] = None
@@ -737,7 +750,65 @@ class ConjectureData:
#
# `observe` formalizes this. The choice will only be written to the choice
# sequence if observe is True.
- def _draw(self, choice_type, constraints, *, observe, forced):
+
+ @overload
+ def _draw(
+ self,
+ choice_type: Literal["integer"],
+ constraints: IntegerConstraints,
+ *,
+ observe: bool,
+ forced: Optional[int],
+ ) -> int: ...
+
+ @overload
+ def _draw(
+ self,
+ choice_type: Literal["float"],
+ constraints: FloatConstraints,
+ *,
+ observe: bool,
+ forced: Optional[float],
+ ) -> float: ...
+
+ @overload
+ def _draw(
+ self,
+ choice_type: Literal["string"],
+ constraints: StringConstraints,
+ *,
+ observe: bool,
+ forced: Optional[str],
+ ) -> str: ...
+
+ @overload
+ def _draw(
+ self,
+ choice_type: Literal["bytes"],
+ constraints: BytesConstraints,
+ *,
+ observe: bool,
+ forced: Optional[bytes],
+ ) -> bytes: ...
+
+ @overload
+ def _draw(
+ self,
+ choice_type: Literal["boolean"],
+ constraints: BooleanConstraints,
+ *,
+ observe: bool,
+ forced: Optional[bool],
+ ) -> bool: ...
+
+ def _draw(
+ self,
+ choice_type: ChoiceTypeT,
+ constraints: ChoiceConstraintsT,
+ *,
+ observe: bool,
+ forced: Optional[ChoiceT],
+ ) -> ChoiceT:
# this is somewhat redundant with the length > max_length check at the
# end of the function, but avoids trying to use a null self.random when
# drawing past the node of a ConjectureData.for_choices data.
@@ -776,8 +847,10 @@ class ConjectureData:
# bring that back (ABOVE the choice sequence layer) in the future.
#
# See https://github.com/HypothesisWorks/hypothesis/issues/3926.
- if choice_type == "float" and math.isnan(value):
- value = int_to_float(float_to_int(value))
+ if choice_type == "float":
+ assert isinstance(value, float)
+ if math.isnan(value):
+ value = int_to_float(float_to_int(value))
if observe:
was_forced = forced is not None
@@ -850,10 +923,9 @@ class ConjectureData:
*,
allow_nan: bool = True,
smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
- # TODO: consider supporting these float widths at the IR level in the
- # future.
+ # TODO: consider supporting these float widths at the choice sequence
+ # level in the future.
# width: Literal[16, 32, 64] = 64,
- # exclude_min and exclude_max handled higher up,
forced: Optional[float] = None,
observe: bool = True,
) -> float:
@@ -861,6 +933,16 @@ class ConjectureData:
assert not math.isnan(min_value)
assert not math.isnan(max_value)
+ if smallest_nonzero_magnitude == 0.0: # pragma: no cover
+ raise FloatingPointError(
+ "Got allow_subnormal=True, but we can't represent subnormal floats "
+ "right now, in violation of the IEEE-754 floating-point "
+ "specification. This is usually because something was compiled with "
+ "-ffast-math or a similar option, which sets global processor state. "
+ "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
+ "writeup - and good luck!"
+ )
+
if forced is not None:
assert allow_nan or not math.isnan(forced)
assert math.isnan(forced) or (
@@ -931,7 +1013,34 @@ class ConjectureData:
constraints: BooleanConstraints = self._pooled_constraints("boolean", {"p": p})
return self._draw("boolean", constraints, observe=observe, forced=forced)
- def _pooled_constraints(self, choice_type, constraints):
+ @overload
+ def _pooled_constraints(
+ self, choice_type: Literal["integer"], constraints: IntegerConstraints
+ ) -> IntegerConstraints: ...
+
+ @overload
+ def _pooled_constraints(
+ self, choice_type: Literal["float"], constraints: FloatConstraints
+ ) -> FloatConstraints: ...
+
+ @overload
+ def _pooled_constraints(
+ self, choice_type: Literal["string"], constraints: StringConstraints
+ ) -> StringConstraints: ...
+
+ @overload
+ def _pooled_constraints(
+ self, choice_type: Literal["bytes"], constraints: BytesConstraints
+ ) -> BytesConstraints: ...
+
+ @overload
+ def _pooled_constraints(
+ self, choice_type: Literal["boolean"], constraints: BooleanConstraints
+ ) -> BooleanConstraints: ...
+
+ def _pooled_constraints(
+ self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT
+ ) -> ChoiceConstraintsT:
"""Memoize common dictionary objects to reduce memory pressure."""
# caching runs afoul of nondeterminism checks
if self.provider.avoid_realization:
@@ -967,17 +1076,10 @@ class ConjectureData:
if node.type == "simplest":
if forced is not None:
choice = forced
- elif isinstance(self.provider, HypothesisProvider):
- try:
- choice = choice_from_index(0, choice_type, constraints)
- except ChoiceTooLarge:
- self.mark_overrun()
- else:
- # give alternative backends control over ChoiceTemplate draws
- # as well
- choice = getattr(self.provider, f"draw_{choice_type}")(
- **constraints
- )
+ try:
+ choice = choice_from_index(0, choice_type, constraints)
+ except ChoiceTooLarge:
+ self.mark_overrun()
else:
raise NotImplementedError
@@ -996,12 +1098,12 @@ class ConjectureData:
bytes: "bytes",
}[type(choice)]
# If we're trying to:
- # * draw a different ir type at the same location
- # * draw the same ir type with a different constraints, which does not permit
+ # * draw a different choice type at the same location
+ # * draw the same choice type with a different constraints, which does not permit
# the current value
#
# then we call this a misalignment, because the choice sequence has
- # slipped from what we expected at some point. An easy misalignment is
+ # changed from what we expected at some point. An easy misalignment is
#
# one_of(integers(0, 100), integers(101, 200))
#
@@ -1090,6 +1192,7 @@ class ConjectureData:
observe_as: Optional[str] = None,
) -> "Ex":
from hypothesis.internal.observability import TESTCASE_CALLBACKS
+ from hypothesis.strategies._internal.lazy import unwrap_strategies
from hypothesis.strategies._internal.utils import to_jsonable
if self.is_find and not strategy.supports_find:
@@ -1116,19 +1219,22 @@ class ConjectureData:
if self.depth >= MAX_DEPTH:
self.mark_invalid("max depth exceeded")
+ # Jump directly to the unwrapped strategy for the label and for do_draw.
+ # This avoids adding an extra span to all lazy strategies.
+ unwrapped = unwrap_strategies(strategy)
if label is None:
- assert isinstance(strategy.label, int)
- label = strategy.label
+ label = unwrapped.label
+ assert isinstance(label, int)
+
self.start_span(label=label)
try:
if not at_top_level:
- return strategy.do_draw(self)
+ return unwrapped.do_draw(self)
assert start_time is not None
key = observe_as or f"generate:unlabeled_{len(self.draw_times)}"
try:
- strategy.validate()
try:
- v = strategy.do_draw(self)
+ v = unwrapped.do_draw(self)
finally:
# Subtract the time spent in GC to avoid overcounting, as it is
# accounted for at the overall example level.
@@ -1256,9 +1362,7 @@ class ConjectureData:
self.freeze()
raise StopTest(self.testcounter)
- def mark_interesting(
- self, interesting_origin: Optional[InterestingOrigin] = None
- ) -> NoReturn:
+ def mark_interesting(self, interesting_origin: InterestingOrigin) -> NoReturn:
self.conclude_test(Status.INTERESTING, interesting_origin)
def mark_invalid(self, why: Optional[str] = None) -> NoReturn:
@@ -1270,6 +1374,8 @@ class ConjectureData:
self.conclude_test(Status.OVERRUN)
-def draw_choice(choice_type, constraints, *, random):
+def draw_choice(
+ choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, random: Random
+) -> ChoiceT:
cd = ConjectureData(random=random)
- return getattr(cd.provider, f"draw_{choice_type}")(**constraints)
+ return cast(ChoiceT, getattr(cd.provider, f"draw_{choice_type}")(**constraints))
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py
index 1621d7c3ff2..083a105c6a2 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py
@@ -158,31 +158,47 @@ class Conclusion:
# The one case where this may be detrimental is fuzzing, where the throughput of
# examples is so high that it really may saturate important nodes. We'll cross
# that bridge when we come to it.
-MAX_CHILDREN_EFFECTIVELY_INFINITE: Final[int] = 100_000
+MAX_CHILDREN_EFFECTIVELY_INFINITE: Final[int] = 10_000_000
def _count_distinct_strings(*, alphabet_size: int, min_size: int, max_size: int) -> int:
# We want to estimate if we're going to have more children than
# MAX_CHILDREN_EFFECTIVELY_INFINITE, without computing a potentially
- # extremely expensive pow. We'll check if the number of strings in
- # the largest string size alone is enough to put us over this limit.
- # We'll also employ a trick of estimating against log, which is cheaper
- # than computing a pow.
- #
- # x = max_size
- # y = alphabet_size
- # n = MAX_CHILDREN_EFFECTIVELY_INFINITE
- #
- # x**y > n
- # <=> log(x**y) > log(n)
- # <=> y * log(x) > log(n)
- definitely_too_large = max_size * math.log(alphabet_size) > math.log(
- MAX_CHILDREN_EFFECTIVELY_INFINITE
- )
- if definitely_too_large:
- return MAX_CHILDREN_EFFECTIVELY_INFINITE
+ # extremely expensive pow. We'll check the two extreme cases - if the
+ # number of strings in the largest string size alone is enough to put us
+ # over this limit (at alphabet_size >= 2), and if the variation in sizes
+ # (at alphabet_size == 1) is enough. If neither result in an early return,
+ # the exact result should be reasonably cheap to compute.
+ if alphabet_size == 0:
+ # Special-case the empty string, avoid error in math.log(0).
+ return 1
+ elif alphabet_size == 1:
+ # Special-case the constant alphabet, invalid in the geom-series sum.
+ return max_size - min_size + 1
+ else:
+ # Estimate against log, which is cheaper than computing a pow.
+ #
+ # m = max_size
+ # a = alphabet_size
+ # N = MAX_CHILDREN_EFFECTIVELY_INFINITE
+ #
+ # a**m > N
+ # <=> m * log(a) > log(N)
+ log_max_sized_children = max_size * math.log(alphabet_size)
+ if log_max_sized_children > math.log(MAX_CHILDREN_EFFECTIVELY_INFINITE):
+ return MAX_CHILDREN_EFFECTIVELY_INFINITE
- return sum(alphabet_size**k for k in range(min_size, max_size + 1))
+ # The sum of a geometric series is given by (ref: wikipedia):
+ # ᵐ∑ₖ₌₀ aᵏ = (aᵐ⁺¹ - 1) / (a - 1)
+ # = S(m) / S(0)
+ # assuming a != 1 and using the definition
+ # S(m) := aᵐ⁺¹ - 1.
+ # The sum we want, starting from a number n [0 <= n <= m] rather than zero, is
+ # ᵐ∑ₖ₌ₙ aᵏ = ᵐ∑ₖ₌₀ aᵏ - ⁿ⁻¹∑ₖ₌₀ aᵏ = S(m) / S(0) - S(n - 1) / S(0)
+ def S(n):
+ return alphabet_size ** (n + 1) - 1
+
+ return (S(max_size) - S(min_size - 1)) // S(0)
def compute_max_children(
@@ -226,16 +242,6 @@ def compute_max_children(
max_size = constraints["max_size"]
intervals = constraints["intervals"]
- if len(intervals) == 0:
- # Special-case the empty alphabet to avoid an error in math.log(0).
- # Only possibility is the empty string.
- return 1
-
- # avoid math.log(1) == 0 and incorrectly failing our effectively_infinite
- # estimate, even when we definitely are too large.
- if len(intervals) == 1 and max_size > MAX_CHILDREN_EFFECTIVELY_INFINITE:
- return MAX_CHILDREN_EFFECTIVELY_INFINITE
-
return _count_distinct_strings(
alphabet_size=len(intervals), min_size=min_size, max_size=max_size
)
@@ -701,7 +707,7 @@ class DataTree:
def generate_novel_prefix(self, random: Random) -> tuple[ChoiceT, ...]:
"""Generate a short random string that (after rewriting) is not
- a prefix of any buffer previously added to the tree.
+ a prefix of any choice sequence previously added to the tree.
The resulting prefix is essentially arbitrary - it would be nice
for it to be uniform at random, but previous attempts to do that
@@ -893,8 +899,9 @@ class DataTree:
# entails some bookkeeping such that we're careful about when the
# float key is in its bits form (as a key into branch.children) and
# when it is in its float form (as a value we want to write to the
- # buffer), and converting between the two forms as appropriate.
+ # choice sequence), and converting between the two forms as appropriate.
if choice_type == "float":
+ assert isinstance(value, float)
value = float_to_int(value)
return value
@@ -986,9 +993,13 @@ class DataTree:
class TreeRecordingObserver(DataObserver):
def __init__(self, tree: DataTree):
- self.__current_node: TreeNode = tree.root
- self.__index_in_current_node: int = 0
- self.__trail: list[TreeNode] = [self.__current_node]
+ # this attr isn't read, but is very useful for local debugging flaky
+ # errors, with
+ # `from hypothesis.vendor import pretty; print(pretty.pretty(self._root))`
+ self._root = tree.root
+ self._current_node: TreeNode = tree.root
+ self._index_in_current_node: int = 0
+ self._trail: list[TreeNode] = [self._current_node]
self.killed: bool = False
def draw_integer(
@@ -1028,9 +1039,9 @@ class TreeRecordingObserver(DataObserver):
was_forced: bool,
constraints: ChoiceConstraintsT,
) -> None:
- i = self.__index_in_current_node
- self.__index_in_current_node += 1
- node = self.__current_node
+ i = self._index_in_current_node
+ self._index_in_current_node += 1
+ node = self._current_node
if isinstance(value, float):
value = float_to_int(value)
@@ -1056,8 +1067,8 @@ class TreeRecordingObserver(DataObserver):
new_node = TreeNode()
assert isinstance(node.transition, Branch)
node.transition.children[value] = new_node
- self.__current_node = new_node
- self.__index_in_current_node = 0
+ self._current_node = new_node
+ self._index_in_current_node = 0
else:
trans = node.transition
if trans is None:
@@ -1088,8 +1099,8 @@ class TreeRecordingObserver(DataObserver):
):
node.split_at(i)
assert isinstance(node.transition, Branch)
- self.__current_node = node.transition.children[value]
- self.__index_in_current_node = 0
+ self._current_node = node.transition.children[value]
+ self._index_in_current_node = 0
elif isinstance(trans, Conclusion):
assert trans.status != Status.OVERRUN
# We tried to draw where history says we should have
@@ -1100,12 +1111,12 @@ class TreeRecordingObserver(DataObserver):
if choice_type != trans.choice_type or constraints != trans.constraints:
raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG)
try:
- self.__current_node = trans.children[value]
+ self._current_node = trans.children[value]
except KeyError:
- self.__current_node = trans.children.setdefault(value, TreeNode())
- self.__index_in_current_node = 0
- if self.__trail[-1] is not self.__current_node:
- self.__trail.append(self.__current_node)
+ self._current_node = trans.children.setdefault(value, TreeNode())
+ self._index_in_current_node = 0
+ if self._trail[-1] is not self._current_node:
+ self._trail.append(self._current_node)
def kill_branch(self) -> None:
"""Mark this part of the tree as not worth re-exploring."""
@@ -1114,19 +1125,19 @@ class TreeRecordingObserver(DataObserver):
self.killed = True
- if self.__index_in_current_node < len(self.__current_node.values) or (
- self.__current_node.transition is not None
- and not isinstance(self.__current_node.transition, Killed)
+ if self._index_in_current_node < len(self._current_node.values) or (
+ self._current_node.transition is not None
+ and not isinstance(self._current_node.transition, Killed)
):
raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG)
- if self.__current_node.transition is None:
- self.__current_node.transition = Killed(TreeNode())
+ if self._current_node.transition is None:
+ self._current_node.transition = Killed(TreeNode())
self.__update_exhausted()
- self.__current_node = self.__current_node.transition.next_node
- self.__index_in_current_node = 0
- self.__trail.append(self.__current_node)
+ self._current_node = self._current_node.transition.next_node
+ self._index_in_current_node = 0
+ self._trail.append(self._current_node)
def conclude_test(
self, status: Status, interesting_origin: Optional[InterestingOrigin]
@@ -1135,8 +1146,8 @@ class TreeRecordingObserver(DataObserver):
node if necessary and checks for consistency."""
if status == Status.OVERRUN:
return
- i = self.__index_in_current_node
- node = self.__current_node
+ i = self._index_in_current_node
+ node = self._current_node
if i < len(node.values) or isinstance(node.transition, Branch):
raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG)
@@ -1162,7 +1173,7 @@ class TreeRecordingObserver(DataObserver):
else:
node.transition = new_transition
- assert node is self.__trail[-1]
+ assert node is self._trail[-1]
node.check_exhausted()
assert len(node.values) > 0 or node.check_exhausted()
@@ -1170,7 +1181,7 @@ class TreeRecordingObserver(DataObserver):
self.__update_exhausted()
def __update_exhausted(self) -> None:
- for t in reversed(self.__trail):
+ for t in reversed(self._trail):
# Any node we've traversed might have now become exhausted.
# We check from the right. As soon as we hit a node that
# isn't exhausted, this automatically implies that all of
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
index b0ea8ba62c6..799fde74cde 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py
@@ -9,31 +9,30 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import importlib
+import inspect
import math
-import textwrap
import time
from collections import defaultdict
from collections.abc import Generator, Sequence
-from contextlib import contextmanager, suppress
+from contextlib import AbstractContextManager, contextmanager, nullcontext, suppress
+from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum
from random import Random, getrandbits
from typing import Callable, Final, List, Literal, NoReturn, Optional, Union, cast
-import attr
-
from hypothesis import HealthCheck, Phase, Verbosity, settings as Settings
-from hypothesis._settings import local_settings
+from hypothesis._settings import local_settings, note_deprecation
from hypothesis.database import ExampleDatabase, choices_from_bytes, choices_to_bytes
from hypothesis.errors import (
BackendCannotProceed,
- FlakyReplay,
+ FlakyBackendFailure,
HypothesisException,
InvalidArgument,
StopTest,
)
from hypothesis.internal.cache import LRUReusedCache
-from hypothesis.internal.compat import NotRequired, TypeAlias, TypedDict, ceil, override
+from hypothesis.internal.compat import NotRequired, TypedDict, ceil, override
from hypothesis.internal.conjecture.choice import (
ChoiceConstraintsT,
ChoiceKeyT,
@@ -68,35 +67,49 @@ from hypothesis.internal.conjecture.providers import (
from hypothesis.internal.conjecture.shrinker import Shrinker, ShrinkPredicateT, sort_key
from hypothesis.internal.escalation import InterestingOrigin
from hypothesis.internal.healthcheck import fail_health_check
+from hypothesis.internal.observability import Observation, with_observation_callback
from hypothesis.reporting import base_report, report
+#: The maximum number of times the shrinker will reduce the complexity of a failing
+#: input before giving up. This avoids falling down a trap of exponential (or worse)
+#: complexity, where the shrinker appears to be making progress but will take a
+#: substantially long time to finish completely.
MAX_SHRINKS: Final[int] = 500
-CACHE_SIZE: Final[int] = 10000
-MUTATION_POOL_SIZE: Final[int] = 100
-MIN_TEST_CALLS: Final[int] = 10
-BUFFER_SIZE: Final[int] = 8 * 1024
# If the shrinking phase takes more than five minutes, abort it early and print
# a warning. Many CI systems will kill a build after around ten minutes with
# no output, and appearing to hang isn't great for interactive use either -
# showing partially-shrunk examples is better than quitting with no examples!
# (but make it monkeypatchable, for the rare users who need to keep on shrinking)
+
+#: The maximum total time in seconds that the shrinker will try to shrink a failure
+#: for before giving up. This is across all shrinks for the same failure, so even
+#: if the shrinker successfully reduces the complexity of a single failure several
+#: times, it will stop when it hits |MAX_SHRINKING_SECONDS| of total time taken.
MAX_SHRINKING_SECONDS: Final[int] = 300
-Ls: TypeAlias = list["Ls | int"]
+#: The maximum amount of entropy a single test case can use before giving up
+#: while making random choices during input generation.
+#:
+#: The "unit" of one |BUFFER_SIZE| does not have any defined semantics, and you
+#: should not rely on it, except that a linear increase |BUFFER_SIZE| will linearly
+#: increase the amount of entropy a test case can use during generation.
+BUFFER_SIZE: Final[int] = 8 * 1024
+CACHE_SIZE: Final[int] = 10000
+MIN_TEST_CALLS: Final[int] = 10
def shortlex(s):
return (len(s), s)
+@dataclass
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: "defaultdict[str, List[float]]" = attr.ib(
- factory=lambda: defaultdict(list)
+ valid_examples: int = field(default=0)
+ invalid_examples: int = field(default=0)
+ overrun_examples: int = field(default=0)
+ draw_times: "defaultdict[str, List[float]]" = field(
+ default_factory=lambda: defaultdict(list)
)
@property
@@ -213,9 +226,25 @@ class DiscardObserver(DataObserver):
raise ContainsDiscard
-def realize_choices(data: ConjectureData) -> None:
+def realize_choices(data: ConjectureData, *, for_failure: bool) -> None:
+ # backwards-compatibility with backends without for_failure, can remove
+ # in a few months
+ kwargs = {}
+ if for_failure:
+ if "for_failure" in inspect.signature(data.provider.realize).parameters:
+ kwargs["for_failure"] = True
+ else:
+ note_deprecation(
+ f"{type(data.provider).__qualname__}.realize does not have the "
+ "for_failure parameter. This will be an error in future versions "
+ "of Hypothesis. (If you installed this backend from a separate "
+ "package, upgrading that package may help).",
+ has_codemod=False,
+ since="2025-05-07",
+ )
+
for node in data.nodes:
- value = data.provider.realize(node.value)
+ value = data.provider.realize(node.value, **kwargs)
expected_type = {
"string": str,
"float": float,
@@ -231,7 +260,10 @@ def realize_choices(data: ConjectureData) -> None:
constraints = cast(
ChoiceConstraintsT,
- {k: data.provider.realize(v) for k, v in node.constraints.items()},
+ {
+ k: data.provider.realize(v, **kwargs)
+ for k, v in node.constraints.items()
+ },
)
node.value = value
node.constraints = constraints
@@ -266,10 +298,7 @@ class ConjectureRunner:
self.statistics: StatisticsDict = {}
self.stats_per_test_case: list[CallStats] = []
- # At runtime, the keys are only ever type `InterestingOrigin`, but can be `None` during tests.
- self.interesting_examples: dict[
- Optional[InterestingOrigin], ConjectureResult
- ] = {}
+ self.interesting_examples: dict[InterestingOrigin, ConjectureResult] = {}
# We use call_count because there may be few possible valid_examples.
self.first_bug_found_at: Optional[int] = None
self.last_bug_found_at: Optional[int] = None
@@ -310,12 +339,25 @@ class ConjectureRunner:
self.reused_previously_shrunk_test_case: bool = False
self.__pending_call_explanation: Optional[str] = None
+ self._backend_found_failure: bool = False
+ self._backend_exceeded_deadline: bool = False
self._switch_to_hypothesis_provider: bool = False
self.__failed_realize_count: int = 0
# note unsound verification by alt backends
self._verified_by: Optional[str] = None
+ @contextmanager
+ def _with_switch_to_hypothesis_provider(
+ self, value: bool
+ ) -> Generator[None, None, None]:
+ previous = self._switch_to_hypothesis_provider
+ try:
+ self._switch_to_hypothesis_provider = value
+ yield
+ finally:
+ self._switch_to_hypothesis_provider = previous
+
@property
def using_hypothesis_backend(self) -> bool:
return (
@@ -469,8 +511,8 @@ class ConjectureRunner:
self.__pending_call_explanation = None
self.call_count += 1
-
interrupted = False
+
try:
self.__stoppable_test_function(data)
except KeyboardInterrupt:
@@ -488,6 +530,13 @@ class ConjectureRunner:
and (self.__failed_realize_count / self.call_count) > 0.2
):
self._switch_to_hypothesis_provider = True
+
+ # treat all BackendCannotProceed exceptions as invalid. This isn't
+ # great; "verified" should really be counted as self.valid_examples += 1.
+ # But we check self.valid_examples == 0 to determine whether to raise
+ # Unsatisfiable, and that would throw this check off.
+ self.invalid_examples += 1
+
# skip the post-test-case tracking; we're pretending this never happened
interrupted = True
data.cannot_proceed_scope = exc.scope
@@ -496,7 +545,7 @@ class ConjectureRunner:
except BaseException:
data.freeze()
if self.settings.backend != "hypothesis":
- realize_choices(data)
+ realize_choices(data, for_failure=True)
self.save_choices(data.choices)
raise
finally:
@@ -516,7 +565,7 @@ class ConjectureRunner:
}
self.stats_per_test_case.append(call_stats)
if self.settings.backend != "hypothesis":
- realize_choices(data)
+ realize_choices(data, for_failure=data.status is Status.INTERESTING)
self._cache(data)
if data.misaligned_at is not None: # pragma: no branch # coverage bug?
@@ -565,33 +614,35 @@ class ConjectureRunner:
if not self.using_hypothesis_backend:
# replay this failure on the hypothesis backend to ensure it still
# finds a failure. otherwise, it is flaky.
- initial_origin = data.interesting_origin
- initial_traceback = data.expected_traceback
+ initial_exception = data.expected_exception
data = ConjectureData.for_choices(data.choices)
- self.__stoppable_test_function(data)
+ # we've already going to use the hypothesis provider for this
+ # data, so the verb "switch" is a bit misleading here. We're really
+ # setting this to inform our on_observation logic that the observation
+ # generated here was from a hypothesis backend, and shouldn't be
+ # sent to the on_observation of any alternative backend.
+ with self._with_switch_to_hypothesis_provider(True):
+ self.__stoppable_test_function(data)
data.freeze()
- # TODO: Convert to FlakyFailure on the way out. Should same-origin
- # also be checked?
+ # TODO: Should same-origin also be checked? (discussion in
+ # https://github.com/HypothesisWorks/hypothesis/pull/4470#discussion_r2217055487)
if data.status != Status.INTERESTING:
desc_new_status = {
data.status.VALID: "passed",
data.status.INVALID: "failed filters",
data.status.OVERRUN: "overran",
}[data.status]
- wrapped_tb = (
- ""
- if initial_traceback is None
- else textwrap.indent(initial_traceback, " | ")
- )
- raise FlakyReplay(
- f"Inconsistent results from replaying a failing test case!\n"
- f"{wrapped_tb}on backend={self.settings.backend!r} but "
- f"{desc_new_status} under backend='hypothesis'",
- interesting_origins=[initial_origin],
+ raise FlakyBackendFailure(
+ f"Inconsistent results from replaying a failing test case! "
+ f"Raised {type(initial_exception).__name__} on "
+ f"backend={self.settings.backend!r}, but "
+ f"{desc_new_status} under backend='hypothesis'.",
+ [initial_exception],
)
self._cache(data)
+ assert data.interesting_origin is not None
key = data.interesting_origin
changed = False
try:
@@ -611,6 +662,8 @@ class ConjectureRunner:
if changed:
self.save_choices(data.choices)
self.interesting_examples[key] = data.as_result() # type: ignore
+ if not self.using_hypothesis_backend:
+ self._backend_found_failure = True
self.__data_cache.pin(self._cache_key(data.choices), data.as_result())
self.shrunk_examples.discard(key)
@@ -657,7 +710,7 @@ class ConjectureRunner:
self.settings.database.delete(self.pareto_key, choices_to_bytes(data.choices))
def generate_novel_prefix(self) -> tuple[ChoiceT, ...]:
- """Uses the tree to proactively generate a starting sequence of bytes
+ """Uses the tree to proactively generate a starting choice sequence
that we haven't explored yet for this test.
When this method is called, we assume that there must be at
@@ -788,18 +841,42 @@ class ConjectureRunner:
f"{', ' + data.output if data.output else ''}"
)
+ def observe_for_provider(self) -> AbstractContextManager:
+ def on_observation(observation: Observation) -> None:
+ assert observation.type == "test_case"
+ # because lifetime == "test_function"
+ assert isinstance(self.provider, PrimitiveProvider)
+ # only fire if we actually used that provider to generate this observation
+ if not self._switch_to_hypothesis_provider:
+ self.provider.on_observation(observation)
+
+ if (
+ self.settings.backend != "hypothesis"
+ # only for lifetime = "test_function" providers (guaranteed
+ # by this isinstance check)
+ and isinstance(self.provider, PrimitiveProvider)
+ # and the provider opted-in to observations
+ and self.provider.add_observability_callback
+ ):
+ return with_observation_callback(on_observation)
+ return nullcontext()
+
def run(self) -> None:
with local_settings(self.settings):
- try:
- self._run()
- except RunIsComplete:
- pass
- for v in self.interesting_examples.values():
- self.debug_data(v)
- self.debug(
- "Run complete after %d examples (%d valid) and %d shrinks"
- % (self.call_count, self.valid_examples, self.shrinks)
- )
+ # NOTE: For compatibility with Python 3.9's LL(1)
+ # parser, this is written as a nested with-statement,
+ # instead of a compound one.
+ with self.observe_for_provider():
+ try:
+ self._run()
+ except RunIsComplete:
+ pass
+ for v in self.interesting_examples.values():
+ self.debug_data(v)
+ self.debug(
+ "Run complete after %d examples (%d valid) and %d shrinks"
+ % (self.call_count, self.valid_examples, self.shrinks)
+ )
@property
def database(self) -> Optional[ExampleDatabase]:
@@ -964,6 +1041,7 @@ class ConjectureRunner:
self.debug("Generating new examples")
assert self.should_generate_more()
+ self._switch_to_hypothesis_provider = True
zero_data = self.cached_test_function((ChoiceTemplate("simplest", count=None),))
if zero_data.status > Status.OVERRUN:
assert isinstance(zero_data, ConjectureResult)
@@ -1045,12 +1123,11 @@ class ConjectureRunner:
small_example_cap = min(self.settings.max_examples // 10, 50)
optimise_at = max(self.settings.max_examples // 2, small_example_cap + 1, 10)
ran_optimisations = False
+ self._switch_to_hypothesis_provider = False
while self.should_generate_more():
- # Unfortunately generate_novel_prefix still operates in terms of
- # a buffer and uses HypothesisProvider as its backing provider,
- # not whatever is specified by the backend. We can improve this
- # once more things are on the ir.
+ # we don't yet integrate DataTree with backends. Instead of generating
+ # a novel prefix, ask the backend for an input.
if not self.using_hypothesis_backend:
data = self.new_conjecture_data([])
with suppress(BackendCannotProceed):
@@ -1462,16 +1539,14 @@ class ConjectureRunner:
choices_to_bytes(v.choices)
for v in self.interesting_examples.values()
}
- cap = max(map(shortlex, primary))
-
- if shortlex(c) > cap:
+ if shortlex(c) > max(map(shortlex, primary)):
break
- else:
- self.cached_test_function(choices)
- # We unconditionally remove c from the secondary key as it
- # is either now primary or worse than our primary example
- # of this reason for interestingness.
- self.settings.database.delete(self.secondary_key, c)
+
+ self.cached_test_function(choices)
+ # We unconditionally remove c from the secondary key as it
+ # is either now primary or worse than our primary example
+ # of this reason for interestingness.
+ self.settings.database.delete(self.secondary_key, c)
def shrink(
self,
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
index 800c2f22c92..96f2f80c41b 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
@@ -19,25 +19,26 @@ import time
import warnings
from array import ArrayType
from collections.abc import Iterable, Iterator, Sequence
-from typing import Any, Callable, Generic, Literal, Optional, TypeVar, Union, overload
+from threading import Lock
+from typing import (
+ Any,
+ Callable,
+ ClassVar,
+ Generic,
+ Literal,
+ Optional,
+ TypeVar,
+ Union,
+ overload,
+)
from sortedcontainers import SortedList
from hypothesis.errors import HypothesisWarning
-ARRAY_CODES = ["B", "H", "I", "L", "Q", "O"]
-
T = TypeVar("T")
-def array_or_list(
- code: str, contents: Iterable[int]
-) -> Union[list[int], "ArrayType[int]"]:
- if code == "O":
- return list(contents)
- return array.array(code, contents)
-
-
def replace_all(
ls: Sequence[T],
replacements: Iterable[tuple[int, int, Sequence[T]]],
@@ -60,9 +61,6 @@ def replace_all(
return result
-NEXT_ARRAY_CODE = dict(zip(ARRAY_CODES, ARRAY_CODES[1:]))
-
-
class IntList(Sequence[int]):
"""Class for storing a list of non-negative integers compactly.
@@ -71,14 +69,15 @@ class IntList(Sequence[int]):
we upgrade the array to the smallest word size needed to store
the new value."""
- __slots__ = ("__underlying",)
+ ARRAY_CODES: ClassVar[list[str]] = ["B", "H", "I", "L", "Q", "O"]
+ NEXT_ARRAY_CODE: ClassVar[dict[str, str]] = dict(zip(ARRAY_CODES, ARRAY_CODES[1:]))
- __underlying: Union[list[int], "ArrayType[int]"]
+ __slots__ = ("__underlying",)
def __init__(self, values: Sequence[int] = ()):
- for code in ARRAY_CODES:
+ for code in self.ARRAY_CODES:
try:
- underlying = array_or_list(code, values)
+ underlying = self._array_or_list(code, values)
break
except OverflowError:
pass
@@ -88,11 +87,19 @@ class IntList(Sequence[int]):
for v in underlying:
if not isinstance(v, int) or v < 0:
raise ValueError(f"Could not create IntList for {values!r}")
- self.__underlying = underlying
+ self.__underlying: Union[list[int], "ArrayType[int]"] = underlying
@classmethod
def of_length(cls, n: int) -> "IntList":
- return cls(array_or_list("B", [0]) * n)
+ return cls(array.array("B", [0]) * n)
+
+ @staticmethod
+ def _array_or_list(
+ code: str, contents: Iterable[int]
+ ) -> Union[list[int], "ArrayType[int]"]:
+ if code == "O":
+ return list(contents)
+ return array.array(code, contents)
def count(self, value: int) -> int:
return self.__underlying.count(value)
@@ -140,9 +147,14 @@ class IntList(Sequence[int]):
return self.__underlying != other.__underlying
def append(self, n: int) -> None:
- i = len(self)
- self.__underlying.append(0)
- self[i] = n
+ # try the fast path of appending n first. If this overflows, use the
+ # __setitem__ path, which will upgrade the underlying array.
+ try:
+ self.__underlying.append(n)
+ except OverflowError:
+ i = len(self.__underlying)
+ self.__underlying.append(0)
+ self[i] = n
def __setitem__(self, i: int, n: int) -> None:
while True:
@@ -159,8 +171,8 @@ class IntList(Sequence[int]):
def __upgrade(self) -> None:
assert isinstance(self.__underlying, array.array)
- code = NEXT_ARRAY_CODE[self.__underlying.typecode]
- self.__underlying = array_or_list(code, self.__underlying)
+ code = self.NEXT_ARRAY_CODE[self.__underlying.typecode]
+ self.__underlying = self._array_or_list(code, self.__underlying)
def binary_search(lo: int, hi: int, f: Callable[[int], bool]) -> int:
@@ -192,7 +204,7 @@ class LazySequenceCopy(Generic[T]):
self.__values = values
self.__len = len(values)
self.__mask: Optional[dict[int, T]] = None
- self.__popped_indices: Optional[SortedList] = None
+ self.__popped_indices: Optional[SortedList[int]] = None
def __len__(self) -> int:
if self.__popped_indices is None:
@@ -283,6 +295,74 @@ def stack_depth_of_caller() -> int:
return size
+class StackframeLimiter:
+ # StackframeLimiter is used to make the recursion limit warning issued via
+ # ensure_free_stackframes thread-safe. We track the known values we have
+ # passed to sys.setrecursionlimit in _known_limits, and only issue a warning
+ # if sys.getrecursionlimit is not in _known_limits.
+ #
+ # This will always be an under-approximation of when we would ideally issue
+ # this warning, since a non-hypothesis caller could coincidentaly set the
+ # recursion limit to one of our known limits. Currently, StackframeLimiter
+ # resets _known_limits whenever all of the ensure_free_stackframes contexts
+ # have exited. We could increase the power of the warning by tracking a
+ # refcount for each limit, and removing it as soon as the refcount hits zero.
+ # I didn't think this extra complexity is worth the minor power increase for
+ # what is already only a "nice to have" warning.
+
+ def __init__(self):
+ self._active_contexts = 0
+ self._known_limits: set[int] = set()
+ self._original_limit: Optional[int] = None
+
+ def _setrecursionlimit(self, new_limit: int, *, check: bool = True) -> None:
+ if check and sys.getrecursionlimit() not in self._known_limits:
+ warnings.warn(
+ "The recursion limit will not be reset, since it was changed "
+ "during test execution.",
+ HypothesisWarning,
+ stacklevel=4,
+ )
+ return
+
+ self._known_limits.add(new_limit)
+ sys.setrecursionlimit(new_limit)
+
+ def enter_context(self, new_limit: int, *, current_limit: int) -> None:
+ if self._active_contexts == 0:
+ # this is the first context on the stack. Record the true original
+ # limit, to restore later.
+ assert self._original_limit is None
+ self._original_limit = current_limit
+ self._known_limits.add(self._original_limit)
+
+ self._active_contexts += 1
+ self._setrecursionlimit(new_limit)
+
+ def exit_context(self, new_limit: int, *, check: bool = True) -> None:
+ assert self._active_contexts > 0
+ self._active_contexts -= 1
+
+ if self._active_contexts == 0:
+ # this is the last context to exit. Restore the true original
+ # limit and clear our known limits.
+ original_limit = self._original_limit
+ assert original_limit is not None
+ try:
+ self._setrecursionlimit(original_limit, check=check)
+ finally:
+ self._original_limit = None
+ # we want to clear the known limits, but preserve the limit
+ # we just set it to as known.
+ self._known_limits = {original_limit}
+ else:
+ self._setrecursionlimit(new_limit, check=check)
+
+
+_stackframe_limiter = StackframeLimiter()
+_stackframe_limiter_lock = Lock()
+
+
class ensure_free_stackframes:
"""Context manager that ensures there are at least N free stackframes (for
a reasonable value of N).
@@ -290,33 +370,37 @@ class ensure_free_stackframes:
def __enter__(self) -> None:
cur_depth = stack_depth_of_caller()
- self.old_maxdepth = sys.getrecursionlimit()
- # The default CPython recursionlimit is 1000, but pytest seems to bump
- # it to 3000 during test execution. Let's make it something reasonable:
- self.new_maxdepth = cur_depth + 2000
- # Because we add to the recursion limit, to be good citizens we also
- # add a check for unbounded recursion. The default limit is typically
- # 1000/3000, so this can only ever trigger if something really strange
- # is happening and it's hard to imagine an
- # intentionally-deeply-recursive use of this code.
- assert cur_depth <= 1000, (
- "Hypothesis would usually add %d to the stack depth of %d here, "
- "but we are already much deeper than expected. Aborting now, to "
- "avoid extending the stack limit in an infinite loop..."
- % (self.new_maxdepth - self.old_maxdepth, self.old_maxdepth)
- )
- sys.setrecursionlimit(self.new_maxdepth)
+ with _stackframe_limiter_lock:
+ self.old_limit = sys.getrecursionlimit()
+ # The default CPython recursionlimit is 1000, but pytest seems to bump
+ # it to 3000 during test execution. Let's make it something reasonable:
+ self.new_limit = cur_depth + 2000
+ # Because we add to the recursion limit, to be good citizens we also
+ # add a check for unbounded recursion. The default limit is typically
+ # 1000/3000, so this can only ever trigger if something really strange
+ # is happening and it's hard to imagine an
+ # intentionally-deeply-recursive use of this code.
+ assert cur_depth <= 1000, (
+ "Hypothesis would usually add %d to the stack depth of %d here, "
+ "but we are already much deeper than expected. Aborting now, to "
+ "avoid extending the stack limit in an infinite loop..."
+ % (self.new_limit - self.old_limit, self.old_limit)
+ )
+ try:
+ _stackframe_limiter.enter_context(
+ self.new_limit, current_limit=self.old_limit
+ )
+ except Exception:
+ # if the stackframe limiter raises a HypothesisWarning (under eg
+ # -Werror), __exit__ is not called, since we errored in __enter__.
+ # Preserve the state of the stackframe limiter by exiting, and
+ # avoid showing a duplicate warning with check=False.
+ _stackframe_limiter.exit_context(self.old_limit, check=False)
+ raise
def __exit__(self, *args, **kwargs):
- if self.new_maxdepth == sys.getrecursionlimit():
- sys.setrecursionlimit(self.old_maxdepth)
- else: # pragma: no cover
- warnings.warn(
- "The recursion limit will not be reset, since it was changed "
- "from another thread or during execution of a test.",
- HypothesisWarning,
- stacklevel=2,
- )
+ with _stackframe_limiter_lock:
+ _stackframe_limiter.exit_context(self.old_limit)
def find_integer(f: Callable[[int], bool]) -> int:
@@ -411,6 +495,11 @@ _perf_counter = time.perf_counter
def gc_cumulative_time() -> float:
global _gc_initialized
+
+ # I don't believe we need a lock for the _gc_cumulative_time increment here,
+ # since afaik each gc callback is only executed once when the garbage collector
+ # runs, by the thread which initiated the gc.
+
if not _gc_initialized:
if hasattr(gc, "callbacks"):
# CPython
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py
index 73f3e6a533a..fea0f118546 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py
@@ -87,6 +87,7 @@ def dominance(left: ConjectureResult, right: ConjectureResult) -> DominanceRelat
# the dominance relationship.
if (
left.status == Status.INTERESTING
+ and right.interesting_origin is not None
and left.interesting_origin != right.interesting_origin
):
return DominanceRelation.NO_DOMINANCE
@@ -229,7 +230,7 @@ class ParetoFront:
already_replaced = True
dominators[j] = candidate
j += 1
- else:
+ else: # pragma: no cover # flaky, by test_database_contains_only_pareto_front
dominators[j], dominators[-1] = (
dominators[-1],
dominators[j],
@@ -248,7 +249,7 @@ class ParetoFront:
i -= 1
for v in to_remove:
- self.__remove(v)
+ self._remove(v)
return data in self.front
finally:
self.__pending = None
@@ -259,9 +260,14 @@ class ParetoFront:
self.__eviction_listeners.append(f)
def __contains__(self, data: object) -> bool:
- return isinstance(data, (ConjectureData, ConjectureResult)) and (
- data.as_result() in self.front
- )
+ if not isinstance(data, (ConjectureData, ConjectureResult)):
+ return False
+
+ result = data.as_result()
+ if isinstance(result, _Overrun):
+ return False
+
+ return result in self.front
def __iter__(self) -> Iterator[ConjectureResult]:
return iter(self.front)
@@ -272,7 +278,7 @@ class ParetoFront:
def __len__(self) -> int:
return len(self.front)
- def __remove(self, data: ConjectureResult) -> None:
+ def _remove(self, data: ConjectureResult) -> None:
try:
self.front.remove(data)
except ValueError:
@@ -337,10 +343,7 @@ class ParetoOptimiser:
# must be dominated in the front - either ``destination`` is in
# the front, or it was not added to it because it was
# dominated by something in it.
- try:
- self.front.front.remove(source)
- except ValueError:
- pass
+ self.front._remove(source)
return True
return False
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py
new file mode 100644
index 00000000000..f4fdd57c8f8
--- /dev/null
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py
@@ -0,0 +1,487 @@
+# 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 math
+import sys
+from collections.abc import Collection, Iterable, Sequence
+from typing import Any, Optional
+
+from hypothesis import (
+ HealthCheck,
+ assume,
+ note,
+ settings as Settings,
+ strategies as st,
+)
+from hypothesis.errors import BackendCannotProceed
+from hypothesis.internal.compat import batched
+from hypothesis.internal.conjecture.choice import (
+ ChoiceTypeT,
+ choice_permitted,
+)
+from hypothesis.internal.conjecture.data import ConjectureData
+from hypothesis.internal.conjecture.providers import (
+ COLLECTION_DEFAULT_MAX_SIZE,
+ PrimitiveProvider,
+)
+from hypothesis.internal.floats import SMALLEST_SUBNORMAL, sign_aware_lte
+from hypothesis.internal.intervalsets import IntervalSet
+from hypothesis.stateful import RuleBasedStateMachine, initialize, precondition, rule
+from hypothesis.strategies import DrawFn, SearchStrategy
+from hypothesis.strategies._internal.strings import OneCharStringStrategy, TextStrategy
+
+
+def build_intervals(intervals: list[int]) -> list[tuple[int, int]]:
+ if len(intervals) % 2:
+ intervals = intervals[:-1]
+ intervals.sort()
+ return list(batched(intervals, 2, strict=True))
+
+
+def interval_lists(
+ *, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0
+) -> SearchStrategy[Iterable[Sequence[int]]]:
+ return (
+ st.lists(
+ st.integers(min_codepoint, max_codepoint),
+ unique=True,
+ min_size=min_size * 2,
+ )
+ .map(sorted)
+ .map(build_intervals)
+ )
+
+
+def intervals(
+ *, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0
+) -> SearchStrategy[IntervalSet]:
+ return st.builds(
+ IntervalSet,
+ interval_lists(
+ min_codepoint=min_codepoint, max_codepoint=max_codepoint, min_size=min_size
+ ),
+ )
+
+
+def integer_weights(
+ draw: DrawFn, min_value: Optional[int] = None, max_value: Optional[int] = None
+) -> dict[int, float]:
+ # Sampler doesn't play well with super small floats, so exclude them
+ weights = draw(
+ st.dictionaries(
+ st.integers(min_value=min_value, max_value=max_value),
+ st.floats(0.001, 1),
+ min_size=1,
+ max_size=255,
+ )
+ )
+ # invalid to have a weighting that disallows all possibilities
+ assume(sum(weights.values()) != 0)
+ # re-normalize probabilities to sum to some arbitrary target < 1
+ target = draw(st.floats(0.001, 0.999))
+ factor = target / sum(weights.values())
+ weights = {k: v * factor for k, v in weights.items()}
+ # float rounding error can cause this to fail.
+ assume(0.001 <= sum(weights.values()) <= 0.999)
+ return weights
+
+
+def integer_constraints(
+ draw,
+ *,
+ use_min_value=None,
+ use_max_value=None,
+ use_shrink_towards=None,
+ use_weights=None,
+ use_forced=False,
+):
+ min_value = None
+ max_value = None
+ shrink_towards = 0
+ weights = None
+
+ if use_min_value is None:
+ use_min_value = draw(st.booleans())
+ if use_max_value is None:
+ use_max_value = draw(st.booleans())
+ use_shrink_towards = draw(st.booleans())
+ if use_weights is None:
+ use_weights = (
+ draw(st.booleans()) if (use_min_value and use_max_value) else False
+ )
+
+ # Invariants:
+ # (1) min_value <= forced <= max_value
+ # (2) sum(weights.values()) < 1
+ # (3) len(weights) <= 255
+
+ if use_shrink_towards:
+ shrink_towards = draw(st.integers())
+
+ forced = draw(st.integers()) if use_forced else None
+ if use_weights:
+ assert use_max_value
+ assert use_min_value
+
+ min_value = draw(st.integers(max_value=forced))
+ min_val = max(min_value, forced) if forced is not None else min_value
+ max_value = draw(st.integers(min_value=min_val))
+
+ weights = draw(integer_weights(min_value, max_value))
+ else:
+ if use_min_value:
+ min_value = draw(st.integers(max_value=forced))
+ if use_max_value:
+ min_vals = []
+ if min_value is not None:
+ min_vals.append(min_value)
+ if forced is not None:
+ min_vals.append(forced)
+ min_val = max(min_vals) if min_vals else None
+ max_value = draw(st.integers(min_value=min_val))
+
+ if forced is not None:
+ assume((forced - shrink_towards).bit_length() < 128)
+
+ return {
+ "min_value": min_value,
+ "max_value": max_value,
+ "shrink_towards": shrink_towards,
+ "weights": weights,
+ "forced": forced,
+ }
+
+
+def _collection_constraints(
+ draw: DrawFn,
+ *,
+ forced: Optional[Any],
+ use_min_size: Optional[bool] = None,
+ use_max_size: Optional[bool] = None,
+) -> dict[str, int]:
+ min_size = 0
+ max_size = COLLECTION_DEFAULT_MAX_SIZE
+ # collections are quite expensive in entropy. cap to avoid overruns.
+ cap = 50
+
+ if use_min_size is None:
+ use_min_size = draw(st.booleans())
+ if use_max_size is None:
+ use_max_size = draw(st.booleans())
+
+ if use_min_size:
+ min_size = draw(
+ st.integers(0, min(len(forced), cap) if forced is not None else cap)
+ )
+
+ if use_max_size:
+ max_size = draw(
+ st.integers(
+ min_value=min_size if forced is None else max(min_size, len(forced))
+ )
+ )
+ if forced is None:
+ # cap to some reasonable max size to avoid overruns.
+ max_size = min(max_size, min_size + 100)
+
+ return {"min_size": min_size, "max_size": max_size}
+
+
+def string_constraints(
+ draw: DrawFn,
+ *,
+ use_min_size: Optional[bool] = None,
+ use_max_size: Optional[bool] = None,
+ use_forced: bool = False,
+) -> Any:
+ interval_set = draw(intervals())
+ forced = (
+ draw(TextStrategy(OneCharStringStrategy(interval_set))) if use_forced else None
+ )
+ constraints = draw(
+ _collection_constraints(
+ forced=forced, use_min_size=use_min_size, use_max_size=use_max_size
+ )
+ )
+ # if the intervalset is empty, then the min size must be zero, because the
+ # only valid value is the empty string.
+ if len(interval_set) == 0:
+ constraints["min_size"] = 0
+
+ return {"intervals": interval_set, "forced": forced, **constraints}
+
+
+def bytes_constraints(
+ draw: DrawFn,
+ *,
+ use_min_size: Optional[bool] = None,
+ use_max_size: Optional[bool] = None,
+ use_forced: bool = False,
+) -> Any:
+ forced = draw(st.binary()) if use_forced else None
+
+ constraints = draw(
+ _collection_constraints(
+ forced=forced, use_min_size=use_min_size, use_max_size=use_max_size
+ )
+ )
+ return {"forced": forced, **constraints}
+
+
+def float_constraints(
+ draw,
+ *,
+ use_min_value=None,
+ use_max_value=None,
+ use_forced=False,
+):
+ if use_min_value is None:
+ use_min_value = draw(st.booleans())
+ if use_max_value is None:
+ use_max_value = draw(st.booleans())
+
+ forced = draw(st.floats()) if use_forced else None
+ pivot = forced if (use_forced and not math.isnan(forced)) else None
+ min_value = -math.inf
+ max_value = math.inf
+ smallest_nonzero_magnitude = SMALLEST_SUBNORMAL
+ allow_nan = True if (use_forced and math.isnan(forced)) else draw(st.booleans())
+
+ if use_min_value:
+ min_value = draw(st.floats(max_value=pivot, allow_nan=False))
+
+ if use_max_value:
+ if pivot is None:
+ min_val = min_value
+ else:
+ min_val = pivot if sign_aware_lte(min_value, pivot) else min_value
+ max_value = draw(st.floats(min_value=min_val, allow_nan=False))
+
+ largest_magnitude = max(abs(min_value), abs(max_value))
+ # can't force something smaller than our smallest magnitude.
+ if pivot is not None and pivot != 0.0:
+ largest_magnitude = min(largest_magnitude, pivot)
+
+ # avoid drawing from an empty range
+ if largest_magnitude > 0:
+ smallest_nonzero_magnitude = draw(
+ st.floats(
+ min_value=0,
+ # smallest_nonzero_magnitude breaks internal clamper invariants if
+ # it is allowed to be larger than the magnitude of {min, max}_value.
+ #
+ # Let's also be reasonable here; smallest_nonzero_magnitude is used
+ # for subnormals, so we will never provide a number above 1 in practice.
+ max_value=min(largest_magnitude, 1.0),
+ exclude_min=True,
+ )
+ )
+
+ assert sign_aware_lte(min_value, max_value)
+ return {
+ "min_value": min_value,
+ "max_value": max_value,
+ "forced": forced,
+ "allow_nan": allow_nan,
+ "smallest_nonzero_magnitude": smallest_nonzero_magnitude,
+ }
+
+
+def boolean_constraints(draw: DrawFn, *, use_forced: bool = False) -> Any:
+ forced = draw(st.booleans()) if use_forced else None
+ # avoid invalid forced combinations
+ p = draw(st.floats(0, 1, exclude_min=forced is True, exclude_max=forced is False))
+
+ return {"p": p, "forced": forced}
+
+
+def constraints_strategy(choice_type, strategy_constraints=None, *, use_forced=False):
+ strategy = {
+ "boolean": boolean_constraints,
+ "integer": integer_constraints,
+ "float": float_constraints,
+ "bytes": bytes_constraints,
+ "string": string_constraints,
+ }[choice_type]
+ if strategy_constraints is None:
+ strategy_constraints = {}
+ return strategy(**strategy_constraints.get(choice_type, {}), use_forced=use_forced)
+
+
+def choice_types_constraints(strategy_constraints=None, *, use_forced=False):
+ options: list[ChoiceTypeT] = ["boolean", "integer", "float", "bytes", "string"]
+ return st.one_of(
+ st.tuples(
+ st.just(name),
+ constraints_strategy(name, strategy_constraints, use_forced=use_forced),
+ )
+ for name in options
+ )
+
+
+def run_conformance_test(
+ Provider: type[PrimitiveProvider],
+ *,
+ context_manager_exceptions: Collection[type[BaseException]] = (),
+ settings: Optional[Settings] = None,
+ _realize_objects: SearchStrategy[Any] = (
+ st.from_type(object) | st.from_type(type).flatmap(st.from_type)
+ ),
+) -> None:
+ """
+ Test that the given ``Provider`` class conforms to the |PrimitiveProvider|
+ interface.
+
+ For instance, this tests that ``Provider`` does not return out of bounds
+ choices from any of the ``draw_*`` methods, or violate other invariants
+ depended on by Hypothesis.
+
+ This function is intended to be called at test-time, not at runtime. It is
+ provided by Hypothesis to make it easy for third-party backend authors to
+ test their provider. Backend authors wishing to test their provider should
+ include a test similar to the following in their test suite:
+
+ .. code-block:: python
+
+ from hypothesis.internal.conjecture.provider_conformance import run_conformance_test
+
+ def test_conformance():
+ run_conformance_test(MyProvider)
+
+ If your provider can raise control flow exceptions inside one of the five
+ ``draw_*`` methods that are handled by your provider's
+ ``per_test_case_context_manager``, pass a list of these exceptions types to
+ ``context_manager_exceptions``. Otherwise, ``run_conformance_test`` will
+ treat those exceptions as fatal errors.
+ """
+
+ @Settings(settings, suppress_health_check=[HealthCheck.too_slow])
+ class ProviderConformanceTest(RuleBasedStateMachine):
+ def __init__(self):
+ super().__init__()
+
+ @initialize(random=st.randoms())
+ def setup(self, random):
+ if Provider.lifetime == "test_case":
+ data = ConjectureData(random=random, provider=Provider)
+ self.provider = data.provider
+ else:
+ self.provider = Provider(None)
+
+ self.context_manager = self.provider.per_test_case_context_manager()
+ self.context_manager.__enter__()
+ self.frozen = False
+
+ def _draw(self, choice_type, constraints):
+ del constraints["forced"]
+ draw_func = getattr(self.provider, f"draw_{choice_type}")
+
+ try:
+ choice = draw_func(**constraints)
+ note(f"drew {choice_type} {choice}")
+ expected_type = {
+ "integer": int,
+ "float": float,
+ "bytes": bytes,
+ "string": str,
+ "boolean": bool,
+ }[choice_type]
+ assert isinstance(choice, expected_type)
+ assert choice_permitted(choice, constraints)
+ except context_manager_exceptions as e:
+ note(f"caught exception {type(e)} in context_manager_exceptions: {e}")
+ try:
+ self.context_manager.__exit__(type(e), e, None)
+ except BackendCannotProceed:
+ self.frozen = True
+ return None
+
+ return choice
+
+ @precondition(lambda self: not self.frozen)
+ @rule(constraints=integer_constraints())
+ def draw_integer(self, constraints):
+ self._draw("integer", constraints)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(constraints=float_constraints())
+ def draw_float(self, constraints):
+ self._draw("float", constraints)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(constraints=bytes_constraints())
+ def draw_bytes(self, constraints):
+ self._draw("bytes", constraints)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(constraints=string_constraints())
+ def draw_string(self, constraints):
+ self._draw("string", constraints)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(constraints=boolean_constraints())
+ def draw_boolean(self, constraints):
+ self._draw("boolean", constraints)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(label=st.integers())
+ def span_start(self, label):
+ self.provider.span_start(label)
+
+ @precondition(lambda self: not self.frozen)
+ @rule(discard=st.booleans())
+ def span_end(self, discard):
+ self.provider.span_end(discard)
+
+ @precondition(lambda self: not self.frozen)
+ @rule()
+ def freeze(self):
+ # phase-transition, mimicking data.freeze() at the end of a test case.
+ self.frozen = True
+ self.context_manager.__exit__(None, None, None)
+
+ @precondition(lambda self: self.frozen)
+ @rule(value=_realize_objects)
+ def realize(self, value):
+ # filter out nans and weirder things
+ try:
+ assume(value == value)
+ except Exception:
+ # e.g. value = Decimal('-sNaN')
+ assume(False)
+
+ # if `value` is non-symbolic, the provider should return it as-is.
+ assert self.provider.realize(value) == value
+
+ @precondition(lambda self: self.frozen)
+ @rule()
+ def observe_test_case(self):
+ observations = self.provider.observe_test_case()
+ assert isinstance(observations, dict)
+
+ @precondition(lambda self: self.frozen)
+ @rule(lifetime=st.sampled_from(["test_function", "test_case"]))
+ def observe_information_messages(self, lifetime):
+ observations = self.provider.observe_information_messages(lifetime=lifetime)
+ for observation in observations:
+ assert isinstance(observation, dict)
+
+ def teardown(self):
+ if not self.frozen:
+ self.context_manager.__exit__(None, None, None)
+
+ ProviderConformanceTest.TestCase().runTest()
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py
index beccc75c361..827969360a9 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py
@@ -11,14 +11,18 @@
import abc
import contextlib
import math
+import sys
import warnings
from collections.abc import Iterable
+from contextlib import AbstractContextManager
+from functools import cached_property
from random import Random
from sys import float_info
+from types import ModuleType
from typing import (
TYPE_CHECKING,
Any,
- Callable,
+ ClassVar,
Literal,
Optional,
TypedDict,
@@ -26,15 +30,20 @@ from typing import (
Union,
)
+from sortedcontainers import SortedSet
+
from hypothesis.errors import HypothesisWarning
from hypothesis.internal.cache import LRUCache
from hypothesis.internal.compat import WINDOWS, int_from_bytes
from hypothesis.internal.conjecture.choice import (
- StringConstraints,
+ ChoiceConstraintsT,
+ ChoiceT,
+ ChoiceTypeT,
+ FloatConstraints,
choice_constraints_key,
choice_permitted,
)
-from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float
+from hypothesis.internal.conjecture.floats import lex_to_float
from hypothesis.internal.conjecture.junkdrawer import bits_to_bytes
from hypothesis.internal.conjecture.utils import (
INT_SIZES,
@@ -42,46 +51,73 @@ from hypothesis.internal.conjecture.utils import (
Sampler,
many,
)
+from hypothesis.internal.constants_ast import (
+ Constants,
+ constants_from_module,
+ is_local_module_file,
+)
from hypothesis.internal.floats import (
SIGNALING_NAN,
float_to_int,
make_float_clamper,
next_down,
next_up,
- sign_aware_lte,
)
from hypothesis.internal.intervalsets import IntervalSet
+from hypothesis.internal.observability import InfoObservationType, TestCaseObservation
if TYPE_CHECKING:
from typing import TypeAlias
from hypothesis.internal.conjecture.data import ConjectureData
+ from hypothesis.internal.constants_ast import ConstantT
T = TypeVar("T")
-_Lifetime: "TypeAlias" = Literal["test_case", "test_function"]
+LifetimeT: "TypeAlias" = Literal["test_case", "test_function"]
COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large"
-# The available `PrimitiveProvider`s, and therefore also the available backends
-# for use by @settings(backend=...). The key is the name to be used in the backend=
-# value, and the value is the importable path to a subclass of PrimitiveProvider.
-#
-# See also
-# https://hypothesis.readthedocs.io/en/latest/strategies.html#alternative-backends-for-hypothesis.
-#
-# NOTE: the PrimitiveProvider interface is not yet stable. We may continue to
-# make breaking changes to it. (but if you want to experiment and don't mind
-# breakage, here you go!)
+#: Registered Hypothesis backends. This is a dictionary whose keys are the name
+#: to be used in |settings.backend|, and whose values are a string of the absolute
+#: importable path to a subclass of |PrimitiveProvider|, which Hypothesis will
+#: instantiate when your backend is requested by a test's |settings.backend| value.
+#:
+#: For example, the default Hypothesis backend is registered as:
+#:
+#: .. code-block:: python
+#:
+#: from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS
+#:
+#: AVAILABLE_PROVIDERS["hypothesis"] = "hypothesis.internal.conjecture.providers.HypothesisProvider"
+#:
+#: And can be used with:
+#:
+#: .. code-block:: python
+#:
+#: from hypothesis import given, settings, strategies as st
+#:
+#: @given(st.integers())
+#: @settings(backend="hypothesis")
+#: def f(n):
+#: pass
+#:
+#: Though, as ``backend="hypothesis"`` is the default setting, the above would
+#: typically not have any effect.
+#:
+#: The purpose of mapping to an absolute importable path, rather than the actual
+#: |PrimitiveProvider| class, is to avoid slowing down Hypothesis startup times
+#: by only importing alternative backends when required.
AVAILABLE_PROVIDERS = {
"hypothesis": "hypothesis.internal.conjecture.providers.HypothesisProvider",
"hypothesis-urandom": "hypothesis.internal.conjecture.providers.URandomProvider",
}
-FLOAT_INIT_LOGIC_CACHE = LRUCache(4096)
-STRING_SAMPLER_CACHE = LRUCache(64)
+# cache the choice_permitted constants for a particular set of constraints.
+CacheKeyT: "TypeAlias" = tuple[ChoiceTypeT, tuple[Any, ...]]
+CacheValueT: "TypeAlias" = tuple[tuple["ConstantT", ...], tuple["ConstantT", ...]]
+CONSTANTS_CACHE: LRUCache[CacheKeyT, CacheValueT] = LRUCache(1024)
-NASTY_FLOATS = sorted(
+_constant_floats = (
[
- 0.0,
0.5,
1.1,
1.5,
@@ -94,7 +130,7 @@ NASTY_FLOATS = sorted(
float_info.min,
float_info.max,
3.402823466e38,
- 9007199254740992,
+ 9007199254740992.0,
1 - 10e-6,
2 + 10e-6,
1.192092896e-07,
@@ -102,185 +138,260 @@ NASTY_FLOATS = sorted(
]
+ [2.0**-n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32
+ [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64
- + [math.inf, math.nan] * 5
- + [SIGNALING_NAN],
- key=float_to_lex,
)
-NASTY_FLOATS = list(map(float, NASTY_FLOATS))
-NASTY_FLOATS.extend([-x for x in NASTY_FLOATS])
+_constant_floats.extend([-x for x in _constant_floats])
+assert all(isinstance(f, float) for f in _constant_floats)
-NASTY_STRINGS = sorted(
- [
- # strings which can be interpreted as code / logic
- "undefined",
- "null",
- "NULL",
- "nil",
- "NIL",
- "true",
- "false",
- "True",
- "False",
- "TRUE",
- "FALSE",
- "None",
- "none",
- "if",
- "then",
- "else",
- # strings which can be interpreted as a number
- "0",
- "1e100",
- "0..0",
- "0/0",
- "1/0",
- "+0.0",
- "Infinity",
- "-Infinity",
- "Inf",
- "INF",
- "NaN",
- "9" * 30,
- # common ascii characters
- ",./;'[]\\-=<>?:\"{}|_+!@#$%^&*()`~",
- # common unicode characters
- "Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ“‘¡™£¢∞§¶•ªº–≠¸˛Ç◊ı˜Â¯˘¿ÅÍÎÏ˝ÓÔÒÚÆ☃Œ„´‰ˇÁ¨ˆØ∏”’`⁄€‹›fifl‡°·‚—±",
- # characters which increase in length when lowercased
- "Ⱥ",
- "Ⱦ",
- # ligatures
- "æœÆŒffʤʨß"
- # emoticons
- "(╯°□°)╯︵ ┻━┻)",
- # emojis
- "😍",
- "🇺🇸",
- # emoji modifiers
- "🏻" # U+1F3FB Light Skin Tone,
- "👍🏻", # 👍 followed by U+1F3FB
- # RTL text
- "الكل في المجمو عة",
- # Ogham text, which contains the only character in the Space Separators
- # unicode category (Zs) that isn't visually blank:  . # noqa: RUF003
- "᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜",
- # readable variations on text (bolt/italic/script)
- "𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠",
- "𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌",
- "𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈",
- "𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰",
- "𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘",
- # upsidown text
- "ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥",
- # reserved strings in windows
- "NUL",
- "COM1",
- "LPT1",
- # scunthorpe problem
- "Scunthorpe",
- # zalgo text
- "Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣",
- #
- # examples from https://faultlore.com/blah/text-hates-you/
- "मनीष منش",
- "पन्ह पन्ह त्र र्च कृकृ ड्ड न्हृे إلا بسم الله",
- "lorem لا بسم الله ipsum 你好1234你好",
- ],
- key=len,
+_constant_strings = {
+ # strings which can be interpreted as code / logic
+ "undefined",
+ "null",
+ "NULL",
+ "nil",
+ "NIL",
+ "true",
+ "false",
+ "True",
+ "False",
+ "TRUE",
+ "FALSE",
+ "None",
+ "none",
+ "if",
+ "then",
+ "else",
+ # strings which can be interpreted as a number
+ "0",
+ "1e100",
+ "0..0",
+ "0/0",
+ "1/0",
+ "+0.0",
+ "Infinity",
+ "-Infinity",
+ "Inf",
+ "INF",
+ "NaN",
+ "9" * 30,
+ # common ascii characters
+ ",./;'[]\\-=<>?:\"{}|_+!@#$%^&*()`~",
+ # common unicode characters
+ "Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ“‘¡™£¢∞§¶•ªº–≠¸˛Ç◊ı˜Â¯˘¿ÅÍÎÏ˝ÓÔÒÚÆ☃Œ„´‰ˇÁ¨ˆØ∏”’`⁄€‹›fifl‡°·‚—±",
+ # characters which increase in length when lowercased
+ "Ⱥ",
+ "Ⱦ",
+ # ligatures
+ "æœÆŒffʤʨß"
+ # emoticons
+ "(╯°□°)╯︵ ┻━┻)",
+ # emojis
+ "😍",
+ "🇺🇸",
+ # emoji modifiers
+ "🏻" # U+1F3FB Light Skin Tone,
+ "👍🏻", # 👍 followed by U+1F3FB
+ # RTL text
+ "الكل في المجمو عة",
+ # Ogham text, which contains the only character in the Space Separators
+ # unicode category (Zs) that isn't visually blank:  . # noqa: RUF003
+ "᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜",
+ # readable variations on text (bolt/italic/script)
+ "𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠",
+ "𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌",
+ "𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈",
+ "𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰",
+ "𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘",
+ # upsidown text
+ "ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥",
+ # reserved strings in windows
+ "NUL",
+ "COM1",
+ "LPT1",
+ # scunthorpe problem
+ "Scunthorpe",
+ # zalgo text
+ "Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣",
+ #
+ # examples from https://faultlore.com/blah/text-hates-you/
+ "मनीष منش",
+ "पन्ह पन्ह त्र र्च कृकृ ड्ड न्हृे إلا بسم الله",
+ "lorem لا بسم الله ipsum 你好1234你好",
+}
+
+
+# we don't actually care what order the constants are sorted in, just that the
+# ordering is deterministic.
+GLOBAL_CONSTANTS = Constants(
+ integers=SortedSet(),
+ floats=SortedSet(_constant_floats, key=float_to_int),
+ bytes=SortedSet(),
+ strings=SortedSet(_constant_strings),
)
-# Masks for masking off the first byte of an n-bit buffer.
-# The appropriate mask is stored at position n % 8.
-BYTE_MASKS = [(1 << n) - 1 for n in range(8)]
-BYTE_MASKS[0] = 255
+_local_constants = Constants(
+ integers=SortedSet(),
+ floats=SortedSet(key=float_to_int),
+ bytes=SortedSet(),
+ strings=SortedSet(),
+)
+# modules that we've already seen and processed for local constants. These are
+# are all modules, not necessarily local ones. This lets us quickly see which
+# modules are new without an expensive path.resolve() or is_local_module_file
+# cache lookup.
+_seen_modules: set[ModuleType] = set()
+_sys_modules_len: Optional[int] = None
+
+
+def _get_local_constants() -> Constants:
+ global _sys_modules_len, _local_constants
+
+ if sys.platform == "emscripten": # pragma: no cover
+ # pyodide builds bundle the stdlib in a nonstandard location, like
+ # `/lib/python312.zip/heapq.py`. To avoid identifying the entirety of
+ # the stdlib as local code and slowing down on emscripten, instead return
+ # that nothing is local.
+ #
+ # pyodide may provide some way to distinguish stdlib/third-party/local
+ # code. I haven't looked into it. If they do, we should correctly implement
+ # ModuleLocation for pyodide instead of this.
+ return _local_constants
+
+ count_constants = len(_local_constants)
+ # We call this function once per HypothesisProvider instance, i.e. once per
+ # input, so it needs to be performant. The logic here is more complicated
+ # than necessary because of this.
+ #
+ # First, we check whether there are any new modules with a very cheap length
+ # check. This check can be fooled if a module is added while another module is
+ # removed, but the more correct check against tuple(sys.modules.keys()) is
+ # substantially more expensive. Such a new module would eventually be discovered
+ # if / when the length changes again in the future.
+ #
+ # If the length has changed, we find just modules we haven't seen before. Of
+ # those, we find the ones which correspond to local modules, and extract their
+ # constants.
+
+ # careful: store sys.modules length when we first check to avoid race conditions
+ # with other threads loading a module before we set _sys_modules_len.
+ if (sys_modules_len := len(sys.modules)) != _sys_modules_len:
+ # set(_seen_modules) shouldn't typically be required, but I have run into
+ # a "set changed size during iteration" error here when running
+ # test_provider_conformance_crosshair.
+ new_modules = set(sys.modules.values()) - set(_seen_modules)
+ # Repeated SortedSet unions are expensive. Do the initial unions on a
+ # set(), then do a one-time union with _local_constants after.
+ new_constants = Constants()
+ for module in new_modules:
+ if (
+ module_file := getattr(module, "__file__", None)
+ ) is not None and is_local_module_file(module_file):
+ new_constants |= constants_from_module(module)
+ _local_constants |= new_constants
+ _seen_modules.update(new_modules)
+ _sys_modules_len = sys_modules_len
+
+ # if we add any new constant, invalidate the constant cache for permitted values.
+ # A more efficient approach would be invalidating just the keys with this
+ # choice_type.
+ if len(_local_constants) > count_constants:
+ CONSTANTS_CACHE.cache.clear()
+
+ return _local_constants
class _BackendInfoMsg(TypedDict):
- type: str
+ type: InfoObservationType
title: str
content: Union[str, dict[str, Any]]
-class PrimitiveProvider(abc.ABC):
- # This is the low-level interface which would also be implemented
- # by e.g. CrossHair, by an Atheris-hypothesis integration, etc.
- # We'd then build the structured tree handling, database and replay
- # support, etc. on top of this - so all backends get those for free.
- #
- # See https://github.com/HypothesisWorks/hypothesis/issues/3086
+# TODO_DOCS: link to choice sequence explanation page
- # How long a provider instance is used for. One of test_function or
- # test_case. Defaults to test_function.
- #
- # If test_function, a single provider instance will be instantiated and used
- # for the entirety of each test function. I.e., roughly one provider per
- # @given annotation. This can be useful if you need to track state over many
- # executions to a test function.
- #
- # This lifetime will cause None to be passed for the ConjectureData object
- # in PrimitiveProvider.__init__, because that object is instantiated per
- # test case.
- #
- # If test_case, a new provider instance will be instantiated and used each
- # time hypothesis tries to generate a new input to the test function. This
- # lifetime can access the passed ConjectureData object.
- #
- # Non-hypothesis providers probably want to set a lifetime of test_function.
- lifetime: _Lifetime = "test_function"
- # Solver-based backends such as hypothesis-crosshair use symbolic values
- # which record operations performed on them in order to discover new paths.
- # If avoid_realization is set to True, hypothesis will avoid interacting with
- # symbolic choices returned by the provider in any way that would force the
- # solver to narrow the range of possible values for that symbolic.
- #
- # Setting this to True disables some hypothesis features, such as
- # DataTree-based deduplication, and some internal optimizations, such as
- # caching constraints. Only enable this if it is necessary for your backend.
- avoid_realization = False
+class PrimitiveProvider(abc.ABC):
+ """
+ |PrimitiveProvider| is the implementation interface of a
+ :ref:`Hypothesis backend <alternative-backends>`.
- def __init__(self, conjecturedata: Optional["ConjectureData"], /) -> None:
- self._cd = conjecturedata
+ A |PrimitiveProvider| is required to implement the following five
+ ``draw_*`` methods:
- def per_test_case_context_manager(self):
- return contextlib.nullcontext()
+ * |PrimitiveProvider.draw_integer|
+ * |PrimitiveProvider.draw_boolean|
+ * |PrimitiveProvider.draw_float|
+ * |PrimitiveProvider.draw_string|
+ * |PrimitiveProvider.draw_bytes|
- def realize(self, value: T) -> T:
- """
- Called whenever hypothesis requires a concrete (non-symbolic) value from
- a potentially symbolic value. Hypothesis will not check that `value` is
- symbolic before calling `realize`, so you should handle the case where
- `value` is non-symbolic.
+ Each strategy in Hypothesis generates values by drawing a series of choices
+ from these five methods. By overriding them, a |PrimitiveProvider| can control
+ the distribution of inputs generated by Hypothesis.
- The returned value should be non-symbolic. If you cannot provide a value,
- raise hypothesis.errors.BackendCannotProceed("discard_test_case")
- """
- return value
+ For example, :pypi:`hypothesis-crosshair` implements a |PrimitiveProvider|
+ which uses an SMT solver to generate inputs that uncover new branches.
- def observe_test_case(self) -> dict[str, Any]:
- """Called at the end of the test case when observability mode is active.
+ Once you implement a |PrimitiveProvider|, you can make it available for use
+ through |AVAILABLE_PROVIDERS|.
+ """
- The return value should be a non-symbolic json-encodable dictionary,
- and will be included as `observation["metadata"]["backend"]`.
- """
- return {}
+ #: The lifetime of a |PrimitiveProvider| instance. Either ``test_function``
+ #: or ``test_case``.
+ #:
+ #: If ``test_function`` (the default), a single provider instance will be
+ #: instantiated and used for the entirety of each test function (i.e., roughly
+ #: one provider per |@given| annotation). This can be useful for tracking state
+ #: over the entirety of a test function.
+ #:
+ #: If ``test_case``, a new provider instance will be instantiated and used for
+ #: each input Hypothesis generates.
+ #:
+ #: The ``conjecturedata`` argument to ``PrimitiveProvider.__init__`` will
+ #: be ``None`` for a lifetime of ``test_function``, and an instance of
+ #: ``ConjectureData`` for a lifetime of ``test_case``.
+ #:
+ #: Third-party providers likely want to set a lifetime of ``test_function``.
+ lifetime: ClassVar[LifetimeT] = "test_function"
- def observe_information_messages(
- self, *, lifetime: _Lifetime
- ) -> Iterable[_BackendInfoMsg]:
- """Called at the end of each test case and again at end of the test function.
+ #: Solver-based backends such as ``hypothesis-crosshair`` use symbolic values
+ #: which record operations performed on them in order to discover new paths.
+ #: If ``avoid_realization`` is set to ``True``, hypothesis will avoid interacting
+ #: with symbolic choices returned by the provider in any way that would force
+ #: the solver to narrow the range of possible values for that symbolic.
+ #:
+ #: Setting this to ``True`` disables some hypothesis features and optimizations.
+ #: Only set this to ``True`` if it is necessary for your backend.
+ avoid_realization: ClassVar[bool] = False
- Return an iterable of `{type: info/alert/error, title: str, content: str|dict}`
- dictionaries to be delivered as individual information messages.
- (Hypothesis adds the `run_start` timestamp and `property` name for you.)
- """
- assert lifetime in ("test_case", "test_function")
- yield from []
+ #: If ``True``, |PrimitiveProvider.on_observation| will be added as a
+ #: callback to |TESTCASE_CALLBACKS|, enabling observability during the lifetime
+ #: of this provider. If ``False``, |PrimitiveProvider.on_observation| will
+ #: never be called by Hypothesis.
+ #:
+ #: The opt-in behavior of observability is because enabling observability
+ #: might increase runtime or memory usage.
+ add_observability_callback: ClassVar[bool] = False
+
+ def __init__(self, conjecturedata: Optional["ConjectureData"], /) -> None:
+ self._cd = conjecturedata
@abc.abstractmethod
def draw_boolean(
self,
p: float = 0.5,
) -> bool:
+ """
+ Draw a boolean choice.
+
+ Parameters
+ ----------
+ p: float
+ The probability of returning ``True``. Between 0 and 1 inclusive.
+
+ Except for ``0`` and ``1``, the value of ``p`` is a hint provided by
+ Hypothesis, and may be ignored by the backend.
+
+ If ``0``, the provider must return ``False``. If ``1``, the provider
+ must return ``True``.
+ """
raise NotImplementedError
@abc.abstractmethod
@@ -289,10 +400,27 @@ class PrimitiveProvider(abc.ABC):
min_value: Optional[int] = None,
max_value: Optional[int] = None,
*,
- # weights are for choosing an element index from a bounded range
weights: Optional[dict[int, float]] = None,
shrink_towards: int = 0,
) -> int:
+ """
+ Draw an integer choice.
+
+ Parameters
+ ----------
+ min_value : int | None
+ (Inclusive) lower bound on the integer value. If ``None``, there is
+ no lower bound.
+ max_value : int | None
+ (Inclusive) upper bound on the integer value. If ``None``, there is
+ no upper bound.
+ weights: dict[int, float] | None
+ Maps keys in the range [``min_value``, ``max_value``] to the probability
+ of returning that key.
+ shrink_towards: int
+ The integer to shrink towards. This is not used during generation and
+ can be ignored by backends.
+ """
raise NotImplementedError
@abc.abstractmethod
@@ -303,11 +431,22 @@ class PrimitiveProvider(abc.ABC):
max_value: float = math.inf,
allow_nan: bool = True,
smallest_nonzero_magnitude: float,
- # TODO: consider supporting these float widths at the IR level in the
- # future.
- # width: Literal[16, 32, 64] = 64,
- # exclude_min and exclude_max handled higher up,
) -> float:
+ """
+ Draw a float choice.
+
+ Parameters
+ ----------
+ min_value : float
+ (Inclusive) lower bound on the float value.
+ max_value : float
+ (Inclusive) upper bound on the float value.
+ allow_nan : bool
+ If ``False``, it is invalid to return ``math.nan``.
+ smallest_nonzero_magnitude : float
+ The smallest allowed nonzero magnitude. ``draw_float`` should not
+ return a float ``f`` if ``abs(f) < smallest_nonzero_magnitude``.
+ """
raise NotImplementedError
@abc.abstractmethod
@@ -318,6 +457,18 @@ class PrimitiveProvider(abc.ABC):
min_size: int = 0,
max_size: int = COLLECTION_DEFAULT_MAX_SIZE,
) -> str:
+ """
+ Draw a string choice.
+
+ Parameters
+ ----------
+ intervals : IntervalSet
+ The set of codepoints to sample from.
+ min_size : int
+ (Inclusive) lower bound on the string length.
+ max_size : int
+ (Inclusive) upper bound on the string length.
+ """
raise NotImplementedError
@abc.abstractmethod
@@ -326,27 +477,189 @@ class PrimitiveProvider(abc.ABC):
min_size: int = 0,
max_size: int = COLLECTION_DEFAULT_MAX_SIZE,
) -> bytes:
+ """
+ Draw a bytes choice.
+
+ Parameters
+ ----------
+ min_size : int
+ (Inclusive) lower bound on the bytes length.
+ max_size : int
+ (Inclusive) upper bound on the bytes length.
+ """
raise NotImplementedError
+ def per_test_case_context_manager(self) -> AbstractContextManager:
+ """
+ Returns a context manager which will be entered each time Hypothesis
+ starts generating and executing one test case, and exited when that test
+ case finishes generating and executing, including if any exception is
+ thrown.
+
+ In the lifecycle of a Hypothesis test, this is called before
+ generating strategy values for each test case. This is just before any
+ :ref:`custom executor <custom-function-execution>` is called.
+
+ Even if not returning a custom context manager, |PrimitiveProvider|
+ subclasses are welcome to override this method to know when Hypothesis
+ starts and ends the execution of a single test case.
+ """
+ return contextlib.nullcontext()
+
+ def realize(self, value: T, *, for_failure: bool = False) -> T:
+ """
+ Called whenever hypothesis requires a concrete (non-symbolic) value from
+ a potentially symbolic value. Hypothesis will not check that ``value`` is
+ symbolic before calling ``realize``, so you should handle the case where
+ ``value`` is non-symbolic.
+
+ The returned value should be non-symbolic. If you cannot provide a value,
+ raise |BackendCannotProceed| with a value of ``"discard_test_case"``.
+
+ If ``for_failure`` is ``True``, the value is associated with a failing example.
+ In this case, the backend should spend substantially more effort when
+ attempting to realize the value, since it is important to avoid discarding
+ failing examples. Backends may still raise |BackendCannotProceed| when
+ ``for_failure`` is ``True``, if realization is truly impossible or if
+ realization takes significantly longer than expected (say, 5 minutes).
+ """
+ return value
+
+ def replay_choices(self, choices: tuple[ChoiceT, ...]) -> None:
+ """
+ Called when Hypothesis has discovered a choice sequence which the provider
+ may wish to enqueue to replay under its own instrumentation when we next
+ ask to generate a test case, rather than generating one from scratch.
+
+ This is used to e.g. warm-start :pypi:`hypothesis-crosshair` with a corpus
+ of high-code-coverage inputs discovered by
+ `HypoFuzz <https://hypofuzz.com/>`_.
+ """
+ return None
+
+ def observe_test_case(self) -> dict[str, Any]:
+ """Called at the end of the test case when :ref:`observability
+ <observability>` is enabled.
+
+ The return value should be a non-symbolic json-encodable dictionary,
+ and will be included in observations as ``observation["metadata"]["backend"]``.
+ """
+ return {}
+
+ def observe_information_messages(
+ self, *, lifetime: LifetimeT
+ ) -> Iterable[_BackendInfoMsg]:
+ """Called at the end of each test case and again at end of the test function.
+
+ Return an iterable of ``{type: info/alert/error, title: str, content: str | dict}``
+ dictionaries to be delivered as individual information messages. Hypothesis
+ adds the ``run_start`` timestamp and ``property`` name for you.
+ """
+ assert lifetime in ("test_case", "test_function")
+ yield from []
+
+ def on_observation(self, observation: TestCaseObservation) -> None: # noqa: B027
+ """
+ Called at the end of each test case which uses this provider, with the same
+ ``observation["type"] == "test_case"`` observation that is passed to
+ other callbacks in |TESTCASE_CALLBACKS|. This method is not called with
+ ``observation["type"] in {"info", "alert", "error"}`` observations.
+
+ .. important::
+
+ For |PrimitiveProvider.on_observation| to be called by Hypothesis,
+ |PrimitiveProvider.add_observability_callback| must be set to ``True``.
+
+ |PrimitiveProvider.on_observation| is explicitly opt-in, as enabling
+ observability might increase runtime or memory usage.
+
+ Calls to this method are guaranteed to alternate with calls to
+ |PrimitiveProvider.per_test_case_context_manager|. For example:
+
+ .. code-block:: python
+
+ # test function starts
+ per_test_case_context_manager()
+ on_observation()
+ per_test_case_context_manager()
+ on_observation()
+ ...
+ # test function ends
+
+ Note that |PrimitiveProvider.on_observation| will not be called for test
+ cases which did not use this provider during generation, for example
+ during |Phase.reuse| or |Phase.shrink|, or because Hypothesis switched
+ to the standard Hypothesis backend after this backend raised too many
+ |BackendCannotProceed| exceptions.
+ """
+
def span_start(self, label: int, /) -> None: # noqa: B027 # non-abstract noop
- """Marks the beginning of a semantically meaningful span.
+ """Marks the beginning of a semantically meaningful span of choices.
+
+ Spans are a depth-first tree structure. A span is opened by a call to
+ |PrimitiveProvider.span_start|, and a call to |PrimitiveProvider.span_end|
+ closes the most recently opened span. So the following sequence of calls:
+
+ .. code-block:: python
+
+ span_start(label=1)
+ n1 = draw_integer()
+ span_start(label=2)
+ b1 = draw_boolean()
+ n2 = draw_integer()
+ span_end()
+ f1 = draw_float()
+ span_end()
+
+ produces the following two spans of choices:
+
+ .. code-block::
- Providers can optionally track this data to learn which sub-sequences
- of draws correspond to a higher-level object, recovering the parse tree.
- `label` is an opaque integer, which will be shared by all spans drawn
- from a particular strategy.
+ 1: [n1, b1, n2, f1]
+ 2: [b1, n2]
- This method is called from ConjectureData.start_span().
+ Hypothesis uses spans to denote "semantically meaningful" sequences of
+ choices. For instance, Hypothesis opens a span for the sequence of choices
+ made while drawing from each strategy. Not every span corresponds to a
+ strategy; the generation of e.g. each element in |st.lists| is also marked
+ with a span, among others.
+
+ ``label`` is an opaque integer, which has no defined semantics.
+ The only guarantee made by Hypothesis is that all spans with the same
+ "meaning" will share the same ``label``. So all spans from the same
+ strategy will share the same label, as will e.g. the spans for |st.lists|
+ elements.
+
+ Providers can track calls to |PrimitiveProvider.span_start| and
+ |PrimitiveProvider.span_end| to learn something about the semantics of
+ the test's choice sequence. For instance, a provider could track the depth
+ of the span tree, or the number of unique labels, which says something about
+ the complexity of the choices being generated. Or a provider could track
+ the span tree across test cases in order to determine what strategies are
+ being used in what contexts.
+
+ It is possible for Hypothesis to start and immediately stop a span,
+ without calling a ``draw_*`` method in between. These spans contain zero
+ choices.
+
+ Hypothesis will always balance the number of calls to
+ |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end|. A call
+ to |PrimitiveProvider.span_start| will always be followed by a call to
+ |PrimitiveProvider.span_end| before the end of the test case.
+
+ |PrimitiveProvider.span_start| is called from ``ConjectureData.start_span()``
+ internally.
"""
- def span_end(self, discard: bool, /) -> None: # noqa: B027, FBT001
- """Marks the end of a semantically meaningful span.
+ def span_end(self, discard: bool, /) -> None: # noqa: B027
+ """Marks the end of a semantically meaningful span of choices.
- `discard` is True when the draw was filtered out or otherwise marked as
- unlikely to contribute to the input data as seen by the user's test.
+ ``discard`` is ``True`` when the draw was filtered out or otherwise marked
+ as unlikely to contribute to the input data as seen by the user's test.
Note however that side effects can make this determination unsound.
- This method is called from ConjectureData.stop_span().
+ |PrimitiveProvider.span_end| is called from ``ConjectureData.stop_span()``
+ internally.
"""
@@ -357,6 +670,60 @@ class HypothesisProvider(PrimitiveProvider):
super().__init__(conjecturedata)
self._random = None if self._cd is None else self._cd._random
+ @cached_property
+ def _local_constants(self):
+ # defer computation of local constants until/if we need it
+ return _get_local_constants()
+
+ def _maybe_draw_constant(
+ self,
+ choice_type: ChoiceTypeT,
+ constraints: ChoiceConstraintsT,
+ *,
+ p: float = 0.05,
+ ) -> Optional["ConstantT"]:
+ assert self._random is not None
+ assert choice_type != "boolean"
+ # check whether we even want a constant before spending time computing
+ # and caching the allowed constants.
+ if self._random.random() > p:
+ return None
+
+ # note: this property access results in computation being done
+ assert self._local_constants is not None
+
+ key = (choice_type, choice_constraints_key(choice_type, constraints))
+ if key not in CONSTANTS_CACHE:
+ CONSTANTS_CACHE[key] = (
+ tuple(
+ choice
+ for choice in GLOBAL_CONSTANTS.set_for_type(choice_type)
+ if choice_permitted(choice, constraints)
+ ),
+ tuple(
+ choice
+ for choice in self._local_constants.set_for_type(choice_type)
+ if choice_permitted(choice, constraints)
+ ),
+ )
+
+ # split constants into two pools, so we still have a good chance to draw
+ # global constants even if there are many local constants.
+ (global_constants, local_constants) = CONSTANTS_CACHE[key]
+ constants_lists = ([global_constants] if global_constants else []) + (
+ [local_constants] if local_constants else []
+ )
+ if not constants_lists:
+ return None
+
+ # At this point, we've decided to use a constant. Now we select which pool
+ # to draw that constant from.
+ #
+ # Note that this approach has a different probability distribution than
+ # attempting a random.random for both global_constants and local_constants.
+ constants = self._random.choice(constants_lists)
+ return self._random.choice(constants)
+
def draw_boolean(
self,
p: float = 0.5,
@@ -379,6 +746,19 @@ class HypothesisProvider(PrimitiveProvider):
shrink_towards: int = 0,
) -> int:
assert self._cd is not None
+ if (
+ constant := self._maybe_draw_constant(
+ "integer",
+ {
+ "min_value": min_value,
+ "max_value": max_value,
+ "weights": weights,
+ "shrink_towards": shrink_towards,
+ },
+ )
+ ) is not None:
+ assert isinstance(constant, int)
+ return constant
center = 0
if min_value is not None:
@@ -436,39 +816,64 @@ class HypothesisProvider(PrimitiveProvider):
max_value: float = math.inf,
allow_nan: bool = True,
smallest_nonzero_magnitude: float,
- # TODO: consider supporting these float widths at the IR level in the
- # future.
- # width: Literal[16, 32, 64] = 64,
- # exclude_min and exclude_max handled higher up,
) -> float:
- (
- sampler,
- clamper,
- nasty_floats,
- ) = self._draw_float_init_logic(
- min_value=min_value,
- max_value=max_value,
- allow_nan=allow_nan,
+ assert self._random is not None
+
+ constraints: FloatConstraints = {
+ "min_value": min_value,
+ "max_value": max_value,
+ "allow_nan": allow_nan,
+ "smallest_nonzero_magnitude": smallest_nonzero_magnitude,
+ }
+ if (
+ constant := self._maybe_draw_constant("float", constraints, p=0.15)
+ ) is not None:
+ assert isinstance(constant, float)
+ return constant
+
+ # on top of the probability to draw a constant float, we independently
+ # upweight 0.0/-0.0, math.inf, -math.inf, nans, and boundary values.
+ weird_floats = [
+ f
+ for f in [
+ 0.0,
+ -0.0,
+ math.inf,
+ -math.inf,
+ math.nan,
+ -math.nan,
+ SIGNALING_NAN,
+ -SIGNALING_NAN,
+ min_value,
+ next_up(min_value),
+ min_value + 1,
+ max_value - 1,
+ next_down(max_value),
+ max_value,
+ ]
+ if choice_permitted(f, constraints)
+ ]
+
+ if weird_floats and self._random.random() < 0.05:
+ return self._random.choice(weird_floats)
+
+ clamper = make_float_clamper(
+ min_value,
+ max_value,
smallest_nonzero_magnitude=smallest_nonzero_magnitude,
+ allow_nan=allow_nan,
)
- assert self._cd is not None
-
- while True:
- i = sampler.sample(self._cd) if sampler else 0
- if i == 0:
- result = self._draw_float()
- if allow_nan and math.isnan(result):
- clamped = result # pragma: no cover
- else:
- clamped = clamper(result)
- if float_to_int(clamped) != float_to_int(result) and not (
- math.isnan(result) and allow_nan
- ):
- result = clamped
- else:
- result = nasty_floats[i - 1]
- return result
+ result = self._draw_float()
+ if allow_nan and math.isnan(result):
+ clamped = result # pragma: no cover
+ else:
+ clamped = clamper(result)
+ if float_to_int(clamped) != float_to_int(result) and not (
+ math.isnan(result) and allow_nan
+ ):
+ result = clamped
+ return result
def draw_string(
self,
@@ -483,14 +888,14 @@ class HypothesisProvider(PrimitiveProvider):
if len(intervals) == 0:
return ""
- sampler, nasty_strings = self._draw_string_sampler(
- intervals=intervals,
- min_size=min_size,
- max_size=max_size,
- )
-
- if sampler is not None and self.draw_boolean(p=0.05):
- return nasty_strings[sampler.sample(self._cd)]
+ if (
+ constant := self._maybe_draw_constant(
+ "string",
+ {"intervals": intervals, "min_size": min_size, "max_size": max_size},
+ )
+ ) is not None:
+ assert isinstance(constant, str)
+ return constant
average_size = min(
max(min_size * 2, min_size + 5),
@@ -526,6 +931,14 @@ class HypothesisProvider(PrimitiveProvider):
assert self._cd is not None
assert self._random is not None
+ if (
+ constant := self._maybe_draw_constant(
+ "bytes", {"min_size": min_size, "max_size": max_size}
+ )
+ ) is not None:
+ assert isinstance(constant, bytes)
+ return constant
+
buf = bytearray()
average_size = min(
max(min_size * 2, min_size + 5),
@@ -589,118 +1002,11 @@ class HypothesisProvider(PrimitiveProvider):
return self._random.randint(lower, upper)
- @classmethod
- def _draw_float_init_logic(
- cls,
- *,
- min_value: float,
- max_value: float,
- allow_nan: bool,
- smallest_nonzero_magnitude: float,
- ) -> tuple[
- Optional[Sampler],
- Callable[[float], float],
- list[float],
- ]:
- """
- Caches initialization logic for draw_float, as an alternative to
- computing this for *every* float draw.
- """
- # float_to_int allows us to distinguish between e.g. -0.0 and 0.0,
- # even in light of hash(-0.0) == hash(0.0) and -0.0 == 0.0.
- key = (
- float_to_int(min_value),
- float_to_int(max_value),
- allow_nan,
- float_to_int(smallest_nonzero_magnitude),
- )
- if key in FLOAT_INIT_LOGIC_CACHE:
- return FLOAT_INIT_LOGIC_CACHE[key]
-
- result = cls._compute_draw_float_init_logic(
- min_value=min_value,
- max_value=max_value,
- allow_nan=allow_nan,
- smallest_nonzero_magnitude=smallest_nonzero_magnitude,
- )
- FLOAT_INIT_LOGIC_CACHE[key] = result
- return result
- @staticmethod
- def _compute_draw_float_init_logic(
- *,
- min_value: float,
- max_value: float,
- allow_nan: bool,
- smallest_nonzero_magnitude: float,
- ) -> tuple[
- Optional[Sampler],
- Callable[[float], float],
- list[float],
- ]:
- if smallest_nonzero_magnitude == 0.0: # pragma: no cover
- raise FloatingPointError(
- "Got allow_subnormal=True, but we can't represent subnormal floats "
- "right now, in violation of the IEEE-754 floating-point "
- "specification. This is usually because something was compiled with "
- "-ffast-math or a similar option, which sets global processor state. "
- "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
- "writeup - and good luck!"
- )
-
- def permitted(f: float) -> bool:
- if math.isnan(f):
- return allow_nan
- if 0 < abs(f) < smallest_nonzero_magnitude:
- return False
- return sign_aware_lte(min_value, f) and sign_aware_lte(f, max_value)
-
- boundary_values = [
- min_value,
- next_up(min_value),
- min_value + 1,
- max_value - 1,
- next_down(max_value),
- max_value,
- ]
- nasty_floats = [f for f in NASTY_FLOATS + boundary_values if permitted(f)]
- weights = [0.2 * len(nasty_floats)] + [0.8] * len(nasty_floats)
- sampler = Sampler(weights, observe=False) if nasty_floats else None
-
- clamper = make_float_clamper(
- min_value,
- max_value,
- smallest_nonzero_magnitude=smallest_nonzero_magnitude,
- allow_nan=allow_nan,
- )
- return (sampler, clamper, nasty_floats)
-
- @classmethod
- def _draw_string_sampler(
- cls,
- *,
- intervals: IntervalSet,
- min_size: int,
- max_size: int,
- ) -> tuple[Optional[Sampler], list[str]]:
- constraints: StringConstraints = {
- "intervals": intervals,
- "min_size": min_size,
- "max_size": max_size,
- }
- key = choice_constraints_key("string", constraints)
- if key in STRING_SAMPLER_CACHE:
- return STRING_SAMPLER_CACHE[key]
-
- nasty_strings = [s for s in NASTY_STRINGS if choice_permitted(s, constraints)]
- sampler = (
- Sampler([1 / len(nasty_strings)] * len(nasty_strings), observe=False)
- if nasty_strings
- else None
- )
- result = (sampler, nasty_strings)
- STRING_SAMPLER_CACHE[key] = result
- return result
+# Masks for masking off the first byte of an n-bit buffer.
+# The appropriate mask is stored at position n % 8.
+BYTE_MASKS = [(1 << n) - 1 for n in range(8)]
+BYTE_MASKS[0] = 255
class BytestringProvider(PrimitiveProvider):
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
index 0aac4360122..1c8452625f9 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py
@@ -11,9 +11,8 @@
import math
from collections import defaultdict
from collections.abc import Sequence
-from typing import TYPE_CHECKING, Callable, Literal, Optional, Union, cast
-
-import attr
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast
from hypothesis.internal.conjecture.choice import (
ChoiceNode,
@@ -85,50 +84,28 @@ def sort_key(nodes: Sequence[ChoiceNode]) -> tuple[int, tuple[int, ...]]:
)
-SHRINK_PASS_DEFINITIONS: dict[str, "ShrinkPassDefinition"] = {}
-
-
-class ShrinkPassDefinition:
- """A shrink pass bundles together a large number of local changes to
- the current shrink target.
-
- Each shrink pass is defined by some function and some arguments to that
- function. The ``generate_arguments`` function returns all arguments that
- might be useful to run on the current shrink target.
-
- The guarantee made by methods defined this way is that after they are
- called then *either* the shrink target has changed *or* each of
- ``fn(*args)`` has been called for every ``args`` in ``generate_arguments(self)``.
- No guarantee is made that all of these will be called if the shrink target
- changes.
- """
-
- run_with_chooser = attr.ib()
-
- @property
- def name(self) -> str:
- return self.run_with_chooser.__name__
-
- def __attrs_post_init__(self) -> None:
- assert self.name not in SHRINK_PASS_DEFINITIONS, self.name
- SHRINK_PASS_DEFINITIONS[self.name] = self
-
+@dataclass
+class ShrinkPass:
+ function: Any
+ name: Optional[str] = None
+ last_prefix: Any = ()
-def defines_shrink_pass():
- """A convenient decorator for defining shrink passes."""
+ # some execution statistics
+ calls: int = 0
+ misaligned: int = 0
+ shrinks: int = 0
+ deletions: int = 0
- def accept(run_step):
- ShrinkPassDefinition(run_with_chooser=run_step)
+ def __post_init__(self):
+ if self.name is None:
+ self.name = self.function.__name__
- def run(self):
- raise NotImplementedError("Shrink passes should not be run directly")
+ def __hash__(self):
+ return hash(self.name)
- run.__name__ = run_step.__name__
- run.is_shrink_pass = True
- return run
- return accept
+class StopShrinking(Exception):
+ pass
class Shrinker:
@@ -325,7 +302,21 @@ class Shrinker:
self.initial_misaligned = self.engine.misaligned_count
self.calls_at_last_shrink = self.initial_calls
- self.passes_by_name: dict[str, ShrinkPass] = {}
+ self.shrink_passes: list[ShrinkPass] = [
+ ShrinkPass(self.try_trivial_spans),
+ self.node_program("X" * 5),
+ self.node_program("X" * 4),
+ self.node_program("X" * 3),
+ self.node_program("X" * 2),
+ self.node_program("X" * 1),
+ ShrinkPass(self.pass_to_descendant),
+ ShrinkPass(self.reorder_spans),
+ ShrinkPass(self.minimize_duplicated_choices),
+ ShrinkPass(self.minimize_individual_choices),
+ ShrinkPass(self.redistribute_numeric_pairs),
+ ShrinkPass(self.lower_integers_together),
+ ShrinkPass(self.lower_duplicated_characters),
+ ]
# Because the shrinker is also used to `pareto_optimise` in the target phase,
# we sometimes want to allow extending buffers instead of aborting at the end.
@@ -346,27 +337,6 @@ class Shrinker:
return accept
- def add_new_pass(self, run):
- """Creates a shrink pass corresponding to calling ``run(self)``"""
-
- definition = SHRINK_PASS_DEFINITIONS[run]
-
- p = ShrinkPass(
- run_with_chooser=definition.run_with_chooser,
- shrinker=self,
- index=len(self.passes_by_name),
- )
- self.passes_by_name[p.name] = p
- return p
-
- def shrink_pass(self, name):
- """Return the ShrinkPass object for the pass with the given name."""
- if isinstance(name, ShrinkPass):
- return name
- if name not in self.passes_by_name:
- self.add_new_pass(name)
- return self.passes_by_name[name]
-
@property
def calls(self) -> int:
"""Return the number of calls that have been made to the underlying
@@ -466,19 +436,19 @@ class Shrinker:
else:
self.debug("Useless passes:")
self.debug("")
- for p in sorted(
- self.passes_by_name.values(),
+ for pass_ in sorted(
+ self.shrink_passes,
key=lambda t: (-t.calls, t.deletions, t.shrinks),
):
- if p.calls == 0:
+ if pass_.calls == 0:
continue
- if (p.shrinks != 0) != useful:
+ if (pass_.shrinks != 0) != useful:
continue
self.debug(
- f" * {p.name} made {p.calls} call{s(p.calls)} of which "
- f"{p.shrinks} shrank and {p.misaligned} were misaligned, "
- f"deleting {p.deletions} choice{s(p.deletions)}."
+ f" * {pass_.name} made {pass_.calls} call{s(pass_.calls)} of which "
+ f"{pass_.shrinks} shrank and {pass_.misaligned} were misaligned, "
+ f"deleting {pass_.deletions} choice{s(pass_.deletions)}."
)
self.debug("")
self.explain()
@@ -627,23 +597,7 @@ class Shrinker:
This method iterates to a fixed point and so is idempontent - calling
it twice will have exactly the same effect as calling it once.
"""
- self.fixate_shrink_passes(
- [
- "try_trivial_spans",
- node_program("X" * 5),
- node_program("X" * 4),
- node_program("X" * 3),
- node_program("X" * 2),
- node_program("X" * 1),
- "pass_to_descendant",
- "reorder_spans",
- "minimize_duplicated_choices",
- "minimize_individual_choices",
- "redistribute_numeric_pairs",
- "lower_integers_together",
- "lower_duplicated_characters",
- ]
- )
+ self.fixate_shrink_passes(self.shrink_passes)
def initial_coarse_reduction(self):
"""Performs some preliminary reductions that should not be
@@ -758,14 +712,42 @@ class Shrinker:
return False
@derived_value # type: ignore
- def shrink_pass_choice_trees(self):
+ def shrink_pass_choice_trees(self) -> dict[Any, ChoiceTree]:
return defaultdict(ChoiceTree)
- def fixate_shrink_passes(self, passes):
+ def step(self, shrink_pass: ShrinkPass, *, random_order: bool = False) -> bool:
+ tree = self.shrink_pass_choice_trees[shrink_pass]
+ if tree.exhausted:
+ return False
+
+ initial_shrinks = self.shrinks
+ initial_calls = self.calls
+ initial_misaligned = self.misaligned
+ size = len(self.shrink_target.choices)
+ assert shrink_pass.name is not None
+ self.engine.explain_next_call_as(shrink_pass.name)
+
+ if random_order:
+ selection_order = random_selection_order(self.random)
+ else:
+ selection_order = prefix_selection_order(shrink_pass.last_prefix)
+
+ try:
+ shrink_pass.last_prefix = tree.step(
+ selection_order,
+ lambda chooser: shrink_pass.function(chooser),
+ )
+ finally:
+ shrink_pass.calls += self.calls - initial_calls
+ shrink_pass.misaligned += self.misaligned - initial_misaligned
+ shrink_pass.shrinks += self.shrinks - initial_shrinks
+ shrink_pass.deletions += size - len(self.shrink_target.choices)
+ self.engine.clear_call_explanation()
+ return True
+
+ def fixate_shrink_passes(self, passes: list[ShrinkPass]) -> None:
"""Run steps from each pass in ``passes`` until the current shrink target
is a fixed point of all of them."""
- passes = list(map(self.shrink_pass, passes))
-
any_ran = True
while any_ran:
any_ran = False
@@ -823,7 +805,7 @@ class Shrinker:
# to do anything) we switch to randomly jumping around. If we
# find a success then we'll resume deterministic order from
# there which, with any luck, is in a new good region.
- if not sp.step(random_order=failures >= max_failures // 2):
+ if not self.step(sp, random_order=failures >= max_failures // 2):
# step returns False when there is nothing to do because
# the entire choice tree is exhausted. If this happens
# we break because we literally can't run this pass any
@@ -885,7 +867,6 @@ class Shrinker:
def distinct_labels(self):
return sorted(self.spans_by_label, key=str)
- @defines_shrink_pass()
def pass_to_descendant(self, chooser):
"""Attempt to replace each span with a descendant span.
@@ -963,13 +944,13 @@ class Shrinker:
sequence: The number of iterations that reduce the length of the choice
sequence is bounded by that length.
- So what we do is this: We keep track of which blocks are changing, and
+ So what we do is this: We keep track of which nodes are changing, and
then if there's some non-zero common offset to them we try and minimize
them all at once by lowering that offset.
This may not work, and it definitely won't get us out of all possible
exponential slow downs (an example of where it doesn't is where the
- shape of the blocks changes as a result of this bouncing behaviour),
+ shape of the nodes changes as a result of this bouncing behaviour),
but it fails fast when it doesn't work and gets us out of a really
nastily slow case when it does.
"""
@@ -1075,7 +1056,7 @@ class Shrinker:
to shrink_target).
In current usage it is expected that each of the nodes currently have
- the same value and ir type, although this is not essential. Note that
+ the same value and choice_type, although this is not essential. Note that
n must be < the node at min(nodes) or this is not a valid shrink.
This method will attempt to do some small amount of work to delete data
@@ -1176,7 +1157,7 @@ class Shrinker:
# We now look for contiguous regions to delete that might help fix up
# this failed shrink. We only look for contiguous regions of the right
# lengths because doing anything more than that starts to get very
- # expensive. See minimize_individual_blocks for where we
+ # expensive. See minimize_individual_choices for where we
# try to be more aggressive.
regions_to_delete = {(end, end + lost_nodes)}
@@ -1263,7 +1244,45 @@ class Shrinker:
duplicates[(node.type, choice_key(node.value))].append(node)
return list(duplicates.values())
- @defines_shrink_pass()
+ def node_program(self, program: str) -> ShrinkPass:
+ return ShrinkPass(
+ lambda chooser: self._node_program(chooser, program),
+ name=f"node_program_{program}",
+ )
+
+ def _node_program(self, chooser, program):
+ n = len(program)
+ # Adaptively attempt to run the node program at the current
+ # index. If this successfully applies the node program ``k`` times
+ # then this runs in ``O(log(k))`` test function calls.
+ i = chooser.choose(range(len(self.nodes) - n + 1))
+
+ # First, run the node program at the chosen index. If this fails,
+ # don't do any extra work, so that failure is as cheap as possible.
+ if not self.run_node_program(i, program, original=self.shrink_target):
+ return
+
+ # Because we run in a random order we will often find ourselves in the middle
+ # of a region where we could run the node program. We thus start by moving
+ # left to the beginning of that region if possible in order to to start from
+ # the beginning of that region.
+ def offset_left(k):
+ return i - k * n
+
+ i = offset_left(
+ find_integer(
+ lambda k: self.run_node_program(
+ offset_left(k), program, original=self.shrink_target
+ )
+ )
+ )
+
+ original = self.shrink_target
+ # Now try to run the node program multiple times here.
+ find_integer(
+ lambda k: self.run_node_program(i, program, original=original, repeats=k)
+ )
+
def minimize_duplicated_choices(self, chooser):
"""Find choices that have been duplicated in multiple places and attempt
to minimize all of the duplicates simultaneously.
@@ -1281,7 +1300,7 @@ class Shrinker:
to replace either 3 with 0 on its own the test would start passing.
It is also useful for when that duplication is accidental and the value
- of the blocks doesn't matter very much because it allows us to replace
+ of the choices don't matter very much because it allows us to replace
more values at once.
"""
nodes = chooser.choose(self.duplicated_nodes)
@@ -1293,7 +1312,6 @@ class Shrinker:
self.minimize_nodes(nodes)
- @defines_shrink_pass()
def redistribute_numeric_pairs(self, chooser):
"""If there is a sum of generated numbers that we need their sum
to exceed some bound, lowering one of them requires raising the
@@ -1359,7 +1377,6 @@ class Shrinker:
find_integer(boost)
- @defines_shrink_pass()
def lower_integers_together(self, chooser):
node1 = chooser.choose(
self.nodes, lambda n: n.type == "integer" and not n.trivial
@@ -1392,7 +1409,6 @@ class Shrinker:
find_integer(lambda n: consider(shrink_towards - n))
find_integer(lambda n: consider(n - shrink_towards))
- @defines_shrink_pass()
def lower_duplicated_characters(self, chooser):
"""
Select two string choices no more than 4 choices apart and simultaneously
@@ -1515,7 +1531,6 @@ class Shrinker:
else:
raise NotImplementedError
- @defines_shrink_pass()
def try_trivial_spans(self, chooser):
i = chooser.choose(range(len(self.spans)))
@@ -1546,7 +1561,6 @@ class Shrinker:
new_replacement = attempt.nodes[new_ex.start : new_ex.end]
self.consider_new_nodes(prefix + new_replacement + suffix)
- @defines_shrink_pass()
def minimize_individual_choices(self, chooser):
"""Attempt to minimize each choice in sequence.
@@ -1647,7 +1661,6 @@ class Shrinker:
node = self.nodes[chooser.choose(range(node.index + 1, len(self.nodes)))]
self.consider_new_nodes(lowered[: node.index] + lowered[node.index + 1 :])
- @defines_shrink_pass()
def reorder_spans(self, chooser):
"""This pass allows us to reorder the children of each span.
@@ -1694,7 +1707,7 @@ class Shrinker:
key=lambda i: sort_key(st.nodes[spans[i].start : spans[i].end]),
)
- def run_node_program(self, i, description, original, repeats=1):
+ def run_node_program(self, i, program, original, repeats=1):
"""Node programs are a mini-DSL for node rewriting, defined as a sequence
of commands that can be run at some index into the nodes
@@ -1702,18 +1715,18 @@ class Shrinker:
* "X", delete this node
- This method runs the node program in ``description`` at node index
+ This method runs the node program in ``program`` at node index
``i`` on the ConjectureData ``original``. If ``repeats > 1`` then it
will attempt to approximate the results of running it that many times.
Returns True if this successfully changes the underlying shrink target,
else False.
"""
- if i + len(description) > len(original.nodes) or i < 0:
+ if i + len(program) > len(original.nodes) or i < 0:
return False
attempt = list(original.nodes)
for _ in range(repeats):
- for k, command in reversed(list(enumerate(description))):
+ for k, command in reversed(list(enumerate(program))):
j = i + k
if j >= len(attempt):
return False
@@ -1724,105 +1737,3 @@ class Shrinker:
raise NotImplementedError(f"Unrecognised command {command!r}")
return self.consider_new_nodes(attempt)
-
-
-def shrink_pass_family(f):
- def accept(*args):
- name = "{}({})".format(f.__name__, ", ".join(map(repr, args)))
- if name not in SHRINK_PASS_DEFINITIONS:
-
- def run(self, chooser):
- return f(self, chooser, *args)
-
- run.__name__ = name
- defines_shrink_pass()(run)
- assert name in SHRINK_PASS_DEFINITIONS
- return name
-
- return accept
-
-
-@shrink_pass_family
-def node_program(self, chooser, description):
- n = len(description)
- # Adaptively attempt to run the node program at the current
- # index. If this successfully applies the node program ``k`` times
- # then this runs in ``O(log(k))`` test function calls.
- i = chooser.choose(range(len(self.nodes) - n + 1))
-
- # First, run the node program at the chosen index. If this fails,
- # don't do any extra work, so that failure is as cheap as possible.
- if not self.run_node_program(i, description, original=self.shrink_target):
- return
-
- # Because we run in a random order we will often find ourselves in the middle
- # of a region where we could run the node program. We thus start by moving
- # left to the beginning of that region if possible in order to to start from
- # the beginning of that region.
- def offset_left(k):
- return i - k * n
-
- i = offset_left(
- find_integer(
- lambda k: self.run_node_program(
- offset_left(k), description, original=self.shrink_target
- )
- )
- )
-
- original = self.shrink_target
- # Now try to run the block program multiple times here.
- find_integer(
- lambda k: self.run_node_program(i, description, original=original, repeats=k)
- )
-
-
[email protected](slots=True, eq=False)
-class ShrinkPass:
- run_with_chooser = attr.ib()
- index = attr.ib()
- shrinker = attr.ib()
-
- last_prefix = attr.ib(default=())
- successes = attr.ib(default=0)
- calls = attr.ib(default=0)
- misaligned = attr.ib(default=0)
- shrinks = attr.ib(default=0)
- deletions = attr.ib(default=0)
-
- def step(self, *, random_order=False):
- tree = self.shrinker.shrink_pass_choice_trees[self]
- if tree.exhausted:
- return False
-
- initial_shrinks = self.shrinker.shrinks
- initial_calls = self.shrinker.calls
- initial_misaligned = self.shrinker.misaligned
- size = len(self.shrinker.shrink_target.choices)
- self.shrinker.engine.explain_next_call_as(self.name)
-
- if random_order:
- selection_order = random_selection_order(self.shrinker.random)
- else:
- selection_order = prefix_selection_order(self.last_prefix)
-
- try:
- self.last_prefix = tree.step(
- selection_order,
- lambda chooser: self.run_with_chooser(self.shrinker, chooser),
- )
- finally:
- self.calls += self.shrinker.calls - initial_calls
- self.misaligned += self.shrinker.misaligned - initial_misaligned
- self.shrinks += self.shrinker.shrinks - initial_shrinks
- self.deletions += size - len(self.shrinker.shrink_target.choices)
- self.shrinker.engine.clear_call_explanation()
- return True
-
- @property
- def name(self) -> str:
- return self.run_with_chooser.__name__
-
-
-class StopShrinking(Exception):
- pass
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py
index 0ba8ab819b7..c757a2e0466 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py
@@ -11,7 +11,7 @@
from collections import defaultdict
from collections.abc import Iterable, Sequence
from random import Random
-from typing import Callable, List, Optional
+from typing import Callable, Optional
from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy
@@ -58,7 +58,7 @@ class Chooser:
):
self.__selection_order = selection_order
self.__node_trail = [tree.root]
- self.__choices: "List[int]" = []
+ self.__choices: list[int] = []
self.__finished = False
def choose(
@@ -146,8 +146,8 @@ class ChoiceTree:
class TreeNode:
def __init__(self) -> None:
self.children: dict[int, TreeNode] = defaultdict(TreeNode)
- self.live_child_count: "Optional[int]" = None
- self.n: "Optional[int]" = None
+ self.live_child_count: Optional[int] = None
+ self.n: Optional[int] = None
@property
def exhausted(self) -> bool:
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py b/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py
index c69e5ba7072..c297b45154b 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py
@@ -9,30 +9,144 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import ast
+import hashlib
import inspect
import math
import sys
-from ast import AST, Constant, Expr, NodeVisitor, UnaryOp, USub
+from ast import Constant, Expr, NodeVisitor, UnaryOp, USub
+from collections.abc import Iterator, MutableSet
from functools import lru_cache
+from itertools import chain
+from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Optional, Union
+import hypothesis
+from hypothesis.configuration import storage_directory
+from hypothesis.internal.conjecture.choice import ChoiceTypeT
from hypothesis.internal.escalation import is_hypothesis_file
-from hypothesis.internal.scrutineer import ModuleLocation
if TYPE_CHECKING:
from typing import TypeAlias
ConstantT: "TypeAlias" = Union[int, float, bytes, str]
+# unfortunate collision with builtin. I don't want to name the init arg bytes_.
+bytesT = bytes
+
+
+class Constants:
+ def __init__(
+ self,
+ *,
+ integers: Optional[MutableSet[int]] = None,
+ floats: Optional[MutableSet[float]] = None,
+ bytes: Optional[MutableSet[bytes]] = None,
+ strings: Optional[MutableSet[str]] = None,
+ ):
+ self.integers: MutableSet[int] = set() if integers is None else integers
+ self.floats: MutableSet[float] = set() if floats is None else floats
+ self.bytes: MutableSet[bytesT] = set() if bytes is None else bytes
+ self.strings: MutableSet[str] = set() if strings is None else strings
+
+ def set_for_type(
+ self, constant_type: Union[type[ConstantT], ChoiceTypeT]
+ ) -> Union[MutableSet[int], MutableSet[float], MutableSet[bytes], MutableSet[str]]:
+ if constant_type is int or constant_type == "integer":
+ return self.integers
+ elif constant_type is float or constant_type == "float":
+ return self.floats
+ elif constant_type is bytes or constant_type == "bytes":
+ return self.bytes
+ elif constant_type is str or constant_type == "string":
+ return self.strings
+ raise ValueError(f"unknown constant_type {constant_type}")
+
+ def add(self, constant: ConstantT) -> None:
+ self.set_for_type(type(constant)).add(constant) # type: ignore
+
+ def __contains__(self, constant: ConstantT) -> bool:
+ return constant in self.set_for_type(type(constant))
+
+ def __or__(self, other: "Constants") -> "Constants":
+ return Constants(
+ integers=self.integers | other.integers, # type: ignore
+ floats=self.floats | other.floats, # type: ignore
+ bytes=self.bytes | other.bytes, # type: ignore
+ strings=self.strings | other.strings, # type: ignore
+ )
+
+ def __iter__(self) -> Iterator[ConstantT]:
+ return iter(chain(self.integers, self.floats, self.bytes, self.strings))
+
+ def __len__(self) -> int:
+ return (
+ len(self.integers) + len(self.floats) + len(self.bytes) + len(self.strings)
+ )
+
+ def __repr__(self) -> str:
+ return f"Constants({self.integers=}, {self.floats=}, {self.bytes=}, {self.strings=})"
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Constants):
+ return False
+ return (
+ self.integers == other.integers
+ and self.floats == other.floats
+ and self.bytes == other.bytes
+ and self.strings == other.strings
+ )
+
+
+class TooManyConstants(Exception):
+ # a control flow exception which we raise in ConstantsVisitor when the
+ # number of constants in a module gets too large.
+ pass
+
class ConstantVisitor(NodeVisitor):
- def __init__(self):
+ CONSTANTS_LIMIT: int = 1024
+
+ def __init__(self, *, limit: bool):
super().__init__()
- self.constants: set[ConstantT] = set()
+ self.constants = Constants()
+ self.limit = limit
+
+ def _add_constant(self, value: object) -> None:
+ if self.limit and len(self.constants) >= self.CONSTANTS_LIMIT:
+ raise TooManyConstants
+
+ if isinstance(value, str) and (
+ value.isspace()
+ or value == ""
+ # long strings are unlikely to be useful.
+ or len(value) > 20
+ ):
+ return
+ if isinstance(value, bytes) and (
+ value == b""
+ # long bytes seem plausibly more likely to be useful than long strings
+ # (e.g. AES-256 has a 32 byte key), but we still want to cap at some
+ # point to avoid performance issues.
+ or len(value) > 50
+ ):
+ return
+ if isinstance(value, bool):
+ return
+ if isinstance(value, float) and math.isinf(value):
+ # we already upweight inf.
+ return
+ if isinstance(value, int) and -100 < value < 100:
+ # we already upweight small integers.
+ return
+
+ if isinstance(value, (int, float, bytes, str)):
+ self.constants.add(value)
+ return
- def _add_constant(self, constant: object) -> None:
- self.constants |= self._unfold_constant(constant)
+ # I don't kow what case could go here, but am also not confident there
+ # isn't one.
+ return # pragma: no cover
def visit_UnaryOp(self, node: UnaryOp) -> None:
# `a = -1` is actually a combination of a USub and the constant 1.
@@ -59,86 +173,107 @@ class ConstantVisitor(NodeVisitor):
# in f strings are unlikely to be helpful.
return
- @classmethod
- def _unfold_constant(cls, value: object) -> set[ConstantT]:
- if isinstance(value, str) and (
- len(value) > 20 or value.isspace() or value == ""
- ):
- # discard long strings, which are unlikely to be useful.
- return set()
- if isinstance(value, bool):
- return set()
- if isinstance(value, float) and math.isinf(value):
- # we already upweight inf.
- return set()
- if isinstance(value, (int, float, bytes, str)):
- return {value}
- # I don't kow what case could go here, but am also not confident there
- # isn't one.
- return set() # pragma: no cover
-
def visit_Constant(self, node):
self._add_constant(node.value)
self.generic_visit(node)
-@lru_cache(1024)
-def constants_from_ast(tree: AST) -> set[ConstantT]:
- visitor = ConstantVisitor()
- visitor.visit(tree)
+def _constants_from_source(source: Union[str, bytes], *, limit: bool) -> Constants:
+ tree = ast.parse(source)
+ visitor = ConstantVisitor(limit=limit)
+
+ try:
+ visitor.visit(tree)
+ except TooManyConstants:
+ # in the case of an incomplete collection, return nothing, to avoid
+ # muddying caches etc.
+ return Constants()
+
return visitor.constants
-@lru_cache(1024)
-def _module_ast(module: ModuleType) -> Optional[AST]:
+def _constants_file_str(constants: Constants) -> str:
+ return str(sorted(constants, key=lambda v: (str(type(v)), v)))
+
+
+@lru_cache(4096)
+def constants_from_module(module: ModuleType, *, limit: bool = True) -> Constants:
try:
- source = inspect.getsource(module)
- tree = ast.parse(source)
+ module_file = inspect.getsourcefile(module)
+ # use type: ignore because we know this might error
+ source_bytes = Path(module_file).read_bytes() # type: ignore
except Exception:
- return None
+ return Constants()
- return tree
+ if limit and len(source_bytes) > 512 * 1024:
+ # Skip files over 512kb. For reference, the largest source file
+ # in Hypothesis is strategies/_internal/core.py at 107kb at time
+ # of writing.
+ return Constants()
+ source_hash = hashlib.sha1(source_bytes).hexdigest()[:16]
+ # separate cache files for each limit param. see discussion in pull/4398
+ cache_p = storage_directory("constants") / (
+ source_hash + ("" if limit else "_nolimit")
+ )
+ try:
+ return _constants_from_source(cache_p.read_bytes(), limit=limit)
+ except Exception:
+ # if the cached location doesn't exist, or it does exist but there was
+ # a problem reading it, fall back to standard computation of the constants
+ pass
-def local_modules() -> tuple[ModuleType, ...]:
- modules = []
- for module in sys.modules.values():
- if (
- not hasattr(module, "__file__")
- or module.__file__ is None
- # Skip expensive path lookup for stdlib modules.
- # This will cause false negatives if a user names their module the
- # same as a stdlib module.
- #
- # sys.stdlib_module_names is new in 3.10
- or (
- sys.version_info >= (3, 10)
- and module.__name__ in sys.stdlib_module_names
- )
- or ModuleLocation.from_path(module.__file__) is not ModuleLocation.LOCAL
- ):
- continue
+ try:
+ constants = _constants_from_source(source_bytes, limit=limit)
+ except Exception:
+ # A bunch of things can go wrong here.
+ # * ast.parse may fail on the source code
+ # * NodeVisitor may hit a RecursionError (see many related issues on
+ # e.g. libcst https://github.com/Instagram/LibCST/issues?q=recursion),
+ # or a MemoryError (`"[1, " * 200 + "]" * 200`)
+ return Constants()
- modules.append(module)
- return tuple(modules)
+ try:
+ cache_p.parent.mkdir(parents=True, exist_ok=True)
+ cache_p.write_text(
+ f"# file: {module_file}\n# hypothesis_version: {hypothesis.__version__}\n\n"
+ # somewhat arbitrary sort order. The cache file doesn't *have* to be
+ # stable... but it is aesthetically pleasing, and means we could rely
+ # on it in the future!
+ + _constants_file_str(constants),
+ encoding="utf-8",
+ )
+ except Exception: # pragma: no cover
+ pass
+
+ return constants
-def local_constants():
- constants = set()
- for module in local_modules():
+@lru_cache(4096)
+def is_local_module_file(path: str) -> bool:
+ from hypothesis.internal.scrutineer import ModuleLocation
+
+ return (
+ # Skip expensive path lookup for stdlib modules.
+ # This will cause false negatives if a user names their module the
+ # same as a stdlib module.
+ #
+ # sys.stdlib_module_names is new in 3.10
+ not (sys.version_info >= (3, 10) and path in sys.stdlib_module_names)
+ # A path containing site-packages is extremely likely to be
+ # ModuleLocation.SITE_PACKAGES. Skip the expensive path lookup here.
+ and "/site-packages/" not in path
+ and ModuleLocation.from_path(path) is ModuleLocation.LOCAL
# normally, hypothesis is a third-party library and is not returned
# by local_modules. However, if it is installed as an editable package
# with pip install -e, then we will pick up on it. Just hardcode an
# ignore here.
-
- # this is actually covered by test_constants_from_running_file, but
- # not in the same process.
- if is_hypothesis_file(module.__file__): # pragma: no cover
- continue
-
- tree = _module_ast(module)
- if tree is None: # pragma: no cover
- continue
- constants |= constants_from_ast(tree)
-
- return constants
+ and not is_hypothesis_file(path)
+ # avoid collecting constants from test files
+ and not (
+ "test" in (p := Path(path)).parts
+ or "tests" in p.parts
+ or p.stem.startswith("test_")
+ or p.stem.endswith("_test")
+ )
+ )
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/detection.py b/contrib/python/hypothesis/py3/hypothesis/internal/detection.py
index 8ce11808c32..6fa01a8e6f7 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/detection.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/detection.py
@@ -11,7 +11,31 @@
from types import MethodType
-def is_hypothesis_test(test: object) -> bool:
- if isinstance(test, MethodType):
- return is_hypothesis_test(test.__func__)
- return getattr(test, "is_hypothesis_test", False)
+def is_hypothesis_test(f: object) -> bool:
+ """
+ Returns ``True`` if ``f`` represents a test function that has been defined
+ with Hypothesis. This is true for:
+
+ * Functions decorated with |@given|
+ * The ``runTest`` method of stateful tests
+
+ For example:
+
+ .. code-block:: python
+
+ @given(st.integers())
+ def f(n): ...
+
+ class MyStateMachine(RuleBasedStateMachine): ...
+
+ assert is_hypothesis_test(f)
+ assert is_hypothesis_test(MyStateMachine.TestCase().runTest)
+
+ .. seealso::
+
+ See also the :doc:`Detect Hypothesis tests
+ </how-to/detect-hypothesis-tests>` how-to.
+ """
+ if isinstance(f, MethodType):
+ return is_hypothesis_test(f.__func__)
+ return getattr(f, "is_hypothesis_test", False)
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py b/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py
index d7ce1463bdd..e63694fbe05 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py
@@ -15,6 +15,7 @@ import sys
import warnings
from collections.abc import Generator, Hashable
from itertools import count
+from random import Random
from typing import TYPE_CHECKING, Any, Callable, Optional
from weakref import WeakValueDictionary
@@ -35,12 +36,13 @@ if TYPE_CHECKING:
else: # pragma: no cover
RandomLike = random.Random
+_RKEY = count()
+_global_random_rkey = next(_RKEY)
# This is effectively a WeakSet, which allows us to associate the saved states
# with their respective Random instances even as new ones are registered and old
# ones go out of scope and get garbage collected. Keys are ascending integers.
-_RKEY = count()
RANDOMS_TO_MANAGE: WeakValueDictionary[int, RandomLike] = WeakValueDictionary(
- {next(_RKEY): random}
+ {_global_random_rkey: random}
)
@@ -115,7 +117,11 @@ def register_random(r: RandomLike) -> None:
if not (hasattr(r, "seed") and hasattr(r, "getstate") and hasattr(r, "setstate")):
raise InvalidArgument(f"{r=} does not have all the required methods")
- if r in RANDOMS_TO_MANAGE.values():
+ if r in [
+ random
+ for ref in RANDOMS_TO_MANAGE.data.copy().values() # type: ignore
+ if (random := ref()) is not None
+ ]:
return
if not (PYPY or GRAALPY): # pragma: no branch
@@ -148,6 +154,14 @@ def register_random(r: RandomLike) -> None:
RANDOMS_TO_MANAGE[next(_RKEY)] = r
+# Used to make the warning issued by `deprecate_random_in_strategy` thread-safe,
+# as well as to avoid warning on uses of st.randoms().
+# Store just the hash to reduce memory consumption. This is an underapproximation
+# of membership (distinct items might have the same hash), which is fine for the
+# warning, as it results in missed alarms, not false alarms.
+_known_random_state_hashes: set[Any] = set()
+
+
def get_seeder_and_restorer(
seed: Hashable = 0,
) -> tuple[Callable[[], None], Callable[[], None]]:
@@ -171,16 +185,57 @@ def get_seeder_and_restorer(
NP_RANDOM = RANDOMS_TO_MANAGE[next(_RKEY)] = NumpyRandomWrapper()
def seed_all() -> None:
+ global _most_recent_random_state_enter
assert not states
- for k, r in RANDOMS_TO_MANAGE.items():
+ # access .data.copy().items() instead of .items() to avoid a "dictionary
+ # changed size during iteration" error under multithreading.
+ #
+ # I initially expected this to be fixed by
+ # https://github.com/python/cpython/commit/96d37dbcd23e65a7a57819aeced9034296ef747e,
+ # but I believe that is addressing the size change from weakrefs expiring
+ # during gc, not from the user adding new elements to the dict.
+ #
+ # Since we're accessing .data, we have to manually handle checking for
+ # expired ref instances during iteration. Normally WeakValueDictionary
+ # handles this for us.
+ #
+ # This command reproduces at time of writing:
+ # pytest hypothesis-python/tests/ -k test_intervals_are_equivalent_to_their_lists
+ # --parallel-threads 2
+ for k, ref in RANDOMS_TO_MANAGE.data.copy().items(): # type: ignore
+ r = ref()
+ if r is None:
+ # ie the random instance has been gc'd
+ continue # pragma: no cover
states[k] = r.getstate()
+ if k == _global_random_rkey:
+ # r.seed sets the random's state. We want to add that state to
+ # _known_random_states before calling r.seed, in case a thread
+ # switch occurs between the two. To figure out the seed -> state
+ # mapping, set the seed on a dummy random and add that state to
+ # _known_random_state.
+ #
+ # we could use a global dummy random here, but then we'd have to
+ # put a lock around it, and it's not clear to me if that's more
+ # efficient than constructing a new instance each time.
+ dummy_random = Random()
+ dummy_random.seed(seed)
+ _known_random_state_hashes.add(hash(dummy_random.getstate()))
+ # we expect `assert r.getstate() == dummy_random.getstate()` to
+ # hold here, but thread switches means it might not.
+
r.seed(seed)
def restore_all() -> None:
for k, state in states.items():
r = RANDOMS_TO_MANAGE.get(k)
- if r is not None: # i.e., hasn't been garbage-collected
- r.setstate(state)
+ if r is None: # i.e., has been garbage-collected
+ continue
+
+ if k == _global_random_rkey:
+ _known_random_state_hashes.add(hash(state))
+ r.setstate(state)
+
states.clear()
return seed_all, restore_all
@@ -195,9 +250,11 @@ def deterministic_PRNG(seed: int = 0) -> Generator[None, None, None]:
bad idea in principle, and breaks all kinds of independence assumptions
in practice.
"""
- if hypothesis.core._hypothesis_global_random is None: # pragma: no cover
- hypothesis.core._hypothesis_global_random = random.Random()
- register_random(hypothesis.core._hypothesis_global_random)
+ if (
+ hypothesis.core.threadlocal._hypothesis_global_random is None
+ ): # pragma: no cover
+ hypothesis.core.threadlocal._hypothesis_global_random = Random()
+ register_random(hypothesis.core.threadlocal._hypothesis_global_random)
seed_all, restore_all = get_seeder_and_restorer(seed)
seed_all()
@@ -205,3 +262,7 @@ def deterministic_PRNG(seed: int = 0) -> Generator[None, None, None]:
yield
finally:
restore_all()
+ # TODO it would be nice to clean up _known_random_state_hashes when no
+ # active deterministic_PRNG contexts remain, to free memory (see similar
+ # logic in StackframeLimiter). But it's a bit annoying to get right, and
+ # likely not a big deal.
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py b/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py
index 9a7996ce21f..338088d2a8c 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py
@@ -42,10 +42,7 @@ def belongs_to(package: ModuleType) -> Callable[[str], bool]:
except KeyError:
pass
try:
- if not filepath.startswith("<frozen "):
- Path(filepath).resolve().relative_to(root)
- else:
- raise ValueError
+ Path(filepath).resolve().relative_to(root)
result = True
except Exception:
result = False
@@ -125,7 +122,7 @@ class InterestingOrigin(NamedTuple):
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
seen = (*seen, exception)
make = partial(cls.from_exception, seen=seen)
- context: "InterestingOrigin | tuple[()]" = ()
+ context: InterestingOrigin | tuple[()] = ()
if exception.__context__ is not None and exception.__context__ not in seen:
context = make(exception.__context__)
return cls(
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py b/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py
index 49673ca07a5..352c084b506 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py
@@ -20,7 +20,7 @@ def fail_health_check(settings, message, label):
return
message += (
"\nSee "
- "https://hypothesis.readthedocs.io/en/latest/reference/api.html#health-checks "
+ "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck "
"for more information about this. "
f"If you want to disable just this health check, add {label} "
"to the suppress_health_check settings for this test."
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py
index 1bf96cd2a5f..7753642dfca 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py
@@ -28,6 +28,13 @@ IntervalsT: "TypeAlias" = tuple[tuple[int, int], ...]
@final
class IntervalSet:
+ """
+ A compact and efficient representation of a set of ``(a, b)`` intervals. Can
+ be treated like a set of integers, in that ``n in intervals`` will return
+ ``True`` if ``n`` is contained in any of the ``(a, b)`` intervals, and
+ ``False`` otherwise.
+ """
+
@classmethod
def from_string(cls, s: str) -> "Self":
"""Return a tuple of intervals, covering the codepoints of characters in `s`.
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py
index 9449e894ce2..c594ced5f7e 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py
@@ -10,107 +10,393 @@
"""Observability tools to spit out analysis-ready tables, one row per test case."""
+import base64
+import dataclasses
import json
+import math
import os
import sys
import time
import warnings
+from collections.abc import Generator
+from contextlib import contextmanager
+from dataclasses import dataclass
from datetime import date, timedelta
from functools import lru_cache
-from typing import Any, Callable, Optional
+from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast
from hypothesis.configuration import storage_directory
from hypothesis.errors import HypothesisWarning
-from hypothesis.internal.conjecture.data import ConjectureData, Status
+from hypothesis.internal.conjecture.choice import (
+ BooleanConstraints,
+ BytesConstraints,
+ ChoiceConstraintsT,
+ ChoiceNode,
+ ChoiceT,
+ ChoiceTypeT,
+ FloatConstraints,
+ IntegerConstraints,
+ StringConstraints,
+)
+from hypothesis.internal.escalation import InterestingOrigin
+from hypothesis.internal.floats import float_to_int
+from hypothesis.internal.intervalsets import IntervalSet
+
+if TYPE_CHECKING:
+ from typing import TypeAlias
+
+ from hypothesis.internal.conjecture.data import ConjectureData, Spans, Status
+
+
+@dataclass
+class PredicateCounts:
+ satisfied: int = 0
+ unsatisfied: int = 0
+
+ def update_count(self, *, condition: bool) -> None:
+ if condition:
+ self.satisfied += 1
+ else:
+ self.unsatisfied += 1
+
+
+def _choice_to_json(choice: Union[ChoiceT, None]) -> Any:
+ if choice is None:
+ return None
+ # see the note on the same check in to_jsonable for why we cast large
+ # integers to floats.
+ if (
+ isinstance(choice, int)
+ and not isinstance(choice, bool)
+ and abs(choice) >= 2**63
+ ):
+ return ["integer", str(choice)]
+ elif isinstance(choice, bytes):
+ return ["bytes", base64.b64encode(choice).decode()]
+ elif isinstance(choice, float) and math.isnan(choice):
+ # handle nonstandard nan bit patterns. We don't need to do this for -0.0
+ # vs 0.0 since json doesn't normalize -0.0 to 0.0.
+ return ["float", float_to_int(choice)]
+ return choice
+
+
+def choices_to_json(choices: tuple[ChoiceT, ...]) -> list[Any]:
+ return [_choice_to_json(choice) for choice in choices]
+
+
+def _constraints_to_json(
+ choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT
+) -> dict[str, Any]:
+ constraints = constraints.copy()
+ if choice_type == "integer":
+ constraints = cast(IntegerConstraints, constraints)
+ return {
+ "min_value": _choice_to_json(constraints["min_value"]),
+ "max_value": _choice_to_json(constraints["max_value"]),
+ "weights": (
+ None
+ if constraints["weights"] is None
+ # wrap up in a list, instead of a dict, because json dicts
+ # require string keys
+ else [
+ (_choice_to_json(k), v) for k, v in constraints["weights"].items()
+ ]
+ ),
+ "shrink_towards": _choice_to_json(constraints["shrink_towards"]),
+ }
+ elif choice_type == "float":
+ constraints = cast(FloatConstraints, constraints)
+ return {
+ "min_value": _choice_to_json(constraints["min_value"]),
+ "max_value": _choice_to_json(constraints["max_value"]),
+ "allow_nan": constraints["allow_nan"],
+ "smallest_nonzero_magnitude": constraints["smallest_nonzero_magnitude"],
+ }
+ elif choice_type == "string":
+ constraints = cast(StringConstraints, constraints)
+ assert isinstance(constraints["intervals"], IntervalSet)
+ return {
+ "intervals": constraints["intervals"].intervals,
+ "min_size": _choice_to_json(constraints["min_size"]),
+ "max_size": _choice_to_json(constraints["max_size"]),
+ }
+ elif choice_type == "bytes":
+ constraints = cast(BytesConstraints, constraints)
+ return {
+ "min_size": _choice_to_json(constraints["min_size"]),
+ "max_size": _choice_to_json(constraints["max_size"]),
+ }
+ elif choice_type == "boolean":
+ constraints = cast(BooleanConstraints, constraints)
+ return {
+ "p": constraints["p"],
+ }
+ else:
+ raise NotImplementedError(f"unknown choice type {choice_type}")
+
+
+def nodes_to_json(nodes: tuple[ChoiceNode, ...]) -> list[dict[str, Any]]:
+ return [
+ {
+ "type": node.type,
+ "value": _choice_to_json(node.value),
+ "constraints": _constraints_to_json(node.type, node.constraints),
+ "was_forced": node.was_forced,
+ }
+ for node in nodes
+ ]
+
+
+@dataclass
+class ObservationMetadata:
+ traceback: Optional[str]
+ reproduction_decorator: Optional[str]
+ predicates: dict[str, PredicateCounts]
+ backend: dict[str, Any]
+ sys_argv: list[str]
+ os_getpid: int
+ imported_at: float
+ data_status: "Status"
+ interesting_origin: Optional[InterestingOrigin]
+ choice_nodes: Optional[tuple[ChoiceNode, ...]]
+ choice_spans: Optional["Spans"]
+
+ def to_json(self) -> dict[str, Any]:
+ data = {
+ "traceback": self.traceback,
+ "reproduction_decorator": self.reproduction_decorator,
+ "predicates": self.predicates,
+ "backend": self.backend,
+ "sys.argv": self.sys_argv,
+ "os.getpid()": self.os_getpid,
+ "imported_at": self.imported_at,
+ "data_status": self.data_status,
+ "interesting_origin": self.interesting_origin,
+ "choice_nodes": (
+ None if self.choice_nodes is None else nodes_to_json(self.choice_nodes)
+ ),
+ "choice_spans": (
+ None
+ if self.choice_spans is None
+ else [
+ (
+ # span.label is an int, but cast to string to avoid conversion
+ # to float (and loss of precision) for large label values.
+ #
+ # The value of this label is opaque to consumers anyway, so its
+ # type shouldn't matter as long as it's consistent.
+ str(span.label),
+ span.start,
+ span.end,
+ span.discarded,
+ )
+ for span in self.choice_spans
+ ]
+ ),
+ }
+ # check that we didn't forget one
+ assert len(data) == len(dataclasses.fields(self))
+ return data
+
+
+@dataclass
+class BaseObservation:
+ type: Literal["test_case", "info", "alert", "error"]
+ property: str
+ run_start: float
+
+
+InfoObservationType = Literal["info", "alert", "error"]
+TestCaseStatus = Literal["gave_up", "passed", "failed"]
-TESTCASE_CALLBACKS: list[Callable[[dict], None]] = []
+@dataclass
+class InfoObservation(BaseObservation):
+ type: InfoObservationType
+ title: str
+ content: Union[str, dict]
-def deliver_json_blob(value: dict) -> None:
+
+@dataclass
+class TestCaseObservation(BaseObservation):
+ __test__ = False # no! bad pytest!
+
+ type: Literal["test_case"]
+ status: TestCaseStatus
+ status_reason: str
+ representation: str
+ arguments: dict
+ how_generated: str
+ features: dict
+ coverage: Optional[dict[str, list[int]]]
+ timing: dict[str, float]
+ metadata: ObservationMetadata
+
+
+Observation: "TypeAlias" = Union[InfoObservation, TestCaseObservation]
+
+#: A list of callback functions for :ref:`observability <observability>`. Whenever
+#: a new observation is created, each function in this list will be called with a
+#: single value, which is a dictionary representing that observation.
+#:
+#: You can append a function to this list to receive observability reports, and
+#: remove that function from the list to stop receiving observability reports.
+#: Observability is considered enabled if this list is nonempty.
+TESTCASE_CALLBACKS: list[Callable[[Observation], None]] = []
+
+
+@contextmanager
+def with_observation_callback(
+ callback: Callable[[Observation], None],
+) -> Generator[None, None, None]:
+ TESTCASE_CALLBACKS.append(callback)
+ try:
+ yield
+ finally:
+ TESTCASE_CALLBACKS.remove(callback)
+
+
+def deliver_observation(observation: Observation) -> None:
for callback in TESTCASE_CALLBACKS:
- callback(value)
+ callback(observation)
def make_testcase(
*,
- start_timestamp: float,
- test_name_or_nodeid: str,
- data: ConjectureData,
+ run_start: float,
+ property: str,
+ data: "ConjectureData",
how_generated: str,
- string_repr: str = "<unknown>",
+ representation: str = "<unknown>",
arguments: Optional[dict] = None,
timing: dict[str, float],
coverage: Optional[dict[str, list[int]]] = None,
phase: Optional[str] = None,
backend_metadata: Optional[dict[str, Any]] = None,
-) -> dict:
- if data.interesting_origin:
+ status: Optional[
+ Union[TestCaseStatus, "Status"]
+ ] = None, # overrides automatic calculation
+ status_reason: Optional[str] = None, # overrides automatic calculation
+ # added to calculated metadata. If keys overlap, the value from this `metadata`
+ # is used
+ metadata: Optional[dict[str, Any]] = None,
+) -> TestCaseObservation:
+ from hypothesis.core import reproduction_decorator
+ from hypothesis.internal.conjecture.data import Status
+
+ # We should only be sending observability reports for datas that have finished
+ # being modified.
+ assert data.frozen
+
+ if status_reason is not None:
+ pass
+ elif data.interesting_origin:
status_reason = str(data.interesting_origin)
elif phase == "shrink" and data.status == Status.OVERRUN:
status_reason = "exceeded size of current best example"
else:
status_reason = str(data.events.pop("invalid because", ""))
- return {
- "type": "test_case",
- "run_start": start_timestamp,
- "property": test_name_or_nodeid,
- "status": {
- Status.OVERRUN: "gave_up",
- Status.INVALID: "gave_up",
- Status.VALID: "passed",
- Status.INTERESTING: "failed",
- }[data.status],
- "status_reason": status_reason,
- "representation": string_repr,
- "arguments": {
+ status_map: dict[Status, TestCaseStatus] = {
+ Status.OVERRUN: "gave_up",
+ Status.INVALID: "gave_up",
+ Status.VALID: "passed",
+ Status.INTERESTING: "failed",
+ }
+
+ if status is not None and isinstance(status, Status):
+ status = status_map[status]
+ if status is None:
+ status = status_map[data.status]
+
+ return TestCaseObservation(
+ type="test_case",
+ status=status,
+ status_reason=status_reason,
+ representation=representation,
+ arguments={
k.removeprefix("generate:"): v for k, v in (arguments or {}).items()
},
- "how_generated": how_generated, # iid, mutation, etc.
- "features": {
+ how_generated=how_generated, # iid, mutation, etc.
+ features={
**{
f"target:{k}".strip(":"): v for k, v in data.target_observations.items()
},
**data.events,
},
- "timing": timing,
- "metadata": {
- "traceback": data.expected_traceback,
- "predicates": dict(data._observability_predicates),
- "backend": backend_metadata or {},
- **_system_metadata(),
- },
- "coverage": coverage,
- }
+ coverage=coverage,
+ timing=timing,
+ metadata=ObservationMetadata(
+ **{
+ "traceback": data.expected_traceback,
+ "reproduction_decorator": (
+ reproduction_decorator(data.choices) if status == "failed" else None
+ ),
+ "predicates": dict(data._observability_predicates),
+ "backend": backend_metadata or {},
+ "data_status": data.status,
+ "interesting_origin": data.interesting_origin,
+ "choice_nodes": data.nodes if OBSERVABILITY_CHOICES else None,
+ "choice_spans": data.spans if OBSERVABILITY_CHOICES else None,
+ **_system_metadata(),
+ # unpack last so it takes precedence for duplicate keys
+ **(metadata or {}),
+ }
+ ),
+ run_start=run_start,
+ property=property,
+ )
_WROTE_TO = set()
-def _deliver_to_file(value): # pragma: no cover
- kind = "testcases" if value["type"] == "test_case" else "info"
+def _deliver_to_file(observation: Observation) -> None: # pragma: no cover
+ from hypothesis.strategies._internal.utils import to_jsonable
+
+ kind = "testcases" if observation.type == "test_case" else "info"
fname = storage_directory("observed", f"{date.today().isoformat()}_{kind}.jsonl")
fname.parent.mkdir(exist_ok=True, parents=True)
_WROTE_TO.add(fname)
with fname.open(mode="a") as f:
- f.write(json.dumps(value) + "\n")
+ f.write(json.dumps(to_jsonable(observation, avoid_realization=False)) + "\n")
_imported_at = time.time()
@lru_cache
-def _system_metadata():
+def _system_metadata() -> dict[str, Any]:
return {
- "sys.argv": sys.argv,
- "os.getpid()": os.getpid(),
+ "sys_argv": sys.argv,
+ "os_getpid": os.getpid(),
"imported_at": _imported_at,
}
+#: If ``False``, do not collect coverage information when observability is enabled.
+#:
+#: This is exposed both for performance (as coverage collection can be slow on
+#: Python 3.11 and earlier) and size (if you do not use coverage information,
+#: you may not want to store it in-memory).
OBSERVABILITY_COLLECT_COVERAGE = (
"HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_NOCOVER" not in os.environ
)
+#: If ``True``, include the ``metadata.choice_nodes`` and ``metadata.spans`` keys
+#: in test case observations.
+#:
+#: ``False`` by default. ``metadata.choice_nodes`` and ``metadata.spans`` can be
+#: a substantial amount of data, and so must be opted-in to, even when
+#: observability is enabled.
+#:
+#: .. warning::
+#:
+#: EXPERIMENTAL AND UNSTABLE. We are actively working towards a better
+#: interface for this as of June 2025, and this attribute may disappear or
+#: be renamed without notice.
+#:
+OBSERVABILITY_CHOICES = "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_CHOICES" in os.environ
+
if OBSERVABILITY_COLLECT_COVERAGE is False and (
sys.version_info[:2] >= (3, 12)
): # pragma: no cover
@@ -120,8 +406,10 @@ if OBSERVABILITY_COLLECT_COVERAGE is False and (
HypothesisWarning,
stacklevel=2,
)
-if "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY" in os.environ or (
- OBSERVABILITY_COLLECT_COVERAGE is False
+
+if (
+ "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY" in os.environ
+ or OBSERVABILITY_COLLECT_COVERAGE is False
): # pragma: no cover
TESTCASE_CALLBACKS.append(_deliver_to_file)
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py b/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py
index 6e3de425aaf..6a7a0551635 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py
@@ -29,15 +29,18 @@ from keyword import iskeyword
from random import _inst as global_random_instance
from tokenize import COMMENT, detect_encoding, generate_tokens, untokenize
from types import ModuleType
-from typing import Any, Callable, Optional, TypeVar
+from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
from unittest.mock import _patch as PatchType
from weakref import WeakKeyDictionary
from hypothesis.errors import HypothesisWarning
-from hypothesis.internal.compat import is_typed_named_tuple
+from hypothesis.internal.compat import EllipsisType, is_typed_named_tuple
from hypothesis.utils.conventions import not_set
from hypothesis.vendor.pretty import pretty
+if TYPE_CHECKING:
+ from hypothesis.strategies._internal.strategies import SearchStrategy
+
T = TypeVar("T")
READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True"
@@ -182,7 +185,11 @@ def arg_is_required(param: Parameter) -> bool:
)
-def required_args(target, args=(), kwargs=()):
+def required_args(
+ target: Callable[..., Any],
+ args: tuple["SearchStrategy[Any]", ...] = (),
+ kwargs: Optional[dict[str, Union["SearchStrategy[Any]", EllipsisType]]] = None,
+) -> set[str]:
"""Return a set of names of required args to target that were not supplied
in args or kwargs.
@@ -191,6 +198,7 @@ def required_args(target, args=(), kwargs=()):
and bound methods). args and kwargs should be as they are passed to
builds() - that is, a tuple of values and a dict of names: values.
"""
+ kwargs = {} if kwargs is None else kwargs
# We start with a workaround for NamedTuples, which don't have nice inits
if inspect.isclass(target) and is_typed_named_tuple(target):
provided = set(kwargs) | set(target._fields[: len(args)])
@@ -322,9 +330,13 @@ def _extract_lambda_source(f):
source = LINE_CONTINUATION.sub(" ", source)
source = WHITESPACE.sub(" ", source)
source = source.strip()
- if "lambda" not in source and sys.platform == "emscripten": # pragma: no cover
- return if_confused # work around Pyodide bug in inspect.getsource()
- assert "lambda" in source, source
+ if "lambda" not in source: # pragma: no cover
+ # If a user starts a hypothesis process, then edits their code, the lines
+ # in the parsed source code might not match the live __code__ objects.
+ #
+ # (and on sys.platform == "emscripten", this can happen regardless
+ # due to a pyodide bug in inspect.getsource()).
+ return if_confused
tree = None
@@ -445,7 +457,7 @@ def get_pretty_function_description(f: object) -> str:
return pretty(f)
if not hasattr(f, "__name__"):
return repr(f)
- name = f.__name__ # type: ignore # validated by hasattr above
+ name = f.__name__ # type: ignore
if name == "<lambda>":
return extract_lambda_source(f)
elif isinstance(f, (types.MethodType, types.BuiltinMethodType)):
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py
index 1df6a043adb..af65613091c 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py
@@ -35,7 +35,7 @@ Branch: "TypeAlias" = tuple[Optional[Location], Location]
Trace: "TypeAlias" = set[Branch]
-@lru_cache(maxsize=None)
def should_trace_file(fname: str) -> bool:
# fname.startswith("<") indicates runtime code-generation via compile,
# e.g. compile("def ...", "<string>", "exec") in e.g. attrs methods.
@@ -54,11 +54,17 @@ if sys.version_info[:2] >= (3, 12):
class Tracer:
"""A super-simple branch coverage tracer."""
- __slots__ = ("_previous_location", "_should_trace", "branches")
+ __slots__ = (
+ "_previous_location",
+ "_should_trace",
+ "_tried_and_failed_to_trace",
+ "branches",
+ )
def __init__(self, *, should_trace: bool) -> None:
self.branches: Trace = set()
self._previous_location: Optional[Location] = None
+ self._tried_and_failed_to_trace = False
self._should_trace = should_trace and self.can_trace()
@staticmethod
@@ -96,6 +102,8 @@ class Tracer:
self._previous_location = current_location
def __enter__(self):
+ self._tried_and_failed_to_trace = False
+
if not self._should_trace:
return self
@@ -103,7 +111,14 @@ class Tracer:
sys.settrace(self.trace)
return self
- sys.monitoring.use_tool_id(MONITORING_TOOL_ID, "scrutineer")
+ try:
+ sys.monitoring.use_tool_id(MONITORING_TOOL_ID, "scrutineer")
+ except ValueError:
+ # another thread may have registered a tool for MONITORING_TOOL_ID
+ # since we checked in can_trace.
+ self._tried_and_failed_to_trace = True
+ return self
+
for event, callback_name in MONITORING_EVENTS.items():
sys.monitoring.set_events(MONITORING_TOOL_ID, event)
callback = getattr(self, callback_name)
@@ -119,6 +134,9 @@ class Tracer:
sys.settrace(None)
return
+ if self._tried_and_failed_to_trace:
+ return
+
sys.monitoring.free_tool_id(MONITORING_TOOL_ID)
for event in MONITORING_EVENTS:
sys.monitoring.register_callback(MONITORING_TOOL_ID, event, None)
@@ -136,11 +154,7 @@ UNHELPFUL_LOCATIONS = (
"/re/__init__.py", # refactored in Python 3.11
"/warnings.py",
# Quite rarely, the first AFNP line is in Pytest's internals.
- "/_pytest/_io/saferepr.py",
- "/_pytest/_io/terminalwriter.py",
- "/_pytest/assertion/*.py",
- "/_pytest/config/__init__.py",
- "/_pytest/pytester.py",
+ "/_pytest/**",
"/pluggy/_*.py",
# used by pytest for failure formatting in the terminal
"/pygments/lexer.py",
@@ -149,17 +163,19 @@ UNHELPFUL_LOCATIONS = (
"/reprlib.py",
"/typing.py",
"/conftest.py",
+ "/pprint.py",
)
def _glob_to_re(locs: Iterable[str]) -> str:
"""Translate a list of glob patterns to a combined regular expression.
- Only the * wildcard is supported, and patterns including special
+ Only the * and ** wildcards are supported, and patterns including special
characters will only work by chance."""
# fnmatch.translate is not an option since its "*" consumes path sep
return "|".join(
- loc.replace("*", r"[^/]+")
- .replace(".", re.escape("."))
+ loc.replace(".", re.escape("."))
+ .replace("**", r".+")
+ .replace("*", r"[^/]+")
.replace("/", re.escape(sep))
+ r"\Z" # right anchored
for loc in locs
diff --git a/contrib/python/hypothesis/py3/hypothesis/provisional.py b/contrib/python/hypothesis/py3/hypothesis/provisional.py
index 3f6f27ef2db..47f63d75737 100644
--- a/contrib/python/hypothesis/py3/hypothesis/provisional.py
+++ b/contrib/python/hypothesis/py3/hypothesis/provisional.py
@@ -189,7 +189,7 @@ def urls() -> st.SearchStrategy[str]:
"""
def url_encode(s: str) -> str:
- return "".join(c if c in URL_SAFE_CHARACTERS else "%%%02X" % ord(c) for c in s)
+ return "".join(c if c in URL_SAFE_CHARACTERS else f"%{ord(c):02X}" for c in s)
schemes = st.sampled_from(["http", "https"])
ports = st.integers(min_value=1, max_value=2**16 - 1).map(":{}".format)
diff --git a/contrib/python/hypothesis/py3/hypothesis/stateful.py b/contrib/python/hypothesis/py3/hypothesis/stateful.py
index 2ade095dbe8..e10085dcf0a 100644
--- a/contrib/python/hypothesis/py3/hypothesis/stateful.py
+++ b/contrib/python/hypothesis/py3/hypothesis/stateful.py
@@ -16,17 +16,16 @@ Notably, the set of steps available at any point may depend on the
execution to date.
"""
import collections
+import dataclasses
import inspect
from collections.abc import Iterable, Sequence
-from copy import copy
+from dataclasses import dataclass, field
from functools import lru_cache
from io import StringIO
from time import perf_counter
from typing import Any, Callable, ClassVar, Optional, TypeVar, Union, overload
from unittest import TestCase
-import attr
-
from hypothesis import strategies as st
from hypothesis._settings import (
HealthCheck,
@@ -37,7 +36,7 @@ from hypothesis._settings import (
from hypothesis.control import _current_build_context, current_build_context
from hypothesis.core import TestFunc, given
from hypothesis.errors import InvalidArgument, InvalidDefinition
-from hypothesis.internal.compat import add_note
+from hypothesis.internal.compat import add_note, batched
from hypothesis.internal.conjecture import utils as cu
from hypothesis.internal.conjecture.engine import BUFFER_SIZE
from hypothesis.internal.conjecture.junkdrawer import gc_cumulative_time
@@ -115,7 +114,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0
@given(st.data())
def run_state_machine(factory, data):
cd = data.conjecture_data
- machine = factory()
+ machine: RuleBasedStateMachine = factory()
check_type(RuleBasedStateMachine, machine, "state_machine_factory()")
cd.hypothesis_runner = machine
machine._observability_predicates = cd._observability_predicates # alias
@@ -178,7 +177,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0
cd.draw_times[draw_label] += perf_counter() - start_draw - in_gctime
# Pretty-print the values this rule was called with *before* calling
- # _add_result_to_targets, to avoid printing arguments which are also
+ # _add_results_to_targets, to avoid printing arguments which are also
# a return value using the variable name they are assigned to.
# See https://github.com/HypothesisWorks/hypothesis/issues/2341
if print_steps or TESTCASE_CALLBACKS:
@@ -207,12 +206,9 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0
if rule.targets:
if isinstance(result, MultipleResults):
- for single_result in result.values:
- machine._add_result_to_targets(
- rule.targets, single_result
- )
+ machine._add_results_to_targets(rule.targets, result.values)
else:
- machine._add_result_to_targets(rule.targets, result)
+ machine._add_results_to_targets(rule.targets, [result])
elif result is not None:
fail_health_check(
settings,
@@ -273,6 +269,13 @@ class StateMachineMeta(type):
return super().__setattr__(name, value)
+@dataclass
+class _SetupState:
+ rules: list["Rule"]
+ invariants: list["Invariant"]
+ initializers: list["Rule"]
+
+
class RuleBasedStateMachine(metaclass=StateMachineMeta):
"""A RuleBasedStateMachine gives you a structured way to define state machines.
@@ -284,23 +287,14 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
At any given point a random applicable rule will be executed.
"""
- _rules_per_class: ClassVar[dict[type, list[classmethod]]] = {}
- _invariants_per_class: ClassVar[dict[type, list[classmethod]]] = {}
- _initializers_per_class: ClassVar[dict[type, list[classmethod]]] = {}
+ _setup_state_per_class: ClassVar[dict[type, _SetupState]] = {}
def __init__(self) -> None:
- if not self.rules():
- raise InvalidDefinition(f"Type {type(self).__name__} defines no rules")
- self.bundles: dict[str, list] = {}
- self.names_counters: collections.Counter = collections.Counter()
- self.names_list: list[str] = []
- self.names_to_values: dict[str, Any] = {}
- self.__stream = StringIO()
- self.__printer = RepresentationPrinter(
- self.__stream, context=_current_build_context.value
- )
- self._initialize_rules_to_run = copy(self.initialize_rules())
- self._rules_strategy = RuleStrategy(self)
+ setup_state = self.setup_state()
+ if not setup_state.rules:
+ raise InvalidDefinition(
+ f"State machine {type(self).__name__} defines no rules"
+ )
if isinstance(s := vars(type(self)).get("settings"), Settings):
tname = type(self).__name__
@@ -311,6 +305,21 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
f"on the {tname} class."
)
+ self.rules = setup_state.rules
+ self.invariants = setup_state.invariants
+ # copy since we pop from this as we run initialize rules.
+ self._initialize_rules_to_run = setup_state.initializers.copy()
+
+ self.bundles: dict[str, list] = {}
+ self.names_counters: collections.Counter = collections.Counter()
+ self.names_list: list[str] = []
+ self.names_to_values: dict[str, Any] = {}
+ self.__stream = StringIO()
+ self.__printer = RepresentationPrinter(
+ self.__stream, context=_current_build_context.value
+ )
+ self._rules_strategy = RuleStrategy(self)
+
def _pretty_print(self, value):
if isinstance(value, VarReference):
return value.name
@@ -336,7 +345,7 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
self.names_list.append(result)
return result
- def _last_names(self, n):
+ def _last_names(self, n: int) -> list[str]:
len_ = len(self.names_list)
assert len_ >= n
return self.names_list[len_ - n :]
@@ -345,79 +354,95 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
return self.bundles.setdefault(name, [])
@classmethod
- def initialize_rules(cls):
+ def setup_state(cls):
try:
- return cls._initializers_per_class[cls]
+ return cls._setup_state_per_class[cls]
except KeyError:
pass
- cls._initializers_per_class[cls] = []
- for _, v in inspect.getmembers(cls):
- r = getattr(v, INITIALIZE_RULE_MARKER, None)
- if r is not None:
- cls._initializers_per_class[cls].append(r)
- return cls._initializers_per_class[cls]
-
- @classmethod
- def rules(cls):
- try:
- return cls._rules_per_class[cls]
- except KeyError:
- pass
+ rules: list[Rule] = []
+ initializers: list[Rule] = []
+ invariants: list[Invariant] = []
- cls._rules_per_class[cls] = []
- for _, v in inspect.getmembers(cls):
- r = getattr(v, RULE_MARKER, None)
- if r is not None:
- cls._rules_per_class[cls].append(r)
- return cls._rules_per_class[cls]
+ for _name, f in inspect.getmembers(cls):
+ rule = getattr(f, RULE_MARKER, None)
+ initializer = getattr(f, INITIALIZE_RULE_MARKER, None)
+ invariant = getattr(f, INVARIANT_MARKER, None)
+ if rule is not None:
+ rules.append(rule)
+ if initializer is not None:
+ initializers.append(initializer)
+ if invariant is not None:
+ invariants.append(invariant)
- @classmethod
- def invariants(cls):
- try:
- return cls._invariants_per_class[cls]
- except KeyError:
- pass
+ if (
+ getattr(f, PRECONDITIONS_MARKER, None) is not None
+ and rule is None
+ and invariant is None
+ ):
+ raise InvalidDefinition(
+ f"{_rule_qualname(f)} has been decorated with @precondition, "
+ "but not @rule (or @invariant), which is not allowed. A "
+ "precondition must be combined with a rule or an invariant, "
+ "since it has no effect alone."
+ )
- target = []
- for _, v in inspect.getmembers(cls):
- i = getattr(v, INVARIANT_MARKER, None)
- if i is not None:
- target.append(i)
- cls._invariants_per_class[cls] = target
- return cls._invariants_per_class[cls]
+ state = _SetupState(
+ rules=rules, initializers=initializers, invariants=invariants
+ )
+ cls._setup_state_per_class[cls] = state
+ return state
- def _repr_step(self, rule, data, result):
+ def _repr_step(self, rule: "Rule", data: Any, result: Any) -> str:
output_assignment = ""
+ extra_assignment_lines = []
if rule.targets:
+ number_of_results = (
+ len(result.values) if isinstance(result, MultipleResults) else 1
+ )
+ number_of_last_names = len(rule.targets) * number_of_results
+ last_names = self._last_names(number_of_last_names)
if isinstance(result, MultipleResults):
if len(result.values) == 1:
- output_assignment = f"({self._last_names(1)[0]},) = "
+ # len-1 tuples
+ output_per_target = [f"({name},)" for name in last_names]
+ output_assignment = " = ".join(output_per_target) + " = "
elif result.values:
- number_of_last_names = len(rule.targets) * len(result.values)
- output_names = self._last_names(number_of_last_names)
- output_assignment = ", ".join(output_names) + " = "
+ # multiple values, multiple targets -- use the first target
+ # for the assignment from function, and do the other target
+ # assignments on separate lines
+ names_per_target = list(batched(last_names, number_of_results))
+ first_target_output = ", ".join(names_per_target[0])
+ output_assignment = first_target_output + " = "
+ for other_target_names in names_per_target[1:]:
+ other_target_output = ", ".join(other_target_names)
+ extra_assignment_lines.append(
+ other_target_output + " = " + first_target_output
+ )
else:
- output_assignment = self._last_names(1)[0] + " = "
- args = ", ".join("%s=%s" % kv for kv in data.items())
- return f"{output_assignment}state.{rule.function.__name__}({args})"
+ output_assignment = " = ".join(last_names) + " = "
+ args = ", ".join(f"{k}={v}" for k, v in data.items())
+ output_line = f"{output_assignment}state.{rule.function.__name__}({args})"
+ return "\n".join([output_line] + extra_assignment_lines)
- def _add_result_to_targets(self, targets, result):
+ def _add_results_to_targets(self, targets, results):
+ # Note, the assignment order here is reflected in _repr_step
for target in targets:
- name = self._new_name(target)
+ for result in results:
+ name = self._new_name(target)
- def printer(obj, p, cycle, name=name):
- return p.text(name)
+ def printer(obj, p, cycle, name=name):
+ return p.text(name)
- # see
- # https://github.com/HypothesisWorks/hypothesis/pull/4266#discussion_r1949619102
- if not _is_singleton(result):
- self.__printer.singleton_pprinters.setdefault(id(result), printer)
- self.names_to_values[name] = result
- self.bundles.setdefault(target, []).append(VarReference(name))
+ # see
+ # https://github.com/HypothesisWorks/hypothesis/pull/4266#discussion_r1949619102
+ if not _is_singleton(result):
+ self.__printer.singleton_pprinters.setdefault(id(result), printer)
+ self.names_to_values[name] = result
+ self.bundles.setdefault(target, []).append(VarReference(name))
def check_invariants(self, settings, output, runtimes):
- for invar in self.invariants():
+ for invar in self.invariants:
if self._initialize_rules_to_run and not invar.check_during_init:
continue
if not all(precond(self) for precond in invar.preconditions):
@@ -467,15 +492,17 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
return StateMachineTestCase
[email protected](repr=False)
+@dataclass
class Rule:
- targets = attr.ib()
- function = attr.ib(repr=get_pretty_function_description)
- arguments = attr.ib()
- preconditions = attr.ib()
- bundles = attr.ib(init=False)
+ targets: Any
+ function: Any
+ arguments: Any
+ preconditions: Any
+ bundles: tuple["Bundle", ...] = field(init=False)
+ _cached_hash: Optional[int] = field(init=False, default=None)
+ _cached_repr: Optional[str] = field(init=False, default=None)
- def __attrs_post_init__(self):
+ def __post_init__(self):
self.arguments_strategies = {}
bundles = []
for k, v in sorted(self.arguments.items()):
@@ -488,9 +515,30 @@ class Rule:
self.bundles = tuple(bundles)
def __repr__(self) -> str:
- rep = get_pretty_function_description
- bits = [f"{k}={rep(v)}" for k, v in attr.asdict(self).items() if v]
- return f"{self.__class__.__name__}({', '.join(bits)})"
+ if self._cached_repr is None:
+ bits = [
+ f"{field.name}="
+ f"{get_pretty_function_description(getattr(self, field.name))}"
+ for field in dataclasses.fields(self)
+ if getattr(self, field.name)
+ ]
+ self._cached_repr = f"{self.__class__.__name__}({', '.join(bits)})"
+ return self._cached_repr
+
+ def __hash__(self):
+ # sampled_from uses hash in calc_label, and we want this to be fast when
+ # sampling stateful rules, so we cache here.
+ if self._cached_hash is None:
+ self._cached_hash = hash(
+ (
+ self.targets,
+ self.function,
+ tuple(self.arguments.items()),
+ self.preconditions,
+ self.bundles,
+ )
+ )
+ return self._cached_hash
self_strategy = st.runner()
@@ -498,6 +546,7 @@ self_strategy = st.runner()
class BundleReferenceStrategy(SearchStrategy):
def __init__(self, name: str, *, consume: bool = False):
+ super().__init__()
self.name = name
self.consume = consume
@@ -541,6 +590,7 @@ class Bundle(SearchStrategy[Ex]):
def __init__(
self, name: str, *, consume: bool = False, draw_references: bool = True
) -> None:
+ super().__init__()
self.name = name
self.__reference_strategy = BundleReferenceStrategy(name, consume=consume)
self.draw_references = draw_references
@@ -560,7 +610,7 @@ class Bundle(SearchStrategy[Ex]):
# We assume that a bundle will grow over time
return False
- def available(self, data):
+ def _available(self, data):
# ``self_strategy`` is an instance of the ``st.runner()`` strategy.
# Hence drawing from it only returns the current state machine without
# modifying the underlying buffer.
@@ -576,6 +626,14 @@ class Bundle(SearchStrategy[Ex]):
).flatmap(expand)
return super().flatmap(expand)
+ def __hash__(self):
+ # Making this hashable means we hit the fast path of "everything is
+ # hashable" in st.sampled_from label calculation when sampling which rule
+ # to invoke next.
+
+ # Mix in "Bundle" for collision resistance
+ return hash(("Bundle", self.name))
+
class BundleConsumer(Bundle[Ex]):
def __init__(self, bundle: Bundle[Ex]) -> None:
@@ -600,9 +658,9 @@ def consumes(bundle: Bundle[Ex]) -> SearchStrategy[Ex]:
return BundleConsumer(bundle)
+@dataclass
class MultipleResults(Iterable[Ex]):
- values = attr.ib()
+ values: tuple[Ex, ...]
def __iter__(self):
return iter(self.values)
@@ -623,8 +681,8 @@ def _convert_targets(targets, target):
if target is not None:
if targets:
raise InvalidArgument(
- "Passing both targets=%r and target=%r is redundant - pass "
- "targets=%r instead." % (targets, target, (*targets, target))
+ f"Passing both targets={targets!r} and target={target!r} is "
+ f"redundant - pass targets={(*targets, target)!r} instead."
)
targets = (target,)
@@ -665,6 +723,16 @@ _RuleType = Callable[..., Union[MultipleResults[Ex], Ex]]
_RuleWrapper = Callable[[_RuleType[Ex]], _RuleType[Ex]]
+def _rule_qualname(f: Any) -> str:
+ # we define rules / invariants / initializes inside of wrapper functions, which
+ # makes f.__qualname__ look like:
+ # test_precondition.<locals>.BadStateMachine.has_precondition_but_no_rule
+ # which is not ideal. This function returns just
+ # BadStateMachine.has_precondition_but_no_rule
+ # instead.
+ return f.__qualname__.rsplit("<locals>.")[-1]
+
+
# We cannot exclude `target` or `targets` from any of these signatures because
# otherwise they would be matched against the `kwargs`, either leading to
# overlapping overloads of incompatible return types, or a concrete
@@ -749,15 +817,23 @@ def rule(
def accept(f):
if getattr(f, INVARIANT_MARKER, None):
raise InvalidDefinition(
- "A function cannot be used for both a rule and an invariant.",
- Settings.default,
+ f"{_rule_qualname(f)} is used with both @rule and @invariant, "
+ "which is not allowed. A function may be either a rule or an "
+ "invariant, but not both."
)
existing_rule = getattr(f, RULE_MARKER, None)
existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None)
- if existing_rule is not None or existing_initialize_rule is not None:
+ if existing_rule is not None:
raise InvalidDefinition(
- "A function cannot be used for two distinct rules. ", Settings.default
+ f"{_rule_qualname(f)} has been decorated with @rule twice, which is "
+ "not allowed."
)
+ if existing_initialize_rule is not None:
+ raise InvalidDefinition(
+ f"{_rule_qualname(f)} has been decorated with both @rule and "
+ "@initialize, which is not allowed."
+ )
+
preconditions = getattr(f, PRECONDITIONS_MARKER, ())
rule = Rule(
targets=converted_targets,
@@ -825,19 +901,28 @@ def initialize(
def accept(f):
if getattr(f, INVARIANT_MARKER, None):
raise InvalidDefinition(
- "A function cannot be used for both a rule and an invariant.",
- Settings.default,
+ f"{_rule_qualname(f)} is used with both @initialize and @invariant, "
+ "which is not allowed. A function may be either an initialization "
+ "rule or an invariant, but not both."
)
existing_rule = getattr(f, RULE_MARKER, None)
existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None)
- if existing_rule is not None or existing_initialize_rule is not None:
+ if existing_rule is not None:
raise InvalidDefinition(
- "A function cannot be used for two distinct rules. ", Settings.default
+ f"{_rule_qualname(f)} has been decorated with both @rule and "
+ "@initialize, which is not allowed."
+ )
+ if existing_initialize_rule is not None:
+ raise InvalidDefinition(
+ f"{_rule_qualname(f)} has been decorated with @initialize twice, "
+ "which is not allowed."
)
preconditions = getattr(f, PRECONDITIONS_MARKER, ())
if preconditions:
raise InvalidDefinition(
- "An initialization rule cannot have a precondition. ", Settings.default
+ f"{_rule_qualname(f)} has been decorated with both @initialize and "
+ "@precondition, which is not allowed. An initialization rule "
+ "runs unconditionally and may not have a precondition."
)
rule = Rule(
targets=converted_targets,
@@ -856,9 +941,9 @@ def initialize(
return accept
+@dataclass
class VarReference:
- name = attr.ib()
+ name: str
# There are multiple alternatives for annotating the `precond` type, all of them
@@ -894,18 +979,22 @@ def precondition(precond: Callable[[Any], bool]) -> Callable[[TestFunc], TestFun
existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None)
if existing_initialize_rule is not None:
raise InvalidDefinition(
- "An initialization rule cannot have a precondition. ", Settings.default
+ f"{_rule_qualname(f)} has been decorated with both @initialize and "
+ "@precondition, which is not allowed. An initialization rule "
+ "runs unconditionally and may not have a precondition."
)
rule = getattr(f, RULE_MARKER, None)
invariant = getattr(f, INVARIANT_MARKER, None)
if rule is not None:
assert invariant is None
- new_rule = attr.evolve(rule, preconditions=(*rule.preconditions, precond))
+ new_rule = dataclasses.replace(
+ rule, preconditions=(*rule.preconditions, precond)
+ )
setattr(precondition_wrapper, RULE_MARKER, new_rule)
elif invariant is not None:
assert rule is None
- new_invariant = attr.evolve(
+ new_invariant = dataclasses.replace(
invariant, preconditions=(*invariant.preconditions, precond)
)
setattr(precondition_wrapper, INVARIANT_MARKER, new_invariant)
@@ -921,11 +1010,19 @@ def precondition(precond: Callable[[Any], bool]) -> Callable[[TestFunc], TestFun
return decorator
+@dataclass
class Invariant:
- function = attr.ib(repr=get_pretty_function_description)
- preconditions = attr.ib()
- check_during_init = attr.ib()
+ function: Any
+ preconditions: Any
+ check_during_init: bool
+
+ def __repr__(self) -> str:
+ parts = [
+ f"function={get_pretty_function_description(self.function)}",
+ f"{self.preconditions=}",
+ f"{self.check_during_init=}",
+ ]
+ return f"Invariant({', '.join(parts)})"
def invariant(*, check_during_init: bool = False) -> Callable[[TestFunc], TestFunc]:
@@ -952,14 +1049,14 @@ def invariant(*, check_during_init: bool = False) -> Callable[[TestFunc], TestFu
def accept(f):
if getattr(f, RULE_MARKER, None) or getattr(f, INITIALIZE_RULE_MARKER, None):
raise InvalidDefinition(
- "A function cannot be used for both a rule and an invariant.",
- Settings.default,
+ f"{_rule_qualname(f)} has been decorated with both @invariant and "
+ "@rule, which is not allowed."
)
existing_invariant = getattr(f, INVARIANT_MARKER, None)
if existing_invariant is not None:
raise InvalidDefinition(
- "A function cannot be used for two distinct invariants.",
- Settings.default,
+ f"{_rule_qualname(f)} has been decorated with @invariant twice, "
+ "which is not allowed."
)
preconditions = getattr(f, PRECONDITIONS_MARKER, ())
invar = Invariant(
@@ -982,10 +1079,10 @@ LOOP_LABEL = cu.calc_label_from_name("RuleStrategy loop iteration")
class RuleStrategy(SearchStrategy):
- def __init__(self, machine):
+ def __init__(self, machine: RuleBasedStateMachine) -> None:
super().__init__()
self.machine = machine
- self.rules = list(machine.rules())
+ self.rules = machine.rules.copy()
self.enabled_rules_strategy = st.shared(
FeatureStrategy(at_least_one_of={r.function.__name__ for r in self.rules}),
@@ -1006,13 +1103,18 @@ class RuleStrategy(SearchStrategy):
rule.function.__name__,
)
)
+ self.rules_strategy = st.sampled_from(self.rules)
def __repr__(self):
return f"{self.__class__.__name__}(machine={self.machine.__class__.__name__}({{...}}))"
def do_draw(self, data):
if not any(self.is_valid(rule) for rule in self.rules):
- msg = f"No progress can be made from state {self.machine!r}"
+ rules = ", ".join([rule.function.__name__ for rule in self.rules])
+ msg = (
+ f"No progress can be made from state {self.machine!r}, because no "
+ f"available rule had a True precondition. rules: {rules}"
+ )
raise InvalidDefinition(msg) from None
feature_flags = data.draw(self.enabled_rules_strategy)
@@ -1026,7 +1128,7 @@ class RuleStrategy(SearchStrategy):
# be artificially large.
return self.is_valid(r) and feature_flags.is_enabled(r.function.__name__)
- rule = data.draw(st.sampled_from(self.rules).filter(rule_is_enabled))
+ rule = data.draw(self.rules_strategy.filter(rule_is_enabled))
arguments = {}
for k, strat in rule.arguments_strategies.items():
@@ -1048,7 +1150,7 @@ class RuleStrategy(SearchStrategy):
for pred in rule.preconditions:
meets_precond = pred(self.machine)
where = f"{desc} precondition {get_pretty_function_description(pred)}"
- predicates[where]["satisfied" if meets_precond else "unsatisfied"] += 1
+ predicates[where].update_count(condition=meets_precond)
if not meets_precond:
return False
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py
index 3b08f3a43dd..a487315f99c 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py
@@ -8,20 +8,40 @@
# 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/.
+from collections.abc import Collection, Generator, Iterable, Sequence
from functools import reduce
from itertools import chain
+from typing import Any, Optional, TypeVar, Union
import attr
+# attr/validators.pyi does not expose types for these, even though they exist
+# in source.
+from attr.validators import ( # type: ignore
+ _AndValidator,
+ _InstanceOfValidator,
+ _InValidator,
+ _OptionalValidator,
+)
+from attrs import Attribute, AttrsInstance, Factory
+
from hypothesis import strategies as st
from hypothesis.errors import ResolutionFailed
-from hypothesis.internal.compat import get_type_hints
+from hypothesis.internal.compat import EllipsisType, get_type_hints
from hypothesis.strategies._internal.core import BuildsStrategy
+from hypothesis.strategies._internal.strategies import SearchStrategy
from hypothesis.strategies._internal.types import is_a_type, type_sorting_key
from hypothesis.utils.conventions import infer
+T = TypeVar("T")
+
-def get_attribute_by_alias(fields, alias, *, target=None):
+def get_attribute_by_alias(
+ fields: Iterable[Attribute],
+ alias: str,
+ *,
+ target: Optional[type[AttrsInstance]] = None,
+) -> Attribute:
"""
Get an attrs attribute by its alias, rather than its name (compare
getattr(fields, name)).
@@ -46,12 +66,17 @@ def get_attribute_by_alias(fields, alias, *, target=None):
return matched_fields[0]
-def from_attrs(target, args, kwargs, to_infer):
+def from_attrs(
+ target: type[AttrsInstance],
+ args: tuple[SearchStrategy[Any], ...],
+ kwargs: dict[str, Union[SearchStrategy[Any], EllipsisType]],
+ to_infer: Iterable[str],
+) -> SearchStrategy:
"""An internal version of builds(), specialised for Attrs classes."""
- fields = attr.fields(target)
+ attributes: tuple[Attribute, ...] = attr.fields(target)
kwargs = {k: v for k, v in kwargs.items() if v is not infer}
for name in to_infer:
- attrib = get_attribute_by_alias(fields, name, target=target)
+ attrib = get_attribute_by_alias(attributes, name, target=target)
kwargs[name] = from_attrs_attribute(attrib, target)
# We might make this strategy more efficient if we added a layer here that
# retries drawing if validation fails, for improved composition.
@@ -59,38 +84,47 @@ def from_attrs(target, args, kwargs, to_infer):
return BuildsStrategy(target, args, kwargs)
-def from_attrs_attribute(attrib, target):
+def from_attrs_attribute(
+ attrib: Attribute, target: type[AttrsInstance]
+) -> SearchStrategy:
"""Infer a strategy from the metadata on an attr.Attribute object."""
# Try inferring from the default argument. Note that this will only help if
# the user passed `...` to builds() for this attribute, but in that case
# we use it as the minimal example.
- default = st.nothing()
- if isinstance(attrib.default, attr.Factory):
+ default: SearchStrategy = st.nothing()
+ # attr/__init__.pyi uses overloads to declare Factory as a function, not a
+ # class. This is a fib - at runtime and always, it is a class.
+ if isinstance(attrib.default, Factory): # type: ignore
+ assert attrib.default is not None
if not attrib.default.takes_self:
default = st.builds(attrib.default.factory)
elif attrib.default is not attr.NOTHING:
default = st.just(attrib.default)
# Try inferring None, exact values, or type from attrs provided validators.
- null = st.nothing() # updated to none() on seeing an OptionalValidator
- in_collections = [] # list of in_ validator collections to sample from
- validator_types = set() # type constraints to pass to types_to_strategy()
+
+ # updated to none() on seeing an OptionalValidator
+ null: SearchStrategy = st.nothing()
+ # list of in_ validator collections to sample from
+ in_collections = []
+ # type constraints to pass to types_to_strategy()
+ validator_types = set()
if attrib.validator is not None:
validator = attrib.validator
- if isinstance(validator, attr.validators._OptionalValidator):
+ if isinstance(validator, _OptionalValidator):
null = st.none()
validator = validator.validator
- if isinstance(validator, attr.validators._AndValidator):
+ if isinstance(validator, _AndValidator):
vs = validator._validators
else:
vs = [validator]
for v in vs:
- if isinstance(v, attr.validators._InValidator):
+ if isinstance(v, _InValidator):
if isinstance(v.options, str):
in_collections.append(list(all_substrings(v.options)))
else:
in_collections.append(v.options)
- elif isinstance(v, attr.validators._InstanceOfValidator):
+ elif isinstance(v, _InstanceOfValidator):
validator_types.add(v.type)
# This is the important line. We compose the final strategy from various
@@ -115,7 +149,7 @@ def from_attrs_attribute(attrib, target):
return strat
-def types_to_strategy(attrib, types):
+def types_to_strategy(attrib: Attribute, types: Collection[Any]) -> SearchStrategy:
"""Find all the type metadata for this attribute, reconcile it, and infer a
strategy from the mess."""
# If we know types from the validator(s), that's sufficient.
@@ -141,6 +175,7 @@ def types_to_strategy(attrib, types):
# Otherwise, try the `type` attribute as a fallback, and finally try
# the type hints on a converter (desperate!) before giving up.
if is_a_type(getattr(attrib, "type", None)):
+ assert attrib.type is not None
# The convoluted test is because variable annotations may be stored
# in string form; attrs doesn't evaluate them and we don't handle them.
# See PEP 526, PEP 563, and Hypothesis issue #1004 for details.
@@ -157,7 +192,7 @@ def types_to_strategy(attrib, types):
return st.nothing()
-def ordered_intersection(in_):
+def ordered_intersection(in_: Sequence[Iterable[T]]) -> Generator[T, None, None]:
"""Set union of n sequences, ordered for reproducibility across runs."""
intersection = reduce(set.intersection, in_, set(in_[0]))
for x in chain.from_iterable(in_):
@@ -166,7 +201,7 @@ def ordered_intersection(in_):
intersection.remove(x)
-def all_substrings(s):
+def all_substrings(s: str) -> Generator[str, None, None]:
"""Generate all substrings of `s`, in order of length then occurrence.
Includes the empty string (first), and any duplicates that are present.
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
index bcdbe7f333b..10502615a0b 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py
@@ -352,6 +352,7 @@ class FixedDictStrategy(SearchStrategy[dict[Any, Any]]):
*,
optional: Optional[dict[Any, SearchStrategy[Any]]],
):
+ super().__init__()
dict_type = type(mapping)
self.mapping = mapping
keys = tuple(mapping.keys())
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
index 8e35ebab0b2..0f8c507d773 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py
@@ -48,7 +48,6 @@ import attr
from hypothesis._settings import note_deprecation
from hypothesis.control import (
- RandomSeeder,
cleanup,
current_build_context,
deprecate_random_in_strategy,
@@ -72,6 +71,7 @@ from hypothesis.internal.charmap import (
)
from hypothesis.internal.compat import (
Concatenate,
+ EllipsisType,
ParamSpec,
bit_count,
ceil,
@@ -142,14 +142,6 @@ from hypothesis.strategies._internal.utils import cacheable, defines_strategy
from hypothesis.utils.conventions import not_set
from hypothesis.vendor.pretty import RepresentationPrinter
-if sys.version_info >= (3, 10):
- from types import EllipsisType as EllipsisType
-elif typing.TYPE_CHECKING: # pragma: no cover
- from builtins import ellipsis as EllipsisType
-
-else:
- EllipsisType = type(Ellipsis) # pragma: no cover
-
@cacheable
@defines_strategy(force_reusable_values=True)
@@ -203,13 +195,17 @@ def sampled_from(
that behaviour, use ``sampled_from(seq) if seq else nothing()``.
"""
values = check_sample(elements, "sampled_from")
- try:
- if isinstance(elements, type) and issubclass(elements, enum.Enum):
- repr_ = f"sampled_from({elements.__module__}.{elements.__name__})"
- else:
- repr_ = f"sampled_from({elements!r})"
- except Exception: # pragma: no cover
- repr_ = None
+ force_repr = None
+ # check_sample converts to tuple unconditionally, but we want to preserve
+ # square braces for list reprs.
+ # This will not cover custom sequence implementations which return different
+ # braces (or other, more unusual things) for their reprs, but this is a tradeoff
+ # between repr accuracy and greedily-evaluating all sequence reprs (at great
+ # cost for large sequences).
+ force_repr_braces = ("[", "]") if isinstance(elements, list) else None
+ if isinstance(elements, type) and issubclass(elements, enum.Enum):
+ force_repr = f"sampled_from({elements.__module__}.{elements.__name__})"
+
if isclass(elements) and issubclass(elements, enum.Flag):
# Combinations of enum.Flag members (including empty) are also members. We generate these
# dynamically, because static allocation takes O(2^n) memory. LazyStrategy is used for the
@@ -244,7 +240,7 @@ def sampled_from(
.flatmap(lambda r: sets(sampled_from(flags), min_size=r, max_size=r))
.map(lambda s: elements(reduce(operator.or_, s))),
]
- return LazyStrategy(one_of, args=inner, kwargs={}, force_repr=repr_)
+ return LazyStrategy(one_of, args=inner, kwargs={}, force_repr=force_repr)
if not values:
if (
isinstance(elements, type)
@@ -260,7 +256,9 @@ def sampled_from(
raise InvalidArgument("Cannot sample from a length-zero sequence.")
if len(values) == 1:
return just(values[0])
- return SampledFromStrategy(values, repr_)
+ return SampledFromStrategy(
+ values, force_repr=force_repr, force_repr_braces=force_repr_braces
+ )
@cacheable
@@ -549,8 +547,9 @@ def fixed_dictionaries(
check_strategy(v, f"optional[{k!r}]")
if type(mapping) != type(optional):
raise InvalidArgument(
- "Got arguments of different types: mapping=%s, optional=%s"
- % (nicerepr(type(mapping)), nicerepr(type(optional)))
+ f"Got arguments of different types: "
+ f"mapping={nicerepr(type(mapping))}, "
+ f"optional={nicerepr(type(optional))}"
)
if set(mapping) & set(optional):
raise InvalidArgument(
@@ -983,8 +982,16 @@ def randoms(
)
+class RandomSeeder:
+ def __init__(self, seed):
+ self.seed = seed
+
+ def __repr__(self):
+ return f"RandomSeeder({self.seed!r})"
+
+
class RandomModule(SearchStrategy):
- def do_draw(self, data):
+ def do_draw(self, data: ConjectureData) -> RandomSeeder:
# It would be unsafe to do run this method more than once per test case,
# because cleanup() runs tasks in FIFO order (at time of writing!).
# Fortunately, the random_module() strategy wraps us in shared(), so
@@ -1015,14 +1022,20 @@ def random_module() -> SearchStrategy[RandomSeeder]:
return shared(RandomModule(), key="hypothesis.strategies.random_module()")
-class BuildsStrategy(SearchStrategy):
- def __init__(self, target, args, kwargs):
+class BuildsStrategy(SearchStrategy[Ex]):
+ def __init__(
+ self,
+ target: Callable[..., Ex],
+ args: tuple[SearchStrategy[Any], ...],
+ kwargs: dict[str, SearchStrategy[Any]],
+ ):
+ super().__init__()
self.target = target
self.args = args
self.kwargs = kwargs
- def do_draw(self, data):
- args = [data.draw(a) for a in self.args]
+ 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()}
try:
obj = self.target(*args, **kwargs)
@@ -1058,7 +1071,7 @@ class BuildsStrategy(SearchStrategy):
current_build_context().record_call(obj, self.target, args, kwargs)
return obj
- def validate(self):
+ def do_validate(self) -> None:
tuples(*self.args).validate()
fixed_dictionaries(self.kwargs).validate()
@@ -1140,6 +1153,9 @@ def builds(
# and `...` contains recursion on `cls`. See
# https://github.com/HypothesisWorks/hypothesis/issues/3026
kwargs[kw] = deferred(lambda t=t: from_type(t)) # type: ignore
+
+ # validated by handling all EllipsisType in the to_infer case
+ kwargs = cast(dict[str, SearchStrategy], kwargs)
return BuildsStrategy(target, args, kwargs)
@@ -1148,17 +1164,16 @@ def builds(
def from_type(thing: type[T]) -> SearchStrategy[T]:
"""Looks up the appropriate search strategy for the given type.
- ``from_type`` is used internally to fill in missing arguments to
- :func:`~hypothesis.strategies.builds` and can be used interactively
+ |st.from_type| is used internally to fill in missing arguments to
+ |st.builds| and can be used interactively
to explore what strategies are available or to debug type resolution.
- You can use :func:`~hypothesis.strategies.register_type_strategy` to
+ You can use |st.register_type_strategy| to
handle your custom types, or to globally redefine certain strategies -
for example excluding NaN from floats, or use timezone-aware instead of
naive time and datetime strategies.
- The resolution logic may be changed in a future version, but currently
- tries these five options:
+ |st.from_type| looks up a strategy in the following order:
1. If ``thing`` is in the default lookup mapping or user-registered lookup,
return the corresponding strategy. The default lookup covers all types
@@ -1170,14 +1185,14 @@ def from_type(thing: type[T]) -> SearchStrategy[T]:
other elements in the lookup.
4. Finally, if ``thing`` has type annotations for all required arguments,
and is not an abstract class, it is resolved via
- :func:`~hypothesis.strategies.builds`.
+ |st.builds|.
5. Because :mod:`abstract types <python:abc>` cannot be instantiated,
we treat abstract types as the union of their concrete subclasses.
Note that this lookup works via inheritance but not via
:obj:`~python:abc.ABCMeta.register`, so you may still need to use
- :func:`~hypothesis.strategies.register_type_strategy`.
+ |st.register_type_strategy|.
- There is a valuable recipe for leveraging ``from_type()`` to generate
+ There is a valuable recipe for leveraging |st.from_type| to generate
"everything except" values from a specified type. I.e.
.. code-block:: python
@@ -1190,9 +1205,9 @@ def from_type(thing: type[T]) -> SearchStrategy[T]:
)
For example, ``everything_except(int)`` returns a strategy that can
- generate anything that ``from_type()`` can ever generate, except for
- instances of :class:`python:int`, and excluding instances of types
- added via :func:`~hypothesis.strategies.register_type_strategy`.
+ generate anything that |st.from_type| can ever generate, except for
+ instances of |int|, and excluding instances of types
+ added via |st.register_type_strategy|.
This is useful when writing tests which check that invalid input is
rejected in a certain way.
@@ -1784,6 +1799,7 @@ def recursive(
class PermutationStrategy(SearchStrategy):
def __init__(self, values):
+ super().__init__()
self.values = values
def do_draw(self, data):
@@ -1814,6 +1830,7 @@ def permutations(values: Sequence[T]) -> SearchStrategy[list[T]]:
class CompositeStrategy(SearchStrategy):
def __init__(self, definition, args, kwargs):
+ super().__init__()
self.definition = definition
self.args = args
self.kwargs = kwargs
@@ -1831,12 +1848,13 @@ class DrawFn(Protocol):
.. code-block:: python
+ def draw(strategy: SearchStrategy[Ex], label: object = None) -> Ex: ...
+
@composite
def list_and_index(draw: DrawFn) -> tuple[int, str]:
- i = draw(integers()) # type inferred as 'int'
- s = draw(text()) # type inferred as 'str'
+ i = draw(integers()) # type of `i` inferred as 'int'
+ s = draw(text()) # type of `s` inferred as 'str'
return i, s
-
"""
def __init__(self):
@@ -2158,20 +2176,18 @@ def uuids(
class RunnerStrategy(SearchStrategy):
def __init__(self, default):
+ super().__init__()
self.default = default
def do_draw(self, data):
- runner = getattr(data, "hypothesis_runner", not_set)
- if runner is not_set:
+ if data.hypothesis_runner is not_set:
if self.default is not_set:
raise InvalidArgument(
"Cannot use runner() strategy with no "
"associated runner or explicit default."
)
- else:
- return self.default
- else:
- return runner
+ return self.default
+ return data.hypothesis_runner
@defines_strategy(force_reusable_values=True)
@@ -2210,6 +2226,7 @@ class DataObject:
return "data(...)"
def draw(self, strategy: SearchStrategy[Ex], label: Any = None) -> Ex:
+ """Like :obj:`~hypothesis.strategies.DrawFn`."""
check_strategy(strategy, "strategy")
self.count += 1
desc = f"Draw {self.count}{'' if label is None else f' ({label})'}"
@@ -2322,7 +2339,7 @@ def register_type_strategy(
"""Add an entry to the global type-to-strategy lookup.
This lookup is used in :func:`~hypothesis.strategies.builds` and
- :func:`@given <hypothesis.given>`.
+ |@given|.
:func:`~hypothesis.strategies.builds` will be used automatically for
classes with type annotations on ``__init__`` , so you only need to
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py
index 12f5154ad2f..4bf3a020715 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py
@@ -12,7 +12,7 @@ import datetime as dt
import operator as op
import zoneinfo
from calendar import monthrange
-from functools import lru_cache, partial
+from functools import cache, partial
from importlib import resources
from pathlib import Path
from typing import Optional
@@ -120,6 +120,7 @@ def draw_capped_multipart(
class DatetimeStrategy(SearchStrategy):
def __init__(self, min_value, max_value, timezones_strat, allow_imaginary):
+ super().__init__()
assert isinstance(min_value, dt.datetime)
assert isinstance(max_value, dt.datetime)
assert min_value.tzinfo is None
@@ -219,6 +220,7 @@ def datetimes(
class TimeStrategy(SearchStrategy):
def __init__(self, min_value, max_value, timezones_strat):
+ super().__init__()
self.min_value = min_value
self.max_value = max_value
self.tz_strat = timezones_strat
@@ -257,6 +259,7 @@ def times(
class DateStrategy(SearchStrategy):
def __init__(self, min_value, max_value):
+ super().__init__()
assert isinstance(min_value, dt.date)
assert isinstance(max_value, dt.date)
assert min_value < max_value
@@ -320,6 +323,7 @@ def dates(
class TimedeltaStrategy(SearchStrategy):
def __init__(self, min_value, max_value):
+ super().__init__()
assert isinstance(min_value, dt.timedelta)
assert isinstance(max_value, dt.timedelta)
assert min_value < max_value
@@ -359,7 +363,7 @@ def timedeltas(
return TimedeltaStrategy(min_value=min_value, max_value=max_value)
-@lru_cache(maxsize=None)
+@cache
def _valid_key_cacheable(tzpath, key):
assert isinstance(tzpath, tuple) # zoneinfo changed, better update this function!
for root in tzpath:
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py
index f37ff421e92..915b9d7f18c 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py
@@ -124,7 +124,7 @@ class FeatureFlags:
class FeatureStrategy(SearchStrategy[FeatureFlags]):
- def __init__(self, at_least_one_of: Sequence[Hashable] = ()):
+ def __init__(self, at_least_one_of: Iterable[Hashable] = ()):
super().__init__()
self._at_least_one_of = frozenset(at_least_one_of)
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py
index b398c165e21..d5c706c6d7e 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py
@@ -8,7 +8,7 @@
# 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/.
-from collections.abc import MutableMapping, Sequence
+from collections.abc import Sequence
from inspect import signature
from typing import Any, Callable, Optional
from weakref import WeakKeyDictionary
@@ -21,46 +21,46 @@ from hypothesis.internal.reflection import (
get_pretty_function_description,
repr_call,
)
+from hypothesis.strategies._internal.deferred import DeferredStrategy
from hypothesis.strategies._internal.strategies import Ex, RecurT, SearchStrategy
+from hypothesis.utils.threading import ThreadLocal
-unwrap_cache: MutableMapping[SearchStrategy, SearchStrategy] = WeakKeyDictionary()
-unwrap_depth = 0
+threadlocal = ThreadLocal(unwrap_depth=int, unwrap_cache=WeakKeyDictionary)
def unwrap_strategies(s):
- global unwrap_depth
-
- if not isinstance(s, SearchStrategy):
+ # optimization
+ if not isinstance(s, (LazyStrategy, DeferredStrategy)):
return s
+
try:
- return unwrap_cache[s]
+ return threadlocal.unwrap_cache[s]
except KeyError:
pass
- unwrap_cache[s] = s
+ threadlocal.unwrap_cache[s] = s
+ threadlocal.unwrap_depth += 1
try:
- unwrap_depth += 1
+ result = unwrap_strategies(s.wrapped_strategy)
+ threadlocal.unwrap_cache[s] = result
+
try:
- result = unwrap_strategies(s.wrapped_strategy)
- unwrap_cache[s] = result
- try:
- assert result.force_has_reusable_values == s.force_has_reusable_values
- except AttributeError:
- pass
+ assert result.force_has_reusable_values == s.force_has_reusable_values
+ except AttributeError:
+ pass
- try:
- result.force_has_reusable_values = s.force_has_reusable_values
- except AttributeError:
- pass
- return result
+ try:
+ result.force_has_reusable_values = s.force_has_reusable_values
except AttributeError:
- return s
+ pass
+
+ return result
finally:
- unwrap_depth -= 1
- if unwrap_depth <= 0:
- unwrap_cache.clear()
- assert unwrap_depth >= 0
+ threadlocal.unwrap_depth -= 1
+ if threadlocal.unwrap_depth <= 0:
+ threadlocal.unwrap_cache.clear()
+ assert threadlocal.unwrap_depth >= 0
class LazyStrategy(SearchStrategy[Ex]):
@@ -119,13 +119,12 @@ class LazyStrategy(SearchStrategy[Ex]):
base = self.function(*self.__args, **self.__kwargs)
if unwrapped_args == self.__args and unwrapped_kwargs == self.__kwargs:
- self.__wrapped_strategy = base
+ _wrapped_strategy = base
else:
- self.__wrapped_strategy = self.function(
- *unwrapped_args, **unwrapped_kwargs
- )
+ _wrapped_strategy = self.function(*unwrapped_args, **unwrapped_kwargs)
for method, fn in self._transformations:
- self.__wrapped_strategy = getattr(self.__wrapped_strategy, method)(fn)
+ _wrapped_strategy = getattr(_wrapped_strategy, method)(fn)
+ self.__wrapped_strategy = _wrapped_strategy
assert self.__wrapped_strategy is not None
return self.__wrapped_strategy
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py
index 59bf66ecc9b..afb00bab7eb 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py
@@ -50,6 +50,7 @@ Real = Union[int, float, Fraction, Decimal]
class IntegersStrategy(SearchStrategy[int]):
def __init__(self, start: Optional[int], end: Optional[int]) -> None:
+ super().__init__()
assert isinstance(start, int) or start is None
assert isinstance(end, int) or end is None
assert start is None or end is None or start <= end
@@ -127,15 +128,15 @@ def integers(
if min_value is not None:
if min_value != int(min_value):
raise InvalidArgument(
- "min_value=%r of type %r cannot be exactly represented as an integer."
- % (min_value, type(min_value))
+ f"min_value={min_value!r} of type {type(min_value)!r} "
+ "cannot be exactly represented as an integer."
)
min_value = int(min_value)
if max_value is not None:
if max_value != int(max_value):
raise InvalidArgument(
- "max_value=%r of type %r cannot be exactly represented as an integer."
- % (max_value, type(max_value))
+ f"max_value={max_value!r} of type {type(max_value)!r} "
+ "cannot be exactly represented as an integer."
)
max_value = int(max_value)
@@ -422,8 +423,8 @@ def floats(
# This is a custom alternative to check_valid_interval, because we want
# to include the bit-width and exclusion information in the message.
msg = (
- "There are no %s-bit floating-point values between min_value=%r "
- "and max_value=%r" % (width, min_arg, max_arg)
+ f"There are no {width}-bit floating-point values between "
+ f"min_value={min_arg!r} and max_value={max_arg!r}"
)
if exclude_min or exclude_max:
msg += f", {exclude_min=} and {exclude_max=}"
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py
index e28b2596f6e..1050f13d3a0 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py
@@ -434,6 +434,7 @@ class TrueRandom(HypothesisRandom):
class RandomStrategy(SearchStrategy[HypothesisRandom]):
def __init__(self, *, note_method_calls: bool, use_true_random: bool) -> None:
+ super().__init__()
self.__note_method_calls = note_method_calls
self.__use_true_random = use_true_random
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/recursive.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/recursive.py
index 5f632111448..aa665ce0b82 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/recursive.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/recursive.py
@@ -73,6 +73,7 @@ class LimitedStrategy(SearchStrategy):
class RecursiveStrategy(SearchStrategy):
def __init__(self, base, extend, max_leaves):
+ super().__init__()
self.max_leaves = max_leaves
self.base = base
self.limited_base = LimitedStrategy(base)
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py
index 285dbb68f8f..d79b3389c73 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py
@@ -18,6 +18,7 @@ from hypothesis.strategies._internal.strategies import Ex
class SharedStrategy(SearchStrategy[Ex]):
def __init__(self, base: SearchStrategy[Ex], key: Optional[Hashable] = None):
+ super().__init__()
self.key = key
self.base = base
@@ -34,7 +35,27 @@ class SharedStrategy(SearchStrategy[Ex]):
# Ideally would be -> Ex, but key collisions with different-typed values are
# possible. See https://github.com/HypothesisWorks/hypothesis/issues/4301.
def do_draw(self, data: ConjectureData) -> Any:
+ if self.key is None or getattr(self.base, "_is_singleton", False):
+ strat_label = id(self.base)
+ else:
+ # Assume that uncached strategies are distinguishable by their
+ # label. False negatives (even collisions w/id above) are ok as
+ # long as they are infrequent.
+ strat_label = self.base.label
key = self.key or self
if key not in data._shared_strategy_draws:
- data._shared_strategy_draws[key] = data.draw(self.base)
- return data._shared_strategy_draws[key]
+ drawn = data.draw(self.base)
+ data._shared_strategy_draws[key] = (strat_label, drawn)
+ else:
+ drawn_strat_label, drawn = data._shared_strategy_draws[key]
+ # Check disabled pending resolution of #4301
+ if drawn_strat_label != strat_label: # pragma: no cover
+ pass
+ # warnings.warn(
+ # f"Different strategies are shared under {key=}. This"
+ # " risks drawing values that are not valid examples for the strategy,"
+ # " or that have a narrower range than expected.",
+ # HypothesisWarning,
+ # stacklevel=1,
+ # )
+ return drawn
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py
index 58cb66b2f1c..fcd25befd6f 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py
@@ -9,11 +9,13 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import sys
+import threading
import warnings
from collections import abc, defaultdict
from collections.abc import Sequence
from functools import lru_cache
from random import shuffle
+from threading import RLock
from typing import (
TYPE_CHECKING,
Any,
@@ -77,6 +79,8 @@ FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name(
"single loop iteration in FilteredStrategy"
)
+label_lock = RLock()
+
def recursive_property(strategy: "SearchStrategy", name: str, default: object) -> Any:
"""Handle properties which may be mutually recursive among a set of
@@ -104,6 +108,7 @@ def recursive_property(strategy: "SearchStrategy", name: str, default: object) -
and performance of parsing with derivatives." ACM SIGPLAN Notices 51.6
(2016): 224-236.
"""
+ assert name in {"is_empty", "has_reusable_values", "is_cacheable"}
cache_key = "cached_" + name
calculation = "calc_" + name
force_key = "force_" + name
@@ -217,20 +222,24 @@ def recursive_property(strategy: "SearchStrategy", name: str, default: object) -
class SearchStrategy(Generic[Ex]):
- """A SearchStrategy is an object that knows how to explore data of a given
- type.
+ """A ``SearchStrategy`` tells Hypothesis how to generate that kind of input.
- Except where noted otherwise, methods on this class are not part of
- the public API and their behaviour may change significantly between
- minor version releases. They will generally be stable between patch
- releases.
+ This class is only part of the public API for use in type annotations, so that
+ you can write e.g. ``-> SearchStrategy[Foo]`` for your function which returns
+ ``builds(Foo, ...)``. Do not inherit from or directly instantiate this class.
"""
- validate_called: bool = False
- __label: Union[int, UniqueIdentifier, None] = None
__module__: str = "hypothesis.strategies"
+ LABELS: ClassVar[dict[type, int]] = {}
+ # triggers `assert isinstance(label, int)` under threading when setting this
+ # in init instead of a classvar. I'm not sure why, init should be safe. But
+ # this works so I'm not looking into it further atm.
+ __label: Union[int, UniqueIdentifier, None] = None
+
+ def __init__(self):
+ self.validate_called: dict[int, bool] = {}
- def available(self, data: ConjectureData) -> bool:
+ def _available(self, data: ConjectureData) -> bool:
"""Returns whether this strategy can *currently* draw any
values. This typically useful for stateful testing where ``Bundle``
grows over time a list of value to choose from.
@@ -287,15 +296,12 @@ class SearchStrategy(Generic[Ex]):
def calc_has_reusable_values(self, recur: RecurT) -> bool:
return False
- def example(self) -> Ex:
- """Provide an example of the sort of value that this strategy
- generates. This is biased to be slightly simpler than is typical for
- values from this strategy, for clarity purposes.
+ def example(self) -> Ex: # FIXME
+ """Provide an example of the sort of value that this strategy generates.
- This method shouldn't be taken too seriously. It's here for interactive
- exploration of the API, not for any sort of real testing.
-
- This method is part of the public API.
+ This method is designed for use in a REPL, and will raise an error if
+ called from inside |@given| or a strategy definition. For serious use,
+ see |@composite| or |st.data|.
"""
if getattr(sys, "ps1", None) is None: # pragma: no branch
# The other branch *is* covered in cover/test_examples.py; but as that
@@ -304,7 +310,7 @@ class SearchStrategy(Generic[Ex]):
"The `.example()` method is good for exploring strategies, but should "
"only be used interactively. We recommend using `@given` for tests - "
"it performs better, saves and replays failures to avoid flakiness, "
- "and reports minimal examples. (strategy: %r)" % (self,),
+ f"and reports minimal examples. (strategy: {self!r})",
NonInteractiveExampleWarning,
stacklevel=2,
)
@@ -361,10 +367,9 @@ class SearchStrategy(Generic[Ex]):
return self.__examples.pop()
def map(self, pack: Callable[[Ex], T]) -> "SearchStrategy[T]":
- """Returns a new strategy that generates values by generating a value
- from this strategy and then calling pack() on the result, giving that.
-
- This method is part of the public API.
+ """Returns a new strategy which generates a value from this one, and
+ then returns ``pack(value)``. For example, ``integers().map(str)``
+ could generate ``str(5)`` == ``"5"``.
"""
if is_identity_function(pack):
return self # type: ignore # Mypy has no way to know that `Ex == T`
@@ -372,12 +377,21 @@ class SearchStrategy(Generic[Ex]):
def flatmap(
self, expand: Callable[[Ex], "SearchStrategy[T]"]
- ) -> "SearchStrategy[T]":
- """Returns a new strategy that generates values by generating a value
- from this strategy, say x, then generating a value from
- strategy(expand(x))
+ ) -> "SearchStrategy[T]": # FIXME
+ """Old syntax for a special case of |@composite|:
- This method is part of the public API.
+ .. code-block:: python
+
+ @st.composite
+ def flatmap_like(draw, base_strategy, expand):
+ value = draw(base_strategy)
+ new_strategy = expand(value)
+ return draw(new_strategy)
+
+ We find that the greater readability of |@composite| usually outweighs
+ the verbosity, with a few exceptions for simple cases or recipes like
+ ``from_type(type).flatmap(from_type)`` ("pick a type, get a strategy for
+ any instance of that type, and then generate one of those").
"""
from hypothesis.strategies._internal.flatmapped import FlatMapStrategy
@@ -393,11 +407,21 @@ class SearchStrategy(Generic[Ex]):
# reference the local TypeVar context.
def filter(self, condition: Callable[[Ex], Any]) -> "SearchStrategy[Ex]":
"""Returns a new strategy that generates values from this strategy
- which satisfy the provided condition. Note that if the condition is too
- hard to satisfy this might result in your tests failing with
- Unsatisfiable.
+ which satisfy the provided condition.
- This method is part of the public API.
+ Note that if the condition is too hard to satisfy this might result
+ in your tests failing with an Unsatisfiable exception.
+ A basic version of the filtering logic would look something like:
+
+ .. code-block:: python
+
+ @st.composite
+ def filter_like(draw, strategy, condition):
+ for _ in range(3):
+ value = draw(strategy)
+ if condition(value):
+ return value
+ assume(False)
"""
return FilteredStrategy(conditions=(condition,), strategy=self)
@@ -460,21 +484,39 @@ class SearchStrategy(Generic[Ex]):
def validate(self) -> None:
"""Throw an exception if the strategy is not valid.
- This can happen due to lazy construction
+ Strategies should implement ``do_validate``, which is called by this
+ method. They should not override ``validate``.
+
+ This can happen due to invalid arguments, or lazy construction.
"""
- if self.validate_called:
+ thread_id = threading.get_ident()
+ if self.validate_called.get(thread_id, False):
return
+ # we need to set validate_called before calling do_validate, for
+ # recursive / deferred strategies. But if a thread switches after
+ # validate_called but before do_validate, we might have a strategy
+ # which does weird things like drawing when do_validate would error but
+ # its params are technically valid (e.g. a param was passed as 1.0
+ # instead of 1) and get into weird internal states.
+ #
+ # There are two ways to fix this.
+ # (1) The first is a per-strategy lock around do_validate. Even though we
+ # expect near-zero lock contention, this still adds the lock overhead.
+ # (2) The second is allowing concurrent .validate calls. Since validation
+ # is (assumed to be) deterministic, both threads will produce the same
+ # end state, so the validation order or race conditions does not matter.
+ #
+ # In order to avoid the lock overhead of (1), we use (2) here. See also
+ # discussion in https://github.com/HypothesisWorks/hypothesis/pull/4473.
try:
- self.validate_called = True
+ self.validate_called[thread_id] = True
self.do_validate()
self.is_empty
self.has_reusable_values
except Exception:
- self.validate_called = False
+ self.validate_called[thread_id] = False
raise
- LABELS: ClassVar[dict[type, int]] = {}
-
@property
def class_label(self) -> int:
cls = self.__class__
@@ -488,12 +530,16 @@ class SearchStrategy(Generic[Ex]):
@property
def label(self) -> int:
- if self.__label is calculating:
- return 0
- if self.__label is None:
+ if isinstance((label := self.__label), int):
+ # avoid locking if we've already completely computed the label.
+ return label
+
+ with label_lock:
+ if self.__label is calculating:
+ return 0
self.__label = calculating
self.__label = self.calc_label()
- return cast(int, self.__label)
+ return self.__label
def calc_label(self) -> int:
return self.class_label
@@ -505,12 +551,17 @@ class SearchStrategy(Generic[Ex]):
raise NotImplementedError(f"{type(self).__name__}.do_draw")
-def is_hashable(value: object) -> bool:
+def _is_hashable(value: object) -> tuple[bool, Optional[int]]:
+ # hashing can be expensive; return the hash value if we compute it, so that
+ # callers don't have to recompute.
try:
- hash(value)
- return True
+ return (True, hash(value))
except TypeError:
- return False
+ return (False, None)
+
+
+def is_hashable(value: object) -> bool:
+ return _is_hashable(value)[0]
class SampledFromStrategy(SearchStrategy[Ex]):
@@ -524,7 +575,9 @@ class SampledFromStrategy(SearchStrategy[Ex]):
def __init__(
self,
elements: Sequence[Ex],
- repr_: Optional[str] = None,
+ *,
+ force_repr: Optional[str] = None,
+ force_repr_braces: Optional[tuple[str, str]] = None,
transformations: tuple[
tuple[Literal["filter", "map"], Callable[[Ex], Any]],
...,
@@ -533,13 +586,17 @@ class SampledFromStrategy(SearchStrategy[Ex]):
super().__init__()
self.elements = cu.check_sample(elements, "sampled_from")
assert self.elements
- self.repr_ = repr_
+ self.force_repr = force_repr
+ self.force_repr_braces = force_repr_braces
self._transformations = transformations
+ self._cached_repr: Optional[str] = None
+
def map(self, pack: Callable[[Ex], T]) -> SearchStrategy[T]:
s = type(self)(
self.elements,
- repr_=self.repr_,
+ force_repr=self.force_repr,
+ force_repr_braces=self.force_repr_braces,
transformations=(*self._transformations, ("map", pack)),
)
# guaranteed by the ("map", pack) transformation
@@ -548,20 +605,30 @@ class SampledFromStrategy(SearchStrategy[Ex]):
def filter(self, condition: Callable[[Ex], Any]) -> SearchStrategy[Ex]:
return type(self)(
self.elements,
- repr_=self.repr_,
+ force_repr=self.force_repr,
+ force_repr_braces=self.force_repr_braces,
transformations=(*self._transformations, ("filter", condition)),
)
- def __repr__(self) -> str:
- return (
- self.repr_
- or "sampled_from(["
- + ", ".join(map(get_pretty_function_description, self.elements))
- + "])"
- ) + "".join(
- f".{name}({get_pretty_function_description(f)})"
- for name, f in self._transformations
- )
+ def __repr__(self):
+ if self._cached_repr is None:
+ rep = get_pretty_function_description
+ elements_s = (
+ ", ".join(rep(v) for v in self.elements[:512]) + ", ..."
+ if len(self.elements) > 512
+ else ", ".join(rep(v) for v in self.elements)
+ )
+ braces = self.force_repr_braces or ("(", ")")
+ instance_s = (
+ self.force_repr or f"sampled_from({braces[0]}{elements_s}{braces[1]})"
+ )
+ transforms_s = "".join(
+ f".{name}({get_pretty_function_description(f)})"
+ for name, f in self._transformations
+ )
+ repr_s = instance_s + transforms_s
+ self._cached_repr = repr_s
+ return self._cached_repr
def calc_label(self) -> int:
# strategy.label is effectively an under-approximation of structural
@@ -592,12 +659,14 @@ class SampledFromStrategy(SearchStrategy[Ex]):
# The worst case performance of this scheme is
# itertools.chain(range(2**100), [st.none()]), where it degrades to
# hashing every int in the range.
-
+ (elements_is_hashable, hash_value) = _is_hashable(self.elements)
if isinstance(self.elements, range) or (
- is_hashable(self.elements)
+ elements_is_hashable
and not any(isinstance(e, SearchStrategy) for e in self.elements)
):
- return combine_labels(self.class_label, calc_label_from_hash(self.elements))
+ return combine_labels(
+ self.class_label, calc_label_from_name(str(hash_value))
+ )
labels = [self.class_label]
for element in self.elements:
@@ -780,13 +849,13 @@ class OneOfStrategy(SearchStrategy[Ex]):
def do_draw(self, data: ConjectureData) -> Ex:
strategy = data.draw(
SampledFromStrategy(self.element_strategies).filter(
- lambda s: s.available(data)
+ lambda s: s._available(data)
)
)
return data.draw(strategy)
def __repr__(self) -> str:
- return "one_of(%s)" % ", ".join(map(repr, self.original_strategies))
+ return "one_of({})".format(", ".join(map(repr, self.original_strategies)))
def do_validate(self) -> None:
for e in self.element_strategies:
@@ -812,8 +881,8 @@ class OneOfStrategy(SearchStrategy[Ex]):
@overload
def one_of(
- __args: Sequence[SearchStrategy[Any]],
-) -> SearchStrategy[Any]: # pragma: no cover
+ __args: Sequence[SearchStrategy[Ex]],
+) -> SearchStrategy[Ex]: # pragma: no cover
...
@@ -1158,6 +1227,6 @@ def check_strategy(arg: object, name: str = "") -> None:
if name:
name += "="
raise InvalidArgument(
- "Expected a SearchStrategy%s but got %s%r (type=%s)"
- % (hint, name, arg, type(arg).__name__)
+ f"Expected a SearchStrategy{hint} but got {name}{arg!r} "
+ f"(type={type(arg).__name__})"
)
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py
index 4f38627180a..352d8577ca9 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py
@@ -11,7 +11,7 @@
import copy
import re
import warnings
-from functools import lru_cache, partial
+from functools import cache, lru_cache, partial
from typing import Optional
from hypothesis.errors import HypothesisWarning, InvalidArgument
@@ -32,7 +32,7 @@ from hypothesis.vendor.pretty import pretty
# Cache size is limited by sys.maxunicode, but passing None makes it slightly faster.
-@lru_cache(maxsize=None)
+@cache
def _check_is_single_character(c):
# In order to mitigate the performance cost of this check, we use a shared cache,
# even at the cost of showing the culprit strategy in the error message.
@@ -50,6 +50,7 @@ class OneCharStringStrategy(SearchStrategy[str]):
def __init__(
self, intervals: IntervalSet, force_repr: Optional[str] = None
) -> None:
+ super().__init__()
assert isinstance(intervals, IntervalSet)
self.intervals = intervals
self._force_repr = force_repr
@@ -75,6 +76,7 @@ class OneCharStringStrategy(SearchStrategy[str]):
)
if codec is not None:
intervals &= charmap.intervals_from_codec(codec)
+
_arg_repr = ", ".join(
f"{k}={v!r}"
for k, v in [
@@ -85,7 +87,8 @@ class OneCharStringStrategy(SearchStrategy[str]):
("exclude_characters", exclude_characters),
("include_characters", include_characters),
]
- if v not in (None, "", set(charmap.categories()) - {"Cs"})
+ if v not in (None, "")
+ and not (k == "categories" and set(v) == set(charmap.categories()) - {"Cs"})
)
if not intervals:
raise InvalidArgument(
@@ -156,9 +159,9 @@ _nonempty_and_content_names = (
class TextStrategy(ListStrategy[str]):
def do_draw(self, data):
# if our element strategy is OneCharStringStrategy, we can skip the
- # ListStrategy draw and jump right to our nice IR string draw.
+ # ListStrategy draw and jump right to data.draw_string.
# Doing so for user-provided element strategies is not correct in
- # general, as they may define a different distribution than our IR.
+ # general, as they may define a different distribution than data.draw_string.
elems = unwrap_strategies(self.element_strategy)
if isinstance(elems, OneCharStringStrategy):
return data.draw_string(
@@ -347,6 +350,7 @@ def _identifier_characters():
class BytesStrategy(SearchStrategy):
def __init__(self, min_size: int, max_size: Optional[int]):
+ super().__init__()
self.min_size = min_size
self.max_size = (
max_size if max_size is not None else COLLECTION_DEFAULT_MAX_SIZE
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
index 5e396964af2..9ac5f534eac 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py
@@ -997,6 +997,7 @@ def resolve_Match(thing):
class GeneratorStrategy(st.SearchStrategy):
def __init__(self, yields, returns):
+ super().__init__()
assert isinstance(yields, st.SearchStrategy)
assert isinstance(returns, st.SearchStrategy)
self.yields = yields
diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py
index ec7e5833cf1..dc75cc83586 100644
--- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py
+++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py
@@ -8,6 +8,7 @@
# 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 dataclasses
import sys
import threading
from functools import partial
@@ -17,7 +18,6 @@ from typing import TYPE_CHECKING, Callable
import attr
from hypothesis.internal.cache import LRUReusedCache
-from hypothesis.internal.compat import dataclass_asdict
from hypothesis.internal.floats import clamp, float_to_int
from hypothesis.internal.reflection import proxies
from hypothesis.vendor.pretty import pretty
@@ -88,6 +88,7 @@ def cacheable(fn: "T") -> "T":
else:
result = fn(*args, **kwargs)
if not isinstance(result, SearchStrategy) or result.is_cacheable:
+ result._is_singleton = True
cache[cache_key] = result
return result
@@ -165,6 +166,10 @@ def to_jsonable(obj: object, *, avoid_realization: bool) -> object:
known types.
"""
if isinstance(obj, (str, int, float, bool, type(None))):
+ # We convert integers of 2**63 to floats, to avoid crashing external
+ # utilities with a 64 bit integer cap (notable, sqlite). See
+ # https://github.com/HypothesisWorks/hypothesis/pull/3797#discussion_r1413425110
+ # and https://github.com/simonw/sqlite-utils/issues/605.
if isinstance(obj, int) and not isinstance(obj, bool) and abs(obj) >= 2**63:
# Silently clamp very large ints to max_float, to avoid OverflowError when
# casting to float. (but avoid adding more constraints to symbolic values)
@@ -175,6 +180,7 @@ def to_jsonable(obj: object, *, avoid_realization: bool) -> object:
return obj
if avoid_realization:
return "<symbolic>"
+
recur = partial(to_jsonable, avoid_realization=avoid_realization)
if isinstance(obj, (list, tuple, set, frozenset)):
if isinstance(obj, tuple) and hasattr(obj, "_asdict"):
@@ -199,7 +205,12 @@ def to_jsonable(obj: object, *, avoid_realization: bool) -> object:
and dcs.is_dataclass(obj)
and not isinstance(obj, type)
):
- return recur(dataclass_asdict(obj))
+ # Avoid dataclasses.asdict here to ensure that inner to_json overrides
+ # can get called as well
+ return {
+ field.name: recur(getattr(obj, field.name))
+ for field in dataclasses.fields(obj) # type: ignore
+ }
if attr.has(type(obj)):
return recur(attr.asdict(obj, recurse=False)) # type: ignore
if (pyd := sys.modules.get("pydantic")) and isinstance(obj, pyd.BaseModel):
diff --git a/contrib/python/hypothesis/py3/hypothesis/utils/threading.py b/contrib/python/hypothesis/py3/hypothesis/utils/threading.py
new file mode 100644
index 00000000000..d9c2e550c94
--- /dev/null
+++ b/contrib/python/hypothesis/py3/hypothesis/utils/threading.py
@@ -0,0 +1,51 @@
+# 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 threading
+from typing import Any, Callable
+
+
+class ThreadLocal:
+ """
+ Manages thread-local state. ThreadLocal forwards getattr and setattr to a
+ threading.local() instance. The passed kwargs defines the available attributes
+ on the threadlocal and their default values.
+
+ The only supported names to geattr and setattr are the keys of the passed kwargs.
+ """
+
+ def __init__(self, **kwargs: Callable) -> None:
+ for name, value in kwargs.items():
+ if not callable(value):
+ raise TypeError(f"Attribute {name} must be a callable. Got {value}")
+
+ self.__initialized = False
+ self.__kwargs = kwargs
+ self.__threadlocal = threading.local()
+ self.__initialized = True
+
+ def __getattr__(self, name: str) -> Any:
+ if name not in self.__kwargs:
+ raise AttributeError(f"No attribute {name}")
+
+ if not hasattr(self.__threadlocal, name):
+ default = self.__kwargs[name]()
+ setattr(self.__threadlocal, name, default)
+
+ return getattr(self.__threadlocal, name)
+
+ def __setattr__(self, name: str, value: Any) -> None:
+ # disable attribute-forwarding while initializing
+ if "_ThreadLocal__initialized" not in self.__dict__ or not self.__initialized:
+ super().__setattr__(name, value)
+ else:
+ if name not in self.__kwargs:
+ raise AttributeError(f"No attribute {name}")
+ setattr(self.__threadlocal, name, value)
diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
index b31404b8267..b96818a5221 100644
--- a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
+++ b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py
@@ -83,8 +83,6 @@ if TYPE_CHECKING:
from hypothesis.control import BuildContext
-# ruff: noqa: FBT001
-
T = TypeVar("T")
PrettyPrintFunction: "TypeAlias" = Callable[[Any, "RepresentationPrinter", bool], None]
@@ -692,6 +690,9 @@ def _dict_pprinter_factory(
if cycle:
return p.text("{...}")
+ # NOTE: For compatibility with Python 3.9's LL(1)
+ # parser, this is written as a nested with-statement,
+ # instead of a compound one.
with p.group(1, start, end):
# If the dict contains both "" and b"" (empty string and empty bytes), we
# ignore the BytesWarning raised by `python -bb` mode. We can't use
diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py
index 28ac392f296..01f9058ab9b 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, 130, 13)
+__version_info__ = (6, 136, 2)
__version__ = ".".join(map(str, __version_info__))
diff --git a/contrib/python/hypothesis/py3/patches/01-fix-crash-with-pydebug.patch b/contrib/python/hypothesis/py3/patches/01-fix-crash-with-pydebug.patch
deleted file mode 100644
index 967eb845f85..00000000000
--- a/contrib/python/hypothesis/py3/patches/01-fix-crash-with-pydebug.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- contrib/python/hypothesis/py3/hypothesis/internal/escalation.py (index)
-+++ contrib/python/hypothesis/py3/hypothesis/internal/escalation.py (working tree)
-@@ -38,7 +38,10 @@ def belongs_to(package):
- except KeyError:
- pass
- try:
-- Path(filepath).resolve().relative_to(root)
-+ if not filepath.startswith("<frozen "):
-+ Path(filepath).resolve().relative_to(root)
-+ else:
-+ raise ValueError
- result = True
- except Exception:
- result = False
diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make
index 8750c13d8bc..eb4ab3a2337 100644
--- a/contrib/python/hypothesis/py3/ya.make
+++ b/contrib/python/hypothesis/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(6.130.13)
+VERSION(6.136.2)
LICENSE(MPL-2.0)
@@ -65,6 +65,7 @@ PY_SRCS(
hypothesis/internal/conjecture/junkdrawer.py
hypothesis/internal/conjecture/optimiser.py
hypothesis/internal/conjecture/pareto.py
+ hypothesis/internal/conjecture/provider_conformance.py
hypothesis/internal/conjecture/providers.py
hypothesis/internal/conjecture/shrinker.py
hypothesis/internal/conjecture/shrinking/__init__.py
@@ -120,6 +121,7 @@ PY_SRCS(
hypothesis/utils/conventions.py
hypothesis/utils/dynamicvariables.py
hypothesis/utils/terminal.py
+ hypothesis/utils/threading.py
hypothesis/vendor/__init__.py
hypothesis/vendor/pretty.py
hypothesis/version.py