Coverage for tests/test_000_frigid/test_010_internals.py: 100%

224 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-05 03:26 +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 internals. ''' 

22 

23# mypy: ignore-errors 

24# pylint: disable=attribute-defined-outside-init 

25# pylint: disable=magic-value-comparison 

26# pylint: disable=missing-class-docstring 

27# pylint: disable=protected-access 

28# pylint: disable=unexpected-keyword-arg 

29# ruff: noqa: E711,E712 

30 

31 

32import pytest 

33 

34from platform import python_implementation 

35from types import MappingProxyType as DictionaryProxy 

36 

37from . import PACKAGE_NAME, cache_import_module 

38 

39 

40MODULE_QNAME = f"{PACKAGE_NAME}.__" 

41MODULE_ATTRIBUTE_NAMES = ( 

42 'Absent', 

43 'ConcealerExtension', 

44 'Docstring', 

45 'Falsifier', 

46 'InternalClass', 

47 'InternalObject', 

48 #'absent', 

49 'calculate_class_fqname', 

50 'calculate_fqname', 

51 'discover_public_attributes', 

52 'generate_docstring', 

53) 

54 

55exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) 

56module = cache_import_module( MODULE_QNAME ) 

57 

58dictionary_posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': True } ) 

59dictionary_nomargs = DictionaryProxy( dict( orb = False ) ) 

60 

61pypy_skip_mark = pytest.mark.skipif( 

62 'PyPy' == python_implementation( ), 

63 reason = "PyPy handles class cell updates differently" 

64) 

65 

66 

67def test_102_concealer_extension_attribute_visibility( ): 

68 ''' Instance conceals attributes according to visibility rules. ''' 

69 obj = module.ConcealerExtension( ) 

70 obj.public = 42 

71 assert ( 'public', ) == tuple( dir( obj ) ) 

72 obj._hidden = 24 

73 assert ( 'public', ) == tuple( dir( obj ) ) 

74 obj._visible = 12 

75 obj._attribute_visibility_includes_ = frozenset( ( '_visible', ) ) 

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

77 

78 

79def test_111_internal_class_immutability( ): 

80 ''' Class attributes become immutable after initialization. ''' 

81 factory = module.InternalClass 

82 class Example( metaclass = factory ): value = 42 

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

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

85 

86 

87def test_112_internal_class_decorator_handling( ): 

88 ''' Class properly handles decorators during creation. ''' 

89 factory = module.InternalClass 

90 def decorator1( cls ): 

91 cls.attr1 = 'one' 

92 return cls 

93 def decorator2( cls ): 

94 cls.attr2 = 'two' 

95 return cls 

96 

97 class Example( 

98 metaclass = factory, decorators = ( decorator1, decorator2 ) 

99 ): pass 

100 

101 assert 'one' == Example.attr1 

102 assert 'two' == Example.attr2 # pylint: disable=no-member 

103 with pytest.raises( AttributeError ): Example.attr1 = 'changed' 

104 

105 

106def test_113_internal_class_attribute_visibility( ): 

107 ''' Class conceals attributes according to visibility rules. ''' 

108 factory = module.InternalClass 

109 

110 class Example( metaclass = factory ): 

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

112 public = 42 

113 _hidden = 24 

114 _visible = 12 

115 

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

117 

118 

119@pypy_skip_mark 

120def test_114_internal_class_decorator_replacement( ): 

121 ''' Class properly handles decorators that return new classes. ''' 

122 from dataclasses import dataclass 

123 factory = module.InternalClass 

124 

125 class Example( 

126 metaclass = factory, decorators = ( dataclass( slots = True ), ) 

127 ): 

128 field1: str 

129 field2: int 

130 

131 assert hasattr( Example, '__slots__' ) 

132 with pytest.raises( AttributeError ): Example.field1 = 'changed' 

133 

134 

135def test_115_internal_class_behaviors_extension( ): 

136 ''' Class properly extends existing behaviors. ''' 

137 factory = module.InternalClass 

138 

139 class Base( metaclass = factory ): 

140 _class_behaviors_ = { 'existing' } 

141 

142 assert 'existing' in Base._class_behaviors_ 

143 assert module.behavior_label in Base._class_behaviors_ 

144 

145 

146def test_116_internal_class_nested_visibility( ): 

147 ''' Class properly handles visibility in nested hierarchies. ''' 

148 factory = module.InternalClass 

149 

150 class Base( metaclass = factory ): 

151 _class_attribute_visibility_includes_ = ( 

152 frozenset( ( '_base_visible', ) ) ) 

153 _base_visible = 'visible' 

154 _base_hidden = 'hidden' 

155 

156 class Derived( Base ): 

157 _class_attribute_visibility_includes_ = ( 

158 frozenset( ( '_derived_visible', ) ) ) 

159 _derived_visible = 'visible' 

160 _derived_hidden = 'hidden' 

161 

162 assert ( '_base_visible', '_derived_visible' ) == tuple( 

163 sorted( name for name in dir( Derived ) 

164 if name.startswith( '_' ) ) ) 

165 

166@pypy_skip_mark 

167def test_117_internal_class_complex_decorator( ): 

168 ''' Class properly handles complex decorator scenarios. ''' 

169 from dataclasses import dataclass 

170 from typing import ClassVar 

171 factory = module.InternalClass 

172 

173 def add_class_var( cls ): 

174 cls.class_var = 'added' 

175 return cls 

176 

177 @dataclass 

178 class Mixin: 

179 field3: str = 'mixin' # Optional field with default 

180 

181 class Example( 

182 Mixin, 

183 metaclass = factory, 

184 decorators = ( 

185 dataclass( kw_only = True, slots = True ), 

186 add_class_var, 

187 ) 

188 ): 

189 field1: str 

190 field2: int 

191 const: ClassVar[str] = 'const' 

192 

193 obj = Example( field1 = 'test', field2 = 42 ) # field3 uses default 

194 assert 'test' == obj.field1 

195 assert 42 == obj.field2 

196 assert 'mixin' == obj.field3 

197 assert 'const' == Example.const 

198 assert 'added' == Example.class_var 

199 with pytest.raises( AttributeError ): 

200 Example.class_var = 'modified' 

201 

202 

203def test_150_internal_object_immutability( ): 

204 ''' Instance attributes cannot be modified or deleted. ''' 

205 class Example( module.InternalObject ): 

206 def __init__( self ): 

207 # Need to bypass normal setattr to initialize 

208 super( module.InternalObject, self ).__setattr__( 'value', 42 ) 

209 

210 obj = Example( ) 

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

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

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

214 

215 

216def test_160_falsifier_behavior( ): 

217 ''' Falsifier objects are falsey and compare properly. ''' 

218 class Example( module.Falsifier ): pass 

219 

220 obj1 = Example( ) 

221 obj2 = Example( ) 

222 assert not obj1 

223 assert obj1 == obj1 # pylint: disable=comparison-with-itself 

224 assert obj1 != obj2 

225 assert obj1 is not True 

226 assert obj1 is not False 

227 assert obj1 is not None 

228 

229 

230def test_170_absent_singleton( ): 

231 ''' Absent class produces singleton instance. ''' 

232 obj1 = module.Absent( ) 

233 obj2 = module.Absent( ) 

234 assert obj1 is obj2 

235 assert obj1 is module.absent 

236 assert not obj1 

237 assert obj1 == obj1 # pylint: disable=comparison-with-itself 

238 assert obj1 != None # pylint: disable=singleton-comparison 

239 assert obj1 != False # pylint: disable=singleton-comparison 

240 

241 

242def test_172_absent_type_guard_edge_cases( ): 

243 ''' Type guard handles edge cases properly. ''' 

244 

245 def example( value: module.Optional[ str ] ) -> str: 

246 if not module.is_absent( value ): return value 

247 return 'default' 

248 

249 # Test with various falsey values 

250 assert '' == example( '' ) 

251 assert '0' == example( '0' ) 

252 assert 'False' == example( 'False' ) 

253 assert 'None' == example( 'None' ) 

254 # Test with various truthy values 

255 assert 'test' == example( 'test' ) 

256 assert '42' == example( '42' ) 

257 assert 'True' == example( 'True' ) 

258 # Test for absence. 

259 assert 'default' == example( module.absent ) 

260 

261 

262def test_200_immutable_dictionary_instantiation( ): 

263 ''' Dictionary instantiates with various input types. ''' 

264 factory = module.ImmutableDictionary 

265 dct1 = factory( ) 

266 assert isinstance( dct1, factory ) 

267 dct2 = factory( *dictionary_posargs, **dictionary_nomargs ) 

268 assert isinstance( dct2, factory ) 

269 assert 1 == dct2[ 'foo' ] 

270 assert 2 == dct2[ 'bar' ] 

271 assert dct2[ 'unicorn' ] 

272 assert not dct2[ 'orb' ] 

273 assert ( 'foo', 'bar', 'unicorn', 'orb' ) == tuple( dct2.keys( ) ) 

274 assert ( 1, 2, True, False ) == tuple( dct2.values( ) ) 

275 

276 

277def test_201_immutable_dictionary_duplication( ): 

278 ''' Dictionary is duplicable. ''' 

279 factory = module.ImmutableDictionary 

280 odct = factory( *dictionary_posargs, **dictionary_nomargs ) 

281 ddct = odct.copy( ) 

282 assert odct == ddct 

283 assert id( odct ) != id( ddct ) 

284 

285 

286def test_202_immutable_dictionary_prevents_key_overwrite( ): 

287 ''' Dictionary prevents overwriting existing keys during creation. ''' 

288 with pytest.raises( exceptions.EntryImmutabilityError ): 

289 module.ImmutableDictionary( [ ( 'a', 1 ) ], { 'a': 2 } ) 

290 

291 

292def test_210_immutable_dictionary_entry_protection( ): 

293 ''' Dictionary prevents entry modification and deletion. ''' 

294 factory = module.ImmutableDictionary 

295 dct = factory( *dictionary_posargs, **dictionary_nomargs ) 

296 with pytest.raises( exceptions.EntryImmutabilityError ): 

297 dct[ 'foo' ] = 42 

298 with pytest.raises( exceptions.EntryImmutabilityError ): 

299 del dct[ 'foo' ] 

300 with pytest.raises( exceptions.EntryImmutabilityError ): 

301 dct[ 'baz' ] = 3.1415926535 

302 

303 

304def test_211_immutable_dictionary_operation_prevention( ): 

305 ''' Dictionary prevents all mutating operations. ''' 

306 factory = module.ImmutableDictionary 

307 dct = factory( *dictionary_posargs, **dictionary_nomargs ) 

308 with pytest.raises( exceptions.OperationValidityError ): 

309 dct.clear( ) 

310 with pytest.raises( exceptions.OperationValidityError ): 

311 dct.pop( 'foo' ) 

312 with pytest.raises( exceptions.OperationValidityError ): 

313 dct.pop( 'foo', default = -1 ) 

314 with pytest.raises( exceptions.OperationValidityError ): 

315 dct.popitem( ) 

316 with pytest.raises( exceptions.OperationValidityError ): 

317 dct.update( baz = 42 ) 

318 

319 

320def test_212_immutable_dictionary_initialization_validation( ): 

321 ''' Dictionary properly handles various input types during creation. ''' 

322 factory = module.ImmutableDictionary 

323 dct1 = factory( { 'a': 1, 'b': 2 } ) 

324 assert 1 == dct1[ 'a' ] 

325 assert 2 == dct1[ 'b' ] 

326 dct2 = factory( [ ( 'c', 3 ), ( 'd', 4 ) ] ) 

327 assert 3 == dct2[ 'c' ] 

328 assert 4 == dct2[ 'd' ] 

329 dct3 = factory( e = 5, f = 6 ) 

330 assert 5 == dct3[ 'e' ] 

331 assert 6 == dct3[ 'f' ] 

332 dct4 = factory( { 'g': 7 }, [ ( 'h', 8 ) ], i = 9 ) 

333 assert 7 == dct4[ 'g' ] 

334 assert 8 == dct4[ 'h' ] 

335 assert 9 == dct4[ 'i' ] 

336 

337 

338def test_213_immutable_dictionary_behaviors( ): 

339 ''' Dictionary has proper immutability behavior. ''' 

340 factory = module.ImmutableDictionary 

341 dct = factory( a = 1 ) 

342 assert module.behavior_label in dct._behaviors_ 

343 

344 

345def test_171_absent_type_guard( ): 

346 ''' Type guard correctly identifies absent values. ''' 

347 def example( value: module.Optional[ str ] ) -> str: 

348 if not module.is_absent( value ): return value 

349 return 'default' 

350 

351 assert 'test' == example( 'test' ) 

352 assert 'default' == example( module.absent ) 

353 

354 

355def test_300_fqname_discovery( ): 

356 ''' Fully-qualified name of object is discovered. ''' 

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

358 assert ( 

359 'builtins.type' 

360 == module.calculate_fqname( module.ConcealerExtension ) ) 

361 obj = module.ConcealerExtension( ) 

362 assert ( 

363 f"{MODULE_QNAME}.ConcealerExtension" 

364 == module.calculate_fqname( obj ) ) 

365 

366 

367@pytest.mark.parametrize( 

368 'provided, expected', 

369 ( 

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

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

372 ( 

373 { name: getattr( module, name ) 

374 for name in MODULE_ATTRIBUTE_NAMES }, 

375 MODULE_ATTRIBUTE_NAMES 

376 ), 

377 ) 

378) 

379def test_400_public_attribute_discovery( provided, expected ): 

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

381 assert expected == module.discover_public_attributes( provided ) 

382 

383 

384def test_500_docstring_generation_argument_acceptance( ): 

385 ''' Docstring generator errors on invalid arguments. ''' 

386 class Foo: pass # pylint: disable=missing-class-docstring 

387 with pytest.raises( KeyError ): 

388 module.generate_docstring( 1 ) 

389 with pytest.raises( KeyError ): 

390 module.generate_docstring( '8-bit theater' ) 

391 assert not module.generate_docstring( Foo ) 

392 assert module.generate_docstring( 'instance attributes immutability' ) 

393 assert module.generate_docstring( module.Docstring( 'foo bar' ) ) 

394 

395 

396def test_501_docstring_generation_validity( ): 

397 ''' Generated docstrings are correctly formatted. ''' 

398 from inspect import getdoc 

399 

400 class Foo: 

401 ''' headline 

402 

403 additional information 

404 ''' 

405 

406 docstring_generated = module.generate_docstring( 

407 Foo, 

408 module.Docstring( 'foo bar' ), 

409 'class attributes immutability' ) 

410 docstring_expected = '\n\n'.join( ( 

411 getdoc( Foo ), 

412 'foo bar', 

413 module.generate_docstring( 'class attributes immutability' ) ) ) 

414 assert docstring_expected == docstring_generated