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

222 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-24 04: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 class factory classes. ''' 

22 

23# mypy: ignore-errors 

24# pylint: disable=magic-value-comparison,protected-access 

25 

26 

27import pytest 

28 

29from itertools import product 

30from platform import python_implementation 

31 

32from . import ( 

33 MODULES_QNAMES, 

34 PACKAGE_NAME, 

35 cache_import_module, 

36) 

37 

38 

39THESE_MODULE_QNAMES = tuple( 

40 name for name in MODULES_QNAMES if name.endswith( '.classes' ) ) 

41THESE_CLASSES_NAMES = ( 'Class', 'ABCFactory', 'ProtocolClass' ) 

42 

43base = cache_import_module( f"{PACKAGE_NAME}.__" ) 

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

45 

46pypy_skip_mark = pytest.mark.skipif( 

47 'PyPy' == python_implementation( ), 

48 reason = "PyPy handles class cell updates differently" 

49) 

50 

51 

52@pytest.mark.parametrize( 

53 'module_qname, class_name', 

54 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

55) 

56def test_100_instantiation( module_qname, class_name ): 

57 ''' Class instantiates. ''' 

58 module = cache_import_module( module_qname ) 

59 class_factory_class = getattr( module, class_name ) 

60 

61 class Object( metaclass = class_factory_class ): 

62 ''' test ''' 

63 

64 assert isinstance( Object, class_factory_class ) 

65 

66 

67@pytest.mark.parametrize( 

68 'module_qname, class_name', 

69 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

70) 

71def test_101_immutability( module_qname, class_name ): 

72 ''' Class attributes are immutable. ''' 

73 module = cache_import_module( module_qname ) 

74 class_factory_class = getattr( module, class_name ) 

75 

76 class Object( metaclass = class_factory_class ): 

77 ''' test ''' 

78 attr = 42 

79 

80 with pytest.raises( exceptions.AttributeImmutabilityError ): 

81 Object.attr = -1 

82 assert 42 == Object.attr 

83 with pytest.raises( exceptions.AttributeImmutabilityError ): 

84 del Object.attr 

85 assert 42 == Object.attr 

86 with pytest.raises( exceptions.AttributeImmutabilityError ): 

87 Object.new_attr = 'foo' 

88 

89 

90@pytest.mark.parametrize( 

91 'module_qname, class_name', 

92 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

93) 

94def test_110_class_decorators( module_qname, class_name ): 

95 ''' Class accepts and applies decorators correctly. ''' 

96 module = cache_import_module( module_qname ) 

97 class_factory_class = getattr( module, class_name ) 

98 decorator_calls = [ ] 

99 

100 def test_decorator1( cls ): 

101 decorator_calls.append( 'decorator1' ) 

102 cls.decorator1_attr = 'value1' 

103 return cls 

104 

105 def test_decorator2( cls ): 

106 decorator_calls.append( 'decorator2' ) 

107 cls.decorator2_attr = 'value2' 

108 return cls 

109 

110 class Object( 

111 metaclass = class_factory_class, 

112 decorators = ( test_decorator1, test_decorator2 ) 

113 ): 

114 ''' test ''' 

115 attr = 42 

116 

117 _class_behaviors_ = { 'foo' } 

118 

119 assert [ 'decorator1', 'decorator2' ] == decorator_calls 

120 assert 'value1' == Object.decorator1_attr 

121 assert 'value2' == Object.decorator2_attr 

122 with pytest.raises( exceptions.AttributeImmutabilityError ): 

123 Object.decorator1_attr = 'new_value' 

124 with pytest.raises( exceptions.AttributeImmutabilityError ): 

125 Object.decorator2_attr = 'new_value' 

126 

127 

128@pypy_skip_mark 

129@pytest.mark.parametrize( 

130 'module_qname, class_name', 

131 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

132) 

133def test_111_class_decorator_reproduction_method( module_qname, class_name ): 

134 ''' Class handles decorator reproduction with super() method. ''' 

135 module = cache_import_module( module_qname ) 

136 class_factory_class = getattr( module, class_name ) 

137 from dataclasses import dataclass 

138 

139 class Object( 

140 metaclass = class_factory_class, 

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

142 ): 

143 ''' test ''' 

144 value: str = 'test' 

145 

146 def method_with_super( self ): 

147 ''' References class cell on CPython. ''' 

148 super( ).__init__( ) 

149 return self.__class__.__name__ 

150 

151 def other_method_with_super( self ): 

152 ''' References class cell on CPython. ''' 

153 super( ).__init__( ) 

154 return 'other' 

155 

156 # Verify class was properly reproduced and both methods work 

157 obj = Object( ) 

158 assert 'Object' == obj.method_with_super( ) 

159 assert 'other' == obj.other_method_with_super( ) 

160 

161 

162@pypy_skip_mark 

163@pytest.mark.parametrize( 

164 'module_qname, class_name', 

165 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

166) 

167def test_112_class_decorator_reproduction_property( module_qname, class_name ): 

168 ''' Class handles decorator reproduction with dotted access property. ''' 

169 module = cache_import_module( module_qname ) 

170 class_factory_class = getattr( module, class_name ) 

171 from dataclasses import dataclass 

172 

173 class Object( 

174 metaclass = class_factory_class, 

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

176 ): 

177 ''' test ''' 

178 value: str = 'test' 

179 

180 @property 

181 def prop_with_class( self ): 

182 ''' References class cell on CPython. ''' 

183 return self.__class__.__name__ 

184 

185 @property 

186 def other_prop_with_class( self ): 

187 ''' References class cell on CPython. ''' 

188 return f"other_{self.__class__.__name__}" 

189 

190 # Verify class was properly reproduced and both properties work 

191 obj = Object( ) 

192 assert 'Object' == obj.prop_with_class 

193 assert 'other_Object' == obj.other_prop_with_class 

194 

195 

196@pypy_skip_mark 

197@pytest.mark.parametrize( 

198 'module_qname, class_name', 

199 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

200) 

201def test_113_class_decorator_reproduction_no_cell( module_qname, class_name ): 

202 ''' Class handles decorator reproduction with no class cell. ''' 

203 module = cache_import_module( module_qname ) 

204 class_factory_class = getattr( module, class_name ) 

205 from dataclasses import dataclass 

206 

207 class Object( 

208 metaclass = class_factory_class, 

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

210 ): 

211 ''' test ''' 

212 value: str = 'test' 

213 

214 def method_without_cell( self ): # pylint: disable=no-self-use 

215 ''' Operates without class cell on CPython. ''' 

216 return 'no_cell' 

217 

218 # Verify class was properly reproduced 

219 obj = Object( ) 

220 assert 'no_cell' == obj.method_without_cell( ) 

221 

222 

223@pytest.mark.parametrize( 

224 'module_qname, class_name', 

225 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

226) 

227def test_114_decorator_error_handling( module_qname, class_name ): 

228 ''' Class handles decorator errors appropriately. ''' 

229 module = cache_import_module( module_qname ) 

230 class_factory_class = ( # pylint: disable=unused-variable 

231 getattr( module, class_name ) ) 

232 

233 def failing_decorator( cls ): 

234 raise ValueError( "Decorator failure" ) # noqa 

235 

236 with pytest.raises( ValueError, match = "Decorator failure" ): 

237 class Object( # pylint: disable=unused-variable 

238 metaclass = class_factory_class, 

239 decorators = ( failing_decorator, ) 

240 ): 

241 ''' test ''' 

242 

243 

244@pytest.mark.parametrize( 

245 'module_qname, class_name', 

246 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

247) 

248def test_120_docstring_assignment( module_qname, class_name ): 

249 ''' Class has dynamically-assigned docstring. ''' 

250 module = cache_import_module( module_qname ) 

251 class_factory_class = getattr( module, class_name ) 

252 

253 class Object( metaclass = class_factory_class, docstring = 'dynamic' ): 

254 ''' test ''' 

255 attr = 42 

256 

257 assert 'test' != Object.__doc__ 

258 assert 'dynamic' == Object.__doc__ 

259 

260 

261@pytest.mark.parametrize( 

262 'module_qname, class_name', 

263 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

264) 

265def test_130_mutable_attributes( module_qname, class_name ): 

266 ''' Specified attributes remain mutable. ''' 

267 module = cache_import_module( module_qname ) 

268 class_factory_class = getattr( module, class_name ) 

269 

270 class Object( 

271 metaclass = class_factory_class, mutables = ( 'mutable_attr', ) 

272 ): 

273 ''' test ''' 

274 mutable_attr = 42 

275 immutable_attr = 'fixed' 

276 

277 Object.mutable_attr = -1 

278 assert -1 == Object.mutable_attr 

279 with pytest.raises( exceptions.AttributeImmutabilityError ): 

280 Object.immutable_attr = 'changed' 

281 

282 

283@pytest.mark.parametrize( 

284 'module_qname, class_name', 

285 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

286) 

287def test_131_mutable_inheritance( module_qname, class_name ): 

288 ''' Mutable attributes are inherited properly. ''' 

289 module = cache_import_module( module_qname ) 

290 class_factory_class = getattr( module, class_name ) 

291 

292 class Base( 

293 metaclass = class_factory_class, mutables = ( 'base_mutable', ) 

294 ): 

295 ''' test base ''' 

296 base_mutable = 'base' 

297 base_immutable = 'fixed' 

298 

299 class Child( Base, mutables = ( 'child_mutable', ) ): 

300 ''' test child ''' 

301 child_mutable = 'child' 

302 child_immutable = 'fixed' 

303 

304 # Base class mutables remain mutable 

305 Base.base_mutable = 'changed_base' 

306 assert 'changed_base' == Base.base_mutable 

307 with pytest.raises( exceptions.AttributeImmutabilityError ): 

308 Base.base_immutable = 'attempt' 

309 

310 # Child inherits base mutables and adds its own 

311 Child.base_mutable = 'inherited_changed' 

312 assert 'inherited_changed' == Child.base_mutable 

313 Child.child_mutable = 'child_changed' 

314 assert 'child_changed' == Child.child_mutable 

315 with pytest.raises( exceptions.AttributeImmutabilityError ): 

316 Child.child_immutable = 'attempt' 

317 with pytest.raises( exceptions.AttributeImmutabilityError ): 

318 Child.base_immutable = 'attempt' 

319 

320 

321@pytest.mark.parametrize( 

322 'module_qname, class_name', 

323 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

324) 

325def test_132_mutable_edge_cases( module_qname, class_name ): 

326 ''' Handle edge cases for mutable attributes. ''' 

327 module = cache_import_module( module_qname ) 

328 class_factory_class = getattr( module, class_name ) 

329 

330 # Empty mutables collection 

331 class EmptyMutables( metaclass = class_factory_class, mutables = ( ) ): 

332 ''' test empty mutables ''' 

333 attr = 42 

334 

335 with pytest.raises( exceptions.AttributeImmutabilityError ): 

336 EmptyMutables.attr = -1 

337 

338 # Non-existent attributes can be added if listed as mutable 

339 class NonExistentMutable( 

340 metaclass = class_factory_class, 

341 mutables = ( 'does_not_exist', 'another_future_attr' ) 

342 ): 

343 ''' test non-existent mutable ''' 

344 attr = 42 

345 

346 # Should succeed because it's in mutables list 

347 NonExistentMutable.does_not_exist = 'new' 

348 assert 'new' == NonExistentMutable.does_not_exist 

349 

350 # Should fail because it's not in mutables list 

351 with pytest.raises( exceptions.AttributeImmutabilityError ): 

352 NonExistentMutable.not_in_mutables = 'attempt' 

353 

354 # Special method names as mutable attributes 

355 class SpecialMutable( 

356 metaclass = class_factory_class, 

357 mutables = ( '__special__', ) 

358 ): 

359 ''' test special method mutable ''' 

360 __special__ = None 

361 

362 SpecialMutable.__special__ = 42 

363 assert 42 == SpecialMutable.__special__ 

364 

365 

366@pytest.mark.parametrize( 

367 'module_qname, class_name', 

368 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

369) 

370def test_133_multiple_inheritance_mutables( module_qname, class_name ): 

371 ''' Handle mutable attributes with multiple inheritance. ''' 

372 module = cache_import_module( module_qname ) 

373 class_factory_class = getattr( module, class_name ) 

374 

375 class First( 

376 metaclass = class_factory_class, mutables = ( 'shared', 'first' ) 

377 ): 

378 ''' test first parent ''' 

379 shared = 1 

380 first = 'a' 

381 

382 class Second( 

383 metaclass = class_factory_class, mutables = ( 'shared', 'second' ) 

384 ): 

385 ''' test second parent ''' 

386 shared = 2 

387 second = 'b' 

388 

389 class Child( First, Second, mutables = ( 'child', ) ): 

390 ''' test child ''' 

391 child = 'c' 

392 fixed = 'd' 

393 

394 # All declared mutables should work 

395 Child.shared = 3 

396 assert 3 == Child.shared 

397 Child.first = 'changed_a' 

398 assert 'changed_a' == Child.first 

399 Child.second = 'changed_b' 

400 assert 'changed_b' == Child.second 

401 Child.child = 'changed_c' 

402 assert 'changed_c' == Child.child 

403 with pytest.raises( exceptions.AttributeImmutabilityError ): 

404 Child.fixed = 'attempt' 

405 

406 

407@pytest.mark.parametrize( 

408 'module_qname, class_name', 

409 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

410) 

411def test_134_mutable_deletion( module_qname, class_name ): 

412 ''' Handle deletion of mutable attributes. ''' 

413 module = cache_import_module( module_qname ) 

414 class_factory_class = getattr( module, class_name ) 

415 

416 class DeletableMutable( 

417 metaclass = class_factory_class, 

418 mutables = ( 'deletable', 'not_yet_set' ) 

419 ): 

420 ''' test mutable deletion ''' 

421 deletable = 'original' 

422 fixed = 'constant' 

423 

424 # Can delete mutable attribute that exists 

425 del DeletableMutable.deletable 

426 assert not hasattr( DeletableMutable, 'deletable' ) 

427 

428 # Can set and then delete mutable attribute that didn't exist at creation 

429 DeletableMutable.not_yet_set = 'temporary' 

430 assert 'temporary' == DeletableMutable.not_yet_set 

431 del DeletableMutable.not_yet_set 

432 assert not hasattr( DeletableMutable, 'not_yet_set' ) 

433 

434 # Cannot delete immutable attribute 

435 with pytest.raises( exceptions.AttributeImmutabilityError ): 

436 del DeletableMutable.fixed 

437 

438 

439@pytest.mark.parametrize( 

440 'module_qname, class_name', 

441 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

442) 

443def test_900_docstring_sanity( module_qname, class_name ): 

444 ''' Class has valid docstring. ''' 

445 module = cache_import_module( module_qname ) 

446 class_factory_class = getattr( module, class_name ) 

447 assert hasattr( class_factory_class, '__doc__' ) 

448 assert isinstance( class_factory_class.__doc__, str ) 

449 assert class_factory_class.__doc__ 

450 

451 

452@pytest.mark.parametrize( 

453 'module_qname, class_name', 

454 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

455) 

456def test_901_docstring_describes_cfc( module_qname, class_name ): 

457 ''' Class docstring describes class factory class. ''' 

458 module = cache_import_module( module_qname ) 

459 class_factory_class = getattr( module, class_name ) 

460 fragment = base.generate_docstring( 'description of class factory class' ) 

461 assert fragment in class_factory_class.__doc__ 

462 

463 

464@pytest.mark.parametrize( 

465 'module_qname, class_name', 

466 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

467) 

468def test_902_docstring_mentions_immutability( module_qname, class_name ): 

469 ''' Class docstring mentions immutability. ''' 

470 module = cache_import_module( module_qname ) 

471 class_factory_class = getattr( module, class_name ) 

472 fragment = base.generate_docstring( 'class attributes immutability' ) 

473 assert fragment in class_factory_class.__doc__