Skip to content
Open
2 changes: 2 additions & 0 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ class _ColorlessTheme:
def __getattr__(self, name):
# _colorize's no_color themes are just all empty strings
# by directly using empty strings the import is avoided
if name.startswith("_"):
raise AttributeError(name)
return ""

_colorless_theme = _ColorlessTheme()
Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,48 @@ def test_parse_args(self):
)


class TestArgumentParserCopiable(unittest.TestCase):
Comment thread
DavidCEllis marked this conversation as resolved.
def _get_parser(self):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('--foo', type=int, default=42)
parser.add_argument('bar', nargs='?', default='baz')
return parser

@force_not_colorized
def test_copiable(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want @force_not_colorized on these tests right?

Separately, we may also be able to fold copiable and deepcopiable into one test, since these are pretty much the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left these as two tests for now as there's testing for the different copy/deepcopy behaviour

import copy
parser = self._get_parser()
parser2 = copy.copy(parser)
ns = parser2.parse_args(['--foo', '123', 'quux'])
self.assertEqual(ns.foo, 123)
self.assertEqual(ns.bar, 'quux')
ns2 = parser2.parse_args([])
self.assertEqual(ns2.foo, 42)
self.assertEqual(ns2.bar, 'baz')

# Test shallow copy also gets new arguments
parser.add_argument("--extra")
ns3 = parser2.parse_args(["--extra", "bar"])
self.assertEqual(ns3.extra, "bar")

@force_not_colorized
def test_deepcopiable(self):
import copy
parser = self._get_parser()
parser2 = copy.deepcopy(parser)
ns = parser2.parse_args(['--foo', '123', 'quux'])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be more robust to mutate parser2 (e.g. add_argument) and assert parser doesn't see it. As written, the test would still pass if deepcopy silently returned a shallow copy or even the same object.

self.assertEqual(ns.foo, 123)
self.assertEqual(ns.bar, 'quux')
ns2 = parser2.parse_args([])
self.assertEqual(ns2.foo, 42)
self.assertEqual(ns2.bar, 'baz')

# Test deep copy does not get new arguments
parser.add_argument("--extra")
with self.assertRaises(argparse.ArgumentError):
parser2.parse_args(["--extra", "bar"])


class TestArgumentParserPickleable(unittest.TestCase):

@force_not_colorized
Expand Down Expand Up @@ -7863,12 +7905,25 @@ def fake_can_colorize(*, file=None):

def test_fake_color_theme_matches_real(self):
from argparse import _colorless_theme

# Check the attributes match those of the 'real' theme
_colorize_nocolor = _colorize.get_theme(force_no_color=True).argparse
for k in _colorize_nocolor:
self.assertEqual(
getattr(_colorless_theme, k), getattr(_colorize_nocolor, k)
)

def test_fake_color_theme_raises(self):
from argparse import _colorless_theme

# Make sure the _colorless_theme doesn't return empty strings
# for magic methods or private attributes
with self.assertRaises(AttributeError):
_colorless_theme.__unknown_dunder__

with self.assertRaises(AttributeError):
_colorless_theme._private_attribute


class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a regression that broke the ability to deepcopy ``argparse.ArgumentParser`` instances.
Loading