diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 2094027f3..b54a12731 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -849,7 +849,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: Modularity: using fixtures from a fixture function ---------------------------------------------------------- -You can not only use fixtures in test functions but fixture functions +In addition to using fixtures in test functions, fixture functions can use other fixtures themselves. This contributes to a modular design of your fixtures and allows re-use of framework-specific fixtures across many projects. As a simple example, we can extend the previous example diff --git a/scripts/publish-gh-release-notes.py b/scripts/publish-gh-release-notes.py index 583b5bfc7..2531b0221 100644 --- a/scripts/publish-gh-release-notes.py +++ b/scripts/publish-gh-release-notes.py @@ -61,7 +61,9 @@ def parse_changelog(tag_name): def convert_rst_to_md(text): - return pypandoc.convert_text(text, "md", format="rst", extra_args=["--wrap=none"]) + return pypandoc.convert_text( + text, "md", format="rst", extra_args=["--wrap=preserve"] + ) def main(argv): diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh index 6aa931383..fbcf20ca9 100755 --- a/scripts/report-coverage.sh +++ b/scripts/report-coverage.sh @@ -14,5 +14,5 @@ python -m coverage combine python -m coverage xml python -m coverage report -m # Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 -curl -S -L --connect-timeout 5 --retry 6 --retry-connrefused -s https://codecov.io/bash -o codecov-upload.sh +curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index f96afce6d..a060723a7 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -7,6 +7,10 @@ from typing import Optional from _pytest.assertion import rewrite from _pytest.assertion import truncate from _pytest.assertion import util +from _pytest.compat import TYPE_CHECKING + +if TYPE_CHECKING: + from _pytest.main import Session def pytest_addoption(parser): @@ -91,7 +95,7 @@ def install_importhook(config): return hook -def pytest_collection(session): +def pytest_collection(session: "Session") -> None: # this hook is only called when test modules are collected # so for example not in the master process of pytest-xdist # (which does not collect test modules) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 9ddf49316..0fc48bdba 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -144,12 +144,12 @@ def getfuncargnames( the case of cls, the function is a static method. The name parameter should be the original name in which the function was collected. - - @RonnyPfannschmidt: This function should be refactored when we - revisit fixtures. The fixture mechanism should ask the node for - the fixture names, and not try to obtain directly from the - function object well after collection has occurred. """ + # TODO(RonnyPfannschmidt): This function should be refactored when we + # revisit fixtures. The fixture mechanism should ask the node for + # the fixture names, and not try to obtain directly from the + # function object well after collection has occurred. + # The parameters attribute of a Signature object contains an # ordered mapping of parameter names to Parameter instances. This # creates a tuple of the names of the parameters that don't have diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 08486fbff..fb513be68 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -31,6 +31,7 @@ from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.deprecated import FUNCARGNAMES +from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -1262,8 +1263,6 @@ class FixtureManager: This things are done later as well when dealing with parametrization so this could be improved """ - from _pytest.mark import ParameterSet - parametrize_argnames = [] for marker in node.iter_markers(name="parametrize"): if not marker.kwargs.get("indirect", False): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 74dff1e82..a2c82f271 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,6 +1,14 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +from typing import Any +from typing import Optional + from pluggy import HookspecMarker +from _pytest.compat import TYPE_CHECKING + +if TYPE_CHECKING: + from _pytest.main import Session + hookspec = HookspecMarker("pytest") @@ -158,7 +166,7 @@ def pytest_load_initial_conftests(early_config, parser, args): @hookspec(firstresult=True) -def pytest_collection(session): +def pytest_collection(session: "Session") -> Optional[Any]: """Perform the collection protocol for the given session. Stops at first non-None result, see :ref:`firstresult`. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index e4ccad2c3..be8215b4e 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from io import StringIO from typing import AbstractSet from typing import Dict +from typing import Generator from typing import List from typing import Mapping @@ -597,7 +598,7 @@ class LoggingPlugin: ) is not None or self._config.getini("log_cli") @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_collection(self): + def pytest_collection(self) -> Generator[None, None, None]: with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("collection") diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 371b25f2f..1db97dc55 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -8,8 +8,10 @@ import sys from typing import Dict from typing import FrozenSet from typing import List +from typing import Optional from typing import Sequence from typing import Tuple +from typing import Union import attr import py @@ -253,7 +255,7 @@ def pytest_cmdline_main(config): return wrap_session(config, _main) -def _main(config, session): +def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: """ default command line protocol for initialization, session, running tests and reporting. """ config.hook.pytest_collection(session=session) @@ -263,6 +265,7 @@ def _main(config, session): return ExitCode.TESTS_FAILED elif session.testscollected == 0: return ExitCode.NO_TESTS_COLLECTED + return None def pytest_collection(session): @@ -352,18 +355,6 @@ def pytest_collection_modifyitems(items, config): items[:] = remaining -class FSHookProxy: - def __init__(self, fspath, pm, remove_mods): - self.fspath = fspath - self.pm = pm - self.remove_mods = remove_mods - - def __getattr__(self, name): - x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) - self.__dict__[name] = x - return x - - class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ @@ -405,7 +396,6 @@ class Session(nodes.FSCollector): self.shouldstop = False self.shouldfail = False self.trace = config.trace.root.get("collection") - self._norecursepatterns = config.getini("norecursedirs") self.startdir = config.invocation_dir self._initialpaths = frozenset() # type: FrozenSet[py.path.local] @@ -466,19 +456,8 @@ class Session(nodes.FSCollector): def isinitpath(self, path): return path in self._initialpaths - def gethookproxy(self, fspath): - # check if we have the common case of running - # hooks with all conftest.py files - pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules(fspath) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # one or more conftests are not in use at this fspath - proxy = FSHookProxy(fspath, pm, remove_mods) - else: - # all plugins are active for this fspath - proxy = self.config.hook - return proxy + def gethookproxy(self, fspath: py.path.local): + return super()._gethookproxy(fspath) def perform_collect(self, args=None, genitems=True): hook = self.config.hook @@ -643,19 +622,6 @@ class Session(nodes.FSCollector): return ihook.pytest_collect_file(path=path, parent=self) - def _recurse(self, dirpath): - if dirpath.basename == "__pycache__": - return False - ihook = self.gethookproxy(dirpath.dirpath()) - if ihook.pytest_ignore_collect(path=dirpath, config=self.config): - return False - for pat in self._norecursepatterns: - if dirpath.check(fnmatch=pat): - return False - ihook = self.gethookproxy(dirpath) - ihook.pytest_collect_directory(path=dirpath, parent=self) - return True - @staticmethod def _visit_filter(f): return f.check(file=1) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9f192ac83..0b0e394ac 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -19,6 +19,7 @@ from _pytest.compat import cached_property from _pytest.compat import getfslineno from _pytest.compat import TYPE_CHECKING from _pytest.config import Config +from _pytest.config import PytestPluginManager from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError @@ -423,6 +424,20 @@ def _check_initialpaths_for_relpath(session, fspath): return fspath.relto(initial_path) +class FSHookProxy: + def __init__( + self, fspath: py.path.local, pm: PytestPluginManager, remove_mods + ) -> None: + self.fspath = fspath + self.pm = pm + self.remove_mods = remove_mods + + def __getattr__(self, name: str): + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) + self.__dict__[name] = x + return x + + class FSCollector(Collector): def __init__( self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None @@ -447,6 +462,8 @@ class FSCollector(Collector): super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) + self._norecursepatterns = self.config.getini("norecursedirs") + @classmethod def from_parent(cls, parent, *, fspath): """ @@ -454,6 +471,33 @@ class FSCollector(Collector): """ return super().from_parent(parent=parent, fspath=fspath) + def _gethookproxy(self, fspath: py.path.local): + # check if we have the common case of running + # hooks with all conftest.py files + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugins are active for this fspath + proxy = self.config.hook + return proxy + + def _recurse(self, dirpath: py.path.local) -> bool: + if dirpath.basename == "__pycache__": + return False + ihook = self._gethookproxy(dirpath.dirpath()) + if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + return False + for pat in self._norecursepatterns: + if dirpath.check(fnmatch=pat): + return False + ihook = self._gethookproxy(dirpath) + ihook.pytest_collect_directory(path=dirpath, parent=self) + return True + class File(FSCollector): """ base class for collecting tests from a file. """ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6f553f6b1..5e3b5f286 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -36,7 +36,6 @@ from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl from _pytest.deprecated import FUNCARGNAMES -from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import Mark @@ -149,27 +148,30 @@ def pytest_configure(config): ) -@hookimpl(trylast=True) -def pytest_pyfunc_call(pyfuncitem): - def async_warn(): - msg = "async def functions are not natively supported and have been skipped.\n" - msg += "You need to install a suitable plugin for your async framework, for example:\n" - msg += " - pytest-asyncio\n" - msg += " - pytest-trio\n" - msg += " - pytest-tornasync" - warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) - skip(msg="async def function and no async plugin installed (see warnings)") +def async_warn(nodeid: str) -> None: + msg = "async def functions are not natively supported and have been skipped.\n" + msg += ( + "You need to install a suitable plugin for your async framework, for example:\n" + ) + msg += " - pytest-asyncio\n" + msg += " - pytest-trio\n" + msg += " - pytest-tornasync" + warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid))) + skip(msg="async def function and no async plugin installed (see warnings)") + +@hookimpl(trylast=True) +def pytest_pyfunc_call(pyfuncitem: "Function"): testfunction = pyfuncitem.obj if iscoroutinefunction(testfunction) or ( sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) ): - async_warn() + async_warn(pyfuncitem.nodeid) funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} result = testfunction(**testargs) if hasattr(result, "__await__") or hasattr(result, "__aiter__"): - async_warn() + async_warn(pyfuncitem.nodeid) return True @@ -547,15 +549,23 @@ class Module(nodes.File, PyCollector): class Package(Module): - def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): + def __init__( + self, + fspath: py.path.local, + parent: nodes.Collector, + # NOTE: following args are unused: + config=None, + session=None, + nodeid=None, + ) -> None: + # NOTE: could be just the following, but kept as-is for compat. + # nodes.FSCollector.__init__(self, fspath, parent=parent) session = parent.session nodes.FSCollector.__init__( self, fspath, parent=parent, config=config, session=session, nodeid=nodeid ) + self.name = fspath.dirname - self.trace = session.trace - self._norecursepatterns = session._norecursepatterns - self.fspath = fspath def setup(self): # not using fixtures to call setup_module here because autouse fixtures @@ -573,32 +583,8 @@ class Package(Module): func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) - def _recurse(self, dirpath): - if dirpath.basename == "__pycache__": - return False - ihook = self.gethookproxy(dirpath.dirpath()) - if ihook.pytest_ignore_collect(path=dirpath, config=self.config): - return - for pat in self._norecursepatterns: - if dirpath.check(fnmatch=pat): - return False - ihook = self.gethookproxy(dirpath) - ihook.pytest_collect_directory(path=dirpath, parent=self) - return True - - def gethookproxy(self, fspath): - # check if we have the common case of running - # hooks with all conftest.py filesall conftest.py - pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules(fspath) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # one or more conftests are not in use at this fspath - proxy = FSHookProxy(fspath, pm, remove_mods) - else: - # all plugins are active for this fspath - proxy = self.config.hook - return proxy + def gethookproxy(self, fspath: py.path.local): + return super()._gethookproxy(fspath) def _collectfile(self, path, handle_dupes=True): assert ( diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 1bfd2f91d..1248abe1b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -521,7 +521,7 @@ class TerminalReporter: # py < 1.6.0 return self._tw.chars_on_current_line - def pytest_collection(self): + def pytest_collection(self) -> None: if self.isatty: if self.config.option.verbose >= 0: self.write("collecting ... ", bold=True) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index b6ee049ec..18e4def21 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,8 +1,10 @@ import sys import warnings from contextlib import contextmanager +from typing import Generator import pytest +from _pytest.main import Session def _setoption(wmod, arg): @@ -117,7 +119,7 @@ def pytest_runtest_protocol(item): @pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection(session): +def pytest_collection(session: Session) -> Generator[None, None, None]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="collect", item=None diff --git a/tox.ini b/tox.ini index 707f239d0..65af0a4a8 100644 --- a/tox.ini +++ b/tox.ini @@ -147,7 +147,7 @@ commands = python scripts/publish-gh-release-notes.py {posargs} [pytest] minversion = 2.0 -addopts = -ra -p pytester --strict-markers +addopts = -rfEX -p pytester --strict-markers rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance