3838import dataclasses
3939import functools
4040import typing as t
41+ from typing import (
42+ Any ,
43+ Callable ,
44+ Protocol ,
45+ TypeVar ,
46+ runtime_checkable ,
47+ )
4148
42- from typing_extensions import dataclass_transform
49+ # Type definitions for better type hints
50+ T = TypeVar ("T" , bound = type )
4351
44- # Type definition for better hints
45- T = t .TypeVar ("T" )
52+
53+ @runtime_checkable
54+ class SealableProtocol (Protocol ):
55+ """Protocol defining the interface for sealable objects."""
56+
57+ _sealed : bool
58+
59+ def seal (self , deep : bool = False ) -> None :
60+ """Seal the object to prevent further modifications.
61+
62+ Parameters
63+ ----------
64+ deep : bool, optional
65+ If True, recursively seal any nested sealable objects, by default False
66+ """
67+ ...
68+
69+ @classmethod
70+ def is_sealable (cls ) -> bool :
71+ """Check if this class is sealable.
72+
73+ Returns
74+ -------
75+ bool
76+ True if the class is sealable, False otherwise
77+ """
78+ ...
79+
80+
81+ class Sealable :
82+ """Base class for sealable objects.
83+
84+ This class provides the basic implementation of the SealableProtocol,
85+ which can be used for explicit inheritance to create sealable classes.
86+
87+ Attributes
88+ ----------
89+ _sealed : bool
90+ Whether the object is sealed or not
91+ """
92+
93+ _sealed : bool = False
94+
95+ def seal (self , deep : bool = False ) -> None :
96+ """Seal the object to prevent further modifications.
97+
98+ Parameters
99+ ----------
100+ deep : bool, optional
101+ If True, recursively seal any nested sealable objects, by default False
102+ """
103+ # Basic implementation that can be overridden by subclasses
104+ object .__setattr__ (self , "_sealed" , True )
105+
106+ @classmethod
107+ def is_sealable (cls ) -> bool :
108+ """Check if this class is sealable.
109+
110+ Returns
111+ -------
112+ bool
113+ Always returns True for Sealable and its subclasses
114+ """
115+ return True
46116
47117
48118def mutable_field (
49- factory : t . Callable [[], t . Any ] = list ,
50- ) -> dataclasses .Field [t . Any ]:
119+ factory : Callable [[], Any ] = list ,
120+ ) -> dataclasses .Field [Any ]:
51121 """Create a field that is mutable during initialization but immutable after sealing.
52122
53123 Parameters
@@ -66,8 +136,8 @@ def mutable_field(
66136
67137
68138def mutable_during_init (
69- field_method : t . Callable [[], T ] | None = None ,
70- ) -> t . Any : # mypy doesn't handle complex return types well here
139+ field_method : Callable [[], T ] | None = None ,
140+ ) -> Any : # mypy doesn't handle complex return types well here
71141 """Mark a field as mutable during initialization but immutable after sealing.
72142
73143 This decorator applies to a method that returns the field's default value.
@@ -160,16 +230,7 @@ def mutable_during_init(
160230 )
161231
162232
163- # Protocol for classes with seal method
164- class _Sealable (t .Protocol ):
165- """Protocol for classes with seal method."""
166-
167- def seal (self ) -> None :
168- """Seal the object to prevent further modifications."""
169- ...
170-
171-
172- def is_sealable (cls_or_obj : t .Any ) -> bool :
233+ def is_sealable (cls_or_obj : Any ) -> bool :
173234 """Check if a class or object is sealable.
174235
175236 Parameters
@@ -186,7 +247,7 @@ def is_sealable(cls_or_obj: t.Any) -> bool:
186247 --------
187248 >>> from dataclasses import dataclass
188249 >>> from libtmux._internal.frozen_dataclass_sealable import (
189- ... frozen_dataclass_sealable, is_sealable
250+ ... frozen_dataclass_sealable, is_sealable, Sealable, SealableProtocol
190251 ... )
191252
192253 >>> # Regular class is not sealable
@@ -207,19 +268,61 @@ def is_sealable(cls_or_obj: t.Any) -> bool:
207268 False
208269 >>> is_sealable(None)
209270 False
271+
272+ >>> # Classes explicitly inheriting from Sealable are sealable
273+ >>> @dataclass
274+ ... class ExplicitSealable(Sealable):
275+ ... value: int
276+
277+ >>> is_sealable(ExplicitSealable)
278+ True
279+ >>> explicit = ExplicitSealable(value=42)
280+ >>> is_sealable(explicit)
281+ True
282+
283+ >>> # Classes decorated with frozen_dataclass_sealable are sealable
284+ >>> @frozen_dataclass_sealable
285+ ... class DecoratedSealable:
286+ ... value: int
287+
288+ >>> is_sealable(DecoratedSealable)
289+ True
290+ >>> decorated = DecoratedSealable(value=42)
291+ >>> is_sealable(decorated)
292+ True
293+
294+ >>> # Classes that implement SealableProtocol are sealable
295+ >>> class CustomSealable:
296+ ... _sealed = False
297+ ... def seal(self, deep=False):
298+ ... self._sealed = True
299+ ... @classmethod
300+ ... def is_sealable(cls):
301+ ... return True
302+
303+ >>> is_sealable(CustomSealable)
304+ True
305+ >>> custom = CustomSealable()
306+ >>> is_sealable(custom)
307+ True
210308 """
211- # If it's a class, check if it has a seal method
309+ # Check if the object is an instance of SealableProtocol
310+ if isinstance (cls_or_obj , SealableProtocol ):
311+ return True
312+
313+ # If it's a class, check if it's a subclass of Sealable or has a seal method
212314 if isinstance (cls_or_obj , type ):
315+ # Check if it's a subclass of Sealable
316+ if issubclass (cls_or_obj , Sealable ):
317+ return True
318+ # For backward compatibility, check if it has a seal method
213319 return hasattr (cls_or_obj , "seal" ) and callable (cls_or_obj .seal )
214320
215321 # If it's an instance, check if it has a seal method
216322 return hasattr (cls_or_obj , "seal" ) and callable (cls_or_obj .seal )
217323
218324
219- @dataclass_transform (frozen_default = True )
220- def frozen_dataclass_sealable (
221- cls : type | None = None , / , ** kwargs : t .Any
222- ) -> t .Callable [[type ], type ] | type :
325+ def frozen_dataclass_sealable (cls : type ) -> type :
223326 """Create a dataclass that is immutable, with field-level mutability control.
224327
225328 Enhances the standard dataclass with:
@@ -231,15 +334,13 @@ def frozen_dataclass_sealable(
231334
232335 Parameters
233336 ----------
234- cls : type, optional
235- The class to decorate, by default None
236- **kwargs : dict
237- Additional arguments passed to dataclasses.dataclass
337+ cls : type
338+ The class to decorate
238339
239340 Returns
240341 -------
241- type or callable
242- A decorated class with immutability features, or a decorator function if cls is None
342+ type
343+ The decorated class with immutability features
243344
244345 Examples
245346 --------
@@ -358,11 +459,10 @@ def frozen_dataclass_sealable(
358459 Error: AttributeError
359460 """
360461 # Support both @frozen_dataclass_sealable and @frozen_dataclass_sealable() usage
361- if cls is None :
362- return t .cast (
363- t .Callable [[type ], type ],
364- functools .partial (frozen_dataclass_sealable , ** kwargs ),
365- )
462+ # This branch is for direct decorator usage: @frozen_dataclass_sealable
463+ if not isinstance (cls , type ):
464+ err_msg = "Expected a class when calling frozen_dataclass_sealable directly"
465+ raise TypeError (err_msg )
366466
367467 # From here, we know cls is not None, so we can safely use cls.__name__
368468 class_name = cls .__name__
@@ -372,22 +472,7 @@ def frozen_dataclass_sealable(
372472 # Our custom __setattr__ and __delattr__ will handle immutability
373473 if not dataclasses .is_dataclass (cls ):
374474 # Explicitly set frozen=False to preserve inheritance flexibility
375- kwargs_copy = kwargs .copy ()
376- if "frozen" in kwargs_copy :
377- del kwargs_copy ["frozen" ]
378- cls = dataclasses .dataclass (frozen = False , ** kwargs_copy )(cls )
379- elif kwargs .get ("frozen" , False ):
380- # If the class is already a dataclass and frozen=True was specified,
381- # warn the user
382- import warnings
383-
384- warnings .warn (
385- f"Class { class_name } specified frozen=True which contradicts the "
386- "purpose of frozen_dataclass_sealable. "
387- "The custom implementation will override this." ,
388- UserWarning ,
389- stacklevel = 2 ,
390- )
475+ cls = dataclasses .dataclass (frozen = False )(cls )
391476
392477 # Store the original __post_init__ if it exists
393478 original_post_init = getattr (cls , "__post_init__" , None )
@@ -413,7 +498,7 @@ def frozen_dataclass_sealable(
413498 mutable_fields .add (name )
414499
415500 # Custom attribute setting implementation
416- def custom_setattr (self : t . Any , name : str , value : t . Any ) -> None :
501+ def custom_setattr (self : Any , name : str , value : Any ) -> None :
417502 # Allow setting private attributes always
418503 if name .startswith ("_" ):
419504 object .__setattr__ (self , name , value )
@@ -440,7 +525,7 @@ def custom_setattr(self: t.Any, name: str, value: t.Any) -> None:
440525 raise AttributeError (error_msg )
441526
442527 # Custom attribute deletion implementation
443- def custom_delattr (self : t . Any , name : str ) -> None :
528+ def custom_delattr (self : Any , name : str ) -> None :
444529 if name .startswith ("_" ):
445530 object .__delattr__ (self , name )
446531 return
@@ -454,7 +539,7 @@ def custom_delattr(self: t.Any, name: str) -> None:
454539 raise AttributeError (error_msg )
455540
456541 # Custom initialization to set initial attribute values
457- def custom_init (self : t . Any , * args : t . Any , ** kwargs : t . Any ) -> None :
542+ def custom_init (self : Any , * args : Any , ** kwargs : Any ) -> None :
458543 # Set the initializing flag
459544 object .__setattr__ (self , "_initializing" , True )
460545 object .__setattr__ (self , "_sealed" , False )
@@ -501,7 +586,6 @@ def custom_init(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
501586 base_fields = set ()
502587
503588 # Skip the current class in the MRO (it's the first one)
504- assert cls is not None , "cls should not be None here - this is a mypy guard"
505589 for base_cls in cls .__mro__ [1 :]:
506590 if hasattr (base_cls , "__dataclass_fields__" ):
507591 for name in base_cls .__dataclass_fields__ :
@@ -520,7 +604,6 @@ def custom_init(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
520604
521605 # Initialize base classes first
522606 # Skip the current class in the MRO (it's the first one)
523- assert cls is not None , "cls should not be None here - this is a mypy guard"
524607 for base_cls in cls .__mro__ [1 :]:
525608 base_init = getattr (base_cls , "__init__" , None )
526609 if (
@@ -555,10 +638,12 @@ def custom_init(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
555638 # Automatically seal if no mutable fields are defined
556639 # But ONLY for classes that don't have any fields marked mutable_during_init
557640 if not mutable_fields :
558- seal (self )
641+ seal_method = getattr (self , "seal" , None )
642+ if seal_method and callable (seal_method ):
643+ seal_method ()
559644
560- # Method to explicitly seal the object
561- def seal (self : t . Any , deep : bool = False ) -> None :
645+ # Define methods that will be attached to the class
646+ def seal_method (self : Any , deep : bool = False ) -> None :
562647 """Seal the object to prevent further modifications.
563648
564649 Parameters
@@ -574,21 +659,12 @@ def seal(self: t.Any, deep: bool = False) -> None:
574659 for field_obj in dataclasses .fields (self ):
575660 field_value = getattr (self , field_obj .name , None )
576661 # Check if the field value is sealable
577- from libtmux ._internal .frozen_dataclass_sealable import is_sealable
578-
579662 if field_value is not None and is_sealable (field_value ):
580663 # Seal the nested object
581664 field_value .seal (deep = True )
582665
583- # Add custom methods to the class
584- cls .__setattr__ = custom_setattr # type: ignore
585- cls .__delattr__ = custom_delattr # type: ignore
586- cls .__init__ = custom_init # type: ignore
587- cls .seal = seal # type: ignore
588-
589- # Add a class method to check if the class is sealable
590- @classmethod
591- def is_sealable (cls ) -> bool :
666+ # Define the is_sealable class method
667+ def is_sealable_class_method (cls_param : type ) -> bool :
592668 """Check if this class is sealable.
593669
594670 Returns
@@ -598,6 +674,11 @@ def is_sealable(cls) -> bool:
598674 """
599675 return True
600676
601- cls .is_sealable = is_sealable # type: ignore
677+ # Add custom methods to the class
678+ cls .__setattr__ = custom_setattr # type: ignore
679+ cls .__delattr__ = custom_delattr # type: ignore
680+ cls .__init__ = custom_init # type: ignore
681+ cls .seal = seal_method # type: ignore
682+ cls .is_sealable = classmethod (is_sealable_class_method ) # type: ignore
602683
603684 return cls
0 commit comments