Skip to content

Commit 576f3a4

Browse files
authored
Merge branch 'main' into feat/tests-with-uv
2 parents 94c93c1 + fd7742e commit 576f3a4

18 files changed

+603
-144
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: "v0.14.0"
3+
rev: "v0.14.1"
44
hooks:
55
- id: ruff-check
66
args: ["--fix"]
@@ -12,7 +12,7 @@ repos:
1212
- id: end-of-file-fixer
1313
- id: check-yaml
1414
- repo: https://github.com/woodruffw/zizmor-pre-commit
15-
rev: v1.14.2
15+
rev: v1.15.2
1616
hooks:
1717
- id: zizmor
1818
- repo: https://github.com/adamchainz/blacken-docs
@@ -66,7 +66,7 @@ repos:
6666
# Manual because passing pyright is a work in progress.
6767
stages: [manual]
6868
- repo: https://github.com/tox-dev/pyproject-fmt
69-
rev: "v2.10.0"
69+
rev: "v2.11.0"
7070
hooks:
7171
- id: pyproject-fmt
7272
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ Oliver Bestwalter
346346
Olivier Grisel
347347
Omar Kohl
348348
Omer Hadari
349+
Omri Golan
349350
Ondřej Súkup
350351
Oscar Benjamin
351352
Parth Patel

changelog/13737.feature.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added the :confval:`strict_parametrization_ids` configuration option.
2+
3+
When set, pytest emits an error if it detects non-unique parameter set IDs,
4+
rather than automatically making the IDs unique by adding `0`, `1`, ... to them.

changelog/13829.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added support for ini option aliases via the ``aliases`` parameter in :meth:`Parser.addini() <pytest.Parser.addini>`.
2+
3+
Plugins can now register alternative names for ini options,
4+
allowing for more flexibility in configuration naming and supporting backward compatibility when renaming options.
5+
The canonical name always takes precedence if both the canonical name and an alias are specified in the configuration file.

changelog/13830.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Configuration overrides (``-o``/``--override-ini``) are now processed during startup rather than during :func:`config.getini() <pytest.Config.getini>`.

doc/en/reference/plugin_list.rst

Lines changed: 112 additions & 80 deletions
Large diffs are not rendered by default.

doc/en/reference/reference.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,44 @@ passed multiple times. The expected format is ``name=value``. For example::
20822082
[pytest]
20832083
xfail_strict = True
20842084
2085+
.. confval:: strict_parametrization_ids
2086+
2087+
If set, pytest emits an error if it detects non-unique parameter set IDs.
2088+
2089+
If not set (the default), pytest automatically handles this by adding `0`, `1`, ... to duplicate IDs,
2090+
making them unique.
2091+
2092+
.. code-block:: ini
2093+
2094+
[pytest]
2095+
strict_parametrization_ids = True
2096+
2097+
For example,
2098+
2099+
.. code-block:: python
2100+
2101+
import pytest
2102+
2103+
2104+
@pytest.mark.parametrize("letter", ["a", "a"])
2105+
def test_letter_is_ascii(letter):
2106+
assert letter.isascii()
2107+
2108+
will emit an error because both cases (parameter sets) have the same auto-generated ID "a".
2109+
2110+
To fix the error, if you decide to keep the duplicates, explicitly assign unique IDs:
2111+
2112+
.. code-block:: python
2113+
2114+
import pytest
2115+
2116+
2117+
@pytest.mark.parametrize("letter", ["a", "a"], ids=["a0", "a1"])
2118+
def test_letter_is_ascii(letter):
2119+
assert letter.isascii()
2120+
2121+
See :func:`parametrize <pytest.Metafunc.parametrize>` and :func:`pytest.param` for other ways to set IDs.
2122+
20852123

20862124
.. _`command-line-flags`:
20872125

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ norecursedirs = [
379379
"dist",
380380
]
381381
xfail_strict = true
382+
strict_parametrization_ids = true
382383
filterwarnings = [
383384
"error",
384385
"default:Using or importing the ABCs:DeprecationWarning:unittest2.*",

src/_pytest/config/__init__.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,6 @@ def __init__(
10831083
self.trace = self.pluginmanager.trace.root.get("config")
10841084
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
10851085
self._inicache: dict[str, Any] = {}
1086-
self._override_ini: Sequence[str] = ()
10871086
self._opt2dest: dict[str, str] = {}
10881087
self._cleanup_stack = contextlib.ExitStack()
10891088
self.pluginmanager.register(self, "pytestconfig")
@@ -1251,6 +1250,7 @@ def _initini(self, args: Sequence[str]) -> None:
12511250
)
12521251
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
12531252
inifile=ns.inifilename,
1253+
override_ini=ns.override_ini,
12541254
args=ns.file_or_dir + unknown_args,
12551255
rootdir_cmd_arg=ns.rootdir or None,
12561256
invocation_dir=self.invocation_params.dir,
@@ -1272,7 +1272,6 @@ def _initini(self, args: Sequence[str]) -> None:
12721272
type="args",
12731273
default=[],
12741274
)
1275-
self._override_ini = ns.override_ini or ()
12761275

