Skip to content

Commit 072e2a0

Browse files
authored
Merge branch 'main' into main
2 parents 5902175 + 66834f3 commit 072e2a0

File tree

13 files changed

+214
-135
lines changed

13 files changed

+214
-135
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ jobs:
139139
python: "3.14"
140140
os: windows-latest
141141
tox_env: "py314"
142+
use_coverage: true
142143

143144
# Use separate jobs for different unittest flavors (twisted, asynctest) to ensure proper coverage.
144145
- name: "ubuntu-py310-unittest-asynctest"

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
minimum_pre_commit_version: "4.4.0"
22
repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: "v0.14.4"
4+
rev: "v0.14.5"
55
hooks:
66
- id: ruff-check
77
args: ["--fix"]

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ TJ Bruno
459459
Tobias Diez
460460
Tobias Petersen
461461
Tom Dalton
462+
Tom Most
462463
Tom Viner
463464
Tomáš Gavenčiak
464465
Tomer Keren

changelog/13896.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The terminal progress feature added in pytest 9.0.0 has been disabled by default, except on Windows, due to compatibility issues with some terminal emulators.
2+
3+
You may enable it again by passing ``-p terminalprogress``. We may enable it by default again once compatibility improves in the future.
4+
5+
Additionally, when the environment variable ``TERM`` is ``dumb``, the escape codes are no longer emitted, even if the plugin is enabled.

doc/en/changelog.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ New features
178178

179179

180180
- `#13072 <https://github.com/pytest-dev/pytest/issues/13072>`_: Added support for displaying test session **progress in the terminal tab** using the `OSC 9;4; <https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC>`_ ANSI sequence.
181+
182+
**Note**: *This feature has been disabled by default in version 9.0.2, except on Windows, due to compatibility issues with some terminal emulators.
183+
You may enable it again by passing* ``-p terminalprogress``. *We may enable it by default again once compatibility improves in the future.*
184+
181185
When pytest runs in a supported terminal emulator like ConEmu, Gnome Terminal, Ptyxis, Windows Terminal, Kitty or Ghostty,
182186
you'll see the progress in the terminal tab or window,
183187
allowing you to monitor pytest's progress at a glance.
@@ -326,7 +330,7 @@ Bug fixes
326330
- `#13865 <https://github.com/pytest-dev/pytest/issues/13865>`_: Fixed `--show-capture` with `--tb=line`.
327331

328332

329-
- `#13522 <https://github.com/pytest-dev/pytest/issues/13522>`_: Fixed :fixture:`pytester` in subprocess mode ignored all :attr`pytester.plugins <pytest.Pytester.plugins>` except the first.
333+
- `#13522 <https://github.com/pytest-dev/pytest/issues/13522>`_: Fixed :fixture:`pytester` in subprocess mode ignored all :attr:`pytester.plugins <pytest.Pytester.plugins>` except the first.
330334

331335
Fixed :fixture:`pytester` in subprocess mode silently ignored non-str :attr:`pytester.plugins <pytest.Pytester.plugins>`.
332336
Now it errors instead.

src/_pytest/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ def directory_arg(path: str, optname: str) -> str:
303303
*default_plugins,
304304
"pytester",
305305
"pytester_assertions",
306+
"terminalprogress",
306307
}
307308

308309

src/_pytest/fixtures.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import Final
2626
from typing import final
2727
from typing import Generic
28+
from typing import Literal
2829
from typing import NoReturn
2930
from typing import overload
3031
from typing import TYPE_CHECKING
@@ -1472,6 +1473,45 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
14721473
return None
14731474

14741475

