From 02ea5c0b62df6088cdcf7720e87ffb7b219a3cd6 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Wed, 29 Jun 2022 00:24:17 -0700 Subject: [PATCH 1/3] Use signature() in test --- hypothesis-python/tests/cover/test_functions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/tests/cover/test_functions.py b/hypothesis-python/tests/cover/test_functions.py index ffdd42aeef..e0e3868c3d 100644 --- a/hypothesis-python/tests/cover/test_functions.py +++ b/hypothesis-python/tests/cover/test_functions.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 signature import pytest @@ -111,9 +111,10 @@ def t(f): def test_can_call_default_like_arg(): # This test is somewhat silly, but coverage complains about the uncovered # branch for calling it otherwise and alternative workarounds are worse. - defaults = getfullargspec(functions).kwonlydefaults - assert defaults["like"]() is None - assert defaults["returns"] is ... + like, returns, pure = signature(functions).parameters.values() + assert like.default() is None + assert returns.default is ... + assert pure.default is False def func(arg, *, kwonly_arg): From a01b66c3ab348add2539aa55d661b1e04c44c3be Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Wed, 29 Jun 2022 00:24:17 -0700 Subject: [PATCH 2/3] Use signature for argument conversion --- hypothesis-python/RELEASE.rst | 4 + .../src/hypothesis/internal/reflection.py | 87 ++++--------------- .../tests/cover/test_annotations.py | 2 +- .../tests/cover/test_reflection.py | 14 +-- 4 files changed, 24 insertions(+), 83 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..1640b744e1 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,4 @@ +RELEASE_TYPE: patch + +This patch tidies up some internal introspection logic, which will improve +support for positional-only arguments in a future release (:issue:`2706`). diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py index 65329d549d..0484b76d3f 100644 --- a/hypothesis-python/src/hypothesis/internal/reflection.py +++ b/hypothesis-python/src/hypothesis/internal/reflection.py @@ -176,44 +176,9 @@ def convert_keyword_arguments(function, args, kwargs): passed as positional and keyword args to the function. Unless function has kwonlyargs or **kwargs the dictionary will always be empty. """ - argspec = getfullargspec_except_self(function) - new_args = [] - kwargs = dict(kwargs) - - defaults = dict(argspec.kwonlydefaults or {}) - - if argspec.defaults: - for name, value in zip( - argspec.args[-len(argspec.defaults) :], argspec.defaults - ): - defaults[name] = value - - n = max(len(args), len(argspec.args)) - - for i in range(n): - if i < len(args): - new_args.append(args[i]) - else: - arg_name = argspec.args[i] - if arg_name in kwargs: - new_args.append(kwargs.pop(arg_name)) - elif arg_name in defaults: - new_args.append(defaults[arg_name]) - else: - raise TypeError(f"No value provided for argument {arg_name!r}") - - if kwargs and not (argspec.varkw or argspec.kwonlyargs): - if len(kwargs) > 1: - raise TypeError( - "%s() got unexpected keyword arguments %s" - % (function.__name__, ", ".join(map(repr, kwargs))) - ) - else: - bad_kwarg = next(iter(kwargs)) - raise TypeError( - f"{function.__name__}() got an unexpected keyword argument {bad_kwarg!r}" - ) - return tuple(new_args), kwargs + sig = inspect.signature(function, follow_wrapped=False) + bound = sig.bind(*args, **kwargs) + return bound.args, bound.kwargs def convert_positional_arguments(function, args, kwargs): @@ -222,38 +187,20 @@ def convert_positional_arguments(function, args, kwargs): new_args will only be non-empty if function has a variadic argument. """ - argspec = getfullargspec_except_self(function) - new_kwargs = dict(argspec.kwonlydefaults or {}) - new_kwargs.update(kwargs) - if not argspec.varkw: - for k in new_kwargs.keys(): - if k not in argspec.args and k not in argspec.kwonlyargs: - raise TypeError( - f"{function.__name__}() got an unexpected keyword argument {k!r}" - ) - if len(args) < len(argspec.args): - for i in range(len(args), len(argspec.args) - len(argspec.defaults or ())): - if argspec.args[i] not in kwargs: - raise TypeError(f"No value provided for argument {argspec.args[i]}") - for kw in argspec.kwonlyargs: - if kw not in new_kwargs: - raise TypeError(f"No value provided for argument {kw}") - - if len(args) > len(argspec.args) and not argspec.varargs: - raise TypeError( - f"{function.__name__}() takes at most {len(argspec.args)} " - f"positional arguments ({len(args)} given)" - ) - - for arg, name in zip(args, argspec.args): - if name in new_kwargs: - raise TypeError( - f"{function.__name__}() got multiple values for keyword argument {name!r}" - ) - else: - new_kwargs[name] = arg - - return (tuple(args[len(argspec.args) :]), new_kwargs) + sig = inspect.signature(function, follow_wrapped=False) + bound = sig.bind(*args, **kwargs) + new_args = [] + new_kwargs = dict(bound.arguments) + for p in sig.parameters.values(): + if p.name in new_kwargs: + if p.kind is p.POSITIONAL_ONLY: + new_args.append(new_kwargs.pop(p.name)) + elif p.kind is p.VAR_POSITIONAL: + new_args.extend(new_kwargs.pop(p.name)) + elif p.kind is p.VAR_KEYWORD: + assert set(new_kwargs[p.name]).isdisjoint(set(new_kwargs) - {p.name}) + new_kwargs.update(new_kwargs.pop(p.name)) + return tuple(new_args), new_kwargs def ast_arguments_matches_signature(args, sig): diff --git a/hypothesis-python/tests/cover/test_annotations.py b/hypothesis-python/tests/cover/test_annotations.py index 452c96ecef..70fde8bfc7 100644 --- a/hypothesis-python/tests/cover/test_annotations.py +++ b/hypothesis-python/tests/cover/test_annotations.py @@ -78,7 +78,7 @@ def f(*, a, b=2): pass out = convert_positional_arguments(f, (), {"a": 1}) - assert out == ((), {"a": 1, "b": 2}) + assert out == ((), {"a": 1}) def test_converter_notices_missing_kwonly_args(): diff --git a/hypothesis-python/tests/cover/test_reflection.py b/hypothesis-python/tests/cover/test_reflection.py index cd4d17123d..420c849cef 100644 --- a/hypothesis-python/tests/cover/test_reflection.py +++ b/hypothesis-python/tests/cover/test_reflection.py @@ -59,16 +59,6 @@ def foo(a, b, c): do_conversion_test(foo, (1,), {"c": 2, "b": "foo"}) -def test_populates_defaults(): - def bar(x=[], y=1): - pass - - assert convert_keyword_arguments(bar, (), {}) == (([], 1), {}) - assert convert_keyword_arguments(bar, (), {"y": 42}) == (([], 42), {}) - do_conversion_test(bar, (), {}) - do_conversion_test(bar, (1,), {}) - - def test_leaves_unknown_kwargs_in_dict(): def bar(x, **kwargs): pass @@ -130,7 +120,7 @@ def test_positional_errors_if_too_many_args(): def foo(a): pass - with raises(TypeError, match="2 given"): + with raises(TypeError, match="too many positional arguments"): convert_positional_arguments(foo, (1, 2), {}) @@ -153,7 +143,7 @@ def test_positional_errors_if_given_bad_kwargs(): def foo(a): pass - with raises(TypeError, match="unexpected keyword argument"): + with raises(TypeError, match="missing a required argument: 'a'"): convert_positional_arguments(foo, (), {"b": 1}) From 98734bed7798485ce0021e40b57e8fd79d479cf7 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Wed, 29 Jun 2022 00:24:18 -0700 Subject: [PATCH 3/3] Upgrade define_function_signature --- hypothesis-python/src/hypothesis/core.py | 44 +++++-- .../src/hypothesis/extra/django/_impl.py | 4 +- .../src/hypothesis/internal/reflection.py | 104 ++--------------- .../hypothesis/strategies/_internal/core.py | 6 +- .../hypothesis/strategies/_internal/random.py | 10 +- .../tests/cover/test_annotations.py | 33 +++--- .../tests/cover/test_lookup_py38.py | 43 ++++++- .../tests/cover/test_reflection.py | 107 ++++-------------- 8 files changed, 130 insertions(+), 221 deletions(-) diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 627511b2d9..0274d745dd 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -58,6 +58,7 @@ Flaky, Found, HypothesisDeprecationWarning, + HypothesisException, HypothesisWarning, InvalidArgument, MultipleFailures, @@ -272,6 +273,13 @@ def wrapped_test(*arguments, **kwargs): if not (given_arguments or given_kwargs): return invalid("given must be called with at least one argument") + p = inspect.signature(test).parameters + if p and list(p.values())[0].kind is inspect.Parameter.POSITIONAL_ONLY: + return invalid( + "given does not support tests with positional-only arguments", + exc=HypothesisException, + ) + if given_arguments and any( [original_argspec.varargs, original_argspec.varkw, original_argspec.kwonlyargs] ): @@ -310,13 +318,6 @@ def wrapped_test(*arguments, **kwargs): ) if original_argspec.defaults or original_argspec.kwonlydefaults: return invalid("Cannot apply @given to a function with defaults.") - missing = [repr(kw) for kw in original_argspec.kwonlyargs if kw not in given_kwargs] - if missing: - return invalid( - "Missing required kwarg{}: {}".format( - "s" if len(missing) > 1 else "", ", ".join(missing) - ) - ) # This case would raise Unsatisfiable *anyway*, but by detecting it here we can # provide a much more helpful error message for people e.g. using the Ghostwriter. @@ -972,6 +973,30 @@ 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): + annot = argspec.annotations.get(name, P.empty) + return P(name, kind, default=defaults.get(name, P.empty), annotation=annot) + + 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 or {})) + if argspec.varkw: + params.append(as_param(argspec.varkw, P.VAR_KEYWORD, {})) + return inspect.Signature(params, return_annotation=return_annotation) + + @overload def given( *_given_arguments: Union[SearchStrategy[Any], InferType], @@ -1039,6 +1064,7 @@ def run_test_as_given(test): del given_arguments argspec = new_given_argspec(original_argspec, given_kwargs) + new_signature = fullargspec_to_signature(argspec, return_annotation=None) # Use type information to convert "infer" arguments into appropriate strategies. if infer in given_kwargs.values(): @@ -1049,7 +1075,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 +1088,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..0be295250f 100644 --- a/hypothesis-python/src/hypothesis/internal/reflection.py +++ b/hypothesis-python/src/hypothesis/internal/reflection.py @@ -185,7 +185,7 @@ def convert_positional_arguments(function, args, kwargs): """Return a tuple (new_args, new_kwargs) where all possible arguments have been moved to kwargs. - new_args will only be non-empty if function has a variadic argument. + new_args will only be non-empty if function has pos-only args or *args. """ sig = inspect.signature(function, follow_wrapped=False) bound = sig.bind(*args, **kwargs) @@ -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,11 @@ 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. - + if name == "": + name = "_lambda_" check_valid_identifier(name) for a in signature.parameters: check_valid_identifier(a) @@ -624,6 +532,8 @@ def accept(f): for p in signature.parameters.values() if p.annotation is not signature.empty } + if signature.return_annotation is not signature.empty: + annotations["return"] = signature.return_annotation if annotations: result.__annotations__ = annotations return result @@ -653,7 +563,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..70eb9d3fc4 100644 --- a/hypothesis-python/tests/cover/test_annotations.py +++ b/hypothesis-python/tests/cover/test_annotations.py @@ -8,13 +8,12 @@ # 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 from hypothesis import given, strategies as st -from hypothesis.errors import InvalidArgument from hypothesis.internal.reflection import ( convert_positional_arguments, define_function_signature, @@ -37,16 +36,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( @@ -69,8 +62,9 @@ def test_given_notices_missing_kwonly_args(): def reqs_kwonly(*, a, b): pass - with pytest.raises(InvalidArgument): + with pytest.raises(TypeError): reqs_kwonly() + reqs_kwonly(b=None) def test_converter_handles_kwonly_args(): @@ -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_lookup_py38.py b/hypothesis-python/tests/cover/test_lookup_py38.py index 9302e78c9d..5d8b5c611c 100644 --- a/hypothesis-python/tests/cover/test_lookup_py38.py +++ b/hypothesis-python/tests/cover/test_lookup_py38.py @@ -15,12 +15,15 @@ import pytest from hypothesis import given, strategies as st -from hypothesis.errors import Unsatisfiable -from hypothesis.internal.reflection import get_pretty_function_description +from hypothesis.errors import HypothesisException, Unsatisfiable +from hypothesis.internal.reflection import ( + convert_positional_arguments, + get_pretty_function_description, +) from hypothesis.strategies import from_type from tests.common.debug import find_any -from tests.common.utils import temp_registered +from tests.common.utils import fails_with, temp_registered @given(st.data()) @@ -149,3 +152,37 @@ def test_can_register_new_type_for_typeddicts(): def test_posonly_lambda_formatting(lam, source): # Testing posonly lambdas, with and without default values assert get_pretty_function_description(lam) == source + + +def test_does_not_convert_posonly_to_keyword(): + args, kws = convert_positional_arguments(lambda x, /: None, (1,), {}) + assert args + assert not kws + + +@given(x=st.booleans()) +def test_given_works_with_keyword_only_params(*, x): + pass + + +def test_given_works_with_keyword_only_params_some_unbound(): + @given(x=st.booleans()) + def test(*, x, y): + assert y is None + + test(y=None) + + +@fails_with(HypothesisException) +@given(st.booleans()) +def test_given_works_with_positional_only_params(x, /): + pass + + +def test_given_works_with_positional_only_params_some_unbound(): + @fails_with(HypothesisException) + @given(st.booleans()) + def test(x, y, /): + assert y is None + + test(None) 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: