From e300864eaa9a3e6d23a1dec01ba965515e0c0875 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 21 Apr 2026 00:00:13 -0400 Subject: [PATCH] Made CompletionItem deepcopy-safe. --- cmd2/completion.py | 14 ++++++++++++++ tests/test_completion.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/cmd2/completion.py b/cmd2/completion.py index 6733e56c9..fff0e999d 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -1,5 +1,6 @@ """Provides classes and functions related to command-line completion.""" +import copy import re import sys from collections.abc import ( @@ -124,6 +125,19 @@ def __post_init__(self) -> None: ru.prepare_objects_for_rendering(*renderable_data), ) + def __deepcopy__(self, memo: dict[int, Any]) -> "CompletionItem": + """Return a shallow copy of this CompletionItem during a deepcopy operation. + + This is necessary because cmd2 deepcopies argument parsers to keep them unique + across command instances. This override prevents the deepcopying of + CompletionItems stored within a parser's 'choices' list. + + Since the 'value' and 'table_data' fields may contain complex objects which + should not be deep copied, a shallow copy ensures the original object + references are preserved. + """ + return copy.copy(self) + def __str__(self) -> str: """Return the completion text.""" return self.text diff --git a/tests/test_completion.py b/tests/test_completion.py index ecc5bcd1a..a8b02d95f 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -5,6 +5,7 @@ """ import argparse +import copy import dataclasses import enum import os @@ -919,6 +920,36 @@ def test_remove_duplicates() -> None: assert new_meta in completions +def test_completion_item_deepcopy() -> None: + """Test that deepcopy of a CompletionItem preserves identity of its members.""" + + class ComplexValue: + pass + + value = ComplexValue() + table_data = (ComplexValue(),) + orig_item = CompletionItem( + value=value, + text="my_text", + display="my_display", + display_meta="my_meta", + table_data=table_data, + ) + + # Perform deepcopy + copied_item = copy.deepcopy(orig_item) + + # We should have a new object + assert copied_item is not orig_item + + # But its member should be the same objects + assert copied_item.value is orig_item.value + assert copied_item.text is orig_item.text + assert copied_item.display is orig_item.display + assert copied_item.display_meta is orig_item.display_meta + assert copied_item.table_data is orig_item.table_data + + def test_plain_fields() -> None: """Test the plain text fields in CompletionItem.""" display = "\x1b[31mApple\x1b[0m"