From 08181fb62c1fe391c75c7487555c406982150180 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 28 Jun 2022 03:38:07 -0700 Subject: [PATCH] Upgrade define_function_signature --- hypothesis-python/src/hypothesis/core.py | 33 +++++- .../src/hypothesis/extra/django/_impl.py | 4 +- .../src/hypothesis/internal/reflection.py | 98 +--------------- .../hypothesis/strategies/_internal/core.py | 6 +- .../hypothesis/strategies/_internal/random.py | 10 +- .../tests/cover/test_annotations.py | 29 ++--- .../tests/cover/test_reflection.py | 107 ++++-------------- 7 files changed, 79 insertions(+), 208 deletions(-) diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 627511b2d9..8fb48fec8d 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -972,6 +972,34 @@ def fuzz_one_input( return self.__cached_target +def fullargspec_to_signature( + argspec: inspect.FullArgSpec, *, return_annotation: object = inspect.Parameter.empty +) -> inspect.Signature: + # Construct a new signature based on this argspec. We'll later convert everything + # over to explicit use of signature everywhere, but this is a nice stopgap. + + def as_param(name, kind, defaults): + return P( + name, + kind=kind, + default=defaults.get(name, P.empty), + annotation=argspec.annotations.get(name, P.empty), + ) + + params = [] + P = inspect.Parameter + for arg in argspec.args: + defaults = dict(zip(argspec.args[::-1], (argspec.defaults or [])[::-1])) + params.append(as_param(arg, P.POSITIONAL_OR_KEYWORD, defaults)) + if argspec.varargs: + params.append(as_param(argspec.varargs, P.VAR_POSITIONAL, {})) + for arg in argspec.kwonlyargs: + params.append(as_param(arg, P.KEYWORD_ONLY, argspec.kwonlydefaults)) + if argspec.varkw: + params.append(as_param(argspec.varkw, P.VAR_POSITIONAL, {})) + return inspect.Signature(params, return_annotation=return_annotation) + + @overload def given( *_given_arguments: Union[SearchStrategy[Any], InferType], @@ -1039,6 +1067,7 @@ def run_test_as_given(test): del given_arguments argspec = new_given_argspec(original_argspec, given_kwargs) + new_signature = fullargspec_to_signature(argspec) # Use type information to convert "infer" arguments into appropriate strategies. if infer in given_kwargs.values(): @@ -1049,7 +1078,7 @@ def run_test_as_given(test): # not when it's decorated. @impersonate(test) - @define_function_signature(test.__name__, test.__doc__, argspec) + @define_function_signature(test.__name__, test.__doc__, new_signature) def wrapped_test(*arguments, **kwargs): __tracebackhide__ = True raise InvalidArgument( @@ -1062,7 +1091,7 @@ def wrapped_test(*arguments, **kwargs): given_kwargs[name] = st.from_type(hints[name]) @impersonate(test) - @define_function_signature(test.__name__, test.__doc__, argspec) + @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 diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 4ad845a454..f5f7e7c8ab 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -22,7 +22,7 @@ from hypothesis import reject, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra.django._fields import from_field -from hypothesis.internal.reflection import define_function_signature_from_signature +from hypothesis.internal.reflection import define_function_signature from hypothesis.strategies._internal.utils import defines_strategy from hypothesis.utils.conventions import infer @@ -137,7 +137,7 @@ def from_model( sig = signature(from_model) params = list(sig.parameters.values()) params[0] = params[0].replace(kind=Parameter.POSITIONAL_ONLY) - from_model = define_function_signature_from_signature( + from_model = define_function_signature( name=from_model.__name__, docstring=from_model.__doc__, signature=sig.replace(parameters=params), diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py index 0484b76d3f..69cc10b61e 100644 --- a/hypothesis-python/src/hypothesis/internal/reflection.py +++ b/hypothesis-python/src/hypothesis/internal/reflection.py @@ -436,96 +436,6 @@ def source_exec_as_module(source): return result -COPY_ARGSPEC_SCRIPT = """ -from hypothesis.utils.conventions import not_set - -def accept({funcname}): - def {name}({argspec}): - return {funcname}({invocation}) - return {name} -""".lstrip() - - -def define_function_signature(name, docstring, argspec): - """A decorator which sets the name, argspec and docstring of the function - passed into it.""" - if name == "": - name = "_lambda_" - check_valid_identifier(name) - for a in argspec.args: - check_valid_identifier(a) - if argspec.varargs is not None: - check_valid_identifier(argspec.varargs) - if argspec.varkw is not None: - check_valid_identifier(argspec.varkw) - n_defaults = len(argspec.defaults or ()) - if n_defaults: - parts = [] - for a in argspec.args[:-n_defaults]: - parts.append(a) - for a in argspec.args[-n_defaults:]: - parts.append(f"{a}=not_set") - else: - parts = list(argspec.args) - used_names = list(argspec.args) + list(argspec.kwonlyargs) - used_names.append(name) - - for a in argspec.kwonlyargs: - check_valid_identifier(a) - - def accept(f): - fargspec = getfullargspec_except_self(f) - must_pass_as_kwargs = [] - invocation_parts = [] - for a in argspec.args: - if a not in fargspec.args and not fargspec.varargs: - must_pass_as_kwargs.append(a) # pragma: no cover - else: - invocation_parts.append(a) - if argspec.varargs: - used_names.append(argspec.varargs) - parts.append("*" + argspec.varargs) - invocation_parts.append("*" + argspec.varargs) - elif argspec.kwonlyargs: - parts.append("*") - for k in must_pass_as_kwargs: - invocation_parts.append(f"{k}={k}") # pragma: no cover - - for k in argspec.kwonlyargs: - invocation_parts.append(f"{k}={k}") - if k in (argspec.kwonlydefaults or []): - parts.append(f"{k}=not_set") - else: - parts.append(k) - if argspec.varkw: - used_names.append(argspec.varkw) - parts.append("**" + argspec.varkw) - invocation_parts.append("**" + argspec.varkw) - - candidate_names = ["f"] + [f"f_{i}" for i in range(1, len(used_names) + 2)] - - for funcname in candidate_names: # pragma: no branch - if funcname not in used_names: - break - - source = COPY_ARGSPEC_SCRIPT.format( - name=name, - funcname=funcname, - argspec=", ".join(parts), - invocation=", ".join(invocation_parts), - ) - result = source_exec_as_module(source).accept(f) - result.__doc__ = docstring - result.__defaults__ = argspec.defaults - if argspec.kwonlydefaults: - result.__kwdefaults__ = argspec.kwonlydefaults - if argspec.annotations: - result.__annotations__ = argspec.annotations - return result - - return accept - - COPY_SIGNATURE_SCRIPT = """ from hypothesis.utils.conventions import not_set @@ -543,13 +453,9 @@ def get_varargs(sig, kind=inspect.Parameter.VAR_POSITIONAL): return None -def define_function_signature_from_signature(name, docstring, signature): +def define_function_signature(name, docstring, signature): """A decorator which sets the name, argspec and docstring of the function passed into it.""" - # TODO: we will (eventually...) replace the last few uses of getfullargspec - # with this version, and then delete the one above. For now though, this - # works for @proxies() and @given() is under stricter constraints anyway. - check_valid_identifier(name) for a in signature.parameters: check_valid_identifier(a) @@ -653,7 +559,7 @@ def accept(f): def proxies(target: "T") -> Callable[[Callable], "T"]: - replace_sig = define_function_signature_from_signature( + replace_sig = define_function_signature( target.__name__.replace("", "_lambda_"), # type: ignore target.__doc__, get_signature(target, follow_wrapped=False), diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index c15af99cad..ee9723101a 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -56,7 +56,7 @@ ) from hypothesis.internal.entropy import get_seeder_and_restorer from hypothesis.internal.reflection import ( - define_function_signature_from_signature, + define_function_signature, get_pretty_function_description, get_signature, nicerepr, @@ -934,7 +934,7 @@ def builds( # matches the semantics of the function. Great for documentation! sig = signature(builds) args, kwargs = sig.parameters.values() - builds = define_function_signature_from_signature( + builds = define_function_signature( name=builds.__name__, docstring=builds.__doc__, signature=sig.replace( @@ -1543,7 +1543,7 @@ def composite(f: Callable[..., Ex]) -> Callable[..., SearchStrategy[Ex]]: ) @defines_strategy() - @define_function_signature_from_signature(f.__name__, f.__doc__, newsig) + @define_function_signature(f.__name__, f.__doc__, newsig) def accept(*args, **kwargs): return CompositeStrategy(f, args, kwargs) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/random.py b/hypothesis-python/src/hypothesis/strategies/_internal/random.py index e23f56e46a..2c8997e523 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/random.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/random.py @@ -17,7 +17,7 @@ from hypothesis.control import should_note from hypothesis.internal.conjecture import utils as cu -from hypothesis.internal.reflection import define_function_signature_from_signature +from hypothesis.internal.reflection import define_function_signature from hypothesis.reporting import report from hypothesis.strategies._internal.core import ( binary, @@ -133,11 +133,11 @@ def implementation(self, **kwargs): self._hypothesis_log_random(name, kwargs, result) return result - spec = inspect.signature(STUBS.get(name, target)) + sig = inspect.signature(STUBS.get(name, target)) - result = define_function_signature_from_signature( - target.__name__, target.__doc__, spec - )(implementation) + result = define_function_signature(target.__name__, target.__doc__, sig)( + implementation + ) result.__module__ = __name__ result.__qualname__ = "HypothesisRandom." + result.__name__ diff --git a/hypothesis-python/tests/cover/test_annotations.py b/hypothesis-python/tests/cover/test_annotations.py index 70fde8bfc7..63e3031feb 100644 --- a/hypothesis-python/tests/cover/test_annotations.py +++ b/hypothesis-python/tests/cover/test_annotations.py @@ -8,7 +8,7 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -from inspect import getfullargspec +from inspect import Parameter as P, signature import attr import pytest @@ -37,16 +37,10 @@ def has_annotation(a: int, *b, c=2) -> None: @pytest.mark.parametrize("f", [has_annotation, lambda *, a: a, lambda *, a=1: a]) def test_copying_preserves_argspec(f): - af = getfullargspec(f) + af = signature(f) t = define_function_signature("foo", "docstring", af)(universal_acceptor) - at = getfullargspec(t) - assert af.args == at.args[: len(af.args)] - assert af.varargs == at.varargs - assert af.varkw == at.varkw - assert len(af.defaults or ()) == len(at.defaults or ()) - assert af.kwonlyargs == at.kwonlyargs - assert af.kwonlydefaults == at.kwonlydefaults - assert af.annotations == at.annotations + at = signature(t) + assert af.parameters == at.parameters @pytest.mark.parametrize( @@ -102,17 +96,18 @@ def first_annot(draw: None): def test_composite_edits_annotations(): - spec_comp = getfullargspec(st.composite(pointless_composite)) - assert spec_comp.annotations["return"] == st.SearchStrategy[int] - assert "nothing" in spec_comp.annotations - assert "draw" not in spec_comp.annotations + sig_comp = signature(st.composite(pointless_composite)) + assert sig_comp.return_annotation == st.SearchStrategy[int] + assert sig_comp.parameters["nothing"].annotation is not P.empty + assert "draw" not in sig_comp.parameters @pytest.mark.parametrize("nargs", [1, 2, 3]) def test_given_edits_annotations(nargs): - spec_given = getfullargspec(given(*(nargs * [st.none()]))(pointless_composite)) - assert spec_given.annotations.pop("return") is None - assert len(spec_given.annotations) == 3 - nargs + sig_given = signature(given(*(nargs * [st.none()]))(pointless_composite)) + assert sig_given.return_annotation is None + assert len(sig_given.parameters) == 3 - nargs + assert all(p.annotation is not P.empty for p in sig_given.parameters.values()) def a_converter(x) -> int: diff --git a/hypothesis-python/tests/cover/test_reflection.py b/hypothesis-python/tests/cover/test_reflection.py index 420c849cef..58da9c3f06 100644 --- a/hypothesis-python/tests/cover/test_reflection.py +++ b/hypothesis-python/tests/cover/test_reflection.py @@ -12,7 +12,7 @@ from copy import deepcopy from datetime import time from functools import partial, wraps -from inspect import FullArgSpec, Parameter, Signature, getfullargspec +from inspect import Parameter, Signature, signature from unittest.mock import MagicMock, Mock, NonCallableMagicMock, NonCallableMock import pytest @@ -295,24 +295,18 @@ def has_kwargs(**kwargs): @pytest.mark.parametrize("f", [has_one_arg, has_two_args, has_varargs, has_kwargs]) -def test_copying_preserves_argspec(f): - af = getfullargspec(f) +def test_copying_preserves_signature(f): + af = get_signature(f) t = define_function_signature("foo", "docstring", af)(universal_acceptor) - at = getfullargspec(t) - assert af.args == at.args - assert af.varargs == at.varargs - assert af.varkw == at.varkw - assert len(af.defaults or ()) == len(at.defaults or ()) - assert af.kwonlyargs == at.kwonlyargs - assert af.kwonlydefaults == at.kwonlydefaults - assert af.annotations == at.annotations + at = get_signature(t) + assert af == at def test_name_does_not_clash_with_function_names(): def f(): pass - @define_function_signature("f", "A docstring for f", getfullargspec(f)) + @define_function_signature("f", "A docstring for f", signature(f)) def g(): pass @@ -321,29 +315,29 @@ def g(): def test_copying_sets_name(): f = define_function_signature( - "hello_world", "A docstring for hello_world", getfullargspec(has_two_args) + "hello_world", "A docstring for hello_world", signature(has_two_args) )(universal_acceptor) assert f.__name__ == "hello_world" def test_copying_sets_docstring(): f = define_function_signature( - "foo", "A docstring for foo", getfullargspec(has_two_args) + "foo", "A docstring for foo", signature(has_two_args) )(universal_acceptor) assert f.__doc__ == "A docstring for foo" def test_uses_defaults(): f = define_function_signature( - "foo", "A docstring for foo", getfullargspec(has_a_default) + "foo", "A docstring for foo", signature(has_a_default) )(universal_acceptor) assert f(3, 2) == ((3, 2, 1), {}) def test_uses_varargs(): - f = define_function_signature( - "foo", "A docstring for foo", getfullargspec(has_varargs) - )(universal_acceptor) + f = define_function_signature("foo", "A docstring for foo", signature(has_varargs))( + universal_acceptor + ) assert f(1, 2) == ((1, 2), {}) @@ -377,92 +371,39 @@ def accepts_everything(*args, **kwargs): define_function_signature( "hello", "A docstring for hello", - FullArgSpec( - args=("f",), - varargs=None, - varkw=None, - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ), + Signature(parameters=[Parameter("f", Parameter.POSITIONAL_OR_KEYWORD)]), )(accepts_everything)(1) define_function_signature( "hello", "A docstring for hello", - FullArgSpec( - args=(), - varargs="f", - varkw=None, - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ), + Signature(parameters=[Parameter("f", Parameter.VAR_POSITIONAL)]), )(accepts_everything)(1) define_function_signature( "hello", "A docstring for hello", - FullArgSpec( - args=(), - varargs=None, - varkw="f", - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ), + Signature(parameters=[Parameter("f", Parameter.VAR_KEYWORD)]), )(accepts_everything)() define_function_signature( "hello", "A docstring for hello", - FullArgSpec( - args=("f", "f_3"), - varargs="f_1", - varkw="f_2", - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, + Signature( + parameters=[ + Parameter("f", Parameter.POSITIONAL_OR_KEYWORD), + Parameter("f_3", Parameter.POSITIONAL_OR_KEYWORD), + Parameter("f_1", Parameter.VAR_POSITIONAL), + Parameter("f_2", Parameter.VAR_KEYWORD), + ] ), )(accepts_everything)(1, 2) -def test_define_function_signature_validates_arguments(): - with raises(ValueError): - define_function_signature( - "hello_world", - None, - FullArgSpec( - args=["a b"], - varargs=None, - varkw=None, - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ), - ) - - def test_define_function_signature_validates_function_name(): + define_function_signature("hello_world", None, Signature()) with raises(ValueError): - define_function_signature( - "hello world", - None, - FullArgSpec( - args=["a", "b"], - varargs=None, - varkw=None, - defaults=None, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ), - ) + define_function_signature("hello world", None, Signature()) class Container: