Skip to content

Commit edd1866

Browse files
authored
Merge pull request #177 from pytest-dev/176-add-mypy-to-ci
Add mypy to CI
2 parents f39adda + 86f496f commit edd1866

File tree

10 files changed

+121
-32
lines changed

10 files changed

+121
-32
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ src/pytest_flask/_version.py
2929
# Editors
3030
.vscode
3131
.code-workspace
32+
.python-version

docs/changelog.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ Changelog
66
UNRELEASED
77
----------
88

9-
* Added support for Python 3.10, 3.11, and 3.12.
10-
* Dropped support for EOL Python 3.7.
9+
* Add support for Python 3.10, 3.11, and 3.12.
10+
* Drop support for EOL Python 3.7.
11+
* Add type hints.
1112

1213
1.3.0 (2023-10-23)
1314
------------------

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@ requires = [
44
"setuptools-scm[toml]",
55
]
66
build-backend = "setuptools.build_meta"
7+
8+
[tool.mypy]
9+
warn_unreachable = true
10+
warn_unused_ignores = true
11+
warn_redundant_casts = true
12+
enable_error_code = [
13+
"ignore-without-code",
14+
"truthy-bool",
15+
]

requirements/test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ mock
22
pylint
33
coverage
44
pytest-pep8
5+
mypy

src/pytest_flask/_internal.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import functools
22
import warnings
3+
from typing import Callable
4+
from typing import Literal
35

6+
from pytest import Config as _PytestConfig
47

5-
def deprecated(reason):
8+
9+
_PytestScopeName = Literal["session", "package", "module", "class", "function"]
10+
11+
12+
def deprecated(reason: str) -> Callable:
613
"""Decorator which can be used to mark function or method as deprecated.
714
It will result a warning being emitted when the function is called."""
815

@@ -19,15 +26,15 @@ def deprecated_call(*args, **kwargs):
1926
return decorator
2027

2128

22-
def _rewrite_server_name(server_name, new_port):
29+
def _rewrite_server_name(server_name: str, new_port: str) -> str:
2330
"""Rewrite server port in ``server_name`` with ``new_port`` value."""
2431
sep = ":"
2532
if sep in server_name:
2633
server_name, _ = server_name.split(sep, 1)
2734
return sep.join((server_name, new_port))
2835

2936

30-
def _determine_scope(*, fixture_name, config):
37+
def _determine_scope(*, fixture_name: str, config: _PytestConfig) -> _PytestScopeName:
3138
return config.getini("live_server_scope")
3239

3340

src/pytest_flask/fixtures.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
#!/usr/bin/env python
22
import socket
3+
from typing import Any
4+
from typing import cast
5+
from typing import Generator
36

47
import pytest
8+
from flask import Flask as _FlaskApp
9+
from flask.config import Config as _FlaskAppConfig
10+
from flask.testing import FlaskClient as _FlaskTestClient
11+
from pytest import Config as _PytestConfig
12+
from pytest import FixtureRequest as _PytestFixtureRequest
513

614
from ._internal import _determine_scope
715
from ._internal import _make_accept_header
@@ -10,7 +18,7 @@
1018

1119

1220
@pytest.fixture
13-
def client(app):
21+
def client(app: _FlaskApp) -> Generator[_FlaskTestClient, Any, Any]:
1422
"""A Flask test client. An instance of :class:`flask.testing.TestClient`
1523
by default.
1624
"""
@@ -19,7 +27,7 @@ def client(app):
1927

2028

2129
@pytest.fixture
22-
def client_class(request, client):
30+
def client_class(request: _PytestFixtureRequest, client: _FlaskTestClient) -> None:
2331
"""Uses to set a ``client`` class attribute to current Flask test client::
2432
2533
@pytest.mark.usefixtures('client_class')
@@ -37,8 +45,10 @@ def test_login(self):
3745
request.cls.client = client
3846

3947

40-
@pytest.fixture(scope=_determine_scope)
41-
def live_server(request, app, pytestconfig): # pragma: no cover
48+
@pytest.fixture(scope=_determine_scope) # type: ignore[arg-type]
49+
def live_server(
50+
request: _PytestFixtureRequest, app: _FlaskApp, pytestconfig: _PytestConfig
51+
) -> Generator[LiveServer, Any, Any]: # pragma: no cover
4252
"""Run application in a separate process.
4353
4454
When the ``live_server`` fixture is applied, the ``url_for`` function
@@ -64,34 +74,36 @@ def test_server_is_up_and_running(live_server):
6474
port = s.getsockname()[1]
6575
s.close()
6676

