Skip to content

Commit

Permalink
Merge pull request #3382 from HypothesisWorks/fix-InferType
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Jun 25, 2022
2 parents 047487f + d653df2 commit f00ef0d
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 39 deletions.
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

0 comments on commit f00ef0d

Please sign in to comment.