Coverage for tests/test_000_falsifier/test_011_immutables.py: 100%

200 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-21 00:09 +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''' Assert correct function of immutables. ''' 

22 

23 

24from platform import python_implementation 

25 

26import pytest 

27 

28from . import PACKAGE_NAME, cache_import_module 

29 

30 

31MODULE_QNAME = f"{PACKAGE_NAME}.__.immutables" 

32THESE_CLASSES_NAMES = ( 'ImmutableClass', ) 

33 

34pypy_skip_mark = pytest.mark.skipif( 

35 'PyPy' == python_implementation( ), 

36 reason = "PyPy handles class cell updates differently" ) 

37 

38 

39def test_100_concealer_instantiation( ): 

40 ''' Concealer extension class instantiates. ''' 

41 module = cache_import_module( MODULE_QNAME ) 

42 obj = module.ConcealerExtension( ) 

43 assert isinstance( obj, module.ConcealerExtension ) 

44 assert hasattr( obj, '_attribute_visibility_includes_' ) 

45 

46 

47def test_110_concealer_visibility( ): 

48 ''' Concealer extension class conceals attributes according to rules. ''' 

49 module = cache_import_module( MODULE_QNAME ) 

50 

51 class Example( module.ConcealerExtension ): 

52 _attribute_visibility_includes_ = frozenset( ( '_visible', ) ) 

53 

54 obj = Example( ) 

55 obj.public = 42 

56 obj._hidden = 24 

57 obj._visible = 12 

58 assert ( '_visible', 'public' ) == tuple( sorted( dir( obj ) ) ) 

59 

60 

61@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES ) 

62def test_200_immutable_class_init( class_name ): 

63 ''' Class prevents modification after initialization. ''' 

64 module = cache_import_module( MODULE_QNAME ) 

65 factory = getattr( module, class_name ) 

66 

67 class Example( metaclass = factory ): 

68 value = 42 

69 

70 with pytest.raises( AttributeError ): Example.value = 24 

71 with pytest.raises( AttributeError ): del Example.value 

72 

73 

74@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES ) 

75def test_210_immutable_class_visibility( class_name ): 

76 ''' Class conceals attributes according to rules. ''' 

77 module = cache_import_module( MODULE_QNAME ) 

78 factory = getattr( module, class_name ) 

79 

80 class Example( metaclass = factory ): 

81 _class_behaviors_ = { 'foobar' } 

82 _class_attribute_visibility_includes_ = frozenset( ( '_visible', ) ) 

83 public = 42 

84 _hidden = 24 

85 _visible = 12 

86 

87 assert ( '_visible', 'public' ) == tuple( sorted( dir( Example ) ) ) 

88 

89 

90@pypy_skip_mark 

91@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES ) 

92def test_220_immutable_class_decorators( class_name ): 

93 ''' Class handles decorators correctly. ''' 

94 from dataclasses import dataclass 

95 module = cache_import_module( MODULE_QNAME ) 

96 factory = getattr( module, class_name ) 

97 

98 def add_attr( cls ): 

99 cls.added = 'value' 

100 return cls 

101 

102 class Example( 

103 metaclass = factory, 

104 decorators = ( dataclass( slots = True ), add_attr ) 

105 ): 

106 field: str = 'test' 

107 

108 assert hasattr( Example, '__slots__' ) 

109 assert 'value' == Example.added 

110 with pytest.raises( AttributeError ): Example.added = 'changed' 

111 

112 

113@pypy_skip_mark 

114def test_221_immutable_class_replacement_super_method( ): 

115 ''' ImmutableClass handles class replacement by decorators. ''' 

116 from dataclasses import dataclass 

117 module = cache_import_module( MODULE_QNAME ) 

118 factory = module.ImmutableClass 

119 

120 class Example( 

121 metaclass = factory, 

122 decorators = ( dataclass( slots = True ), ) 

123 ): 

124 field1: str 

125 field2: int 

126 

127 def method_with_super( self ): 

128 ''' References class cell on CPython. ''' 

129 super( ).__init__( ) 

130 return self.__class__.__name__ 

131 

132 obj = Example( field1 = 'test', field2 = 42 ) 

133 assert 'Example' == obj.method_with_super( ) 

134 assert hasattr( Example, '__slots__' ) 

