diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..34de7f7271 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This patch fixes a significant slowdown when using the :func:`~hypothesis.stateful.precondition` decorator in some cases, due to expensive repr formatting internally (:issue:`3963`). diff --git a/hypothesis-python/src/RELEASE.rst b/hypothesis-python/src/RELEASE.rst deleted file mode 100644 index 373954606d..0000000000 --- a/hypothesis-python/src/RELEASE.rst +++ /dev/null @@ -1,3 +0,0 @@ -RELEASE_TYPE: patch - -This patch cleans up some internal code. diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py index c9ffa6eb2d..b3bb52c67e 100644 --- a/hypothesis-python/src/hypothesis/internal/reflection.py +++ b/hypothesis-python/src/hypothesis/internal/reflection.py @@ -27,8 +27,9 @@ 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 +from typing import TYPE_CHECKING, Any, Callable, MutableMapping from unittest.mock import _patch as PatchType +from weakref import WeakKeyDictionary from hypothesis.errors import HypothesisWarning from hypothesis.internal.compat import PYPY, is_typed_named_tuple @@ -39,6 +40,7 @@ from hypothesis.strategies._internal.strategies import T READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True" +LAMBDA_SOURCE_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary() def is_mock(obj): @@ -303,7 +305,7 @@ def visit_Lambda(self, node): SPACE_PRECEDES_CLOSE_BRACKET = re.compile(r" \)") -def extract_lambda_source(f): +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. @@ -439,6 +441,17 @@ def extract_lambda_source(f): 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): if isinstance(f, partial): return pretty(f) @@ -492,7 +505,7 @@ def repr_call(f, args, kwargs, *, reorder=True): if repr_len > 30000: warnings.warn( "Generating overly large repr. This is an expensive operation, and with " - f"a length of {repr_len//1000} kB is is unlikely to be useful. Use -Wignore " + f"a length of {repr_len//1000} kB is unlikely to be useful. Use -Wignore " "to ignore the warning, or -Werror to get a traceback.", HypothesisWarning, stacklevel=2, diff --git a/hypothesis-python/tests/cover/test_reflection.py b/hypothesis-python/tests/cover/test_reflection.py index 18b1262eff..3f9565e7be 100644 --- a/hypothesis-python/tests/cover/test_reflection.py +++ b/hypothesis-python/tests/cover/test_reflection.py @@ -541,12 +541,10 @@ def test_required_args(target, args, kwargs, expected): assert required_args(target, args, kwargs) == expected -# fmt: off -pi = "π"; is_str_pi = lambda x: x == pi # noqa: E702 -# fmt: on - - def test_can_handle_unicode_identifier_in_same_line_as_lambda_def(): + # fmt: off + pi = "π"; is_str_pi = lambda x: x == pi # noqa: E702 + # fmt: on assert get_pretty_function_description(is_str_pi) == "lambda x: x == pi" @@ -567,6 +565,9 @@ def test_does_not_crash_on_utf8_lambda_without_encoding(monkeypatch): # has to fall back to assuming it's ASCII. monkeypatch.setattr(reflection, "detect_encoding", None) + # fmt: off + pi = "π"; is_str_pi = lambda x: x == pi # noqa: E702 + # fmt: on assert get_pretty_function_description(is_str_pi) == "lambda x: "