From 7a5abfd239c29e173c9ed4835c44234af0d19957 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 May 2026 15:40:51 -0400 Subject: [PATCH 1/3] Added monkeypatch to handle argparse deepcopy issue introduced in Python 3.15.0b1. --- cmd2/argparse_utils.py | 19 +++++++++++++++++++ cmd2/utils.py | 8 +++----- tests/test_argparse_utils.py | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cmd2/argparse_utils.py b/cmd2/argparse_utils.py index cf06825e0..f05f4922d 100644 --- a/cmd2/argparse_utils.py +++ b/cmd2/argparse_utils.py @@ -369,6 +369,25 @@ def _validate_completion_callable(self: argparse.Action, value: Any) -> Any: register_argparse_argument_parameter("nargs_range") register_argparse_argument_parameter("suppress_tab_hint") +############################################################################################################ +# Workaround for Python 3.15.0b1 argparse bug +# _ColorlessTheme.__getattr__ incorrectly returns "" for dunder methods, which breaks +# protocols like copy.deepcopy(). +############################################################################################################ + +# _ColorlessTheme only exists in 3.15+ +if hasattr(argparse, "_ColorlessTheme"): + + def _ColorlessTheme_getattr(_self: argparse._ColorlessTheme, name: str) -> Any: # noqa: N802 + """Patched __getattr__ that allows dunder lookups to fail correctly.""" + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + return "" + + # If the bug still exists, then install the patch. + if getattr(argparse._ColorlessTheme(), "__deepcopy__", None) == "": + argparse._ColorlessTheme.__getattr__ = _ColorlessTheme_getattr + ############################################################################################################ # Patch _ActionsContainer.add_argument to support more arguments diff --git a/cmd2/utils.py b/cmd2/utils.py index a5057ff79..f5411bf8a 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -480,11 +480,9 @@ def line_buffering(self) -> bool: except AttributeError: return False - def __getattr__(self, item: str) -> Any: - """When an attribute lookup fails to find the attribute in the usual places, this special method is called.""" - if item in self.__dict__: - return self.__dict__[item] - return getattr(self.inner_stream, item) + def __getattr__(self, name: str) -> Any: + """Forward attribute lookups to the inner stream for attributes not defined on this class.""" + return getattr(self.inner_stream, name) class ByteBuf: diff --git a/tests/test_argparse_utils.py b/tests/test_argparse_utils.py index a2315773a..731f1faa7 100644 --- a/tests/test_argparse_utils.py +++ b/tests/test_argparse_utils.py @@ -748,3 +748,19 @@ def test_argparse_output_capture(base_app: cmd2.Cmd) -> None: # Prove that the console style settings were used assert styled_help_out != unstyled_help_out assert su.strip_style("\n".join(styled_help_out)) == "\n".join(unstyled_help_out) + + +def test_colorless_theme_monkeypatch() -> None: + """Test the _ColorlessTheme.__getattr__ monkey patch.""" + + # _ColorlessTheme only exists in 3.15+ + if not hasattr(argparse, "_ColorlessTheme"): + return + + # If this assertion fails, then the bug no longer exists and our patch wasn't installed. + # We can remove the patch function and this test. + assert argparse._ColorlessTheme.__getattr__ == argparse_utils._ColorlessTheme_getattr + + # Our patch raises an Attribute error for dunder attributes. + with pytest.raises(AttributeError): + getattr(argparse._ColorlessTheme(), "__deep_copy__") # noqa: B009 From a3aed053ac230b42de96a56d3c230b6149e9f939 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 May 2026 15:56:04 -0400 Subject: [PATCH 2/3] Fixed mypy issues. --- cmd2/argparse_utils.py | 3 +-- tests/test_argparse_utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd2/argparse_utils.py b/cmd2/argparse_utils.py index f05f4922d..5f42993d5 100644 --- a/cmd2/argparse_utils.py +++ b/cmd2/argparse_utils.py @@ -375,8 +375,7 @@ def _validate_completion_callable(self: argparse.Action, value: Any) -> Any: # protocols like copy.deepcopy(). ############################################################################################################ -# _ColorlessTheme only exists in 3.15+ -if hasattr(argparse, "_ColorlessTheme"): +if sys.version_info >= (3, 15): def _ColorlessTheme_getattr(_self: argparse._ColorlessTheme, name: str) -> Any: # noqa: N802 """Patched __getattr__ that allows dunder lookups to fail correctly.""" diff --git a/tests/test_argparse_utils.py b/tests/test_argparse_utils.py index 731f1faa7..6c41910c7 100644 --- a/tests/test_argparse_utils.py +++ b/tests/test_argparse_utils.py @@ -750,13 +750,13 @@ def test_argparse_output_capture(base_app: cmd2.Cmd) -> None: assert su.strip_style("\n".join(styled_help_out)) == "\n".join(unstyled_help_out) +@pytest.mark.skipif( + sys.version_info < (3, 15), + reason="_ColorlessTheme only exists in 3.15+", +) def test_colorless_theme_monkeypatch() -> None: """Test the _ColorlessTheme.__getattr__ monkey patch.""" - # _ColorlessTheme only exists in 3.15+ - if not hasattr(argparse, "_ColorlessTheme"): - return - # If this assertion fails, then the bug no longer exists and our patch wasn't installed. # We can remove the patch function and this test. assert argparse._ColorlessTheme.__getattr__ == argparse_utils._ColorlessTheme_getattr From a172958b0c73ce7a3f5926606b2628b389d33448 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 May 2026 16:02:25 -0400 Subject: [PATCH 3/3] Fixed spelling. --- tests/test_argparse_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_argparse_utils.py b/tests/test_argparse_utils.py index 6c41910c7..7d990a057 100644 --- a/tests/test_argparse_utils.py +++ b/tests/test_argparse_utils.py @@ -763,4 +763,4 @@ def test_colorless_theme_monkeypatch() -> None: # Our patch raises an Attribute error for dunder attributes. with pytest.raises(AttributeError): - getattr(argparse._ColorlessTheme(), "__deep_copy__") # noqa: B009 + getattr(argparse._ColorlessTheme(), "__deepcopy__") # noqa: B009