135 with pytest.raises( AttributeError ): 

136 Example.field1 = 'changed' 

137 

138 

139@pypy_skip_mark 

140def test_222_immutable_class_replacement_super_property( ): 

141 ''' ImmutableClass handles class replacement by decorators. ''' 

142 from dataclasses import dataclass 

143 module = cache_import_module( MODULE_QNAME ) 

144 factory = module.ImmutableClass 

145 

146 class Example( 

147 metaclass = factory, 

148 decorators = ( dataclass( slots = True ), ) 

149 ): 

150 field1: str 

151 field2: int 

152 

153 @property 

154 def prop_with_class( self ): 

155 ''' References class cell on CPython. ''' 

156 return self.__class__.__name__ 

157 

158 obj = Example( field1 = 'test', field2 = 42 ) 

159 assert 'Example' == obj.prop_with_class 

160 assert hasattr( Example, '__slots__' ) 

161 with pytest.raises( AttributeError ): 

162 Example.field1 = 'changed' 

163 

164 

165# pylint: disable=no-member 

166 

167def test_300_module_reclassification_by_name( ): 

168 ''' Module reclassification works with module name. ''' 

169 module = cache_import_module( MODULE_QNAME ) 

170 from types import ModuleType 

171 test_module = ModuleType( f"{PACKAGE_NAME}.test" ) 

172 test_module.__package__ = PACKAGE_NAME 

173 from sys import modules 

174 modules[ test_module.__name__ ] = test_module 

175 module.reclassify_modules( test_module.__name__ ) 

176 assert isinstance( test_module, module.ImmutableModule ) 

177 with pytest.raises( AttributeError ): 

178 test_module.new_attr = 42 

179 

180 

181def test_301_module_reclassification_by_object( ): 

182 ''' Module reclassification works with module object. ''' 

183 module = cache_import_module( MODULE_QNAME ) 

184 from types import ModuleType 

185 test_module = ModuleType( f"{PACKAGE_NAME}.test" ) 

186 test_module.__package__ = PACKAGE_NAME 

187 module.reclassify_modules( test_module ) 

188 assert isinstance( test_module, module.ImmutableModule ) 

189 with pytest.raises( AttributeError ): 

190 test_module.new_attr = 42 

191 

192 

193def test_302_recursive_module_reclassification( ): 

194 ''' Recursive module reclassification works. ''' 

195 module = cache_import_module( MODULE_QNAME ) 

196 from types import ModuleType 

197 root = ModuleType( f"{PACKAGE_NAME}.test" ) 

198 root.__package__ = PACKAGE_NAME 

199 sub1 = ModuleType( f"{PACKAGE_NAME}.test.sub1" ) 

200 sub2 = ModuleType( f"{PACKAGE_NAME}.test.sub2" ) 

201 root.sub1 = sub1 

202 root.sub2 = sub2 

203 module.reclassify_modules( root, recursive = True ) 

204 assert isinstance( root, module.ImmutableModule ) 

205 assert isinstance( sub1, module.ImmutableModule ) 

206 assert isinstance( sub2, module.ImmutableModule ) 

207 with pytest.raises( AttributeError ): 

208 root.new_attr = 42 

209 with pytest.raises( AttributeError ): 

210 sub1.new_attr = 42 

211 with pytest.raises( AttributeError ): 

212 sub2.new_attr = 42 

213 

214 

215def test_303_module_reclassification_respects_package( ): 

216 ''' Module reclassification only affects package modules. ''' 

217 module = cache_import_module( MODULE_QNAME ) 

218 from types import ModuleType 

219 root = ModuleType( f"{PACKAGE_NAME}.test" ) 

220 root.__package__ = PACKAGE_NAME 

221 external = ModuleType( "other_package.module" ) 

222 other_pkg = ModuleType( "other_package" ) 

223 root.external = external 

224 root.other_pkg = other_pkg 

225 module.reclassify_modules( root, recursive = True ) 

226 assert isinstance( root, module.ImmutableModule ) 

227 assert not isinstance( external, module.ImmutableModule ) 

228 assert not isinstance( other_pkg, module.ImmutableModule ) 

229 with pytest.raises( AttributeError ): 

230 root.new_attr = 42 

231 external.new_attr = 42 # Should work 

