Skip to content

Commit 17c4d15

Browse files
committed
frozen_dataclass(test): Enhance test coverage and add doctests
why: Improve test coverage and documentation for frozen_dataclass what: - Add comprehensive doctests demonstrating basic usage and limitations - Add test for nested mutability leak scenario - Add test for inheritance compatibility - Improve exception message matching in existing tests All tests pass successfully (6 pytest tests, 16 doctests)
1 parent 0542165 commit 17c4d15

File tree

2 files changed

+129
-60
lines changed

2 files changed

+129
-60
lines changed

src/libtmux/_internal/frozen_dataclass.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,57 @@ def frozen_dataclass(cls: type[_T]) -> type[_T]:
3535
-------
3636
Type[_T]
3737
The processed class with immutability enforced at runtime
38+
39+
Examples
40+
--------
41+
Basic usage:
42+
43+
>>> @frozen_dataclass
44+
... class User:
45+
... id: int
46+
... name: str
47+
>>> user = User(id=1, name="Alice")
48+
>>> user.name
49+
'Alice'
50+
>>> try:
51+
... user.name = "Bob"
52+
... except AttributeError as e:
53+
... print(f"Error: {e}")
54+
Error: User is immutable: cannot modify field 'name'
55+
56+
Mutating internal attributes (_-prefixed):
57+
58+
>>> user._cache = {"logged_in": True}
59+
>>> user._cache
60+
{'logged_in': True}
61+
62+
Nested mutable fields limitation:
63+
64+
>>> @frozen_dataclass
65+
... class Container:
66+
... items: list[int]
67+
>>> c = Container(items=[1, 2])
68+
>>> c.items.append(3) # allowed; mutable field itself isn't protected
69+
>>> c.items
70+
[1, 2, 3]
71+
72+
Inheritance from mutable base classes:
73+
74+
>>> import dataclasses
75+
>>> @dataclasses.dataclass
76+
... class MutableBase:
77+
... value: int
78+
>>> @frozen_dataclass
79+
... class ImmutableSub(MutableBase):
80+
... pass
81+
>>> obj = ImmutableSub(42)
82+
>>> obj.value
83+
42
84+
>>> try:
85+
... obj.value = 100
86+
... except AttributeError as e:
87+
... print(f"Error: {e}")
88+
Error: ImmutableSub is immutable: cannot modify field 'value'
3889
"""
3990
# A. Convert to a dataclass with frozen=False
4091
cls = dataclasses.dataclass(cls)

tests/_internal/test_frozen_dataclass.py

Lines changed: 78 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -55,38 +55,24 @@ class WindowSnapshot:
5555
panes: list[PaneSnapshot] = dataclasses.field(default_factory=list)
5656

5757

58-
def test_basic_functionality() -> None:
59-
"""Test that the base class is mutable but the snapshot is not."""
60-
# Create a regular mutable pane
61-
pane = BasePane(pane_id="pane123", width=80, height=24)
62-
63-
# Should be mutable
64-
pane.width = 100
65-
assert pane.width == 100
66-
pane.resize(120, 30)
67-
assert pane.width == 120
68-
assert pane.height == 30
69-
70-
# Create a frozen snapshot with our decorator
71-
snapshot = PaneSnapshot(
72-
pane_id=pane.pane_id,
73-
width=pane.width,
74-
height=pane.height,
75-
captured_content=["Line1", "Line2"],
58+
# Core behavior tests
59+
# ------------------
60+
61+
def test_snapshot_initialization() -> None:
62+
"""Test proper initialization of fields in a frozen dataclass."""
63+
pane = PaneSnapshot(
64+
pane_id="pane123",
65+
width=80,
66+
height=24,
67+
captured_content=["Line1", "Line2"]
7668
)
77-
78-
# Test type checking
79-
assert isinstance(snapshot, PaneSnapshot)
80-
81-
# Should maintain the inheritance relationship
82-
assert isinstance(snapshot, BasePane)
83-
69+
8470
# Values should be correctly assigned
85-
assert snapshot.pane_id == pane.pane_id
86-
assert snapshot.width == pane.width
87-
assert snapshot.height == pane.height
88-
assert snapshot.captured_content == ["Line1", "Line2"]
89-
assert isinstance(snapshot.created_at, datetime)
71+
assert pane.pane_id == "pane123"
72+
assert pane.width == 80
73+
assert pane.height == 24
74+
assert pane.captured_content == ["Line1", "Line2"]
75+
assert isinstance(pane.created_at, datetime)
9076

9177

9278
def test_immutability() -> None:
@@ -96,33 +82,82 @@ def test_immutability() -> None:
9682
)
9783

9884
# Attempting to modify a field should raise AttributeError
99-
with pytest.raises(AttributeError) as excinfo:
85+
with pytest.raises(AttributeError, match="immutable.*cannot modify field 'width'"):
10086
snapshot.width = 200 # type: ignore
101-
assert "immutable" in str(excinfo.value)
10287

10388
# Attempting to add a new field should raise AttributeError
104-
with pytest.raises(AttributeError) as excinfo:
89+
with pytest.raises(AttributeError, match="immutable.*cannot modify field 'new_field'"):
10590
snapshot.new_field = "value" # type: ignore
106-
assert "immutable" in str(excinfo.value)
10791

10892
# Attempting to delete a field should raise AttributeError
109-
with pytest.raises(AttributeError) as excinfo:
93+
with pytest.raises(AttributeError, match="immutable.*cannot delete field 'width'"):
11094
del snapshot.width
111-
assert "immutable" in str(excinfo.value)
11295

11396
# Calling a method that tries to modify state should fail
114-
# Use separate variable for the NotImplementedError exception info
115-
with pytest.raises(NotImplementedError) as resize_excinfo:
97+
with pytest.raises(NotImplementedError, match="immutable"):
11698
snapshot.resize(200, 50)
117-
assert "immutable" in str(resize_excinfo.value)
11899

119100

120-
def test_nested_references() -> None:
121-
"""Test that nested structures work properly."""
101+
def test_inheritance() -> None:
102+
"""Test that frozen classes correctly inherit from mutable base classes."""
103+
# Create instances of both classes
104+
base_pane = BasePane(pane_id="base1", width=80, height=24)
105+
snapshot = PaneSnapshot(pane_id="snap1", width=80, height=24)
106+
107+
# Verify inheritance relationship
108+
assert isinstance(snapshot, BasePane)
109+
assert isinstance(snapshot, PaneSnapshot)
110+
111+
# Base class remains mutable
112+
base_pane.width = 100
113+
assert base_pane.width == 100
114+
115+
# Derived class is immutable
116+
with pytest.raises(AttributeError, match="immutable"):
117+
snapshot.width = 100
118+
119+
120+
# Edge case tests
121+
# --------------
122+
123+
def test_internal_attributes() -> None:
124+
"""Test that internal attributes (starting with _) can be modified."""
125+
snapshot = PaneSnapshot(
126+
pane_id="pane123",
127+
width=80,
128+
height=24,
129+
)
130+
131+
# Should be able to set internal attributes
132+
snapshot._internal_cache = {"test": "value"} # type: ignore
133+
assert snapshot._internal_cache == {"test": "value"} # type: ignore
134+
135+
136+
def test_nested_mutability_leak() -> None:
137+
"""Test the known limitation that nested mutable fields can still be modified."""
138+
# Create a frozen dataclass with a mutable field
139+
snapshot = PaneSnapshot(
140+
pane_id="pane123",
141+
width=80,
142+
height=24,
143+
captured_content=["initial"]
144+
)
145+
146+
# Can't reassign the field itself
147+
with pytest.raises(AttributeError, match="immutable"):
148+
snapshot.captured_content = ["new"] # type: ignore
149+
150+
# But we can modify its contents (limitation of Python immutability)
151+
snapshot.captured_content.append("mutated")
152+
assert "mutated" in snapshot.captured_content
153+
assert snapshot.captured_content == ["initial", "mutated"]
154+
155+
156+
def test_bidirectional_references() -> None:
157+
"""Test that nested structures with bidirectional references work properly."""
122158
# Create temporary panes (will be re-created with the window)
123159
temp_panes: list[PaneSnapshot] = []
124160

125-
# We need to create objects in a specific order to handle bi-directional references
126161
# First, create a window with an empty panes list
127162
window = WindowSnapshot(window_id="win1", name="Test Window", panes=temp_panes)
128163

@@ -131,8 +166,6 @@ def test_nested_references() -> None:
131166
pane2 = PaneSnapshot(pane_id="pane2", width=80, height=24, parent_window=window)
132167

133168
# Update the panes list before it gets frozen
134-
# This is a bit of a hack, but that's how you'd need to handle bi-directional
135-
# references with immutable objects in real code
136169
temp_panes.append(pane1)
137170
temp_panes.append(pane2)
138171

@@ -142,26 +175,11 @@ def test_nested_references() -> None:
142175
assert pane1 in window.panes
143176
assert pane2 in window.panes
144177

145-
# Can't test by trying to reassign since we'll hit a type error first
146-
# But we can still modify the contents of lists (limitation of Python)
147-
# Let's verify this limitation exists:
178+
# Can still modify the contents of mutable collections
148179
pane3 = PaneSnapshot(pane_id="pane3", width=100, height=30)
149180
window.panes.append(pane3)
150181
assert len(window.panes) == 3 # Successfully modified
151182

152183
# This is a "leaky abstraction" in Python's immutability model
153184
# In real code, consider using immutable collections (tuple, frozenset)
154185
# or deep freezing containers
155-
156-
157-
def test_internal_attributes() -> None:
158-
"""Test that internal attributes (starting with _) can be modified."""
159-
snapshot = PaneSnapshot(
160-
pane_id="pane123",
161-
width=80,
162-
height=24,
163-
)
164-
165-
# Should be able to set internal attributes
166-
snapshot._internal_cache = {"test": "value"} # type: ignore
167-
assert snapshot._internal_cache == {"test": "value"} # type: ignore

0 commit comments

Comments
 (0)