Skip to content

Commit

Permalink
Add shell-style wildcard support to 'testpaths'
Browse files Browse the repository at this point in the history
The implementation uses the standard `glob` module to perform wildcard
expansion in Config.parse().

The related logic that determines whether or not to include 'testpaths'
in the terminal header was previously relying on a weak heuristic: if
Config.args matched 'testpaths', then its value was printed. That
generally worked, but it could also print when the user explicitly used
the same arguments on the command-line as listed in 'testpaths'. Not a
big deal, but it shows that the check was logically incorrect.

Now that 'testpaths' can contain wildcards, it's no longer possible to
perform this simple comparison, so this change also introduces a public
Config.ArgSource enum and Config.args_source attribute that explicitly
names the "source" of the arguments: the command line, the invocation
directory, or the 'testdata' configuration value.
  • Loading branch information
jparise committed Apr 27, 2022
1 parent eb8b3ad commit 70df85c
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 15 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Jeff Widman
Jenni Rinker
John Eddie Ayson
John Towler
Jon Parise
Jon Sonesen
Jonas Obrist
Jordan Guymon
Expand Down
1 change: 1 addition & 0 deletions changelog/X.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added shell-style wildcard support to ``testpaths``.
1 change: 1 addition & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,7 @@ passed multiple times. The expected format is ``name=value``. For example::
Sets list of directories that should be searched for tests when
no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory.
May use shell-style wildcards, including the recursive ``**`` pattern.
Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident.

Expand Down
24 changes: 23 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections.abc
import copy
import enum
import glob
import inspect
import os
import re
Expand Down Expand Up @@ -899,6 +900,20 @@ class InvocationParams:
dir: Path
"""The directory from which :func:`pytest.main` was invoked."""

@final
class ArgsSource(enum.IntEnum):
"""Indicates the source of the test arguments.
.. versionadded:: 7.2
"""

#: Command line arguments.
ARGS = 0
#: Invocation directory.
INCOVATION_DIR = 1
#: 'testpaths' configuration value.
TESTPATHS = 2

def __init__(
self,
pluginmanager: PytestPluginManager,
Expand Down Expand Up @@ -1308,15 +1323,22 @@ 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:
args = self.getini("testpaths")
args = []
source = Config.ArgsSource.TESTPATHS
testpaths = cast(List[str], self.getini("testpaths"))
for path in testpaths:
args.extend(glob.iglob(path, recursive=True))
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
except PrintHelp:
pass

Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,8 @@ def pytest_report_header(self, config: Config) -> List[str]:
if config.inipath:
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)

testpaths: List[str] = config.getini("testpaths")
if config.invocation_params.dir == config.rootpath and config.args == testpaths:
if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: List[str] = config.getini("testpaths")
line += ", testpaths: {}".format(", ".join(testpaths))

result = [line]
Expand Down
23 changes: 11 additions & 12 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,26 +1207,25 @@ def test_1():
def test_collect_pyargs_with_testpaths(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
testmod = pytester.mkdir("testmod")
# NOTE: __init__.py is not collected since it does not match python_files.
testmod.joinpath("__init__.py").write_text("def test_func(): pass")
testmod.joinpath("test_file.py").write_text("def test_func(): pass")
for i in range(3):
testpkg = pytester.mkpydir(f"tests{i}")
testmod = pytester.mkdir(str(testpkg.joinpath("testmod")))
# NOTE: __init__.py is not collected since it does not match python_files.
testmod.joinpath("__init__.py").write_text("def test_func(): pass")
testmod.joinpath("test_file.py").write_text("def test_func(): pass")

root = pytester.mkdir("root")
root.joinpath("pytest.ini").write_text(
textwrap.dedent(
"""
pytester.makeini(
"""
[pytest]
addopts = --pyargs
testpaths = testmod
testpaths = tests*/testmod
"""
)
)
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
with monkeypatch.context() as mp:
mp.chdir(root)
mp.chdir(pytester.path)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 passed in*"])
result.stdout.fnmatch_lines(["*3 passed in*"])


def test_collect_symlink_file_arg(pytester: Pytester) -> None:
Expand Down

0 comments on commit 70df85c

Please sign in to comment.