1476+
def _resolve_args_directness(
1477+
argnames: Sequence[str],
1478+
indirect: bool | Sequence[str],
1479+
nodeid: str,
1480+
) -> dict[str, Literal["indirect", "direct"]]:
1481+
"""Resolve if each parametrized argument must be considered an indirect
1482+
parameter to a fixture of the same name, or a direct parameter to the
1483+
parametrized function, based on the ``indirect`` parameter of the
1484+
parametrize() call.
1485+
1486+
:param argnames:
1487+
List of argument names passed to ``parametrize()``.
1488+
:param indirect:
1489+
Same as the ``indirect`` parameter of ``parametrize()``.
1490+
:param nodeid:
1491+
Node ID to which the parametrization is applied.
1492+
:returns:
1493+
A dict mapping each arg name to either "indirect" or "direct".
1494+
"""
1495+
arg_directness: dict[str, Literal["indirect", "direct"]]
1496+
if isinstance(indirect, bool):
1497+
arg_directness = dict.fromkeys(argnames, "indirect" if indirect else "direct")
1498+
elif isinstance(indirect, Sequence):
1499+
arg_directness = dict.fromkeys(argnames, "direct")
1500+
for arg in indirect:
1501+
if arg not in argnames:
1502+
fail(
1503+
f"In {nodeid}: indirect fixture '{arg}' doesn't exist",
1504+
pytrace=False,
1505+
)
1506+
arg_directness[arg] = "indirect"
1507+
else:
1508+
fail(
1509+
f"In {nodeid}: expected Sequence or boolean for indirect, got {type(indirect).__name__}",
1510+
pytrace=False,
1511+
)
1512+
return arg_directness
1513+
1514+
14751515
def _get_direct_parametrize_args(node: nodes.Node) -> set[str]:
14761516
"""Return all direct parametrization arguments of a node, so we don't
14771517
mistake them for fixtures.
@@ -1483,11 +1523,16 @@ def _get_direct_parametrize_args(node: nodes.Node) -> set[str]:
14831523
"""
14841524
parametrize_argnames: set[str] = set()
14851525
for marker in node.iter_markers(name="parametrize"):
1486-
if not marker.kwargs.get("indirect", False):
1487-
p_argnames, _ = ParameterSet._parse_parametrize_args(
1488-
*marker.args, **marker.kwargs
1489-
)
1490-
parametrize_argnames.update(p_argnames)
1526+
indirect = marker.kwargs.get("indirect", False)
1527+
p_argnames, _ = ParameterSet._parse_parametrize_args(
1528+
*marker.args, **marker.kwargs
1529+
)
1530+
p_directness = _resolve_args_directness(p_argnames, indirect, node.nodeid)
1531+
parametrize_argnames.update(
1532+
argname
1533+
for argname, directness in p_directness.items()
1534+
if directness == "direct"
1535+
)
14911536
return parametrize_argnames
14921537

14931538