67-
host = pytestconfig.getvalue("live_server_host")
77+
host = cast(str, pytestconfig.getvalue("live_server_host"))
6878

6979
# Explicitly set application ``SERVER_NAME`` for test suite
7080
original_server_name = app.config["SERVER_NAME"] or "localhost.localdomain"
7181
final_server_name = _rewrite_server_name(original_server_name, str(port))
7282
app.config["SERVER_NAME"] = final_server_name
7383

74-
wait = request.config.getvalue("live_server_wait")
75-
clean_stop = request.config.getvalue("live_server_clean_stop")
84+
wait = cast(int, request.config.getvalue("live_server_wait"))
85+
clean_stop = cast(bool, request.config.getvalue("live_server_clean_stop"))
86+
7687
server = LiveServer(app, host, port, wait, clean_stop)
7788
if request.config.getvalue("start_live_server"):
7889
server.start()
7990

8091
request.addfinalizer(server.stop)
92+
8193
yield server
8294

8395
if original_server_name is not None:
8496
app.config["SERVER_NAME"] = original_server_name
8597

8698

8799
@pytest.fixture
88-
def config(app):
100+
def config(app: _FlaskApp) -> _FlaskAppConfig:
89101
"""An application config."""
90102
return app.config
91103

92104

93105
@pytest.fixture(params=["application/json", "text/html"])
94-
def mimetype(request):
106+
def mimetype(request) -> str:
95107
return request.param
96108

97109

src/pytest_flask/live_server.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,30 @@
55
import signal
66
import socket
77
import time
8+
from multiprocessing import Process
9+
from typing import Any
10+
from typing import cast
11+
from typing import Protocol
12+
from typing import Union
813

914
import pytest
1015

1116

17+
class _SupportsFlaskAppRun(Protocol):
18+
def run(
19+
self,
20+
host: Union[str, None] = None,
21+
port: Union[int, None] = None,
22+
debug: Union[bool, None] = None,
23+
load_dotenv: bool = True,
24+
**options: Any,
25+
) -> None:
26+
...
27+
28+
1229
# force 'fork' on macOS
1330
if platform.system() == "Darwin":
14-
multiprocessing = multiprocessing.get_context("fork")
31+
multiprocessing = multiprocessing.get_context("fork") # type: ignore[assignment]
1532

1633

1734
class LiveServer: # pragma: no cover
@@ -25,18 +42,25 @@ class LiveServer: # pragma: no cover
2542
application is not started.
2643
"""
2744

28-
def __init__(self, app, host, port, wait, clean_stop=False):
45+
def __init__(
46+
self,
47+
app: _SupportsFlaskAppRun,
48+
host: str,
49+
port: int,
50+
wait: int,
51+
clean_stop: bool = False,
52+
):
2953
self.app = app
3054
self.port = port
3155
self.host = host
3256
self.wait = wait
3357
self.clean_stop = clean_stop
34-
self._process = None
58+
self._process: Union[Process, None] = None
3559

36-
def start(self):
60+
def start(self) -> None:
3761
"""Start application in a separate process."""
3862

39-
def worker(app, host, port):
63+
def worker(app: _SupportsFlaskAppRun, host: str, port: int) -> None:
4064
app.run(host=host, port=port, use_reloader=False, threaded=True)
4165

4266
self._process = multiprocessing.Process(
@@ -45,7 +69,7 @@ def worker(app, host, port):
4569
self._process.daemon = True
4670
self._process.start()
4771

48-
keep_trying = True
72+
keep_trying: bool = True
4973
start_time = time.time()
5074
while keep_trying:
5175
elapsed_time = time.time() - start_time
@@ -57,7 +81,7 @@ def worker(app, host, port):
5781
if self._is_ready():
5882
keep_trying = False
5983

60-
def _is_ready(self):
84+
def _is_ready(self) -> bool:
6185
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
6286
try:
6387
sock.connect((self.host, self.port))
@@ -69,13 +93,13 @@ def _is_ready(self):
6993
sock.close()
7094
return ret
7195

72-
def url(self, url=""):
96+
def url(self, url: str = "") -> str:
7397
"""Returns the complete url based on server options."""
7498
return "http://{host!s}:{port!s}{url!s}".format(
7599
host=self.host, port=self.port, url=url
76100
)
77101

78-
def stop(self):
102+
def stop(self) -> None:
79103
"""Stop application process."""
80104
if self._process:
81105
if self.clean_stop and self._stop_cleanly():
@@ -84,14 +108,17 @@ def stop(self):
84108
# If it's still alive, kill it
85109
self._process.terminate()
86110

87-
def _stop_cleanly(self, timeout=5):
111+
def _stop_cleanly(self, timeout: int = 5) -> bool:
88112
"""Attempts to stop the server cleanly by sending a SIGINT
89113
signal and waiting for ``timeout`` seconds.
90114
91115
:return: True if the server was cleanly stopped, False otherwise.
92116
"""
117+
if not self._process:
118+
return True
119+
93120
try:
94-
os.kill(self._process.pid, signal.SIGINT)
121+
os.kill(cast(int, self._process.pid), signal.SIGINT)
95122
self._process.join(timeout)
96123
return True
97124
except Exception as ex:

src/pytest_flask/plugin.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
:copyright: (c) by Vital Kudzelka
66
:license: MIT
77
"""
8+
from typing import Any
9+
from typing import List
10+
from typing import Protocol
11+
from typing import Type
12+
from typing import TypeVar
13+
from typing import Union
14+
815
import pytest
16+
from _pytest.config import Config as _PytestConfig
917

