Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix InferType and improve type-checker tests #3382

Merged
merged 9 commits into from Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

This patch fixes type annotations that had caused the signature of
:func:`@given <hypothesis.given>` to be partially-unknown to type-checkers for Python
versions before 3.10.
4 changes: 3 additions & 1 deletion hypothesis-python/src/hypothesis/core.py
Expand Up @@ -25,6 +25,7 @@
from itertools import chain
from random import Random
from typing import (
TYPE_CHECKING,
Any,
BinaryIO,
Callable,
Expand Down Expand Up @@ -114,7 +115,8 @@

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType

elif TYPE_CHECKING:
from builtins import ellipsis as InferType
else:
InferType = type(Ellipsis)

Expand Down
5 changes: 3 additions & 2 deletions hypothesis-python/src/hypothesis/extra/django/_impl.py
Expand Up @@ -12,7 +12,7 @@
import unittest
from functools import partial
from inspect import Parameter, signature
from typing import Optional, Type, Union
from typing import TYPE_CHECKING, Optional, Type, Union

from django import forms as df, test as dt
from django.contrib.staticfiles import testing as dst
Expand All @@ -28,7 +28,8 @@

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType

elif TYPE_CHECKING:
from builtins import ellipsis as InferType
else:
InferType = type(Ellipsis)

Expand Down
4 changes: 3 additions & 1 deletion hypothesis-python/src/hypothesis/extra/ghostwriter.py
Expand Up @@ -83,6 +83,7 @@
from string import ascii_lowercase
from textwrap import dedent, indent
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Expand Down Expand Up @@ -119,7 +120,8 @@

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType

elif TYPE_CHECKING:
from builtins import ellipsis as InferType
else:
InferType = type(Ellipsis)

Expand Down
Expand Up @@ -109,7 +109,8 @@

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType

elif typing.TYPE_CHECKING: # pragma: no cover
from builtins import ellipsis as InferType
else:
InferType = type(Ellipsis)

Expand Down
2 changes: 1 addition & 1 deletion requirements/tools.txt
Expand Up @@ -236,7 +236,7 @@ pygments==2.12.0
# sphinx
pyparsing==3.0.9
# via packaging
pyright==1.1.249
pyright==1.1.255
# via -r requirements/tools.in
pytest==7.1.2
# via -r requirements/tools.in
Expand Down
95 changes: 75 additions & 20 deletions whole-repo-tests/test_mypy.py
Expand Up @@ -16,6 +16,8 @@
from hypothesistooling.projects.hypothesispython import PYTHON_SRC
from hypothesistooling.scripts import pip_tool, tool_path

PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"]


def test_mypy_passes_on_hypothesis():
pip_tool("mypy", PYTHON_SRC)
Expand Down Expand Up @@ -56,14 +58,20 @@ def get_mypy_analysed_type(fname, val):
)


def assert_mypy_errors(fname, expected):
out = get_mypy_output(fname, "--no-error-summary", "--show-error-codes")
def assert_mypy_errors(fname, expected, python_version=None):
_args = ["--no-error-summary", "--show-error-codes"]

if python_version:
_args.append(f"--python-version={python_version}")

out = get_mypy_output(fname, *_args)
del _args
# Shell output looks like:
# file.py:2: error: Incompatible types in assignment ... [assignment]

def convert_lines():
for error_line in out.splitlines():
col, category = error_line.split(":")[1:3]
col, category = error_line.split(":")[-3:-1]
if category.strip() != "error":
# mypy outputs "note" messages for overload problems, even with
# --hide-error-context. Don't include these
Expand Down Expand Up @@ -343,23 +351,6 @@ def test_stateful_consumed_bundle_cannot_be_target(tmpdir):
assert_mypy_errors(str(f.realpath()), [(3, "call-overload")])


def test_raises_for_mixed_pos_kwargs_in_given(tmpdir):
f = tmpdir.join("raises_for_mixed_pos_kwargs_in_given.py")
f.write(
textwrap.dedent(
"""
from hypothesis import given
from hypothesis.strategies import text

@given(text(), x=text())
def test_bar(x):
...
"""
)
)
assert_mypy_errors(str(f.realpath()), [(5, "call-overload")])


@pytest.mark.parametrize(
"return_val,errors",
[
Expand Down Expand Up @@ -445,3 +436,67 @@ def test_pos_only_args(tmpdir):
(8, "call-overload"),
],
)


