Skip to content
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

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

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions 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.
121 changes: 80 additions & 41 deletions src/_pytest/config/__init__.py
Expand Up @@ -527,9 +527,12 @@
#
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 @@
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 @@
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 @@

@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 @@

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

Check warning on line 1258 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1258

Added line #L1258 was not covered by tests
if pyargs:
result = testpaths

Check warning on line 1260 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1260

Added line #L1260 was not covered by tests
else:
result = []

Check warning on line 1262 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1262

Added line #L1262 was not covered by tests
for path in testpaths:
result.extend(sorted(glob.iglob(path, recursive=True)))

Check warning on line 1264 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1264

Added line #L1264 was not covered by tests
if testpaths and not result:
if warn:
warning_text = (

Check warning on line 1267 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1267

Added line #L1267 was not covered by tests
"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(

Check warning on line 1272 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1272

Added line #L1272 was not covered by tests
PytestConfigWarning(warning_text), stacklevel=3
)
else:
result = []

Check warning on line 1276 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1276

Added line #L1276 was not covered by tests
if not result:
source = Config.ArgsSource.INCOVATION_DIR
result = [str(invocation_dir)]

Check warning on line 1279 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1278-L1279

Added lines #L1278 - L1279 were not covered by tests
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 @@
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
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
@@ -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