src/_pytest/python.py

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from _pytest.config import hookimpl
5454
from _pytest.config.argparsing import Parser
5555
from _pytest.deprecated import check_ispytest
56+
from _pytest.fixtures import _resolve_args_directness
5657
from _pytest.fixtures import FixtureDef
5758
from _pytest.fixtures import FixtureRequest
5859
from _pytest.fixtures import FuncFixtureInfo
@@ -870,7 +871,6 @@ class IdMaker:
870871
__slots__ = (
871872
"argnames",
872873
"config",
873-
"func_name",
874874
"idfn",
875875
"ids",
876876
"nodeid",
@@ -893,9 +893,6 @@ class IdMaker:
893893
# Optionally, the ID of the node being parametrized.
894894
# Used only for clearer error messages.
895895
nodeid: str | None
896-
# Optionally, the ID of the function being parametrized.
897-
# Used only for clearer error messages.
898-
func_name: str | None
899896

900897
def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]:
901898
"""Make a unique identifier for each ParameterSet, that may be used to
@@ -1083,9 +1080,7 @@ def _complain_multiple_hidden_parameter_sets(self) -> NoReturn:
10831080
)
10841081

10851082
def _make_error_prefix(self) -> str:
1086-
if self.func_name is not None:
1087-
return f"In {self.func_name}: "
1088-
elif self.nodeid is not None:
1083+
if self.nodeid is not None:
10891084
return f"In {self.nodeid}: "
10901085
else:
10911086
return ""
@@ -1333,7 +1328,9 @@ def parametrize(
13331328
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
13341329

13351330
# Calculate directness.
1336-
arg_directness = self._resolve_args_directness(argnames, indirect)
1331+
arg_directness = _resolve_args_directness(
1332+
argnames, indirect, self.definition.nodeid
1333+
)
13371334
self._params_directness.update(arg_directness)
13381335

13391336
# Add direct parametrizations as fixturedefs to arg2fixturedefs by
@@ -1435,23 +1432,21 @@ def _resolve_parameter_set_ids(
14351432
ids_ = None
14361433
else:
14371434
idfn = None
1438-
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
1435+
ids_ = self._validate_ids(ids, parametersets)
14391436
id_maker = IdMaker(
14401437
argnames,
14411438
parametersets,
14421439
idfn,
14431440
ids_,
14441441
self.config,
14451442
nodeid=nodeid,
1446-
func_name=self.function.__name__,
14471443
)
14481444
return id_maker.make_unique_parameterset_ids()
14491445

14501446
def _validate_ids(
14511447
self,
14521448
ids: Iterable[object | None],
14531449
parametersets: Sequence[ParameterSet],
1454-
func_name: str,
14551450
) -> list[object | None]:
14561451
try:
14571452
num_ids = len(ids) # type: ignore[arg-type]
@@ -1464,49 +1459,13 @@ def _validate_ids(
14641459

14651460
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
14661461
if num_ids != len(parametersets) and num_ids != 0:
1467-
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
1468-
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
1469-
1470-
return list(itertools.islice(ids, num_ids))
1471-
1472-
def _resolve_args_directness(
1473-
self,
1474-
argnames: Sequence[str],
1475-
indirect: bool | Sequence[str],
1476-
) -> dict[str, Literal["indirect", "direct"]]:
1477-
"""Resolve if each parametrized argument must be considered an indirect
1478-
parameter to a fixture of the same name, or a direct parameter to the
1479-
parametrized function, based on the ``indirect`` parameter of the
1480-
parametrized() call.
1481-
1482-
:param argnames:
1483-
List of argument names passed to ``parametrize()``.
1484-
:param indirect:
1485-
Same as the ``indirect`` parameter of ``parametrize()``.
1486-
:returns
1487-
A dict mapping each arg name to either "indirect" or "direct".
1488-
"""
1489-
arg_directness: dict[str, Literal["indirect", "direct"]]
1490-
if isinstance(indirect, bool):
1491-
arg_directness = dict.fromkeys(
1492-
argnames, "indirect" if indirect else "direct"
1493-
)
1494-
elif isinstance(indirect, Sequence):
1495-
arg_directness = dict.fromkeys(argnames, "direct")
1496-
for arg in indirect:
1497-
if arg not in argnames:
1498-
fail(
1499-
f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist",
1500-
pytrace=False,
1501-
)
1502-
arg_directness[arg] = "indirect"
1503-
else:
1462+
nodeid = self.definition.nodeid
15041463
fail(
1505-
f"In {self.function.__name__}: expected Sequence or boolean"
1506-
f" for indirect, got {type(indirect).__name__}",
1464+
f"In {nodeid}: {len(parametersets)} parameter sets specified, with different number of ids: {num_ids}",
15071465
pytrace=False,
15081466
)
1509-
return arg_directness
1467+
1468+
return list(itertools.islice(ids, num_ids))
15101469

15111470
def _validate_if_using_arg_names(
15121471
self,
@@ -1520,12 +1479,12 @@ def _validate_if_using_arg_names(
15201479
:raises ValueError: If validation fails.
15211480
"""
15221481
default_arg_names = set(get_default_arg_names(self.function))
1523-
func_name = self.function.__name__
1482+
nodeid = self.definition.nodeid
15241483
for arg in argnames:
15251484
if arg not in self.fixturenames:
15261485
if arg in default_arg_names:
15271486
fail(
1528-
f"In {func_name}: function already takes an argument '{arg}' with a default value",
1487+
f"In {nodeid}: function already takes an argument '{arg}' with a default value",
15291488
pytrace=False,
15301489
)
15311490
else:
@@ -1534,7 +1493,7 @@ def _validate_if_using_arg_names(
15341493
else:
15351494
name = "fixture" if indirect else "argument"
15361495
fail(
1537-
f"In {func_name}: function uses no {name} '{arg}'",
1496+
f"In {nodeid}: function uses no {name} '{arg}'",
15381497
pytrace=False,
15391498
)
15401499

src/_pytest/terminal.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import datetime
1717
from functools import partial
1818
import inspect
19-
import os
2019
from pathlib import Path
2120
import platform
2221
import sys
@@ -299,16 +298,10 @@ def mywriter(tags, args):
299298

300299
config.trace.root.setprocessor("pytest:config", mywriter)
301300

302-
if reporter.isatty():
303-
# Some terminals interpret OSC 9;4 as desktop notification,
304-
# skip on those we know (#13896).
305-
should_skip_terminal_progress = (
306-
# iTerm2 (reported on version 3.6.5).
307-
"ITERM_SESSION_ID" in os.environ
308-
)
309-
if not should_skip_terminal_progress:
310-
plugin = TerminalProgressPlugin(reporter)
311-
config.pluginmanager.register(plugin, "terminalprogress")
301+
# See terminalprogress.py.
302+
# On Windows it's safe to load by default.
303+
if sys.platform == "win32":
304+
config.pluginmanager.import_plugin("terminalprogress")
312305

313306

314307
def getreportopt(config: Config) -> str:

src/_pytest/terminalprogress.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# A plugin to register the TerminalProgressPlugin plugin.
2+
#
3+
# This plugin is not loaded by default due to compatibility issues (#13896),
4+
# but can be enabled in one of these ways:
5+
# - The terminal plugin enables it in a few cases where it's safe, and not
6+
# blocked by the user (using e.g. `-p no:terminalprogress`).
7+
# - The user explicitly requests it, e.g. using `-p terminalprogress`.
8+
#
9+
# In a few years, if it's safe, we can consider enabling it by default. Then,
10+
# this file will become unnecessary and can be inlined into terminal.py.
11+
12+
from __future__ import annotations
13+
14+
import os
15+
16+
from _pytest.config import Config
17+
from _pytest.config import hookimpl
18+
from _pytest.terminal import TerminalProgressPlugin
19+
from _pytest.terminal import TerminalReporter
20+
21+
22+
@hookimpl(trylast=True)
23+
def pytest_configure(config: Config) -> None:
24+
reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
25+
"terminalreporter"
26+
)
27+
28+
if reporter is not None and reporter.isatty() and os.environ.get("TERM") != "dumb":
29+
plugin = TerminalProgressPlugin(reporter)
30+
config.pluginmanager.register(plugin, name="terminalprogress-plugin")

0 commit comments

Comments
 (0)