Skip to content

config: fix the paths considered for initial conftest discovery #11125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/11104.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
121 changes: 80 additions & 41 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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".

Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1223,6 +1236,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", "")
Expand Down Expand Up @@ -1371,34 +1427,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

Expand Down
7 changes: 7 additions & 0 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down
25 changes: 13 additions & 12 deletions testing/test_conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import argparse
import os
import textwrap
from pathlib import Path
Expand All @@ -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
Expand All @@ -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")
Expand Down