Skip to content

Commit d78c477

Browse files
authored
Merge branch 'main' into fix-capteesys-duplicate-output
2 parents e105a13 + 37acc4f commit d78c477

File tree

13 files changed

+386
-69
lines changed

13 files changed

+386
-69
lines changed

AUTHORS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ Mike Hoyle (hoylemd)
320320
Mike Lundy
321321
Milan Lesnek
322322
Miro Hrončok
323+
Mulat Mekonen
323324
mrbean-bremen
324325
Nathan Goldbaum
325326
Nathan Rousseau
@@ -383,6 +384,7 @@ Reza Mousavi
383384
Raquel Alegre
384385
Ravi Chandra
385386
Reagan Lee
387+
Reilly Brogan
386388
Rob Arrow
387389
Robert Holt
388390
Roberto Aldera
@@ -401,6 +403,7 @@ Sadra Barikbin
401403
Saiprasad Kale
402404
Samuel Colvin
403405
Samuel Dion-Girardeau
406+
Samuel Gaist
404407
Samuel Jirovec
405408
Samuel Searles-Bryant
406409
Samuel Therrien (Avasam)

changelog/12244.contrib.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed self-test failures when `TERM=dumb`.

changelog/13330.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Having pytest configuration spread over more than one file (for example having both a ``pytest.ini`` file and ``pyproject.toml`` with a ``[tool.pytest.ini_options]`` table) will now print a warning to make it clearer to the user that only one of them is actually used.
2+
3+
-- by :user:`sgaist`

changelog/13771.contrib.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Skip `test_do_not_collect_symlink_siblings` on Windows environments without symlink support to avoid false negatives.

src/_pytest/config/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1242,14 +1242,15 @@ def _initini(self, args: Sequence[str]) -> None:
12421242
ns, unknown_args = self._parser.parse_known_and_unknown_args(
12431243
args, namespace=copy.copy(self.option)
12441244
)
1245-
rootpath, inipath, inicfg = determine_setup(
1245+
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
12461246
inifile=ns.inifilename,
12471247
args=ns.file_or_dir + unknown_args,
12481248
rootdir_cmd_arg=ns.rootdir or None,
12491249
invocation_dir=self.invocation_params.dir,
12501250
)
12511251
self._rootpath = rootpath
12521252
self._inipath = inipath
1253+
self._ignored_config_files = ignored_config_files
12531254
self.inicfg = inicfg
12541255
self._parser.extra_info["rootdir"] = str(self.rootpath)
12551256
self._parser.extra_info["inifile"] = str(self.inipath)

