Skip to content

Commit 7d09fd9

Browse files
committed
Provide typing info (#260)
1 parent c150f80 commit 7d09fd9

File tree

6 files changed

+136
-42
lines changed

6 files changed

+136
-42
lines changed

Makefile

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ clean-test: ## remove test and coverage artifacts
2323
lint:
2424
# CI env-var is set by GitHub actions
2525
ifdef CI
26-
pre-commit run --all-files --show-diff-on-failure
26+
python -m pre_commit run --all-files --show-diff-on-failure
2727
else
28-
pre-commit run --all-files
28+
python -m pre_commit run --all-files
2929
endif
30+
python -m mypy pytest_asyncio --show-error-codes
3031

3132
test:
3233
coverage run -m pytest tests

README.rst

+1
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ Changelog
261261
- Fixes a bug that prevents async Hypothesis tests from working without explicit ``asyncio`` marker when ``--asyncio-mode=auto`` is set. `#258 <https://github.com/pytest-dev/pytest-asyncio/issues/258>`_
262262
- Fixed a bug that closes the default event loop if the loop doesn't exist `#257 <https://github.com/pytest-dev/pytest-asyncio/issues/257>`_
263263
- Relax ``asyncio_mode`` type definition; it allows to support pytest 6.1+. `#262 <https://github.com/pytest-dev/pytest-asyncio/issues/262>`_
264+
- Added type annotations. `#198 <https://github.com/pytest-dev/pytest-asyncio/issues/198>`_
264265

265266
0.17.0 (22-01-13)
266267
~~~~~~~~~~~~~~~~~~~

pytest_asyncio/plugin.py

+127-39
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,45 @@
66
import inspect
77
import socket
88
import warnings
9+
from typing import (
10+
Any,
11+
AsyncIterator,
12+
Awaitable,
13+
Callable,
14+
Dict,
15+
Iterable,
16+
Iterator,
17+
List,
18+
Optional,
19+
Set,
20+
TypeVar,
21+
Union,
22+
cast,
23+
overload,
24+
)
925

1026
import pytest
27+
from typing_extensions import Literal
28+
29+
_R = TypeVar("_R")
30+
31+
_ScopeName = Literal["session", "package", "module", "class", "function"]
32+
_T = TypeVar("_T")
33+
34+
SimpleFixtureFunction = TypeVar(
35+
"SimpleFixtureFunction", bound=Callable[..., Awaitable[_R]]
36+
)
37+
FactoryFixtureFunction = TypeVar(
38+
"FactoryFixtureFunction", bound=Callable[..., AsyncIterator[_R]]
39+
)
40+
FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction]
41+
FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction]
42+
43+
Config = Any # pytest < 7.0
44+
PytestPluginManager = Any # pytest < 7.0
45+
FixtureDef = Any # pytest < 7.0
46+
Parser = Any # pytest < 7.0
47+
SubRequest = Any # pytest < 7.0
1148

1249

