diff --git a/cmd2/argparse_utils.py b/cmd2/argparse_utils.py index cf06825e0..5f42993d5 100644 --- a/cmd2/argparse_utils.py +++ b/cmd2/argparse_utils.py @@ -369,6 +369,24 @@ 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(). +############################################################################################################ + +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.""" + 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..7d990a057 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) + + +@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.""" + + # 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(), "__deepcopy__") # noqa: B009