Skip to content

Commit 816cec5

Browse files
committed
Add is_sealable function and deep sealing capability
- Implement is_sealable() function to check if a class or object is sealable - Enhance seal() method to support deep=True for recursive sealing - Add tests for recursive sealing with multiple levels - Fix field order in test dataclasses - Move test classes to module level for pickling support
1 parent 410c195 commit 816cec5

File tree

2 files changed

+67
-16
lines changed

2 files changed

+67
-16
lines changed

src/libtmux/_internal/frozen_dataclass_sealable.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,28 @@ def values(self) -> dict[str, int]:
188188

189189
# Protocol for classes with seal method
190190
class _Sealable(t.Protocol):
191-
def seal(self) -> None: ...
191+
"""Protocol for classes with seal method."""
192+
193+
def seal(self) -> None:
194+
"""Seal the object to prevent further modifications."""
195+
...
196+
197+
198+
def is_sealable(cls_or_obj: t.Any) -> bool:
199+
"""Check if a class or object is sealable.
200+
201+
Args:
202+
cls_or_obj: A class or object to check
203+
204+
Returns:
205+
True if the class or object is sealable, False otherwise
206+
"""
207+
# Check if it's a class
208+
if isinstance(cls_or_obj, type):
209+
return hasattr(cls_or_obj, "seal") and callable(cls_or_obj.seal)
210+
211+
# It's an object instance
212+
return hasattr(cls_or_obj, "seal") and callable(cls_or_obj.seal)
192213

193214

194215
@dataclass_transform(frozen_default=True)
@@ -447,13 +468,37 @@ def custom_init(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
447468
seal(self)
448469

449470
# Method to explicitly seal the object
450-
def seal(self: t.Any) -> None:
471+
def seal(self: t.Any, deep: bool = False) -> None:
472+
"""Seal the object to prevent further modifications.
473+
474+
Args:
475+
deep: If True, recursively seal any nested sealable objects
476+
"""
477+
# First seal this object
451478
object.__setattr__(self, "_sealed", True)
452479

480+
# If deep sealing requested, look for nested sealable objects
481+
if deep:
482+
for field_obj in dataclasses.fields(self):
483+
field_value = getattr(self, field_obj.name, None)
484+
# Check if the field value is sealable
485+
from libtmux._internal.frozen_dataclass_sealable import is_sealable
486+
if field_value is not None and is_sealable(field_value):
487+
# Seal the nested object
488+
field_value.seal(deep=True)
489+
453490
# Add custom methods to the class
454491
cls.__setattr__ = custom_setattr # type: ignore
455492
cls.__delattr__ = custom_delattr # type: ignore
456493
cls.__init__ = custom_init # type: ignore
457494
cls.seal = seal # type: ignore
495+
496+
# Add a class method to check if the class is sealable
497+
@classmethod
498+
def is_sealable(cls) -> bool:
499+
"""Check if this class is sealable."""
500+
return True
501+
502+
cls.is_sealable = is_sealable # type: ignore
458503

459504
return cls

tests/_internal/test_frozen_dataclass_sealable.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ class _FrozenChild(MutableBase):
140140
FrozenChild = frozen_dataclass_sealable(_FrozenChild)
141141

142142

143+
# Class used for pickling tests, defined at module level
144+
@frozen_dataclass_sealable
145+
class PickleTest:
146+
name: str
147+
values: list[int] = field(
148+
default_factory=list, metadata={"mutable_during_init": True}
149+
)
150+
151+
143152
# Core behavior tests
144153
# -----------------
145154

@@ -666,8 +675,8 @@ class Inner:
666675

667676
@frozen_dataclass_sealable
668677
class Outer:
669-
inner: Inner = field(default=None, metadata={"mutable_during_init": True})
670678
data: str = field(metadata={"mutable_during_init": True})
679+
inner: Inner = field(default=None, metadata={"mutable_during_init": True})
671680

672681
# Case 1: Deep sealing (deep=True)
673682
inner_obj = Inner(val=42)
@@ -809,8 +818,12 @@ def test_mutable_containers_after_sealing() -> None:
809818

810819
@frozen_dataclass_sealable
811820
class ContainerHolder:
812-
items: list[int] = field(default_factory=list)
813-
mapping: dict[str, int] = field(default_factory=dict)
821+
items: list[int] = field(
822+
default_factory=list, metadata={"mutable_during_init": True}
823+
)
824+
mapping: dict[str, int] = field(
825+
default_factory=dict, metadata={"mutable_during_init": True}
826+
)
814827

815828
obj = ContainerHolder()
816829
obj.items.extend([1, 2, 3])
@@ -866,13 +879,6 @@ def test_pickling_sealed_objects() -> None:
866879
"""Test that sealed objects can be pickled and unpickled while preserving their sealed state."""
867880
import pickle
868881

869-
@frozen_dataclass_sealable
870-
class PickleTest:
871-
name: str
872-
values: list[int] = field(
873-
default_factory=list, metadata={"mutable_during_init": True}
874-
)
875-
876882
# Create and configure object
877883
obj = PickleTest(name="test")
878884
obj.values.extend([1, 2, 3])
@@ -968,13 +974,13 @@ class Level3:
968974

969975
@frozen_dataclass_sealable
970976
class Level2:
971-
level3: Level3 = field(default=None, metadata={"mutable_during_init": True})
972977
name: str = field(metadata={"mutable_during_init": True})
978+
level3: Level3 = field(default=None, metadata={"mutable_during_init": True})
973979

974980
@frozen_dataclass_sealable
975981
class Level1:
976-
level2: Level2 = field(default=None, metadata={"mutable_during_init": True})
977982
data: str = field(metadata={"mutable_during_init": True})
983+
level2: Level2 = field(default=None, metadata={"mutable_during_init": True})
978984

979985
# Create nested structure
980986
level3 = Level3(value=42)
@@ -987,7 +993,7 @@ class Level1:
987993
level1.data = "modified top"
988994

989995
# Deep seal from the top level
990-
level1.seal(deep=True)
996+
level1.seal(deep=True) # This should seal all levels
991997

992998
# All levels should now be sealed
993999
with pytest.raises(AttributeError):
@@ -1013,10 +1019,10 @@ class RegularClass:
10131019

10141020
@frozen_dataclass_sealable
10151021
class MixedContainer:
1022+
data: str = field(metadata={"mutable_during_init": True})
10161023
regular: RegularClass = field(
10171024
default=None, metadata={"mutable_during_init": True}
10181025
)
1019-
data: str = field(metadata={"mutable_during_init": True})
10201026

10211027
# Create objects
10221028
regular = RegularClass(name="test", value=42)

0 commit comments

Comments
 (0)