From d97d44a97af2303eb3f3aea1f16fd834f5415509 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Jun 2023 20:52:59 +0300 Subject: [PATCH 1/2] config: extract initial paths/nodeids args logic to a function Will be reused in the next commit. --- src/_pytest/config/__init__.py | 76 +++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 85d8830e766..686be1277f3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1223,6 +1223,49 @@ def _validate_args(self, args: List[str], via: str) -> List[str]: return args + def _decide_args( + self, + *, + args: List[str], + pyargs: List[str], + testpaths: List[str], + invocation_dir: Path, + rootpath: Path, + warn: bool, + ) -> Tuple[List[str], ArgsSource]: + """Decide the args (initial paths/nodeids) to use given the relevant inputs. + + :param warn: Whether can issue warnings. + """ + if args: + source = Config.ArgsSource.ARGS + result = args + else: + if invocation_dir == rootpath: + source = Config.ArgsSource.TESTPATHS + if pyargs: + result = testpaths + else: + result = [] + for path in testpaths: + result.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not result: + if warn: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) + else: + result = [] + if not result: + source = Config.ArgsSource.INCOVATION_DIR + result = [str(invocation_dir)] + return result, source + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") @@ -1371,34 +1414,17 @@ def parse(self, args: List[str], addopts: bool = True) -> None: self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.after_preparse = True # type: ignore try: - source = Config.ArgsSource.ARGS args = self._parser.parse_setoption( args, self.option, namespace=self.option ) - if not args: - if self.invocation_params.dir == self.rootpath: - source = Config.ArgsSource.TESTPATHS - testpaths: List[str] = self.getini("testpaths") - if self.known_args_namespace.pyargs: - args = testpaths - else: - args = [] - for path in testpaths: - args.extend(sorted(glob.iglob(path, recursive=True))) - if testpaths and not args: - warning_text = ( - "No files were found in testpaths; " - "consider removing or adjusting your testpaths configuration. " - "Searching recursively from the current directory instead." - ) - self.issue_config_time_warning( - PytestConfigWarning(warning_text), stacklevel=3 - ) - if not args: - source = Config.ArgsSource.INCOVATION_DIR - args = [str(self.invocation_params.dir)] - self.args = args - self.args_source = source + self.args, self.args_source = self._decide_args( + args=args, + pyargs=self.known_args_namespace.pyargs, + testpaths=self.getini("testpaths"), + invocation_dir=self.invocation_params.dir, + rootpath=self.rootpath, + warn=True, + ) except PrintHelp: pass From 14890329dcdc37085a8ac9c53ce332df9faead30 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Jun 2023 20:59:22 +0300 Subject: [PATCH 2/2] config: fix the paths considered for initial conftest discovery Fixes #11104. See the issue for a description of the problem. Now, we use the same logic for initial conftest paths as we do for deciding the initial args, which was the idea behind checking `namespace.file_or_dir` and `testpaths` previously. This fixes the issue of `testpaths` being considered for initial conftests even when it's not used for the args. (Another issue in faeb16146b811488ebbcbd17ef6f9102314065b2 was that the `testpaths` were not glob-expanded, this is also fixed.) --- changelog/11104.bugfix.rst | 3 +++ src/_pytest/config/__init__.py | 45 ++++++++++++++++++++++------------ testing/test_collection.py | 7 ++++++ testing/test_conftest.py | 25 ++++++++++--------- 4 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 changelog/11104.bugfix.rst diff --git a/changelog/11104.bugfix.rst b/changelog/11104.bugfix.rst new file mode 100644 index 00000000000..10f0db92515 --- /dev/null +++ b/changelog/11104.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests, +even when it was not utilized (e.g. when explicit paths were given on the command line). +Now the ``testpaths`` are only considered when they are in use. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 686be1277f3..c9a4b7f63cb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -527,9 +527,12 @@ def pytest_configure(self, config: "Config") -> None: # def _set_initial_conftests( self, - namespace: argparse.Namespace, + args: Sequence[Union[str, Path]], + pyargs: bool, + noconftest: bool, rootpath: Path, - testpaths_ini: Sequence[str], + confcutdir: Optional[Path], + importmode: Union[ImportMode, str], ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -539,17 +542,12 @@ def _set_initial_conftests( common options will not confuse our logic here. """ current = Path.cwd() - self._confcutdir = ( - absolutepath(current / namespace.confcutdir) - if namespace.confcutdir - else None - ) - self._noconftest = namespace.noconftest - self._using_pyargs = namespace.pyargs - testpaths = namespace.file_or_dir + testpaths_ini + self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None + self._noconftest = noconftest + self._using_pyargs = pyargs foundanchor = False - for testpath in testpaths: - path = str(testpath) + for intitial_path in args: + path = str(intitial_path) # remove node-id syntax i = path.find("::") if i != -1: @@ -563,10 +561,10 @@ def _set_initial_conftests( except OSError: # pragma: no cover anchor_exists = False if anchor_exists: - self._try_load_conftest(anchor, namespace.importmode, rootpath) + self._try_load_conftest(anchor, importmode, rootpath) foundanchor = True if not foundanchor: - self._try_load_conftest(current, namespace.importmode, rootpath) + self._try_load_conftest(current, importmode, rootpath) def _is_in_confcutdir(self, path: Path) -> bool: """Whether a path is within the confcutdir. @@ -1140,10 +1138,25 @@ def _processopt(self, opt: "Argument") -> None: @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: + # We haven't fully parsed the command line arguments yet, so + # early_config.args it not set yet. But we need it for + # discovering the initial conftests. So "pre-run" the logic here. + # It will be done for real in `parse()`. + args, args_source = early_config._decide_args( + args=early_config.known_args_namespace.file_or_dir, + pyargs=early_config.known_args_namespace.pyargs, + testpaths=early_config.getini("testpaths"), + invocation_dir=early_config.invocation_params.dir, + rootpath=early_config.rootpath, + warn=False, + ) self.pluginmanager._set_initial_conftests( - early_config.known_args_namespace, + args=args, + pyargs=early_config.known_args_namespace.pyargs, + noconftest=early_config.known_args_namespace.noconftest, rootpath=early_config.rootpath, - testpaths_ini=self.getini("testpaths"), + confcutdir=early_config.known_args_namespace.confcutdir, + importmode=early_config.known_args_namespace.importmode, ) def _initini(self, args: Sequence[str]) -> None: diff --git a/testing/test_collection.py b/testing/test_collection.py index bbcb358b6ff..3021398720f 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1264,11 +1264,18 @@ def pytest_sessionstart(session): testpaths = some_path """ ) + + # No command line args - falls back to testpaths. result = pytester.runpytest() + assert result.ret == ExitCode.INTERNAL_ERROR result.stdout.fnmatch_lines( "INTERNALERROR* Exception: pytest_sessionstart hook successfully run" ) + # No fallback. + result = pytester.runpytest(".") + assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None: """Long option values do not break initial conftests handling (#10169).""" diff --git a/testing/test_conftest.py b/testing/test_conftest.py index c64bd11d4ed..f857cde04ba 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,3 @@ -import argparse import os import textwrap from pathlib import Path @@ -7,6 +6,8 @@ from typing import Generator from typing import List from typing import Optional +from typing import Sequence +from typing import Union import pytest from _pytest.config import ExitCode @@ -24,18 +25,18 @@ def ConftestWithSetinitial(path) -> PytestPluginManager: def conftest_setinitial( - conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None + conftest: PytestPluginManager, + args: Sequence[Union[str, Path]], + confcutdir: Optional[Path] = None, ) -> None: - class Namespace: - def __init__(self) -> None: - self.file_or_dir = args - self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None - self.noconftest = False - self.pyargs = False - self.importmode = "prepend" - - namespace = cast(argparse.Namespace, Namespace()) - conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[]) + conftest._set_initial_conftests( + args=args, + pyargs=False, + noconftest=False, + rootpath=Path(args[0]), + confcutdir=confcutdir, + importmode="prepend", + ) @pytest.mark.usefixtures("_sys_snapshot")