Skip to content

Commit f547106

Browse files
committed
feat: add cli, ini option skips pytest internal logic of id generation and raise error
1 parent 37acc4f commit f547106

File tree

5 files changed

+145
-0
lines changed

5 files changed

+145
-0
lines changed

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 ``--require-unique-paramset-ids`` flag to pytest.
2+
3+
When passed, this flag causes pytest to raise an exception upon detection of non-unique parameter set IDs,
4+
rather than attempting to generate them automatically.

doc/en/reference/reference.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,30 @@ passed multiple times. The expected format is ``name=value``. For example::
20822082
[pytest]
20832083
xfail_strict = True
20842084
2085+
.. confval:: require_unique_paramset_ids
2086+
2087+
When passed, this flag causes pytest to raise an exception upon detection of non-unique parameter set IDs,
2088+
rather than attempting to generate them automatically.
2089+
2090+
Can be overridden by `--require-unique-paramset-ids`.
2091+
2092+
.. code-block:: ini
2093+
2094+
[pytest]
2095+
require_unique_paramset_ids = True
2096+
2097+
.. code-block:: python
2098+
2099+
import pytest
2100+
2101+
2102+
@pytest.mark.parametrize("x", [1, 2], ids=["a", "a"])
2103+
def test_example(x):
2104+
assert x in (1, 2)
2105+
2106+
will raise an exception due to the duplicate IDs "a".
2107+
when normal pytest behavior would be to handle this by generating unique IDs like "a-0", "a-1".
2108+
20852109

20862110
.. _`command-line-flags`:
20872111

@@ -2239,6 +2263,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
22392263
--doctest-continue-on-failure
22402264
For a given doctest, continue to run after the first
22412265
failure
2266+
--require-unique-paramset-ids
2267+
If pytest collects test ids with non-unique names, raise an
2268+
error rather than handling it.
2269+
Useful if you collect in one process,
2270+
and then execute tests in independent workers.
22422271

22432272
test session debugging and configuration:
22442273
-c, --config-file FILE

src/_pytest/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ def pytest_addoption(parser: Parser) -> None:
9090
action="store_true",
9191
help="(Deprecated) alias to --strict-markers",
9292
)
93+
group.addoption(
94+
"--require-unique-paramset-ids",
95+
action="store_true",
96+
default=False,
97+
help="When passed, this flag causes pytest to raise an exception upon detection of non-unique parameter set IDs"
98+
"rather than attempting to generate them automatically.",
99+
)
100+
101+
parser.addini(
102+
"require_unique_paramset_ids",
103+
type="bool",
104+
default=False,
105+
help="When passed, this flag causes pytest to raise an exception upon detection of non-unique parameter set IDs"
106+
"rather than attempting to generate them automatically.",
107+
)
93108

94109
group = parser.getgroup("pytest-warnings")
95110
group.addoption(

src/_pytest/python.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from _pytest.config import Config
5151
from _pytest.config import hookimpl
5252
from _pytest.config.argparsing import Parser
53+
from _pytest.config.exceptions import UsageError
5354
from _pytest.deprecated import check_ispytest
5455
from _pytest.fixtures import FixtureDef
5556
from _pytest.fixtures import FixtureRequest
@@ -902,6 +903,23 @@ def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]:
902903
resolved_ids = list(self._resolve_ids())
903904
# All IDs must be unique!
904905
if len(resolved_ids) != len(set(resolved_ids)):
906+
if self._require_unique_ids_enabled():
907+
duplicate_indexs = defaultdict(list)
908+
for i, val in enumerate(resolved_ids):
909+
duplicate_indexs[val].append(i)
910+
911+
# Keep only duplicates
912+
duplicates = {k: v for k, v in duplicate_indexs.items() if len(v) > 1}
913+
raise UsageError(f"""
914+
Because --require-unique-paramset-ids given, pytest won't
915+
attempt to generate unique IDs for parameter sets.
916+
argument values: {self.parametersets}
917+
argument names: {self.argnames}
918+
function name: {self.func_name}
919+
test name: {self.nodeid}
920+
resolved (with non-unique) IDs: {resolved_ids}
921+
duplicates: {duplicates}
922+
""")
905923
# Record the number of occurrences of each ID.
906924
id_counts = Counter(resolved_ids)
907925
# Map the ID to its next suffix.
@@ -925,6 +943,14 @@ def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]:
925943
)
926944
return resolved_ids
927945