@pytest.mark.parametrize("python_version", PYTHON_VERSIONS)
def test_mypy_passes_on_basic_test(tmpdir, python_version):
f = tmpdir.join("check_mypy_on_basic_tests.py")
f.write(
textwrap.dedent(
"""
import hypothesis
import hypothesis.strategies as st

@hypothesis.given(x=st.text())
def test_foo(x: str) -> None:
assert x == x

from hypothesis import given
from hypothesis.strategies import text

@given(x=text())
def test_bar(x: str) -> None:
assert x == x
"""
)
)
assert_mypy_errors(str(f.realpath()), [], python_version=python_version)


@pytest.mark.parametrize("python_version", PYTHON_VERSIONS)
def test_given_only_allows_strategies(tmpdir, python_version):
f = tmpdir.join("check_mypy_given_expects_strategies.py")
f.write(
textwrap.dedent(
"""
from hypothesis import given

@given(1)
def f():
pass
"""
)
)
assert_mypy_errors(
str(f.realpath()), [(4, "call-overload")], python_version=python_version
)


@pytest.mark.parametrize("python_version", PYTHON_VERSIONS)
def test_raises_for_mixed_pos_kwargs_in_given(tmpdir, python_version):
f = tmpdir.join("raises_for_mixed_pos_kwargs_in_given.py")
f.write(
textwrap.dedent(
"""
from hypothesis import given
from hypothesis.strategies import text

@given(text(), x=text())
def test_bar(x):
...
"""
)
)
assert_mypy_errors(
str(f.realpath()), [(5, "call-overload")], python_version=python_version
)
70 changes: 57 additions & 13 deletions whole-repo-tests/test_pyright.py
Expand Up @@ -21,6 +21,8 @@
from hypothesistooling.projects.hypothesispython import HYPOTHESIS_PYTHON, PYTHON_SRC
from hypothesistooling.scripts import pip_tool, tool_path

PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"]


@pytest.mark.skip(
reason="Hypothesis type-annotates the public API as a convenience for users, "
Expand All @@ -30,7 +32,8 @@ def test_pyright_passes_on_hypothesis():
pip_tool("pyright", "--project", HYPOTHESIS_PYTHON)


def test_pyright_passes_on_basic_test(tmp_path: Path):
@pytest.mark.parametrize("python_version", PYTHON_VERSIONS)
def test_pyright_passes_on_basic_test(tmp_path: Path, python_version: str):
file = tmp_path / "test.py"
file.write_text(
textwrap.dedent(
Expand All @@ -51,10 +54,40 @@ def test_bar(x: str):
"""
)
)
_write_config(tmp_path, {"typeCheckingMode": "strict"})
_write_config(
tmp_path, {"typeCheckingMode": "strict", "pythonVersion": python_version}
)
assert _get_pyright_errors(file) == []


@pytest.mark.parametrize("python_version", PYTHON_VERSIONS)
def test_given_only_allows_strategies(tmp_path: Path, python_version: str):
file = tmp_path / "test.py"
file.write_text(
textwrap.dedent(
"""
from hypothesis import given

@given(1)
def f():
pass
"""
)
)
_write_config(
tmp_path, {"typeCheckingMode": "strict", "pythonVersion": python_version}
)
assert (
sum(
e["message"].startswith(
'Argument of type "Literal[1]" cannot be assigned to parameter "_given_arguments"'
)
for e in _get_pyright_errors(file)
)
== 1
)


def test_pyright_issue_3296(tmp_path: Path):
file = tmp_path / "test.py"
file.write_text(
Expand Down Expand Up @@ -85,9 +118,14 @@ def test_bar(x: str):
)
)
_write_config(tmp_path, {"typeCheckingMode": "strict"})
assert any(
e["message"].startswith('No overloads for "given" match the provided arguments')
for e in _get_pyright_errors(file)
assert (
sum(
e["message"].startswith(
'No overloads for "given" match the provided arguments'
)
for e in _get_pyright_errors(file)
)
== 1
)


Expand Down Expand Up @@ -122,11 +160,14 @@ def test_pyright_tuples_pos_args_only(tmp_path: Path):
)
)
_write_config(tmp_path, {"typeCheckingMode": "strict"})
assert any(
e["message"].startswith(
'No overloads for "tuples" match the provided arguments'
assert (
sum(
e["message"].startswith(
'No overloads for "tuples" match the provided arguments'
)
for e in _get_pyright_errors(file)
)
for e in _get_pyright_errors(file)
== 2
)


Expand All @@ -143,11 +184,14 @@ def test_pyright_one_of_pos_args_only(tmp_path: Path):
)
)
_write_config(tmp_path, {"typeCheckingMode": "strict"})
assert any(
e["message"].startswith(
'No overloads for "one_of" match the provided arguments'
assert (
sum(
e["message"].startswith(
'No overloads for "one_of" match the provided arguments'
)
for e in _get_pyright_errors(file)
)
for e in _get_pyright_errors(file)
== 2
)


Expand Down