diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed7d0cb0..ee649b19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,10 +37,12 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.8.0 hooks: - id: mypy exclude: ^(docs|tests)/.* + additional_dependencies: + - pytest - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index f662da68..bb54c0b0 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -5,6 +5,7 @@ Changelog 0.23.5 (UNRELEASED) =================== - Declare compatibility with pytest 8 `#737 `_ +- Fix typing errors with recent versions of mypy `#769 `_ Known issues ------------ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 82e21834..b238955d 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -16,11 +16,14 @@ Awaitable, Callable, Dict, + Generator, Iterable, Iterator, List, Literal, + Mapping, Optional, + Sequence, Set, Type, TypeVar, @@ -47,16 +50,14 @@ StashKey, ) -_R = TypeVar("_R") - _ScopeName = Literal["session", "package", "module", "class", "function"] _T = TypeVar("_T") SimpleFixtureFunction = TypeVar( - "SimpleFixtureFunction", bound=Callable[..., Awaitable[_R]] + "SimpleFixtureFunction", bound=Callable[..., Awaitable[object]] ) FactoryFixtureFunction = TypeVar( - "FactoryFixtureFunction", bound=Callable[..., AsyncIterator[_R]] + "FactoryFixtureFunction", bound=Callable[..., AsyncIterator[object]] ) FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction] FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction] @@ -204,6 +205,7 @@ def _preprocess_async_fixtures( config = collector.config asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") + assert fixturemanager is not None for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: func = fixturedef.func @@ -217,11 +219,13 @@ def _preprocess_async_fixtures( continue scope = fixturedef.scope if scope == "function": - event_loop_fixture_id = "event_loop" + event_loop_fixture_id: Optional[str] = "event_loop" else: event_loop_node = _retrieve_scope_root(collector, scope) event_loop_fixture_id = event_loop_node.stash.get( - _event_loop_fixture_id, None + # Type ignored because of non-optimal mypy inference. + _event_loop_fixture_id, # type: ignore[arg-type] + None, ) _make_asyncio_fixture_function(func) function_signature = inspect.signature(func) @@ -234,8 +238,15 @@ def _preprocess_async_fixtures( f"instead." ) ) - _inject_fixture_argnames(fixturedef, event_loop_fixture_id) - _synchronize_async_fixture(fixturedef, event_loop_fixture_id) + assert event_loop_fixture_id + _inject_fixture_argnames( + fixturedef, + event_loop_fixture_id, + ) + _synchronize_async_fixture( + fixturedef, + event_loop_fixture_id, + ) assert _is_asyncio_fixture_function(fixturedef.func) processed_fixturedefs.add(fixturedef) @@ -512,25 +523,26 @@ def pytest_pycollect_makeitem_preprocess_async_fixtures( return None +# TODO: #778 Narrow down return type of function when dropping support for pytest 7 # The function name needs to start with "pytest_" # see https://github.com/pytest-dev/pytest/issues/11307 @pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True) def pytest_pycollect_makeitem_convert_async_functions_to_subclass( collector: Union[pytest.Module, pytest.Class], name: str, obj: object -) -> Union[ - pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None -]: +) -> Generator[None, Any, None]: """ Converts coroutines and async generators collected as pytest.Functions to AsyncFunction items. """ hook_result = yield - node_or_list_of_nodes = hook_result.get_result() + node_or_list_of_nodes: Union[ + pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None + ] = hook_result.get_result() if not node_or_list_of_nodes: return - try: + if isinstance(node_or_list_of_nodes, Sequence): node_iterator = iter(node_or_list_of_nodes) - except TypeError: + else: # Treat single node as a single-element iterable node_iterator = iter((node_or_list_of_nodes,)) updated_node_collection = [] @@ -549,8 +561,8 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( hook_result.force_result(updated_node_collection) -_event_loop_fixture_id = StashKey[str] -_fixture_scope_by_collector_type = { +_event_loop_fixture_id = StashKey[str]() +_fixture_scope_by_collector_type: Mapping[Type[pytest.Collector], _ScopeName] = { Class: "class", # Package is a subclass of module and the dict is used in isinstance checks # Therefore, the order matters and Package needs to appear before Module @@ -565,7 +577,7 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( @pytest.hookimpl -def pytest_collectstart(collector: pytest.Collector): +def pytest_collectstart(collector: pytest.Collector) -> None: try: collector_scope = next( scope @@ -639,8 +651,8 @@ def _patched_collect(): pass return collector.__original_collect() - collector.__original_collect = collector.collect - collector.collect = _patched_collect + collector.__original_collect = collector.collect # type: ignore[attr-defined] + collector.collect = _patched_collect # type: ignore[method-assign] elif isinstance(collector, Class): collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop @@ -708,6 +720,7 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: if event_loop_fixture_id in metafunc.fixturenames: return fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") + assert fixturemanager is not None if "event_loop" in metafunc.fixturenames: raise MultipleEventLoopsRequestedError( _MULTIPLE_LOOPS_REQUESTED_ERROR.format( @@ -726,10 +739,11 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: ) +# TODO: #778 Narrow down return type of function when dropping support for pytest 7 @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup( - fixturedef: FixtureDef, request: SubRequest -) -> Optional[object]: + fixturedef: FixtureDef, +) -> Generator[None, Any, None]: """Adjust the event loop policy when an event loop is produced.""" if fixturedef.argname == "event_loop": # The use of a fixture finalizer is preferred over the @@ -744,7 +758,7 @@ def pytest_fixture_setup( _provide_clean_event_loop, ) outcome = yield - loop = outcome.get_result() + loop: asyncio.AbstractEventLoop = outcome.get_result() # Weird behavior was observed when checking for an attribute of FixtureDef.func # Instead, we now check for a special attribute of the returned event loop fixture_filename = inspect.getsourcefile(fixturedef.func) @@ -946,6 +960,7 @@ def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: scope_root_type = node_type_by_scope[scope] for node in reversed(item.listchain()): if isinstance(node, scope_root_type): + assert isinstance(node, pytest.Collector) return node error_message = ( f"{item.name} is marked to be run in an event loop with scope {scope}, " diff --git a/setup.cfg b/setup.cfg index f1f3ea00..9fba05ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ classifiers = [options] python_requires = >=3.8 -packages = find: +packages = pytest_asyncio include_package_data = True # Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hypothesis/__init__.py b/tests/hypothesis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/loop_fixture_scope/__init__.py b/tests/loop_fixture_scope/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/markers/__init__.py b/tests/markers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/modes/__init__.py b/tests/modes/__init__.py new file mode 100644 index 00000000..e69de29b