Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd2/argparse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_argparse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading