diff options
| author | robot-piglet <[email protected]> | 2025-11-16 19:23:40 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2025-11-16 21:35:40 +0300 |
| commit | dae705ef49fc13c4fe8bc1ec6bdc985c6eed1efc (patch) | |
| tree | 9da79607f68660a1d05a0bbcf58719d3b635367d /contrib/python/hypothesis | |
| parent | 9a07a28dc0d44861b027fe30ef3b3a607af319b4 (diff) | |
Intermediate changes
commit_hash:d9c3066ef2f9667bb86a57b9033b2a0c91aae04f
Diffstat (limited to 'contrib/python/hypothesis')
78 files changed, 2902 insertions, 2140 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index de8da2ed2bd..dd1a0d3f396 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.136.2 +Version: 6.143.0 Summary: A library for property-based testing Author-email: "David R. MacIver and Zac Hatfield-Dodds" <[email protected]> License-Expression: MPL-2.0 @@ -20,17 +20,17 @@ Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Education :: Testing Classifier: Topic :: Software Development :: Testing Classifier: Typing :: Typed -Requires-Python: >=3.9 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE.txt Requires-Dist: attrs>=22.2.0 @@ -38,12 +38,12 @@ Requires-Dist: exceptiongroup>=1.0.0; python_version < "3.11" Requires-Dist: sortedcontainers<3.0.0,>=2.1.0 Provides-Extra: cli Requires-Dist: click>=7.0; extra == "cli" -Requires-Dist: black>=19.10b0; extra == "cli" +Requires-Dist: black>=20.8b0; extra == "cli" Requires-Dist: rich>=9.0.0; extra == "cli" Provides-Extra: codemods Requires-Dist: libcst>=0.3.16; extra == "codemods" Provides-Extra: ghostwriter -Requires-Dist: black>=19.10b0; extra == "ghostwriter" +Requires-Dist: black>=20.8b0; extra == "ghostwriter" Provides-Extra: pytz Requires-Dist: pytz>=2014.1; extra == "pytz" Provides-Extra: dateutil @@ -51,7 +51,7 @@ Requires-Dist: python-dateutil>=1.4; extra == "dateutil" Provides-Extra: lark Requires-Dist: lark>=0.10.1; extra == "lark" Provides-Extra: numpy -Requires-Dist: numpy>=1.19.3; extra == "numpy" +Requires-Dist: numpy>=1.21.6; extra == "numpy" Provides-Extra: pandas Requires-Dist: pandas>=1.1; extra == "pandas" Provides-Extra: pytest @@ -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.23; extra == "crosshair" -Requires-Dist: crosshair-tool>=0.0.88; extra == "crosshair" +Requires-Dist: hypothesis-crosshair>=0.0.25; extra == "crosshair" +Requires-Dist: crosshair-tool>=0.0.97; extra == "crosshair" Provides-Extra: zoneinfo Requires-Dist: tzdata>=2025.2; (sys_platform == "win32" or sys_platform == "emscripten") and extra == "zoneinfo" Provides-Extra: django @@ -70,15 +70,15 @@ Requires-Dist: django>=4.2; extra == "django" Provides-Extra: watchdog Requires-Dist: watchdog>=4.0.0; extra == "watchdog" Provides-Extra: all -Requires-Dist: black>=19.10b0; extra == "all" +Requires-Dist: black>=20.8b0; extra == "all" Requires-Dist: click>=7.0; extra == "all" -Requires-Dist: crosshair-tool>=0.0.88; extra == "all" +Requires-Dist: crosshair-tool>=0.0.97; extra == "all" Requires-Dist: django>=4.2; extra == "all" Requires-Dist: dpcontracts>=0.4; extra == "all" -Requires-Dist: hypothesis-crosshair>=0.0.23; extra == "all" +Requires-Dist: hypothesis-crosshair>=0.0.25; 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" +Requires-Dist: numpy>=1.21.6; extra == "all" Requires-Dist: pandas>=1.1; extra == "all" Requires-Dist: pytest>=4.6; extra == "all" Requires-Dist: python-dateutil>=1.4; extra == "all" diff --git a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py index 19fa31e735d..5eaadadd512 100644 --- a/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py +++ b/contrib/python/hypothesis/py3/_hypothesis_ftz_detector.py @@ -18,13 +18,13 @@ import of Hypothesis itself from each subprocess which must import the worker fu import importlib import sys -from typing import TYPE_CHECKING, Callable, Optional +from collections.abc import Callable +from typing import TYPE_CHECKING, TypeAlias if TYPE_CHECKING: from multiprocessing import Queue - from typing import TypeAlias -FTZCulprits: "TypeAlias" = tuple[Optional[bool], set[str]] +FTZCulprits: TypeAlias = tuple[bool | None, set[str]] KNOWN_EVER_CULPRITS = ( diff --git a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py index f7a89268452..340bf90ebfa 100644 --- a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py +++ b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py @@ -49,20 +49,6 @@ _ALL_OPTIONS = [ SEED_OPTION, EXPLAIN_OPTION, ] -_FIXTURE_MSG = """Function-scoped fixture {0!r} used by {1!r} - -Function-scoped fixtures are not reset between examples generated by -`@given(...)`, which is often surprising and can cause subtle test bugs. - -If you were expecting the fixture to run separately for each generated example, -then unfortunately you will need to find a different way to achieve your goal -(e.g. using a similar context manager instead of a fixture). - -If you are confident that your test will work correctly even though the -fixture is not reset between generated examples, you can suppress this health -check to assure Hypothesis that you understand what you are doing. -""" - STATS_KEY = "_hypothesis_stats" FAILING_EXAMPLES_KEY = "_hypothesis_failing_examples" @@ -160,7 +146,9 @@ else: settings_str = settings.default.show_changed() if settings_str != "": settings_str = f" -> {settings_str}" - return f"hypothesis profile {settings._current_profile!r}{settings_str}" + return ( + f"hypothesis profile {settings.get_current_profile_name()!r}{settings_str}" + ) def pytest_configure(config): config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.") @@ -174,7 +162,9 @@ else: verbosity_name = config.getoption(VERBOSITY_OPTION) if verbosity_name and verbosity_name != settings.default.verbosity.name: verbosity_value = Verbosity[verbosity_name] - name = f"{settings._current_profile}-with-{verbosity_name}-verbosity" + name = ( + f"{settings.get_current_profile_name()}-with-{verbosity_name}-verbosity" + ) # register_profile creates a new profile, exactly like the current one, # with the extra values given (in this case 'verbosity') settings.register_profile(name, verbosity=verbosity_value) @@ -183,7 +173,7 @@ else: config.getoption(EXPLAIN_OPTION) and Phase.explain not in settings.default.phases ): - name = f"{settings._current_profile}-with-explain-phase" + name = f"{settings.get_current_profile_name()}-with-explain-phase" phases = (*settings.default.phases, Phase.explain) settings.register_profile(name, phases=phases) settings.load_profile(name) @@ -240,79 +230,97 @@ else: raise_hypothesis_usage_error(message % (name,)) yield - else: - from hypothesis import HealthCheck, settings as Settings - from hypothesis.internal.escalation import current_pytest_item - from hypothesis.internal.healthcheck import fail_health_check - from hypothesis.reporting import with_reporter - from hypothesis.statistics import collector, describe_statistics + return - # Retrieve the settings for this test from the test object, which - # is normally a Hypothesis wrapped_test wrapper. If this doesn't - # work, the test object is probably something weird - # (e.g a stateful test wrapper), so we skip the function-scoped - # fixture check. - settings = getattr( - item.obj, "_hypothesis_internal_use_settings", Settings.default - ) + from hypothesis import HealthCheck, settings as Settings + from hypothesis.internal.escalation import current_pytest_item + from hypothesis.internal.healthcheck import fail_health_check + from hypothesis.reporting import with_reporter + from hypothesis.statistics import collector, describe_statistics - # Check for suspicious use of function-scoped fixtures, but only - # if the corresponding health check is not suppressed. - fixture_params = False - if not set(settings.suppress_health_check).issuperset( - {HealthCheck.function_scoped_fixture, HealthCheck.differing_executors} - ): - # Warn about function-scoped fixtures, excluding autouse fixtures because - # the advice is probably not actionable and the status quo seems OK... - # See https://github.com/HypothesisWorks/hypothesis/issues/377 for detail. - argnames = None - for fx_defs in item._request._fixturemanager.getfixtureinfo( - node=item, func=item.function, cls=None - ).name2fixturedefs.values(): - if argnames is None: - argnames = frozenset(signature(item.function).parameters) - for fx in fx_defs: - fixture_params |= bool(fx.params) - if fx.argname in argnames: - active_fx = item._request._get_active_fixturedef(fx.argname) - if active_fx.scope == "function": - fail_health_check( - settings, - _FIXTURE_MSG.format(fx.argname, item.nodeid), - HealthCheck.function_scoped_fixture, - ) + # Retrieve the settings for this test from the test object, which + # is normally a Hypothesis wrapped_test wrapper. If this doesn't + # work, the test object is probably something weird + # (e.g a stateful test wrapper), so we skip the function-scoped + # fixture check. + settings = getattr( + item.obj, "_hypothesis_internal_use_settings", Settings.default + ) - if fixture_params or (item.get_closest_marker("parametrize") is not None): - # Disable the differing_executors health check due to false alarms: - # see https://github.com/HypothesisWorks/hypothesis/issues/3733 - from hypothesis import settings as Settings + # Check for suspicious use of function-scoped fixtures, but only + # if the corresponding health check is not suppressed. + fixture_params = False + if not set(settings.suppress_health_check).issuperset( + {HealthCheck.function_scoped_fixture, HealthCheck.differing_executors} + ): + # Warn about function-scoped fixtures, excluding autouse fixtures because + # the advice is probably not actionable and the status quo seems OK... + # See https://github.com/HypothesisWorks/hypothesis/issues/377 for detail. + argnames = None + for fx_defs in item._request._fixturemanager.getfixtureinfo( + node=item, func=item.function, cls=None + ).name2fixturedefs.values(): + if argnames is None: + argnames = frozenset(signature(item.function).parameters) + for fx in fx_defs: + fixture_params |= bool(fx.params) + if fx.argname not in argnames: + continue + active_fx = item._request._get_active_fixturedef(fx.argname) + if active_fx.scope == "function": + fail_health_check( + settings, + f"{item.nodeid!r} uses a function-scoped fixture {fx.argname!r}." + "\n\n" + "Function-scoped fixtures are not reset between inputs " + "generated by `@given(...)`, which is often surprising and " + "can cause subtle test bugs." + "\n\n" + "If you were expecting the fixture to run separately " + "for each generated input, then unfortunately you " + "will need to find a different way to achieve your " + "goal (for example, replacing the fixture with a similar " + "context manager inside of the test)." + "\n\n" + "If you are confident that your test will work correctly " + "even though the fixture is not reset between generated " + "inputs, you can suppress this health check with " + "@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]). " + "See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", + HealthCheck.function_scoped_fixture, + ) - fn = getattr(item.obj, "__func__", item.obj) - fn._hypothesis_internal_use_settings = Settings( - parent=settings, - suppress_health_check={HealthCheck.differing_executors} - | set(settings.suppress_health_check), - ) + if fixture_params or (item.get_closest_marker("parametrize") is not None): + # Disable the differing_executors health check due to false alarms: + # see https://github.com/HypothesisWorks/hypothesis/issues/3733 + fn = getattr(item.obj, "__func__", item.obj) + fn._hypothesis_internal_use_settings = Settings( + parent=settings, + suppress_health_check={HealthCheck.differing_executors} + | set(settings.suppress_health_check), + ) + + # Give every parametrized test invocation a unique database key + key = item.nodeid.encode() + item.obj.hypothesis.inner_test._hypothesis_internal_add_digest = key - # Give every parametrized test invocation a unique database key - key = item.nodeid.encode() - item.obj.hypothesis.inner_test._hypothesis_internal_add_digest = key + store = StoringReporter(item.config) - store = StoringReporter(item.config) + def note_statistics(stats): + stats["nodeid"] = item.nodeid + item.hypothesis_statistics = describe_statistics(stats) - def note_statistics(stats): - stats["nodeid"] = item.nodeid - item.hypothesis_statistics = describe_statistics(stats) + with ( + collector.with_value(note_statistics), + with_reporter(store), + current_pytest_item.with_value(item), + ): + yield - 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 - if store.results: - item.hypothesis_report_information = "\n".join(store.results) + if store.results: + item.hypothesis_report_information = "\n".join(store.results) def _stash_get(config, key, default): if hasattr(config, "stash"): diff --git a/contrib/python/hypothesis/py3/hypothesis/_settings.py b/contrib/python/hypothesis/py3/hypothesis/_settings.py index e361dacfd1a..54c218c7b41 100644 --- a/contrib/python/hypothesis/py3/hypothesis/_settings.py +++ b/contrib/python/hypothesis/py3/hypothesis/_settings.py @@ -27,7 +27,6 @@ from typing import ( ClassVar, Optional, TypeVar, - Union, ) from hypothesis.errors import ( @@ -142,18 +141,47 @@ class HealthCheckMeta(EnumMeta): @unique class HealthCheck(Enum, metaclass=HealthCheckMeta): - """Arguments for :attr:`~hypothesis.settings.suppress_health_check`. + """ + A |HealthCheck| is proactively raised by Hypothesis when Hypothesis detects + that your test has performance problems, which may result in less rigorous + testing than you expect. For example, if your test takes a long time to generate + inputs, or filters away too many inputs using |assume| or |filter|, Hypothesis + will raise a corresponding health check. + + A health check is a proactive warning, not an error. We encourage suppressing + health checks where you have evaluated they will not pose a problem, or where + you have evaluated that fixing the underlying issue is not worthwhile. + + With the exception of |HealthCheck.function_scoped_fixture| and + |HealthCheck.differing_executors|, all health checks warn about performance + problems, not correctness errors. + + Disabling health checks + ----------------------- + + Health checks can be disabled by |settings.suppress_health_check|. To suppress + all health checks, you can pass ``suppress_health_check=list(HealthCheck)``. + + .. seealso:: + + See also the :doc:`/how-to/suppress-healthchecks` how-to. - Each member of this enum is a specific health check to suppress. + Correctness health checks + ------------------------- - Hypothesis' health checks are designed to detect and warn you about performance - problems where your tests are slow, inefficient, or generating very large examples. + Some health checks report potential correctness errors, rather than performance + problems. - 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. + * |HealthCheck.function_scoped_fixture| indicates that a function-scoped + pytest fixture is used by an |@given| test. Many Hypothesis users expect + function-scoped fixtures to reset once per input, but they actually reset once + per test. We proactively raise |HealthCheck.function_scoped_fixture| to + ensure you have considered this case. + * |HealthCheck.differing_executors| indicates that the same |@given| test has + been executed multiple times with multiple distinct executors. + + We recommend treating these particular health checks with more care, as + suppressing them may result in an unsound test. """ def __repr__(self) -> str: @@ -186,14 +214,21 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta): internal reasons.""" too_slow = 3 - """Check for when your data generation is extremely slow and likely to hurt - testing.""" + """ + Check for when input generation is very slow. Since Hypothesis generates 100 + (by default) inputs per test execution, a slowdown in generating each input + can result in very slow tests overall. + """ return_value = 5 """Deprecated; we always error if a test returns a non-None value.""" large_base_example = 7 - """Checks if the natural example to shrink towards is very large.""" + """ + Checks if the smallest natural input to your test is very large. This makes + it difficult for Hypothesis to generate good inputs, especially when trying to + shrink failing inputs. + """ not_a_test_method = 8 """Deprecated; we always error if |@given| is applied @@ -255,11 +290,30 @@ class duration(datetime.timedelta): return f"timedelta(milliseconds={int(ms) if ms == int(ms) else ms!r})" +# see https://adamj.eu/tech/2020/03/09/detect-if-your-tests-are-running-on-ci +# initially from https://github.com/tox-dev/tox/blob/e911788a/src/tox/util/ci.py +_CI_VARS = { + "CI": None, # various, including GitHub Actions, Travis CI, and AppVeyor + # see https://github.com/tox-dev/tox/issues/3442 + "__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI": None, + "TF_BUILD": "true", # Azure Pipelines + "bamboo.buildKey": None, # Bamboo + "BUILDKITE": "true", # Buildkite + "CIRCLECI": "true", # Circle CI + "CIRRUS_CI": "true", # Cirrus CI + "CODEBUILD_BUILD_ID": None, # CodeBuild + "GITHUB_ACTIONS": "true", # GitHub Actions + "GITLAB_CI": None, # GitLab CI + "HEROKU_TEST_RUN_ID": None, # Heroku CI + "TEAMCITY_VERSION": None, # TeamCity +} + + 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 + return any( + key in os.environ and (value is None or os.environ[key] == value) + for key, value in _CI_VARS.items() + ) default_variable = DynamicVariable[Optional["settings"]](None) @@ -332,8 +386,8 @@ def _validate_suppress_health_check(suppressions): def _validate_deadline( - x: Union[int, float, datetime.timedelta, None], -) -> Optional[duration]: + x: int | float | datetime.timedelta | None, +) -> duration | None: if x is None: return x invalid_deadline_error = InvalidArgument( @@ -438,15 +492,17 @@ class settings(metaclass=settingsMeta): settings objects created after the profile was made active, but not in existing settings objects. + .. _builtin-profiles: + 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. + By default, the ``default`` profile is active. If the ``CI`` environment + variable is set to any value, the ``ci`` profile is active by default. Hypothesis + also automatically detects various vendor-specific CI environment variables. The attributes of the currently active settings profile can be retrieved with ``settings()`` (so ``settings().max_examples`` is the currently active default @@ -460,7 +516,7 @@ class settings(metaclass=settingsMeta): "default", max_examples=100, derandomize=False, - database=not_set, # see settings.database for details + database=not_set, # see settings.database for the default database verbosity=Verbosity.normal, phases=tuple(Phase), stateful_step_count=50, @@ -494,7 +550,7 @@ class settings(metaclass=settingsMeta): """ _profiles: ClassVar[dict[str, "settings"]] = {} - _current_profile: ClassVar[Optional[str]] = None + _current_profile: ClassVar[str | None] = None def __init__( self, @@ -512,7 +568,7 @@ class settings(metaclass=settingsMeta): 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 + deadline: int | float | datetime.timedelta | None = not_set, # type: ignore print_blob: bool = not_set, # type: ignore backend: str = not_set, # type: ignore ) -> None: @@ -574,7 +630,7 @@ class settings(metaclass=settingsMeta): ) self._deadline = ( self._fallback.deadline # type: ignore - if deadline is not_set + if deadline is not_set # type: ignore else _validate_deadline(deadline) ) self._print_blob = ( @@ -792,7 +848,18 @@ class settings(metaclass=settingsMeta): @property def suppress_health_check(self): """ - A list of |HealthCheck| items to disable. + Suppress the given |HealthCheck| exceptions. Those health checks will not + be raised by Hypothesis. To suppress all health checks, you can pass + ``suppress_health_check=list(HealthCheck)``. + + Health checks are proactive warnings, not correctness errors, so we + encourage suppressing health checks where you have evaluated they will + not pose a problem, or where you have evaluated that fixing the underlying + issue is not worthwhile. + + .. seealso:: + + See also the :doc:`/how-to/suppress-healthchecks` how-to. """ return self._suppress_health_check @@ -863,7 +930,7 @@ class settings(metaclass=settingsMeta): ) setattr(test, attr_name, True) _test.TestCase.settings = self - return test # type: ignore + return test else: raise InvalidArgument( "@settings(...) can only be used as a decorator on " @@ -959,6 +1026,20 @@ class settings(metaclass=settingsMeta): settings._current_profile = name default_variable.value = settings.get_profile(name) + @staticmethod + def get_current_profile_name() -> str: + """ + The name of the current settings profile. For example: + + .. code-block:: python + + >>> settings.load_profile("myprofile") + >>> settings.get_current_profile_name() + 'myprofile' + """ + assert settings._current_profile is not None + return settings._current_profile + @contextlib.contextmanager def local_settings(s: settings) -> Generator[settings, None, None]: diff --git a/contrib/python/hypothesis/py3/hypothesis/configuration.py b/contrib/python/hypothesis/py3/hypothesis/configuration.py index cd9115533af..205f7eb7e25 100644 --- a/contrib/python/hypothesis/py3/hypothesis/configuration.py +++ b/contrib/python/hypothesis/py3/hypothesis/configuration.py @@ -12,7 +12,6 @@ import os import sys import warnings from pathlib import Path -from typing import Union import _hypothesis_globals @@ -22,7 +21,7 @@ __hypothesis_home_directory_default = Path.cwd() / ".hypothesis" __hypothesis_home_directory = None -def set_hypothesis_home_dir(directory: Union[str, Path, None]) -> None: +def set_hypothesis_home_dir(directory: str | Path | None) -> None: global __hypothesis_home_directory __hypothesis_home_directory = None if directory is None else Path(directory) diff --git a/contrib/python/hypothesis/py3/hypothesis/control.py b/contrib/python/hypothesis/py3/hypothesis/control.py index 27679c662ed..ac8a3c48fb3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/control.py +++ b/contrib/python/hypothesis/py3/hypothesis/control.py @@ -12,9 +12,9 @@ import inspect import math import random from collections import defaultdict -from collections.abc import Sequence +from collections.abc import Callable, Sequence from contextlib import contextmanager -from typing import Any, Callable, NoReturn, Optional, Union +from typing import Any, NoReturn, Optional from weakref import WeakKeyDictionary from hypothesis import Verbosity, settings @@ -22,7 +22,7 @@ from hypothesis._settings import note_deprecation from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.internal.conjecture.data import ConjectureData -from hypothesis.internal.observability import TESTCASE_CALLBACKS +from hypothesis.internal.observability import observability_enabled from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type from hypothesis.reporting import report, verbose_report @@ -62,9 +62,9 @@ def assume(condition: object) -> bool: since="2023-09-25", has_codemod=False, ) - if TESTCASE_CALLBACKS or not condition: + if observability_enabled() or not condition: where = _calling_function_location("assume", inspect.currentframe()) - if TESTCASE_CALLBACKS and currently_in_test_context(): + if observability_enabled() and currently_in_test_context(): counts = current_build_context().data._observability_predicates[where] counts.update_count(condition=bool(condition)) if not condition: @@ -155,6 +155,7 @@ class BuildContext: self, obj: object, func: object, + *, args: Sequence[object], kwargs: dict[str, object], ) -> None: @@ -237,7 +238,7 @@ def note(value: object) -> None: report(value) -def event(value: str, payload: Union[str, int, float] = "") -> None: +def event(value: str, payload: str | int | float = "") -> None: """Record an event that occurred during this test. Statistics on the number of test runs with each event will be reported at the end if you run Hypothesis in statistics reporting mode. @@ -271,7 +272,7 @@ def _event_to_string(event, allowed_types=str): return result -def target(observation: Union[int, float], *, label: str = "") -> Union[int, float]: +def target(observation: int | float, *, label: str = "") -> int | float: """Calling this function with an ``int`` or ``float`` observation gives it feedback with which to guide our search for inputs that will cause an error, in addition to all the usual heuristics. Observations must always be finite. diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py index da5f5ea81d9..9315b89a8a4 100644 --- a/contrib/python/hypothesis/py3/hypothesis/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/core.py @@ -18,6 +18,7 @@ import io import math import os import sys +import threading import time import traceback import types @@ -25,18 +26,17 @@ import unittest import warnings import zlib from collections import defaultdict -from collections.abc import Coroutine, Generator, Hashable, Iterable, Sequence +from collections.abc import Callable, Coroutine, Generator, Hashable, Iterable, Sequence from dataclasses import dataclass, field from functools import partial from inspect import Parameter from random import Random +from threading import Lock +from types import EllipsisType from typing import ( Any, BinaryIO, - Callable, - Optional, TypeVar, - Union, overload, ) from unittest import TestCase @@ -73,7 +73,6 @@ from hypothesis.internal import observability from hypothesis.internal.compat import ( PYPY, BaseExceptionGroup, - EllipsisType, add_note, bad_django_TestCase, get_type_hints, @@ -101,11 +100,11 @@ from hypothesis.internal.escalation import ( ) from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import ( - TESTCASE_CALLBACKS, InfoObservation, InfoObservationType, deliver_observation, make_testcase, + observability_enabled, ) from hypothesis.internal.reflection import ( convert_positional_arguments, @@ -140,6 +139,7 @@ from hypothesis.strategies._internal.strategies import ( SearchStrategy, check_strategy, ) +from hypothesis.utils.conventions import not_set from hypothesis.utils.threading import ThreadLocal from hypothesis.vendor.pretty import RepresentationPrinter from hypothesis.version import __version__ @@ -243,9 +243,7 @@ class example: condition: bool = True, # noqa: FBT002 *, reason: str = "", - raises: Union[ - type[BaseException], tuple[type[BaseException], ...] - ] = BaseException, + raises: type[BaseException] | tuple[type[BaseException], ...] = BaseException, ) -> "example": """Mark this example as an expected failure, similarly to :obj:`pytest.mark.xfail(strict=True) <pytest.mark.xfail>`. @@ -554,7 +552,9 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s "example has too many arguments for test. Expected at most " f"{len(posargs)} but got {len(example.args)}" ) - example_kwargs = dict(zip(posargs[-len(example.args) :], example.args)) + example_kwargs = dict( + zip(posargs[-len(example.args) :], example.args, strict=True) + ) else: example_kwargs = dict(example.kwargs) given_kws = ", ".join( @@ -681,15 +681,16 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s ) empty_data.freeze() - tc = make_testcase( - run_start=state._start_timestamp, - property=state.test_identifier, - data=empty_data, - how_generated="explicit example", - representation=state._string_repr, - timing=state._timing_features, - ) - deliver_observation(tc) + if observability_enabled(): + tc = make_testcase( + run_start=state._start_timestamp, + property=state.test_identifier, + data=empty_data, + how_generated="explicit example", + representation=state._string_repr, + timing=state._timing_features, + ) + deliver_observation(tc) if fragments_reported: verbose_report(fragments_reported[0].replace("Falsifying", "Trying", 1)) @@ -703,16 +704,18 @@ def get_random_for_wrapped_test(test, wrapped_test): if wrapped_test._hypothesis_internal_use_seed is not None: return Random(wrapped_test._hypothesis_internal_use_seed) - elif settings.derandomize: + + if settings.derandomize: return Random(int_from_bytes(function_digest(test))) - elif global_force_seed is not None: + + if global_force_seed is not None: return Random(global_force_seed) - else: - 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) + + 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 @@ -843,25 +846,27 @@ def get_executor(runner): return default_executor -def unwrap_markers_from_group() -> Generator[None, None, None]: - # This function is a crude solution, a better way of resolving it would probably - # be to rewrite a bunch of exception handlers to use except*. - T = TypeVar("T", bound=BaseException) +# This function is a crude solution, a better way of resolving it would probably +# be to rewrite a bunch of exception handlers to use except*. +T = TypeVar("T", bound=BaseException) - def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]: - found_exceptions: list[T] = [] - for exc in excgroup.exceptions: - if isinstance(exc, BaseExceptionGroup): - found_exceptions.extend(_flatten_group(exc)) - else: - found_exceptions.append(exc) - return found_exceptions +def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]: + found_exceptions: list[T] = [] + for exc in excgroup.exceptions: + if isinstance(exc, BaseExceptionGroup): + found_exceptions.extend(_flatten_group(exc)) + else: + found_exceptions.append(exc) + return found_exceptions + + +def unwrap_markers_from_group() -> Generator[None, None, None]: try: yield except BaseExceptionGroup as excgroup: - frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen) + _frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen) # group only contains Frozen, reraise the group # it doesn't matter what we raise, since any exceptions get disregarded @@ -907,25 +912,25 @@ def unwrap_markers_from_group() -> Generator[None, None, None]: class StateForActualGivenExecution: - def __init__(self, stuff, test, settings, random, wrapped_test): - self.test_runner = get_executor(stuff.selfy) + def __init__( + self, stuff, test, settings, random, wrapped_test, *, thread_overlap=None + ): self.stuff = stuff + self.test = test self.settings = settings - self.last_exception = None - self.falsifying_examples = () self.random = random - self.ever_executed = False - - self.is_find = getattr(wrapped_test, "_hypothesis_internal_is_find", False) self.wrapped_test = wrapped_test - self.xfail_example_reprs = set() - - self.test = test + self.thread_overlap = {} if thread_overlap is None else thread_overlap + self.test_runner = get_executor(stuff.selfy) self.print_given_args = getattr( wrapped_test, "_hypothesis_internal_print_given_args", True ) + self.last_exception = None + self.falsifying_examples = () + self.ever_executed = False + self.xfail_example_reprs = set() self.files_to_propagate = set() self.failed_normally = False self.failed_due_to_deadline = False @@ -936,7 +941,7 @@ class StateForActualGivenExecution: self._timing_features = {} @property - def test_identifier(self): + def test_identifier(self) -> str: return getattr( current_pytest_item.value, "nodeid", None ) or get_pretty_function_description(self.wrapped_test) @@ -944,7 +949,9 @@ class StateForActualGivenExecution: def _should_trace(self): # NOTE: we explicitly support monkeypatching this. Keep the namespace # access intact. - _trace_obs = TESTCASE_CALLBACKS and observability.OBSERVABILITY_COLLECT_COVERAGE + _trace_obs = ( + observability_enabled() and observability.OBSERVABILITY_COLLECT_COVERAGE + ) _trace_failure = ( self.failed_normally and not self.failed_due_to_deadline @@ -973,20 +980,15 @@ class StateForActualGivenExecution: """ self.ever_executed = True - data.is_find = self.is_find self._string_repr = "" text_repr = None - if self.settings.deadline is None and not TESTCASE_CALLBACKS: + if self.settings.deadline is None and not observability_enabled(): @proxies(self.test) def 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) + with unwrap_markers_from_group(), ensure_free_stackframes(): + return self.test(*args, **kwargs) else: @@ -995,30 +997,31 @@ class StateForActualGivenExecution: arg_drawtime = math.fsum(data.draw_times.values()) arg_stateful = math.fsum(data._stateful_run_times.values()) arg_gctime = gc_cumulative_time() - start = time.perf_counter() - try: - 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 - in_stateful = ( - math.fsum(data._stateful_run_times.values()) - arg_stateful - ) - in_gctime = gc_cumulative_time() - arg_gctime - runtime = finish - start - in_drawtime - in_stateful - in_gctime - self._timing_features = { - "execute:test": runtime, - "overall:gc": in_gctime, - **data.draw_times, - **data._stateful_run_times, - } + with unwrap_markers_from_group(), ensure_free_stackframes(): + start = time.perf_counter() + try: + result = self.test(*args, **kwargs) + finally: + finish = time.perf_counter() + in_drawtime = math.fsum(data.draw_times.values()) - arg_drawtime + in_stateful = ( + math.fsum(data._stateful_run_times.values()) - arg_stateful + ) + in_gctime = gc_cumulative_time() - arg_gctime + runtime = finish - start - in_drawtime - in_stateful - in_gctime + self._timing_features = { + "execute:test": runtime, + "overall:gc": in_gctime, + **data.draw_times, + **data._stateful_run_times, + } - if (current_deadline := self.settings.deadline) is not None: + if ( + (current_deadline := self.settings.deadline) is not None + # we disable the deadline check under concurrent threads, since + # cpython may switch away from a thread for arbitrarily long. + and not self.thread_overlap.get(threading.get_ident(), False) + ): if not is_final: current_deadline = (current_deadline // 4) * 5 if runtime >= current_deadline.total_seconds(): @@ -1070,7 +1073,7 @@ class StateForActualGivenExecution: ) report(printer.getvalue()) - if TESTCASE_CALLBACKS: + if observability_enabled(): printer = RepresentationPrinter(context=context) printer.repr_call( test.__name__, @@ -1100,10 +1103,10 @@ class StateForActualGivenExecution: add_note(e, msg.format(format_arg)) raise finally: - if parts := getattr(data, "_stateful_repr_parts", None): - self._string_repr = "\n".join(parts) + if data._stateful_repr_parts is not None: + self._string_repr = "\n".join(data._stateful_repr_parts) - if TESTCASE_CALLBACKS: + if observability_enabled(): printer = RepresentationPrinter(context=context) for name, value in data._observability_args.items(): if name.startswith("generate:Draw "): @@ -1118,21 +1121,20 @@ class StateForActualGivenExecution: # 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. - # - # 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) + with ( + local_settings(self.settings), + deterministic_PRNG(), + 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. @@ -1283,7 +1285,7 @@ class StateForActualGivenExecution: if trace: # pragma: no cover # Trace collection is explicitly disabled under coverage. self.explain_traces[interesting_origin].add(frozenset(trace)) - if interesting_origin[0] == DeadlineExceeded: + if interesting_origin.exc_type == DeadlineExceeded: self.failed_due_to_deadline = True self.explain_traces.clear() try: @@ -1294,7 +1296,7 @@ class StateForActualGivenExecution: finally: # Conditional here so we can save some time constructing the payload; in # other cases (without coverage) it's cheap enough to do that regardless. - if TESTCASE_CALLBACKS: + if observability_enabled(): if runner := getattr(self, "_runner", None): phase = runner._current_phase else: # pragma: no cover # in case of messing with internals @@ -1310,11 +1312,19 @@ class StateForActualGivenExecution: data._observability_args = data.provider.realize( data._observability_args ) - self._string_repr = data.provider.realize(self._string_repr) except BackendCannotProceed: data._observability_args = {} + + try: + self._string_repr = data.provider.realize(self._string_repr) + except BackendCannotProceed: self._string_repr = "<backend failed to realize symbolic arguments>" + try: + data.events = data.provider.realize(data.events) + except BackendCannotProceed: + data.events = {} + data.freeze() tc = make_testcase( run_start=self._start_timestamp, @@ -1329,6 +1339,7 @@ class StateForActualGivenExecution: backend_metadata=data.provider.observe_test_case(), ) deliver_observation(tc) + for msg in data.provider.observe_information_messages( lifetime="test_case" ): @@ -1336,7 +1347,7 @@ class StateForActualGivenExecution: self._timing_features = {} def _deliver_information_message( - self, *, type: InfoObservationType, title: str, content: Union[str, dict] + self, *, type: InfoObservationType, title: str, content: str | dict ) -> None: deliver_observation( InfoObservation( @@ -1367,12 +1378,13 @@ class StateForActualGivenExecution: settings=self.settings, random=self.random, database_key=database_key, + thread_overlap=self.thread_overlap, ) # Use the Conjecture engine to run the test function many times # on different inputs. runner.run() note_statistics(runner.statistics) - if TESTCASE_CALLBACKS: + if observability_enabled(): self._deliver_information_message( type="info", title="Hypothesis Statistics", @@ -1424,7 +1436,7 @@ class StateForActualGivenExecution: # If we have not traced executions, warn about that now (but only when # we'd expect to do so reliably, i.e. on CPython>=3.12) if ( - sys.version_info[:2] >= (3, 12) + hasattr(sys, "monitoring") and not PYPY and self._should_trace() and not Tracer.can_trace() @@ -1469,7 +1481,7 @@ class StateForActualGivenExecution: with with_reporter(fragments.append): self.execute_once( ran_example, - print_example=not self.is_find, + print_example=True, is_final=True, expected_failure=( falsifying_example.expected_exception, @@ -1513,21 +1525,23 @@ class StateForActualGivenExecution: raise NotImplementedError("This should be unreachable") finally: ran_example.freeze() - # log our observability line for the final failing example - 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) + if observability_enabled(): + # log our observability line for the final failing example + 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: @@ -1580,8 +1594,11 @@ def _raise_to_user( add_note(the_error_hypothesis_found, line) 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) + add_note( + err, + f"backend={unsound_backend!r} claimed to verify this test passes - " + "please send them a bug report!", + ) raise the_error_hypothesis_found @@ -1628,7 +1645,7 @@ class HypothesisHandle: @property def fuzz_one_input( self, - ) -> Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]: + ) -> Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]: """Run the test as a fuzz target, driven with the `buffer` of bytes. Returns None if buffer invalid for the strategy, canonical pruned @@ -1649,7 +1666,7 @@ class HypothesisHandle: def given( _: EllipsisType, / ) -> Callable[ - [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[[], None] + [Callable[..., Coroutine[Any, Any, None] | None]], Callable[[], None] ]: # pragma: no cover ... @@ -1658,26 +1675,24 @@ def given( def given( *_given_arguments: SearchStrategy[Any], ) -> Callable[ - [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None] + [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None] ]: # pragma: no cover ... @overload def given( - **_given_kwargs: Union[SearchStrategy[Any], EllipsisType], + **_given_kwargs: SearchStrategy[Any] | EllipsisType, ) -> Callable[ - [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None] + [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None] ]: # pragma: no cover ... def given( - *_given_arguments: Union[SearchStrategy[Any], EllipsisType], - **_given_kwargs: Union[SearchStrategy[Any], EllipsisType], -) -> Callable[ - [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None] -]: + *_given_arguments: SearchStrategy[Any] | EllipsisType, + **_given_kwargs: SearchStrategy[Any] | EllipsisType, +) -> Callable[[Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]]: """ The |@given| decorator turns a function into a Hypothesis test. This is the main entry point to Hypothesis. @@ -1734,7 +1749,7 @@ def given( test(manual_string="x") The reason for this "from the right" behavior is to support using |@given| - with instance methods, by passing through ``self``: + with instance methods, by automatically passing through ``self``: .. code-block:: python @@ -1776,8 +1791,15 @@ def given( fail_health_check( Settings(), "Nesting @given tests results in quadratic generation and shrinking " - "behavior and can usually be more cleanly expressed by replacing the " - "inner function with an st.data() parameter on the outer @given.", + "behavior, and can usually be more cleanly expressed by replacing the " + "inner function with an st.data() parameter on the outer @given." + "\n\n" + "If it is difficult or impossible to refactor this test to remove the " + "nested @given, you can disable this health check with " + "@settings(suppress_health_check=[HealthCheck.nested_given]) on the " + "outer @given. See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", HealthCheck.nested_given, ) @@ -1831,7 +1853,9 @@ def given( for p in original_sig.parameters.values() if p.kind is p.POSITIONAL_OR_KEYWORD ] - given_kwargs = dict(list(zip(posargs[::-1], given_arguments[::-1]))[::-1]) + given_kwargs = dict( + list(zip(posargs[::-1], given_arguments[::-1], strict=False))[::-1] + ) # These have been converted, so delete them to prevent accidental use. del given_arguments @@ -1850,212 +1874,253 @@ def given( ) given_kwargs[name] = st.from_type(hints[name]) - prev_self = Unset = object() + # only raise if the same thread uses two different executors, not if two + # different threads use different executors. + thread_local = ThreadLocal(prev_self=lambda: not_set) + # maps thread_id to whether that thread overlaps in execution with any + # other thread in this @given. We use this to detect whether an @given is + # being run from multiple different threads at once, which informs + # decisions like whether to raise DeadlineExceeded or HealthCheck.too_slow. + thread_overlap: dict[int, bool] = {} + thread_overlap_lock = Lock() @impersonate(test) @define_function_signature(test.__name__, test.__doc__, new_signature) def wrapped_test(*arguments, **kwargs): # Tell pytest to omit the body of this function from tracebacks __tracebackhide__ = True + with thread_overlap_lock: + for overlap_thread_id in thread_overlap: + thread_overlap[overlap_thread_id] = True - test = wrapped_test.hypothesis.inner_test - - if getattr(test, "is_hypothesis_test", False): - raise InvalidArgument( - f"You have applied @given to the test {test.__name__} more than " - "once, which wraps the test several times and is extremely slow. " - "A similar effect can be gained by combining the arguments " - "of the two calls to given. For example, instead of " - "@given(booleans()) @given(integers()), you could write " - "@given(booleans(), integers())" - ) - - settings = wrapped_test._hypothesis_internal_use_settings + threadid = threading.get_ident() + # if there are existing threads when this thread starts, then + # this thread starts at an overlapped state. + has_existing_threads = len(thread_overlap) > 0 + thread_overlap[threadid] = has_existing_threads - random = get_random_for_wrapped_test(test, wrapped_test) - - arguments, kwargs, stuff = process_arguments_to_given( - wrapped_test, arguments, kwargs, given_kwargs, new_signature.parameters - ) - - if ( - inspect.iscoroutinefunction(test) - and get_executor(stuff.selfy) is default_executor - ): - # See https://github.com/HypothesisWorks/hypothesis/issues/3054 - # If our custom executor doesn't handle coroutines, or we return an - # awaitable from a non-async-def function, we just rely on the - # return_value health check. This catches most user errors though. - raise InvalidArgument( - "Hypothesis doesn't know how to run async test functions like " - f"{test.__name__}. You'll need to write a custom executor, " - "or use a library like pytest-asyncio or pytest-trio which can " - "handle the translation for you.\n See https://hypothesis." - "readthedocs.io/en/latest/details.html#custom-function-execution" - ) - - runner = stuff.selfy - if isinstance(stuff.selfy, TestCase) and test.__name__ in dir(TestCase): - msg = ( - f"You have applied @given to the method {test.__name__}, which is " - "used by the unittest runner but is not itself a test." - " This is not useful in any way." - ) - fail_health_check(settings, msg, HealthCheck.not_a_test_method) - if bad_django_TestCase(runner): # pragma: no cover - # Covered by the Django tests, but not the pytest coverage task - raise InvalidArgument( - "You have applied @given to a method on " - f"{type(runner).__qualname__}, but this " - "class does not inherit from the supported versions in " - "`hypothesis.extra.django`. Use the Hypothesis variants " - "to ensure that each example is run in a separate " - "database transaction." - ) + try: + test = wrapped_test.hypothesis.inner_test + if getattr(test, "is_hypothesis_test", False): + raise InvalidArgument( + f"You have applied @given to the test {test.__name__} more than " + "once, which wraps the test several times and is extremely slow. " + "A similar effect can be gained by combining the arguments " + "of the two calls to given. For example, instead of " + "@given(booleans()) @given(integers()), you could write " + "@given(booleans(), integers())" + ) - nonlocal prev_self - # Check selfy really is self (not e.g. a mock) before we health-check - cur_self = ( - stuff.selfy - if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test - else None - ) - if prev_self is Unset: - prev_self = cur_self - elif cur_self is not prev_self: - msg = ( - f"The method {test.__qualname__} was called from multiple " - "different executors. This may lead to flaky tests and " - "nonreproducible errors when replaying from database." + settings = wrapped_test._hypothesis_internal_use_settings + random = get_random_for_wrapped_test(test, wrapped_test) + arguments, kwargs, stuff = process_arguments_to_given( + wrapped_test, + arguments, + kwargs, + given_kwargs, + new_signature.parameters, ) - fail_health_check(settings, msg, HealthCheck.differing_executors) - - state = StateForActualGivenExecution( - stuff, test, settings, random, wrapped_test - ) - - reproduce_failure = wrapped_test._hypothesis_internal_use_reproduce_failure - # If there was a @reproduce_failure decorator, use it to reproduce - # the error (or complain that we couldn't). Either way, this will - # always raise some kind of error. - if reproduce_failure is not None: - expected_version, failure = reproduce_failure - if expected_version != __version__: + if ( + inspect.iscoroutinefunction(test) + and get_executor(stuff.selfy) is default_executor + ): + # See https://github.com/HypothesisWorks/hypothesis/issues/3054 + # If our custom executor doesn't handle coroutines, or we return an + # awaitable from a non-async-def function, we just rely on the + # return_value health check. This catches most user errors though. raise InvalidArgument( - "Attempting to reproduce a failure from a different " - 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." + "Hypothesis doesn't know how to run async test functions like " + f"{test.__name__}. You'll need to write a custom executor, " + "or use a library like pytest-asyncio or pytest-trio which can " + "handle the translation for you.\n See https://hypothesis." + "readthedocs.io/en/latest/details.html#custom-function-execution" ) - try: - state.execute_once( - ConjectureData.for_choices(decode_failure(failure)), - print_example=True, - is_final=True, + + runner = stuff.selfy + if isinstance(stuff.selfy, TestCase) and test.__name__ in dir(TestCase): + fail_health_check( + settings, + f"You have applied @given to the method {test.__name__}, which is " + "used by the unittest runner but is not itself a test. " + "This is not useful in any way.", + HealthCheck.not_a_test_method, ) - raise DidNotReproduce( - "Expected the test to raise an error, but it " - "completed successfully." + if bad_django_TestCase(runner): # pragma: no cover + # Covered by the Django tests, but not the pytest coverage task + raise InvalidArgument( + "You have applied @given to a method on " + f"{type(runner).__qualname__}, but this " + "class does not inherit from the supported versions in " + "`hypothesis.extra.django`. Use the Hypothesis variants " + "to ensure that each example is run in a separate " + "database transaction." ) - except StopTest: - raise DidNotReproduce( - "The shape of the test data has changed in some way " - "from where this blob was defined. Are you sure " - "you're running the same test?" - ) from None - except UnsatisfiedAssumption: - raise DidNotReproduce( - "The test data failed to satisfy an assumption in the " - "test. Have you added it since this blob was generated?" - ) from None - # There was no @reproduce_failure, so start by running any explicit - # examples from @example decorators. - errors = list( - execute_explicit_examples( - state, wrapped_test, arguments, kwargs, original_sig + nonlocal thread_local + # Check selfy really is self (not e.g. a mock) before we health-check + cur_self = ( + stuff.selfy + if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test + else None + ) + if thread_local.prev_self is not_set: + thread_local.prev_self = cur_self + elif cur_self is not thread_local.prev_self: + fail_health_check( + settings, + f"The method {test.__qualname__} was called from multiple " + "different executors. This may lead to flaky tests and " + "nonreproducible errors when replaying from database." + "\n\n" + "Unlike most health checks, HealthCheck.differing_executors " + "warns about a correctness issue with your test. We " + "therefore recommend fixing the underlying issue, rather " + "than suppressing this health check. However, if you are " + "confident this health check can be safely disabled, you can " + "do so with " + "@settings(suppress_health_check=[HealthCheck.differing_executors]). " + "See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", + HealthCheck.differing_executors, + ) + + state = StateForActualGivenExecution( + stuff, + test, + settings, + random, + wrapped_test, + thread_overlap=thread_overlap, ) - ) - if errors: - # If we're not going to report multiple bugs, we would have - # stopped running explicit examples at the first failure. - assert len(errors) == 1 or state.settings.report_multiple_bugs - # If an explicit example raised a 'skip' exception, ensure it's never - # wrapped up in an exception group. Because we break out of the loop - # immediately on finding a skip, if present it's always the last error. - if isinstance(errors[-1][1], skip_exceptions_to_reraise()): - # Covered by `test_issue_3453_regression`, just in a subprocess. - del errors[:-1] # pragma: no cover + # If there was a @reproduce_failure decorator, use it to reproduce + # the error (or complain that we couldn't). Either way, this will + # always raise some kind of error. + if ( + reproduce_failure := wrapped_test._hypothesis_internal_use_reproduce_failure + ) is not None: + expected_version, failure = reproduce_failure + if expected_version != __version__: + raise InvalidArgument( + "Attempting to reproduce a failure from a different " + 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." + ) + try: + state.execute_once( + ConjectureData.for_choices(decode_failure(failure)), + print_example=True, + is_final=True, + ) + raise DidNotReproduce( + "Expected the test to raise an error, but it " + "completed successfully." + ) + except StopTest: + raise DidNotReproduce( + "The shape of the test data has changed in some way " + "from where this blob was defined. Are you sure " + "you're running the same test?" + ) from None + except UnsatisfiedAssumption: + raise DidNotReproduce( + "The test data failed to satisfy an assumption in the " + "test. Have you added it since this blob was generated?" + ) from None - _raise_to_user(errors, state.settings, [], " in explicit examples") + # There was no @reproduce_failure, so start by running any explicit + # examples from @example decorators. + if errors := list( + execute_explicit_examples( + state, wrapped_test, arguments, kwargs, original_sig + ) + ): + # If we're not going to report multiple bugs, we would have + # stopped running explicit examples at the first failure. + assert len(errors) == 1 or state.settings.report_multiple_bugs - # If there were any explicit examples, they all ran successfully. - # The next step is to use the Conjecture engine to run the test on - # many different inputs. + # If an explicit example raised a 'skip' exception, ensure it's never + # wrapped up in an exception group. Because we break out of the loop + # immediately on finding a skip, if present it's always the last error. + if isinstance(errors[-1][1], skip_exceptions_to_reraise()): + # Covered by `test_issue_3453_regression`, just in a subprocess. + del errors[:-1] # pragma: no cover - ran_explicit_examples = Phase.explicit in state.settings.phases and getattr( - wrapped_test, "hypothesis_explicit_examples", () - ) - SKIP_BECAUSE_NO_EXAMPLES = unittest.SkipTest( - "Hypothesis has been told to run no examples for this test." - ) - if not ( - Phase.reuse in settings.phases or Phase.generate in settings.phases - ): - if not ran_explicit_examples: - raise SKIP_BECAUSE_NO_EXAMPLES - return + _raise_to_user(errors, state.settings, [], " in explicit examples") - try: - if isinstance(runner, TestCase) and hasattr(runner, "subTest"): - subTest = runner.subTest - try: - runner.subTest = types.MethodType(fake_subTest, runner) + # If there were any explicit examples, they all ran successfully. + # The next step is to use the Conjecture engine to run the test on + # many different inputs. + ran_explicit_examples = ( + Phase.explicit in state.settings.phases + and getattr(wrapped_test, "hypothesis_explicit_examples", ()) + ) + SKIP_BECAUSE_NO_EXAMPLES = unittest.SkipTest( + "Hypothesis has been told to run no examples for this test." + ) + if not ( + Phase.reuse in settings.phases or Phase.generate in settings.phases + ): + if not ran_explicit_examples: + raise SKIP_BECAUSE_NO_EXAMPLES + return + + try: + if isinstance(runner, TestCase) and hasattr(runner, "subTest"): + subTest = runner.subTest + try: + runner.subTest = types.MethodType(fake_subTest, runner) + state.run_engine() + finally: + runner.subTest = subTest + else: state.run_engine() - finally: - runner.subTest = subTest - else: - state.run_engine() - except BaseException as e: - # The exception caught here should either be an actual test - # failure (or BaseExceptionGroup), or some kind of fatal error - # that caused the engine to stop. - generated_seed = wrapped_test._hypothesis_internal_use_generated_seed - with local_settings(settings): - if not (state.failed_normally or generated_seed is None): - if running_under_pytest: - report( - f"You can add @seed({generated_seed}) to this test or " - f"run pytest with --hypothesis-seed={generated_seed} " - "to reproduce this failure." - ) - else: - report( - f"You can add @seed({generated_seed}) to this test to " - "reproduce this failure." - ) - # The dance here is to avoid showing users long tracebacks - # full of Hypothesis internals they don't care about. - # We have to do this inline, to avoid adding another - # internal stack frame just when we've removed the rest. - # - # Using a variable for our trimmed error ensures that the line - # which will actually appear in tracebacks is as clear as - # possible - "raise the_error_hypothesis_found". - the_error_hypothesis_found = e.with_traceback( - None - if isinstance(e, BaseExceptionGroup) - else get_trimmed_traceback() + except BaseException as e: + # The exception caught here should either be an actual test + # failure (or BaseExceptionGroup), or some kind of fatal error + # that caused the engine to stop. + generated_seed = ( + wrapped_test._hypothesis_internal_use_generated_seed ) - raise the_error_hypothesis_found + with local_settings(settings): + if not (state.failed_normally or generated_seed is None): + if running_under_pytest: + report( + f"You can add @seed({generated_seed}) to this test or " + f"run pytest with --hypothesis-seed={generated_seed} " + "to reproduce this failure." + ) + else: + report( + f"You can add @seed({generated_seed}) to this test to " + "reproduce this failure." + ) + # The dance here is to avoid showing users long tracebacks + # full of Hypothesis internals they don't care about. + # We have to do this inline, to avoid adding another + # internal stack frame just when we've removed the rest. + # + # Using a variable for our trimmed error ensures that the line + # which will actually appear in tracebacks is as clear as + # possible - "raise the_error_hypothesis_found". + the_error_hypothesis_found = e.with_traceback( + None + if isinstance(e, BaseExceptionGroup) + else get_trimmed_traceback() + ) + raise the_error_hypothesis_found - if not (ran_explicit_examples or state.ever_executed): - raise SKIP_BECAUSE_NO_EXAMPLES + if not (ran_explicit_examples or state.ever_executed): + raise SKIP_BECAUSE_NO_EXAMPLES + finally: + with thread_overlap_lock: + del thread_overlap[threadid] def _get_fuzz_target() -> ( - Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]] + Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None] ): # Because fuzzing interfaces are very performance-sensitive, we use a # somewhat more complicated structure here. `_get_fuzz_target()` is @@ -2077,7 +2142,12 @@ def given( assert not _args assert not _kwargs state = StateForActualGivenExecution( - stuff, test, settings, random, wrapped_test + stuff, + test, + settings, + random, + wrapped_test, + thread_overlap=thread_overlap, ) database_key = function_digest(test) + b".secondary" # We track the minimal-so-far example for each distinct origin, so @@ -2087,8 +2157,8 @@ def given( minimal_failures: dict = {} def fuzz_one_input( - buffer: Union[bytes, bytearray, memoryview, BinaryIO], - ) -> Optional[bytes]: + buffer: bytes | bytearray | memoryview | BinaryIO, + ) -> bytes | None: # This inner part is all that the fuzzer will actually run, # so we keep it as small and as fast as possible. if isinstance(buffer, io.IOBase): @@ -2120,7 +2190,7 @@ def given( status = Status.INTERESTING raise finally: - if TESTCASE_CALLBACKS: + if observability_enabled(): data.freeze() tc = make_testcase( run_start=state._start_timestamp, @@ -2172,9 +2242,9 @@ def find( specifier: SearchStrategy[Ex], condition: Callable[[Any], bool], *, - settings: Optional[Settings] = None, - random: Optional[Random] = None, - database_key: Optional[bytes] = None, + settings: Settings | None = None, + random: Random | None = None, + database_key: bytes | None = None, ) -> Ex: """Returns the minimal example from the given strategy ``specifier`` that matches the predicate function ``condition``.""" @@ -2209,11 +2279,7 @@ def find( if random is not None: test = seed(random.getrandbits(64))(test) - # Aliasing as Any avoids mypy errors (attr-defined) when accessing and - # setting custom attributes on the decorated function or class. - _test: Any = test - _test._hypothesis_internal_is_find = True - _test._hypothesis_internal_database_key = database_key + test._hypothesis_internal_database_key = database_key # type: ignore try: test() diff --git a/contrib/python/hypothesis/py3/hypothesis/database.py b/contrib/python/hypothesis/py3/hypothesis/database.py index 5772e6c200b..9106a2d2193 100644 --- a/contrib/python/hypothesis/py3/hypothesis/database.py +++ b/contrib/python/hypothesis/py3/hypothesis/database.py @@ -17,7 +17,7 @@ import sys import tempfile import warnings import weakref -from collections.abc import Iterable +from collections.abc import Callable, Iterable from datetime import datetime, timedelta, timezone from functools import lru_cache from hashlib import sha384 @@ -28,11 +28,9 @@ from threading import Thread from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, Literal, - Optional, - Union, + TypeAlias, cast, ) from urllib.error import HTTPError, URLError @@ -55,17 +53,15 @@ __all__ = [ ] if TYPE_CHECKING: - from typing import TypeAlias - from watchdog.observers.api import BaseObserver -StrPathT: "TypeAlias" = Union[str, PathLike[str]] -SaveDataT: "TypeAlias" = tuple[bytes, bytes] # key, value -DeleteDataT: "TypeAlias" = tuple[bytes, Optional[bytes]] # key, value -ListenerEventT: "TypeAlias" = Union[ - tuple[Literal["save"], SaveDataT], tuple[Literal["delete"], DeleteDataT] -] -ListenerT: "TypeAlias" = Callable[[ListenerEventT], Any] +StrPathT: TypeAlias = str | PathLike[str] +SaveDataT: TypeAlias = tuple[bytes, bytes] # key, value +DeleteDataT: TypeAlias = tuple[bytes, bytes | None] # key, value +ListenerEventT: TypeAlias = ( + tuple[Literal["save"], SaveDataT] | tuple[Literal["delete"], DeleteDataT] +) +ListenerT: TypeAlias = Callable[[ListenerEventT], Any] def _usable_dir(path: StrPathT) -> bool: @@ -85,7 +81,7 @@ def _usable_dir(path: StrPathT) -> bool: def _db_for_path( - path: Optional[Union[StrPathT, UniqueIdentifier, Literal[":memory:"]]] = None, + path: StrPathT | UniqueIdentifier | Literal[":memory:"] | None = None, ) -> "ExampleDatabase": if path is not_set: if os.getenv("HYPOTHESIS_DATABASE_FILE") is not None: # pragma: no cover @@ -138,9 +134,16 @@ class _EDMeta(abc.ABCMeta): # downstream ExampleDatabase subclasses too. if "sphinx" in sys.modules: try: - from sphinx.ext.autodoc import _METACLASS_CALL_BLACKLIST + import sphinx.ext.autodoc - _METACLASS_CALL_BLACKLIST.append("hypothesis.database._EDMeta.__call__") + signature = "hypothesis.database._EDMeta.__call__" + # _METACLASS_CALL_BLACKLIST is a frozenset in later sphinx versions + if isinstance(sphinx.ext.autodoc._METACLASS_CALL_BLACKLIST, frozenset): + sphinx.ext.autodoc._METACLASS_CALL_BLACKLIST = ( + sphinx.ext.autodoc._METACLASS_CALL_BLACKLIST | {signature} + ) + else: + sphinx.ext.autodoc._METACLASS_CALL_BLACKLIST.append(signature) except Exception: pass @@ -558,9 +561,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase): _broadcast_change = self._broadcast_change class Handler(FileSystemEventHandler): - def on_created( - _self, event: Union[FileCreatedEvent, DirCreatedEvent] - ) -> None: + def on_created(_self, event: FileCreatedEvent | DirCreatedEvent) -> None: # we only registered for the file creation event assert not isinstance(event, DirCreatedEvent) # watchdog events are only bytes if we passed a byte path to @@ -594,9 +595,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase): _broadcast_change(("save", (key, value))) - def on_deleted( - self, event: Union[FileDeletedEvent, DirDeletedEvent] - ) -> None: + def on_deleted(self, event: FileDeletedEvent | DirDeletedEvent) -> None: assert not isinstance(event, DirDeletedEvent) assert isinstance(event.src_path, str) @@ -607,7 +606,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase): _broadcast_change(("delete", (key, None))) - def on_moved(self, event: Union[FileMovedEvent, DirMovedEvent]) -> None: + def on_moved(self, event: FileMovedEvent | DirMovedEvent) -> None: assert not isinstance(event, DirMovedEvent) assert isinstance(event.src_path, str) assert isinstance(event.dest_path, str) @@ -835,7 +834,7 @@ class GitHubArtifactDatabase(ExampleDatabase): repo: str, artifact_name: str = "hypothesis-example-db", cache_timeout: timedelta = timedelta(days=1), - path: Optional[StrPathT] = None, + path: StrPathT | None = None, ): super().__init__() self.owner = owner @@ -845,7 +844,7 @@ class GitHubArtifactDatabase(ExampleDatabase): # Get the GitHub token from the environment # It's unnecessary to use a token if the repo is public - self.token: Optional[str] = getenv("GITHUB_TOKEN") + self.token: str | None = getenv("GITHUB_TOKEN") if path is None: self.path: Path = Path( @@ -860,9 +859,9 @@ class GitHubArtifactDatabase(ExampleDatabase): # This is the path to the artifact in usage # .hypothesis/github-artifacts/<artifact-name>/<modified_isoformat>.zip - self._artifact: Optional[Path] = None + self._artifact: Path | None = None # This caches the artifact structure - self._access_cache: Optional[dict[PurePath, set[PurePath]]] = None + self._access_cache: dict[PurePath, set[PurePath]] | None = None # Message to display if user doesn't wrap around ReadOnlyDatabase self._read_only_message = ( @@ -983,7 +982,7 @@ class GitHubArtifactDatabase(ExampleDatabase): self._prepare_for_io() - def _get_bytes(self, url: str) -> Optional[bytes]: # pragma: no cover + def _get_bytes(self, url: str) -> bytes | None: # pragma: no cover request = Request( url, headers={ @@ -993,7 +992,7 @@ class GitHubArtifactDatabase(ExampleDatabase): }, ) warning_message = None - response_bytes: Optional[bytes] = None + response_bytes: bytes | None = None try: with urlopen(request) as response: response_bytes = response.read() @@ -1009,6 +1008,8 @@ class GitHubArtifactDatabase(ExampleDatabase): "This could be because because the repository " "or artifact does not exist. " ) + # see https://github.com/python/cpython/issues/128734 + e.close() except URLError: warning_message = "Could not connect to GitHub to get the latest artifact. " except TimeoutError: @@ -1023,7 +1024,7 @@ class GitHubArtifactDatabase(ExampleDatabase): return response_bytes - def _fetch_artifact(self) -> Optional[Path]: # pragma: no cover + def _fetch_artifact(self) -> Path | None: # pragma: no cover # Get the list of artifacts from GitHub url = f"https://api.github.com/repos/{self.owner}/{self.repo}/actions/artifacts" response_bytes = self._get_bytes(url) @@ -1110,7 +1111,7 @@ class BackgroundWriteDatabase(ExampleDatabase): super().__init__() self._db = db self._queue: Queue[tuple[str, tuple[bytes, ...]]] = Queue() - self._thread: Optional[Thread] = None + self._thread: Thread | None = None def _ensure_thread(self): if self._thread is None: @@ -1132,7 +1133,7 @@ class BackgroundWriteDatabase(ExampleDatabase): getattr(self._db, method)(*args) self._queue.task_done() - def _join(self, timeout: Optional[float] = None) -> None: + def _join(self, timeout: float | None = None) -> None: # copy of Queue.join with a timeout. https://bugs.python.org/issue9634 with self._queue.all_tasks_done: while self._queue.unfinished_tasks: @@ -1271,7 +1272,7 @@ def _choices_from_bytes(buffer: bytes, /) -> tuple[ChoiceT, ...]: return tuple(parts) -def choices_from_bytes(buffer: bytes, /) -> Optional[tuple[ChoiceT, ...]]: +def choices_from_bytes(buffer: bytes, /) -> tuple[ChoiceT, ...] | None: """ Deserialize a bytestring to a tuple of choices. Inverts choices_to_bytes. diff --git a/contrib/python/hypothesis/py3/hypothesis/errors.py b/contrib/python/hypothesis/py3/hypothesis/errors.py index 4c64ca001f6..aed44680d8a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/errors.py +++ b/contrib/python/hypothesis/py3/hypothesis/errors.py @@ -9,7 +9,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. from datetime import timedelta -from typing import Any, Literal, Optional +from typing import Any, Literal from hypothesis.internal.compat import ExceptionGroup @@ -28,7 +28,7 @@ class UnsatisfiedAssumption(HypothesisException): If you're seeing this error something has gone wrong. """ - def __init__(self, reason: Optional[str] = None) -> None: + def __init__(self, reason: str | None = None) -> None: self.reason = reason @@ -61,8 +61,14 @@ class ChoiceTooLarge(HypothesisException): class Flaky(_Trimmable): - """Base class for indeterministic failures. Usually one of the more - specific subclasses (FlakyFailure or FlakyStrategyDefinition) is raised.""" + """ + Base class for indeterministic failures. Usually one of the more + specific subclasses (|FlakyFailure| or |FlakyStrategyDefinition|) is raised. + + .. seealso:: + + See also the :doc:`flaky failures tutorial </tutorial/flaky>`. + """ class FlakyReplay(Flaky): @@ -80,12 +86,17 @@ class FlakyReplay(Flaky): class FlakyStrategyDefinition(Flaky): - """This function appears to cause inconsistent data generation. + """ + This function appears to cause inconsistent data generation. Common causes for this problem are: 1. The strategy depends on external state. e.g. it uses an external random number generator. Try to make a version that passes all the relevant state in from Hypothesis. + + .. seealso:: + + See also the :doc:`flaky failures tutorial </tutorial/flaky>`. """ @@ -94,7 +105,8 @@ class _WrappedBaseException(Exception): class FlakyFailure(ExceptionGroup, Flaky): - """This function appears to fail non-deterministically: We have seen it + """ + This function appears to fail non-deterministically: We have seen it fail when passed this example at least once, but a subsequent invocation did not fail, or caused a distinct error. @@ -107,6 +119,10 @@ class FlakyFailure(ExceptionGroup, Flaky): 3. The function is timing sensitive and can fail or pass depending on how long it takes. Try breaking it up into smaller functions which don't do that and testing those instead. + + .. seealso:: + + See also the :doc:`flaky failures tutorial </tutorial/flaky>`. """ def __new__(cls, msg, group): @@ -131,7 +147,7 @@ class FlakyFailure(ExceptionGroup, Flaky): class FlakyBackendFailure(FlakyFailure): """ - A failure was reported by an :ref:`alternative backend <alternative-backends>`, + A failure was reported by an |alternative backend|, but this failure did not reproduce when replayed under the Hypothesis backend. When an alternative backend reports a failure, Hypothesis first replays it @@ -168,7 +184,7 @@ class HypothesisWarning(HypothesisException, Warning): class FailedHealthCheck(_Trimmable): - """Raised when a test fails a healthcheck.""" + """Raised when a test fails a health check. See |HealthCheck|.""" class NonInteractiveExampleWarning(HypothesisWarning): @@ -183,7 +199,7 @@ class HypothesisDeprecationWarning(HypothesisWarning, FutureWarning): Actually inherits from FutureWarning, because DeprecationWarning is hidden by the default warnings filter. - You can configure the Python :mod:`python:warnings` to handle these + You can configure the :mod:`python:warnings` module to handle these warnings differently to others, either turning them into errors or suppressing them entirely. Obviously we would prefer the former! """ @@ -227,7 +243,9 @@ class DeadlineExceeded(_Trimmable): def __init__(self, runtime: timedelta, deadline: timedelta) -> None: super().__init__( f"Test took {runtime.total_seconds() * 1000:.2f}ms, which exceeds " - f"the deadline of {deadline.total_seconds() * 1000:.2f}ms" + f"the deadline of {deadline.total_seconds() * 1000:.2f}ms. If you " + "expect test cases to take this long, you can use @settings(deadline=...) " + "to either set a higher deadline, or to disable it with deadline=None." ) self.runtime = runtime self.deadline = deadline diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py index 0ff33b1c815..67d22fa25a6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py @@ -9,11 +9,11 @@ # obtain one at https://mozilla.org/MPL/2.0/. import re -from typing import NamedTuple, Optional, Union +from types import EllipsisType +from typing import NamedTuple 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 @@ -38,7 +38,7 @@ __all__ = [ Shape = tuple[int, ...] -BasicIndex = tuple[Union[int, slice, None, EllipsisType], ...] +BasicIndex = tuple[int | slice | None | EllipsisType, ...] class BroadcastableShapes(NamedTuple): @@ -81,9 +81,9 @@ def check_valid_dims(dims, name): def array_shapes( *, min_dims: int = 1, - max_dims: Optional[int] = None, + max_dims: int | None = None, min_side: int = 1, - max_side: Optional[int] = None, + max_side: int | None = None, ) -> st.SearchStrategy[Shape]: """Return a strategy for array shapes (tuples of int >= 1). @@ -120,7 +120,7 @@ def valid_tuple_axes( ndim: int, *, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> st.SearchStrategy[tuple[int, ...]]: """All tuples will have a length >= ``min_size`` and <= ``max_size``. The default value for ``max_size`` is ``ndim``. @@ -165,9 +165,9 @@ def broadcastable_shapes( shape: Shape, *, min_dims: int = 0, - max_dims: Optional[int] = None, + max_dims: int | None = None, min_side: int = 1, - max_side: Optional[int] = None, + max_side: int | None = None, ) -> st.SearchStrategy[Shape]: """Return a strategy for shapes that are broadcast-compatible with the provided shape. @@ -237,11 +237,11 @@ def broadcastable_shapes( if not strict_check: # reduce max_dims to exclude unsatisfiable dimensions - for n, s in zip(range(max_dims), shape[::-1]): + for n, s in zip(range(max_dims), shape[::-1], strict=False): if s < min_side and s != 1: max_dims = n break - elif not (min_side <= 1 <= max_side or s <= max_side): + if not (min_side <= 1 <= max_side or s <= max_side): max_dims = n break @@ -344,13 +344,13 @@ def _hypothesis_parse_gufunc_signature(signature): @defines_strategy() def mutually_broadcastable_shapes( *, - num_shapes: Union[UniqueIdentifier, int] = not_set, - signature: Union[UniqueIdentifier, str] = not_set, + num_shapes: UniqueIdentifier | int = not_set, + signature: UniqueIdentifier | str = not_set, base_shape: Shape = (), min_dims: int = 0, - max_dims: Optional[int] = None, + max_dims: int | None = None, min_side: int = 1, - max_side: Optional[int] = None, + max_side: int | None = None, ) -> st.SearchStrategy[BroadcastableShapes]: """Return a strategy for a specified number of shapes N that are mutually-broadcastable with one another and with the provided base shape. @@ -462,11 +462,11 @@ def mutually_broadcastable_shapes( if not strict_check: # reduce max_dims to exclude unsatisfiable dimensions - for n, s in zip(range(max_dims), base_shape[::-1]): + for n, s in zip(range(max_dims), base_shape[::-1], strict=False): if s < min_side and s != 1: max_dims = n break - elif not (min_side <= 1 <= max_side or s <= max_side): + if not (min_side <= 1 <= max_side or s <= max_side): max_dims = n break @@ -525,7 +525,9 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy): return tuple(x for x in (loop + core)[-NDIM_MAX:] if x is not None) return BroadcastableShapes( - input_shapes=tuple(add_shape(l_in, c) for l_in, c in zip(loop_in, core_in)), + input_shapes=tuple( + add_shape(l_in, c) for l_in, c in zip(loop_in, core_in, strict=True) + ), result_shape=add_shape(loop_res, core_res), ) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py index 4027d90df42..3dba3f7b5c2 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/_patching.py @@ -30,7 +30,7 @@ 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 +from typing import Any import libcst as cst from libcst import matchers as m @@ -110,7 +110,7 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand): def __call_node_to_example_dec( self, node: cst.Call, via: str - ) -> Optional[cst.Decorator]: + ) -> cst.Decorator | None: # If we have black installed, remove trailing comma, _unless_ there's a comment node = node.with_changes( func=self.decorator_func, @@ -137,7 +137,7 @@ class AddExamplesCodemod(VisitorBasedCodemodCommand): try: pretty = black.format_str( cst.Module([]).code_for_node(via), - mode=black.FileMode(line_length=self.line_length), + mode=black.Mode(line_length=self.line_length), ) except (ImportError, AttributeError): # pragma: no cover return None # See https://github.com/psf/black/pull/4224 @@ -164,7 +164,7 @@ def get_patch_for( examples: Sequence[tuple[str, str]], *, strip_via: tuple[str, ...] = (), -) -> Optional[tuple[str, str, str]]: +) -> tuple[str, str, str] | None: # Skip this if we're unable to find the location of this function. try: module = sys.modules[func.__module__] @@ -195,7 +195,7 @@ def _get_patch_for( *, strip_via: tuple[str, ...] = (), namespace: dict[str, Any], -) -> Optional[tuple[str, str]]: +) -> tuple[str, str] | None: try: before = inspect.getsource(func) except Exception: # pragma: no cover @@ -209,7 +209,14 @@ def _get_patch_for( # 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: list[tuple[cst.Call, str]] = [] - for ex, via in set(examples): + + # we want to preserve order, but remove duplicates. + seen_examples = set() + for ex, via in examples: + if (ex, via) in seen_examples: + continue + seen_examples.add((ex, via)) + with suppress(Exception): node: Any = cst.parse_module(ex) the_call = node.body[0].body[0].value @@ -288,7 +295,7 @@ def make_patch( triples: Sequence[tuple[str, str, str]], *, msg: str = "Hypothesis: add explicit examples", - when: Optional[datetime] = None, + when: datetime | None = None, author: str = f"Hypothesis {__version__} <[email protected]>", ) -> str: """Create a patch for (fname, before, after) triples.""" diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py index 30ca2b46527..bba7974ccf0 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py @@ -14,13 +14,11 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from numbers import Real from types import SimpleNamespace from typing import ( - TYPE_CHECKING, Any, Literal, NamedTuple, - Optional, + TypeAlias, TypeVar, - Union, get_args, ) from warnings import warn @@ -55,9 +53,6 @@ from hypothesis.internal.validation import ( from hypothesis.strategies._internal.strategies import check_strategy from hypothesis.strategies._internal.utils import defines_strategy -if TYPE_CHECKING: - from typing import TypeAlias - __all__ = [ "make_strategies_namespace", ] @@ -118,7 +113,7 @@ def warn_on_missing_dtypes(xp: Any, stubs: list[str]) -> None: def find_castable_builtin_for_dtype( xp: Any, api_version: NominalVersion, dtype: DataType -) -> type[Union[bool, int, float, complex]]: +) -> type[bool | int | float | complex]: """Returns builtin type which can have values that are castable to the given dtype, according to :xp-ref:`type promotion rules <type_promotion.html>`. @@ -180,16 +175,16 @@ def dtype_from_name(xp: Any, name: str) -> Any: def _from_dtype( xp: Any, api_version: NominalVersion, - dtype: Union[DataType, str], + dtype: DataType | str, *, - min_value: Optional[Union[int, float]] = None, - max_value: Optional[Union[int, float]] = None, - allow_nan: Optional[bool] = None, - allow_infinity: Optional[bool] = None, - allow_subnormal: Optional[bool] = None, - exclude_min: Optional[bool] = None, - exclude_max: Optional[bool] = None, -) -> st.SearchStrategy[Union[bool, int, float, complex]]: + min_value: int | float | None = None, + max_value: int | float | None = None, + allow_nan: bool | None = None, + allow_infinity: bool | None = None, + allow_subnormal: bool | None = None, + exclude_min: bool | None = None, + exclude_max: bool | None = None, +) -> st.SearchStrategy[bool | int | float | complex]: """Return a strategy for any value of the given dtype. Values generated are of the Python scalar which is @@ -426,8 +421,8 @@ class ArrayStrategy(st.SearchStrategy): if val in seen: elements.reject("chose an element we've already used") continue - else: - seen.add(val) + seen.add(val) + result_obj[i] = val assigned.add(i) fill_mask[i] = False @@ -456,11 +451,11 @@ class ArrayStrategy(st.SearchStrategy): def _arrays( xp: Any, api_version: NominalVersion, - dtype: Union[DataType, str, st.SearchStrategy[DataType], st.SearchStrategy[str]], - shape: Union[int, Shape, st.SearchStrategy[Shape]], + dtype: DataType | str | st.SearchStrategy[DataType] | st.SearchStrategy[str], + shape: int | Shape | st.SearchStrategy[Shape], *, - elements: Optional[Union[Mapping[str, Any], st.SearchStrategy]] = None, - fill: Optional[st.SearchStrategy[Any]] = None, + elements: Mapping[str, Any] | st.SearchStrategy | None = None, + fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> st.SearchStrategy: """Returns a strategy for :xp-ref:`arrays <array_object.html>`. @@ -650,13 +645,13 @@ def numeric_dtype_names(base_name: str, sizes: Sequence[int]) -> Iterator[str]: yield f"{base_name}{size}" -IntSize: "TypeAlias" = Literal[8, 16, 32, 64] -FltSize: "TypeAlias" = Literal[32, 64] -CpxSize: "TypeAlias" = Literal[64, 128] +IntSize: TypeAlias = Literal[8, 16, 32, 64] +FltSize: TypeAlias = Literal[32, 64] +CpxSize: TypeAlias = Literal[64, 128] def _integer_dtypes( - xp: Any, *, sizes: Union[IntSize, Sequence[IntSize]] = (8, 16, 32, 64) + xp: Any, *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for signed integer dtype objects. @@ -674,7 +669,7 @@ def _integer_dtypes( def _unsigned_integer_dtypes( - xp: Any, *, sizes: Union[IntSize, Sequence[IntSize]] = (8, 16, 32, 64) + xp: Any, *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for unsigned integer dtype objects. @@ -694,7 +689,7 @@ def _unsigned_integer_dtypes( def _floating_dtypes( - xp: Any, *, sizes: Union[FltSize, Sequence[FltSize]] = (32, 64) + xp: Any, *, sizes: FltSize | Sequence[FltSize] = (32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for real-valued floating-point dtype objects. @@ -712,7 +707,7 @@ def _floating_dtypes( def _complex_dtypes( - xp: Any, *, sizes: Union[CpxSize, Sequence[CpxSize]] = (64, 128) + xp: Any, *, sizes: CpxSize | Sequence[CpxSize] = (64, 128) ) -> st.SearchStrategy[DataType]: """Return a strategy for complex dtype objects. @@ -749,9 +744,9 @@ def mutually_broadcastable_shapes( *, base_shape: Shape = (), min_dims: int = 0, - max_dims: Optional[int] = None, + max_dims: int | None = None, min_side: int = 1, - max_side: Optional[int] = None, + max_side: int | None = None, ) -> st.SearchStrategy[BroadcastableShapes]: return _mutually_broadcastable_shapes( num_shapes=num_shapes, @@ -771,7 +766,7 @@ def indices( shape: Shape, *, min_dims: int = 0, - max_dims: Optional[int] = None, + max_dims: int | None = None, allow_newaxis: bool = False, allow_ellipsis: bool = True, ) -> st.SearchStrategy[BasicIndex]: @@ -845,7 +840,7 @@ _args_to_xps: WeakValueDictionary = WeakValueDictionary() def make_strategies_namespace( - xp: Any, *, api_version: Optional[NominalVersion] = None + xp: Any, *, api_version: NominalVersion | None = None ) -> SimpleNamespace: """Creates a strategies namespace for the given array module. @@ -923,16 +918,16 @@ def make_strategies_namespace( @defines_strategy(force_reusable_values=True) def from_dtype( - dtype: Union[DataType, str], + dtype: DataType | str, *, - min_value: Optional[Union[int, float]] = None, - max_value: Optional[Union[int, float]] = None, - allow_nan: Optional[bool] = None, - allow_infinity: Optional[bool] = None, - allow_subnormal: Optional[bool] = None, - exclude_min: Optional[bool] = None, - exclude_max: Optional[bool] = None, - ) -> st.SearchStrategy[Union[bool, int, float, complex]]: + min_value: int | float | None = None, + max_value: int | float | None = None, + allow_nan: bool | None = None, + allow_infinity: bool | None = None, + allow_subnormal: bool | None = None, + exclude_min: bool | None = None, + exclude_max: bool | None = None, + ) -> st.SearchStrategy[bool | int | float | complex]: return _from_dtype( xp, api_version, @@ -948,13 +943,11 @@ def make_strategies_namespace( @defines_strategy(force_reusable_values=True) def arrays( - dtype: Union[ - DataType, str, st.SearchStrategy[DataType], st.SearchStrategy[str] - ], - shape: Union[int, Shape, st.SearchStrategy[Shape]], + dtype: DataType | str | st.SearchStrategy[DataType] | st.SearchStrategy[str], + shape: int | Shape | st.SearchStrategy[Shape], *, - elements: Optional[Union[Mapping[str, Any], st.SearchStrategy]] = None, - fill: Optional[st.SearchStrategy[Any]] = None, + elements: Mapping[str, Any] | st.SearchStrategy | None = None, + fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> st.SearchStrategy: return _arrays( @@ -985,19 +978,19 @@ def make_strategies_namespace( @defines_strategy() def integer_dtypes( - *, sizes: Union[IntSize, Sequence[IntSize]] = (8, 16, 32, 64) + *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: return _integer_dtypes(xp, sizes=sizes) @defines_strategy() def unsigned_integer_dtypes( - *, sizes: Union[IntSize, Sequence[IntSize]] = (8, 16, 32, 64) + *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: return _unsigned_integer_dtypes(xp, sizes=sizes) @defines_strategy() def floating_dtypes( - *, sizes: Union[FltSize, Sequence[FltSize]] = (32, 64) + *, sizes: FltSize | Sequence[FltSize] = (32, 64) ) -> st.SearchStrategy[DataType]: return _floating_dtypes(xp, sizes=sizes) @@ -1058,7 +1051,7 @@ def make_strategies_namespace( @defines_strategy() def complex_dtypes( - *, sizes: Union[CpxSize, Sequence[CpxSize]] = (64, 128) + *, sizes: CpxSize | Sequence[CpxSize] = (64, 128) ) -> st.SearchStrategy[DataType]: return _complex_dtypes(xp, sizes=sizes) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py b/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py index f39ad34d640..d2e6711e078 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py @@ -217,7 +217,7 @@ class HypothesisFixPositionalKeywonlyArgs(VisitorBasedCodemodCommand): if arg.keyword or arg.star or p.kind is not Parameter.KEYWORD_ONLY else arg.with_changes(keyword=cst.Name(p.name), equal=assign_nospace) ) - for p, arg in zip(params, updated_node.args) + for p, arg in zip(params, updated_node.args, strict=False) ] return updated_node.with_changes(args=newargs) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py index 357a0e6a576..960b52be75f 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/_fields.py @@ -10,14 +10,16 @@ import re import string +from collections.abc import Callable from datetime import datetime, timedelta from decimal import Decimal from functools import lru_cache -from typing import Any, Callable, TypeVar, Union +from typing import Any, TypeAlias, TypeVar, Union import django from django import forms as df from django.conf import settings +from django.core.files.base import ContentFile from django.core.validators import ( validate_ipv4_address, validate_ipv6_address, @@ -31,7 +33,9 @@ from hypothesis.internal.validation import check_type from hypothesis.provisional import urls from hypothesis.strategies import emails -AnyField = Union[dm.Field, df.Field] +# Use old-style union to avoid hitting +# https://github.com/sphinx-doc/sphinx/issues/11211 +AnyField: TypeAlias = Union[dm.Field, df.Field] # noqa: UP007 F = TypeVar("F", bound=AnyField) @@ -70,7 +74,7 @@ def timezones(): # Mapping of field types, to strategy objects or functions of (type) -> strategy _FieldLookUpType = dict[ type[AnyField], - Union[st.SearchStrategy, Callable[[Any], st.SearchStrategy]], + st.SearchStrategy | Callable[[Any], st.SearchStrategy], ] _global_field_lookup: _FieldLookUpType = { dm.SmallIntegerField: integers_for_field(-32768, 32767), @@ -95,6 +99,9 @@ _global_field_lookup: _FieldLookUpType = { df.NullBooleanField: st.one_of(st.none(), st.booleans()), df.URLField: urls(), df.UUIDField: st.uuids(), + df.FileField: st.builds( + ContentFile, st.binary(min_size=1), name=st.text(min_size=1, max_size=100) + ), } _ipv6_strings = st.one_of( @@ -346,7 +353,7 @@ def register_field_strategy( _global_field_lookup[field_type] = strategy -def from_field(field: F) -> st.SearchStrategy[Union[F, None]]: +def from_field(field: F) -> st.SearchStrategy[F | None]: """Return a strategy for values that fit the given field. This function is used by :func:`~hypothesis.extra.django.from_form` and diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py b/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py index a3be6fccb1d..7023f8ab2a3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/django/_impl.py @@ -10,7 +10,8 @@ import unittest from functools import partial -from typing import Optional, TypeVar, Union +from types import EllipsisType +from typing import Any, TypeVar from django import forms as df, test as dt from django.contrib.staticfiles import testing as dst @@ -20,7 +21,6 @@ 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 ModelT = TypeVar("ModelT", bound=dm.Model) @@ -63,7 +63,7 @@ class StaticLiveServerTestCase(HypothesisTestCase, dst.StaticLiveServerTestCase) @defines_strategy() def from_model( - model: type[ModelT], /, **field_strategies: Union[st.SearchStrategy, EllipsisType] + model: type[ModelT], /, **field_strategies: st.SearchStrategy | EllipsisType ) -> st.SearchStrategy[ModelT]: """Return a strategy for examples of ``model``. @@ -134,8 +134,8 @@ def _models_impl(draw, strat): @defines_strategy() def from_form( form: type[df.Form], - form_kwargs: Optional[dict] = None, - **field_strategies: Union[st.SearchStrategy, EllipsisType], + form_kwargs: dict | None = None, + **field_strategies: st.SearchStrategy | EllipsisType, ) -> st.SearchStrategy[df.Form]: """Return a strategy for examples of ``form``. @@ -162,7 +162,6 @@ def from_form( # currently unsupported: # ComboField # FilePathField - # FileField # ImageField form_kwargs = form_kwargs or {} if not issubclass(form, df.BaseForm): @@ -191,6 +190,7 @@ def from_form( fields_by_name[f"{name}_{i}"] = _field else: fields_by_name[name] = field + for name, value in sorted(field_strategies.items()): if value is ...: field_strategies[name] = from_field(fields_by_name[name]) @@ -199,10 +199,24 @@ def from_form( if name not in field_strategies and not field.disabled: field_strategies[name] = from_field(field) + # files are handled a bit specially in forms. A Form accepts two arguments: + # `data` and `files`. The former is for normal fields, and the latter is for + # file fields. + # see https://docs.djangoproject.com/en/5.1/ref/forms/api/#binding-uploaded-files. + data_strategies: dict[str, Any] = {} + file_strategies: dict[str, Any] = {} + for name, field in field_strategies.items(): + form_field = fields_by_name[name] + dictionary = ( + file_strategies if isinstance(form_field, df.FileField) else data_strategies + ) + dictionary[name] = field + return _forms_impl( st.builds( partial(form, **form_kwargs), # type: ignore - data=st.fixed_dictionaries(field_strategies), # type: ignore + data=st.fixed_dictionaries(data_strategies), + files=st.fixed_dictionaries(file_strategies), ) ) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py index c2046561d9c..e9aeb8cf693 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py @@ -82,20 +82,17 @@ import sys import types import warnings from collections import OrderedDict, defaultdict -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping from itertools import permutations, zip_longest from keyword import iskeyword as _iskeyword from string import ascii_lowercase from textwrap import dedent, indent +from types import EllipsisType from typing import ( Any, - Callable, - DefaultDict, ForwardRef, NamedTuple, - Optional, TypeVar, - Union, get_args, get_origin, ) @@ -104,7 +101,7 @@ import black from hypothesis import Verbosity, find, settings, strategies as st from hypothesis.errors import InvalidArgument, SmallSearchSpaceWarning -from hypothesis.internal.compat import EllipsisType, get_type_hints +from hypothesis.internal.compat import get_type_hints from hypothesis.internal.reflection import get_signature, is_mock from hypothesis.internal.validation import check_type from hypothesis.provisional import domains @@ -141,8 +138,8 @@ except {exceptions}: reject() """.strip() -Except = Union[type[Exception], tuple[type[Exception], ...]] -ImportSet = set[Union[str, tuple[str, str]]] +Except = type[Exception] | tuple[type[Exception], ...] +ImportSet = set[str | tuple[str, str]] _quietly_settings = settings( database=None, deadline=None, @@ -218,7 +215,7 @@ def _exceptions_from_docstring(doc: str) -> tuple[type[Exception], ...]: return tuple(_dedupe_exceptions(tuple(raises))) -def _type_from_doc_fragment(token: str) -> Optional[type]: +def _type_from_doc_fragment(token: str) -> type | None: # Special cases for "integer" and for numpy array-like and dtype if token == "integer": return int @@ -267,10 +264,7 @@ def _strategy_for(param: inspect.Parameter, docstring: str) -> st.SearchStrategy if match is None: continue doc_type = match.group(1) - if doc_type.endswith(", optional"): - # Convention to describe "argument may be omitted" - doc_type = doc_type[: -len(", optional")] - doc_type = doc_type.strip("}{") + doc_type = doc_type.removesuffix(", optional").strip("}{") elements = [] types = [] for token in re.split(r",? +or +| *, *", doc_type): @@ -822,8 +816,8 @@ def _make_test_body( except_: tuple[type[Exception], ...], assertions: str = "", style: str, - given_strategies: Optional[Mapping[str, Union[str, st.SearchStrategy]]] = None, - imports: Optional[ImportSet] = None, + given_strategies: Mapping[str, str | st.SearchStrategy] | None = None, + imports: ImportSet | None = None, annotate: bool, ) -> tuple[ImportSet, str]: # A set of modules to import - we might add to this later. The import code @@ -886,7 +880,7 @@ def _make_test_body( def _annotate_args( argnames: Iterable[str], funcs: Iterable[Callable], imports: ImportSet ) -> Iterable[str]: - arg_parameters: DefaultDict[str, set[Any]] = defaultdict(set) + arg_parameters: defaultdict[str, set[Any]] = defaultdict(set) for func in funcs: try: params = tuple(get_signature(func, eval_str=True).parameters.values()) @@ -913,8 +907,8 @@ class _AnnotationData(NamedTuple): def _parameters_to_annotation_name( - parameters: Optional[Iterable[Any]], imports: ImportSet -) -> Optional[str]: + parameters: Iterable[Any] | None, imports: ImportSet +) -> str | None: if parameters is None: return None annotations = tuple( @@ -936,9 +930,9 @@ def _parameters_to_annotation_name( def _join_generics( - origin_type_data: Optional[tuple[str, set[str]]], - annotations: Iterable[Optional[_AnnotationData]], -) -> Optional[_AnnotationData]: + origin_type_data: tuple[str, set[str]] | None, + annotations: Iterable[_AnnotationData | None], +) -> _AnnotationData | None: if origin_type_data is None: return None @@ -962,8 +956,8 @@ def _join_generics( def _join_argument_annotations( - annotations: Iterable[Optional[_AnnotationData]], -) -> Optional[tuple[list[str], set[str]]]: + annotations: Iterable[_AnnotationData | None], +) -> tuple[list[str], set[str]] | None: imports: set[str] = set() arg_types: list[str] = [] @@ -976,15 +970,28 @@ def _join_argument_annotations( return arg_types, imports -def _parameter_to_annotation(parameter: Any) -> Optional[_AnnotationData]: +def _parameter_to_annotation(parameter: Any) -> _AnnotationData | None: # if a ForwardRef could not be resolved if isinstance(parameter, str): return None if isinstance(parameter, ForwardRef): - forwarded_value = parameter.__forward_value__ - if forwarded_value is None: - return None + if sys.version_info[:2] < (3, 14): + forwarded_value = parameter.__forward_value__ + if forwarded_value is None: + return None + else: + # ForwardRef.__forward_value__ was removed in 3.14 in favor of + # ForwardRef.evaluate(). See also PEP 649, PEP 749, and + # typing.evaluate_forward_ref. + # + # .evaluate() with Format.VALUE (the default) throws if the name + # could not be resolved. + # https://docs.python.org/3.14/library/annotationlib.html#annotationlib.ForwardRef.evaluate + try: + forwarded_value = parameter.evaluate() + except Exception: + return None return _parameter_to_annotation(forwarded_value) # the arguments of Callable are in a list @@ -1029,7 +1036,7 @@ def _parameter_to_annotation(parameter: Any) -> Optional[_AnnotationData]: arg_types = () # typing types get translated to classes that don't support generics - origin_annotation: Optional[_AnnotationData] + origin_annotation: _AnnotationData | None if type_name.startswith("typing."): try: new_type_name = type_name[: type_name.index("[")] @@ -1084,13 +1091,21 @@ def _make_test(imports: ImportSet, body: str) -> str: header += "# TODO: replace st.nothing() with an appropriate strategy\n\n" elif nothings >= 1: header += "# TODO: replace st.nothing() with appropriate strategies\n\n" - return black.format_str(header + body, mode=black.FileMode()) + return black.format_str(header + body, mode=black.Mode()) def _is_probably_ufunc(obj): # See https://numpy.org/doc/stable/reference/ufuncs.html - there doesn't seem # to be an upstream function to detect this, so we just guess. - has_attributes = "nin nout nargs ntypes types identity signature".split() + has_attributes = [ + "nin", + "nout", + "nargs", + "ntypes", + "types", + "identity", + "signature", + ] return callable(obj) and all(hasattr(obj, name) for name in has_attributes) @@ -1124,7 +1139,7 @@ ROUNDTRIP_PAIRS = ( def _get_testable_functions(thing: object) -> dict[str, Callable]: by_name = {} if callable(thing): - funcs: list[Optional[Any]] = [thing] + funcs: list[Any | None] = [thing] elif isinstance(thing, types.ModuleType): if hasattr(thing, "__all__"): funcs = [getattr(thing, name, None) for name in thing.__all__] @@ -1176,10 +1191,10 @@ def _get_testable_functions(thing: object) -> dict[str, Callable]: def magic( - *modules_or_functions: Union[Callable, types.ModuleType], + *modules_or_functions: Callable | types.ModuleType, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Guess which ghostwriters to use, for a module or collection of functions. @@ -1337,7 +1352,7 @@ def fuzz( *, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``func``. @@ -1401,7 +1416,7 @@ def idempotent( *, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``func``. @@ -1484,7 +1499,7 @@ def roundtrip( *funcs: Callable, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``funcs``. @@ -1526,7 +1541,7 @@ def _make_equiv_body(funcs, except_, style, annotate): var_names = _get_varnames(funcs) test_lines = [ _write_call(f, assign=vname, except_=except_) - for vname, f in zip(var_names, funcs) + for vname, f in zip(var_names, funcs, strict=True) ] assertions = "\n".join( _assert_eq(style, var_names[0], vname) for vname in var_names[1:] @@ -1571,7 +1586,7 @@ def _make_equiv_errors_body(funcs, except_, style, annotate): catch = f"except {suppress}:\n reject()\n" if suppress else "" test_lines = [EQUIV_FIRST_BLOCK.format(indent(first_call, prefix=" "), catch)] - for vname, f in zip(var_names[1:], rest): + for vname, f in zip(var_names[1:], rest, strict=True): if style == "pytest": ctx = "pytest.raises" extra_imports.add("pytest") @@ -1602,7 +1617,7 @@ def equivalent( allow_same_errors: bool = False, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``funcs``. @@ -1650,11 +1665,11 @@ def binary_operation( *, associative: bool = True, commutative: bool = True, - identity: Union[X, EllipsisType, None] = ..., - distributes_over: Optional[Callable[[X, X], X]] = None, + identity: X | EllipsisType | None = ..., + distributes_over: Callable[[X, X], X] | None = None, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write property tests for the binary operation ``func``. @@ -1717,8 +1732,8 @@ def _make_binop_body( *, associative: bool = True, commutative: bool = True, - identity: Union[X, EllipsisType, None] = ..., - distributes_over: Optional[Callable[[X, X], X]] = None, + identity: X | EllipsisType | None = ..., + distributes_over: Callable[[X, X], X] | None = None, except_: tuple[type[Exception], ...], style: str, annotate: bool, @@ -1736,7 +1751,7 @@ def _make_binop_body( sub_property: str, args: str, body: str, - right: Optional[str] = None, + right: str | None = None, ) -> None: if right is None: assertions = "" @@ -1835,7 +1850,8 @@ def _make_binop_body( ] maker(do.__name__ + "_distributes_over", "abc", "\n".join(dist_parts)) - _, operands_repr = _valid_syntax_repr(operands) + operands_imports, operands_repr = _valid_syntax_repr(operands) + all_imports.update(operands_imports) operands_repr = _st_strategy_names(operands_repr) classdef = "" if style == "unittest": @@ -1851,7 +1867,7 @@ def ufunc( *, except_: Except = (), style: str = "pytest", - annotate: Optional[bool] = None, + annotate: bool | None = None, ) -> str: """Write a property-based test for the :doc:`array ufunc <numpy:reference/ufuncs>` ``func``. diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py index 6762f15e54a..ad554e222ee 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py @@ -21,7 +21,6 @@ your own at all. """ from inspect import signature -from typing import Optional import lark from lark.grammar import NonTerminal, Rule, Symbol, Terminal @@ -64,7 +63,7 @@ class LarkStrategy(st.SearchStrategy): def __init__( self, grammar: Lark, - start: Optional[str], + start: str | None, explicit: dict[str, st.SearchStrategy[str]], alphabet: st.SearchStrategy[str], ) -> None: @@ -209,8 +208,8 @@ def check_explicit(name): def from_lark( grammar: lark.lark.Lark, *, - start: Optional[str] = None, - explicit: Optional[dict[str, st.SearchStrategy[str]]] = None, + start: str | None = None, + explicit: dict[str, st.SearchStrategy[str]] | None = None, alphabet: st.SearchStrategy[str] = st.characters(codec="utf-8"), ) -> st.SearchStrategy[str]: """A strategy for strings accepted by the given context-free grammar. diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py index 7a81d373857..43380e28732 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py @@ -12,7 +12,17 @@ import importlib import math import types from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast, overload +from typing import ( + TYPE_CHECKING, + Any, + Literal, + TypeVar, + Union, + cast, + get_args, + get_origin, + overload, +) import numpy as np @@ -90,7 +100,7 @@ __all__ = [ "valid_tuple_axes", ] -TIME_RESOLUTIONS = tuple("Y M D h m s ms us ns ps fs as".split()) +TIME_RESOLUTIONS = ("Y", "M", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs", "as") # See https://github.com/HypothesisWorks/hypothesis/pull/3394 and linked discussion. NP_FIXED_UNICODE = tuple(int(x) for x in np.__version__.split(".")[:2]) >= (1, 19) @@ -100,18 +110,18 @@ NP_FIXED_UNICODE = tuple(int(x) for x in np.__version__.split(".")[:2]) >= (1, 1 def from_dtype( dtype: np.dtype, *, - alphabet: Optional[st.SearchStrategy[str]] = None, + alphabet: st.SearchStrategy[str] | None = None, min_size: int = 0, - max_size: Optional[int] = None, - min_value: Union[int, float, None] = None, - max_value: Union[int, float, None] = None, - allow_nan: Optional[bool] = None, - allow_infinity: Optional[bool] = None, - allow_subnormal: Optional[bool] = None, - exclude_min: Optional[bool] = None, - exclude_max: Optional[bool] = None, + max_size: int | None = None, + min_value: int | float | None = None, + max_value: int | float | None = None, + allow_nan: bool | None = None, + allow_infinity: bool | None = None, + allow_subnormal: bool | None = None, + exclude_min: bool | None = None, + exclude_max: bool | None = None, min_magnitude: Real = 0, - max_magnitude: Optional[Real] = None, + max_magnitude: Real | None = None, ) -> st.SearchStrategy[Any]: """Creates a strategy which can generate any value of the given dtype. @@ -213,6 +223,8 @@ def from_dtype( else: # NEP-7 defines the NaT value as integer -(2**63) elems = st.integers(-(2**63) + 1, 2**63 - 1) result = st.builds(dtype.type, elems, res) + elif dtype.kind == "O": + return st.from_type(object) else: raise InvalidArgument(f"No strategy inference for {dtype}") return result.map(dtype.type) @@ -236,22 +248,31 @@ class ArrayStrategy(st.SearchStrategy): ) def set_element(self, val, result, idx, *, fill=False): + # `val` is either an arbitrary object (for dtype="O"), or otherwise an + # instance of a numpy dtype. This means we can *usually* expect e.g. + # val.dtype to be present, but can only guarantee it if + # `self.dtype != "O"`. + try: result[idx] = val except TypeError as err: raise InvalidArgument( - f"Could not add element={val!r} of {val.dtype!r} to array of " + f"Could not add element={val!r} of " + f"{getattr(val, 'dtype', type(val))} to array of " f"{result.dtype!r} - possible mismatch of time units in dtypes?" ) from err + try: elem_changed = self._check_elements and val != result[idx] and val == val except Exception as err: # pragma: no cover # This branch only exists to help debug weird behaviour in Numpy, # such as the string problems we had a while back. raise HypothesisException( - f"Internal error when checking element={val!r} of {val.dtype!r} " - f"to array of {result.dtype!r}" + f"Internal error when checking element={val!r} of " + f"{getattr(val, 'dtype', type(val))!r} to array of " + f"{result.dtype!r}" ) from err + if elem_changed: strategy = self.fill if fill else self.element_strategy if self.dtype.kind == "f": # pragma: no cover @@ -349,8 +370,8 @@ class ArrayStrategy(st.SearchStrategy): if result[i] in seen: elements.reject() continue - else: - seen.add(result[i]) + seen.add(result[i]) + needs_fill[i] = False if needs_fill.any(): # We didn't fill all of the indices in the early loop, so we @@ -419,36 +440,34 @@ G = TypeVar("G", bound="np.generic") @overload -@defines_strategy(force_reusable_values=True) def arrays( dtype: Union["np.dtype[G]", st.SearchStrategy["np.dtype[G]"]], - shape: Union[int, st.SearchStrategy[int], Shape, st.SearchStrategy[Shape]], + shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, - elements: Optional[Union[st.SearchStrategy[Any], Mapping[str, Any]]] = None, - fill: Optional[st.SearchStrategy[Any]] = None, + elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, + fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[G]]": ... @overload -@defines_strategy(force_reusable_values=True) def arrays( - dtype: Union[D, st.SearchStrategy[D]], - shape: Union[int, st.SearchStrategy[int], Shape, st.SearchStrategy[Shape]], + dtype: D | st.SearchStrategy[D], + shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, - elements: Optional[Union[st.SearchStrategy[Any], Mapping[str, Any]]] = None, - fill: Optional[st.SearchStrategy[Any]] = None, + elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, + fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[Any]]": ... @defines_strategy(force_reusable_values=True) def arrays( - dtype: Union[D, st.SearchStrategy[D]], - shape: Union[int, st.SearchStrategy[int], Shape, st.SearchStrategy[Shape]], + dtype: D | st.SearchStrategy[D], + shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, - elements: Optional[Union[st.SearchStrategy[Any], Mapping[str, Any]]] = None, - fill: Optional[st.SearchStrategy[Any]] = None, + elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, + fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[Any]]": r"""Returns a strategy for generating :class:`numpy:numpy.ndarray`\ s. @@ -625,7 +644,6 @@ def dtype_factory(kind, sizes, valid_sizes, endianness): @overload -@defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", @@ -634,7 +652,6 @@ def unsigned_integer_dtypes( @overload -@defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", @@ -643,7 +660,6 @@ def unsigned_integer_dtypes( @overload -@defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", @@ -652,7 +668,6 @@ def unsigned_integer_dtypes( @overload -@defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", @@ -661,7 +676,6 @@ def unsigned_integer_dtypes( @overload -@defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", @@ -673,7 +687,7 @@ def unsigned_integer_dtypes( def unsigned_integer_dtypes( *, endianness: str = "?", - sizes: Union[Literal[8, 16, 32, 64], Sequence[Literal[8, 16, 32, 64]]] = ( + sizes: Literal[8, 16, 32, 64] | Sequence[Literal[8, 16, 32, 64]] = ( 8, 16, 32, @@ -693,7 +707,6 @@ def unsigned_integer_dtypes( @overload -@defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", @@ -702,7 +715,6 @@ def integer_dtypes( @overload -@defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", @@ -711,7 +723,6 @@ def integer_dtypes( @overload -@defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", @@ -720,7 +731,6 @@ def integer_dtypes( @overload -@defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", @@ -729,7 +739,6 @@ def integer_dtypes( @overload -@defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", @@ -741,7 +750,7 @@ def integer_dtypes( def integer_dtypes( *, endianness: str = "?", - sizes: Union[Literal[8, 16, 32, 64], Sequence[Literal[8, 16, 32, 64]]] = ( + sizes: Literal[8, 16, 32, 64] | Sequence[Literal[8, 16, 32, 64]] = ( 8, 16, 32, @@ -757,7 +766,6 @@ def integer_dtypes( @overload -@defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", @@ -766,7 +774,6 @@ def floating_dtypes( @overload -@defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", @@ -775,7 +782,6 @@ def floating_dtypes( @overload -@defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", @@ -784,7 +790,6 @@ def floating_dtypes( @overload -@defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", @@ -793,7 +798,6 @@ def floating_dtypes( @overload -@defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", @@ -805,9 +809,11 @@ def floating_dtypes( def floating_dtypes( *, endianness: str = "?", - sizes: Union[ - Literal[16, 32, 64, 96, 128], Sequence[Literal[16, 32, 64, 96, 128]] - ] = (16, 32, 64), + sizes: Literal[16, 32, 64, 96, 128] | Sequence[Literal[16, 32, 64, 96, 128]] = ( + 16, + 32, + 64, + ), ) -> st.SearchStrategy["np.dtype[np.floating[Any]]"]: """Return a strategy for floating-point dtypes. @@ -822,7 +828,6 @@ def floating_dtypes( @overload -@defines_dtype_strategy def complex_number_dtypes( *, endianness: str = "?", @@ -831,7 +836,6 @@ def complex_number_dtypes( @overload -@defines_dtype_strategy def complex_number_dtypes( *, endianness: str = "?", @@ -840,7 +844,6 @@ def complex_number_dtypes( @overload -@defines_dtype_strategy def complex_number_dtypes( *, endianness: str = "?", @@ -849,7 +852,6 @@ def complex_number_dtypes( @overload -@defines_dtype_strategy def complex_number_dtypes( *, endianness: str = "?", @@ -861,7 +863,7 @@ def complex_number_dtypes( def complex_number_dtypes( *, endianness: str = "?", - sizes: Union[Literal[64, 128, 192, 256], Sequence[Literal[64, 128, 192, 256]]] = ( + sizes: Literal[64, 128, 192, 256] | Sequence[Literal[64, 128, 192, 256]] = ( 64, 128, ), @@ -1012,7 +1014,7 @@ def nested_dtypes( subtype_strategy: st.SearchStrategy[np.dtype] = scalar_dtypes(), *, max_leaves: int = 10, - max_itemsize: Optional[int] = None, + max_itemsize: int | None = None, ) -> st.SearchStrategy[np.dtype]: """Return the most-general dtype strategy. @@ -1095,7 +1097,7 @@ def basic_indices( shape: Shape, *, min_dims: int = 0, - max_dims: Optional[int] = None, + max_dims: int | None = None, allow_newaxis: bool = False, allow_ellipsis: bool = True, ) -> st.SearchStrategy[BasicIndex]: @@ -1175,7 +1177,6 @@ I = TypeVar("I", bound=np.integer) @overload -@defines_strategy() def integer_array_indices( shape: Shape, *, @@ -1184,7 +1185,6 @@ def integer_array_indices( @overload -@defines_strategy() def integer_array_indices( shape: Shape, *, @@ -1264,17 +1264,6 @@ def integer_array_indices( ) -def _unpack_generic(thing): - # get_origin and get_args fail on python<3.9 because (some of) the - # relevant types do not inherit from _GenericAlias. So just pick the - # value out directly. - real_thing = getattr(thing, "__origin__", None) - if real_thing is not None: - return (real_thing, getattr(thing, "__args__", ())) - else: - return (thing, ()) - - def _unpack_dtype(dtype): dtype_args = getattr(dtype, "__args__", ()) if dtype_args and type(dtype) not in (getattr(types, "UnionType", object()), Union): @@ -1306,7 +1295,7 @@ def _dtype_from_args(args): return np.dtype(dtype) -def _from_type(thing: type[Ex]) -> Optional[st.SearchStrategy[Ex]]: +def _from_type(thing: type[Ex]) -> st.SearchStrategy[Ex] | None: """Called by st.from_type to try to infer a strategy for thing using numpy. If we can infer a numpy-specific strategy for thing, we return that; otherwise, @@ -1374,9 +1363,13 @@ def _from_type(thing: type[Ex]) -> Optional[st.SearchStrategy[Ex]]: dtype = np.dtype(thing) return from_dtype(dtype) if dtype.kind not in "OV" else None - real_thing, args = _unpack_generic(thing) + origin = get_origin(thing) + # if origin is not generic-like, get_origin returns None. Fall back to thing. + if origin is None: + origin = thing + args = get_args(thing) - if real_thing == _NestedSequence: + if origin == _NestedSequence: # We have to override the default resolution to ensure sequences are of # equal length. Actually they are still not, if the arg specialization # returns arbitrary-shaped sequences or arrays - hence the even more special @@ -1390,7 +1383,7 @@ def _from_type(thing: type[Ex]) -> Optional[st.SearchStrategy[Ex]]: st.recursive(st.tuples(base_strat, base_strat), st.tuples), ) - if real_thing in [np.ndarray, _SupportsArray]: + if origin in [np.ndarray, _SupportsArray]: dtype = _dtype_from_args(args) return arrays(dtype, array_shapes(max_dims=2)) # type: ignore[return-value] diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py index 64e3d055c76..7160566f62e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/pandas/impl.py @@ -12,7 +12,7 @@ from collections import OrderedDict, abc from collections.abc import Sequence from copy import copy from datetime import datetime, timedelta -from typing import Any, Generic, Optional, Union +from typing import Any, Generic, Union import attr import numpy as np @@ -107,28 +107,29 @@ def elements_and_dtype(elements, dtype, source=None): _get_subclasses = getattr(IntegerDtype, "__subclasses__", list) dtype = {t.name: t() for t in _get_subclasses()}.get(dtype, dtype) + is_na_dtype = False if isinstance(dtype, IntegerDtype): is_na_dtype = True dtype = np.dtype(dtype.name.lower()) elif dtype is not None: - is_na_dtype = False dtype = try_convert(np.dtype, dtype, "dtype") - else: - is_na_dtype = False if elements is None: elements = npst.from_dtype(dtype) if is_na_dtype: elements = st.none() | elements - elif dtype is not None: + # as an optimization, avoid converting object dtypes, which will always + # remain unchanged. + elif dtype is not None and dtype.kind != "O": def convert_element(value): if is_na_dtype and value is None: return None - name = f"draw({prefix}elements)" + try: return np.array([value], dtype=dtype)[0] except (TypeError, ValueError, OverflowError): + name = f"draw({prefix}elements)" raise InvalidArgument( f"Cannot convert {name}={value!r} of type " f"{type(value).__name__} to dtype {dtype.str}" @@ -186,8 +187,8 @@ DEFAULT_MAX_SIZE = 10 @defines_strategy() def range_indexes( min_size: int = 0, - max_size: Optional[int] = None, - name: st.SearchStrategy[Optional[str]] = st.none(), + max_size: int | None = None, + name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.RangeIndex]: """Provides a strategy which generates an :class:`~pandas.Index` whose values are 0, 1, ..., n for some n. @@ -213,12 +214,12 @@ def range_indexes( @defines_strategy() def indexes( *, - elements: Optional[st.SearchStrategy[Ex]] = None, + elements: st.SearchStrategy[Ex] | None = None, dtype: Any = None, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, unique: bool = True, - name: st.SearchStrategy[Optional[str]] = st.none(), + name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.Index]: """Provides a strategy for producing a :class:`pandas.Index`. @@ -256,12 +257,17 @@ def indexes( @defines_strategy() def series( *, - elements: Optional[st.SearchStrategy[Ex]] = None, + elements: st.SearchStrategy[Ex] | None = None, dtype: Any = None, - index: Optional[st.SearchStrategy[Union[Sequence, pandas.Index]]] = None, - fill: Optional[st.SearchStrategy[Ex]] = None, + # new-style unions hit https://github.com/sphinx-doc/sphinx/issues/11211 during + # doc builds. See related comment in django/_fields.py. Quote to prevent + # shed/pyupgrade from changing it. + index: ( + st.SearchStrategy["Union[Sequence, pandas.Index]"] | None # noqa: UP007 + ) = None, + fill: st.SearchStrategy[Ex] | None = None, unique: bool = False, - name: st.SearchStrategy[Optional[str]] = st.none(), + name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.Series]: """Provides a strategy for producing a :class:`pandas.Series`. @@ -376,19 +382,19 @@ class column(Generic[Ex]): * unique: If all values in this column should be distinct. """ - name: Optional[Union[str, int]] = attr.ib(default=None) - elements: Optional[st.SearchStrategy[Ex]] = attr.ib(default=None) + name: str | int | None = attr.ib(default=None) + elements: st.SearchStrategy[Ex] | None = attr.ib(default=None) dtype: Any = attr.ib(default=None, repr=get_pretty_function_description) - fill: Optional[st.SearchStrategy[Ex]] = attr.ib(default=None) + fill: st.SearchStrategy[Ex] | None = attr.ib(default=None) unique: bool = attr.ib(default=False) def columns( - names_or_number: Union[int, Sequence[str]], + names_or_number: int | Sequence[str], *, dtype: Any = None, - elements: Optional[st.SearchStrategy[Ex]] = None, - fill: Optional[st.SearchStrategy[Ex]] = None, + elements: st.SearchStrategy[Ex] | None = None, + fill: st.SearchStrategy[Ex] | None = None, unique: bool = False, ) -> list[column[Ex]]: """A convenience function for producing a list of :class:`column` objects @@ -401,7 +407,7 @@ def columns( create the columns. """ if isinstance(names_or_number, (int, float)): - names: list[Union[int, str, None]] = [None] * names_or_number + names: list[int | str | None] = [None] * names_or_number else: names = list(names_or_number) return [ @@ -412,10 +418,10 @@ def columns( @defines_strategy() def data_frames( - columns: Optional[Sequence[column]] = None, + columns: Sequence[column] | None = None, *, - rows: Optional[st.SearchStrategy[Union[dict, Sequence[Any]]]] = None, - index: Optional[st.SearchStrategy[Ex]] = None, + rows: st.SearchStrategy[dict | Sequence[Any]] | None = None, + index: st.SearchStrategy[Ex] | None = None, ) -> st.SearchStrategy[pandas.DataFrame]: """Provides a strategy for producing a :class:`pandas.DataFrame`. @@ -578,7 +584,6 @@ def data_frames( raise InvalidArgument(f"duplicate definition of column name {c.name!r}") column_names.add(c.name) - c.elements, _ = elements_and_dtype(c.elements, c.dtype, label) if c.dtype is None and rows is not None: @@ -589,7 +594,6 @@ def data_frames( c.fill = npst.fill_for( fill=c.fill, elements=c.elements, unique=c.unique, name=label ) - rewritten_columns.append(c) if rows is None: @@ -609,13 +613,12 @@ def data_frames( # For columns with no filling the problem is harder, and drawing # them like that would result in rows being very far apart from - # each other in the underlying data stream, which gets in the way + # each other in the choice sequence, which gets in the way # of shrinking. So what we do is reorder and draw those columns # row wise, so that the values of each row are next to each other. - # This makes life easier for the shrinker when deleting blocks of - # data. - columns_without_fill = [c for c in rewritten_columns if c.fill.is_empty] + # This makes life easier for the shrinker when deleting choices. + columns_without_fill = [c for c in rewritten_columns if c.fill.is_empty] if columns_without_fill: for c in columns_without_fill: data[c.name] = pandas.Series( @@ -637,6 +640,7 @@ def data_frames( reject() else: value = draw(c.elements) + try: data[c.name].iloc[i] = value except ValueError as err: # pragma: no cover @@ -725,7 +729,7 @@ def data_frames( row = as_list if any_unique: has_duplicate = False - for seen, value in zip(all_seen, row): + for seen, value in zip(all_seen, row, strict=False): if seen is None: continue if value in seen: diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/redis.py b/contrib/python/hypothesis/py3/hypothesis/extra/redis.py index 272eb6dc6ac..87a4e6997e5 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/redis.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/redis.py @@ -123,8 +123,10 @@ class RedisExampleDatabase(ExampleDatabase): changed = pipe.execute() if changed[0] > 0: + # did the value set of the first key change? self._publish(("delete", (src, value))) if changed[1] > 0: + # did the value set of the second key change? self._publish(("save", (dest, value))) def _handle_message(self, message: dict) -> None: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/cache.py b/contrib/python/hypothesis/py3/hypothesis/internal/cache.py index 7905fc93b3f..73b9ed76d2c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/cache.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/cache.py @@ -117,15 +117,7 @@ class GenericCache(Generic[K, V]): raise ValueError( "Cannot increase size of cache where all keys have been pinned." ) from None - - # 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 - + del self.keys_to_indices[evicted.key] i = 0 self.data[0] = entry else: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py b/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py index 35fdb8d7b19..6b17f4043e3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/charmap.py @@ -15,21 +15,18 @@ import os import sys import tempfile import unicodedata -from collections.abc import Iterable +from collections.abc import Collection, Iterable from functools import cache from pathlib import Path -from typing import TYPE_CHECKING, Literal, Optional +from typing import Literal, TypeAlias from hypothesis.configuration import storage_directory from hypothesis.control import _current_build_context from hypothesis.errors import InvalidArgument from hypothesis.internal.intervalsets import IntervalSet, IntervalsT -if TYPE_CHECKING: - from typing import TypeAlias - # See https://en.wikipedia.org/wiki/Unicode_character_property#General_Category -CategoryName: "TypeAlias" = Literal[ +CategoryName: TypeAlias = Literal[ "L", # Letter "Lu", # Letter, uppercase "Ll", # Letter, lowercase @@ -68,8 +65,8 @@ CategoryName: "TypeAlias" = Literal[ "Co", # Other, private use "Cn", # Other, not assigned ] -Categories: "TypeAlias" = Iterable[CategoryName] -CategoriesTuple: "TypeAlias" = tuple[CategoryName, ...] +Categories: TypeAlias = Iterable[CategoryName] +CategoriesTuple: TypeAlias = tuple[CategoryName, ...] def charmap_file(fname: str = "charmap") -> Path: @@ -78,7 +75,7 @@ def charmap_file(fname: str = "charmap") -> Path: ) -_charmap = None +_charmap: dict[CategoryName, IntervalsT] | None = None def charmap() -> dict[CategoryName, IntervalsT]: @@ -120,9 +117,9 @@ def charmap() -> dict[CategoryName, IntervalsT]: fd, tmpfile = tempfile.mkstemp(dir=tmpdir) os.close(fd) # Explicitly set the mtime to get reproducible output - with gzip.GzipFile(tmpfile, "wb", mtime=1) as o: + with gzip.GzipFile(tmpfile, "wb", mtime=1) as fp: result = json.dumps(sorted(tmp_charmap.items())) - o.write(result.encode()) + fp.write(result.encode()) os.renames(tmpfile, f) except Exception: @@ -173,15 +170,15 @@ def intervals_from_codec(codec_name: str) -> IntervalSet: # pragma: no cover fd, tmpfile = tempfile.mkstemp(dir=tmpdir) os.close(fd) # Explicitly set the mtime to get reproducible output - with gzip.GzipFile(tmpfile, "wb", mtime=1) as o: - o.write(json.dumps(res.intervals).encode()) + with gzip.GzipFile(tmpfile, "wb", mtime=1) as f: + f.write(json.dumps(res.intervals).encode()) os.renames(tmpfile, fname) except Exception: pass return res -_categories: Optional[Categories] = None +_categories: Categories | None = None def categories() -> Categories: @@ -234,7 +231,7 @@ def as_general_categories(cats: Categories, name: str = "cats") -> CategoriesTup category_index_cache: dict[frozenset[CategoryName], IntervalsT] = {frozenset(): ()} -def _category_key(cats: Optional[Iterable[str]]) -> CategoriesTuple: +def _category_key(cats: Iterable[str] | None) -> CategoriesTuple: """Return a normalised tuple of all Unicode categories that are in `include`, but not in `exclude`. @@ -290,11 +287,11 @@ limited_category_index_cache: dict[ def query( *, - categories: Optional[Categories] = None, - min_codepoint: Optional[int] = None, - max_codepoint: Optional[int] = None, - include_characters: str = "", - exclude_characters: str = "", + categories: Categories | None = None, + min_codepoint: int | None = None, + max_codepoint: int | None = None, + include_characters: Collection[str] = "", + exclude_characters: Collection[str] = "", ) -> IntervalSet: """Return a tuple of intervals covering the codepoints for all characters that meet the criteria. @@ -314,8 +311,8 @@ def query( if max_codepoint is None: max_codepoint = sys.maxunicode catkey = _category_key(categories) - character_intervals = IntervalSet.from_string(include_characters or "") - exclude_intervals = IntervalSet.from_string(exclude_characters or "") + character_intervals = IntervalSet.from_string("".join(include_characters)) + exclude_intervals = IntervalSet.from_string("".join(exclude_characters)) qkey = ( catkey, min_codepoint, diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/compat.py b/contrib/python/hypothesis/py3/hypothesis/internal/compat.py index 136dac0275f..0c82a53e301 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/compat.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/compat.py @@ -24,7 +24,6 @@ from typing import ( ForwardRef, Optional, TypedDict as TypedDict, - Union, get_args, ) @@ -38,10 +37,7 @@ except NameError: ) if TYPE_CHECKING: from typing_extensions import ( - Concatenate as Concatenate, NotRequired as NotRequired, - ParamSpec as ParamSpec, - TypeAlias as TypeAlias, TypedDict as TypedDict, override as override, ) @@ -67,32 +63,16 @@ else: try: from typing import ( - Concatenate as Concatenate, - ParamSpec as ParamSpec, - TypeAlias as TypeAlias, override as override, ) except ImportError: try: from typing_extensions import ( - Concatenate as Concatenate, - ParamSpec as ParamSpec, - TypeAlias as TypeAlias, override as override, ) except ImportError: - Concatenate, ParamSpec = None, None - 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" WINDOWS = platform.system() == "Windows" @@ -116,7 +96,7 @@ def escape_unicode_characters(s: str) -> str: return codecs.encode(s, "unicode_escape").decode("ascii") -def int_from_bytes(data: Union[bytes, bytearray]) -> int: +def int_from_bytes(data: bytes | bytearray) -> int: return int.from_bytes(data, "big") @@ -203,11 +183,12 @@ def get_type_hints(thing: object) -> dict[str, Any]: for sig_hint, hint in zip( _hint_and_args(p.annotation), _hint_and_args(hints.get(p.name, Any)), + strict=False, ) ): p_hint = hints[p.name] if p.default is None: - hints[p.name] = typing.Optional[p_hint] + hints[p.name] = p_hint | None else: hints[p.name] = p_hint except (AttributeError, TypeError, NameError): # pragma: no cover @@ -236,7 +217,7 @@ def ceil(x): return y -def extract_bits(x: int, /, width: Optional[int] = None) -> list[int]: +def extract_bits(x: int, /, width: int | None = None) -> list[int]: assert x >= 0 result = [] while x: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py index 500d8f20709..ffc08246fa6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/choice.py @@ -9,15 +9,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. import math -from collections.abc import Hashable, Iterable, Sequence +from collections.abc import Callable, Hashable, Iterable, Sequence from typing import ( - TYPE_CHECKING, - Callable, Literal, - Optional, + TypeAlias, TypedDict, TypeVar, - Union, cast, ) @@ -31,14 +28,11 @@ from hypothesis.internal.intervalsets import IntervalSet T = TypeVar("T") -if TYPE_CHECKING: - from typing import TypeAlias - class IntegerConstraints(TypedDict): - min_value: Optional[int] - max_value: Optional[int] - weights: Optional[dict[int, float]] + min_value: int | None + max_value: int | None + weights: dict[int, float] | None shrink_towards: int @@ -64,24 +58,24 @@ class BooleanConstraints(TypedDict): p: float -ChoiceT: "TypeAlias" = Union[int, str, bool, float, bytes] -ChoiceConstraintsT: "TypeAlias" = Union[ - IntegerConstraints, - FloatConstraints, - StringConstraints, - BytesConstraints, - BooleanConstraints, -] -ChoiceTypeT: "TypeAlias" = Literal["integer", "string", "boolean", "float", "bytes"] -ChoiceKeyT: "TypeAlias" = Union[ - int, str, bytes, tuple[Literal["bool"], bool], tuple[Literal["float"], int] -] +ChoiceT: TypeAlias = int | str | bool | float | bytes +ChoiceConstraintsT: TypeAlias = ( + IntegerConstraints + | FloatConstraints + | StringConstraints + | BytesConstraints + | BooleanConstraints +) +ChoiceTypeT: TypeAlias = Literal["integer", "string", "boolean", "float", "bytes"] +ChoiceKeyT: TypeAlias = ( + int | str | bytes | tuple[Literal["bool"], bool] | tuple[Literal["float"], int] +) @attr.s(slots=True) class ChoiceTemplate: type: Literal["simplest"] = attr.ib() - count: Optional[int] = attr.ib() + count: int | None = attr.ib() def __attrs_post_init__(self) -> None: if self.count is not None: @@ -94,13 +88,13 @@ class ChoiceNode: value: ChoiceT = attr.ib() constraints: ChoiceConstraintsT = attr.ib() was_forced: bool = attr.ib() - index: Optional[int] = attr.ib(default=None) + index: int | None = attr.ib(default=None) def copy( self, *, - with_value: Optional[ChoiceT] = None, - with_constraints: Optional[ChoiceConstraintsT] = None, + with_value: ChoiceT | None = None, + with_constraints: ChoiceConstraintsT | None = None, ) -> "ChoiceNode": # we may want to allow this combination in the future, but for now it's # a footgun. diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py index 5e3b51eabc1..6f9d07591bb 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py @@ -20,9 +20,8 @@ from typing import ( Any, Literal, NoReturn, - Optional, + TypeAlias, TypeVar, - Union, cast, overload, ) @@ -75,9 +74,9 @@ from hypothesis.utils.conventions import not_set from hypothesis.utils.threading import ThreadLocal if TYPE_CHECKING: - from typing import TypeAlias - from hypothesis.strategies import SearchStrategy + from hypothesis.strategies._internal.core import DataObject + from hypothesis.strategies._internal.random import RandomState from hypothesis.strategies._internal.strategies import Ex @@ -101,11 +100,9 @@ def __getattr__(name: str) -> Any: T = TypeVar("T") -TargetObservations = dict[str, Union[int, float]] +TargetObservations = dict[str, int | float] # index, choice_type, constraints, forced value -MisalignedAt: "TypeAlias" = tuple[ - int, ChoiceTypeT, ChoiceConstraintsT, Optional[ChoiceT] -] +MisalignedAt: TypeAlias = tuple[int, ChoiceTypeT, ChoiceConstraintsT, ChoiceT | None] TOP_LABEL = calc_label_from_name("top") MAX_DEPTH = 100 @@ -113,19 +110,6 @@ MAX_DEPTH = 100 threadlocal = ThreadLocal(global_test_counter=int) -class ExtraInformation: - """A class for holding shared state on a ``ConjectureData`` that should - be added to the final ``ConjectureResult``.""" - - def __repr__(self) -> str: - return "ExtraInformation({})".format( - ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items()), - ) - - def has_information(self) -> bool: - return bool(self.__dict__) - - class Status(IntEnum): OVERRUN = 0 INVALID = 1 @@ -215,7 +199,7 @@ class Span: return self.owner.labels[self.owner.label_indices[self.index]] @property - def parent(self) -> Optional[int]: + def parent(self) -> int | None: """The index of the span that this one is nested directly within.""" if self.index == 0: return None @@ -335,7 +319,7 @@ class SpanRecord: def __init__(self) -> None: self.labels: list[int] = [] - self.__index_of_labels: Optional[dict[int, int]] = {} + self.__index_of_labels: dict[int, int] | None = {} self.trail = IntList() self.nodes: list[ChoiceNode] = [] @@ -462,7 +446,7 @@ class Spans: self.__length = self.trail.count( TrailType.STOP_SPAN_DISCARD ) + record.trail.count(TrailType.STOP_SPAN_NO_DISCARD) - self.__children: Optional[list[Sequence[int]]] = None + self.__children: list[Sequence[int]] | None = None @cached_property def starts_and_ends(self) -> tuple[IntList, IntList]: @@ -547,7 +531,7 @@ class DataObserver: def conclude_test( self, status: Status, - interesting_origin: Optional[InterestingOrigin], + interesting_origin: InterestingOrigin | None, ) -> None: """Called when ``conclude_test`` is called on the observed ``ConjectureData``, with the same arguments. @@ -591,21 +575,20 @@ class ConjectureResult: usefulness.""" status: Status = attr.ib() - interesting_origin: Optional[InterestingOrigin] = attr.ib() + interesting_origin: InterestingOrigin | None = attr.ib() nodes: tuple[ChoiceNode, ...] = attr.ib(eq=False, repr=False) length: int = attr.ib() output: str = attr.ib() - extra_information: Optional[ExtraInformation] = attr.ib() - expected_exception: Optional[BaseException] = attr.ib() - expected_traceback: Optional[str] = attr.ib() + expected_exception: BaseException | None = attr.ib() + expected_traceback: str | None = attr.ib() has_discards: bool = attr.ib() target_observations: TargetObservations = attr.ib() tags: frozenset[StructuralCoverageTag] = attr.ib() spans: Spans = attr.ib(repr=False, eq=False) arg_slices: set[tuple[int, int]] = attr.ib(repr=False) slice_comments: dict[tuple[int, int], str] = attr.ib(repr=False) - misaligned_at: Optional[MisalignedAt] = attr.ib(repr=False) - cannot_proceed_scope: Optional[CannotProceedScopeT] = attr.ib(repr=False) + misaligned_at: MisalignedAt | None = attr.ib(repr=False) + cannot_proceed_scope: CannotProceedScopeT | None = attr.ib(repr=False) def as_result(self) -> "ConjectureResult": return self @@ -619,11 +602,11 @@ class ConjectureData: @classmethod def for_choices( cls, - choices: Sequence[Union[ChoiceTemplate, ChoiceT]], + choices: Sequence[ChoiceTemplate | ChoiceT], *, - observer: Optional[DataObserver] = None, - provider: Union[type, PrimitiveProvider] = HypothesisProvider, - random: Optional[Random] = None, + observer: DataObserver | None = None, + provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider, + random: Random | None = None, ) -> "ConjectureData": from hypothesis.internal.conjecture.engine import choice_count @@ -638,12 +621,12 @@ class ConjectureData: def __init__( self, *, - random: Optional[Random], - observer: Optional[DataObserver] = None, - provider: Union[type, PrimitiveProvider] = HypothesisProvider, - prefix: Optional[Sequence[Union[ChoiceTemplate, ChoiceT]]] = None, - max_choices: Optional[int] = None, - provider_kw: Optional[dict[str, Any]] = None, + random: Random | None, + observer: DataObserver | None = None, + provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider, + prefix: Sequence[ChoiceTemplate | ChoiceT] | None = None, + max_choices: int | None = None, + provider_kw: dict[str, Any] | None = None, ) -> None: from hypothesis.internal.conjecture.engine import BUFFER_SIZE @@ -661,7 +644,6 @@ class ConjectureData: self.observer = observer self.max_choices = max_choices self.max_length = BUFFER_SIZE - self.is_find = False self.overdraw = 0 self._random = random @@ -674,8 +656,8 @@ class ConjectureData: 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]] = {} - self.interesting_origin: Optional[InterestingOrigin] = None + self.events: dict[str, str | int | float] = {} + self.interesting_origin: InterestingOrigin | None = None self.draw_times: dict[str, float] = {} self._stateful_run_times: dict[str, float] = defaultdict(float) self.max_depth = 0 @@ -686,7 +668,7 @@ class ConjectureData: ) assert isinstance(self.provider, PrimitiveProvider) - self.__result: Optional[ConjectureResult] = None + self.__result: ConjectureResult | None = None # Observations used for targeted search. They'll be aggregated in # ConjectureRunner.generate_new_examples and fed to TargetSelector. @@ -700,7 +682,7 @@ class ConjectureData: # Normally unpopulated but we need this in the niche case # that self.as_result() is Overrun but we still want the # examples for reporting purposes. - self.__spans: Optional[Spans] = None + self.__spans: Spans | None = None # We want the top level span to have depth 0, so we start # at -1. @@ -715,20 +697,24 @@ class ConjectureData: 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, tuple[int, Any]] = {} - self.hypothesis_runner = not_set - self.expected_exception: Optional[BaseException] = None - self.expected_traceback: Optional[str] = None - self.extra_information = ExtraInformation() + self._sampled_from_all_strategies_elements_message: ( + tuple[str, object] | None + ) = None + self._shared_strategy_draws: dict[Hashable, tuple[Any, SearchStrategy]] = {} + self._shared_data_strategy: DataObject | None = None + self._stateful_repr_parts: list[Any] | None = None + self.states_for_ids: dict[int, RandomState] | None = None + self.seeds_to_states: dict[Any, RandomState] | None = None + self.hypothesis_runner: Any = not_set + + self.expected_exception: BaseException | None = None + self.expected_traceback: str | None = None self.prefix = prefix self.nodes: tuple[ChoiceNode, ...] = () - self.misaligned_at: Optional[MisalignedAt] = None - self.cannot_proceed_scope: Optional[CannotProceedScopeT] = None + self.misaligned_at: MisalignedAt | None = None + self.cannot_proceed_scope: CannotProceedScopeT | None = None self.start_span(TOP_LABEL) def __repr__(self) -> str: @@ -758,7 +744,7 @@ class ConjectureData: constraints: IntegerConstraints, *, observe: bool, - forced: Optional[int], + forced: int | None, ) -> int: ... @overload @@ -768,7 +754,7 @@ class ConjectureData: constraints: FloatConstraints, *, observe: bool, - forced: Optional[float], + forced: float | None, ) -> float: ... @overload @@ -778,7 +764,7 @@ class ConjectureData: constraints: StringConstraints, *, observe: bool, - forced: Optional[str], + forced: str | None, ) -> str: ... @overload @@ -788,7 +774,7 @@ class ConjectureData: constraints: BytesConstraints, *, observe: bool, - forced: Optional[bytes], + forced: bytes | None, ) -> bytes: ... @overload @@ -798,7 +784,7 @@ class ConjectureData: constraints: BooleanConstraints, *, observe: bool, - forced: Optional[bool], + forced: bool | None, ) -> bool: ... def _draw( @@ -807,7 +793,7 @@ class ConjectureData: constraints: ChoiceConstraintsT, *, observe: bool, - forced: Optional[ChoiceT], + forced: ChoiceT | None, ) -> 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 @@ -879,12 +865,12 @@ class ConjectureData: def draw_integer( self, - min_value: Optional[int] = None, - max_value: Optional[int] = None, + min_value: int | None = None, + max_value: int | None = None, *, - weights: Optional[dict[int, float]] = None, + weights: dict[int, float] | None = None, shrink_towards: int = 0, - forced: Optional[int] = None, + forced: int | None = None, observe: bool = True, ) -> int: # Validate arguments @@ -926,7 +912,7 @@ class ConjectureData: # TODO: consider supporting these float widths at the choice sequence # level in the future. # width: Literal[16, 32, 64] = 64, - forced: Optional[float] = None, + forced: float | None = None, observe: bool = True, ) -> float: assert smallest_nonzero_magnitude > 0 @@ -966,7 +952,7 @@ class ConjectureData: *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, - forced: Optional[str] = None, + forced: str | None = None, observe: bool = True, ) -> str: assert forced is None or min_size <= len(forced) <= max_size @@ -989,7 +975,7 @@ class ConjectureData: min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, *, - forced: Optional[bytes] = None, + forced: bytes | None = None, observe: bool = True, ) -> bytes: assert forced is None or min_size <= len(forced) <= max_size @@ -1004,7 +990,7 @@ class ConjectureData: self, p: float = 0.5, *, - forced: Optional[bool] = None, + forced: bool | None = None, observe: bool = True, ) -> bool: assert (forced is not True) or p > 0 @@ -1058,7 +1044,7 @@ class ConjectureData: choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, - forced: Optional[ChoiceT], + forced: ChoiceT | None, ) -> ChoiceT: assert self.prefix is not None # checked in _draw @@ -1142,7 +1128,7 @@ class ConjectureData: self.index += 1 return choice - def as_result(self) -> Union[ConjectureResult, _Overrun]: + def as_result(self) -> ConjectureResult | _Overrun: """Convert the result of running this test into either an Overrun object or a ConjectureResult.""" @@ -1159,11 +1145,6 @@ class ConjectureData: output=self.output, expected_traceback=self.expected_traceback, expected_exception=self.expected_exception, - extra_information=( - self.extra_information - if self.extra_information.has_information() - else None - ), has_discards=self.has_discards, target_observations=self.target_observations, tags=frozenset(self.tags), @@ -1188,19 +1169,13 @@ class ConjectureData: def draw( self, strategy: "SearchStrategy[Ex]", - label: Optional[int] = None, - observe_as: Optional[str] = None, + label: int | None = None, + observe_as: str | None = None, ) -> "Ex": - from hypothesis.internal.observability import TESTCASE_CALLBACKS + from hypothesis.internal.observability import observability_enabled 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: - raise InvalidArgument( - f"Cannot use strategy {strategy!r} within a call to find " - "(presumably because it would be invalid after the call had ended)." - ) - at_top_level = self.depth == 0 start_time = None if at_top_level: @@ -1246,7 +1221,7 @@ class ConjectureData: f"while generating {key.removeprefix('generate:')!r} from {strategy!r}", ) raise - if TESTCASE_CALLBACKS: + if observability_enabled(): avoid = self.provider.avoid_realization self._observability_args[key] = to_jsonable(v, avoid_realization=avoid) return v @@ -1338,7 +1313,7 @@ class ConjectureData: self, values: Sequence[T], *, - forced: Optional[T] = None, + forced: T | None = None, observe: bool = True, ) -> T: forced_i = None if forced is None else values.index(forced) @@ -1353,7 +1328,7 @@ class ConjectureData: def conclude_test( self, status: Status, - interesting_origin: Optional[InterestingOrigin] = None, + interesting_origin: InterestingOrigin | None = None, ) -> NoReturn: assert (interesting_origin is None) or (status == Status.INTERESTING) self.__assert_not_frozen("conclude_test") @@ -1365,7 +1340,7 @@ class ConjectureData: def mark_interesting(self, interesting_origin: InterestingOrigin) -> NoReturn: self.conclude_test(Status.INTERESTING, interesting_origin) - def mark_invalid(self, why: Optional[str] = None) -> NoReturn: + def mark_invalid(self, why: str | None = None) -> NoReturn: if why is not None: self.events["invalid because"] = why self.conclude_test(Status.INVALID) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py index 083a105c6a2..54e9b2664db 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/datatree.py @@ -9,9 +9,9 @@ # obtain one at https://mozilla.org/MPL/2.0/. import math -from collections.abc import Generator +from collections.abc import Generator, Set from random import Random -from typing import TYPE_CHECKING, AbstractSet, Final, Optional, Union, cast +from typing import TYPE_CHECKING, Final, TypeAlias, cast import attr @@ -43,11 +43,9 @@ from hypothesis.internal.floats import ( ) if TYPE_CHECKING: - from typing import TypeAlias - from hypothesis.vendor.pretty import RepresentationPrinter -ChildrenCacheValueT: "TypeAlias" = tuple[ +ChildrenCacheValueT: TypeAlias = tuple[ Generator[ChoiceT, None, None], list[ChoiceT], set[ChoiceT] ] @@ -124,7 +122,7 @@ class Conclusion: """Represents a transition to a finished state.""" status: Status = attr.ib() - interesting_origin: Optional[InterestingOrigin] = attr.ib() + interesting_origin: InterestingOrigin | None = attr.ib() def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False @@ -220,7 +218,7 @@ def compute_max_children( # or downwards with our full 128 bit generation, but only half of these # (plus one for the case of generating zero) result in a probe in the # direction we want. ((2**128 - 1) // 2) + 1 == 2 ** 127 - assert (min_value is None) ^ (max_value is None) + assert (min_value is None) != (max_value is None) return 2**127 elif choice_type == "boolean": constraints = cast(BooleanConstraints, constraints) @@ -410,7 +408,7 @@ class TreeNode: # # Stored as None if no indices have been forced, purely for space saving # reasons (we force quite rarely). - __forced: Optional[set[int]] = attr.ib(default=None, init=False) + __forced: set[int] | None = attr.ib(default=None, init=False) # What happens next after drawing these nodes. (conceptually, "what is the # child/children of the last node stored here"). @@ -421,7 +419,7 @@ class TreeNode: # - Conclusion (ConjectureData.conclude_test was called here) # - Killed (this branch is valid and may even have children, but should not # be explored when generating novel prefixes) - transition: Union[None, Branch, Conclusion, Killed] = attr.ib(default=None) + transition: None | Branch | Conclusion | Killed = attr.ib(default=None) # A tree node is exhausted if every possible sequence of draws below it has # been explored. We only update this when performing operations that could @@ -431,7 +429,7 @@ class TreeNode: is_exhausted: bool = attr.ib(default=False, init=False) @property - def forced(self) -> AbstractSet[int]: + def forced(self) -> Set[int]: if not self.__forced: return EMPTY return self.__forced @@ -528,7 +526,7 @@ class TreeNode: assert cycle is False indent = 0 for i, (choice_type, constraints, value) in enumerate( - zip(self.choice_types, self.constraints, self.values) + zip(self.choice_types, self.constraints, self.values, strict=True) ): with p.indent(indent): if i > 0: @@ -730,6 +728,7 @@ class DataTree: current_node.choice_types, current_node.constraints, current_node.values, + strict=True, ) ): if i in current_node.forced: @@ -769,55 +768,55 @@ class DataTree: # We've now found a value that is allowed to # vary, so what follows is not fixed. return tuple(prefix) - else: - assert not isinstance(current_node.transition, (Conclusion, Killed)) - if current_node.transition is None: - return tuple(prefix) - branch = current_node.transition - assert isinstance(branch, Branch) - attempts = 0 - while True: - if attempts <= 10: - try: - node_value = self._draw( - branch.choice_type, branch.constraints, random=random - ) - except StopTest: # pragma: no cover - attempts += 1 - continue - else: - node_value = self._draw_from_cache( - branch.choice_type, - branch.constraints, - key=id(branch), - random=random, - ) + assert not isinstance(current_node.transition, (Conclusion, Killed)) + if current_node.transition is None: + return tuple(prefix) + branch = current_node.transition + assert isinstance(branch, Branch) + + attempts = 0 + while True: + if attempts <= 10: try: - child = branch.children[node_value] - except KeyError: - append_choice(branch.choice_type, node_value) - return tuple(prefix) - if not child.is_exhausted: - append_choice(branch.choice_type, node_value) - current_node = child - break - attempts += 1 - self._reject_child( + node_value = self._draw( + branch.choice_type, branch.constraints, random=random + ) + except StopTest: # pragma: no cover + attempts += 1 + continue + else: + node_value = self._draw_from_cache( branch.choice_type, branch.constraints, - child=node_value, key=id(branch), + random=random, ) + try: + child = branch.children[node_value] + except KeyError: + append_choice(branch.choice_type, node_value) + return tuple(prefix) + if not child.is_exhausted: + append_choice(branch.choice_type, node_value) + current_node = child + break + attempts += 1 + self._reject_child( + branch.choice_type, + branch.constraints, + child=node_value, + key=id(branch), + ) - # We don't expect this assertion to ever fire, but coverage - # wants the loop inside to run if you have branch checking - # on, hence the pragma. - assert ( # pragma: no cover - attempts != 1000 - or len(branch.children) < branch.max_children - or any(not v.is_exhausted for v in branch.children.values()) - ) + # We don't expect this assertion to ever fire, but coverage + # wants the loop inside to run if you have branch checking + # on, hence the pragma. + assert ( # pragma: no cover + attempts != 1000 + or len(branch.children) < branch.max_children + or any(not v.is_exhausted for v in branch.children.values()) + ) def rewrite(self, choices): """Use previously seen ConjectureData objects to return a tuple of @@ -852,7 +851,7 @@ class DataTree: try: while True: for i, (choice_type, constraints, previous) in enumerate( - zip(node.choice_types, node.constraints, node.values) + zip(node.choice_types, node.constraints, node.values, strict=True) ): v = draw( choice_type, @@ -1140,7 +1139,7 @@ class TreeRecordingObserver(DataObserver): self._trail.append(self._current_node) def conclude_test( - self, status: Status, interesting_origin: Optional[InterestingOrigin] + self, status: Status, interesting_origin: InterestingOrigin | None ) -> None: """Says that ``status`` occurred at node ``node``. This updates the node if necessary and checks for consistency.""" diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/dfa/__init__.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/dfa/__init__.py index 1b9d033f132..f30602c7394 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/dfa/__init__.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/dfa/__init__.py @@ -177,7 +177,7 @@ class DFA: assert not self.is_dead(k) cache[k] = inf break - elif k not in cache and not self.is_dead(k): + if k not in cache and not self.is_dead(k): stack.append(k) stack_set.add(k) break diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py index 799fde74cde..1a20c61220f 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/engine.py @@ -11,15 +11,16 @@ import importlib import inspect import math +import threading import time from collections import defaultdict -from collections.abc import Generator, Sequence +from collections.abc import Callable, Generator, Sequence 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 +from random import Random +from typing import Literal, NoReturn, cast from hypothesis import HealthCheck, Phase, Verbosity, settings as Settings from hypothesis._settings import local_settings, note_deprecation @@ -67,14 +68,18 @@ 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.internal.observability import Observation, with_observability_callback from hypothesis.reporting import base_report, report +# In most cases, the following constants are all Final. However, we do allow users +# to monkeypatch all of these variables, which means we cannot annotate them as +# Final or mypyc will inline them and render monkeypatching useless. + #: 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 +MAX_SHRINKS: int = 500 # 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 @@ -86,7 +91,7 @@ MAX_SHRINKS: Final[int] = 500 #: 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 +MAX_SHRINKING_SECONDS: int = 300 #: The maximum amount of entropy a single test case can use before giving up #: while making random choices during input generation. @@ -94,9 +99,14 @@ MAX_SHRINKING_SECONDS: Final[int] = 300 #: 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 +BUFFER_SIZE: int = 8 * 1024 +CACHE_SIZE: int = 10000 +MIN_TEST_CALLS: int = 10 + +# we use this to isolate Hypothesis from interacting with the global random, +# to make it easier to reason about our global random warning logic easier (see +# deprecate_random_in_strategy). +_random = Random() def shortlex(s): @@ -108,7 +118,7 @@ class HealthCheckState: 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( + draw_times: defaultdict[str, list[float]] = field( default_factory=lambda: defaultdict(list) ) @@ -164,9 +174,12 @@ class RunIsComplete(Exception): pass -def _get_provider(backend: str) -> Union[type, PrimitiveProvider]: - mname, cname = AVAILABLE_PROVIDERS[backend].rsplit(".", 1) - provider_cls = getattr(importlib.import_module(mname), cname) +def _get_provider(backend: str) -> PrimitiveProvider | type[PrimitiveProvider]: + provider_cls = AVAILABLE_PROVIDERS[backend] + if isinstance(provider_cls, str): + module_name, class_name = provider_cls.rsplit(".", 1) + provider_cls = getattr(importlib.import_module(module_name), class_name) + if provider_cls.lifetime == "test_function": return provider_cls(None) elif provider_cls.lifetime == "test_case": @@ -208,7 +221,7 @@ StatisticsDict = TypedDict( ) -def choice_count(choices: Sequence[Union[ChoiceT, ChoiceTemplate]]) -> Optional[int]: +def choice_count(choices: Sequence[ChoiceT | ChoiceTemplate]) -> int | None: count = 0 for choice in choices: if isinstance(choice, ChoiceTemplate): @@ -274,23 +287,25 @@ class ConjectureRunner: self, test_function: Callable[[ConjectureData], None], *, - settings: Optional[Settings] = None, - random: Optional[Random] = None, - database_key: Optional[bytes] = None, + settings: Settings | None = None, + random: Random | None = None, + database_key: bytes | None = None, ignore_limits: bool = False, + thread_overlap: dict[int, bool] | None = None, ) -> None: self._test_function: Callable[[ConjectureData], None] = test_function self.settings: Settings = settings or Settings() self.shrinks: int = 0 - self.finish_shrinking_deadline: Optional[float] = None + self.finish_shrinking_deadline: float | None = None self.call_count: int = 0 self.misaligned_count: int = 0 self.valid_examples: int = 0 self.invalid_examples: int = 0 self.overrun_examples: int = 0 - self.random: Random = random or Random(getrandbits(128)) - self.database_key: Optional[bytes] = database_key + self.random: Random = random or Random(_random.getrandbits(128)) + self.database_key: bytes | None = database_key self.ignore_limits: bool = ignore_limits + self.thread_overlap = {} if thread_overlap is None else thread_overlap # Global dict of per-phase statistics, and a list of per-call stats # which transfer to the global dict at the end of each phase. @@ -300,17 +315,13 @@ class ConjectureRunner: 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 - - # At runtime, the keys are only ever type `InterestingOrigin`, but can be `None` during tests. - self.shrunk_examples: set[Optional[InterestingOrigin]] = set() - - self.health_check_state: Optional[HealthCheckState] = None + self.first_bug_found_at: int | None = None + self.last_bug_found_at: int | None = None + self.shrunk_examples: set[InterestingOrigin] = set() + self.health_check_state: HealthCheckState | None = None self.tree: DataTree = DataTree() - - self.provider: Union[type, PrimitiveProvider] = _get_provider( + self.provider: PrimitiveProvider | type[PrimitiveProvider] = _get_provider( self.settings.backend ) @@ -323,29 +334,29 @@ class ConjectureRunner: # is only marginally useful at present, but speeds up local development # because it means that large targets will be quickly surfaced in your # testing. - self.pareto_front: Optional[ParetoFront] = None + self.pareto_front: ParetoFront | None = None if self.database_key is not None and self.settings.database is not None: self.pareto_front = ParetoFront(self.random) self.pareto_front.on_evict(self.on_pareto_evict) # We want to be able to get the ConjectureData object that results - # from running a buffer without recalculating, especially during + # from running a choice sequence without recalculating, especially during # shrinking where we need to know about the structure of the # executed test case. self.__data_cache = LRUReusedCache[ - tuple[ChoiceKeyT, ...], Union[ConjectureResult, _Overrun] + tuple[ChoiceKeyT, ...], ConjectureResult | _Overrun ](CACHE_SIZE) self.reused_previously_shrunk_test_case: bool = False - self.__pending_call_explanation: Optional[str] = None + self.__pending_call_explanation: str | None = 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 + self._verified_by: str | None = None @contextmanager def _with_switch_to_hypothesis_provider( @@ -380,8 +391,6 @@ class ConjectureRunner: self._current_phase = phase yield finally: - # We ignore the mypy type error here. Because `phase` is a string literal and "-phase" is a string literal - # as well, the concatenation will always be valid key in the dictionary. self.statistics[phase + "-phase"] = { # type: ignore "duration-seconds": time.perf_counter() - start_time, "test-cases": list(self.stats_per_test_case), @@ -429,11 +438,11 @@ class ConjectureRunner: def cached_test_function( self, - choices: Sequence[Union[ChoiceT, ChoiceTemplate]], + choices: Sequence[ChoiceT | ChoiceTemplate], *, error_on_discard: bool = False, - extend: Union[int, Literal["full"]] = 0, - ) -> Union[ConjectureResult, _Overrun]: + extend: int | Literal["full"] = 0, + ) -> ConjectureResult | _Overrun: """ If ``error_on_discard`` is set to True this will raise ``ContainsDiscard`` in preference to running the actual test function. This is to allow us @@ -469,7 +478,7 @@ class ConjectureRunner: # The reason is we don't expect simulate_test_function to explore new choices # and write back to the tree, so we don't want the overhead of the # TreeRecordingObserver tracking those calls. - trial_observer: Optional[DataObserver] = DataObserver() + trial_observer: DataObserver | None = DataObserver() if error_on_discard: trial_observer = DiscardObserver() @@ -500,8 +509,7 @@ class ConjectureRunner: pass data = self.new_conjecture_data(choices, max_choices=max_length) - # note that calling test_function caches `data` for us, for both an ir - # tree key and a buffer key. + # note that calling test_function caches `data` for us. self.test_function(data) return data.as_result() @@ -754,45 +762,101 @@ class ConjectureRunner: if state.overrun_examples == max_overrun_draws: fail_health_check( self.settings, - "Examples routinely exceeded the max allowable size. " - f"({state.overrun_examples} examples overran while generating " - f"{state.valid_examples} valid ones). Generating examples this large " - "will usually lead to bad results. You could try setting max_size " - "parameters on your collections and turning max_leaves down on " - "recursive() calls.", + "Generated inputs routinely consumed more than the maximum " + f"allowed entropy: {state.valid_examples} inputs were generated " + f"successfully, while {state.overrun_examples} inputs exceeded the " + f"maximum allowed entropy during generation." + "\n\n" + f"Testing with inputs this large tends to be slow, and to produce " + "failures that are both difficult to shrink and difficult to understand. " + "Try decreasing the amount of data generated, for example by " + "decreasing the minimum size of collection strategies like " + "st.lists()." + "\n\n" + "If you expect the average size of your input to be this large, " + "you can disable this health check with " + "@settings(suppress_health_check=[HealthCheck.data_too_large]). " + "See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", HealthCheck.data_too_large, ) if state.invalid_examples == max_invalid_draws: fail_health_check( self.settings, - "It looks like your strategy is filtering out a lot of data. Health " - f"check found {state.invalid_examples} filtered examples but only " - f"{state.valid_examples} good ones. This will make your tests much " - "slower, and also will probably distort the data generation quite a " - "lot. You should adapt your strategy to filter less. This can also " - "be caused by a low max_leaves parameter in recursive() calls", + "It looks like this test is filtering out a lot of inputs. " + f"{state.valid_examples} inputs were generated successfully, " + f"while {state.invalid_examples} inputs were filtered out. " + "\n\n" + "An input might be filtered out by calls to assume(), " + "strategy.filter(...), or occasionally by Hypothesis internals." + "\n\n" + "Applying this much filtering makes input generation slow, since " + "Hypothesis must discard inputs which are filtered out and try " + "generating it again. It is also possible that applying this much " + "filtering will distort the domain and/or distribution of the test, " + "leaving your testing less rigorous than expected." + "\n\n" + "If you expect this many inputs to be filtered out during generation, " + "you can disable this health check with " + "@settings(suppress_health_check=[HealthCheck.filter_too_much]). See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", HealthCheck.filter_too_much, ) - draw_time = state.total_draw_time - # Allow at least the greater of one second or 5x the deadline. If deadline # is None, allow 30s - the user can disable the healthcheck too if desired. + draw_time = state.total_draw_time draw_time_limit = 5 * (self.settings.deadline or timedelta(seconds=6)) - if draw_time > max(1.0, draw_time_limit.total_seconds()): + if ( + draw_time > max(1.0, draw_time_limit.total_seconds()) + # we disable HealthCheck.too_slow under concurrent threads, since + # cpython may switch away from a thread for arbitrarily long. + and not self.thread_overlap.get(threading.get_ident(), False) + ): + extra_str = [] + if state.invalid_examples: + extra_str.append(f"{state.invalid_examples} invalid inputs") + if state.overrun_examples: + extra_str.append( + f"{state.overrun_examples} inputs which exceeded the " + "maximum allowed entropy" + ) + extra_str = ", and ".join(extra_str) + extra_str = f" ({extra_str})" if extra_str else "" + fail_health_check( self.settings, - "Data generation is extremely slow: Only produced " - f"{state.valid_examples} valid examples in {draw_time:.2f} seconds " - f"({state.invalid_examples} invalid ones and {state.overrun_examples} " - "exceeded maximum size). Try decreasing size of the data you're " - "generating (with e.g. max_size or max_leaves parameters)." - + state.timing_report(), + "Input generation is slow: Hypothesis only generated " + f"{state.valid_examples} valid inputs after {draw_time:.2f} " + f"seconds{extra_str}." + "\n" + state.timing_report() + "\n\n" + "This could be for a few reasons:" + "\n" + "1. This strategy could be generating too much data per input. " + "Try decreasing the amount of data generated, for example by " + "decreasing the minimum size of collection strategies like " + "st.lists()." + "\n" + "2. Some other expensive computation could be running during input " + "generation. For example, " + "if @st.composite or st.data() is interspersed with an expensive " + "computation, HealthCheck.too_slow is likely to trigger. If this " + "computation is unrelated to input generation, move it elsewhere. " + "Otherwise, try making it more efficient, or disable this health " + "check if that is not possible." + "\n\n" + "If you expect input generation to take this long, you can disable " + "this health check with " + "@settings(suppress_health_check=[HealthCheck.too_slow]). See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", HealthCheck.too_slow, ) def save_choices( - self, choices: Sequence[ChoiceT], sub_key: Optional[bytes] = None + self, choices: Sequence[ChoiceT], sub_key: bytes | None = None ) -> None: if self.settings.database is not None: key = self.sub_key(sub_key) @@ -805,7 +869,7 @@ class ConjectureRunner: if self.settings.database is not None and self.database_key is not None: self.settings.database.move(self.database_key, self.secondary_key, buffer) - def sub_key(self, sub_key: Optional[bytes]) -> Optional[bytes]: + def sub_key(self, sub_key: bytes | None) -> bytes | None: if self.database_key is None: return None if sub_key is None: @@ -813,11 +877,11 @@ class ConjectureRunner: return b".".join((self.database_key, sub_key)) @property - def secondary_key(self) -> Optional[bytes]: + def secondary_key(self) -> bytes | None: return self.sub_key(b"secondary") @property - def pareto_key(self) -> Optional[bytes]: + def pareto_key(self) -> bytes | None: return self.sub_key(b"pareto") def debug(self, message: str) -> None: @@ -828,7 +892,7 @@ class ConjectureRunner: def report_debug_info(self) -> bool: return self.settings.verbosity >= Verbosity.debug - def debug_data(self, data: Union[ConjectureData, ConjectureResult]) -> None: + def debug_data(self, data: ConjectureData | ConjectureResult) -> None: if not self.report_debug_info: return @@ -858,28 +922,24 @@ class ConjectureRunner: # and the provider opted-in to observations and self.provider.add_observability_callback ): - return with_observation_callback(on_observation) + return with_observability_callback(on_observation) return nullcontext() def run(self) -> None: - with local_settings(self.settings): - # 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) - ) + with local_settings(self.settings), self.observe_for_provider(): + try: + self._run() + except RunIsComplete: + pass + for v in self.interesting_examples.values(): + self.debug_data(v) + self.debug( + f"Run complete after {self.call_count} examples " + f"({self.valid_examples} valid) and {self.shrinks} shrinks" + ) @property - def database(self) -> Optional[ExampleDatabase]: + def database(self) -> ExampleDatabase | None: if self.database_key is None: return None return self.settings.database @@ -1065,16 +1125,22 @@ class ConjectureRunner: ): fail_health_check( self.settings, - "The smallest natural example for your test is extremely " + "The smallest natural input for this test is very " "large. This makes it difficult for Hypothesis to generate " - "good examples, especially when trying to reduce failing ones " - "at the end. Consider reducing the size of your data if it is " - "of a fixed size. You could also fix this by improving how " - "your data shrinks (see https://hypothesis.readthedocs.io/en/" - "latest/data.html#shrinking for details), or by introducing " - "default values inside your strategy. e.g. could you replace " - "some arguments with their defaults by using " - "one_of(none(), some_complex_strategy)?", + "good inputs, especially when trying to shrink failing inputs." + "\n\n" + "Consider reducing the amount of data generated by the strategy. " + "Also consider introducing small alternative values for some " + "strategies. For example, could you " + "mark some arguments as optional by replacing `some_complex_strategy`" + "with `st.none() | some_complex_strategy`?" + "\n\n" + "If you are confident that the size of the smallest natural input " + "to your test cannot be reduced, you can suppress this health check " + "with @settings(suppress_health_check=[HealthCheck.large_base_example]). " + "See " + "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " + "for details.", HealthCheck.large_base_example, ) @@ -1215,9 +1281,7 @@ class ConjectureRunner: self._current_phase = "target" self.optimise_targets() - def generate_mutations_from( - self, data: Union[ConjectureData, ConjectureResult] - ) -> None: + def generate_mutations_from(self, data: ConjectureData | ConjectureResult) -> None: # A thing that is often useful but rarely happens by accident is # to generate the same value at multiple different points in the # test case. @@ -1447,10 +1511,10 @@ class ConjectureRunner: def new_conjecture_data( self, - prefix: Sequence[Union[ChoiceT, ChoiceTemplate]], + prefix: Sequence[ChoiceT | ChoiceTemplate], *, - observer: Optional[DataObserver] = None, - max_choices: Optional[int] = None, + observer: DataObserver | None = None, + max_choices: int | None = None, ) -> ConjectureData: provider = ( HypothesisProvider if self._switch_to_hypothesis_provider else self.provider @@ -1508,7 +1572,7 @@ class ConjectureRunner: self.shrink(example, lambda d: d.status == Status.INTERESTING) return - def predicate(d: Union[ConjectureResult, _Overrun]) -> bool: + def predicate(d: ConjectureResult | _Overrun) -> bool: if d.status < Status.INTERESTING: return False d = cast(ConjectureResult, d) @@ -1550,23 +1614,23 @@ class ConjectureRunner: def shrink( self, - example: Union[ConjectureData, ConjectureResult], - predicate: Optional[ShrinkPredicateT] = None, - allow_transition: Optional[ - Callable[[Union[ConjectureData, ConjectureResult], ConjectureData], bool] - ] = None, - ) -> Union[ConjectureData, ConjectureResult]: + example: ConjectureData | ConjectureResult, + predicate: ShrinkPredicateT | None = None, + allow_transition: ( + Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None + ) = None, + ) -> ConjectureData | ConjectureResult: s = self.new_shrinker(example, predicate, allow_transition) s.shrink() return s.shrink_target def new_shrinker( self, - example: Union[ConjectureData, ConjectureResult], - predicate: Optional[ShrinkPredicateT] = None, - allow_transition: Optional[ - Callable[[Union[ConjectureData, ConjectureResult], ConjectureData], bool] - ] = None, + example: ConjectureData | ConjectureResult, + predicate: ShrinkPredicateT | None = None, + allow_transition: ( + Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None + ) = None, ) -> Shrinker: return Shrinker( self, diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py index 96f2f80c41b..ef81176a902 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py @@ -14,19 +14,18 @@ anything that lives here, please move it.""" import array import gc +import itertools import sys import time import warnings from array import ArrayType -from collections.abc import Iterable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Sequence from threading import Lock from typing import ( Any, - Callable, ClassVar, Generic, Literal, - Optional, TypeVar, Union, overload, @@ -70,7 +69,7 @@ class IntList(Sequence[int]): the new value.""" 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:])) + NEXT_ARRAY_CODE: ClassVar[dict[str, str]] = dict(itertools.pairwise(ARRAY_CODES)) __slots__ = ("__underlying",) @@ -87,7 +86,7 @@ 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: Union[list[int], "ArrayType[int]"] = underlying + self.__underlying: list[int] | ArrayType[int] = underlying @classmethod def of_length(cls, n: int) -> "IntList": @@ -116,14 +115,12 @@ class IntList(Sequence[int]): @overload def __getitem__( self, i: slice - ) -> Union[list[int], "ArrayType[int]"]: ... # pragma: no cover + ) -> "list[int] | ArrayType[int]": ... # pragma: no cover - def __getitem__( - self, i: Union[int, slice] - ) -> Union[int, list[int], "ArrayType[int]"]: + def __getitem__(self, i: int | slice) -> "int | list[int] | ArrayType[int]": return self.__underlying[i] - def __delitem__(self, i: Union[int, slice]) -> None: + def __delitem__(self, i: int | slice) -> None: del self.__underlying[i] def insert(self, i: int, v: int) -> None: @@ -203,8 +200,8 @@ class LazySequenceCopy(Generic[T]): def __init__(self, values: Sequence[T]): self.__values = values self.__len = len(values) - self.__mask: Optional[dict[int, T]] = None - self.__popped_indices: Optional[SortedList[int]] = None + self.__mask: dict[int, T] | None = None + self.__popped_indices: SortedList[int] | None = None def __len__(self) -> int: if self.__popped_indices is None: @@ -313,13 +310,16 @@ class StackframeLimiter: def __init__(self): self._active_contexts = 0 self._known_limits: set[int] = set() - self._original_limit: Optional[int] = None + self._original_limit: int | None = None def _setrecursionlimit(self, new_limit: int, *, check: bool = True) -> None: - if check and sys.getrecursionlimit() not in self._known_limits: + if ( + check + and (current_limit := sys.getrecursionlimit()) not in self._known_limits + ): warnings.warn( "The recursion limit will not be reset, since it was changed " - "during test execution.", + f"during test execution (from {self._original_limit} to {current_limit}).", HypothesisWarning, stacklevel=4, ) @@ -547,13 +547,13 @@ def gc_cumulative_time() -> float: def startswith(l1: Sequence[T], l2: Sequence[T]) -> bool: if len(l1) < len(l2): return False - return all(v1 == v2 for v1, v2 in zip(l1[: len(l2)], l2)) + return all(v1 == v2 for v1, v2 in zip(l1[: len(l2)], l2, strict=False)) def endswith(l1: Sequence[T], l2: Sequence[T]) -> bool: if len(l1) < len(l2): return False - return all(v1 == v2 for v1, v2 in zip(l1[-len(l2) :], l2)) + return all(v1 == v2 for v1, v2 in zip(l1[-len(l2) :], l2, strict=False)) def bits_to_bytes(n: int) -> int: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py index 03807cf4be2..2d0b738eca1 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py @@ -8,8 +8,6 @@ # 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 typing import Optional, Union - from hypothesis.internal.compat import int_from_bytes, int_to_bytes from hypothesis.internal.conjecture.choice import ChoiceT, choice_permitted from hypothesis.internal.conjecture.data import ConjectureResult, Status, _Overrun @@ -61,7 +59,7 @@ class Optimiser: def current_score(self) -> float: return self.score_function(self.current_data) - def consider_new_data(self, data: Union[ConjectureResult, _Overrun]) -> bool: + def consider_new_data(self, data: ConjectureResult | _Overrun) -> bool: """Consider a new data object as a candidate target. If it is better than the current one, return True.""" if data.status < Status.VALID: @@ -91,7 +89,7 @@ class Optimiser: nodes_examined = set() - prev: Optional[ConjectureResult] = None + prev: ConjectureResult | None = None i = len(self.current_data.nodes) - 1 while i >= 0 and self.improvements <= self.max_improvements: if prev is not self.current_data: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py index fea0f118546..7c39d9f8cd8 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/pareto.py @@ -8,10 +8,10 @@ # 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 Iterator +from collections.abc import Callable, Iterator from enum import Enum from random import Random -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import TYPE_CHECKING from sortedcontainers import SortedList @@ -144,9 +144,9 @@ class ParetoFront: self.front: SortedList[ConjectureResult] = SortedList( key=lambda d: sort_key(d.nodes) ) - self.__pending: Optional[ConjectureResult] = None + self.__pending: ConjectureResult | None = None - def add(self, data: Union[ConjectureData, ConjectureResult, _Overrun]) -> bool: + def add(self, data: ConjectureData | ConjectureResult | _Overrun) -> bool: """Attempts to add ``data`` to the pareto front. Returns True if ``data`` is now in the front, including if data is already in the collection, and False otherwise""" diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py index f4fdd57c8f8..558bb686425 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/provider_conformance.py @@ -11,7 +11,7 @@ import math import sys from collections.abc import Collection, Iterable, Sequence -from typing import Any, Optional +from typing import Any from hypothesis import ( HealthCheck, @@ -29,7 +29,9 @@ from hypothesis.internal.conjecture.choice import ( from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.providers import ( COLLECTION_DEFAULT_MAX_SIZE, + HypothesisProvider, PrimitiveProvider, + with_register_backend, ) from hypothesis.internal.floats import SMALLEST_SUBNORMAL, sign_aware_lte from hypothesis.internal.intervalsets import IntervalSet @@ -72,7 +74,7 @@ def intervals( @st.composite def integer_weights( - draw: DrawFn, min_value: Optional[int] = None, max_value: Optional[int] = None + draw: DrawFn, min_value: int | None = None, max_value: int | None = None ) -> dict[int, float]: # Sampler doesn't play well with super small floats, so exclude them weights = draw( @@ -165,9 +167,9 @@ def integer_constraints( def _collection_constraints( draw: DrawFn, *, - forced: Optional[Any], - use_min_size: Optional[bool] = None, - use_max_size: Optional[bool] = None, + forced: Any | None, + use_min_size: bool | None = None, + use_max_size: bool | None = None, ) -> dict[str, int]: min_size = 0 max_size = COLLECTION_DEFAULT_MAX_SIZE @@ -201,8 +203,8 @@ def _collection_constraints( def string_constraints( draw: DrawFn, *, - use_min_size: Optional[bool] = None, - use_max_size: Optional[bool] = None, + use_min_size: bool | None = None, + use_max_size: bool | None = None, use_forced: bool = False, ) -> Any: interval_set = draw(intervals()) @@ -226,8 +228,8 @@ def string_constraints( def bytes_constraints( draw: DrawFn, *, - use_min_size: Optional[bool] = None, - use_max_size: Optional[bool] = None, + use_min_size: bool | None = None, + use_max_size: bool | None = None, use_forced: bool = False, ) -> Any: forced = draw(st.binary()) if use_forced else None @@ -337,7 +339,7 @@ def run_conformance_test( Provider: type[PrimitiveProvider], *, context_manager_exceptions: Collection[type[BaseException]] = (), - settings: Optional[Settings] = None, + settings: Settings | None = None, _realize_objects: SearchStrategy[Any] = ( st.from_type(object) | st.from_type(type).flatmap(st.from_type) ), @@ -348,7 +350,7 @@ def run_conformance_test( 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. + which Hypothesis depends on. 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 @@ -369,119 +371,132 @@ def run_conformance_test( treat those exceptions as fatal errors. """ - @Settings(settings, suppress_health_check=[HealthCheck.too_slow]) - class ProviderConformanceTest(RuleBasedStateMachine): - def __init__(self): - super().__init__() + class CopiesRealizationProvider(HypothesisProvider): + avoid_realization = Provider.avoid_realization - @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) + with with_register_backend("copies_realization", CopiesRealizationProvider): - self.context_manager = self.provider.per_test_case_context_manager() - self.context_manager.__enter__() - self.frozen = False + @Settings( + settings, + suppress_health_check=[HealthCheck.too_slow], + backend="copies_realization", + ) + 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}") + 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 + 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 + 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=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=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=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=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(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(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(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: 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) + @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 + # 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() + 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) + @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) + def teardown(self): + if not self.frozen: + self.context_manager.__exit__(None, None, None) - ProviderConformanceTest.TestCase().runTest() + 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 827969360a9..a88e39997bd 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/providers.py @@ -14,7 +14,7 @@ import math import sys import warnings from collections.abc import Iterable -from contextlib import AbstractContextManager +from contextlib import AbstractContextManager, contextmanager from functools import cached_property from random import Random from sys import float_info @@ -25,9 +25,9 @@ from typing import ( ClassVar, Literal, Optional, + TypeAlias, TypedDict, TypeVar, - Union, ) from sortedcontainers import SortedSet @@ -67,20 +67,24 @@ 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") -LifetimeT: "TypeAlias" = Literal["test_case", "test_function"] +LifetimeT: TypeAlias = Literal["test_case", "test_function"] COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large" -#: 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. +#: Registered Hypothesis backends. This is a dictionary where keys are the name +#: to be used in |settings.backend|. The value of a key can be either: +#: +#: * A string corresponding to an importable absolute path of a +#: |PrimitiveProvider| subclass +#: * A |PrimitiveProvider| subclass (the class itself, not an instance of the +#: class) +#: +#: Hypothesis will instantiate the corresponding |PrimitiveProvider| subclass +#: when the backend is requested by a test's |settings.backend| value. #: #: For example, the default Hypothesis backend is registered as: #: @@ -89,6 +93,8 @@ COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large" #: from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS #: #: AVAILABLE_PROVIDERS["hypothesis"] = "hypothesis.internal.conjecture.providers.HypothesisProvider" +#: # or +#: AVAILABLE_PROVIDERS["hypothesis"] = HypothesisProvider #: #: And can be used with: #: @@ -104,16 +110,20 @@ COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large" #: 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 = { +#: For third-party backend authors, we strongly encourage ensuring that +#: ``import hypothesis`` does not automatically import the expensive parts of +#: your package, by: +#: +#: - setting a string path here, instead of a provider class +#: - ensuring the registered hypothesis plugin path references a path which just +#: sets AVAILABLE_PROVIDERS and does not import your package +AVAILABLE_PROVIDERS: dict[str, str | type["PrimitiveProvider"]] = { "hypothesis": "hypothesis.internal.conjecture.providers.HypothesisProvider", "hypothesis-urandom": "hypothesis.internal.conjecture.providers.URandomProvider", } # cache the choice_permitted constants for a particular set of constraints. -CacheKeyT: "TypeAlias" = tuple[ChoiceTypeT, tuple[Any, ...]] -CacheValueT: "TypeAlias" = tuple[tuple["ConstantT", ...], tuple["ConstantT", ...]] +CacheKeyT: TypeAlias = tuple[ChoiceTypeT, tuple[Any, ...]] +CacheValueT: TypeAlias = tuple[tuple["ConstantT", ...], tuple["ConstantT", ...]] CONSTANTS_CACHE: LRUCache[CacheKeyT, CacheValueT] = LRUCache(1024) _constant_floats = ( @@ -239,7 +249,7 @@ _local_constants = Constants( # 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 +_sys_modules_len: int | None = None def _get_local_constants() -> Constants: @@ -299,10 +309,19 @@ def _get_local_constants() -> Constants: return _local_constants +@contextmanager +def with_register_backend(name, provider_cls): + try: + AVAILABLE_PROVIDERS[name] = provider_cls + yield + finally: + del AVAILABLE_PROVIDERS[name] + + class _BackendInfoMsg(TypedDict): type: InfoObservationType title: str - content: Union[str, dict[str, Any]] + content: str | dict[str, Any] # TODO_DOCS: link to choice sequence explanation page @@ -362,9 +381,9 @@ class PrimitiveProvider(abc.ABC): avoid_realization: ClassVar[bool] = False #: 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. + #: callback via |add_observability_callback|, 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. @@ -397,10 +416,10 @@ class PrimitiveProvider(abc.ABC): @abc.abstractmethod def draw_integer( self, - min_value: Optional[int] = None, - max_value: Optional[int] = None, + min_value: int | None = None, + max_value: int | None = None, *, - weights: Optional[dict[int, float]] = None, + weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: """ @@ -562,8 +581,9 @@ class PrimitiveProvider(abc.ABC): """ 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. + other callbacks added via |add_observability_callback|. This method is not + called with ``observation["type"] in {"info", "alert", "error"}`` + observations. .. important:: @@ -739,10 +759,10 @@ class HypothesisProvider(PrimitiveProvider): def draw_integer( self, - min_value: Optional[int] = None, - max_value: Optional[int] = None, + min_value: int | None = None, + max_value: int | None = None, *, - weights: Optional[dict[int, float]] = None, + weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: assert self._cd is not None @@ -1056,10 +1076,10 @@ class BytestringProvider(PrimitiveProvider): def draw_integer( self, - min_value: Optional[int] = None, - max_value: Optional[int] = None, + min_value: int | None = None, + max_value: int | None = None, *, - weights: Optional[dict[int, float]] = None, + weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: assert self._cd is not None diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py index 1c8452625f9..9aad89c6e44 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py @@ -10,9 +10,15 @@ import math from collections import defaultdict -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Literal, + TypeAlias, + cast, +) from hypothesis.internal.conjecture.choice import ( ChoiceNode, @@ -53,11 +59,10 @@ from hypothesis.internal.floats import MAX_PRECISE_INTEGER if TYPE_CHECKING: from random import Random - from typing import TypeAlias from hypothesis.internal.conjecture.engine import ConjectureRunner -ShrinkPredicateT: "TypeAlias" = Callable[[Union[ConjectureResult, _Overrun]], bool] +ShrinkPredicateT: TypeAlias = Callable[[ConjectureResult | _Overrun], bool] def sort_key(nodes: Sequence[ChoiceNode]) -> tuple[int, tuple[int, ...]]: @@ -87,7 +92,7 @@ def sort_key(nodes: Sequence[ChoiceNode]) -> tuple[int, tuple[int, ...]]: @dataclass class ShrinkPass: function: Any - name: Optional[str] = None + name: str | None = None last_prefix: Any = () # some execution statistics @@ -263,12 +268,12 @@ class Shrinker: def __init__( self, engine: "ConjectureRunner", - initial: Union[ConjectureData, ConjectureResult], - predicate: Optional[ShrinkPredicateT], + initial: ConjectureData | ConjectureResult, + predicate: ShrinkPredicateT | None, *, - allow_transition: Optional[ - Callable[[Union[ConjectureData, ConjectureResult], ConjectureData], bool] - ], + allow_transition: ( + Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None + ), explain: bool, in_target_phase: bool = False, ): @@ -320,7 +325,7 @@ class Shrinker: # 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. - self.__extend: Union[Literal["full"], int] = "full" if in_target_phase else 0 + self.__extend: Literal["full"] | int = "full" if in_target_phase else 0 self.should_explain = explain @derived_value # type: ignore @@ -353,7 +358,7 @@ class Shrinker: def cached_test_function( self, nodes: Sequence[ChoiceNode] - ) -> tuple[bool, Optional[Union[ConjectureResult, _Overrun]]]: + ) -> tuple[bool, ConjectureResult | _Overrun | None]: nodes = nodes[: len(self.nodes)] if startswith(nodes, self.nodes): @@ -518,7 +523,9 @@ class Shrinker: and endswith(result.nodes, nodes[end:]) ): # Turns out this was a variable-length part, so grab the infix... - for span1, span2 in zip(shrink_target.spans, result.spans): + for span1, span2 in zip( + shrink_target.spans, result.spans, strict=False + ): assert span1.start == span2.start assert span1.start <= start assert span1.label == span2.label @@ -551,7 +558,7 @@ class Shrinker: # However, it's really hard to write a simple and reliable covering # test, because of our `seen_passing_buffers` check above. break # pragma: no cover - elif self.__predicate(result): # pragma: no branch + if self.__predicate(result): # pragma: no branch n_same_failures += 1 if n_same_failures >= 100: self.shrink_target.slice_comments[(start, end)] = note @@ -582,7 +589,7 @@ class Shrinker: "The test sometimes passed when commented parts were varied together." ) break # Test passed, this param can't vary freely. - elif self.__predicate(result): # pragma: no branch + if self.__predicate(result): # pragma: no branch n_same_failures_together += 1 if n_same_failures_together >= 100: self.shrink_target.slice_comments[(0, 0)] = ( @@ -890,24 +897,28 @@ class Shrinker: self.distinct_labels, lambda l: len(self.spans_by_label[l]) >= 2 ) - ls = self.spans_by_label[label] - i = chooser.choose(range(len(ls) - 1)) - ancestor = ls[i] + spans = self.spans_by_label[label] + i = chooser.choose(range(len(spans) - 1)) + ancestor = spans[i] - if i + 1 == len(ls) or ls[i + 1].start >= ancestor.end: + if i + 1 == len(spans) or spans[i + 1].start >= ancestor.end: return @self.cached(label, i) def descendants(): lo = i + 1 - hi = len(ls) + hi = len(spans) while lo + 1 < hi: mid = (lo + hi) // 2 - if ls[mid].start >= ancestor.end: + if spans[mid].start >= ancestor.end: hi = mid else: lo = mid - return [t for t in ls[i + 1 : hi] if t.choice_count < ancestor.choice_count] + return [ + span + for span in spans[i + 1 : hi] + if span.choice_count < ancestor.choice_count + ] descendant = chooser.choose(descendants, lambda ex: ex.choice_count > 0) @@ -991,7 +1002,7 @@ class Shrinker: st.nodes, [ offset_node(node, sign * (n + v)) - for node, v in zip(changed, ints) + for node, v in zip(changed, ints, strict=False) ], ) ) @@ -1021,13 +1032,13 @@ class Shrinker: assert sort_key(new_target.nodes) < sort_key(prev_target.nodes) if len(prev_nodes) != len(new_nodes) or any( - n1.type != n2.type for n1, n2 in zip(prev_nodes, new_nodes) + n1.type != n2.type for n1, n2 in zip(prev_nodes, new_nodes, strict=True) ): # should we check constraints are equal as well? self.__all_changed_nodes = set() else: assert len(prev_nodes) == len(new_nodes) - for i, (n1, n2) in enumerate(zip(prev_nodes, new_nodes)): + for i, (n1, n2) in enumerate(zip(prev_nodes, new_nodes, strict=True)): assert n1.type == n2.type if not choice_equal(n1.value, n2.value): self.__all_changed_nodes.add(i) @@ -1347,13 +1358,24 @@ class Shrinker: and node1.index < node.index <= node1.index + 4, ) - m: Union[int, float] = node1.value - n: Union[int, float] = node2.value + m: int | float = node1.value + n: int | float = node2.value def boost(k: int) -> bool: - if k > m: + # floats always shrink towards 0 + shrink_towards = ( + node1.constraints["shrink_towards"] if node1.type == "integer" else 0 + ) + if k > abs(m - shrink_towards): return False + # We are trying to move node1 (m) closer to shrink_towards, and node2 + # (n) farther away from shrink_towards. If m is below shrink_towards, + # we want to add to m and subtract from n, and vice versa if above + # shrink_towards. + if m < shrink_towards: + k = -k + try: v1 = m - k v2 = n + k @@ -1364,7 +1386,7 @@ class Shrinker: # if we've increased node2 to the point that we're past max precision, # give up - things have become too unstable. - if node1.type == "float" and v2 >= MAX_PRECISE_INTEGER: + if node1.type == "float" and abs(v2) >= MAX_PRECISE_INTEGER: return False return self.consider_new_nodes( @@ -1700,7 +1722,7 @@ class Shrinker: v, st.nodes[spans[i].start : spans[i].end], ) - for (u, v), i in zip(endpoints, indices) + for (u, v), i in zip(endpoints, indices, strict=True) ], ) ), 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 c757a2e0466..7fd60bc67de 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/choicetree.py @@ -9,9 +9,8 @@ # obtain one at https://mozilla.org/MPL/2.0/. from collections import defaultdict -from collections.abc import Iterable, Sequence +from collections.abc import Callable, Iterable, Sequence from random import Random -from typing import Callable, Optional from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy @@ -146,8 +145,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: int | None = None + self.n: int | None = None @property def exhausted(self) -> bool: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/collection.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/collection.py index 4247221f7da..cd51eed2a07 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/collection.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/collection.py @@ -36,7 +36,7 @@ class Collection(Shrinker): return True # examine elements one by one from the left until an element differs. - for v1, v2 in zip(left, right): + for v1, v2 in zip(left, right, strict=False): if self.to_order(v1) == self.to_order(v2): continue return self.to_order(v1) < self.to_order(v2) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/integer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/integer.py index 06ba9c0564a..815d6e54dc0 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/integer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/integer.py @@ -61,7 +61,7 @@ class Integer(Shrinker): return self.consider(mask & base) @property - def size(self): + def size(self) -> int: return self.current.bit_length() def shrink_by_multiples(self, k): diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py index a6c71cfbff1..d27df88c83d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py @@ -14,13 +14,15 @@ import heapq import math import sys from collections import OrderedDict, abc -from collections.abc import Sequence +from collections.abc import Callable, Sequence from functools import lru_cache -from typing import TYPE_CHECKING, Optional, TypeVar, Union +from types import FunctionType +from typing import TYPE_CHECKING, TypeVar from hypothesis.errors import InvalidArgument from hypothesis.internal.compat import int_from_bytes from hypothesis.internal.floats import next_up +from hypothesis.internal.lambda_sources import _function_key if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData @@ -34,6 +36,20 @@ def calc_label_from_name(name: str) -> int: return int_from_bytes(hashed[:8]) +def calc_label_from_callable(f: Callable) -> int: + if isinstance(f, FunctionType): + return calc_label_from_hash(_function_key(f, ignore_name=True)) + elif isinstance(f, type): + return calc_label_from_cls(f) + else: + # probably an instance defining __call__ + try: + return calc_label_from_hash(f) + except Exception: + # not hashable + return calc_label_from_cls(type(f)) + + def calc_label_from_cls(cls: type) -> int: return calc_label_from_name(cls.__qualname__) @@ -62,7 +78,7 @@ def identity(v: T) -> T: def check_sample( - values: Union[type[enum.Enum], Sequence[T]], strategy_name: str + values: type[enum.Enum] | Sequence[T], strategy_name: str ) -> Sequence[T]: if "numpy" in sys.modules and isinstance(values, sys.modules["numpy"].ndarray): if values.ndim != 1: @@ -87,7 +103,9 @@ def check_sample( "Hypothesis treats earlier values as simpler." ) if isinstance(values, range): - return values + # Pyright is unhappy with every way I've tried to type-annotate this + # function, so fine, we'll just ignore the analysis error. + return values # type: ignore return tuple(values) @@ -186,7 +204,7 @@ class Sampler: self, data: "ConjectureData", *, - forced: Optional[int] = None, + forced: int | None = None, ) -> int: if self.observe: data.start_span(SAMPLE_IN_SAMPLER_LABEL) @@ -247,10 +265,10 @@ class many: self, data: "ConjectureData", min_size: int, - max_size: Union[int, float], - average_size: Union[int, float], + max_size: int | float, + average_size: int | float, *, - forced: Optional[int] = None, + forced: int | None = None, observe: bool = True, ) -> None: assert 0 <= min_size <= average_size <= max_size @@ -314,7 +332,7 @@ class many: self.stop_span() return False - def reject(self, why: Optional[str] = None) -> None: + def reject(self, why: str | None = None) -> None: """Reject the last example (i.e. don't count it towards our budget of elements because it's not going to go in the final collection).""" assert self.count > 0 @@ -334,7 +352,7 @@ SMALLEST_POSITIVE_FLOAT: float = next_up(0.0) or sys.float_info.min @lru_cache -def _calc_p_continue(desired_avg: float, max_size: Union[int, float]) -> float: +def _calc_p_continue(desired_avg: float, max_size: int | float) -> float: """Return the p_continue which will generate the desired average size.""" assert desired_avg <= max_size, (desired_avg, max_size) if desired_avg == max_size: @@ -372,7 +390,7 @@ def _calc_p_continue(desired_avg: float, max_size: Union[int, float]) -> float: return p_continue -def _p_continue_to_avg(p_continue: float, max_size: Union[int, float]) -> float: +def _p_continue_to_avg(p_continue: float, max_size: int | float) -> float: """Return the average_size generated by this p_continue and max_size.""" if p_continue >= 1: return max_size diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py b/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py index c297b45154b..bcc19590159 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/constants_ast.py @@ -19,17 +19,14 @@ from functools import lru_cache from itertools import chain from pathlib import Path from types import ModuleType -from typing import TYPE_CHECKING, Optional, Union +from typing import TypeAlias import hypothesis from hypothesis.configuration import storage_directory from hypothesis.internal.conjecture.choice import ChoiceTypeT from hypothesis.internal.escalation import is_hypothesis_file -if TYPE_CHECKING: - from typing import TypeAlias - -ConstantT: "TypeAlias" = Union[int, float, bytes, str] +ConstantT: TypeAlias = int | float | bytes | str # unfortunate collision with builtin. I don't want to name the init arg bytes_. bytesT = bytes @@ -39,10 +36,10 @@ 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, + integers: MutableSet[int] | None = None, + floats: MutableSet[float] | None = None, + bytes: MutableSet[bytes] | None = None, + strings: MutableSet[str] | None = None, ): self.integers: MutableSet[int] = set() if integers is None else integers self.floats: MutableSet[float] = set() if floats is None else floats @@ -50,8 +47,8 @@ class Constants: 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]]: + self, constant_type: type[ConstantT] | ChoiceTypeT + ) -> 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": @@ -178,7 +175,7 @@ class ConstantVisitor(NodeVisitor): self.generic_visit(node) -def _constants_from_source(source: Union[str, bytes], *, limit: bool) -> Constants: +def _constants_from_source(source: str | bytes, *, limit: bool) -> Constants: tree = ast.parse(source) visitor = ConstantVisitor(limit=limit) @@ -257,9 +254,7 @@ def is_local_module_file(path: str) -> bool: # 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) + path not 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 diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py b/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py index 5f219d64078..98cffed7b4a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/coverage.py @@ -11,8 +11,9 @@ import json import os import sys +from collections.abc import Callable from contextlib import contextmanager -from typing import Callable, TypeVar +from typing import TypeVar from hypothesis.internal.reflection import proxies diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py b/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py index e63694fbe05..0082eb2d88e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/entropy.py @@ -13,10 +13,10 @@ import gc import random import sys import warnings -from collections.abc import Generator, Hashable +from collections.abc import Callable, Generator, Hashable from itertools import count from random import Random -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any from weakref import WeakValueDictionary import hypothesis.core @@ -58,7 +58,7 @@ class NumpyRandomWrapper: self.setstate = numpy.random.set_state -NP_RANDOM: Optional[RandomLike] = None +NP_RANDOM: RandomLike | None = None if not (PYPY or GRAALPY): @@ -185,7 +185,6 @@ 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 # access .data.copy().items() instead of .items() to avoid a "dictionary # changed size during iteration" error under multithreading. diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py b/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py index 338088d2a8c..c29c846b770 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py @@ -13,11 +13,12 @@ import os import sys import textwrap import traceback +from collections.abc import Callable +from dataclasses import dataclass from functools import partial from inspect import getframeinfo from pathlib import Path from types import ModuleType, TracebackType -from typing import Callable, NamedTuple, Optional import hypothesis from hypothesis.errors import _Trimmable @@ -57,8 +58,8 @@ is_hypothesis_file = belongs_to(hypothesis) def get_trimmed_traceback( - exception: Optional[BaseException] = None, -) -> Optional[TracebackType]: + exception: BaseException | None = None, +) -> TracebackType | None: """Return the current traceback, minus any frames added by Hypothesis.""" if exception is None: _, exception, tb = sys.exc_info() @@ -90,7 +91,8 @@ def get_trimmed_traceback( return tb -class InterestingOrigin(NamedTuple): +@dataclass(frozen=True) +class InterestingOrigin: # The `interesting_origin` is how Hypothesis distinguishes between multiple # failures, for reporting and also to replay from the example database (even # if report_multiple_bugs=False). We traditionally use the exception type and @@ -98,8 +100,8 @@ class InterestingOrigin(NamedTuple): # blocks and understand the __cause__ (`raise x from y`) or __context__ that # first raised an exception as well as PEP-654 exception groups. exc_type: type[BaseException] - filename: Optional[str] - lineno: Optional[int] + filename: str | None + lineno: int | None context: "InterestingOrigin | tuple[()]" group_elems: "tuple[InterestingOrigin, ...]" diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py b/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py index f56e9b84b62..c92d7be83a6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/filtering.py @@ -26,18 +26,16 @@ import ast import inspect import math import operator -from collections.abc import Collection +from collections.abc import Callable, Collection from decimal import Decimal from fractions import Fraction from functools import partial -from typing import Any, Callable, NamedTuple, Optional, TypeVar +from typing import Any, NamedTuple, TypeVar from hypothesis.internal.compat import ceil, floor from hypothesis.internal.floats import next_down, next_up -from hypothesis.internal.reflection import ( - extract_lambda_source, - get_pretty_function_description, -) +from hypothesis.internal.lambda_sources import lambda_description +from hypothesis.internal.reflection import get_pretty_function_description Ex = TypeVar("Ex") Predicate = Callable[[Ex], bool] @@ -62,7 +60,7 @@ class ConstructivePredicate(NamedTuple): """ constraints: dict[str, Any] - predicate: Optional[Predicate] + predicate: Predicate | None @classmethod def unchanged(cls, predicate: Predicate) -> "ConstructivePredicate": @@ -191,7 +189,7 @@ def numeric_bounds_from_ast( ops = tree.ops vals = tree.comparators comparisons = [(tree.left, ops[0], vals[0])] - for i, (op, val) in enumerate(zip(ops[1:], vals[1:]), start=1): + for i, (op, val) in enumerate(zip(ops[1:], vals[1:], strict=True), start=1): comparisons.append((vals[i - 1], op, val)) bounds = [] for comp in comparisons: @@ -253,7 +251,7 @@ def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate: # and fall back to standard rejection sampling (a running theme). try: if predicate.__name__ == "<lambda>": - source = extract_lambda_source(predicate) + source = lambda_description(predicate) else: source = inspect.getsource(predicate) tree: ast.AST = ast.parse(source) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py index 70835193dcd..93f09de185e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py @@ -10,19 +10,15 @@ import math import struct +from collections.abc import Callable from sys import float_info -from typing import TYPE_CHECKING, Callable, Literal, SupportsFloat, Union +from typing import Literal, SupportsFloat, TypeAlias -if TYPE_CHECKING: - from typing import TypeAlias -else: - TypeAlias = object - -SignedIntFormat: "TypeAlias" = Literal["!h", "!i", "!q"] -UnsignedIntFormat: "TypeAlias" = Literal["!H", "!I", "!Q"] -IntFormat: "TypeAlias" = Union[SignedIntFormat, UnsignedIntFormat] -FloatFormat: "TypeAlias" = Literal["!e", "!f", "!d"] -Width: "TypeAlias" = Literal[16, 32, 64] +SignedIntFormat: TypeAlias = Literal["!h", "!i", "!q"] +UnsignedIntFormat: TypeAlias = Literal["!H", "!I", "!Q"] +IntFormat: TypeAlias = SignedIntFormat | UnsignedIntFormat +FloatFormat: TypeAlias = Literal["!e", "!f", "!d"] +Width: TypeAlias = Literal[16, 32, 64] # Format codes for (int, float) sized types, used for byte-wise casts. # See https://docs.python.org/3/library/struct.html#format-characters @@ -39,7 +35,7 @@ TO_SIGNED_FORMAT: dict[UnsignedIntFormat, SignedIntFormat] = { } -def reinterpret_bits(x: float, from_: str, to: str) -> float: +def reinterpret_bits(x: float | int, from_: str, to: str) -> float | int: x = struct.unpack(to, struct.pack(from_, x))[0] assert isinstance(x, (float, int)) return x @@ -187,7 +183,7 @@ def make_float_clamper( return float_clamper -def sign_aware_lte(x: float, y: float) -> bool: +def sign_aware_lte(x: float | int, y: float | int) -> bool: """Less-than-or-equals, but strictly orders -0.0 and 0.0""" if x == 0.0 == y: return math.copysign(1.0, x) <= math.copysign(1.0, y) @@ -195,7 +191,7 @@ def sign_aware_lte(x: float, y: float) -> bool: return x <= y -def clamp(lower: float, value: float, upper: float) -> float: +def clamp(lower: float | int, value: float | int, upper: float | int) -> float | int: """Given a value and lower/upper bounds, 'clamp' the value so that it satisfies lower <= value <= upper. NaN is mapped to lower.""" # this seems pointless (and is for integers), but handles the -0.0/0.0 case. diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py b/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py index 352c084b506..356abc48117 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/healthcheck.py @@ -18,11 +18,4 @@ def fail_health_check(settings, message, label): if label in settings.suppress_health_check: return - message += ( - "\nSee " - "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." - ) raise FailedHealthCheck(message) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py index 7753642dfca..ec2f3eb6e18 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py @@ -9,14 +9,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Union, cast, final +from typing import TYPE_CHECKING, TypeAlias, cast, final if TYPE_CHECKING: - from typing import TypeAlias - from typing_extensions import Self -IntervalsT: "TypeAlias" = tuple[tuple[int, int], ...] +IntervalsT: TypeAlias = tuple[tuple[int, int], ...] # @final makes mypy happy with the Self return annotations. We otherwise run @@ -92,7 +90,7 @@ class IntervalSet: assert r <= v return r - def __contains__(self, elem: Union[str, int]) -> bool: + def __contains__(self, elem: str | int) -> bool: if isinstance(elem, str): elem = ord(elem) assert 0 <= elem <= 0x10FFFF @@ -102,7 +100,7 @@ class IntervalSet: return f"IntervalSet({self.intervals!r})" def index(self, value: int) -> int: - for offset, (u, v) in zip(self.offsets, self.intervals): + for offset, (u, v) in zip(self.offsets, self.intervals, strict=True): if u == value: return offset elif u > value: @@ -112,7 +110,7 @@ class IntervalSet: raise ValueError(f"{value} is not in list") def index_above(self, value: int) -> int: - for offset, (u, v) in zip(self.offsets, self.intervals): + for offset, (u, v) in zip(self.offsets, self.intervals, strict=True): if u >= value: return offset if value <= v: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/lambda_sources.py b/contrib/python/hypothesis/py3/hypothesis/internal/lambda_sources.py new file mode 100644 index 00000000000..596b4c88106 --- /dev/null +++ b/contrib/python/hypothesis/py3/hypothesis/internal/lambda_sources.py @@ -0,0 +1,430 @@ +# 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 ast +import hashlib +import inspect +import linecache +import sys +import textwrap +from collections.abc import Callable, MutableMapping +from inspect import Parameter +from typing import Any +from weakref import WeakKeyDictionary + +from hypothesis.internal import reflection +from hypothesis.internal.cache import LRUCache + +# we have several levels of caching for lambda descriptions. +# * LAMBDA_DESCRIPTION_CACHE maps a lambda f to its description _lambda_description(f). +# Note that _lambda_description(f) may not be identical to f as it appears in the +# source code file. +# * LAMBDA_DIGEST_DESCRIPTION_CACHE maps _function_key(f) to _lambda_description(f). +# _function_key implements something close to "ast equality": +# two syntactically identical (minus whitespace etc) lambdas appearing in +# different files have the same key. Cache hits here provide a fast path which +# avoids ast-parsing syntactic lambdas we've seen before. Two lambdas with the +# same _function_key will not have different _lambda_descriptions - if +# they do, that's a bug here. +# * AST_LAMBDAS_CACHE maps source code lines to a list of the lambdas found in +# that source code. A cache hit here avoids reparsing the ast. +LAMBDA_DESCRIPTION_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary() +LAMBDA_DIGEST_DESCRIPTION_CACHE: LRUCache[tuple[Any], str] = LRUCache(max_size=1000) +AST_LAMBDAS_CACHE: LRUCache[tuple[str], list[ast.Lambda]] = LRUCache(max_size=100) + + +def extract_all_lambdas(tree): + lambdas = [] + + class Visitor(ast.NodeVisitor): + + def visit_Lambda(self, node): + lambdas.append(node) + self.visit(node.body) + + Visitor().visit(tree) + return lambdas + + +def extract_all_attributes(tree): + attributes = [] + + class Visitor(ast.NodeVisitor): + def visit_Attribute(self, node): + attributes.append(node) + self.visit(node.value) + + Visitor().visit(tree) + return attributes + + +def _function_key(f, *, bounded_size=False, ignore_name=False): + """Returns a digest that differentiates functions that have different sources. + + Either a function or a code object may be passed. If code object, default + arg/kwarg values are not recoverable - this is the best we can do, and is + sufficient for the use case of comparing nested lambdas. + """ + try: + code = f.__code__ + defaults_repr = repr((f.__defaults__, f.__kwdefaults__)) + except AttributeError: + code = f + defaults_repr = () + consts_repr = repr(code.co_consts) + if bounded_size: + # Compress repr to avoid keeping arbitrarily large strings pinned as cache + # keys. We don't do this unconditionally because hashing takes time, and is + # not necessary if the key is used just for comparison (and is not stored). + if len(consts_repr) > 48: + consts_repr = hashlib.sha384(consts_repr.encode()).digest() + if len(defaults_repr) > 48: + defaults_repr = hashlib.sha384(defaults_repr.encode()).digest() + return ( + consts_repr, + defaults_repr, + code.co_argcount, + code.co_kwonlyargcount, + code.co_code, + code.co_names, + code.co_varnames, + code.co_freevars, + ignore_name or code.co_name, + ) + + +class _op: + # Opcodes, from dis.opmap. These may change between major versions. + NOP = 9 + LOAD_FAST = 85 + LOAD_FAST_LOAD_FAST = 88 + LOAD_FAST_BORROW = 86 + LOAD_FAST_BORROW_LOAD_FAST_BORROW = 87 + + +def _normalize_code(f, l): + # A small selection of possible peephole code transformations, based on what + # is actually seen to differ between compilations in our test suite. Each + # entry contains two equivalent opcode sequences, plus a condition + # function called with their respective oparg sequences, which must return + # true for the transformation to be valid. + Checker = Callable[[list[int], list[int]], bool] + transforms: tuple[list[int], list[int], Checker | None] = [ + ([_op.NOP], [], lambda a, b: True), + ( + [_op.LOAD_FAST, _op.LOAD_FAST], + [_op.LOAD_FAST_LOAD_FAST], + lambda a, b: a == [b[0] >> 4, b[0] & 15], + ), + ( + [_op.LOAD_FAST_BORROW, _op.LOAD_FAST_BORROW], + [_op.LOAD_FAST_BORROW_LOAD_FAST_BORROW], + lambda a, b: a == [b[0] >> 4, b[0] & 15], + ), + ] + # augment with converse + transforms += [ + ( + ops_b, + ops_a, + condition and (lambda a, b, condition=condition: condition(b, a)), + ) + for ops_a, ops_b, condition in transforms + ] + + # Normalize equivalent code. We assume that each bytecode op is 2 bytes, + # which is the case since Python 3.6. Since the opcodes values may change + # between version, there is a risk that a transform may not be equivalent + # -- even so, the risk of a bad transform producing a false positive is + # minuscule. + co_code = list(l.__code__.co_code) + f_code = list(f.__code__.co_code) + + def alternating(code, i, n): + return code[i : i + 2 * n : 2] + + i = 2 + while i < max(len(co_code), len(f_code)): + # note that co_code is mutated in loop + if i < min(len(co_code), len(f_code)) and f_code[i] == co_code[i]: + i += 2 + else: + for op1, op2, condition in transforms: + if ( + op1 == alternating(f_code, i, len(op1)) + and op2 == alternating(co_code, i, len(op2)) + and condition( + alternating(f_code, i + 1, len(op1)), + alternating(co_code, i + 1, len(op2)), + ) + ): + break + else: + # no point in continuing since the bytecodes are different anyway + break + # Splice in the transform and continue + co_code = ( + co_code[:i] + f_code[i : i + 2 * len(op1)] + co_code[i + 2 * len(op2) :] + ) + i += 2 * len(op1) + + # Normalize consts, in particular replace any lambda consts with the + # corresponding const from the template function, IFF they have the same + # source key. + + f_consts = f.__code__.co_consts + l_consts = l.__code__.co_consts + if len(f_consts) == len(l_consts) and any( + inspect.iscode(l_const) for l_const in l_consts + ): + normalized_consts = [] + for f_const, l_const in zip(f_consts, l_consts, strict=True): + if ( + inspect.iscode(l_const) + and inspect.iscode(f_const) + and _function_key(f_const) == _function_key(l_const) + ): + # If the lambdas are compiled from the same source, make them be the + # same object so that the toplevel lambdas end up equal. Note that + # default arguments are not available on the code objects. But if the + # default arguments differ then the lambdas must also differ in other + # ways, since default arguments are set up from bytecode and constants. + # I.e., this appears to be safe wrt false positives. + normalized_consts.append(f_const) + else: + normalized_consts.append(l_const) + else: + normalized_consts = l_consts + + return l.__code__.replace( + co_code=bytes(co_code), + co_consts=tuple(normalized_consts), + ) + + +_module_map: dict[int, str] = {} + + +def _mimic_lambda_from_node(f, node): + # Compile the source (represented by an ast.Lambda node) in a context that + # as far as possible mimics the context that f was compiled in. If - and + # only if - this was the source of f then the result is indistinguishable + # from f itself (to a casual observer such as _function_key). + f_globals = f.__globals__.copy() + f_code = f.__code__ + source = ast.unparse(node) + + # Install values for non-literal argument defaults. Thankfully, these are + # always captured by value - so there is no interaction with the closure. + if f.__defaults__: + for f_default, l_default in zip( + f.__defaults__, node.args.defaults, strict=True + ): + if isinstance(l_default, ast.Name): + f_globals[l_default.id] = f_default + if f.__kwdefaults__: # pragma: no cover + for l_default, l_varname in zip( + node.args.kw_defaults, node.args.kwonlyargs, strict=True + ): + if isinstance(l_default, ast.Name): + f_globals[l_default.id] = f.__kwdefaults__[l_varname.arg] + + # CPython's compiler treats known imports differently than normal globals, + # so check if we use attributes from globals that are modules (if so, we + # import them explicitly and redundantly in the exec below) + referenced_modules = [ + (local_name, module) + for attr in extract_all_attributes(node) + if ( + isinstance(attr.value, ast.Name) + and (local_name := attr.value.id) + and inspect.ismodule(module := f_globals.get(local_name)) + ) + ] + + if not f_code.co_freevars and not referenced_modules: + compiled = eval(source, f_globals) + else: + if f_code.co_freevars: + # We have to reconstruct a local closure. The closure will have + # the same values as the original function, although this is not + # required for source/bytecode equality. + f_globals |= { + f"__lc{i}": c.cell_contents for i, c in enumerate(f.__closure__) + } + captures = [f"{name}=__lc{i}" for i, name in enumerate(f_code.co_freevars)] + capture_str = ";".join(captures) + ";" + else: + capture_str = "" + if referenced_modules: + # We add import statements for all referenced modules, since that + # influences the compiled code. The assumption is that these modules + # were explicitly imported, not assigned, in the source - if not, + # this may/will give a different compilation result. + global _module_map + if len(_module_map) != len(sys.modules): # pragma: no branch + _module_map = {id(module): name for name, module in sys.modules.items()} + imports = [ + (module_name, local_name) + for local_name, module in referenced_modules + if (module_name := _module_map.get(id(module))) is not None + ] + import_fragments = [f"{name} as {asname}" for name, asname in set(imports)] + import_str = f"import {','.join(import_fragments)}\n" + else: + import_str = "" + exec_str = ( + f"{import_str}def __construct_lambda(): {capture_str} return ({source})" + ) + exec(exec_str, f_globals) + compiled = f_globals["__construct_lambda"]() + + return compiled + + +def _lambda_code_matches_node(f, node): + try: + compiled = _mimic_lambda_from_node(f, node) + except (NameError, SyntaxError): # pragma: no cover # source is generated from ast + return False + if _function_key(f) == _function_key(compiled): + return True + # Try harder + compiled.__code__ = _normalize_code(f, compiled) + return _function_key(f) == _function_key(compiled) + + +def _check_unknown_perfectly_aligned_lambda(candidate): + # This is a monkeypatch point for our self-tests, to make unknown + # lambdas raise. + pass + + +def _lambda_description(f, leeway=50, *, fail_if_confused_with_perfect_candidate=False): + if hasattr(f, "__wrapped_target"): + f = f.__wrapped_target + + # You might be wondering how a lambda can have a return-type annotation? + # The answer is that we add this at runtime, in new_given_signature(), + # and we do support strange choices as applying @given() to a lambda. + sig = inspect.signature(f) + assert sig.return_annotation in (Parameter.empty, None), sig + + # Using pytest-xdist on Python 3.13, there's an entry in the linecache for + # file "<string>", which then returns nonsense to getsource. Discard it. + linecache.cache.pop("<string>", None) + + def format_lambda(body): + # The signature is more informative than the corresponding ast.unparse + # output in the case of default argument values, so add the signature + # to the unparsed body + return ( + f"lambda {str(sig)[1:-1]}: {body}" if sig.parameters else f"lambda: {body}" + ) + + if_confused = format_lambda("<unknown>") + + try: + source_lines, lineno0 = inspect.findsource(f) + source_lines = tuple(source_lines) # make it hashable + except OSError: + return if_confused + + try: + all_lambdas = AST_LAMBDAS_CACHE[source_lines] + except KeyError: + # The source isn't already parsed, so we try to shortcut by parsing just + # the local block. If that fails to produce a code-identical lambda, + # fall through to the full parse. + local_lines = inspect.getblock(source_lines[lineno0:]) + local_block = textwrap.dedent("".join(local_lines)) + # The fairly common ".map(lambda x: ...)" case. This partial block + # isn't valid syntax, but it might be if we remove the leading ".". + local_block = local_block.removeprefix(".") + + try: + local_tree = ast.parse(local_block) + except SyntaxError: + pass + else: + local_lambdas = extract_all_lambdas(local_tree) + for candidate in local_lambdas: + if reflection.ast_arguments_matches_signature( + candidate.args, sig + ) and _lambda_code_matches_node(f, candidate): + return format_lambda(ast.unparse(candidate.body)) + + # Local parse failed or didn't produce a match, go ahead with the full parse + try: + tree = ast.parse("".join(source_lines)) + except SyntaxError: + all_lambdas = [] + else: + all_lambdas = extract_all_lambdas(tree) + AST_LAMBDAS_CACHE[source_lines] = all_lambdas + + aligned_lambdas = [] + for candidate in all_lambdas: + if ( + candidate.lineno - leeway <= lineno0 + 1 <= candidate.lineno + leeway + and reflection.ast_arguments_matches_signature(candidate.args, sig) + ): + aligned_lambdas.append(candidate) + + aligned_lambdas.sort(key=lambda c: abs(lineno0 + 1 - c.lineno)) + for candidate in aligned_lambdas: + if _lambda_code_matches_node(f, candidate): + return format_lambda(ast.unparse(candidate.body)) + + # None of the aligned lambdas match perfectly in generated code. + if aligned_lambdas and aligned_lambdas[0].lineno == lineno0 + 1: + _check_unknown_perfectly_aligned_lambda(aligned_lambdas[0]) + + return if_confused + + +def lambda_description(f): + """ + Returns a syntactically-valid expression describing `f`. This is often, but + not always, the exact lambda definition string which appears in the source code. + The difference comes from parsing the lambda ast into `tree` and then returning + the result of `ast.unparse(tree)`, which may differ in whitespace, double vs + single quotes, etc. + + Returns a string indicating an unknown body if the parsing gets confused in any way. + """ + try: + return LAMBDA_DESCRIPTION_CACHE[f] + except KeyError: + pass + + key = _function_key(f, bounded_size=True) + location = (f.__code__.co_filename, f.__code__.co_firstlineno) + try: + description, failed_locations = LAMBDA_DIGEST_DESCRIPTION_CACHE[key] + except KeyError: + failed_locations = set() + else: + # We got a hit in the digests cache, but only use it if either it has + # a good (known) description, or if it is unknown but we already tried + # to parse its exact source location before. + if "<unknown>" not in description or location in failed_locations: + # use the cached result + LAMBDA_DESCRIPTION_CACHE[f] = description + return description + + description = _lambda_description(f) + LAMBDA_DESCRIPTION_CACHE[f] = description + if "<unknown>" in description: + failed_locations.add(location) + else: + failed_locations.clear() # we have a good description now + LAMBDA_DIGEST_DESCRIPTION_CACHE[key] = description, failed_locations + return description diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py index c594ced5f7e..0310967132a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/observability.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/observability.py @@ -16,14 +16,24 @@ import json import math import os import sys +import threading import time import warnings -from collections.abc import Generator +from collections.abc import Callable, Generator from contextlib import contextmanager from dataclasses import dataclass from datetime import date, timedelta from functools import lru_cache -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast +from threading import Lock +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Optional, + TypeAlias, + Union, + cast, +) from hypothesis.configuration import storage_directory from hypothesis.errors import HypothesisWarning @@ -43,11 +53,21 @@ 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 +Observation: TypeAlias = Union["InfoObservation", "TestCaseObservation"] +CallbackThreadT: TypeAlias = Callable[[Observation], None] +# for all_threads=True, we pass the thread id as well. +CallbackAllThreadsT: TypeAlias = Callable[[Observation, int], None] +CallbackT: TypeAlias = CallbackThreadT | CallbackAllThreadsT + +# thread_id: list[callback] +_callbacks: dict[int | None, list[CallbackThreadT]] = {} +# callbacks where all_threads=True was set +_callbacks_all_threads: list[CallbackAllThreadsT] = [] + + @dataclass class PredicateCounts: satisfied: int = 0 @@ -60,7 +80,7 @@ class PredicateCounts: self.unsatisfied += 1 -def _choice_to_json(choice: Union[ChoiceT, None]) -> Any: +def _choice_to_json(choice: ChoiceT | None) -> Any: if choice is None: return None # see the note on the same check in to_jsonable for why we cast large @@ -149,16 +169,17 @@ def nodes_to_json(nodes: tuple[ChoiceNode, ...]) -> list[dict[str, Any]]: @dataclass class ObservationMetadata: - traceback: Optional[str] - reproduction_decorator: Optional[str] + traceback: str | None + reproduction_decorator: str | None 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, ...]] + phase: str + interesting_origin: InterestingOrigin | None + choice_nodes: tuple[ChoiceNode, ...] | None choice_spans: Optional["Spans"] def to_json(self) -> dict[str, Any]: @@ -171,6 +192,7 @@ class ObservationMetadata: "os.getpid()": self.os_getpid, "imported_at": self.imported_at, "data_status": self.data_status, + "phase": self.phase, "interesting_origin": self.interesting_origin, "choice_nodes": ( None if self.choice_nodes is None else nodes_to_json(self.choice_nodes) @@ -181,7 +203,7 @@ class ObservationMetadata: else [ ( # span.label is an int, but cast to string to avoid conversion - # to float (and loss of precision) for large label values. + # 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. @@ -214,7 +236,7 @@ TestCaseStatus = Literal["gave_up", "passed", "failed"] class InfoObservation(BaseObservation): type: InfoObservationType title: str - content: Union[str, dict] + content: str | dict @dataclass @@ -228,38 +250,145 @@ class TestCaseObservation(BaseObservation): arguments: dict how_generated: str features: dict - coverage: Optional[dict[str, list[int]]] + coverage: dict[str, list[int]] | None timing: dict[str, float] metadata: ObservationMetadata -Observation: "TypeAlias" = Union[InfoObservation, TestCaseObservation] +def add_observability_callback(f: CallbackT, /, *, all_threads: bool = False) -> None: + """ + Adds ``f`` as a callback for :ref:`observability <observability>`. ``f`` + should accept one argument, which is an observation. Whenever Hypothesis + produces a new observation, it calls each callback with that observation. + + If Hypothesis tests are being run from multiple threads, callbacks are tracked + per-thread. In other words, ``add_observability_callback(f)`` only adds ``f`` + as an observability callback for observations produced on that thread. + + If ``all_threads=True`` is passed, ``f`` will instead be registered as a + callback for all threads. This means it will be called for observations + generated by all threads, not just the thread which registered ``f`` as a + callback. In this case, ``f`` will be passed two arguments: the first is the + observation, and the second is the integer thread id from + :func:`python:threading.get_ident` where that observation was generated. + + We recommend against registering ``f`` as a callback for both ``all_threads=True`` + and the default ``all_threads=False``, due to unclear semantics with + |remove_observability_callback|. + """ + if all_threads: + _callbacks_all_threads.append(cast(CallbackAllThreadsT, f)) + return + + thread_id = threading.get_ident() + if thread_id not in _callbacks: + _callbacks[thread_id] = [] + + _callbacks[thread_id].append(cast(CallbackThreadT, f)) + + +def remove_observability_callback(f: CallbackT, /) -> None: + """ + Removes ``f`` from the :ref:`observability <observability>` callbacks. + + If ``f`` is not in the list of observability callbacks, silently do nothing. + + If running under multiple threads, ``f`` will only be removed from the + callbacks for this thread. + """ + if f in _callbacks_all_threads: + _callbacks_all_threads.remove(cast(CallbackAllThreadsT, f)) + + thread_id = threading.get_ident() + if thread_id not in _callbacks: + return + + callbacks = _callbacks[thread_id] + if f in callbacks: + callbacks.remove(cast(CallbackThreadT, f)) + + if not callbacks: + del _callbacks[thread_id] -#: 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]] = [] + +def observability_enabled() -> bool: + """ + Returns whether or not Hypothesis considers :ref:`observability <observability>` + to be enabled. Observability is enabled if there is at least one observability + callback present. + + Callers might use this method to determine whether they should compute an + expensive representation that is only used under observability, for instance + by |alternative backends|. + """ + return bool(_callbacks) or bool(_callbacks_all_threads) @contextmanager -def with_observation_callback( - callback: Callable[[Observation], None], +def with_observability_callback( + f: Callable[[Observation], None], /, *, all_threads: bool = False ) -> Generator[None, None, None]: - TESTCASE_CALLBACKS.append(callback) + """ + A simple context manager which calls |add_observability_callback| on ``f`` + when it enters and |remove_observability_callback| on ``f`` when it exits. + """ + add_observability_callback(f, all_threads=all_threads) try: yield finally: - TESTCASE_CALLBACKS.remove(callback) + remove_observability_callback(f) def deliver_observation(observation: Observation) -> None: - for callback in TESTCASE_CALLBACKS: + thread_id = threading.get_ident() + + for callback in _callbacks.get(thread_id, []): callback(observation) + for callback in _callbacks_all_threads: + callback(observation, thread_id) + + +class _TestcaseCallbacks: + def __bool__(self): + self._note_deprecation() + return bool(_callbacks) + + def _note_deprecation(self): + from hypothesis._settings import note_deprecation + + note_deprecation( + "hypothesis.internal.observability.TESTCASE_CALLBACKS is deprecated. " + "Replace TESTCASE_CALLBACKS.append with add_observability_callback, " + "TESTCASE_CALLBACKS.remove with remove_observability_callback, and " + "bool(TESTCASE_CALLBACKS) with observability_enabled().", + since="2025-08-01", + has_codemod=False, + ) + + def append(self, f): + self._note_deprecation() + add_observability_callback(f) + + def remove(self, f): + self._note_deprecation() + remove_observability_callback(f) + + +#: .. warning:: +#: +#: Deprecated in favor of |add_observability_callback|, +#: |remove_observability_callback|, and |observability_enabled|. +#: +#: |TESTCASE_CALLBACKS| remains a thin compatibility +#: shim which forwards ``.append``, ``.remove``, and ``bool()`` to those +#: three methods. It is not an attempt to be fully compatible with the previous +#: ``TESTCASE_CALLBACKS = []``, so iteration or other usages will not work +#: anymore. Please update to using the new methods instead. +#: +#: |TESTCASE_CALLBACKS| will eventually be removed. +TESTCASE_CALLBACKS = _TestcaseCallbacks() + def make_testcase( *, @@ -268,18 +397,18 @@ def make_testcase( data: "ConjectureData", how_generated: str, 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, - status: Optional[ - Union[TestCaseStatus, "Status"] - ] = None, # overrides automatic calculation - status_reason: Optional[str] = None, # overrides automatic calculation + arguments: dict | None = None, + coverage: dict[str, list[int]] | None = None, + phase: str | None = None, + backend_metadata: dict[str, Any] | None = None, + status: ( + Union[TestCaseStatus, "Status"] | None + ) = None, # overrides automatic calculation + status_reason: str | None = None, # overrides automatic calculation # added to calculated metadata. If keys overlap, the value from this `metadata` # is used - metadata: Optional[dict[str, Any]] = None, + metadata: dict[str, Any] | None = None, ) -> TestCaseObservation: from hypothesis.core import reproduction_decorator from hypothesis.internal.conjecture.data import Status @@ -335,6 +464,7 @@ def make_testcase( "predicates": dict(data._observability_predicates), "backend": backend_metadata or {}, "data_status": data.status, + "phase": phase, "interesting_origin": data.interesting_origin, "choice_nodes": data.nodes if OBSERVABILITY_CHOICES else None, "choice_spans": data.spans if OBSERVABILITY_CHOICES else None, @@ -349,17 +479,31 @@ def make_testcase( _WROTE_TO = set() +_deliver_to_file_lock = Lock() -def _deliver_to_file(observation: Observation) -> None: # pragma: no cover +def _deliver_to_file( + observation: Observation, thread_id: int +) -> 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(to_jsonable(observation, avoid_realization=False)) + "\n") + + observation_bytes = ( + json.dumps(to_jsonable(observation, avoid_realization=False)) + "\n" + ) + # only allow one conccurent file write to avoid write races. This is likely to make + # HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY quite slow under threading. A queue + # would be an improvement, but that requires a background thread, and I + # would prefer to avoid a thread in the single-threaded case. We could + # switch over to a queue if we detect multithreading, but it's tricky to get + # right. + with _deliver_to_file_lock: + _WROTE_TO.add(fname) + with fname.open(mode="a") as f: + f.write(observation_bytes) _imported_at = time.time() @@ -411,7 +555,7 @@ if ( "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY" in os.environ or OBSERVABILITY_COLLECT_COVERAGE is False ): # pragma: no cover - TESTCASE_CALLBACKS.append(_deliver_to_file) + add_observability_callback(_deliver_to_file, all_threads=True) # Remove files more than a week old, to cap the size on disk max_age = (date.today() - timedelta(days=8)).isoformat() diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py b/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py index 6a7a0551635..87bf67a3e74 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/reflection.py @@ -14,27 +14,24 @@ to really unreasonable lengths to produce pretty output.""" import ast import hashlib import inspect -import linecache -import os import re -import sys import textwrap import types import warnings -from collections.abc import MutableMapping, Sequence +from collections.abc import Callable, Sequence from functools import partial, wraps from inspect import Parameter, Signature from io import StringIO 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 TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union +from tokenize import COMMENT, generate_tokens, untokenize +from types import EllipsisType, ModuleType +from typing import TYPE_CHECKING, Any, TypeVar, Union from unittest.mock import _patch as PatchType -from weakref import WeakKeyDictionary from hypothesis.errors import HypothesisWarning -from hypothesis.internal.compat import EllipsisType, is_typed_named_tuple +from hypothesis.internal import lambda_sources +from hypothesis.internal.compat import is_typed_named_tuple from hypothesis.utils.conventions import not_set from hypothesis.vendor.pretty import pretty @@ -43,9 +40,6 @@ if TYPE_CHECKING: T = TypeVar("T") -READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True" -LAMBDA_SOURCE_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary() - def is_mock(obj: object) -> bool: """Determine if the given argument is a mock type.""" @@ -165,15 +159,7 @@ def get_signature( parameters=[v for k, v in sig.parameters.items() if k != "self"] ) return sig - # eval_str is only supported by Python 3.10 and newer - if sys.version_info[:2] >= (3, 10): - sig = inspect.signature( - target, follow_wrapped=follow_wrapped, eval_str=eval_str - ) - else: - sig = inspect.signature( - target, follow_wrapped=follow_wrapped - ) # pragma: no cover + sig = inspect.signature(target, follow_wrapped=follow_wrapped, eval_str=eval_str) check_signature(sig) return sig @@ -188,7 +174,7 @@ def arg_is_required(param: Parameter) -> bool: def required_args( target: Callable[..., Any], args: tuple["SearchStrategy[Any]", ...] = (), - kwargs: Optional[dict[str, Union["SearchStrategy[Any]", EllipsisType]]] = None, + kwargs: dict[str, Union["SearchStrategy[Any]", EllipsisType]] | None = None, ) -> set[str]: """Return a set of names of required args to target that were not supplied in args or kwargs. @@ -281,177 +267,6 @@ def is_first_param_referenced_in_function(f: Any) -> bool: ) -def extract_all_lambdas(tree, matching_signature): - lambdas = [] - - class Visitor(ast.NodeVisitor): - def visit_Lambda(self, node): - if ast_arguments_matches_signature(node.args, matching_signature): - lambdas.append(node) - - Visitor().visit(tree) - - return lambdas - - -LINE_CONTINUATION = re.compile(r"\\\n") -WHITESPACE = re.compile(r"\s+") -PROBABLY_A_COMMENT = re.compile("""#[^'"]*$""") -SPACE_FOLLOWS_OPEN_BRACKET = re.compile(r"\( ") -SPACE_PRECEDES_CLOSE_BRACKET = re.compile(r" \)") - - -def _extract_lambda_source(f): - """Extracts a single lambda expression from the string source. Returns a - string indicating an unknown body if it gets confused in any way. - - This is not a good function and I am sorry for it. Forgive me my - sins, oh lord - """ - # You might be wondering how a lambda can have a return-type annotation? - # The answer is that we add this at runtime, in new_given_signature(), - # and we do support strange choices as applying @given() to a lambda. - sig = inspect.signature(f) - assert sig.return_annotation in (Parameter.empty, None), sig - - # Using pytest-xdist on Python 3.13, there's an entry in the linecache for - # file "<string>", which then returns nonsense to getsource. Discard it. - linecache.cache.pop("<string>", None) - - if sig.parameters: - if_confused = f"lambda {str(sig)[1:-1]}: <unknown>" - else: - if_confused = "lambda: <unknown>" - try: - source = inspect.getsource(f) - except OSError: - return if_confused - - source = LINE_CONTINUATION.sub(" ", source) - source = WHITESPACE.sub(" ", source) - source = source.strip() - 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 - - try: - tree = ast.parse(source) - except SyntaxError: - for i in range(len(source) - 1, len("lambda"), -1): - prefix = source[:i] - if "lambda" not in prefix: - break - try: - tree = ast.parse(prefix) - source = prefix - break - except SyntaxError: - continue - if tree is None and source.startswith(("@", ".")): - # This will always eventually find a valid expression because the - # decorator or chained operator must be a valid Python function call, - # so will eventually be syntactically valid and break out of the loop. - # Thus, this loop can never terminate normally. - for i in range(len(source) + 1): - p = source[1:i] - if "lambda" in p: - try: - tree = ast.parse(p) - source = p - break - except SyntaxError: - pass - else: - raise NotImplementedError("expected to be unreachable") - - if tree is None: - return if_confused - - aligned_lambdas = extract_all_lambdas(tree, matching_signature=sig) - if len(aligned_lambdas) != 1: - return if_confused - lambda_ast = aligned_lambdas[0] - assert lambda_ast.lineno == 1 - - # If the source code contains Unicode characters, the bytes of the original - # file don't line up with the string indexes, and `col_offset` doesn't match - # the string we're using. We need to convert the source code into bytes - # before slicing. - # - # Under the hood, the inspect module is using `tokenize.detect_encoding` to - # detect the encoding of the original source file. We'll use the same - # approach to get the source code as bytes. - # - # See https://github.com/HypothesisWorks/hypothesis/issues/1700 for an - # example of what happens if you don't correct for this. - # - # Note: if the code doesn't come from a file (but, for example, a doctest), - # `getsourcefile` will return `None` and the `open()` call will fail with - # an OSError. Or if `f` is a built-in function, in which case we get a - # TypeError. In both cases, fall back to splitting the Unicode string. - # It's not perfect, but it's the best we can do. - try: - with open(inspect.getsourcefile(f), "rb") as src_f: - encoding, _ = detect_encoding(src_f.readline) - - source_bytes = source.encode(encoding) - source_bytes = source_bytes[lambda_ast.col_offset :].strip() - source = source_bytes.decode(encoding) - except (OSError, TypeError): - source = source[lambda_ast.col_offset :].strip() - - # This ValueError can be thrown in Python 3 if: - # - # - There's a Unicode character in the line before the Lambda, and - # - For some reason we can't detect the source encoding of the file - # - # because slicing on `lambda_ast.col_offset` will account for bytes, but - # the slice will be on Unicode characters. - # - # In practice this seems relatively rare, so we just give up rather than - # trying to recover. - try: - source = source[source.index("lambda") :] - except ValueError: - return if_confused - - for i in range(len(source), len("lambda"), -1): # pragma: no branch - try: - parsed = ast.parse(source[:i]) - assert len(parsed.body) == 1 - assert parsed.body - if isinstance(parsed.body[0].value, ast.Lambda): - source = source[:i] - break - except SyntaxError: - pass - lines = source.split("\n") - lines = [PROBABLY_A_COMMENT.sub("", l) for l in lines] - source = "\n".join(lines) - - source = WHITESPACE.sub(" ", source) - source = SPACE_FOLLOWS_OPEN_BRACKET.sub("(", source) - source = SPACE_PRECEDES_CLOSE_BRACKET.sub(")", source) - return source.strip() - - -def extract_lambda_source(f): - try: - return LAMBDA_SOURCE_CACHE[f] - except KeyError: - pass - - source = _extract_lambda_source(f) - LAMBDA_SOURCE_CACHE[f] = source - return source - - def get_pretty_function_description(f: object) -> str: if isinstance(f, partial): return pretty(f) @@ -459,7 +274,7 @@ def get_pretty_function_description(f: object) -> str: return repr(f) name = f.__name__ # type: ignore if name == "<lambda>": - return extract_lambda_source(f) + return lambda_sources.lambda_description(f) elif isinstance(f, (types.MethodType, types.BuiltinMethodType)): self = f.__self__ # Some objects, like `builtins.abs` are of BuiltinMethodType but have @@ -549,7 +364,7 @@ def accept({funcname}): def get_varargs( sig: Signature, kind: int = Parameter.VAR_POSITIONAL -) -> Optional[Parameter]: +) -> Parameter | None: for p in sig.parameters.values(): if p.kind is kind: return p @@ -663,6 +478,9 @@ def impersonate(target): f.__module__ = target.__module__ f.__doc__ = target.__doc__ f.__globals__["__hypothesistracebackhide__"] = True + # But leave an breadcrumb for _describe_lambda to follow, it's + # just confused by the lies above + f.__wrapped_target = target return f return accept @@ -681,6 +499,31 @@ def proxies(target: T) -> Callable[[Callable], T]: return accept -def is_identity_function(f: object) -> bool: - # TODO: pattern-match the AST to handle `def ...` identity functions too - return bool(re.fullmatch(r"lambda (\w+): \1", get_pretty_function_description(f))) +def is_identity_function(f: Callable) -> bool: + try: + code = f.__code__ + except AttributeError: + try: + f = f.__call__ # type: ignore + code = f.__code__ + except AttributeError: + return False + + # We only accept a single unbound argument. While it would be possible to + # accept extra defaulted arguments, it would be pointless as they couldn't + # be referenced at all in the code object (or the co_code check would fail). + bound_args = int(inspect.ismethod(f)) + if code.co_argcount != bound_args + 1 or code.co_kwonlyargcount > 0: + return False + + # We know that f accepts a single positional argument, now check that its + # code object is simply "return first unbound argument". + template = (lambda self, x: x) if bound_args else (lambda x: x) # type: ignore + try: + return code.co_code == template.__code__.co_code + except AttributeError: # pragma: no cover # pypy only + # In PyPy, some builtin functions have a code object ('builtin-code') + # lacking co_code, perhaps because they are native-compiled and don't have + # a corresponding bytecode. Regardless, since Python doesn't have any + # builtin identity function it seems safe to say that this one isn't + return False diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py index af65613091c..a4665d13752 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py @@ -21,18 +21,15 @@ from enum import IntEnum from functools import lru_cache, reduce from os import sep from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TypeAlias from hypothesis._settings import Phase, Verbosity from hypothesis.internal.compat import PYPY from hypothesis.internal.escalation import is_hypothesis_file -if TYPE_CHECKING: - from typing import TypeAlias - -Location: "TypeAlias" = tuple[str, int] -Branch: "TypeAlias" = tuple[Optional[Location], Location] -Trace: "TypeAlias" = set[Branch] +Location: TypeAlias = tuple[str, int] +Branch: TypeAlias = tuple[Location | None, Location] +Trace: TypeAlias = set[Branch] @functools.cache @@ -47,7 +44,7 @@ def should_trace_file(fname: str) -> bool: # tool_id = 1 is designated for coverage, but we intentionally choose a # non-reserved tool id so we can co-exist with coverage tools. MONITORING_TOOL_ID = 3 -if sys.version_info[:2] >= (3, 12): +if hasattr(sys, "monitoring"): MONITORING_EVENTS = {sys.monitoring.events.LINE: "trace_line"} @@ -63,19 +60,17 @@ class Tracer: def __init__(self, *, should_trace: bool) -> None: self.branches: Trace = set() - self._previous_location: Optional[Location] = None + self._previous_location: Location | None = None self._tried_and_failed_to_trace = False self._should_trace = should_trace and self.can_trace() @staticmethod def can_trace() -> bool: - return ( - (sys.version_info[:2] < (3, 12) and sys.gettrace() is None) - or ( - sys.version_info[:2] >= (3, 12) - and sys.monitoring.get_tool(MONITORING_TOOL_ID) is None - ) - ) and not PYPY + if PYPY: + return False + if hasattr(sys, "monitoring"): + return sys.monitoring.get_tool(MONITORING_TOOL_ID) is None + return sys.gettrace() is None def trace(self, frame, event, arg): try: @@ -107,7 +102,7 @@ class Tracer: if not self._should_trace: return self - if sys.version_info[:2] < (3, 12): + if not hasattr(sys, "monitoring"): sys.settrace(self.trace) return self @@ -130,7 +125,7 @@ class Tracer: if not self._should_trace: return - if sys.version_info[:2] < (3, 12): + if not hasattr(sys, "monitoring"): sys.settrace(None) return @@ -156,8 +151,9 @@ UNHELPFUL_LOCATIONS = ( # Quite rarely, the first AFNP line is in Pytest's internals. "/_pytest/**", "/pluggy/_*.py", - # used by pytest for failure formatting in the terminal - "/pygments/lexer.py", + # used by pytest for failure formatting in the terminal. + # seen: pygments/lexer.py, pygments/formatters/, pygments/filter.py. + "/pygments/*", # used by pytest for failure formatting "/difflib.py", "/reprlib.py", diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/validation.py b/contrib/python/hypothesis/py3/hypothesis/internal/validation.py index 9cf9df57fcb..9266cb3a079 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/validation.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/validation.py @@ -11,14 +11,13 @@ import decimal import math from numbers import Rational, Real -from typing import Union from hypothesis.errors import InvalidArgument from hypothesis.internal.coverage import check_function @check_function -def check_type(typ: Union[type, tuple[type, ...]], arg: object, name: str) -> None: +def check_type(typ: type | tuple[type, ...], arg: object, name: str) -> None: if not isinstance(arg, typ): if isinstance(typ, tuple): assert len(typ) >= 2, "Use bare type instead of len-1 tuple" diff --git a/contrib/python/hypothesis/py3/hypothesis/provisional.py b/contrib/python/hypothesis/py3/hypothesis/provisional.py index 47f63d75737..8becf9269af 100644 --- a/contrib/python/hypothesis/py3/hypothesis/provisional.py +++ b/contrib/python/hypothesis/py3/hypothesis/provisional.py @@ -21,7 +21,6 @@ definitions it links to. If not, report the bug! import string from functools import lru_cache from importlib import resources -from typing import Optional from hypothesis import strategies as st from hypothesis.errors import InvalidArgument @@ -60,7 +59,7 @@ def _recase_randomly(draw: DrawFn, tld: str) -> str: class DomainNameStrategy(st.SearchStrategy[str]): @staticmethod def clean_inputs( - minimum: int, maximum: int, value: Optional[int], variable_name: str + minimum: int, maximum: int, value: int | None, variable_name: str ) -> int: if value is None: value = maximum @@ -75,7 +74,7 @@ class DomainNameStrategy(st.SearchStrategy[str]): return value def __init__( - self, max_length: Optional[int] = None, max_element_length: Optional[int] = None + self, max_length: int | None = None, max_element_length: int | None = None ) -> None: """ A strategy for :rfc:`1035` fully qualified domain names. diff --git a/contrib/python/hypothesis/py3/hypothesis/reporting.py b/contrib/python/hypothesis/py3/hypothesis/reporting.py index b1693e45b97..0f0af3f7687 100644 --- a/contrib/python/hypothesis/py3/hypothesis/reporting.py +++ b/contrib/python/hypothesis/py3/hypothesis/reporting.py @@ -8,16 +8,14 @@ # 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 Callable from contextlib import AbstractContextManager -from typing import TYPE_CHECKING, Callable +from typing import TypeAlias from hypothesis._settings import Verbosity, settings from hypothesis.internal.compat import escape_unicode_characters from hypothesis.utils.dynamicvariables import DynamicVariable -if TYPE_CHECKING: - from typing import TypeAlias - def default(value: object) -> None: try: @@ -26,7 +24,7 @@ def default(value: object) -> None: print(escape_unicode_characters(str(value))) -ReporterT: "TypeAlias" = Callable[[object], None] +ReporterT: TypeAlias = Callable[[object], None] reporter = DynamicVariable[ReporterT](default) diff --git a/contrib/python/hypothesis/py3/hypothesis/stateful.py b/contrib/python/hypothesis/py3/hypothesis/stateful.py index e10085dcf0a..7c2ef1ccb40 100644 --- a/contrib/python/hypothesis/py3/hypothesis/stateful.py +++ b/contrib/python/hypothesis/py3/hypothesis/stateful.py @@ -18,12 +18,12 @@ execution to date. import collections import dataclasses import inspect -from collections.abc import Iterable, Sequence +from collections.abc import Callable, Iterable, Sequence 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 typing import Any, ClassVar, TypeVar, overload from unittest import TestCase from hypothesis import strategies as st @@ -41,7 +41,7 @@ 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 from hypothesis.internal.healthcheck import fail_health_check -from hypothesis.internal.observability import TESTCASE_CALLBACKS +from hypothesis.internal.observability import observability_enabled from hypothesis.internal.reflection import ( function_digest, get_pretty_function_description, @@ -112,9 +112,9 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0 @settings @given(st.data()) - def run_state_machine(factory, data): + def run_state_machine(data): cd = data.conjecture_data - machine: RuleBasedStateMachine = factory() + machine: RuleBasedStateMachine = state_machine_factory() check_type(RuleBasedStateMachine, machine, "state_machine_factory()") cd.hypothesis_runner = machine machine._observability_predicates = cd._observability_predicates # alias @@ -127,7 +127,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0 def output(s): if print_steps: report(s) - if TESTCASE_CALLBACKS: + if observability_enabled(): cd._stateful_repr_parts.append(s) try: @@ -180,7 +180,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0 # _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: + if print_steps or observability_enabled(): data_to_print = { k: machine._pretty_print(v) for k, v in data.items() } @@ -217,7 +217,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0 HealthCheck.return_value, ) finally: - if print_steps or TESTCASE_CALLBACKS: + if print_steps or observability_enabled(): # 'result' is only used if the step has target bundles. # If it does, and the result is a 'MultipleResult', # then 'print_step' prints a multi-variable assignment. @@ -254,7 +254,7 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_step state_machine_test = get_state_machine_test( state_machine_factory, settings=settings, _min_steps=_min_steps ) - state_machine_test(state_machine_factory) + state_machine_test() class StateMachineMeta(type): @@ -451,7 +451,7 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta): if ( current_build_context().is_final or settings.verbosity >= Verbosity.debug - or TESTCASE_CALLBACKS + or observability_enabled() ): output(f"state.{name}()") start = perf_counter() @@ -499,8 +499,8 @@ class Rule: 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) + _cached_hash: int | None = field(init=False, default=None) + _cached_repr: str | None = field(init=False, default=None) def __post_init__(self): self.arguments_strategies = {} @@ -610,12 +610,12 @@ class Bundle(SearchStrategy[Ex]): # We assume that a bundle will grow over time return False - def _available(self, data): + def is_currently_empty(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. + # modifying the underlying choice sequence. machine = data.draw(self_strategy) - return bool(machine.bundle(self.name)) + return not bool(machine.bundle(self.name)) def flatmap(self, expand): if self.draw_references: @@ -719,7 +719,7 @@ PRECONDITIONS_MARKER = "hypothesis_stateful_preconditions" INVARIANT_MARKER = "hypothesis_stateful_invariant" -_RuleType = Callable[..., Union[MultipleResults[Ex], Ex]] +_RuleType = Callable[..., MultipleResults[Ex] | Ex] _RuleWrapper = Callable[[_RuleType[Ex]], _RuleType[Ex]] @@ -787,10 +787,10 @@ def rule( def rule( *, - targets: Union[Sequence[Bundle[Ex]], _OmittedArgument] = (), - target: Optional[Bundle[Ex]] = None, + targets: Sequence[Bundle[Ex]] | _OmittedArgument = (), + target: Bundle[Ex] | None = None, **kwargs: SearchStrategy, -) -> Union[_RuleWrapper[Ex], Callable[[Callable[..., None]], Callable[..., None]]]: +) -> _RuleWrapper[Ex] | Callable[[Callable[..., None]], Callable[..., None]]: """Decorator for RuleBasedStateMachine. Any Bundle present in ``target`` or ``targets`` will define where the end result of this function should go. If both are empty then the end result will be discarded. @@ -882,10 +882,10 @@ def initialize( def initialize( *, - targets: Union[Sequence[Bundle[Ex]], _OmittedArgument] = (), - target: Optional[Bundle[Ex]] = None, + targets: Sequence[Bundle[Ex]] | _OmittedArgument = (), + target: Bundle[Ex] | None = None, **kwargs: SearchStrategy, -) -> Union[_RuleWrapper[Ex], Callable[[Callable[..., None]], Callable[..., None]]]: +) -> _RuleWrapper[Ex] | Callable[[Callable[..., None]], Callable[..., None]]: """Decorator for RuleBasedStateMachine. An initialize decorator behaves like a rule, but all ``@initialize()`` decorated diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/__init__.py b/contrib/python/hypothesis/py3/hypothesis/strategies/__init__.py index dfe89502909..80bef4eee8d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/__init__.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/__init__.py @@ -57,7 +57,7 @@ from hypothesis.strategies._internal.ipaddress import ip_addresses from hypothesis.strategies._internal.misc import just, none, nothing from hypothesis.strategies._internal.numbers import floats, integers from hypothesis.strategies._internal.strategies import one_of -from hypothesis.strategies._internal.utils import _strategies +from hypothesis.strategies._internal.utils import _all_strategies # The implementation of all of these lives in `_strategies.py`, but we # re-export them via this module to avoid exposing implementation details @@ -122,7 +122,7 @@ def _check_exports(_public): # Verify that all exported strategy functions were registered with # @declares_strategy. - existing_strategies = set(_strategies) - {"_maybe_nil_uuids"} + existing_strategies = set(_all_strategies) - {"_maybe_nil_uuids"} exported_strategies = set(__all__) - { "DataObject", diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py index a487315f99c..555b7ca09b6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/attrs.py @@ -11,7 +11,8 @@ from collections.abc import Collection, Generator, Iterable, Sequence from functools import reduce from itertools import chain -from typing import Any, Optional, TypeVar, Union +from types import EllipsisType +from typing import Any, TypeVar import attr @@ -27,7 +28,7 @@ from attrs import Attribute, AttrsInstance, Factory from hypothesis import strategies as st from hypothesis.errors import ResolutionFailed -from hypothesis.internal.compat import EllipsisType, get_type_hints +from hypothesis.internal.compat import 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 @@ -40,7 +41,7 @@ def get_attribute_by_alias( fields: Iterable[Attribute], alias: str, *, - target: Optional[type[AttrsInstance]] = None, + target: type[AttrsInstance] | None = None, ) -> Attribute: """ Get an attrs attribute by its alias, rather than its name (compare @@ -69,7 +70,7 @@ def get_attribute_by_alias( def from_attrs( target: type[AttrsInstance], args: tuple[SearchStrategy[Any], ...], - kwargs: dict[str, Union[SearchStrategy[Any], EllipsisType]], + kwargs: dict[str, SearchStrategy[Any] | EllipsisType], to_infer: Iterable[str], ) -> SearchStrategy: """An internal version of builds(), specialised for Attrs classes.""" diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py index 10502615a0b..d585d296f71 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py @@ -10,8 +10,8 @@ import copy import math -from collections.abc import Iterable -from typing import Any, Callable, Optional, Union, overload +from collections.abc import Callable, Iterable +from typing import Any, overload from hypothesis import strategies as st from hypothesis.errors import InvalidArgument @@ -27,6 +27,7 @@ from hypothesis.strategies._internal.strategies import ( T4, T5, Ex, + FilteredStrategy, RecurT, SampledFromStrategy, SearchStrategy, @@ -148,7 +149,7 @@ class ListStrategy(SearchStrategy[list[Ex]]): self, elements: SearchStrategy[Ex], min_size: int = 0, - max_size: Optional[Union[float, int]] = math.inf, + max_size: float | int | None = math.inf, ): super().__init__() self.min_size = min_size or 0 @@ -187,8 +188,7 @@ class ListStrategy(SearchStrategy[list[Ex]]): def calc_is_empty(self, recur: RecurT) -> bool: if self.min_size == 0: return False - else: - return recur(self.element_strategy) + return recur(self.element_strategy) def do_draw(self, data: ConjectureData) -> list[Ex]: if self.element_strategy.is_empty: @@ -252,11 +252,11 @@ class UniqueListStrategy(ListStrategy[Ex]): self, elements: SearchStrategy[Ex], min_size: int, - max_size: Optional[Union[float, int]], + max_size: float | int | None, # TODO: keys are guaranteed to be Hashable, not just Any, but this makes # other things harder to type keys: tuple[Callable[[Ex], Any], ...], - tuple_suffixes: Optional[SearchStrategy[tuple[Ex, ...]]], + tuple_suffixes: SearchStrategy[tuple[Ex, ...]] | None, ): super().__init__(elements, min_size, max_size) self.keys = keys @@ -284,10 +284,13 @@ class UniqueListStrategy(ListStrategy[Ex]): # approach because some strategies have special logic for generation under a # filter, and FilteredStrategy can consolidate multiple filters. def not_yet_in_unique_list(val: Ex) -> bool: # type: ignore # covariant type param - return all(key(val) not in seen for key, seen in zip(self.keys, seen_sets)) + return all( + key(val) not in seen + for key, seen in zip(self.keys, seen_sets, strict=True) + ) - filtered = self.element_strategy._filter_for_filtered_draw( - not_yet_in_unique_list + filtered = FilteredStrategy( + self.element_strategy, conditions=(not_yet_in_unique_list,) ) while elements.more(): value = filtered.do_filtered_draw(data) @@ -295,7 +298,7 @@ class UniqueListStrategy(ListStrategy[Ex]): elements.reject(f"Aborted test because unable to satisfy {filtered!r}") else: assert not isinstance(value, UniqueIdentifier) - for key, seen in zip(self.keys, seen_sets): + for key, seen in zip(self.keys, seen_sets, strict=True): seen.add(key(value)) if self.tuple_suffixes is not None: value = (value, *data.draw(self.tuple_suffixes)) # type: ignore @@ -323,9 +326,10 @@ class UniqueSampledListStrategy(UniqueListStrategy): j = data.draw_integer(0, len(remaining) - 1) value = self.element_strategy._transform(remaining.pop(j)) if value is not filter_not_satisfied and all( - key(value) not in seen for key, seen in zip(self.keys, seen_sets) + key(value) not in seen + for key, seen in zip(self.keys, seen_sets, strict=True) ): - for key, seen in zip(self.keys, seen_sets): + for key, seen in zip(self.keys, seen_sets, strict=True): seen.add(key(value)) if self.tuple_suffixes is not None: value = (value, *data.draw(self.tuple_suffixes)) @@ -350,14 +354,14 @@ class FixedDictStrategy(SearchStrategy[dict[Any, Any]]): self, mapping: dict[Any, SearchStrategy[Any]], *, - optional: Optional[dict[Any, SearchStrategy[Any]]], + optional: dict[Any, SearchStrategy[Any]] | None, ): super().__init__() dict_type = type(mapping) self.mapping = mapping keys = tuple(mapping.keys()) self.fixed = st.tuples(*[mapping[k] for k in keys]).map( - lambda value: dict_type(zip(keys, value)) + lambda value: dict_type(zip(keys, value, strict=True)) ) self.optional = optional diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 0f8c507d773..62bd40e13a4 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -18,25 +18,26 @@ import string import sys import typing import warnings -from collections.abc import Collection, Hashable, Iterable, Sequence +from collections.abc import Callable, Collection, Hashable, Iterable, Sequence from contextvars import ContextVar from decimal import Context, Decimal, localcontext from fractions import Fraction from functools import reduce from inspect import Parameter, Signature, isabstract, isclass from re import Pattern -from types import FunctionType, GenericAlias +from types import EllipsisType, FunctionType, GenericAlias from typing import ( Annotated, Any, AnyStr, - Callable, + Concatenate, Literal, + NewType, NoReturn, - Optional, + ParamSpec, Protocol, + TypeAlias, TypeVar, - Union, cast, get_args, get_origin, @@ -70,9 +71,6 @@ from hypothesis.internal.charmap import ( categories as all_categories, ) from hypothesis.internal.compat import ( - Concatenate, - EllipsisType, - ParamSpec, bit_count, ceil, floor, @@ -81,8 +79,10 @@ from hypothesis.internal.compat import ( ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.utils import ( - calc_label_from_cls, + calc_label_from_callable, + calc_label_from_name, check_sample, + combine_labels, identity, ) from hypothesis.internal.entropy import get_seeder_and_restorer @@ -167,14 +167,14 @@ def sampled_from(elements: type[enum.Enum]) -> SearchStrategy[Any]: # pragma: n @overload def sampled_from( - elements: Union[type[enum.Enum], Sequence[Any]], + elements: type[enum.Enum] | Sequence[Any], ) -> SearchStrategy[Any]: # pragma: no cover ... @defines_strategy(try_non_lazy=True) def sampled_from( - elements: Union[type[enum.Enum], Sequence[Any]], + elements: type[enum.Enum] | Sequence[Any], ) -> SearchStrategy[Any]: """Returns a strategy which generates any value present in ``elements``. @@ -242,10 +242,19 @@ def sampled_from( ] return LazyStrategy(one_of, args=inner, kwargs={}, force_repr=force_repr) if not values: + + def has_annotations(elements): + if sys.version_info[:2] < (3, 14): + return vars(elements).get("__annotations__") + else: # pragma: no cover # covered by 3.14 tests + import annotationlib + + return bool(annotationlib.get_annotations(elements)) + if ( isinstance(elements, type) and issubclass(elements, enum.Enum) - and vars(elements).get("__annotations__") + and has_annotations(elements) ): # See https://github.com/HypothesisWorks/hypothesis/issues/2923 raise InvalidArgument( @@ -261,18 +270,24 @@ def sampled_from( ) +def _gets_first_item(fn: Callable) -> bool: + # Introspection for either `itemgetter(0)`, or `lambda x: x[0]` + if isinstance(fn, FunctionType): + s = get_pretty_function_description(fn) + return bool(re.fullmatch(s, r"lambda ([a-z]+): \1\[0\]")) + return isinstance(fn, operator.itemgetter) and repr(fn) == "operator.itemgetter(0)" + + @cacheable @defines_strategy() def lists( elements: SearchStrategy[Ex], *, min_size: int = 0, - max_size: Optional[int] = None, - unique_by: Union[ - None, - Callable[[Ex], Hashable], - tuple[Callable[[Ex], Hashable], ...], - ] = None, + max_size: int | None = None, + unique_by: ( + None | Callable[[Ex], Hashable] | tuple[Callable[[Ex], Hashable], ...] + ) = None, unique: bool = False, ) -> SearchStrategy[list[Ex]]: """Returns a list containing values drawn from elements with length in the @@ -330,48 +345,33 @@ def lists( # Note that lazy strategies automatically unwrap when passed to a defines_strategy # function. tuple_suffixes = None - # the type: ignores in the TupleStrategy and IntegersStrategy cases are - # for a mypy bug, which incorrectly narrows `elements` to Never. - # https://github.com/python/mypy/issues/16494 if ( # We're generating a list of tuples unique by the first element, perhaps # via st.dictionaries(), and this will be more efficient if we rearrange # our strategy somewhat to draw the first element then draw add the rest. isinstance(elements, TupleStrategy) - and len(elements.element_strategies) >= 1 # type: ignore - and len(unique_by) == 1 - and ( - # Introspection for either `itemgetter(0)`, or `lambda x: x[0]` - ( - isinstance(unique_by[0], operator.itemgetter) - and repr(unique_by[0]) == "operator.itemgetter(0)" - ) - or ( - isinstance(unique_by[0], FunctionType) - and re.fullmatch( - get_pretty_function_description(unique_by[0]), - r"lambda ([a-z]+): \1\[0\]", - ) - ) - ) + and len(elements.element_strategies) >= 1 + and all(_gets_first_item(fn) for fn in unique_by) ): unique_by = (identity,) - tuple_suffixes = TupleStrategy(elements.element_strategies[1:]) # type: ignore - elements = elements.element_strategies[0] # type: ignore + tuple_suffixes = TupleStrategy(elements.element_strategies[1:]) + elements = elements.element_strategies[0] # UniqueSampledListStrategy offers a substantial performance improvement for # unique arrays with few possible elements, e.g. of eight-bit integer types. if ( isinstance(elements, IntegersStrategy) - and elements.start is not None # type: ignore - and elements.end is not None # type: ignore - and (elements.end - elements.start) <= 255 # type: ignore + and elements.start is not None + and elements.end is not None + and (elements.end - elements.start) <= 255 ): elements = SampledFromStrategy( sorted(range(elements.start, elements.end + 1), key=abs) # type: ignore - if elements.end < 0 or elements.start > 0 # type: ignore - else list(range(elements.end + 1)) # type: ignore - + list(range(-1, elements.start - 1, -1)) # type: ignore + if elements.end < 0 or elements.start > 0 + else ( + list(range(elements.end + 1)) + + list(range(-1, elements.start - 1, -1)) + ) ) if isinstance(elements, SampledFromStrategy): @@ -412,7 +412,7 @@ def sets( elements: SearchStrategy[Ex], *, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> SearchStrategy[set[Ex]]: """This has the same behaviour as lists, but returns sets instead. @@ -434,7 +434,7 @@ def frozensets( elements: SearchStrategy[Ex], *, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> SearchStrategy[frozenset[Ex]]: """This is identical to the sets function but instead returns frozensets.""" @@ -463,12 +463,10 @@ def iterables( elements: SearchStrategy[Ex], *, min_size: int = 0, - max_size: Optional[int] = None, - unique_by: Union[ - None, - Callable[[Ex], Hashable], - tuple[Callable[[Ex], Hashable], ...], - ] = None, + max_size: int | None = None, + unique_by: ( + None | Callable[[Ex], Hashable] | tuple[Callable[[Ex], Hashable], ...] + ) = None, unique: bool = False, ) -> SearchStrategy[Iterable[Ex]]: """This has the same behaviour as lists, but returns iterables instead. @@ -522,7 +520,7 @@ def iterables( def fixed_dictionaries( mapping: dict[T, SearchStrategy[Ex]], *, - optional: Optional[dict[T, SearchStrategy[Ex]]] = None, + optional: dict[T, SearchStrategy[Ex]] | None = None, ) -> SearchStrategy[dict[T, Ex]]: """Generates a dictionary of the same type as mapping with a fixed set of keys mapping to strategies. ``mapping`` must be a dict subclass. @@ -560,6 +558,9 @@ def fixed_dictionaries( return FixedDictStrategy(mapping, optional=optional) +_get_first_item = operator.itemgetter(0) + + @cacheable @defines_strategy() def dictionaries( @@ -568,7 +569,7 @@ def dictionaries( *, dict_class: type = dict, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> SearchStrategy[dict[Ex, T]]: # Describing the exact dict_class to Mypy drops the key and value types, # so we report Dict[K, V] instead of Mapping[Any, Any] for now. Sorry! @@ -591,7 +592,7 @@ def dictionaries( tuples(keys, values), min_size=min_size, max_size=max_size, - unique_by=operator.itemgetter(0), + unique_by=_get_first_item, ).map(dict_class) @@ -599,18 +600,18 @@ def dictionaries( @defines_strategy(force_reusable_values=True) def characters( *, - codec: Optional[str] = None, - min_codepoint: Optional[int] = None, - max_codepoint: Optional[int] = None, - categories: Optional[Collection[CategoryName]] = None, - exclude_categories: Optional[Collection[CategoryName]] = None, - exclude_characters: Optional[Collection[str]] = None, - include_characters: Optional[Collection[str]] = None, + codec: str | None = None, + min_codepoint: int | None = None, + max_codepoint: int | None = None, + categories: Collection[CategoryName] | None = None, + exclude_categories: Collection[CategoryName] | None = None, + exclude_characters: Collection[str] | None = None, + include_characters: Collection[str] | None = None, # Note: these arguments are deprecated aliases for backwards compatibility - blacklist_categories: Optional[Collection[CategoryName]] = None, - whitelist_categories: Optional[Collection[CategoryName]] = None, - blacklist_characters: Optional[Collection[str]] = None, - whitelist_characters: Optional[Collection[str]] = None, + blacklist_categories: Collection[CategoryName] | None = None, + whitelist_categories: Collection[CategoryName] | None = None, + blacklist_characters: Collection[str] | None = None, + whitelist_characters: Collection[str] | None = None, ) -> SearchStrategy[str]: r"""Generates characters, length-one :class:`python:str`\ ings, following specified filtering rules. @@ -663,7 +664,7 @@ def characters( check_valid_size(min_codepoint, "min_codepoint") check_valid_size(max_codepoint, "max_codepoint") check_valid_interval(min_codepoint, max_codepoint, "min_codepoint", "max_codepoint") - categories = cast(Optional[Categories], categories) + categories = cast(Categories | None, categories) if categories is not None and exclude_categories is not None: raise InvalidArgument( f"Pass at most one of {categories=} and {exclude_categories=} - " @@ -704,6 +705,16 @@ def characters( ) exclude_characters = exclude_characters or "" include_characters = include_characters or "" + if not_one_char := [c for c in exclude_characters if len(c) != 1]: + raise InvalidArgument( + "Elements of exclude_characters are required to be a single character, " + f"but {not_one_char!r} passed in {exclude_characters=} was not." + ) + if not_one_char := [c for c in include_characters if len(c) != 1]: + raise InvalidArgument( + "Elements of include_characters are required to be a single character, " + f"but {not_one_char!r} passed in {include_characters=} was not." + ) overlap = set(exclude_characters).intersection(include_characters) if overlap: raise InvalidArgument( @@ -783,10 +794,10 @@ characters.__signature__ = (__sig := get_signature(characters)).replace( # type @cacheable @defines_strategy(force_reusable_values=True) def text( - alphabet: Union[Collection[str], SearchStrategy[str]] = characters(codec="utf-8"), + alphabet: Collection[str] | SearchStrategy[str] = characters(codec="utf-8"), *, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> SearchStrategy[str]: """Generates strings with characters drawn from ``alphabet``, which should be a collection of length one strings or a strategy generating such strings. @@ -856,7 +867,7 @@ def text( @overload def from_regex( - regex: Union[bytes, Pattern[bytes]], + regex: bytes | Pattern[bytes], *, fullmatch: bool = False, ) -> SearchStrategy[bytes]: # pragma: no cover @@ -865,10 +876,10 @@ def from_regex( @overload def from_regex( - regex: Union[str, Pattern[str]], + regex: str | Pattern[str], *, fullmatch: bool = False, - alphabet: Union[str, SearchStrategy[str]] = characters(codec="utf-8"), + alphabet: str | SearchStrategy[str] = characters(codec="utf-8"), ) -> SearchStrategy[str]: # pragma: no cover ... @@ -876,10 +887,10 @@ def from_regex( @cacheable @defines_strategy() def from_regex( - regex: Union[AnyStr, Pattern[AnyStr]], + regex: AnyStr | Pattern[AnyStr], *, fullmatch: bool = False, - alphabet: Union[str, SearchStrategy[str], None] = None, + alphabet: str | SearchStrategy[str] | None = None, ) -> SearchStrategy[AnyStr]: r"""Generates strings that contain a match for the given regex (i.e. ones for which :func:`python:re.search` will return a non-None result). @@ -934,7 +945,7 @@ def from_regex( def binary( *, min_size: int = 0, - max_size: Optional[int] = None, + max_size: int | None = None, ) -> SearchStrategy[bytes]: """Generates :class:`python:bytes`. @@ -1034,6 +1045,15 @@ class BuildsStrategy(SearchStrategy[Ex]): self.args = args self.kwargs = kwargs + def calc_label(self) -> int: + return combine_labels( + self.class_label, + calc_label_from_callable(self.target), + *[strat.label for strat in self.args], + *[calc_label_from_name(k) for k in self.kwargs], + *[strat.label for strat in self.kwargs.values()], + ) + 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()} @@ -1051,9 +1071,9 @@ class BuildsStrategy(SearchStrategy[Ex]): f"try using sampled_from({name}) instead of builds({name})" ) from err if not (self.args or self.kwargs): - from .types import is_a_new_type, is_generic_type + from .types import is_generic_type - if is_a_new_type(self.target) or is_generic_type(self.target): + if isinstance(self.target, NewType) or is_generic_type(self.target): raise InvalidArgument( f"Calling {self.target!r} with no arguments raised an " f"error - try using from_type({self.target!r}) instead " @@ -1068,7 +1088,7 @@ class BuildsStrategy(SearchStrategy[Ex]): ) from err raise - current_build_context().record_call(obj, self.target, args, kwargs) + current_build_context().record_call(obj, self.target, args=args, kwargs=kwargs) return obj def do_validate(self) -> None: @@ -1088,7 +1108,7 @@ def builds( target: Callable[..., Ex], /, *args: SearchStrategy[Any], - **kwargs: Union[SearchStrategy[Any], EllipsisType], + **kwargs: SearchStrategy[Any] | EllipsisType, ) -> SearchStrategy[Ex]: """Generates values by drawing from ``args`` and ``kwargs`` and passing them to the callable (provided as the first positional argument) in the @@ -1112,9 +1132,17 @@ def builds( the callable. """ if not callable(target): + from hypothesis.strategies._internal.types import is_a_union + + # before 3.14, unions were callable, so it got an error message in + # BuildsStrategy.do_draw. In 3.14+, unions are not callable, so + # we error earlier here instead. + suggestion = ( + f" Try using from_type({target}) instead?" if is_a_union(target) else "" + ) raise InvalidArgument( "The first positional argument to builds() must be a callable " - "target to construct." + f"target to construct.{suggestion}" ) if ... in args: # type: ignore # we only annotated the allowed types @@ -1303,33 +1331,31 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: strat = resolver(thing) if strat is not None: return strat - if not isinstance(thing, type): - if types.is_a_new_type(thing): - # Check if we have an explicitly registered strategy for this thing, - # resolve it so, and otherwise resolve as for the base type. - if thing in types._global_type_lookup: - strategy = as_strategy(types._global_type_lookup[thing], thing) - if strategy is not NotImplemented: - return strategy - return _from_type(thing.__supertype__) - if types.is_a_type_alias_type( - thing - ): # pragma: no cover # covered by 3.12+ tests - if thing in types._global_type_lookup: - strategy = as_strategy(types._global_type_lookup[thing], thing) - if strategy is not NotImplemented: - return strategy - return _from_type(thing.__value__) - # Unions are not instances of `type` - but we still want to resolve them! - if types.is_a_union(thing): - args = sorted(thing.__args__, key=types.type_sorting_key) - return one_of([_from_type(t) for t in args]) - if thing in types.LiteralStringTypes: # pragma: no cover - # We can't really cover this because it needs either - # typing-extensions or python3.11+ typing. - # `LiteralString` from runtime's point of view is just a string. - # Fallback to regular text. - return text() + + if isinstance(thing, NewType): + # Check if we have an explicitly registered strategy for this thing, + # resolve it so, and otherwise resolve as for the base type. + if thing in types._global_type_lookup: + strategy = as_strategy(types._global_type_lookup[thing], thing) + if strategy is not NotImplemented: + return strategy + return _from_type(thing.__supertype__) + if types.is_a_type_alias_type(thing): # pragma: no cover # covered by 3.12+ tests + if thing in types._global_type_lookup: + strategy = as_strategy(types._global_type_lookup[thing], thing) + if strategy is not NotImplemented: + return strategy + return _from_type(thing.__value__) # type: ignore + if types.is_a_union(thing): + args = sorted(thing.__args__, key=types.type_sorting_key) # type: ignore + return one_of([_from_type(t) for t in args]) + if thing in types.LiteralStringTypes: # pragma: no cover + # We can't really cover this because it needs either + # typing-extensions or python3.11+ typing. + # `LiteralString` from runtime's point of view is just a string. + # Fallback to regular text. + return text() # type: ignore + # We also have a special case for TypeVars. # They are represented as instances like `~T` when they come here. # We need to work with their type instead. @@ -1337,6 +1363,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: strategy = as_strategy(types._global_type_lookup[type(thing)], thing) if strategy is not NotImplemented: return strategy + if not types.is_a_type(thing): if isinstance(thing, str): # See https://github.com/HypothesisWorks/hypothesis/issues/3016 @@ -1347,6 +1374,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: "strings." ) raise InvalidArgument(f"{thing=} must be a type") # pragma: no cover + if thing in types.NON_RUNTIME_TYPES: # Some code like `st.from_type(TypeAlias)` does not make sense. # Because there are types in python that do not exist in runtime. @@ -1354,6 +1382,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: f"Could not resolve {thing!r} to a strategy, " f"because there is no such thing as a runtime instance of {thing!r}" ) + # Now that we know `thing` is a type, the first step is to check for an # explicitly registered strategy. This is the best (and hopefully most # common) way to resolve a type to a strategy. Note that the value in the @@ -1378,6 +1407,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: # We've kept it because we turn out to have more type errors from... somewhere. # FIXME: investigate that, maybe it should be fixed more precisely? pass + if (hasattr(typing, "_TypedDictMeta") and type(thing) is typing._TypedDictMeta) or ( hasattr(types.typing_extensions, "_TypedDictMeta") # type: ignore and type(thing) is types.typing_extensions._TypedDictMeta # type: ignore @@ -1460,10 +1490,13 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: # We'll start by checking if thing is from from the typing module, # because there are several special cases that don't play well with # subclass and instance checks. - if isinstance(thing, types.typing_root_type) or ( - isinstance(get_origin(thing), type) and get_args(thing) + if ( + isinstance(thing, types.typing_root_type) + or (isinstance(get_origin(thing), type) and get_args(thing)) + or isinstance(thing, typing.ForwardRef) ): return types.from_typing_type(thing) + # If it's not from the typing module, we get all registered types that are # a subclass of `thing` and are not themselves a subtype of any other such # type. For example, `Number -> integers() | floats()`, but bools() is @@ -1482,10 +1515,12 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: ] if any(not s.is_empty for s in strategies): return one_of(strategies) + # If we don't have a strategy registered for this type or any subtype, we # may be able to fall back on type annotations. if issubclass(thing, enum.Enum): return sampled_from(thing) + # Finally, try to build an instance by calling the type object. Unlike builds(), # this block *does* try to infer strategies for arguments with default values. # That's because of the semantic different; builds() -> "call this with ..." @@ -1549,6 +1584,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: stacklevel=2, ) return builds(thing, *posonly_args, **kwargs) + # And if it's an abstract type, we'll resolve to a union of subclasses instead. subclasses = thing.__subclasses__() if not subclasses: @@ -1556,6 +1592,7 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: f"Could not resolve {thing!r} to a strategy, because it is an abstract " "type without any subclasses. Consider using register_type_strategy" ) + subclass_strategies: SearchStrategy = nothing() for sc in subclasses: try: @@ -1572,10 +1609,10 @@ def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: @cacheable @defines_strategy(force_reusable_values=True) def fractions( - min_value: Optional[Union[Real, str]] = None, - max_value: Optional[Union[Real, str]] = None, + min_value: Real | str | None = None, + max_value: Real | str | None = None, *, - max_denominator: Optional[int] = None, + max_denominator: int | None = None, ) -> SearchStrategy[Fraction]: """Returns a strategy which generates Fractions. @@ -1656,8 +1693,8 @@ def fractions( def _as_finite_decimal( - value: Union[Real, str, None], name: str, allow_infinity: Optional[bool] -) -> Optional[Decimal]: + value: Real | str | None, name: str, allow_infinity: bool | None +) -> Decimal | None: """Convert decimal bounds to decimals, carefully.""" assert name in ("min_value", "max_value") if value is None: @@ -1679,12 +1716,12 @@ def _as_finite_decimal( @cacheable @defines_strategy(force_reusable_values=True) def decimals( - min_value: Optional[Union[Real, str]] = None, - max_value: Optional[Union[Real, str]] = None, + min_value: Real | str | None = None, + max_value: Real | str | None = None, *, - allow_nan: Optional[bool] = None, - allow_infinity: Optional[bool] = None, - places: Optional[int] = None, + allow_nan: bool | None = None, + allow_infinity: bool | None = None, + places: int | None = None, ) -> SearchStrategy[Decimal]: """Generates instances of :class:`python:decimal.Decimal`, which may be: @@ -1771,7 +1808,7 @@ def recursive( extend: Callable[[SearchStrategy[Any]], SearchStrategy[T]], *, max_leaves: int = 100, -) -> SearchStrategy[Union[T, Ex]]: +) -> SearchStrategy[T | Ex]: """base: A strategy to start from. extend: A function which takes a strategy and returns a new strategy. @@ -1839,7 +1876,10 @@ class CompositeStrategy(SearchStrategy): return self.definition(data.draw, *self.args, **self.kwargs) def calc_label(self) -> int: - return calc_label_from_cls(self.definition) + return combine_labels( + self.class_label, + calc_label_from_callable(self.definition), + ) class DrawFn(Protocol): @@ -1902,7 +1942,7 @@ def _composite(f): f"Return-type annotation is `{ret_repr}`, but the decorated " "function should return a value (not a strategy)", HypothesisWarning, - stacklevel=3 if sys.version_info[:2] > (3, 9) else 5, # ugh + stacklevel=3, ) if params[0].kind.name != "VAR_POSITIONAL": params = params[1:] @@ -1986,9 +2026,9 @@ composite.__doc__ = composite_doc def complex_numbers( *, min_magnitude: Real = 0, - max_magnitude: Optional[Real] = None, - allow_infinity: Optional[bool] = None, - allow_nan: Optional[bool] = None, + max_magnitude: Real | None = None, + allow_infinity: bool | None = None, + allow_nan: bool | None = None, allow_subnormal: bool = True, width: Literal[32, 64, 128] = 128, ) -> SearchStrategy[complex]: @@ -2111,7 +2151,7 @@ def complex_numbers( def shared( base: SearchStrategy[Ex], *, - key: Optional[Hashable] = None, + key: Hashable | None = None, ) -> SearchStrategy[Ex]: """Returns a strategy that draws a single shared value per run, drawn from base. Any two shared instances with the same key will share the same value, @@ -2143,7 +2183,7 @@ def _maybe_nil_uuids(draw, uuid): @cacheable @defines_strategy(force_reusable_values=True) def uuids( - *, version: Optional[Literal[1, 2, 3, 4, 5]] = None, allow_nil: bool = False + *, version: Literal[1, 2, 3, 4, 5] | None = None, allow_nil: bool = False ) -> SearchStrategy[UUID]: """Returns a strategy that generates :class:`UUIDs <uuid.UUID>`. @@ -2246,12 +2286,10 @@ class DataObject: class DataStrategy(SearchStrategy): - supports_find = False - def do_draw(self, data): - if not hasattr(data, "hypothesis_shared_data_strategy"): - data.hypothesis_shared_data_strategy = DataObject(data) - return data.hypothesis_shared_data_strategy + if data._shared_data_strategy is None: + data._shared_data_strategy = DataObject(data) + return data._shared_data_strategy def __repr__(self) -> str: return "data()" @@ -2332,9 +2370,19 @@ def data() -> SearchStrategy[DataObject]: return DataStrategy() +if sys.version_info < (3, 12): + # TypeAliasType is new in 3.12 + RegisterTypeT: TypeAlias = type[Ex] +else: # pragma: no cover # covered by test_mypy.py + from typing import TypeAliasType + + # see https://github.com/HypothesisWorks/hypothesis/issues/4410 + RegisterTypeT: TypeAlias = type[Ex] | TypeAliasType + + def register_type_strategy( - custom_type: type[Ex], - strategy: Union[SearchStrategy[Ex], Callable[[type[Ex]], SearchStrategy[Ex]]], + custom_type: RegisterTypeT, + strategy: SearchStrategy[Ex] | Callable[[type[Ex]], SearchStrategy[Ex]], ) -> None: """Add an entry to the global type-to-strategy lookup. @@ -2548,7 +2596,7 @@ if typing.TYPE_CHECKING or ParamSpec is not None: may return a different value if called again with the same arguments. Generated functions can only be called within the scope of the ``@given`` - which created them. This strategy does not support ``.example()``. + which created them. """ return _functions(like=like, returns=returns, pure=pure) @@ -2558,7 +2606,7 @@ else: # pragma: no cover def functions( *, like: Callable[..., Any] = lambda: None, - returns: Union[SearchStrategy[Any], EllipsisType] = ..., + returns: SearchStrategy[Any] | EllipsisType = ..., pure: bool = False, ) -> SearchStrategy[Callable[..., Any]]: """functions(*, like=lambda: None, returns=..., pure=False) @@ -2579,7 +2627,7 @@ else: # pragma: no cover may return a different value if called again with the same arguments. Generated functions can only be called within the scope of the ``@given`` - which created them. This strategy does not support ``.example()``. + which created them. """ return _functions(like=like, returns=returns, pure=pure) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py index 4bf3a020715..7fbbdd0cf90 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py @@ -15,7 +15,6 @@ from calendar import monthrange from functools import cache, partial from importlib import resources from pathlib import Path -from typing import Optional from hypothesis.errors import InvalidArgument from hypothesis.internal.validation import check_type, check_valid_interval @@ -165,7 +164,7 @@ def datetimes( min_value: dt.datetime = dt.datetime.min, max_value: dt.datetime = dt.datetime.max, *, - timezones: SearchStrategy[Optional[dt.tzinfo]] = none(), + timezones: SearchStrategy[dt.tzinfo | None] = none(), allow_imaginary: bool = True, ) -> SearchStrategy[dt.datetime]: """datetimes(min_value=datetime.datetime.min, max_value=datetime.datetime.max, *, timezones=none(), allow_imaginary=True) @@ -236,7 +235,7 @@ def times( min_value: dt.time = dt.time.min, max_value: dt.time = dt.time.max, *, - timezones: SearchStrategy[Optional[dt.tzinfo]] = none(), + timezones: SearchStrategy[dt.tzinfo | None] = none(), ) -> SearchStrategy[dt.time]: """times(min_value=datetime.time.min, max_value=datetime.time.max, *, timezones=none()) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py index c17cad50e19..1688cf20093 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py @@ -9,8 +9,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. import inspect -from collections.abc import Sequence -from typing import Callable, Optional +from collections.abc import Callable, Sequence from hypothesis.configuration import check_sideeffect_during_initialization from hypothesis.errors import InvalidArgument @@ -29,21 +28,24 @@ class DeferredStrategy(SearchStrategy[Ex]): def __init__(self, definition: Callable[[], SearchStrategy[Ex]]): super().__init__() - self.__wrapped_strategy: Optional[SearchStrategy[Ex]] = None + self.__wrapped_strategy: SearchStrategy[Ex] | None = None self.__in_repr: bool = False - self.__definition: Optional[Callable[[], SearchStrategy[Ex]]] = definition + self.__definition: Callable[[], SearchStrategy[Ex]] | None = definition @property def wrapped_strategy(self) -> SearchStrategy[Ex]: + # we assign this before entering the condition to avoid a race condition + # under threading. See issue #4523. + definition = self.__definition if self.__wrapped_strategy is None: check_sideeffect_during_initialization("deferred evaluation of {!r}", self) - if not inspect.isfunction(self.__definition): + if not inspect.isfunction(definition): raise InvalidArgument( - f"Expected definition to be a function but got {self.__definition!r} " - f"of type {type(self.__definition).__name__} instead." + f"Expected definition to be a function but got {definition!r} " + f"of type {type(definition).__name__} instead." ) - result = self.__definition() + result = definition() if result is self: raise InvalidArgument("Cannot define a deferred strategy to be itself") check_strategy(result, "definition()") @@ -55,10 +57,6 @@ class DeferredStrategy(SearchStrategy[Ex]): def branches(self) -> Sequence[SearchStrategy[Ex]]: return self.wrapped_strategy.branches - @property - def supports_find(self) -> bool: - return self.wrapped_strategy.supports_find - def calc_label(self) -> int: """Deferred strategies don't have a calculated label, because we would end up having to calculate the fixed point of some hash function in diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py index 915b9d7f18c..04ab1328d4d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py @@ -9,7 +9,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Hashable, Iterable, Sequence -from typing import Any, Optional +from typing import Any from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData @@ -37,7 +37,7 @@ class FeatureFlags: def __init__( self, - data: Optional[ConjectureData] = None, + data: ConjectureData | None = None, enabled: Sequence[Any] = (), disabled: Sequence[Any] = (), at_least_one_of: Iterable[Hashable] = (), diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/flatmapped.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/flatmapped.py index 4e4f4ddafb5..92c200d01f1 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/flatmapped.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/flatmapped.py @@ -8,9 +8,14 @@ # 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 typing import Callable, Generic, TypeVar +from collections.abc import Callable +from typing import Generic, TypeVar from hypothesis.internal.conjecture.data import ConjectureData +from hypothesis.internal.conjecture.utils import ( + calc_label_from_callable, + combine_labels, +) from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.strategies import ( RecurT, @@ -35,6 +40,13 @@ class FlatMapStrategy(SearchStrategy[MappedTo], Generic[MappedFrom, MappedTo]): def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.base) + def calc_label(self) -> int: + return combine_labels( + self.class_label, + self.base.label, + calc_label_from_callable(self.expand), + ) + def __repr__(self) -> str: if not hasattr(self, "_cached_repr"): self._cached_repr = ( diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/functions.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/functions.py index a69adf51fc6..ac710cfed20 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/functions.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/functions.py @@ -22,8 +22,6 @@ from hypothesis.strategies._internal.strategies import RecurT, SearchStrategy class FunctionStrategy(SearchStrategy): - supports_find = False - def __init__(self, like, returns, pure): super().__init__() self.like = like diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/ipaddress.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/ipaddress.py index 0f5fb1bcccc..22d2e339d06 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/ipaddress.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/ipaddress.py @@ -9,7 +9,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network -from typing import Literal, Optional, Union +from typing import Literal from hypothesis.errors import InvalidArgument from hypothesis.internal.validation import check_type @@ -73,9 +73,9 @@ SPECIAL_IPv6_RANGES = ( @defines_strategy(force_reusable_values=True) def ip_addresses( *, - v: Optional[Literal[4, 6]] = None, - network: Optional[Union[str, IPv4Network, IPv6Network]] = None, -) -> SearchStrategy[Union[IPv4Address, IPv6Address]]: + v: Literal[4, 6] | None = None, + network: str | IPv4Network | IPv6Network | None = None, +) -> SearchStrategy[IPv4Address | IPv6Address]: r"""Generate IP addresses - ``v=4`` for :class:`~python:ipaddress.IPv4Address`\ es, ``v=6`` for :class:`~python:ipaddress.IPv6Address`\ es, or leave unspecified to allow both versions. diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py index d5c706c6d7e..2cbdeb9698d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/lazy.py @@ -8,9 +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/. -from collections.abc import Sequence +from collections.abc import Callable, Sequence from inspect import signature -from typing import Any, Callable, Optional +from typing import Any from weakref import WeakKeyDictionary from hypothesis.configuration import check_sideeffect_during_initialization @@ -77,20 +77,16 @@ class LazyStrategy(SearchStrategy[Ex]): kwargs: dict[str, object], *, transforms: tuple[tuple[str, Callable[..., Any]], ...] = (), - force_repr: Optional[str] = None, + force_repr: str | None = None, ): super().__init__() - self.__wrapped_strategy: Optional[SearchStrategy[Ex]] = None - self.__representation: Optional[str] = force_repr + self.__wrapped_strategy: SearchStrategy[Ex] | None = None + self.__representation: str | None = force_repr self.function = function self.__args = args self.__kwargs = kwargs self._transformations = transforms - @property - def supports_find(self) -> bool: - return self.wrapped_strategy.supports_find - def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.wrapped_strategy) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/misc.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/misc.py index 7318048ccc3..6e453040e42 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/misc.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/misc.py @@ -8,7 +8,8 @@ # 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 typing import TYPE_CHECKING, Any, Callable, NoReturn, Union +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, NoReturn from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import get_pretty_function_description @@ -56,7 +57,7 @@ class JustStrategy(SampledFromStrategy[Ex]): def calc_is_cacheable(self, recur: RecurT) -> bool: return is_hashable(self.value) - def do_filtered_draw(self, data: ConjectureData) -> Union[Ex, UniqueIdentifier]: + def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: # The parent class's `do_draw` implementation delegates directly to # `do_filtered_draw`, which we can greatly simplify in this case since # we have exactly one value. (This also avoids drawing any data.) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py index afb00bab7eb..307bb373595 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py @@ -11,7 +11,7 @@ import math from decimal import Decimal from fractions import Fraction -from typing import Literal, Optional, Union +from typing import Literal, cast from hypothesis.control import reject from hypothesis.errors import InvalidArgument @@ -45,11 +45,11 @@ from hypothesis.strategies._internal.strategies import ( from hypothesis.strategies._internal.utils import cacheable, defines_strategy # See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong! -Real = Union[int, float, Fraction, Decimal] +Real = int | float | Fraction | Decimal class IntegersStrategy(SearchStrategy[int]): - def __init__(self, start: Optional[int], end: Optional[int]) -> None: + def __init__(self, start: int | None, end: int | None) -> None: super().__init__() assert isinstance(start, int) or start is None assert isinstance(end, int) or end is None @@ -110,8 +110,8 @@ class IntegersStrategy(SearchStrategy[int]): @cacheable @defines_strategy(force_reusable_values=True) def integers( - min_value: Optional[int] = None, - max_value: Optional[int] = None, + min_value: int | None = None, + max_value: int | None = None, ) -> SearchStrategy[int]: """Returns a strategy which generates integers. @@ -248,12 +248,12 @@ class FloatStrategy(SearchStrategy[float]): @cacheable @defines_strategy(force_reusable_values=True) def floats( - min_value: Optional[Real] = None, - max_value: Optional[Real] = None, + min_value: Real | None = None, + max_value: Real | None = None, *, - allow_nan: Optional[bool] = None, - allow_infinity: Optional[bool] = None, - allow_subnormal: Optional[bool] = None, + allow_nan: bool | None = None, + allow_infinity: bool | None = None, + allow_subnormal: bool | None = None, width: Literal[16, 32, 64] = 64, exclude_min: bool = False, exclude_max: bool = False, @@ -306,6 +306,10 @@ def floats( f"Got {width=}, but the only valid values " "are the integers 16, 32, and 64." ) + # Literal[16] accepts both 16 and 16.0. Normalize to the int 16 here, mainly + # for mypyc. We want to support width=16.0 to make e.g. width=mywidth / 2 for + # mywidth=32 easy. + width = cast(Literal[16, 32, 64], int(width)) check_valid_bound(min_value, "min_value") check_valid_bound(max_value, "max_value") diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py index 1050f13d3a0..8ff1d077877 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py @@ -154,12 +154,10 @@ class RandomState: def state_for_seed(data, seed): - try: - seeds_to_states = data.seeds_to_states - except AttributeError: - seeds_to_states = {} - data.seeds_to_states = seeds_to_states + if data.seeds_to_states is None: + data.seeds_to_states = {} + seeds_to_states = data.seeds_to_states try: state = seeds_to_states[seed] except KeyError: @@ -341,12 +339,9 @@ class ArtificialRandom(HypothesisRandom): if self.__state.state_id is not None: return self.__state.state_id - try: - states_for_ids = self.__data.states_for_ids - except AttributeError: - states_for_ids = {} - self.__data.states_for_ids = states_for_ids - + if self.__data.states_for_ids is None: + self.__data.states_for_ids = {} + states_for_ids = self.__data.states_for_ids self.__state.state_id = len(states_for_ids) states_for_ids[self.__state.state_id] = self.__state diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py index d79b3389c73..07fe866ca2c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/shared.py @@ -8,54 +8,49 @@ # 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 warnings from collections.abc import Hashable -from typing import Any, Optional +from typing import Any +from hypothesis.errors import HypothesisWarning from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies._internal import SearchStrategy from hypothesis.strategies._internal.strategies import Ex class SharedStrategy(SearchStrategy[Ex]): - def __init__(self, base: SearchStrategy[Ex], key: Optional[Hashable] = None): + def __init__(self, base: SearchStrategy[Ex], key: Hashable | None = None): super().__init__() self.key = key self.base = base - @property - def supports_find(self) -> bool: - return self.base.supports_find - def __repr__(self) -> str: if self.key is not None: return f"shared({self.base!r}, key={self.key!r})" else: return f"shared({self.base!r})" + def calc_label(self) -> int: + return self.base.calc_label() + # 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: drawn = data.draw(self.base) - data._shared_strategy_draws[key] = (strat_label, drawn) + data._shared_strategy_draws[key] = (drawn, self) 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, - # ) + drawn, other = data._shared_strategy_draws[key] + + # Check that the strategies shared under this key are equivalent + if self.label != other.label: + 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." + f" Conflicting strategies: ({self!r}, {other!r}).", + 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 fcd25befd6f..e5e03145d61 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py @@ -12,20 +12,18 @@ import sys import threading import warnings from collections import abc, defaultdict -from collections.abc import Sequence +from collections.abc import Callable, Sequence from functools import lru_cache from random import shuffle from threading import RLock from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, Generic, Literal, - Optional, + TypeAlias, TypeVar, - Union, cast, overload, ) @@ -56,8 +54,6 @@ from hypothesis.strategies._internal.utils import defines_strategy from hypothesis.utils.conventions import UniqueIdentifier if TYPE_CHECKING: - from typing import TypeAlias - Ex = TypeVar("Ex", covariant=True, default=Any) else: Ex = TypeVar("Ex", covariant=True) @@ -68,7 +64,7 @@ T4 = TypeVar("T4") T5 = TypeVar("T5") MappedFrom = TypeVar("MappedFrom") MappedTo = TypeVar("MappedTo") -RecurT: "TypeAlias" = Callable[["SearchStrategy"], bool] +RecurT: TypeAlias = Callable[["SearchStrategy"], bool] calculating = UniqueIdentifier("calculating") MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name( @@ -234,22 +230,25 @@ class SearchStrategy(Generic[Ex]): # 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 + __label: int | UniqueIdentifier | None = None def __init__(self): self.validate_called: dict[int, 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. + def is_currently_empty(self, data: ConjectureData) -> bool: + """ + Returns whether this strategy is currently empty. Unlike ``empty``, + which is computed based on static information and cannot change, + ``is_currently_empty`` may change over time based on choices made + during the test case. + + This is currently only used for stateful testing, where |Bundle| grows a + list of values to choose from over the course of a test case. - Unlike ``empty`` property, this method's return value may change - over time. - Note: ``data`` parameter will only be used for introspection and no - value drawn from it. + ``data`` will only be used for introspection. No values will be drawn + from it in a way that modifies the choice sequence. """ - return not self.is_empty + return self.is_empty @property def is_empty(self) -> Any: @@ -260,10 +259,6 @@ class SearchStrategy(Generic[Ex]): # intended to be an optimisation for some cases. return recursive_property(self, "is_empty", True) - @property - def supports_find(self) -> bool: - return True - # Returns True if values from this strategy can safely be reused without # this causing unexpected behaviour. @@ -423,27 +418,13 @@ class SearchStrategy(Generic[Ex]): return value assume(False) """ - return FilteredStrategy(conditions=(condition,), strategy=self) - - def _filter_for_filtered_draw( - self, condition: Callable[[Ex], Any] - ) -> "FilteredStrategy[Ex]": - # Hook for parent strategies that want to perform fallible filtering - # on one of their internal strategies (e.g. UniqueListStrategy). - # The returned object must have a `.do_filtered_draw(data)` method - # that behaves like `do_draw`, but returns the sentinel object - # `filter_not_satisfied` if the condition could not be satisfied. - - # This is separate from the main `filter` method so that strategies - # can override `filter` without having to also guarantee a - # `do_filtered_draw` method. - return FilteredStrategy(conditions=(condition,), strategy=self) + return FilteredStrategy(self, conditions=(condition,)) @property def branches(self) -> Sequence["SearchStrategy[Ex]"]: return [self] - def __or__(self, other: "SearchStrategy[T]") -> "SearchStrategy[Union[Ex, T]]": + def __or__(self, other: "SearchStrategy[T]") -> "SearchStrategy[Ex | T]": """Return a strategy which produces values by randomly drawing from one of this strategy or the other strategy. @@ -551,7 +532,7 @@ class SearchStrategy(Generic[Ex]): raise NotImplementedError(f"{type(self).__name__}.do_draw") -def _is_hashable(value: object) -> tuple[bool, Optional[int]]: +def _is_hashable(value: object) -> tuple[bool, int | None]: # hashing can be expensive; return the hash value if we compute it, so that # callers don't have to recompute. try: @@ -576,8 +557,8 @@ class SampledFromStrategy(SearchStrategy[Ex]): self, elements: Sequence[Ex], *, - force_repr: Optional[str] = None, - force_repr_braces: Optional[tuple[str, str]] = None, + force_repr: str | None = None, + force_repr_braces: tuple[str, str] | None = None, transformations: tuple[ tuple[Literal["filter", "map"], Callable[[Ex], Any]], ..., @@ -590,7 +571,7 @@ class SampledFromStrategy(SearchStrategy[Ex]): self.force_repr_braces = force_repr_braces self._transformations = transformations - self._cached_repr: Optional[str] = None + self._cached_repr: str | None = None def map(self, pack: Callable[[Ex], T]) -> SearchStrategy[T]: s = type(self)( @@ -697,13 +678,13 @@ class SampledFromStrategy(SearchStrategy[Ex]): # anywhere in the class so this is still type-safe. mypy is being more # conservative than necessary element: Ex, # type: ignore - ) -> Union[Ex, UniqueIdentifier]: + ) -> Ex | UniqueIdentifier: # Used in UniqueSampledListStrategy for name, f in self._transformations: if name == "map": result = f(element) if build_context := _current_build_context.value: - build_context.record_call(result, f, [element], {}) + build_context.record_call(result, f, args=[element], kwargs={}) element = result else: assert name == "filter" @@ -717,7 +698,7 @@ class SampledFromStrategy(SearchStrategy[Ex]): isinstance(x, SearchStrategy) for x in self.elements ): data._sampled_from_all_strategies_elements_message = ( - "sample_from was given a collection of strategies: " + "sampled_from was given a collection of strategies: " "{!r}. Was one_of intended?", self.elements, ) @@ -726,10 +707,10 @@ class SampledFromStrategy(SearchStrategy[Ex]): assert not isinstance(result, UniqueIdentifier) return result - def get_element(self, i: int) -> Union[Ex, UniqueIdentifier]: + def get_element(self, i: int) -> Ex | UniqueIdentifier: return self._transform(self.elements[i]) - def do_filtered_draw(self, data: ConjectureData) -> Union[Ex, UniqueIdentifier]: + def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: # Set of indices that have been tried so far, so that we never test # the same element twice during a draw. known_bad_indices: set[int] = set() @@ -799,8 +780,9 @@ class OneOfStrategy(SearchStrategy[Ex]): def __init__(self, strategies: Sequence[SearchStrategy[Ex]]): super().__init__() self.original_strategies = tuple(strategies) - self.__element_strategies: Optional[Sequence[SearchStrategy[Ex]]] = None + self.__element_strategies: Sequence[SearchStrategy[Ex]] | None = None self.__in_branches = False + self._branches_lock = RLock() def calc_is_empty(self, recur: RecurT) -> bool: return all(recur(e) for e in self.original_strategies) @@ -849,7 +831,7 @@ 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: not s.is_currently_empty(data) ) ) return data.draw(strategy) @@ -863,14 +845,19 @@ class OneOfStrategy(SearchStrategy[Ex]): @property def branches(self) -> Sequence[SearchStrategy[Ex]]: - if not self.__in_branches: - try: - self.__in_branches = True - return self.element_strategies - finally: - self.__in_branches = False - else: - return [self] + if self.__element_strategies is not None: + # common fast path which avoids the lock + return self.element_strategies + + with self._branches_lock: + if not self.__in_branches: + try: + self.__in_branches = True + return self.element_strategies + finally: + self.__in_branches = False + else: + return [self] def filter(self, condition: Callable[[Ex], Any]) -> SearchStrategy[Ex]: return FilteredStrategy( @@ -894,14 +881,14 @@ def one_of(__a1: SearchStrategy[Ex]) -> SearchStrategy[Ex]: # pragma: no cover @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T] -) -> SearchStrategy[Union[Ex, T]]: # pragma: no cover +) -> SearchStrategy[Ex | T]: # pragma: no cover ... @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3] -) -> SearchStrategy[Union[Ex, T, T3]]: # pragma: no cover +) -> SearchStrategy[Ex | T | T3]: # pragma: no cover ... @@ -911,7 +898,7 @@ def one_of( __a2: SearchStrategy[T], __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], -) -> SearchStrategy[Union[Ex, T, T3, T4]]: # pragma: no cover +) -> SearchStrategy[Ex | T | T3 | T4]: # pragma: no cover ... @@ -922,7 +909,7 @@ def one_of( __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], __a5: SearchStrategy[T5], -) -> SearchStrategy[Union[Ex, T, T3, T4, T5]]: # pragma: no cover +) -> SearchStrategy[Ex | T | T3 | T4 | T5]: # pragma: no cover ... @@ -933,7 +920,7 @@ def one_of(*args: SearchStrategy[Any]) -> SearchStrategy[Any]: # pragma: no cov @defines_strategy(never_lazy=True) def one_of( - *args: Union[Sequence[SearchStrategy[Any]], SearchStrategy[Any]] + *args: Sequence[SearchStrategy[Any]] | SearchStrategy[Any], ) -> SearchStrategy[Any]: # Mypy workaround alert: Any is too loose above; the return parameter # should be the union of the input parameters. Unfortunately, Mypy <=0.600 @@ -1023,7 +1010,9 @@ class MappedStrategy(SearchStrategy[MappedTo], Generic[MappedFrom, MappedTo]): x = data.draw(self.mapped_strategy) result = self.pack(x) data.stop_span() - current_build_context().record_call(result, self.pack, [x], {}) + current_build_context().record_call( + result, self.pack, args=[x], kwargs={} + ) return result except UnsatisfiedAssumption: data.stop_span(discard=True) @@ -1117,7 +1106,7 @@ class FilteredStrategy(SearchStrategy[Ex]): assert isinstance(self.flat_conditions, tuple) assert not isinstance(self.filtered_strategy, FilteredStrategy) - self.__condition: Optional[Callable[[Ex], Any]] = None + self.__condition: Callable[[Ex], Any] | None = None def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.filtered_strategy) @@ -1175,18 +1164,24 @@ class FilteredStrategy(SearchStrategy[Ex]): @property def condition(self) -> Callable[[Ex], Any]: - if self.__condition is None: - if len(self.flat_conditions) == 1: - # Avoid an extra indirection in the common case of only one condition. - self.__condition = self.flat_conditions[0] - elif len(self.flat_conditions) == 0: - # Possible, if unlikely, due to filter predicate rewriting - self.__condition = lambda _: True # type: ignore # covariant type param - else: - self.__condition = lambda x: all( # type: ignore # covariant type param - cond(x) for cond in self.flat_conditions - ) - return self.__condition + # We write this defensively to avoid any threading race conditions + # with our manual FilteredStrategy.__init__ for filter-rewriting. + # See https://github.com/HypothesisWorks/hypothesis/pull/4522. + if (condition := self.__condition) is not None: + return condition + + if len(self.flat_conditions) == 1: + # Avoid an extra indirection in the common case of only one condition. + condition = self.flat_conditions[0] + elif len(self.flat_conditions) == 0: + # Possible, if unlikely, due to filter predicate rewriting + condition = lambda _: True # type: ignore # covariant type param + else: + condition = lambda x: all( # type: ignore # covariant type param + cond(x) for cond in self.flat_conditions + ) + self.__condition = condition + return condition def do_draw(self, data: ConjectureData) -> Ex: result = self.do_filtered_draw(data) @@ -1195,7 +1190,7 @@ class FilteredStrategy(SearchStrategy[Ex]): data.mark_invalid(f"Aborted test because unable to satisfy {self!r}") - def do_filtered_draw(self, data: ConjectureData) -> Union[Ex, UniqueIdentifier]: + def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: for i in range(3): data.start_span(FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL) value = data.draw(self.filtered_strategy) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py index 352d8577ca9..9b4020e20ee 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py @@ -11,11 +11,13 @@ import copy import re import warnings +from collections.abc import Collection from functools import cache, lru_cache, partial -from typing import Optional +from typing import cast from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal import charmap +from hypothesis.internal.charmap import Categories from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.filtering import max_len, min_len @@ -33,7 +35,9 @@ from hypothesis.vendor.pretty import pretty # Cache size is limited by sys.maxunicode, but passing None makes it slightly faster. @cache -def _check_is_single_character(c): +# this is part of our forward-facing validation, so we do *not* tell mypyc that c +# should be a str, because we don't want it to validate it before we can. +def _check_is_single_character(c: object) -> str: # 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. if not isinstance(c, str): @@ -47,9 +51,7 @@ def _check_is_single_character(c): class OneCharStringStrategy(SearchStrategy[str]): """A strategy which generates single character strings of text type.""" - def __init__( - self, intervals: IntervalSet, force_repr: Optional[str] = None - ) -> None: + def __init__(self, intervals: IntervalSet, force_repr: str | None = None) -> None: super().__init__() assert isinstance(intervals, IntervalSet) self.intervals = intervals @@ -59,13 +61,13 @@ class OneCharStringStrategy(SearchStrategy[str]): def from_characters_args( cls, *, - codec=None, - min_codepoint=None, - max_codepoint=None, - categories=None, - exclude_characters=None, - include_characters=None, - ): + codec: str | None = None, + min_codepoint: int | None = None, + max_codepoint: int | None = None, + categories: Categories | None = None, + exclude_characters: Collection[str] = "", + include_characters: Collection[str] = "", + ) -> "OneCharStringStrategy": assert set(categories or ()).issubset(charmap.categories()) intervals = charmap.query( min_codepoint=min_codepoint, @@ -88,7 +90,11 @@ class OneCharStringStrategy(SearchStrategy[str]): ("include_characters", include_characters), ] if v not in (None, "") - and not (k == "categories" and set(v) == set(charmap.categories()) - {"Cs"}) + and not ( + k == "categories" + # v has to be `categories` here. Help mypy along to infer that. + and set(cast(Categories, v)) == set(charmap.categories()) - {"Cs"} + ) ) if not intervals: raise InvalidArgument( @@ -98,7 +104,7 @@ class OneCharStringStrategy(SearchStrategy[str]): return cls(intervals, force_repr=f"characters({_arg_repr})") @classmethod - def from_alphabet(cls, alphabet): + def from_alphabet(cls, alphabet: str | SearchStrategy) -> "OneCharStringStrategy": if isinstance(alphabet, str): return cls.from_characters_args(categories=(), include_characters=alphabet) @@ -320,7 +326,7 @@ _PROPLIST = """ @lru_cache -def _identifier_characters(): +def _identifier_characters() -> tuple[IntervalSet, IntervalSet]: """See https://docs.python.org/3/reference/lexical_analysis.html#identifiers""" # Start by computing the set of special characters chars = {"Other_ID_Start": "", "Other_ID_Continue": ""} @@ -349,14 +355,14 @@ def _identifier_characters(): class BytesStrategy(SearchStrategy): - def __init__(self, min_size: int, max_size: Optional[int]): + def __init__(self, min_size: int, max_size: int | None): super().__init__() self.min_size = min_size self.max_size = ( max_size if max_size is not None else COLLECTION_DEFAULT_MAX_SIZE ) - def do_draw(self, data): + def do_draw(self, data: ConjectureData) -> bytes: return data.draw_bytes(self.min_size, self.max_size) _nonempty_filters = ( diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py index 9ac5f534eac..9965bffe2c9 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py @@ -32,7 +32,7 @@ from collections.abc import Iterator from functools import partial from pathlib import PurePath from types import FunctionType -from typing import TYPE_CHECKING, Any, get_args, get_origin +from typing import TYPE_CHECKING, Any, NewType, get_args, get_origin from hypothesis import strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument, ResolutionFailed @@ -210,11 +210,7 @@ def type_sorting_key(t): if t is None or t is type(None): return (-1, repr(t)) t = get_origin(t) or t - try: - is_container = int(issubclass(t, collections.abc.Container)) - except Exception: # pragma: no cover - # e.g. `typing_extensions.Literal` is not a container - is_container = 0 + is_container = int(try_issubclass(t, collections.abc.Container)) return (is_container, repr(t)) @@ -228,7 +224,7 @@ def _compatible_args(args, superclass_args): # good enough for all the cases that I've seen so far and has the # substantial virtue of (relative) simplicity. a == b or isinstance(a, typing.TypeVar) or isinstance(b, typing.TypeVar) - for a, b in zip(args, superclass_args) + for a, b in zip(args, superclass_args, strict=True) ) @@ -246,8 +242,6 @@ def try_issubclass(thing, superclass): # generics, and so on. If you need to change this code, read PEP-560 # and Hypothesis issue #2951 closely first, and good luck. The tests # will help you, I hope - good luck. - if getattr(thing, "__args__", None) is not None: - return True # pragma: no cover # only possible on Python <= 3.9 for orig_base in getattr(thing, "__orig_bases__", None) or [None]: args = getattr(orig_base, "__args__", None) if _compatible_args(args, superclass_args): @@ -258,20 +252,6 @@ def try_issubclass(thing, superclass): return False -def is_a_new_type(thing): - if not isinstance(typing.NewType, type): - # At runtime, `typing.NewType` returns an identity function rather - # than an actual type, but we can check whether that thing matches. - return ( # pragma: no cover # Python <= 3.9 only - hasattr(thing, "__supertype__") - and getattr(thing, "__module__", None) in ("typing", "typing_extensions") - and inspect.isfunction(thing) - ) - # In 3.10 and later, NewType is actually a class - which simplifies things. - # See https://bugs.python.org/issue44353 for links to the various patches. - return isinstance(thing, typing.NewType) - - def is_a_type_alias_type(thing): # pragma: no cover # covered by 3.12+ tests # TypeAliasType is new in python 3.12, through the type statement. If we're # before python 3.12 then this can't possibly by a TypeAliasType. @@ -289,12 +269,18 @@ def is_a_union(thing: object) -> bool: def is_a_type(thing: object) -> bool: - """Return True if thing is a type or a generic type like thing.""" + """ + Return True if thing is a type or a typing-like thing (union, generic type, etc). + """ return ( isinstance(thing, type) or is_generic_type(thing) - or is_a_new_type(thing) + or isinstance(thing, NewType) or is_a_type_alias_type(thing) + # union and forwardref checks necessary from 3.14+. Before 3.14, they + # were covered by is_generic_type(thing). + or is_a_union(thing) + or isinstance(thing, typing.ForwardRef) ) @@ -452,7 +438,7 @@ def _try_import_forward_ref(thing, typ, *, type_params): # pragma: no cover def from_typing_type(thing): - # We start with Final, Literal, and Annotated since they don't support `isinstance`. + # We start with Final, Literal, and Annotated, since they don't support `isinstance`. # # We then explicitly error on non-Generic types, which don't carry enough # information to sensibly resolve to strategies at runtime. @@ -506,6 +492,7 @@ def from_typing_type(thing): if len(mapping) > 1: _Environ = getattr(os, "_Environ", None) mapping.pop(_Environ, None) + tuple_types = [ t for t in mapping @@ -520,10 +507,40 @@ def from_typing_type(thing): # to fail, due to weird isinstance behaviour around the elements. mapping.pop(collections.abc.ItemsView, None) mapping.pop(typing.ItemsView, None) - if {collections.deque}.intersection(mapping) and len(mapping) > 1: + if collections.deque in mapping and len(mapping) > 1: # Resolving generic sequences to include a deque is more trouble for e.g. # the ghostwriter than it's worth, via undefined names in the repr. - mapping.pop(collections.deque, None) + mapping.pop(collections.deque) + + if ( + memoryview in mapping + and getattr(thing, "__args__", None) + and not hasattr(thing.__args__[0], "__buffer__") + ): # pragma: no cover # covered by 3.14+ + # Both memoryview and list are direct subclasses of Sequence. If we ask for + # st.from_type(Sequence[A]), we will get both list[A] and memoryview[A]. + # But unless A implements the buffer protocol with __buffer__, resolving + # memoryview[A] will error. + # + # Since the user didn't explicitly ask for memoryview, there's no reason + # to expect them to have implemented __buffer__. Remove memoryview in this + # case, before it can fail at resolution-time. + # + # Note: I intentionally did not add a `and len(mapping) > 1` condition here. + # If memoryview[A] is the only resolution for a strategy, but A is not a + # buffer protocol, our options are to (1) pop memoryview and raise + # ResolutionFailed, or (2) to keep memoryview in the mapping and error in + # resolve_memoryview. A failure in test_resolving_standard_contextmanager_as_generic + # (because memoryview is a context manager in 3.14) convinced me the former + # was less confusing to users. + mapping.pop(memoryview) + + elem_type = (getattr(thing, "__args__", None) or ["not int"])[0] + union_elems = elem_type.__args__ if is_a_union(elem_type) else () + allows_integer_elements = any( + isinstance(T, type) and try_issubclass(int, get_origin(T) or T) + for T in [*union_elems, elem_type] + ) if len(mapping) > 1: # issubclass treats bytestring as a kind of sequence, which it is, @@ -535,17 +552,7 @@ def from_typing_type(thing): # This block drops bytes from the types that can be generated # if there is more than one allowed type, and the element type is # not either `int` or a Union with `int` as one of its elements. - elem_type = (getattr(thing, "__args__", None) or ["not int"])[0] - if is_a_union(elem_type): - union_elems = elem_type.__args__ - else: - union_elems = () - if not any( - # see https://github.com/HypothesisWorks/hypothesis/issues/4194 for - # try_issubclass. - isinstance(T, type) and try_issubclass(int, get_origin(T) or T) - for T in [*union_elems, elem_type] - ): + if not allows_integer_elements: mapping.pop(bytes, None) if sys.version_info[:2] <= (3, 13): mapping.pop(collections.abc.ByteString, None) @@ -555,16 +562,51 @@ def from_typing_type(thing): and thing.__forward_arg__ in vars(builtins) ): return st.from_type(getattr(builtins, thing.__forward_arg__)) + + def is_maximal(t): + # For each k in the mapping, we use it if it's the most general type + # available, and exclude any more specific types. So if both + # Sequence and Collection are available, we use the most general Collection + # type. + # + # k being "the most general" is equivalent to saying that k is maximal + # in the partial ordering of types. Note that since the ordering is + # partial there may be multiple maximal elements. (This distinguishes + # maximal from maximum). + return sum(try_issubclass(t, T) for T in mapping) == 1 + + strategies = [ + (t, s if isinstance(s, st.SearchStrategy) else s(thing)) + for t, s in mapping.items() + if is_maximal(t) + ] + strategies = [(t, s) for t, s in strategies if s != NotImplemented] + + # 3.14+ removes typing.ByteString. typing.ByteString was the only reason we + # previously generated bytes for Sequence[int]. There is no equivalent + # for typing.ByteString in 3.14+, but we would still like to generate bytes + # for Sequence[int] and its supertypes. Special case that here. + if ( + sys.version_info[:2] >= (3, 14) + and allows_integer_elements + # For the same reason as the is_maximal check above, we only include + # this ByteString special case if it is not overridden by a more general + # available type. + # + # collections.abc.ByteString was a direct subclass of Sequence, so we + # use that as the standin type when checking. Note we compare to a count + # of 0, instead of 1, since in is_maximal `k` is already in `mapping`, + # and we expect `try_issubclass(k, k) == True`. + and try_issubclass(collections.abc.Sequence, thing) + and sum(try_issubclass(collections.abc.Sequence, T) for T in mapping) == 0 + ): # pragma: no cover # covered on 3.14+ + strategies.append((collections.abc.Sequence, st.binary())) + # Sort strategies according to our type-sorting heuristic for stable output strategies = [ - s - for s in ( - v if isinstance(v, st.SearchStrategy) else v(thing) - for k, v in sorted(mapping.items(), key=lambda kv: type_sorting_key(kv[0])) - if sum(try_issubclass(k, T) for T in mapping) == 1 - ) - if s != NotImplemented + s for _k, s in sorted(strategies, key=lambda kv: type_sorting_key(kv[0])) ] + empty = ", ".join(repr(s) for s in strategies if s.is_empty) if empty or not strategies: raise ResolutionFailed( @@ -610,7 +652,7 @@ utc_offsets = st.builds( # returned without being listed in a function signature: # https://github.com/python/mypy/issues/6710#issuecomment-485580032 _global_type_lookup: dict[ - type, typing.Union[st.SearchStrategy, typing.Callable[[type], st.SearchStrategy]] + type, st.SearchStrategy | typing.Callable[[type], st.SearchStrategy] ] = { type(None): st.none(), bool: st.booleans(), @@ -638,7 +680,6 @@ _global_type_lookup: dict[ type(Ellipsis): st.just(Ellipsis), type(NotImplemented): st.just(NotImplemented), bytearray: st.binary().map(bytearray), - memoryview: st.binary().map(memoryview), numbers.Real: st.floats(), numbers.Rational: st.fractions(), numbers.Number: st.complex_numbers(), @@ -723,13 +764,15 @@ _fallback_type_strategy = st.sampled_from( # includes this... but we don't actually ever want to build one. _global_type_lookup[os._Environ] = st.just(os.environ) -if sys.version_info[:2] <= (3, 13): +if sys.version_info[:2] < (3, 14): # Note: while ByteString notionally also represents the bytearray and # memoryview types, it is a subclass of Hashable and those types are not. # We therefore only generate the bytes type. type-ignored due to deprecation. _global_type_lookup[typing.ByteString] = st.binary() # type: ignore _global_type_lookup[collections.abc.ByteString] = st.binary() # type: ignore + _global_type_lookup[memoryview] = st.binary().map(memoryview) + _global_type_lookup.update( { @@ -793,16 +836,14 @@ _global_type_lookup.update( # installed. To avoid the performance hit of importing anything here, we defer # it until the method is called the first time, at which point we replace the # entry in the lookup table with the direct call. -def _from_numpy_type(thing: type) -> typing.Optional[st.SearchStrategy]: +def _from_numpy_type(thing: type) -> st.SearchStrategy | None: from hypothesis.extra.numpy import _from_type _global_extra_lookup["numpy"] = _from_type return _from_type(thing) -_global_extra_lookup: dict[ - str, typing.Callable[[type], typing.Optional[st.SearchStrategy]] -] = { +_global_extra_lookup: dict[str, typing.Callable[[type], st.SearchStrategy | None]] = { "numpy": _from_numpy_type, } @@ -949,7 +990,7 @@ def resolve_Iterator(thing): return st.iterables(st.from_type(thing.__args__[0])) -@register(typing.Counter, st.builds(collections.Counter)) +@register(collections.Counter, st.builds(collections.Counter)) def resolve_Counter(thing): return st.dictionaries( keys=st.from_type(thing.__args__[0]), @@ -957,17 +998,17 @@ def resolve_Counter(thing): ).map(collections.Counter) -@register(typing.Deque, st.builds(collections.deque)) +@register(collections.deque, st.builds(collections.deque)) def resolve_deque(thing): return st.lists(st.from_type(thing.__args__[0])).map(collections.deque) -@register(typing.ChainMap, st.builds(dict).map(collections.ChainMap)) +@register(collections.ChainMap, st.builds(dict).map(collections.ChainMap)) def resolve_ChainMap(thing): return resolve_Dict(thing).map(collections.ChainMap) -@register(typing.OrderedDict, st.builds(dict).map(collections.OrderedDict)) +@register(collections.OrderedDict, st.builds(dict).map(collections.OrderedDict)) def resolve_OrderedDict(thing): return resolve_Dict(thing).map(collections.OrderedDict) @@ -1104,3 +1145,13 @@ def resolve_TypeVar(thing): ), key=type_var_key, ).flatmap(st.from_type) + + +if sys.version_info[:2] >= (3, 14): + # memoryview is newly generic in 3.14. see + # https://github.com/python/cpython/issues/126012 + # and https://docs.python.org/3/library/stdtypes.html#memoryview + + @register(memoryview, st.binary().map(memoryview)) + def resolve_memoryview(thing): + return st.from_type(thing.__args__[0]).map(memoryview) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py index dc75cc83586..6c5ec635c6a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/utils.py @@ -10,10 +10,10 @@ import dataclasses import sys -import threading +from collections.abc import Callable from functools import partial -from inspect import signature -from typing import TYPE_CHECKING, Callable +from typing import TypeAlias, TypeVar +from weakref import WeakValueDictionary import attr @@ -22,49 +22,29 @@ from hypothesis.internal.floats import clamp, float_to_int from hypothesis.internal.reflection import proxies from hypothesis.vendor.pretty import pretty -if TYPE_CHECKING: - from hypothesis.strategies._internal.strategies import SearchStrategy, T +T = TypeVar("T") +ValueKey: TypeAlias = tuple[type, object] +# (fn, args, kwargs) +StrategyCacheKey: TypeAlias = tuple[ + object, tuple[ValueKey, ...], frozenset[tuple[str, ValueKey]] +] -_strategies: dict[str, Callable[..., "SearchStrategy"]] = {} +_all_strategies: WeakValueDictionary[str, Callable] = WeakValueDictionary() +# note: LRUReusedCache is already thread-local internally +_STRATEGY_CACHE = LRUReusedCache[StrategyCacheKey, object](1024) -class FloatKey: - def __init__(self, f): - self.value = float_to_int(f) - - def __eq__(self, other): - return isinstance(other, FloatKey) and (other.value == self.value) - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.value) - - -def convert_value(v): +def convert_value(v: object) -> ValueKey: if isinstance(v, float): - return FloatKey(v) + return (float, float_to_int(v)) return (type(v), v) -_CACHE = threading.local() - - -def get_cache() -> LRUReusedCache: - try: - return _CACHE.STRATEGY_CACHE - except AttributeError: - _CACHE.STRATEGY_CACHE = LRUReusedCache(1024) - return _CACHE.STRATEGY_CACHE - - def clear_cache() -> None: - cache = get_cache() - cache.clear() + _STRATEGY_CACHE.clear() -def cacheable(fn: "T") -> "T": +def cacheable(fn: T) -> T: from hypothesis.control import _current_build_context from hypothesis.strategies._internal.strategies import SearchStrategy @@ -79,17 +59,15 @@ def cacheable(fn: "T") -> "T": except TypeError: return fn(*args, **kwargs) cache_key = (fn, tuple(map(convert_value, args)), frozenset(kwargs_cache_key)) - cache = get_cache() try: - if cache_key in cache: - return cache[cache_key] + if cache_key in _STRATEGY_CACHE: + return _STRATEGY_CACHE[cache_key] except TypeError: return fn(*args, **kwargs) else: result = fn(*args, **kwargs) if not isinstance(result, SearchStrategy) or result.is_cacheable: - result._is_singleton = True - cache[cache_key] = result + _STRATEGY_CACHE[cache_key] = result return result cached_strategy.__clear_cache = clear_cache # type: ignore @@ -101,7 +79,7 @@ def defines_strategy( force_reusable_values: bool = False, try_non_lazy: bool = False, never_lazy: bool = False, -) -> Callable[["T"], "T"]: +) -> Callable[[T], T]: """Returns a decorator for strategy functions. If ``force_reusable_values`` is True, the returned strategy will be marked @@ -120,7 +98,7 @@ def defines_strategy( def decorator(strategy_definition): """A decorator that registers the function as a strategy and makes it lazily evaluated.""" - _strategies[strategy_definition.__name__] = signature(strategy_definition) + _all_strategies[strategy_definition.__name__] = strategy_definition if never_lazy: assert not try_non_lazy @@ -158,13 +136,7 @@ def defines_strategy( return decorator -def to_jsonable(obj: object, *, avoid_realization: bool) -> object: - """Recursively convert an object to json-encodable form. - - This is not intended to round-trip, but rather provide an analysis-ready - format for observability. To avoid side affects, we pretty-print all but - known types. - """ +def _to_jsonable(obj: object, *, avoid_realization: bool, seen: set[int]) -> object: 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 @@ -181,7 +153,13 @@ def to_jsonable(obj: object, *, avoid_realization: bool) -> object: if avoid_realization: return "<symbolic>" - recur = partial(to_jsonable, avoid_realization=avoid_realization) + obj_id = id(obj) + if obj_id in seen: + return pretty(obj, cycle=True) + + recur = partial( + _to_jsonable, avoid_realization=avoid_realization, seen=seen | {obj_id} + ) if isinstance(obj, (list, tuple, set, frozenset)): if isinstance(obj, tuple) and hasattr(obj, "_asdict"): return recur(obj._asdict()) # treat namedtuples as dicts @@ -218,3 +196,13 @@ def to_jsonable(obj: object, *, avoid_realization: bool) -> object: # If all else fails, we'll just pretty-print as a string. return pretty(obj) + + +def to_jsonable(obj: object, *, avoid_realization: bool) -> object: + """Recursively convert an object to json-encodable form. + + This is not intended to round-trip, but rather provide an analysis-ready + format for observability. To avoid side affects, we pretty-print all but + known types. + """ + return _to_jsonable(obj, avoid_realization=avoid_realization, seen=set()) diff --git a/contrib/python/hypothesis/py3/hypothesis/utils/threading.py b/contrib/python/hypothesis/py3/hypothesis/utils/threading.py index d9c2e550c94..8dbbb3263e2 100644 --- a/contrib/python/hypothesis/py3/hypothesis/utils/threading.py +++ b/contrib/python/hypothesis/py3/hypothesis/utils/threading.py @@ -9,7 +9,8 @@ # obtain one at https://mozilla.org/MPL/2.0/. import threading -from typing import Any, Callable +from collections.abc import Callable +from typing import Any class ThreadLocal: diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py index b96818a5221..83fab2406d7 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py @@ -70,21 +70,19 @@ import sys import types import warnings from collections import Counter, OrderedDict, defaultdict, deque -from collections.abc import Generator, Iterable, Sequence +from collections.abc import Callable, Generator, Iterable, Sequence from contextlib import contextmanager, suppress from enum import Enum, Flag from functools import partial from io import StringIO, TextIOBase from math import copysign, isnan -from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Optional, TypeAlias, TypeVar if TYPE_CHECKING: - from typing import TypeAlias - from hypothesis.control import BuildContext T = TypeVar("T") -PrettyPrintFunction: "TypeAlias" = Callable[[Any, "RepresentationPrinter", bool], None] +PrettyPrintFunction: TypeAlias = Callable[[Any, "RepresentationPrinter", bool], None] __all__ = [ "IDKey", @@ -93,7 +91,7 @@ __all__ = [ ] -def _safe_getattr(obj: object, attr: str, default: Optional[Any] = None) -> Any: +def _safe_getattr(obj: object, attr: str, default: Any | None = None) -> Any: """Safe version of getattr. Same as getattr, but will return ``default`` on any Exception, @@ -106,10 +104,10 @@ def _safe_getattr(obj: object, attr: str, default: Optional[Any] = None) -> Any: return default -def pretty(obj: object) -> str: +def pretty(obj: object, *, cycle: bool = False) -> str: """Pretty print the object's representation.""" printer = RepresentationPrinter() - printer.pretty(obj) + printer.pretty(obj, cycle=cycle) return printer.getvalue() @@ -136,7 +134,7 @@ class RepresentationPrinter: def __init__( self, - output: Optional[TextIOBase] = None, + output: TextIOBase | None = None, *, context: Optional["BuildContext"] = None, ) -> None: @@ -152,7 +150,7 @@ class RepresentationPrinter: self.max_seq_length: int = 1000 self.output_width: int = 0 self.buffer_width: int = 0 - self.buffer: deque[Union[Breakable, Text]] = deque() + self.buffer: deque[Breakable | Text] = deque() root_group = Group(0) self.group_stack = [root_group] @@ -189,10 +187,10 @@ class RepresentationPrinter: self.slice_comments = context.data.slice_comments assert all(isinstance(k, IDKey) for k in self.known_object_printers) - def pretty(self, obj: object) -> None: + def pretty(self, obj: object, *, cycle: bool = False) -> None: """Pretty print the given object.""" obj_id = id(obj) - cycle = obj_id in self.stack + cycle = cycle or obj_id in self.stack self.stack.append(obj_id) try: with self.group(): @@ -442,9 +440,9 @@ class RepresentationPrinter: args: Sequence[object], kwargs: dict[str, object], *, - force_split: Optional[bool] = None, - arg_slices: Optional[dict[str, tuple[int, int]]] = None, - leading_comment: Optional[str] = None, + force_split: bool | None = None, + arg_slices: dict[str, tuple[int, int]] | None = None, + leading_comment: str | None = None, avoid_realization: bool = False, ) -> None: """Helper function to represent a function call. @@ -462,7 +460,7 @@ class RepresentationPrinter: all_args = [(None, v) for v in args] + list(kwargs.items()) # int indicates the position of a positional argument, rather than a keyword # argument. Currently no callers use this; see #3624. - comments: dict[Union[int, str], object] = { + comments: dict[int | str, object] = { k: self.slice_comments[v] for k, v in (arg_slices or {}).items() if v in self.slice_comments @@ -568,7 +566,7 @@ class GroupQueue: self.queue.append([]) self.queue[depth].append(group) - def deq(self) -> Optional[Group]: + def deq(self) -> Group | None: for stack in self.queue: for idx, group in enumerate(reversed(stack)): if group.breakables: @@ -594,7 +592,7 @@ def _seq_pprinter_factory(start: str, end: str, basetype: type) -> PrettyPrintFu """ def inner( - obj: Union[tuple[object], list[object]], p: RepresentationPrinter, cycle: bool + obj: tuple[object] | list[object], p: RepresentationPrinter, cycle: bool ) -> None: typ = type(obj) if ( @@ -634,7 +632,7 @@ def _set_pprinter_factory( frozensets.""" def inner( - obj: Union[set[Any], frozenset[Any]], + obj: set[Any] | frozenset[Any], p: RepresentationPrinter, cycle: bool, ) -> None: @@ -673,7 +671,7 @@ def _set_pprinter_factory( def _dict_pprinter_factory( - start: str, end: str, basetype: Optional[type[object]] = None + start: str, end: str, basetype: type[object] | None = None ) -> PrettyPrintFunction: """Factory that returns a pprint function used by the default pprint of dicts and dict proxies.""" @@ -690,22 +688,21 @@ 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): + 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 # `.items()` because it might be a non-`dict` type of mapping. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", BytesWarning) - for idx, key in p._enumerate(obj): - if idx: - p.text(",") - p.breakable() - p.pretty(key) - p.text(": ") - p.pretty(obj[key]) + warnings.catch_warnings(), + ): + warnings.simplefilter("ignore", BytesWarning) + for idx, key in p._enumerate(obj): + if idx: + p.text(",") + p.breakable() + p.pretty(key) + p.text(": ") + p.pretty(obj[key]) inner.__name__ = f"_dict_pprinter_factory({start!r}, {end!r}, {basetype!r})" return inner @@ -805,7 +802,7 @@ def pprint_fields( def _function_pprint( - obj: Union[types.FunctionType, types.BuiltinFunctionType, types.MethodType], + obj: types.FunctionType | types.BuiltinFunctionType | types.MethodType, p: RepresentationPrinter, cycle: bool, ) -> None: @@ -885,7 +882,7 @@ _deferred_type_pprinters: dict[tuple[str, str], PrettyPrintFunction] = {} def for_type_by_name( type_module: str, type_name: str, func: PrettyPrintFunction -) -> Optional[PrettyPrintFunction]: +) -> PrettyPrintFunction | None: """Add a pretty printer for a type specified by the module and name of a type rather than the type object itself.""" key = (type_module, type_name) diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt index b534e60ce4d..99be72b8788 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt @@ -1,4 +1,4 @@ -# Version 2025030100, Last Updated Sat Mar 1 07:07:02 2025 UTC +# Version 2025083000, Last Updated Sat Aug 30 07:07:01 2025 UTC AAA AARP ABB @@ -118,7 +118,6 @@ BE BEATS BEAUTY BEER -BENTLEY BERLIN BEST BESTBUY @@ -658,7 +657,6 @@ LA LACAIXA LAMBORGHINI LAMER -LANCASTER LAND LANDROVER LANXESS @@ -914,7 +912,6 @@ POLITIE PORN POST PR -PRAMERICA PRAXI PRESS PRIME @@ -948,7 +945,6 @@ REALTOR REALTY RECIPES RED -REDSTONE REDUMBRELLA REHAB REISE diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index 01f9058ab9b..c18a9cfb689 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, 136, 2) +__version_info__ = (6, 143, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index eb4ab3a2337..ed19ada6367 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.136.2) +VERSION(6.143.0) LICENSE(MPL-2.0) @@ -87,6 +87,7 @@ PY_SRCS( hypothesis/internal/floats.py hypothesis/internal/healthcheck.py hypothesis/internal/intervalsets.py + hypothesis/internal/lambda_sources.py hypothesis/internal/observability.py hypothesis/internal/reflection.py hypothesis/internal/scrutineer.py |