1018
from .fixtures import accept_any
1119
from .fixtures import accept_json
@@ -18,31 +26,48 @@
1826
from .pytest_compat import getfixturevalue
1927

2028

29+
_Response = TypeVar("_Response")
30+
31+
32+
class _SupportsPytestFlaskEqual(Protocol):
33+
status_code: int
34+
35+
def __eq__(self, other: Any) -> bool:
36+
...
37+
38+
def __ne__(self, other: Any) -> bool:
39+
...
40+
41+
2142
class JSONResponse:
2243
"""Mixin with testing helper methods for JSON responses."""
2344

24-
def __eq__(self, other):
45+
status_code: int
46+
47+
def __eq__(self, other) -> bool:
2548
if isinstance(other, int):
2649
return self.status_code == other
2750
return super().__eq__(other)
2851

29-
def __ne__(self, other):
52+
def __ne__(self, other) -> bool:
3053
return not self == other
3154

3255

33-
def pytest_assertrepr_compare(op, left, right):
56+
def pytest_assertrepr_compare(
57+
op: str, left: _SupportsPytestFlaskEqual, right: int
58+
) -> Union[List[str], None]:
3459
if isinstance(left, JSONResponse) and op == "==" and isinstance(right, int):
3560
return [
3661
"Mismatch in status code for response: {} != {}".format(
3762
left.status_code,
3863
right,
3964
),
40-
f"Response status: {left.status}",
65+
f"Response status: {left.status_code}",
4166
]
4267
return None
4368

4469

45-
def _make_test_response_class(response_class):
70+
def _make_test_response_class(response_class: Type[_Response]) -> Type[_Response]:
4671
"""Extends the response class with special attribute to test JSON
4772
responses. Don't override user-defined `json` attribute if any.
4873
@@ -186,7 +211,7 @@ def pytest_addoption(parser):
186211
)
187212

188213

189-
def pytest_configure(config):
214+
def pytest_configure(config: _PytestConfig) -> None:
190215
config.addinivalue_line(
191216
"markers", "app(options): pass options to your application factory"
192217
)

src/pytest_flask/py.typed

Whitespace-only changes.

tox.ini

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
envlist =
3-
py{37,38,39,linting}
3+
py{38,39,310,311,312,linting},mypy
44

55
[pytest]
66
norecursedirs = .git .tox env coverage docs
@@ -16,6 +16,7 @@ deps=
1616
-rrequirements/test.txt
1717
-rrequirements/docs.txt
1818
pre-commit>=1.11.0
19+
mypy>=1.6.1
1920
tox
2021
basepython = python3.8
2122
usedevelop = True
@@ -56,6 +57,11 @@ basepython = python3.8
5657
deps = pre-commit>=1.11.0
5758
commands = pre-commit run --all-files --show-diff-on-failure {posargs:}
5859

60+
[testenv:mypy]
61+
basepython = python3.8
62+
deps = mypy>=1.6.1
63+
commands = mypy src
64+
5965
[testenv:docs]
6066
changedir = docs
6167
skipsdist = True

0 commit comments

Comments
 (0)