Skip to content

Commit

Permalink
Upgrade define_function_signature
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Jun 28, 2022
1 parent d479df5 commit 08181fb
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 208 deletions.
33 changes: 31 additions & 2 deletions hypothesis-python/src/hypothesis/core.py
Expand Up @@ -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],
Expand Down Expand Up @@ -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():
Expand All @@ -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(
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions hypothesis-python/src/hypothesis/extra/django/_impl.py
Expand Up @@ -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

Expand Down Expand Up @@ -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),
Expand Down
98 changes: 2 additions & 96 deletions hypothesis-python/src/hypothesis/internal/reflection.py
Expand Up @@ -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 == "<lambda>":
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
Expand All @@ -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)
Expand Down Expand Up @@ -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>", "_lambda_"), # type: ignore
target.__doc__,
get_signature(target, follow_wrapped=False),
Expand Down
6 changes: 3 additions & 3 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions hypothesis-python/src/hypothesis/strategies/_internal/random.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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__
Expand Down
29 changes: 12 additions & 17 deletions hypothesis-python/tests/cover/test_annotations.py
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 08181fb

Please sign in to comment.