Coverage for sources/classcore/utilities.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-25 13:22 +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''' Various utilities for class manipulation. ''' 

22 

23 

24from . import __ 

25 

26 

27def describe_object( objct: object, / ) -> str: 

28 ''' Returns object type with fully-qualified name. ''' 

29 if __.inspect.isclass( objct ): 

30 return "class '{}'".format( qualify_class_name( objct ) ) 

31 # TODO? functions, methods, etc... 

32 return "instance of {}".format( describe_object( type( objct ) ) ) 

33 

34 

35def getattr0( 

36 objct: object, /, name: str, default: __.typx.Any 

37) -> __.typx.Any: 

38 ''' Returns special private attribute from object. 

39 

40 This avoids inheritance-related collisions. 

41 

42 Uses mangled attribute name which is unique to the class, 

43 except when attribute is slotted. Slotted attributes are effectively 

44 isolated from inheritance. 

45 ''' 

46 if not __.inspect.isclass( objct ): 

47 for base in type( objct ).mro( ): 

48 slots = getattr( base, '__slots__', ( ) ) 

49 if name in slots: return getattr( objct, name, default ) 

50 name_m = mangle_name( objct, name ) 

51 return getattr( objct, name_m, default ) 

52 

53 

54def delattr0( objct: object, /, name: str ) -> None: 

55 ''' Deletes special private attribute on object. 

56 

57 This avoids inheritance-related collisions. 

58 

59 Uses mangled attribute name which is unique to the class, 

60 except when attribute is slotted. Slotted attributes are effectively 

61 isolated from inheritance. 

62 ''' 

63 if not __.inspect.isclass( objct ): 

64 for base in type( objct ).mro( ): 

65 slots = getattr( base, '__slots__', ( ) ) 

66 if name in slots: 

67 delattr( objct, name ) 

68 return 

69 name_m = mangle_name( objct, name ) 

70 delattr( objct, name_m ) 

71 

72 

73def setattr0( objct: object, /, name: str, value: __.typx.Any ) -> None: 

74 ''' Assigns special private attribute to object. 

75 

76 This avoids inheritance-related collisions. 

77 

78 Uses mangled attribute name which is unique to the class, 

79 except when attribute is slotted. Slotted attributes are effectively 

80 isolated from inheritance. 

81 ''' 

82 if not __.inspect.isclass( objct ): 

83 for base in type( objct ).mro( ): 

84 slots = getattr( base, '__slots__', ( ) ) 

85 if name in slots: 

86 setattr( objct, name, value ) 

87 return 

88 name_m = mangle_name( objct, name ) 

89 setattr( objct, name_m, value ) 

90 

91 

92def mangle_name( objct: object, /, name: str ) -> str: 

93 ''' Mangles attribute name so that it is unique. 

94 

95 Effectively provides name of private member attribute, 

96 which is unique across class inheritance. 

97 ''' 

98 # TODO: Replace expensive SHA-256 hash with simple 'id'. 

99 # Need to debug weird issue with using 'id' early on dataclasses. 

100 if not __.inspect.isclass( objct ): 

101 return mangle_name( type( objct ), name ) 

102 # return "{name}{uid}".format( name = name, uid = id( objct ) ) 

103 namehash = __.hashlib.sha256( ) 

104 namehash.update( qualify_class_name( objct ).encode( ) ) 

105 namehash_hex = namehash.hexdigest( ) 

106 return f"{name}{namehash_hex}" 

107 

108 

109def qualify_class_name( cls: type ) -> str: 

110 ''' Returns fully-qualified class name. ''' 

111 return f"{cls.__module__}.{cls.__qualname__}" 

112 

113 

114def repair_class_reproduction( original: type, reproduction: type ) -> None: 

115 ''' Repairs a class reproduction, if necessary. ''' 

116 match __.platform.python_implementation( ): 

117 case 'CPython' | 'PyPy': # pragma: no branch 

118 _repair_cpython_class_closures( original, reproduction ) 

119 case _: pass # pragma: no cover 

120 

121 

122def _repair_cpython_class_closures( 

123 original: type, reproduction: type 

124) -> None: 

125 # Adapted from https://github.com/python/cpython/pull/124455/files 

126 def try_repair_closure( 

127 function: __.cabc.Callable[ ..., __.typx.Any ] 

128 ) -> bool: 

129 try: index = function.__code__.co_freevars.index( '__class__' ) 

130 except ValueError: return False 

131 if not function.__closure__: return False # pragma: no branch 

132 closure = function.__closure__[ index ] 

133 if original is closure.cell_contents: # pragma: no branch 

134 closure.cell_contents = reproduction 

135 return True 

136 return False # pragma: no cover 

137 

138 for attribute in reproduction.__dict__.values( ): 

139 attribute_ = __.inspect.unwrap( attribute ) 

140 if ( __.inspect.isfunction( attribute_ ) 

141 and try_repair_closure( attribute_ ) 

142 ): return 

143 if isinstance( attribute_, property ): 

144 for aname in ( 'fget', 'fset', 'fdel' ): 

145 accessor = getattr( attribute_, aname ) 

146 if None is accessor: continue 

147 if try_repair_closure( accessor ): return