946+
def _require_unique_ids_enabled(self) -> bool:
947+
if self.config:
948+
cli_value = self.config.getoption("require_unique_paramset_ids")
949+
if cli_value:
950+
return bool(cli_value)
951+
return bool(self.config.getini("require_unique_paramset_ids"))
952+
return False
953+
928954
def _resolve_ids(self) -> Iterable[str | _HiddenParam]:
929955
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
930956
for idx, parameterset in enumerate(self.parametersets):

testing/test_collection.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from pathlib import PurePath
77
import pprint
8+
import re
89
import shutil
910
import sys
1011
import tempfile
@@ -2702,3 +2703,73 @@ def test_1(): pass
27022703
],
27032704
consecutive=True,
27042705
)
2706+
2707+
2708+
class TestRequireUniqueParamsetIds:
2709+
CASES = [
2710+
("[(1, 1), (1, 1)]", {"1-1": [0, 1]}),
2711+
("[(1, 1), (1, 2), (1, 1)]", {"1-1": [0, 2]}),
2712+
("[(1, 1), (2, 2), (1, 1)]", {"1-1": [0, 2]}),
2713+
("[(1, 1), (2, 2), (1, 2), (2, 1), (1, 1)]", {"1-1": [0, 4]}),
2714+
]
2715+
2716+
@staticmethod
2717+
def _make_testfile(pytester: Pytester, parametrize_args: str) -> None:
2718+
pytester.makepyfile(
2719+
f"""
2720+
import pytest
2721+
2722+
@pytest.mark.parametrize('y, x', {parametrize_args})
2723+
def test1(y, x):
2724+
pass
2725+
"""
2726+
)
2727+
2728+
@staticmethod
2729+
def _fnmatch_escape_repr(obj) -> str:
2730+
return re.sub(r"[*?[\]]", (lambda m: f"[{m.group()}]"), repr(obj))
2731+
2732+
def _assert_duplicate_msg(self, result, expected_indices):
2733+
# Collection errors usually go to stdout; fall back to stderr just in case.
2734+
stream = result.stdout
2735+
stream.fnmatch_lines(
2736+
[
2737+
"E*Because --require-unique-paramset-ids given, pytest won't",
2738+
"E*attempt to generate unique IDs for parameter sets.",
2739+
"E*argument names: [[]'y', 'x'[]]",
2740+
"E*function name: test1",
2741+
"E*test name: *::test1",
2742+
f"E*duplicates: {self._fnmatch_escape_repr(expected_indices)}",
2743+
]
2744+
)
2745+
assert result.ret != 0
2746+
2747+
@pytest.mark.parametrize("parametrize_args, expected_indices", CASES)
2748+
def test_cli_enables(self, pytester: Pytester, parametrize_args, expected_indices):
2749+
self._make_testfile(pytester, parametrize_args)
2750+
result = pytester.runpytest("--require-unique-paramset-ids")
2751+
self._assert_duplicate_msg(result, expected_indices)
2752+
2753+
@pytest.mark.parametrize("parametrize_args, expected_indices", CASES)
2754+
def test_ini_enables(self, pytester: Pytester, parametrize_args, expected_indices):
2755+
pytester.makeini(
2756+
"""
2757+
[pytest]
2758+
require_unique_paramset_ids = true
2759+
"""
2760+
)
2761+
self._make_testfile(pytester, parametrize_args)
2762+
result = pytester.runpytest()
2763+
self._assert_duplicate_msg(result, expected_indices)
2764+
2765+
def test_cli_overrides_ini_false(self, pytester: Pytester):
2766+
"""CLI True should override ini False."""
2767+
pytester.makeini(
2768+
"""
2769+
[pytest]
2770+
require_unique_paramset_ids = false
2771+
"""
2772+
)
2773+
self._make_testfile(pytester, "[(1,1), (1,1)]")
2774+
result = pytester.runpytest("--require-unique-paramset-ids")
2775+
self._assert_duplicate_msg(result, {"1-1": [0, 1]})

0 commit comments

Comments
 (0)