Skip to content

Commit

Permalink
Merge pull request #3097 from Zac-HD/ghostwriter-pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD authored Sep 15, 2021
2 parents ac1d406 + f95a5fb commit 32e944b
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 11 deletions.
3 changes: 3 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: patch

This release improves Ghostwritten tests for builtins (:issue:`2977`).
35 changes: 24 additions & 11 deletions hypothesis-python/src/hypothesis/extra/ghostwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
)


Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
285 changes: 285 additions & 0 deletions hypothesis-python/tests/ghostwriter/recorded/magic_builtins.txt
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions hypothesis-python/tests/ghostwriter/test_expected_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import ast
import base64
import builtins
import operator
import pathlib
import re
Expand Down Expand Up @@ -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],
)
Expand Down

0 comments on commit 32e944b

Please sign in to comment.