1350
class Mode(str, enum.Enum):
@@ -41,7 +78,7 @@ class Mode(str, enum.Enum):
4178
"""
4279

4380

44-
def pytest_addoption(parser, pluginmanager):
81+
def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
4582
group = parser.getgroup("asyncio")
4683
group.addoption(
4784
"--asyncio-mode",
@@ -57,49 +94,87 @@ def pytest_addoption(parser, pluginmanager):
5794
)
5895

5996

60-
def fixture(fixture_function=None, **kwargs):
97+
@overload
98+
def fixture(
99+
fixture_function: FixtureFunction,
100+
*,
101+
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
102+
params: Optional[Iterable[object]] = ...,
103+
autouse: bool = ...,
104+
ids: Optional[
105+
Union[
106+
Iterable[Union[None, str, float, int, bool]],
107+
Callable[[Any], Optional[object]],
108+
]
109+
] = ...,
110+
name: Optional[str] = ...,
111+
) -> FixtureFunction:
112+
...
113+
114+
115+
@overload
116+
def fixture(
117+
fixture_function: None = ...,
118+
*,
119+
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
120+
params: Optional[Iterable[object]] = ...,
121+
autouse: bool = ...,
122+
ids: Optional[
123+
Union[
124+
Iterable[Union[None, str, float, int, bool]],
125+
Callable[[Any], Optional[object]],
126+
]
127+
] = ...,
128+
name: Optional[str] = None,
129+
) -> FixtureFunctionMarker:
130+
...
131+
132+
133+
def fixture(
134+
fixture_function: Optional[FixtureFunction] = None, **kwargs: Any
135+
) -> Union[FixtureFunction, FixtureFunctionMarker]:
61136
if fixture_function is not None:
62137
_set_explicit_asyncio_mark(fixture_function)
63138
return pytest.fixture(fixture_function, **kwargs)
64139

65140
else:
66141

67142
@functools.wraps(fixture)
68-
def inner(fixture_function):
143+
def inner(fixture_function: FixtureFunction) -> FixtureFunction:
69144
return fixture(fixture_function, **kwargs)
70145

71146
return inner
72147

73148

74-
def _has_explicit_asyncio_mark(obj):
149+
def _has_explicit_asyncio_mark(obj: Any) -> bool:
75150
obj = getattr(obj, "__func__", obj) # instance method maybe?
76151
return getattr(obj, "_force_asyncio_fixture", False)
77152

78153

79-
def _set_explicit_asyncio_mark(obj):
154+
def _set_explicit_asyncio_mark(obj: Any) -> None:
80155
if hasattr(obj, "__func__"):
81156
# instance method, check the function object
82157
obj = obj.__func__
83158
obj._force_asyncio_fixture = True
84159

85160

86-
def _is_coroutine(obj):
161+
def _is_coroutine(obj: Any) -> bool:
87162
"""Check to see if an object is really an asyncio coroutine."""
88163
return asyncio.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj)
89164

90165

91-
def _is_coroutine_or_asyncgen(obj):
166+
def _is_coroutine_or_asyncgen(obj: Any) -> bool:
92167
return _is_coroutine(obj) or inspect.isasyncgenfunction(obj)
93168

94169

95-
def _get_asyncio_mode(config):
170+
def _get_asyncio_mode(config: Config) -> Mode:
96171
val = config.getoption("asyncio_mode")
97172
if val is None:
98173
val = config.getini("asyncio_mode")
99174
return Mode(val)
100175

101176

102-
def pytest_configure(config):
177+
def pytest_configure(config: Config) -> None:
103178
"""Inject documentation."""
104179
config.addinivalue_line(
105180
"markers",
@@ -112,10 +187,14 @@ def pytest_configure(config):
112187

113188

114189
@pytest.mark.tryfirst
115-
def pytest_pycollect_makeitem(collector, name, obj):
190+
def pytest_pycollect_makeitem(
191+
collector: Union[pytest.Module, pytest.Class], name: str, obj: object
192+
) -> Union[
193+
None, pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]]
194+
]:
116195
"""A pytest hook to collect asyncio coroutines."""
117196
if not collector.funcnamefilter(name):
118-
return
197+
return None
119198
if (
120199
_is_coroutine(obj)
121200
or _is_hypothesis_test(obj)
@@ -130,10 +209,11 @@ def pytest_pycollect_makeitem(collector, name, obj):
130209
ret = list(collector._genfunctions(name, obj))
131210
for elem in ret:
132211
elem.add_marker("asyncio")
133-
return ret
212+
return ret # type: ignore[return-value]
213+
return None
134214

135215

136-
def _hypothesis_test_wraps_coroutine(function):
216+
def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
137217
return _is_coroutine(function.hypothesis.inner_test)
138218

139219

@@ -143,19 +223,19 @@ class FixtureStripper:
143223
REQUEST = "request"
144224
EVENT_LOOP = "event_loop"
145225

146-
def __init__(self, fixturedef):
226+
def __init__(self, fixturedef: FixtureDef) -> None:
147227
self.fixturedef = fixturedef
148-
self.to_strip = set()
228+
self.to_strip: Set[str] = set()
149229

150-
def add(self, name):
230+
def add(self, name: str) -> None:
151231
"""Add fixture name to fixturedef
152232
and record in to_strip list (If not previously included)"""
153233
if name in self.fixturedef.argnames:
154234
return
155235
self.fixturedef.argnames += (name,)
156236
self.to_strip.add(name)
157237

158-
def get_and_strip_from(self, name, data_dict):
238+
def get_and_strip_from(self, name: str, data_dict: Dict[str, _T]) -> _T:
159239
"""Strip name from data, and return value"""
160240
result = data_dict[name]
161241
if name in self.to_strip:
@@ -164,7 +244,7 @@ def get_and_strip_from(self, name, data_dict):
164244

165245

166246
@pytest.hookimpl(trylast=True)
167-
def pytest_fixture_post_finalizer(fixturedef, request):
247+
def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -> None:
168248
"""Called after fixture teardown"""
169249
if fixturedef.argname == "event_loop":
170250
policy = asyncio.get_event_loop_policy()
@@ -181,7 +261,9 @@ def pytest_fixture_post_finalizer(fixturedef, request):
181261

182262

183263
@pytest.hookimpl(hookwrapper=True)
184-
def pytest_fixture_setup(fixturedef, request):
264+
def pytest_fixture_setup(
265+
fixturedef: FixtureDef, request: SubRequest
266+
) -> Optional[object]:
185267
"""Adjust the event loop policy when an event loop is produced."""
186268
if fixturedef.argname == "event_loop":
187269
outcome = yield
@@ -294,39 +376,43 @@ async def setup():
294376

295377

296378
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
297-
def pytest_pyfunc_call(pyfuncitem):
379+
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]:
298380
"""
299381
Pytest hook called before a test case is run.
300382
301383
Wraps marked tests in a synchronous function
302384
where the wrapped test coroutine is executed in an event loop.
303385
"""
304386
if "asyncio" in pyfuncitem.keywords:
387+
funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined]
388+
loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"])
305389
if _is_hypothesis_test(pyfuncitem.obj):
306390
pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
307391
pyfuncitem.obj.hypothesis.inner_test,
308-
_loop=pyfuncitem.funcargs["event_loop"],
392+
_loop=loop,
309393
)
310394
else:
311395
pyfuncitem.obj = wrap_in_sync(
312-
pyfuncitem.obj, _loop=pyfuncitem.funcargs["event_loop"]
396+
pyfuncitem.obj,
397+
_loop=loop,
313398
)
314399
yield
315400

316401

317-
def _is_hypothesis_test(function) -> bool:
402+
def _is_hypothesis_test(function: Any) -> bool:
318403
return getattr(function, "is_hypothesis_test", False)
319404

320405

321-
def wrap_in_sync(func, _loop):
406+
def wrap_in_sync(func: Callable[..., Awaitable[Any]], _loop: asyncio.AbstractEventLoop):
322407
"""Return a sync wrapper around an async function executing it in the
323408
current event loop."""
324409

325410
# if the function is already wrapped, we rewrap using the original one
326411
# not using __wrapped__ because the original function may already be
327412
# a wrapped one
328-
if hasattr(func, "_raw_test_func"):
329-
func = func._raw_test_func
413+
raw_func = getattr(func, "_raw_test_func", None)
414+
if raw_func is not None:
415+
func = raw_func
330416

331417
@functools.wraps(func)
332418
def inner(**kwargs):
@@ -343,20 +429,22 @@ def inner(**kwargs):
343429
task.exception()
344430
raise
345431

346-
inner._raw_test_func = func
432+
inner._raw_test_func = func # type: ignore[attr-defined]
347433
return inner
348434

349435

350-
def pytest_runtest_setup(item):
436+
def pytest_runtest_setup(item: pytest.Item) -> None:
351437
if "asyncio" in item.keywords:
438+
fixturenames = item.fixturenames # type: ignore[attr-defined]
352439
# inject an event loop fixture for all async tests
353-
if "event_loop" in item.fixturenames:
354-
item.fixturenames.remove("event_loop")
355-
item.fixturenames.insert(0, "event_loop")
440+
if "event_loop" in fixturenames:
441+
fixturenames.remove("event_loop")
442+
fixturenames.insert(0, "event_loop")
443+
obj = item.obj # type: ignore[attr-defined]
356444
if (
357445
item.get_closest_marker("asyncio") is not None
358-
and not getattr(item.obj, "hypothesis", False)
359-
and getattr(item.obj, "is_hypothesis_test", False)
446+
and not getattr(obj, "hypothesis", False)
447+
and getattr(obj, "is_hypothesis_test", False)
360448
):
361449
pytest.fail(
362450
"test function `%r` is using Hypothesis, but pytest-asyncio "
@@ -365,32 +453,32 @@ def pytest_runtest_setup(item):
365453

366454

367455
@pytest.fixture
368-
def event_loop(request):
456+
def event_loop(request: pytest.FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
369457
"""Create an instance of the default event loop for each test case."""
370458
loop = asyncio.get_event_loop_policy().new_event_loop()
371459
yield loop
372460
loop.close()
373461

374462

375-
def _unused_port(socket_type):
463+
def _unused_port(socket_type: int) -> int:
376464
"""Find an unused localhost port from 1024-65535 and return it."""
377465
with contextlib.closing(socket.socket(type=socket_type)) as sock:
378466
sock.bind(("127.0.0.1", 0))
379467
return sock.getsockname()[1]
380468

381469

382470
@pytest.fixture
383-
def unused_tcp_port():
471+
def unused_tcp_port() -> int:
384472
return _unused_port(socket.SOCK_STREAM)
385473

386474

387475
@pytest.fixture
388-
def unused_udp_port():
476+
def unused_udp_port() -> int:
389477
return _unused_port(socket.SOCK_DGRAM)
390478

391479

392480
@pytest.fixture(scope="session")
393-
def unused_tcp_port_factory():
481+
def unused_tcp_port_factory() -> Callable[[], int]:
394482
"""A factory function, producing different unused TCP ports."""
395483
produced = set()
396484

@@ -409,7 +497,7 @@ def factory():
409497

410498

411499
@pytest.fixture(scope="session")
412-
def unused_udp_port_factory():
500+
def unused_udp_port_factory() -> Callable[[], int]:
413501
"""A factory function, producing different unused UDP ports."""
414502
produced = set()
415503

pytest_asyncio/py.typed

Whitespace-only changes.

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ classifiers =
2727

2828
Framework :: AsyncIO
2929
Framework :: Pytest
30+
Typing :: Typed
3031

3132
[options]
3233
python_requires = >=3.7
@@ -38,12 +39,14 @@ setup_requires =
3839

3940
install_requires =
4041
pytest >= 6.1.0
42+
typing-extensions >= 4.0
4143

4244
[options.extras_require]
4345
testing =
4446
coverage==6.2
4547
hypothesis >= 5.7.1
4648
flaky >= 3.5.0
49+
mypy == 0.931
4750

4851
[options.entry_points]
4952
pytest11 =

0 commit comments

Comments
 (0)