diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..5514c08987 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This release improves Ghostwritten tests for builtins (:issue:`2977`). diff --git a/hypothesis-python/src/hypothesis/extra/ghostwriter.py b/hypothesis-python/src/hypothesis/extra/ghostwriter.py index 3c360774ce..1be1290803 100644 --- a/hypothesis-python/src/hypothesis/extra/ghostwriter.py +++ b/hypothesis-python/src/hypothesis/extra/ghostwriter.py @@ -203,13 +203,15 @@ def _exceptions_from_docstring(doc: str) -> Tuple[Type[Exception], ...]: # on analysis of type-annotated code to detect arguments which almost always # take values of a particular type. _GUESS_STRATEGIES_BY_NAME = ( - (st.integers(0, 32), ["ndims"]), + (st.integers(0, 32), ["ndims", "ndigits"]), (st.booleans(), ["keepdims"]), (st.text(), ["name", "filename", "fname"]), (st.floats(), ["real", "imag"]), (st.functions(), ["function", "func", "f"]), (st.functions(returns=st.booleans(), pure=True), ["pred", "predicate"]), (st.iterables(st.integers()) | st.iterables(st.text()), ["iterable"]), + (st.builds(object), ["object"]), + (st.integers() | st.floats() | st.fractions(), ["number"]), ) @@ -333,16 +335,26 @@ def _get_params(func: Callable) -> Dict[str, inspect.Parameter]: # inspect.signature doesn't work on all builtin functions or methods. # In such cases, including the operator module on Python 3.6, we can try # to reconstruct simple signatures from the docstring. - pattern = rf"^{func.__name__}\(([a-z]+(, [a-z]+)*)(, \\)?\)" - args = re.match(pattern, func.__doc__) - if args is None: + match = re.match(rf"^{func.__name__}\((.+?)\)", func.__doc__) + if match is None: raise - params = [ - # Note that we assume that the args are positional-only regardless of - # whether the signature shows a `/`, because this is often the case. - inspect.Parameter(name=name, kind=inspect.Parameter.POSITIONAL_ONLY) - for name in args.group(1).split(", ") - ] + args = match.group(1).replace("[", "").replace("]", "") + params = [] + # Even if the signature doesn't contain a /, we assume that arguments + # are positional-only until shown otherwise - the / is often omitted. + kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_ONLY + for arg in args.split(", "): + arg, *_ = arg.partition("=") + if arg == "/": + kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + continue + if arg.startswith("*"): + kind = inspect.Parameter.KEYWORD_ONLY + continue # we omit *varargs, if there are any + if arg.startswith("**"): + break # and likewise omit **varkw + params.append(inspect.Parameter(name=arg, kind=kind)) + elif _is_probably_ufunc(func): # `inspect.signature` doesn't work on ufunc objects, but we can work out # what the required parameters would look like if it did. @@ -688,7 +700,8 @@ def _make_test(imports: Set[Union[str, Tuple[str, str]]], body: str) -> str: direct = {f"import {i}" for i in imports - do_not_import if isinstance(i, str)} from_imports = defaultdict(set) for module, name in {i for i in imports if isinstance(i, tuple)}: - from_imports[module].add(name) + if not (module.startswith("hypothesis.strategies") and name in st.__all__): + from_imports[module].add(name) from_ = { "from {} import {}".format(module, ", ".join(sorted(names))) for module, names in from_imports.items() diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index 56fa68471a..3eafa89f7d 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -814,6 +814,12 @@ def validate(self): tuples(*self.args).validate() fixed_dictionaries(self.kwargs).validate() + def __repr__(self): + bits = [get_pretty_function_description(self.target)] + bits.extend(map(repr, self.args)) + bits.extend(f"{k}={v!r}" for k, v in self.kwargs.items()) + return f"builds({', '.join(bits)})" + # The ideal signature builds(target, /, *args, **kwargs) is unfortunately a # SyntaxError before Python 3.8 so we emulate it with manual argument unpacking. diff --git a/hypothesis-python/tests/ghostwriter/recorded/magic_builtins.txt b/hypothesis-python/tests/ghostwriter/recorded/magic_builtins.txt new file mode 100644 index 0000000000..99e18aa18f --- /dev/null +++ b/hypothesis-python/tests/ghostwriter/recorded/magic_builtins.txt @@ -0,0 +1,285 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import given, strategies as st + +# TODO: replace st.nothing() with appropriate strategies + + +@given(x=st.nothing()) +def test_fuzz_abs(x): + abs(x) + + +@given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) +def test_fuzz_all(iterable): + all(iterable) + + +@given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) +def test_fuzz_any(iterable): + any(iterable) + + +@given(obj=st.nothing()) +def test_fuzz_ascii(obj): + ascii(obj) + + +@given(number=st.one_of(st.integers(), st.floats(), st.fractions())) +def test_fuzz_bin(number): + bin(number) + + +@given(obj=st.nothing()) +def test_fuzz_callable(obj): + callable(obj) + + +@given(i=st.nothing()) +def test_fuzz_chr(i): + chr(i) + + +@given( + source=st.nothing(), + filename=st.nothing(), + mode=st.nothing(), + flags=st.just(0), + dont_inherit=st.booleans(), + optimize=st.just(-1), + _feature_version=st.just(-1), +) +def test_fuzz_compile( + source, filename, mode, flags, dont_inherit, optimize, _feature_version +): + compile( + source=source, + filename=filename, + mode=mode, + flags=flags, + dont_inherit=dont_inherit, + optimize=optimize, + _feature_version=_feature_version, + ) + + +@given(real=st.just(0), imag=st.just(0)) +def test_fuzz_complex(real, imag): + complex(real=real, imag=imag) + + +@given(obj=st.nothing(), name=st.nothing()) +def test_fuzz_delattr(obj, name): + delattr(obj, name) + + +@given(object=st.builds(object)) +def test_fuzz_dir(object): + dir(object) + + +@given(x=st.nothing(), y=st.nothing()) +def test_fuzz_divmod(x, y): + divmod(x, y) + + +@given( + iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), + start=st.just(0), +) +def test_fuzz_enumerate(iterable, start): + enumerate(iterable=iterable, start=start) + + +@given(source=st.nothing(), globals=st.none(), locals=st.none()) +def test_fuzz_eval(source, globals, locals): + eval(source, globals, locals) + + +@given(source=st.nothing(), globals=st.none(), locals=st.none()) +def test_fuzz_exec(source, globals, locals): + exec(source, globals, locals) + + +@given(x=st.just(0)) +def test_fuzz_float(x): + float(x) + + +@given(value=st.nothing(), format_spec=st.just("")) +def test_fuzz_format(value, format_spec): + format(value, format_spec) + + +@given(object=st.builds(object), name=st.nothing(), default=st.nothing()) +def test_fuzz_getattr(object, name, default): + getattr(object, name, default) + + +@given(obj=st.nothing(), name=st.nothing()) +def test_fuzz_hasattr(obj, name): + hasattr(obj, name) + + +@given(obj=st.nothing()) +def test_fuzz_hash(obj): + hash(obj) + + +@given(number=st.one_of(st.integers(), st.floats(), st.fractions())) +def test_fuzz_hex(number): + hex(number) + + +@given(obj=st.nothing()) +def test_fuzz_id(obj): + id(obj) + + +@given(prompt=st.none()) +def test_fuzz_input(prompt): + input(prompt) + + +@given(obj=st.nothing(), class_or_tuple=st.nothing()) +def test_fuzz_isinstance(obj, class_or_tuple): + isinstance(obj, class_or_tuple) + + +@given(cls=st.nothing(), class_or_tuple=st.nothing()) +def test_fuzz_issubclass(cls, class_or_tuple): + issubclass(cls, class_or_tuple) + + +@given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) +def test_fuzz_iter(iterable): + iter(iterable) + + +@given(obj=st.nothing()) +def test_fuzz_len(obj): + len(obj) + + +@given(iterable=st.just(())) +def test_fuzz_list(iterable): + list(iterable) + + +@given( + iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), + default=st.nothing(), + key=st.nothing(), +) +def test_fuzz_max(iterable, default, key): + max(iterable, default=default, key=key) + + +@given(object=st.builds(object)) +def test_fuzz_memoryview(object): + memoryview(object=object) + + +@given( + iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), + default=st.nothing(), + key=st.nothing(), +) +def test_fuzz_min(iterable, default, key): + min(iterable, default=default, key=key) + + +@given(iterator=st.nothing(), default=st.nothing()) +def test_fuzz_next(iterator, default): + next(iterator, default) + + +@given(number=st.one_of(st.integers(), st.floats(), st.fractions())) +def test_fuzz_oct(number): + oct(number) + + +@given( + file=st.nothing(), + mode=st.just("r"), + buffering=st.just(-1), + encoding=st.none(), + errors=st.none(), + newline=st.none(), + closefd=st.booleans(), + opener=st.none(), +) +def test_fuzz_open(file, mode, buffering, encoding, errors, newline, closefd, opener): + open( + file=file, + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + closefd=closefd, + opener=opener, + ) + + +@given(c=st.nothing()) +def test_fuzz_ord(c): + ord(c) + + +@given(base=st.nothing(), exp=st.nothing(), mod=st.none()) +def test_fuzz_pow(base, exp, mod): + pow(base=base, exp=exp, mod=mod) + + +@given(fget=st.none(), fset=st.none(), fdel=st.none(), doc=st.none()) +def test_fuzz_property(fget, fset, fdel, doc): + property(fget=fget, fset=fset, fdel=fdel, doc=doc) + + +@given(obj=st.nothing()) +def test_fuzz_repr(obj): + repr(obj) + + +@given(sequence=st.nothing()) +def test_fuzz_reversed(sequence): + reversed(sequence) + + +@given(number=st.one_of(st.integers(), st.floats(), st.fractions()), ndigits=st.none()) +def test_fuzz_round(number, ndigits): + round(number=number, ndigits=ndigits) + + +@given(obj=st.nothing(), name=st.nothing(), value=st.nothing()) +def test_fuzz_setattr(obj, name, value): + setattr(obj, name, value) + + +@given( + iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), + key=st.none(), + reverse=st.booleans(), +) +def test_fuzz_sorted(iterable, key, reverse): + sorted(iterable, key=key, reverse=reverse) + + +@given( + iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), + start=st.just(0), +) +def test_fuzz_sum(iterable, start): + sum(iterable, start=start) + + +@given(iterable=st.just(())) +def test_fuzz_tuple(iterable): + tuple(iterable) + + +@given(object=st.builds(object)) +def test_fuzz_vars(object): + vars(object) diff --git a/hypothesis-python/tests/ghostwriter/test_expected_output.py b/hypothesis-python/tests/ghostwriter/test_expected_output.py index 2762c1c727..eed5204f9e 100644 --- a/hypothesis-python/tests/ghostwriter/test_expected_output.py +++ b/hypothesis-python/tests/ghostwriter/test_expected_output.py @@ -21,6 +21,7 @@ import ast import base64 +import builtins import operator import pathlib import re @@ -153,6 +154,11 @@ def divide(a: int, b: int) -> float: style="unittest", ), ), + pytest.param( + ("magic_builtins", ghostwriter.magic(builtins)), + # Signature of builtins.compile() changed in 3.8 and we use that version. + marks=[pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="")], + ), ], ids=lambda x: x[0], )