src/_pytest/config/findpaths.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ def make_scalar(v: object) -> str | list[str]:
9191
def locate_config(
9292
invocation_dir: Path,
9393
args: Iterable[Path],
94-
) -> tuple[Path | None, Path | None, ConfigDict]:
94+
) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]:
9595
"""Search in the list of arguments for a valid ini-file for pytest,
96-
and return a tuple of (rootdir, inifile, cfg-dict)."""
96+
and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where
97+
ignored-config-files is a list of config basenames found that contain
98+
pytest configuration but were ignored."""
9799
config_names = [
98100
"pytest.ini",
99101
".pytest.ini",
@@ -105,6 +107,8 @@ def locate_config(
105107
if not args:
106108
args = [invocation_dir]
107109
found_pyproject_toml: Path | None = None
110+
ignored_config_files: list[str] = []
111+
108112
for arg in args:
109113
argpath = absolutepath(arg)
110114
for base in (argpath, *argpath.parents):
@@ -115,10 +119,18 @@ def locate_config(
115119
found_pyproject_toml = p
116120
ini_config = load_config_dict_from_file(p)
117121
if ini_config is not None:
118-
return base, p, ini_config
122+
index = config_names.index(config_name)
123+
for remainder in config_names[index + 1 :]:
124+
p2 = base / remainder
125+
if (
126+
p2.is_file()
127+
and load_config_dict_from_file(p2) is not None
128+
):
129+
ignored_config_files.append(remainder)
130+
return base, p, ini_config, ignored_config_files
119131
if found_pyproject_toml is not None:
120-
return found_pyproject_toml.parent, found_pyproject_toml, {}
121-
return None, None, {}
132+
return found_pyproject_toml.parent, found_pyproject_toml, {}, []
133+
return None, None, {}, []
122134

123135

124136
def get_common_ancestor(
@@ -178,7 +190,7 @@ def determine_setup(
178190
args: Sequence[str],
179191
rootdir_cmd_arg: str | None,
180192
invocation_dir: Path,
181-
) -> tuple[Path, Path | None, ConfigDict]:
193+
) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]:
182194
"""Determine the rootdir, inifile and ini configuration values from the
183195
command line arguments.
184196
@@ -193,6 +205,8 @@ def determine_setup(
193205
"""
194206
rootdir = None
195207
dirs = get_dirs_from_args(args)
208+
ignored_config_files: Sequence[str] = []
209+
196210
if inifile:
197211
inipath_ = absolutepath(inifile)
198212
inipath: Path | None = inipath_
@@ -201,15 +215,17 @@ def determine_setup(
201215
rootdir = inipath_.parent
202216
else:
203217
ancestor = get_common_ancestor(invocation_dir, dirs)
204-
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
218+
rootdir, inipath, inicfg, ignored_config_files = locate_config(
219+
invocation_dir, [ancestor]
220+
)
205221
if rootdir is None and rootdir_cmd_arg is None:
206222
for possible_rootdir in (ancestor, *ancestor.parents):
207223
if (possible_rootdir / "setup.py").is_file():
208224
rootdir = possible_rootdir
209225
break
210226
else:
211227
if dirs != [ancestor]:
212-
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
228+
rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs)
213229
if rootdir is None:
214230
rootdir = get_common_ancestor(
215231
invocation_dir, [invocation_dir, ancestor]
@@ -223,7 +239,7 @@ def determine_setup(
223239
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
224240
)
225241
assert rootdir is not None
226-
return rootdir, inipath, inicfg or {}
242+
return rootdir, inipath, inicfg or {}, ignored_config_files
227243

228244

229245
def is_fs_root(p: Path) -> bool:

src/_pytest/fixtures.py

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383

8484
# The value of the fixture -- return/yield of the fixture function (type variable).
85-
FixtureValue = TypeVar("FixtureValue")
85+
FixtureValue = TypeVar("FixtureValue", covariant=True)
8686
# The type of the fixture function (type variable).
8787
FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
8888
# The type of a fixture function (type alias generic in fixture value).
@@ -106,12 +106,6 @@
106106
)
107107

108108

109-
@dataclasses.dataclass(frozen=True)
110-
class PseudoFixtureDef(Generic[FixtureValue]):
111-
cached_result: _FixtureCachedResult[FixtureValue]
112-
_scope: Scope
113-
114-
115109
def pytest_sessionstart(session: Session) -> None:
116110
session._fixturemanager = FixtureManager(session)
117111

@@ -420,7 +414,7 @@ def scope(self) -> _ScopeName:
420414
@abc.abstractmethod
421415
def _check_scope(
422416
self,
423-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
417+
requested_fixturedef: FixtureDef[object],
424418
requested_scope: Scope,
425419
) -> None:
426420
raise NotImplementedError()
@@ -559,12 +553,9 @@ def _iter_chain(self) -> Iterator[SubRequest]:
559553
yield current
560554
current = current._parent_request
561555

562-
def _get_active_fixturedef(
563-
self, argname: str
564-
) -> FixtureDef[object] | PseudoFixtureDef[object]:
556+
def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
565557
if argname == "request":
566-
cached_result = (self, [0], None)
567-
return PseudoFixtureDef(cached_result, Scope.Function)
558+
return RequestFixtureDef(self)
568559

569560
# If we already finished computing a fixture by this name in this item,
570561
# return it.
@@ -696,7 +687,7 @@ def _scope(self) -> Scope:
696687

697688
def _check_scope(
698689
self,
699-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
690+
requested_fixturedef: FixtureDef[object],
700691
requested_scope: Scope,
701692
) -> None:
702693
# TopRequest always has function scope so always valid.
@@ -775,11 +766,9 @@ def node(self):
775766

776767
def _check_scope(
777768
self,
778-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
769+
requested_fixturedef: FixtureDef[object],
779770
requested_scope: Scope,
780771
) -> None:
781-
if isinstance(requested_fixturedef, PseudoFixtureDef):
782-
return
783772
if self._scope > requested_scope:
784773
# Try to report something helpful.
785774
argname = requested_fixturedef.argname
@@ -968,7 +957,6 @@ def _eval_scope_callable(
968957
return result
969958

970959

971-
@final
972960
class FixtureDef(Generic[FixtureValue]):
973961
"""A container for a fixture definition.
974962
@@ -1083,8 +1071,7 @@ def execute(self, request: SubRequest) -> FixtureValue:
10831071
# down first. This is generally handled by SetupState, but still currently
10841072
# needed when this fixture is not parametrized but depends on a parametrized
10851073
# fixture.
1086-
if not isinstance(fixturedef, PseudoFixtureDef):
1087-
requested_fixtures_that_should_finalize_us.append(fixturedef)
1074+
requested_fixtures_that_should_finalize_us.append(fixturedef)
10881075

10891076
# Check for (and return) cached value/exception.
10901077
if self.cached_result is not None:
@@ -1136,6 +1123,28 @@ def __repr__(self) -> str:
11361123
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
11371124

11381125

1126+
class RequestFixtureDef(FixtureDef[FixtureRequest]):
1127+
"""A custom FixtureDef for the special "request" fixture.
1128+
1129+
A new one is generated on-demand whenever "request" is requested.
1130+
"""
1131+
1132+
def __init__(self, request: FixtureRequest) -> None:
1133+
super().__init__(
1134+
config=request.config,
1135+
baseid=None,
1136+
argname="request",
1137+
func=lambda: request,
1138+
scope=Scope.Function,
1139+
params=None,
1140+
_ispytest=True,
1141+
)
1142+
self.cached_result = (request, [0], None)
1143+
1144+
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
1145+
pass
1146+
1147+
11391148
def resolve_fixture_function(
11401149
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
11411150
) -> _FixtureFunc[FixtureValue]:
@@ -1634,29 +1643,44 @@ def getfixtureclosure(
16341643
fixturenames_closure = list(initialnames)
16351644

16361645
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {}
1637-
lastlen = -1
1638-
while lastlen != len(fixturenames_closure):
1639-
lastlen = len(fixturenames_closure)
1640-
for argname in fixturenames_closure:
1641-
if argname in ignore_args:
1642-
continue
1643-
if argname in arg2fixturedefs:
1644-
continue
1646+
1647+
# Track the index for each fixture name in the simulated stack.
1648+
# Needed for handling override chains correctly, similar to _get_active_fixturedef.
1649+
# Using negative indices: -1 is the most specific (last), -2 is second to last, etc.
1650+
current_indices: dict[str, int] = {}
1651+
1652+
def process_argname(argname: str) -> None:
1653+
# Optimization: already processed this argname.
1654+
if current_indices.get(argname) == -1:
1655+
return
1656+
1657+
if argname not in fixturenames_closure:
1658+
fixturenames_closure.append(argname)
1659+
1660+
if argname in ignore_args:
1661+
return
1662+
1663+
fixturedefs = arg2fixturedefs.get(argname)
1664+
if not fixturedefs:
16451665
fixturedefs = self.getfixturedefs(argname, parentnode)
1646-
if fixturedefs:
1647-
arg2fixturedefs[argname] = fixturedefs
1648-
1649-
# Add dependencies from this fixture.
1650-
# If it overrides a fixture with the same name and requests
1651-
# it, also add dependencies from the overridden fixtures in
1652-
# the chain. See also similar dealing in _get_active_fixturedef().
1653-
for fixturedef in reversed(fixturedefs): # pragma: no cover
1654-
for arg in fixturedef.argnames:
1655-
if arg not in fixturenames_closure:
1656-
fixturenames_closure.append(arg)
1657-
if argname not in fixturedef.argnames:
1658-
# Overrides, but doesn't request super.
1659-
break
1666+
if not fixturedefs:
1667+
# Fixture not defined or not visible (will error during runtest).
1668+
return
1669+
arg2fixturedefs[argname] = fixturedefs
1670+
1671+
index = current_indices.get(argname, -1)
1672+
if -index > len(fixturedefs):
1673+
# Exhausted the override chain (will error during runtest).
1674+
return
1675+
fixturedef = fixturedefs[index]
1676+
1677+
current_indices[argname] = index - 1
1678+
for dep in fixturedef.argnames:
1679+
process_argname(dep)
1680+
current_indices[argname] = index
1681+
1682+
for name in initialnames:
1683+
process_argname(name)
16601684

16611685
def sort_by_scope(arg_name: str) -> Scope:
16621686
try:

src/_pytest/terminal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,12 @@ def pytest_report_header(self, config: Config) -> list[str]:
879879
result = [f"rootdir: {config.rootpath}"]
880880

881881
if config.inipath:
882-
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
882+
warning = ""
883+
if config._ignored_config_files:
884+
warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)"
885+
result.append(
886+
"configfile: " + bestrelpath(config.rootpath, config.inipath) + warning
887+
)
883888

884889
if config.args_source == Config.ArgsSource.TESTPATHS:
885890
testpaths: list[str] = config.getini("testpaths")

testing/io/test_terminalwriter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def test_NO_COLOR_and_FORCE_COLOR(
224224

225225

226226
def test_empty_NO_COLOR_and_FORCE_COLOR_ignored(monkeypatch: MonkeyPatch) -> None:
227+
monkeypatch.setenv("TERM", "xterm-256color")
227228
monkeypatch.setitem(os.environ, "NO_COLOR", "")
228229
monkeypatch.setitem(os.environ, "FORCE_COLOR", "")
229230
assert_color(True, True)

0 commit comments

Comments
 (0)