From 574caed8e7b44d0c835000b62423d0cf5241b33d Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Fri, 11 Sep 2026 15:42:44 +0200 Subject: [PATCH 1/3] Add only_rerun pytest.ini support --- changes/165.feature.rst | 1 + docs/configuration.rst | 16 ++++++++++++++++ src/pytest_rerunfailures.py | 7 +++++++ tests/test_pytest_rerunfailures.py | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 changes/165.feature.rst diff --git a/changes/165.feature.rst b/changes/165.feature.rst new file mode 100644 index 00000000..6ea5a478 --- /dev/null +++ b/changes/165.feature.rst @@ -0,0 +1 @@ +Allow configuring ``only_rerun`` regular expressions in ``pytest.ini`` files. diff --git a/docs/configuration.rst b/docs/configuration.rst index de56fbe7..9153a014 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -35,6 +35,21 @@ Below are the ``pytest.ini`` options supported by the plugin: [pytest] reruns_delay = 2.5 +``only_rerun`` +^^^^^^^^^^^^^^ + +- **Description**: Sets regular expressions for errors that should be rerun. Add one expression per line. +- **Type**: List of strings +- **Default**: Not set (all errors are eligible for reruns). +- **Example**: + + .. code-block:: ini + + [pytest] + only_rerun = + AssertionError + ValueError + Example ------- @@ -45,6 +60,7 @@ To configure your test environment for consistent retries and delays, add the fo [pytest] reruns = 3 reruns_delay = 2.0 + only_rerun = AssertionError This setup ensures that: diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index 8531d854..ede29700 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -189,6 +189,11 @@ def pytest_addoption(parser): RERUNS_DELAY_BACKOFF_FACTOR_DESC, type=arg_type, ) + parser.addini( + "only_rerun", + "only rerun errors matching the regex provided.", + type="linelist", + ) def _get_global_reruns(config): @@ -591,6 +596,8 @@ def _get_rerun_filter_regex(item, regex_name): regex = [regex] else: regex = getattr(item.session.config.option, regex_name) + if regex is None and regex_name == "only_rerun": + regex = item.session.config.getini("only_rerun") return regex diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index db421aef..054fbf4b 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -1107,6 +1107,28 @@ def test_only_rerun2(): ) +def test_only_rerun_ini(testdir): + testdir.makepyfile( + """ + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + only_rerun = AssertionError + """ + ) + + result = testdir.runpytest() + assert_outcomes(result, passed=0, failed=2, rerun=1) + + @pytest.mark.parametrize( "only_rerun,should_rerun", [ From 00fac85122348318089001dd2da58bcdfdf3b283 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Fri, 11 Sep 2026 21:42:31 +0200 Subject: [PATCH 2/3] test: cover multiple only_rerun entries for pytest.ini --- tests/test_pytest_rerunfailures.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index 054fbf4b..aeef120b 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -1129,6 +1129,33 @@ def test_value_error(): assert_outcomes(result, passed=0, failed=2, rerun=1) +def test_only_rerun_ini_multiple(testdir): + testdir.makepyfile( + """ + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + + def test_key_error(): + raise KeyError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + only_rerun = + AssertionError + ValueError + """ + ) + + result = testdir.runpytest() + assert_outcomes(result, passed=0, failed=3, rerun=2) + + @pytest.mark.parametrize( "only_rerun,should_rerun", [ From 9e9a5429bcc1b101574ccd758d7e58d2e49d4082 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Mon, 14 Sep 2026 23:15:05 +0200 Subject: [PATCH 3/3] Address review: rerun_except ini, regex validation, tests and docs --- README.rst | 20 +++++ docs/configuration.rst | 18 ++++- docs/mark.rst | 6 +- src/pytest_rerunfailures.py | 38 ++++++--- tests/test_pytest_rerunfailures.py | 121 +++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 47fd51a7..f2f18da3 100644 --- a/README.rst +++ b/README.rst @@ -115,6 +115,19 @@ The same matching is applied to each exception in the ``__cause__`` / ``raise RuntimeError(...) from MemoryError(...)`` is still rerun by ``--only-rerun MemoryError``. +The default list can also live in the ``pytest.ini`` (or ``pyproject.toml``) +file, one expression per line: + +.. code-block:: ini + + [pytest] + only_rerun = + AssertionError + ValueError + +A ``--only-rerun`` flag on the command line replaces the ini list rather than +accumulating with it. + Re-run all failures other than matching certain expressions ----------------------------------------------------------- @@ -139,6 +152,9 @@ is excluded by ``--rerun-except ValueError``. Implicit ``__context__`` from ``except`` / ``finally`` is not walked, so a ``ConnectionError`` raised inside ``except AssertionError`` is still rerun by ``--rerun-except AssertionError``. +The exclusion list can also live in the ``pytest.ini`` (or +``pyproject.toml``) file as ``rerun_except``, one expression per line. + Exclude test paths from re-runs -------------------------------- @@ -345,6 +361,10 @@ which one takes priority? * Second priority is what's specified on the command line, like ``--reruns=2`` * Last priority is the ``pyproject.toml`` (or ``pytest.ini``) file setting, like ``reruns = 3`` +The same order applies to the rerun filters: a marker's ``only_rerun`` / +``rerun_except`` beats ``--only-rerun`` / ``--rerun-except`` on the command +line, which in turn beats the ``only_rerun`` / ``rerun_except`` ini settings. + Additionally, all three can be overridden by passing ``--force-reruns`` argument on the command line. Passing ``--reruns-mode=append`` makes the marker count and the global ``--reruns`` / ``reruns`` ini setting additive instead of strict. diff --git a/docs/configuration.rst b/docs/configuration.rst index 9153a014..8dabb2f5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -38,7 +38,7 @@ Below are the ``pytest.ini`` options supported by the plugin: ``only_rerun`` ^^^^^^^^^^^^^^ -- **Description**: Sets regular expressions for errors that should be rerun. Add one expression per line. +- **Description**: Sets regular expressions for errors that should be rerun. Add one expression per line. The ``--only-rerun`` command-line flag replaces this list rather than adding to it, and a project-wide ``only_rerun`` also applies to tests carrying a plain ``@pytest.mark.flaky(reruns=...)`` marker — a marked test raising a non-matching error stops rerunning. - **Type**: List of strings - **Default**: Not set (all errors are eligible for reruns). - **Example**: @@ -50,6 +50,21 @@ Below are the ``pytest.ini`` options supported by the plugin: AssertionError ValueError +``rerun_except`` +^^^^^^^^^^^^^^^^ + +- **Description**: Sets regular expressions for errors that should not be rerun. Add one expression per line. The ``--rerun-except`` command-line flag replaces this list rather than adding to it. +- **Type**: List of strings +- **Default**: Not set (all errors are eligible for reruns). +- **Example**: + + .. code-block:: ini + + [pytest] + rerun_except = + AssertionError + ValueError + Example ------- @@ -66,6 +81,7 @@ This setup ensures that: - Failed tests will be retried up to 3 times. - There will be a 2-second delay between each retry. +- Only failures matching ``AssertionError`` are retried; other errors fail without rerunning. Overriding ``pytest.ini`` Options --------------------------------- diff --git a/docs/mark.rst b/docs/mark.rst index 2447b15f..bd80eb94 100644 --- a/docs/mark.rst +++ b/docs/mark.rst @@ -97,7 +97,8 @@ warning and does not re-run for that failure. ^^^^^^^^^^^^^^ Re-run the test only for specific exception types or patterns. -That overrides the :option:`--only-rerun` command-line option. +That overrides the :option:`--only-rerun` command-line option and the +``only_rerun`` ini setting. .. code-block:: python @@ -109,7 +110,8 @@ That overrides the :option:`--only-rerun` command-line option. ^^^^^^^^^^^^^^^^ Exclude specific exception types or patterns from triggering a re-run. -That overrides the :option:`--rerun-except` command-line option. +That overrides the :option:`--rerun-except` command-line option and the +``rerun_except`` ini setting. .. code-block:: python diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index ede29700..e7dff9ba 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -78,6 +78,16 @@ def works_with_current_xdist(): "exponential backoff (delay * factor ** (attempt - 1)). defaults to 1.0, " "i.e. a constant delay." ) +ONLY_RERUN_DESC = ( + "If passed, only rerun errors matching the regex provided. " + "Pass this flag multiple times (or list one regex per line in the ini " + "file) to accumulate a list of regexes to match" +) +RERUN_EXCEPT_DESC = ( + "If passed, only rerun errors other than matching the regex provided. " + "Pass this flag multiple times (or list one regex per line in the ini " + "file) to accumulate a list of regexes to match" +) # command line options @@ -99,9 +109,7 @@ def pytest_addoption(parser): dest="only_rerun", type=str, default=None, - help="If passed, only rerun errors matching the regex provided. " - "Pass this flag multiple times to accumulate a list of regexes " - "to match", + help=ONLY_RERUN_DESC, ) group._addoption( "--reruns", @@ -130,9 +138,7 @@ def pytest_addoption(parser): dest="rerun_except", type=str, default=None, - help="If passed, only rerun errors other than matching the " - "regex provided. Pass this flag multiple times to accumulate a list " - "of regexes to match", + help=RERUN_EXCEPT_DESC, ) group._addoption( "--rerun-exclude-path", @@ -191,7 +197,12 @@ def pytest_addoption(parser): ) parser.addini( "only_rerun", - "only rerun errors matching the regex provided.", + ONLY_RERUN_DESC, + type="linelist", + ) + parser.addini( + "rerun_except", + RERUN_EXCEPT_DESC, type="linelist", ) @@ -219,6 +230,15 @@ def check_options(config): if config.option.usepdb: # a core option raise pytest.UsageError("--reruns incompatible with --pdb") + for name in ("only_rerun", "rerun_except"): + for pattern in getattr(config.option, name) or config.getini(name): + try: + re.compile(pattern) + except re.error as error: + raise pytest.UsageError( + f"invalid regular expression for {name}: {pattern!r} ({error})" + ) from error + def _get_marker(item): return item.get_closest_marker("flaky") @@ -596,8 +616,8 @@ def _get_rerun_filter_regex(item, regex_name): regex = [regex] else: regex = getattr(item.session.config.option, regex_name) - if regex is None and regex_name == "only_rerun": - regex = item.session.config.getini("only_rerun") + if regex is None: + regex = item.session.config.getini(regex_name) return regex diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index aeef120b..bc960be2 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -1156,6 +1156,127 @@ def test_key_error(): assert_outcomes(result, passed=0, failed=3, rerun=2) +def test_only_rerun_ini_override(testdir): + testdir.makepyfile( + """ + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + only_rerun = AssertionError + """ + ) + + result = testdir.runpytest("--only-rerun", "ValueError") + assert_outcomes(result, passed=0, failed=2, rerun=1) + # test_assertion_error fails outright; only test_value_error is rerun, + # so the progress line must read F-R-F in collection order. + result.stdout.fnmatch_lines(["test_only_rerun_ini_override.py FRF*"]) + + +def test_only_rerun_ini_marker_overrides(testdir): + testdir.makepyfile( + """ + import pytest + + @pytest.mark.flaky(reruns=1, only_rerun="AssertionError") + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + only_rerun = ValueError + """ + ) + + result = testdir.runpytest() + assert_outcomes(result, passed=0, failed=2, rerun=2) + + +def test_only_rerun_ini_with_rerun_except_flag(testdir): + testdir.makepyfile( + """ + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + + def test_os_error(): + raise OSError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + only_rerun = + AssertionError + ValueError + """ + ) + + result = testdir.runpytest("--rerun-except", "ValueError") + assert_outcomes(result, passed=0, failed=3, rerun=1) + + +def test_rerun_except_ini(testdir): + testdir.makepyfile( + """ + def test_assertion_error(): + raise AssertionError("ERR") + + def test_value_error(): + raise ValueError("ERR") + """ + ) + testdir.makeini( + """ + [pytest] + reruns = 1 + rerun_except = ValueError + """ + ) + + result = testdir.runpytest() + assert_outcomes(result, passed=0, failed=2, rerun=1) + + +@pytest.mark.parametrize("option_name", ["only_rerun", "rerun_except"]) +def test_rerun_filter_ini_invalid_regex(testdir, option_name): + testdir.makepyfile( + """ + def test_foo(): + raise AssertionError("ERR") + """ + ) + testdir.makeini( + f""" + [pytest] + reruns = 1 + {option_name} = [unclosed + """ + ) + + result = testdir.runpytest() + result.stderr.fnmatch_lines_random( + f"*invalid regular expression for {option_name}*" + ) + + @pytest.mark.parametrize( "only_rerun,should_rerun", [