Skip to content

Commit 0cf861b

Browse files
committed
Add shell-style wildcard support to 'testpaths'
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.
1 parent eb8b3ad commit 0cf861b

6 files changed

Lines changed: 37 additions & 16 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Jeff Widman
164164
Jenni Rinker
165165
John Eddie Ayson
166166
John Towler
167+
Jon Parise
167168
Jon Sonesen
168169
Jonas Obrist
169170
Jordan Guymon

changelog/9897.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added shell-style wildcard support to ``testpaths``.

doc/en/reference/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,6 +1761,7 @@ passed multiple times. The expected format is ``name=value``. For example::
17611761
Sets list of directories that should be searched for tests when
17621762
no specific directories, files or test ids are given in the command line when
17631763
executing pytest from the :ref:`rootdir <rootdir>` directory.
1764+
May use shell-style wildcards, including the recursive ``**`` pattern.
17641765
Useful when all project tests are in a known location to speed up
17651766
test collection and to avoid picking up undesired tests by accident.
17661767

src/_pytest/config/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections.abc
44
import copy
55
import enum
6+
import glob
67
import inspect
78
import os
89
import re
@@ -899,6 +900,19 @@ class InvocationParams:
899900
dir: Path
900901
"""The directory from which :func:`pytest.main` was invoked."""
901902

903+
class ArgsSource(enum.Enum):
904+
"""Indicates the source of the test arguments.
905+
906+
.. versionadded:: 7.2
907+
"""
908+
909+
#: Command line arguments.
910+
ARGS = enum.auto()
911+
#: Invocation directory.
912+
INCOVATION_DIR = enum.auto()
913+
#: 'testpaths' configuration value.
914+
TESTPATHS = enum.auto()
915+
902916
def __init__(
903917
self,
904918
pluginmanager: PytestPluginManager,
@@ -1308,15 +1322,22 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
13081322
self.hook.pytest_cmdline_preparse(config=self, args=args)
13091323
self._parser.after_preparse = True # type: ignore
13101324
try:
1325+
source = Config.ArgsSource.ARGS
13111326
args = self._parser.parse_setoption(
13121327
args, self.option, namespace=self.option
13131328
)
13141329
if not args:
13151330
if self.invocation_params.dir == self.rootpath:
1316-
args = self.getini("testpaths")
1331+
args = []
1332+
source = Config.ArgsSource.TESTPATHS
1333+
testpaths: List[str] = self.getini("testpaths")
1334+
for path in testpaths:
1335+
args.extend(glob.iglob(path, recursive=True))
13171336
if not args:
1337+
source = Config.ArgsSource.INCOVATION_DIR
13181338
args = [str(self.invocation_params.dir)]
13191339
self.args = args
1340+
self.args_source = source
13201341
except PrintHelp:
13211342
pass
13221343

src/_pytest/terminal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,8 +728,8 @@ def pytest_report_header(self, config: Config) -> List[str]:
728728
if config.inipath:
729729
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
730730

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

735735
result = [line]

testing/test_collection.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -244,28 +244,28 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No
244244
pytester.makeini(
245245
"""
246246
[pytest]
247-
testpaths = gui uts
247+
testpaths = */tests
248248
"""
249249
)
250250
tmp_path = pytester.path
251-
ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass")
252-
ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass")
253-
ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass")
251+
ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass")
252+
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text("def test_b(): pass")
253+
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text("def test_c(): pass")
254254

255255
# executing from rootdir only tests from `testpaths` directories
256256
# are collected
257257
items, reprec = pytester.inline_genitems("-v")
258-
assert [x.name for x in items] == ["test_gui", "test_uts"]
258+
assert {x.name for x in items} == {"test_b", "test_c"}
259259

260260
# check that explicitly passing directories in the command-line
261261
# collects the tests
262-
for dirname in ("env", "gui", "uts"):
262+
for dirname in ("a", "b", "c"):
263263
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
264264
assert [x.name for x in items] == ["test_%s" % dirname]
265265

266266
# changing cwd to each subdirectory and running pytest without
267267
# arguments collects the tests in that directory normally
268-
for dirname in ("env", "gui", "uts"):
268+
for dirname in ("a", "b", "c"):
269269
monkeypatch.chdir(pytester.path.joinpath(dirname))
270270
items, reprec = pytester.inline_genitems()
271271
assert [x.name for x in items] == ["test_%s" % dirname]
@@ -1212,19 +1212,16 @@ def test_collect_pyargs_with_testpaths(
12121212
testmod.joinpath("__init__.py").write_text("def test_func(): pass")
12131213
testmod.joinpath("test_file.py").write_text("def test_func(): pass")
12141214

1215-
root = pytester.mkdir("root")
1216-
root.joinpath("pytest.ini").write_text(
1217-
textwrap.dedent(
1218-
"""
1215+
pytester.makeini(
1216+
"""
12191217
[pytest]
12201218
addopts = --pyargs
12211219
testpaths = testmod
12221220
"""
1223-
)
12241221
)
12251222
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
12261223
with monkeypatch.context() as mp:
1227-
mp.chdir(root)
1224+
mp.chdir(pytester.path)
12281225
result = pytester.runpytest_subprocess()
12291226
result.stdout.fnmatch_lines(["*1 passed in*"])
12301227

0 commit comments

Comments
 (0)