From 77f7f59b2a7058d75c2d4fd8fc15ee6858dc5dc3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Aug 2023 09:53:45 +0300 Subject: [PATCH] Fixes for typed pluggy Since version 1.3 pluggy added typing, which requires some fixes to please mypy. --- changelog/11353.trivial.rst | 1 + doc/en/reference/reference.rst | 4 ++-- setup.cfg | 2 +- src/_pytest/config/__init__.py | 21 +++++++++++++-------- src/_pytest/helpconfig.py | 6 +++++- src/_pytest/logging.py | 2 ++ src/_pytest/pytester.py | 2 +- testing/test_pluginmanager.py | 10 ++++++++-- 8 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 changelog/11353.trivial.rst diff --git a/changelog/11353.trivial.rst b/changelog/11353.trivial.rst new file mode 100644 index 000000000..10a6b4692 --- /dev/null +++ b/changelog/11353.trivial.rst @@ -0,0 +1 @@ +pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index d578fc285..b172c3e9c 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -980,10 +980,10 @@ TestShortLogReport .. autoclass:: pytest.TestShortLogReport() :members: -_Result +Result ~~~~~~~ -Result object used within :ref:`hook wrappers `, see :py:class:`_Result in the pluggy documentation ` for more information. +Result object used within :ref:`hook wrappers `, see :py:class:`Result in the pluggy documentation ` for more information. Stash ~~~~~ diff --git a/setup.cfg b/setup.cfg index 945635369..3b1c627de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ py_modules = py install_requires = iniconfig packaging - pluggy>=1.2.0,<2.0 + pluggy>=1.3.0,<2.0 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" tomli>=1.0.0;python_version<"3.11" diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1d9c49aa1..cde230fdb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -38,7 +38,9 @@ from typing import TYPE_CHECKING from typing import Union from pluggy import HookimplMarker +from pluggy import HookimplOpts from pluggy import HookspecMarker +from pluggy import HookspecOpts from pluggy import PluginManager import _pytest._code @@ -440,15 +442,17 @@ class PytestPluginManager(PluginManager): # Used to know when we are importing conftests after the pytest_configure stage. self._configured = False - def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): + def parse_hookimpl_opts( + self, plugin: _PluggyPlugin, name: str + ) -> Optional[HookimplOpts]: # pytest hooks are always prefixed with "pytest_", # so we avoid accessing possibly non-readable attributes # (see issue #1073). if not name.startswith("pytest_"): - return + return None # Ignore names which can not be hooks. if name == "pytest_plugins": - return + return None opts = super().parse_hookimpl_opts(plugin, name) if opts is not None: @@ -457,18 +461,18 @@ class PytestPluginManager(PluginManager): method = getattr(plugin, name) # Consider only actual functions for hooks (#3775). if not inspect.isroutine(method): - return + return None # Collect unmarked hooks as long as they have the `pytest_' prefix. - return _get_legacy_hook_marks( + return _get_legacy_hook_marks( # type: ignore[return-value] method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") ) - def parse_hookspec_opts(self, module_or_class, name: str): + def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]: opts = super().parse_hookspec_opts(module_or_class, name) if opts is None: method = getattr(module_or_class, name) if name.startswith("pytest_"): - opts = _get_legacy_hook_marks( + opts = _get_legacy_hook_marks( # type: ignore[assignment] method, "spec", ("firstresult", "historic"), @@ -1067,9 +1071,10 @@ class Config: fin() def get_terminal_writer(self) -> TerminalWriter: - terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( + terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin( "terminalreporter" ) + assert terminalreporter is not None return terminalreporter._tw def pytest_cmdline_parse( diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 4122d6009..364bf4c42 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -12,6 +12,7 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PrintHelp from _pytest.config.argparsing import Parser +from _pytest.terminal import TerminalReporter class HelpAction(Action): @@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: def showhelp(config: Config) -> None: import textwrap - reporter = config.pluginmanager.get_plugin("terminalreporter") + reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin( + "terminalreporter" + ) + assert reporter is not None tw = reporter._tw tw.write(config._parser.optparser.format_help()) tw.line() diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 8de690d9b..245b7aed0 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -659,6 +659,8 @@ class LoggingPlugin: ) if self._log_cli_enabled(): terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + # Guaranteed by `_log_cli_enabled()`. + assert terminal_reporter is not None capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. self.log_cli_handler: Union[ diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 649338599..854ee6834 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -751,7 +751,7 @@ class Pytester: def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" - pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) + pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined] self._request.addfinalizer(reprec.finish_recording) return reprec diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index c6f518b1d..e5773412f 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -242,8 +242,12 @@ class TestPytestPluginManager: mod = types.ModuleType("temp") mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"] pytestpm.consider_module(mod) - assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1" - assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2" + p1 = pytestpm.get_plugin("pytest_p1") + assert p1 is not None + assert p1.__name__ == "pytest_p1" + p2 = pytestpm.get_plugin("pytest_p2") + assert p2 is not None + assert p2.__name__ == "pytest_p2" def test_consider_module_import_module( self, pytester: Pytester, _config_for_test: Config @@ -336,6 +340,7 @@ class TestPytestPluginManager: len2 = len(pytestpm.get_plugins()) assert len1 == len2 plugin1 = pytestpm.get_plugin("pytest_hello") + assert plugin1 is not None assert plugin1.__name__.endswith("pytest_hello") plugin2 = pytestpm.get_plugin("pytest_hello") assert plugin2 is plugin1 @@ -351,6 +356,7 @@ class TestPytestPluginManager: pluginname = "pkg.plug" pytestpm.import_plugin(pluginname) mod = pytestpm.get_plugin("pkg.plug") + assert mod is not None assert mod.x == 3 def test_consider_conftest_deps(