From 88b144fa5083af3169babab85da4d21a4211ee9c Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 11:14:19 -0500 Subject: [PATCH 01/14] Fix AttributeError in Argument.__repr__ when dest is not set Fixes #13817 --- src/_pytest/config/argparsing.py | 5 ++- testing/test_argparsing_repr_fix.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 testing/test_argparsing_repr_fix.py diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 8d4ed823325..0f4fbd751fd 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -359,7 +359,10 @@ def __repr__(self) -> str: args += ["_short_opts: " + repr(self._short_opts)] if self._long_opts: args += ["_long_opts: " + repr(self._long_opts)] - args += ["dest: " + repr(self.dest)] + if hasattr(self, "dest"): + args += ["dest: " + repr(self.dest)] + else: + args += ["dest: "] if hasattr(self, "type"): args += ["type: " + repr(self.type)] if hasattr(self, "default"): diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py new file mode 100644 index 00000000000..9245c08ab85 --- /dev/null +++ b/testing/test_argparsing_repr_fix.py @@ -0,0 +1,47 @@ +""" +Test case for issue #13817: AttributeError with invalid flag in pytest_addoption +""" +import pytest +from _pytest.config.argparsing import ArgumentError, Parser + + +# Suppress warning about using private pytest API (we're testing pytest itself) +@pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning") +class TestArgumentReprFix: + """Test that Argument.__repr__ handles missing dest attribute.""" + + def test_invalid_option_without_dashes(self): + """Test that invalid option names produce helpful error messages.""" + parser = Parser() + + with pytest.raises(ArgumentError) as exc_info: + parser.addoption("shuffle") # Missing required -- prefix + + error_message = str(exc_info.value) + assert "invalid long option string" in error_message + assert "shuffle" in error_message + assert "must start with --" in error_message + + # Ensure no AttributeError is mentioned + assert "AttributeError" not in error_message + assert "has no attribute 'dest'" not in error_message + + def test_invalid_short_option(self): + """Test that invalid short option names produce helpful error messages.""" + parser = Parser() + + with pytest.raises(ArgumentError) as exc_info: + parser.addoption("-ab") # 3 chars, treated as invalid long option + + error_message = str(exc_info.value) + # -ab is treated as invalid long option (3+ chars) + assert "invalid long option string" in error_message or "invalid short option string" in error_message + + def test_valid_option_works(self): + """Test that valid options still work correctly.""" + parser = Parser() + parser.addoption("--shuffle", action="store_true", help="Shuffle tests") + + options = parser._anonymous.options + assert len(options) > 0 + assert "--shuffle" in options[0].names() \ No newline at end of file From 26f72b0995c55e1defd7233cd76d7d31a5f68f1a Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 11:23:17 -0500 Subject: [PATCH 02/14] Add changelog and update AUTHORS --- AUTHORS | 1 + changelog/13817.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/13817.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 9539e8dc4f4..200418dc6a8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -440,6 +440,7 @@ Sylvain Marié Tadek Teleżyński Takafumi Arakaki Takumi Otani +Tammy Hartline Taneli Hukkinen Tanvi Mehta Tanya Agarwal diff --git a/changelog/13817.bugfix.rst b/changelog/13817.bugfix.rst new file mode 100644 index 00000000000..1b533777228 --- /dev/null +++ b/changelog/13817.bugfix.rst @@ -0,0 +1 @@ +Fixed ``AttributeError`` in ``Argument.__repr__`` when ``dest`` attribute is not set, which occurred when displaying error messages for invalid option names. The error now shows a helpful message instead of masking the original validation error. From d3bd04aa1f4adcf4c013882fcf9b51fbf558b977 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 16:26:45 +0000 Subject: [PATCH 03/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_argparsing_repr_fix.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 9245c08ab85..a4982d5b568 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,8 +1,12 @@ """ Test case for issue #13817: AttributeError with invalid flag in pytest_addoption """ + +from __future__ import annotations + +from _pytest.config.argparsing import ArgumentError +from _pytest.config.argparsing import Parser import pytest -from _pytest.config.argparsing import ArgumentError, Parser # Suppress warning about using private pytest API (we're testing pytest itself) @@ -13,15 +17,15 @@ class TestArgumentReprFix: def test_invalid_option_without_dashes(self): """Test that invalid option names produce helpful error messages.""" parser = Parser() - + with pytest.raises(ArgumentError) as exc_info: parser.addoption("shuffle") # Missing required -- prefix - + error_message = str(exc_info.value) assert "invalid long option string" in error_message assert "shuffle" in error_message assert "must start with --" in error_message - + # Ensure no AttributeError is mentioned assert "AttributeError" not in error_message assert "has no attribute 'dest'" not in error_message @@ -29,19 +33,22 @@ def test_invalid_option_without_dashes(self): def test_invalid_short_option(self): """Test that invalid short option names produce helpful error messages.""" parser = Parser() - + with pytest.raises(ArgumentError) as exc_info: parser.addoption("-ab") # 3 chars, treated as invalid long option - + error_message = str(exc_info.value) # -ab is treated as invalid long option (3+ chars) - assert "invalid long option string" in error_message or "invalid short option string" in error_message + assert ( + "invalid long option string" in error_message + or "invalid short option string" in error_message + ) def test_valid_option_works(self): """Test that valid options still work correctly.""" parser = Parser() parser.addoption("--shuffle", action="store_true", help="Shuffle tests") - + options = parser._anonymous.options assert len(options) > 0 - assert "--shuffle" in options[0].names() \ No newline at end of file + assert "--shuffle" in options[0].names() From 163866ceb5ff0050efcb5266bb7527c23bfe8bca Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 12:06:54 -0500 Subject: [PATCH 04/14] Add return type annotations for mypy --- testing/test_argparsing_repr_fix.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index a4982d5b568..7ea2487327e 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,12 +1,12 @@ """ Test case for issue #13817: AttributeError with invalid flag in pytest_addoption """ - from __future__ import annotations +import pytest + from _pytest.config.argparsing import ArgumentError from _pytest.config.argparsing import Parser -import pytest # Suppress warning about using private pytest API (we're testing pytest itself) @@ -14,7 +14,7 @@ class TestArgumentReprFix: """Test that Argument.__repr__ handles missing dest attribute.""" - def test_invalid_option_without_dashes(self): + def test_invalid_option_without_dashes(self) -> None: """Test that invalid option names produce helpful error messages.""" parser = Parser() @@ -30,7 +30,7 @@ def test_invalid_option_without_dashes(self): assert "AttributeError" not in error_message assert "has no attribute 'dest'" not in error_message - def test_invalid_short_option(self): + def test_invalid_short_option(self) -> None: """Test that invalid short option names produce helpful error messages.""" parser = Parser() @@ -44,11 +44,11 @@ def test_invalid_short_option(self): or "invalid short option string" in error_message ) - def test_valid_option_works(self): + def test_valid_option_works(self) -> None: """Test that valid options still work correctly.""" parser = Parser() parser.addoption("--shuffle", action="store_true", help="Shuffle tests") options = parser._anonymous.options assert len(options) > 0 - assert "--shuffle" in options[0].names() + assert "--shuffle" in options[0].names() \ No newline at end of file From 87f59428b4264f33753f87f5fa609b5d67730902 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:07:11 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_argparsing_repr_fix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 7ea2487327e..675612d2e2d 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,12 +1,12 @@ """ Test case for issue #13817: AttributeError with invalid flag in pytest_addoption """ -from __future__ import annotations -import pytest +from __future__ import annotations from _pytest.config.argparsing import ArgumentError from _pytest.config.argparsing import Parser +import pytest # Suppress warning about using private pytest API (we're testing pytest itself) @@ -51,4 +51,4 @@ def test_valid_option_works(self) -> None: options = parser._anonymous.options assert len(options) > 0 - assert "--shuffle" in options[0].names() \ No newline at end of file + assert "--shuffle" in options[0].names() From 5d09ee724809fac033634e678b94ef48cd80c160 Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 12:29:29 -0500 Subject: [PATCH 06/14] Trigger CI checks From a4f4696bb0a192d67273b233f16ed730e19bb845 Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 12:31:45 -0500 Subject: [PATCH 07/14] Fix docstring to one line for ruff --- testing/test_argparsing_repr_fix.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 675612d2e2d..1ce15bdd0fa 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,6 +1,4 @@ -""" -Test case for issue #13817: AttributeError with invalid flag in pytest_addoption -""" +"""Test case for issue #13817: AttributeError with invalid flag in pytest_addoption.""" from __future__ import annotations From 52d83ac11627671ef98c645943696c4a5973dd89 Mon Sep 17 00:00:00 2001 From: thartline35 Date: Sat, 18 Oct 2025 12:38:21 -0500 Subject: [PATCH 08/14] Add test for repr with dest set to improve coverage --- testing/test_argparsing_repr_fix.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 1ce15bdd0fa..2c5babbbea8 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -50,3 +50,16 @@ def test_valid_option_works(self) -> None: options = parser._anonymous.options assert len(options) > 0 assert "--shuffle" in options[0].names() + + def test_repr_with_dest_set(self) -> None: + """Test that __repr__ works correctly when dest is set.""" + parser = Parser() + parser.addoption("--valid-option", dest="valid_dest", help="A valid option") + + # Get the argument object and check its repr + option = parser._anonymous.options[0] + repr_str = repr(option) + + # Should contain the dest + assert "dest: 'valid_dest'" in repr_str + assert "" not in repr_str \ No newline at end of file From f0eeb1c6fe2f7db5e7472b676d532cf6a15ce262 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:38:43 +0000 Subject: [PATCH 09/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_argparsing_repr_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 2c5babbbea8..771d836fa58 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -62,4 +62,4 @@ def test_repr_with_dest_set(self) -> None: # Should contain the dest assert "dest: 'valid_dest'" in repr_str - assert "" not in repr_str \ No newline at end of file + assert "" not in repr_str From 9d69b3d2a8d66412996f31b020ada5d9e447c34a Mon Sep 17 00:00:00 2001 From: thartline35 Date: Mon, 20 Oct 2025 17:18:27 -0500 Subject: [PATCH 10/14] Use NOT_SET constant instead of string literal --- src/_pytest/config/argparsing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 0f4fbd751fd..f1d9a4e70c0 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -359,10 +359,7 @@ def __repr__(self) -> str: args += ["_short_opts: " + repr(self._short_opts)] if self._long_opts: args += ["_long_opts: " + repr(self._long_opts)] - if hasattr(self, "dest"): - args += ["dest: " + repr(self.dest)] - else: - args += ["dest: "] + args += ["dest: " + repr(getattr(self, "dest", NOT_SET))] if hasattr(self, "type"): args += ["type: " + repr(self.type)] if hasattr(self, "default"): From 8c437728c2b06ee257a11c52544bf9aa09a71bcb Mon Sep 17 00:00:00 2001 From: thartline35 Date: Mon, 20 Oct 2025 19:19:07 -0500 Subject: [PATCH 11/14] Update test to check for NOT_SET constant --- testing/test_argparsing_repr_fix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 771d836fa58..da04d30806c 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -3,6 +3,7 @@ from __future__ import annotations from _pytest.config.argparsing import ArgumentError +from _pytest.config.argparsing import NOT_SET from _pytest.config.argparsing import Parser import pytest @@ -62,4 +63,4 @@ def test_repr_with_dest_set(self) -> None: # Should contain the dest assert "dest: 'valid_dest'" in repr_str - assert "" not in repr_str + assert "NOT_SET" not in repr_str \ No newline at end of file From cb82c4058d05543be53a6ef0601c2bf291aa5aa7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:20:42 +0000 Subject: [PATCH 12/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_argparsing_repr_fix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index da04d30806c..1395af89709 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -3,7 +3,6 @@ from __future__ import annotations from _pytest.config.argparsing import ArgumentError -from _pytest.config.argparsing import NOT_SET from _pytest.config.argparsing import Parser import pytest @@ -63,4 +62,4 @@ def test_repr_with_dest_set(self) -> None: # Should contain the dest assert "dest: 'valid_dest'" in repr_str - assert "NOT_SET" not in repr_str \ No newline at end of file + assert "NOT_SET" not in repr_str From 5b99c3ca55bb96926b9b7180b8556f904207181b Mon Sep 17 00:00:00 2001 From: thartline35 Date: Mon, 20 Oct 2025 19:30:46 -0500 Subject: [PATCH 13/14] Resolve merge conflict and add repr test --- testing/test_argparsing_repr_fix.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 1395af89709..61ed2e501ed 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,8 +1,8 @@ """Test case for issue #13817: AttributeError with invalid flag in pytest_addoption.""" - from __future__ import annotations from _pytest.config.argparsing import ArgumentError +from _pytest.config.argparsing import NOT_SET from _pytest.config.argparsing import Parser import pytest @@ -63,3 +63,17 @@ def test_repr_with_dest_set(self) -> None: # Should contain the dest assert "dest: 'valid_dest'" in repr_str assert "NOT_SET" not in repr_str + + def test_repr_without_dest(self) -> None: + """Test that __repr__ works when dest is not set due to error.""" + from _pytest.config.argparsing import Argument + + # Create an Argument that will fail during initialization + # This triggers the code path where dest is not set + try: + Argument("invalid") # No dashes, will fail + except ArgumentError as exc: + # The repr was called during error creation + # Verify it contains NOT_SET representation + assert "dest:" in str(exc) + assert "NOT_SET" in str(exc) or "" in str(exc) \ No newline at end of file From 1435534179290ff6a158a4b1bfd7e1d997fe4e6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:31:07 +0000 Subject: [PATCH 14/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_argparsing_repr_fix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_argparsing_repr_fix.py b/testing/test_argparsing_repr_fix.py index 61ed2e501ed..4077700bcc3 100644 --- a/testing/test_argparsing_repr_fix.py +++ b/testing/test_argparsing_repr_fix.py @@ -1,8 +1,8 @@ """Test case for issue #13817: AttributeError with invalid flag in pytest_addoption.""" + from __future__ import annotations from _pytest.config.argparsing import ArgumentError -from _pytest.config.argparsing import NOT_SET from _pytest.config.argparsing import Parser import pytest @@ -76,4 +76,4 @@ def test_repr_without_dest(self) -> None: # The repr was called during error creation # Verify it contains NOT_SET representation assert "dest:" in str(exc) - assert "NOT_SET" in str(exc) or "" in str(exc) \ No newline at end of file + assert "NOT_SET" in str(exc) or "" in str(exc)