Skip to content

Commit 8535b4c

Browse files
committed
fix: keep generic metadata out of AttributeDict mappings
1 parent a142850 commit 8535b4c

3 files changed

Lines changed: 34 additions & 7 deletions

File tree

‎progressbar/utils.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,10 @@ class AttributeDict(dict[str, T], typing.Generic[T]):
521521
"""
522522
A dict that can be accessed with .attribute.
523523
524+
Double-underscore names are stored as instance attributes instead of
525+
dictionary entries. This keeps runtime metadata such as
526+
``__orig_class__`` out of the mapping contents.
527+
524528
>>> attrs = AttributeDict(spam=123)
525529
526530
# Reading
@@ -570,9 +574,15 @@ def __getattr__(self, name: str) -> T:
570574
raise AttributeError(f'No such attribute: {name}')
571575

572576
def __setattr__(self, name: str, value: T) -> None:
577+
if name.startswith('__') and name.endswith('__'):
578+
object.__setattr__(self, name, value)
579+
return
573580
self[name] = value
574581

575582
def __delattr__(self, name: str) -> None:
583+
if name.startswith('__') and name.endswith('__'):
584+
object.__delattr__(self, name)
585+
return
576586
if name in self:
577587
del self[name]
578588
else:

‎tests/test_stream.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ def test_redirect_stdout_unwrapped_after_keyboard_interrupt() -> None:
493493
# #212: when redirect_stdout wraps sys.stdout and iteration is abandoned
494494
# by a KeyboardInterrupt, the bar must unwrap stdout again. Runs in a
495495
# subprocess because stream wrapping mutates process-global sys.stdout.
496-
child = textwrap.dedent(
496+
child: str = textwrap.dedent(
497497
"""
498498
import sys
499499
import progressbar
@@ -510,8 +510,13 @@ def test_redirect_stdout_unwrapped_after_keyboard_interrupt() -> None:
510510
sys.exit(2 if isinstance(sys.stdout, WrappingIO) else 0)
511511
"""
512512
)
513-
result = subprocess.run(
514-
[sys.executable, '-c', child], capture_output=True, text=True
513+
env: dict[str, str] = os.environ.copy()
514+
env['PYTHONPATH'] = os.pathsep.join(sys.path)
515+
result: subprocess.CompletedProcess[str] = subprocess.run(
516+
[sys.executable, '-c', child],
517+
capture_output=True,
518+
text=True,
519+
env=env,
515520
)
516521
assert result.returncode == 0, (
517522
f'stdout left wrapped after interrupt; rc={result.returncode}\n'
@@ -523,9 +528,7 @@ def test_wrap_logging_deduplicates_shared_handler(monkeypatch) -> None:
523528
# A handler attached to more than one logger must be wrapped only once.
524529
# _iter_loggers yields the root logger then named loggers, so a handler
525530
# shared by both is seen twice and the second visit is skipped.
526-
for _ in range(5):
527-
progressbar.streams.unwrap(stderr=True, stdout=True)
528-
progressbar.streams.unwrap_logging()
531+
reset_wrapped_streams()
529532

530533
stream = io.StringIO()
531534
monkeypatch.setattr(sys, 'stderr', stream)

‎tests/test_utils.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ def test_attribute_dict_generic_value_type() -> None:
179179
# The generic upgrade lets homogeneous, explicitly-typed instances flow the
180180
# real value type. Runtime contract asserted here; the static benefit
181181
# (reveal_type -> int, mis-typed assignment flagged) is checked by pyright.
182-
attrs: utils.AttributeDict[int] = utils.AttributeDict()
182+
attrs: utils.AttributeDict[int] = utils.AttributeDict[int]()
183+
assert attrs == {}
184+
assert '__orig_class__' not in attrs
183185
attrs.count = 5
184186
assert attrs.count == 5
185187
assert attrs['count'] == 5
@@ -189,6 +191,18 @@ def test_attribute_dict_generic_value_type() -> None:
189191
assert mixed.b == 'x'
190192

191193

194+
def test_attribute_dict_dunder_attributes_use_instance_storage() -> None:
195+
attrs: utils.AttributeDict[str] = utils.AttributeDict()
196+
attrs.__probe__ = 'metadata'
197+
198+
assert attrs.__probe__ == 'metadata'
199+
assert '__probe__' not in attrs
200+
201+
del attrs.__probe__
202+
with pytest.raises(AttributeError):
203+
_ = attrs.__probe__
204+
205+
192206
def test_stream_wrapper_unwrap_restores_excepthook() -> None:
193207
# Regression: C7 - unwrap_stdout/unwrap_stderr left the custom
194208
# excepthook installed forever.

0 commit comments

Comments
 (0)