Skip to content

Commit 7b14aa9

Browse files
committed
test: add regression test for TupleManager's dunder-name guard
nickname_delimiters/maiden_delimiters' construction already fixed a latent bug where TupleManager[X](...) leaked __orig_class__ into the dict itself (commit 5bd82b2), but nothing in the suite pinned it down directly -- a future "simplification" of __setattr__/__delattr__ back to bare dict.__setitem__/__delitem__ aliases would silently corrupt nickname/maiden parsing again with no test catching it. Flagged by a review agent auditing PR #199.
1 parent 8412d3f commit 7b14aa9

1 file changed

Lines changed: 25 additions & 0 deletions

File tree

tests/test_constants.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,31 @@ def test_extra_nickname_delimiters_removed(self) -> None:
246246
c = Constants()
247247
self.assertFalse(hasattr(c, 'extra_nickname_delimiters'))
248248

249+
def test_tuplemanager_setattr_delattr_ignore_dunder_names(self) -> None:
250+
"""Regression test for the bug that motivated nickname_delimiters'
251+
__setattr__/__delattr__ dunder guard.
252+
253+
Constructing a subscripted generic, e.g.
254+
TupleManager[re.Pattern[str] | str]({...}), makes typing's
255+
GenericAlias.__call__ set __orig_class__ on the new instance right
256+
after __init__ returns. Before the guard existed, __setattr__ was a
257+
bare dict.__setitem__ alias, so that assignment silently inserted a
258+
bogus '__orig_class__' entry into the dict itself, corrupting
259+
.values()/iteration for every TupleManager instance -- exactly what
260+
parse_nicknames() iterates over. This bit nickname_delimiters'
261+
construction (#22) before the guard was added.
262+
"""
263+
tm = TupleManager[re.Pattern[str] | str]({'a': re.compile('x')})
264+
self.assertNotIn('__orig_class__', tm)
265+
self.assertEqual(dict(tm), {'a': re.compile('x')})
266+
# Dunder assignment/deletion still work as normal object attributes,
267+
# just routed around dict-backed storage.
268+
tm.__custom_dunder__ = 'probe' # type: ignore[attr-defined]
269+
self.assertEqual(tm.__custom_dunder__, 'probe') # type: ignore[attr-defined]
270+
self.assertNotIn('__custom_dunder__', tm)
271+
del tm.__custom_dunder__ # type: ignore[attr-defined]
272+
self.assertFalse(hasattr(tm, '__custom_dunder__'))
273+
249274
def test_regextuplemanager_ignores_dunder_lookups(self) -> None:
250275
"""Unknown dunder names report as absent, not as the EMPTY_REGEX default.
251276

0 commit comments

Comments
 (0)