Coverage for sources/frigid/objects.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-05 03:47 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21# pylint: disable=line-too-long 

22''' Immutable objects. 

23 

24 Provides a base class and decorator for creating objects with immutable 

25 attributes. Once an object is initialized, its attributes cannot be modified 

26 or deleted. 

27 

28 The implementation uses a special dictionary type for attribute storage that 

29 enforces immutability. This makes it suitable for: 

30 

31 * Configuration objects 

32 * Value objects 

33 * Immutable data containers 

34 * Objects requiring attribute stability 

35 

36 >>> from frigid import Object 

37 >>> class Point( Object ): 

38 ... def __init__( self, x, y ): 

39 ... self.x = x 

40 ... self.y = y 

41 ... super( ).__init__( ) 

42 ... 

43 >>> obj = Point( 1, 2 ) # Initialize with attributes 

44 >>> obj.z = 3 # Attempt to add attribute 

45 Traceback (most recent call last): 

46 ... 

47 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'. 

48 >>> obj.x = 4 # Attempt modification 

49 Traceback (most recent call last): 

50 ... 

51 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'. 

52''' 

53# pylint: enable=line-too-long 

54 

55 

56from . import __ 

57 

58 

59def _check_behavior( obj: object ) -> bool: 

60 behaviors: __.cabc.MutableSet[ str ] 

61 if _check_dict( obj ): 

62 attributes = getattr( obj, '__dict__' ) 

63 behaviors = attributes.get( '_behaviors_', set( ) ) 

64 else: behaviors = getattr( obj, '_behaviors_', set( ) ) 

65 return __.behavior_label in behaviors 

66 

67 

68def _check_dict( obj: object ) -> bool: 

69 # Return False even if '__dict__' in '__slots__'. 

70 if hasattr( obj, '__slots__' ): return False 

71 return hasattr( obj, '__dict__' ) 

72 

73 

74def immutable( class_: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-complex 

75 ''' Decorator which makes class immutable after initialization. 

76 

77 Cannot be applied to classes which define their own __setattr__ 

78 or __delattr__ methods. 

79 ''' 

80 for method in ( '__setattr__', '__delattr__' ): 

81 if method in class_.__dict__: 

82 from .exceptions import DecoratorCompatibilityError 

83 raise DecoratorCompatibilityError( class_.__name__, method ) 

84 original_init = next( 

85 base.__dict__[ '__init__' ] for base in class_.__mro__ 

86 if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison 

87 

88 def __init__( 

89 self: object, *posargs: __.a.Any, **nomargs: __.a.Any 

90 ) -> None: 

91 # TODO: Use accretive set for behaviors. 

92 original_init( self, *posargs, **nomargs ) 

93 behaviors: __.cabc.MutableSet[ str ] 

94 if _check_dict( self ): 

95 attributes = getattr( self, '__dict__' ) 

96 behaviors = attributes.get( '_behaviors_', set( ) ) 

97 if not behaviors: attributes[ '_behaviors_' ] = behaviors 

98 setattr( self, '__dict__', __.ImmutableDictionary( attributes ) ) 

99 else: 

100 behaviors = getattr( self, '_behaviors_', set( ) ) 

101 if not behaviors: setattr( self, '_behaviors_', behaviors ) 

102 behaviors.add( __.behavior_label ) 

103 

104 def __delattr__( self: object, name: str ) -> None: 

105 if _check_behavior( self ): 

106 from .exceptions import AttributeImmutabilityError 

107 raise AttributeImmutabilityError( name ) 

108 super( class_, self ).__delattr__( name ) 

109 

110 def __setattr__( self: object, name: str, value: __.a.Any ) -> None: 

111 if _check_behavior( self ): 

112 from .exceptions import AttributeImmutabilityError 

113 raise AttributeImmutabilityError( name ) 

114 super( class_, self ).__setattr__( name, value ) 

115 

116 class_.__init__ = __init__ 

117 class_.__delattr__ = __delattr__ 

118 class_.__setattr__ = __setattr__ 

119 return class_ 

120 

121 

122@immutable 

123class Object: 

124 ''' Immutable objects. ''' 

125 

126 __slots__ = ( '__dict__', '_behaviors_' ) 

127 

128Object.__doc__ = __.generate_docstring( 

129 Object, 'instance attributes immutability' )