12771276
def _consider_importhook(self, args: Sequence[str]) -> None:
12781277
"""Install the PEP 302 import hook if using assertion rewriting.
@@ -1462,7 +1461,8 @@ def pytest_collection(self) -> Generator[None, object, object]:
14621461
def _checkversion(self) -> None:
14631462
import pytest
14641463

1465-
minver = self.inicfg.get("minversion", None)
1464+
minver_ini_value = self.inicfg.get("minversion", None)
1465+
minver = minver_ini_value.value if minver_ini_value is not None else None
14661466
if minver:
14671467
# Imported lazily to improve start-up time.
14681468
from packaging.version import Version
@@ -1520,9 +1520,9 @@ def _warn_or_fail_if_strict(self, message: str) -> None:
15201520

15211521
self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
15221522

1523-
def _get_unknown_ini_keys(self) -> list[str]:
1524-
parser_inicfg = self._parser._inidict
1525-
return [name for name in self.inicfg if name not in parser_inicfg]
1523+
def _get_unknown_ini_keys(self) -> set[str]:
1524+
known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1525+
return self.inicfg.keys() - known_keys
15261526

15271527
def parse(self, args: list[str], addopts: bool = True) -> None:
15281528
# Parse given cmdline arguments into this config object.
@@ -1622,10 +1622,11 @@ def getini(self, name: str) -> Any:
16221622
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
16231623
plugin), a ValueError is raised.
16241624
"""
1625+
canonical_name = self._parser._ini_aliases.get(name, name)
16251626
try:
1626-
return self._inicache[name]
1627+
return self._inicache[canonical_name]
16271628
except KeyError:
1628-
self._inicache[name] = val = self._getini(name)
1629+
self._inicache[canonical_name] = val = self._getini(canonical_name)
16291630
return val
16301631

16311632
# Meant for easy monkeypatching by legacypath plugin.
@@ -1637,18 +1638,32 @@ def _getini_unknown_type(self, name: str, type: str, value: object):
16371638
raise ValueError(msg) # pragma: no cover
16381639

16391640
def _getini(self, name: str):
1641+
# If this is an alias, resolve to canonical name.
1642+
canonical_name = self._parser._ini_aliases.get(name, name)
1643+
16401644
try:
1641-
_description, type, default = self._parser._inidict[name]
1645+
_description, type, default = self._parser._inidict[canonical_name]
16421646
except KeyError as e:
16431647
raise ValueError(f"unknown configuration value: {name!r}") from e
1644-
override_value = self._get_override_ini_value(name)
1645-
if override_value is None:
1646-
try:
1647-
value = self.inicfg[name]
1648-
except KeyError:
1649-
return default
1650-
else:
1651-
value = override_value
1648+
1649+
# Collect all possible values (canonical name + aliases) from inicfg.
1650+
# Each candidate is (IniValue, is_canonical).
1651+
candidates = []
1652+
if canonical_name in self.inicfg:
1653+
candidates.append((self.inicfg[canonical_name], True))
1654+
for alias, target in self._parser._ini_aliases.items():
1655+
if target == canonical_name and alias in self.inicfg:
1656+
candidates.append((self.inicfg[alias], False))
1657+
1658+
if not candidates:
1659+
return default
1660+
1661+
# Pick the best candidate based on precedence:
1662+
# 1. CLI override takes precedence over file, then
1663+
# 2. Canonical name takes precedence over alias.
1664+
ini_value = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
1665+
value = ini_value.value
1666+
16521667
# Coerce the values based on types.
16531668
#
16541669
# Note: some coercions are only required if we are reading from .ini files, because
@@ -1719,23 +1734,6 @@ def _getconftest_pathlist(
17191734
values.append(relroot)
17201735
return values
17211736

1722-
def _get_override_ini_value(self, name: str) -> str | None:
1723-
value = None
1724-
# override_ini is a list of "ini=value" options.
1725-
# Always use the last item if multiple values are set for same ini-name,
1726-
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
1727-
for ini_config in self._override_ini:
1728-
try:
1729-
key, user_ini_value = ini_config.split("=", 1)
1730-
except ValueError as e:
1731-
raise UsageError(
1732-
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
1733-
) from e
1734-
else:
1735-
if key == name:
1736-
value = user_ini_value
1737-
return value
1738-
17391737
def getoption(self, name: str, default: Any = notset, skip: bool = False):
17401738
"""Return command line option value.
17411739

src/_pytest/config/argparsing.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def __init__(
5252
self._usage = usage
5353
self._inidict: dict[str, tuple[str, str | None, Any]] = {}
5454
self._ininames: list[str] = []
55+
# Maps alias -> canonical name.
56+
self._ini_aliases: dict[str, str] = {}
5557
self.extra_info: dict[str, Any] = {}
5658

5759
def processoption(self, option: Argument) -> None:
@@ -179,6 +181,8 @@ def addini(
179181
]
180182
| None = None,
181183
default: Any = NOT_SET,
184+
*,
185+
aliases: Sequence[str] = (),
182186
) -> None:
183187
"""Register an ini-file option.
184188
@@ -213,6 +217,12 @@ def addini(
213217
Defaults to ``string`` if ``None`` or not passed.
214218
:param default:
215219
Default value if no ini-file option exists but is queried.
220+
:param aliases:
221+
Additional names by which this option can be referenced.
222+
Aliases resolve to the canonical name.
223+
224+
.. versionadded:: 9.0
225+
The ``aliases`` parameter.
216226
217227
The value of ini-variables can be retrieved via a call to
218228
:py:func:`config.getini(name) <pytest.Config.getini>`.
@@ -234,6 +244,13 @@ def addini(
234244
self._inidict[name] = (help, type, default)
235245
self._ininames.append(name)
236246

247+
for alias in aliases:
248+
if alias in self._inidict:
249+
raise ValueError(f"alias {alias!r} conflicts with existing ini option")
250+
if (already := self._ini_aliases.get(alias)) is not None:
251+
raise ValueError(f"{alias!r} is already an alias of {already!r}")
252+
self._ini_aliases[alias] = name
253+
237254

238255
def get_ini_default_for_type(
239256
type: Literal[

0 commit comments

Comments
 (0)