232 assert 42 == external.new_attr 

233 

234 

235def test_304_module_reclassification_by_dict( ): 

236 ''' Module reclassification works with attribute dictionary. ''' 

237 module = cache_import_module( MODULE_QNAME ) 

238 from types import ModuleType 

239 m1 = ModuleType( f"{PACKAGE_NAME}.test1" ) 

240 m2 = ModuleType( f"{PACKAGE_NAME}.test2" ) 

241 m3 = ModuleType( "other.module" ) 

242 attrs = { 

243 '__package__': PACKAGE_NAME, 

244 'module1': m1, 

245 'module2': m2, 

246 'external': m3, 

247 'other': 42, 

248 } 

249 module.reclassify_modules( attrs ) 

250 assert isinstance( m1, module.ImmutableModule ) 

251 assert isinstance( m2, module.ImmutableModule ) 

252 assert not isinstance( m3, module.ImmutableModule ) 

253 with pytest.raises( AttributeError ): 

254 m1.new_attr = 42 

255 with pytest.raises( AttributeError ): 

256 m2.new_attr = 42 

257 m3.new_attr = 42 # Should work 

258 

259 

260def test_305_module_reclassification_requires_package( ): 

261 ''' Module reclassification requires package name. ''' 

262 module = cache_import_module( MODULE_QNAME ) 

263 from types import ModuleType 

264 m1 = ModuleType( f"{PACKAGE_NAME}.test1" ) 

265 attrs = { 'module1': m1 } 

266 module.reclassify_modules( attrs ) 

267 assert not isinstance( m1, module.ImmutableModule ) 

268 m1.new_attr = 42 

269 assert 42 == m1.new_attr 

270 

271 

272def test_306_module_attribute_operations( ): 

273 ''' Module prevents attribute deletion and modification. ''' 

274 module = cache_import_module( MODULE_QNAME ) 

275 from types import ModuleType 

276 test_module = ModuleType( f"{PACKAGE_NAME}.test" ) 

277 test_module.__package__ = PACKAGE_NAME 

278 test_module.existing = 42 

279 module.reclassify_modules( test_module ) 

280 with pytest.raises( AttributeError ) as exc_info: 

281 del test_module.existing 

282 assert "Cannot delete attribute 'existing'" in str( exc_info.value ) 

283 assert test_module.__name__ in str( exc_info.value ) 

284 with pytest.raises( AttributeError ) as exc_info: 

285 test_module.existing = 24 

286 assert "Cannot assign attribute 'existing'" in str( exc_info.value ) 

287 assert test_module.__name__ in str( exc_info.value ) 

288 assert 42 == test_module.existing 

289 

290# pylint: enable=no-member 

291 

292 

293def test_400_immutable_object_init( ): 

294 ''' Object prevents modification after initialization. ''' 

295 module = cache_import_module( MODULE_QNAME ) 

296 

297 class Example( module.ImmutableObject ): 

298 def __init__( self ): 

299 super( module.ImmutableObject, self ).__setattr__( 'value', 42 ) 

300 super( ).__init__( ) 

301 

302 obj = Example( ) 

303 with pytest.raises( AttributeError ): obj.value = 24 

304 with pytest.raises( AttributeError ): obj.new_attr = 'test' 

305 with pytest.raises( AttributeError ): del obj.value 

306 

307 

308def test_500_name_calculation( ): 

309 ''' Name calculation functions work correctly. ''' 

310 module = cache_import_module( MODULE_QNAME ) 

311 assert 'builtins.NoneType' == module.calculate_fqname( None ) 

312 assert ( 

313 'builtins.type' 

314 == module.calculate_fqname( module.ConcealerExtension ) ) 

315 

316 

317@pytest.mark.parametrize( 

318 'provided, expected', 

319 ( 

320 ( { 'foo': 12 }, ( ) ), 

321 ( { '_foo': cache_import_module }, ( ) ), 

322 ( 

323 { 'public_func': lambda: None }, 

324 ( 'public_func', ) 

325 ), 

326 ) 

327) 

328def test_600_attribute_discovery( provided, expected ): 

329 ''' Public attributes are discovered from dictionary. ''' 

330 module = cache_import_module( MODULE_QNAME ) 

331 assert expected == module.discover_public_attributes( provided )