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
« 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 -*-
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#============================================================================#
21''' Assert correct function of class factory classes. '''
23# mypy: ignore-errors
24# pylint: disable=magic-value-comparison,protected-access
27import pytest
29from itertools import product
30from platform import python_implementation
32from . import (
33 MODULES_QNAMES,
34 PACKAGE_NAME,
35 cache_import_module,
36)
39THESE_MODULE_QNAMES = tuple(
40 name for name in MODULES_QNAMES if name.endswith( '.classes' ) )
41THESE_CLASSES_NAMES = ( 'Class', 'ABCFactory', 'ProtocolClass' )
43base = cache_import_module( f"{PACKAGE_NAME}.__" )
44exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" )
46pypy_skip_mark = pytest.mark.skipif(
47 'PyPy' == python_implementation( ),
48 reason = "PyPy handles class cell updates differently"
49)
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 )
61 class Object( metaclass = class_factory_class ):
62 ''' test '''
64 assert isinstance( Object, class_factory_class )
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 )
76 class Object( metaclass = class_factory_class ):
77 ''' test '''
78 attr = 42
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'
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 = [ ]
100 def test_decorator1( cls ):
101 decorator_calls.append( 'decorator1' )
102 cls.decorator1_attr = 'value1'
103 return cls
105 def test_decorator2( cls ):
106 decorator_calls.append( 'decorator2' )
107 cls.decorator2_attr = 'value2'
108 return cls
110 class Object(
111 metaclass = class_factory_class,
112 decorators = ( test_decorator1, test_decorator2 )
113 ):
114 ''' test '''
115 attr = 42
117 _class_behaviors_ = { 'foo' }
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'
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
139 class Object(
140 metaclass = class_factory_class,
141 decorators = ( dataclass( slots = True ), )
142 ):
143 ''' test '''
144 value: str = 'test'
146 def method_with_super( self ):
147 ''' References class cell on CPython. '''
148 super( ).__init__( )
149 return self.__class__.__name__
151 def other_method_with_super( self ):
152 ''' References class cell on CPython. '''
153 super( ).__init__( )
154 return 'other'
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( )
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
173 class Object(
174 metaclass = class_factory_class,
175 decorators = ( dataclass( slots = True ), )
176 ):
177 ''' test '''
178 value: str = 'test'
180 @property
181 def prop_with_class( self ):
182 ''' References class cell on CPython. '''
183 return self.__class__.__name__
185 @property
186 def other_prop_with_class( self ):
187 ''' References class cell on CPython. '''
188 return f"other_{self.__class__.__name__}"
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
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
207 class Object(
208 metaclass = class_factory_class,
209 decorators = ( dataclass( slots = True ), )
210 ):
211 ''' test '''
212 value: str = 'test'
214 def method_without_cell( self ): # pylint: disable=no-self-use
215 ''' Operates without class cell on CPython. '''
216 return 'no_cell'
218 # Verify class was properly reproduced
219 obj = Object( )
220 assert 'no_cell' == obj.method_without_cell( )
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 ) )
233 def failing_decorator( cls ):
234 raise ValueError( "Decorator failure" ) # noqa
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 '''
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 )
253 class Object( metaclass = class_factory_class, docstring = 'dynamic' ):
254 ''' test '''
255 attr = 42
257 assert 'test' != Object.__doc__
258 assert 'dynamic' == Object.__doc__
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 )
270 class Object(
271 metaclass = class_factory_class, mutables = ( 'mutable_attr', )
272 ):
273 ''' test '''
274 mutable_attr = 42
275 immutable_attr = 'fixed'
277 Object.mutable_attr = -1
278 assert -1 == Object.mutable_attr
279 with pytest.raises( exceptions.AttributeImmutabilityError ):
280 Object.immutable_attr = 'changed'
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 )
292 class Base(
293 metaclass = class_factory_class, mutables = ( 'base_mutable', )
294 ):
295 ''' test base '''
296 base_mutable = 'base'
297 base_immutable = 'fixed'
299 class Child( Base, mutables = ( 'child_mutable', ) ):
300 ''' test child '''
301 child_mutable = 'child'
302 child_immutable = 'fixed'
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'
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'
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 )
330 # Empty mutables collection
331 class EmptyMutables( metaclass = class_factory_class, mutables = ( ) ):
332 ''' test empty mutables '''
333 attr = 42
335 with pytest.raises( exceptions.AttributeImmutabilityError ):
336 EmptyMutables.attr = -1
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
346 # Should succeed because it's in mutables list
347 NonExistentMutable.does_not_exist = 'new'
348 assert 'new' == NonExistentMutable.does_not_exist
350 # Should fail because it's not in mutables list
351 with pytest.raises( exceptions.AttributeImmutabilityError ):
352 NonExistentMutable.not_in_mutables = 'attempt'
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
362 SpecialMutable.__special__ = 42
363 assert 42 == SpecialMutable.__special__
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 )
375 class First(
376 metaclass = class_factory_class, mutables = ( 'shared', 'first' )
377 ):
378 ''' test first parent '''
379 shared = 1
380 first = 'a'
382 class Second(
383 metaclass = class_factory_class, mutables = ( 'shared', 'second' )
384 ):
385 ''' test second parent '''
386 shared = 2
387 second = 'b'
389 class Child( First, Second, mutables = ( 'child', ) ):
390 ''' test child '''
391 child = 'c'
392 fixed = 'd'
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'
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 )
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'
424 # Can delete mutable attribute that exists
425 del DeletableMutable.deletable
426 assert not hasattr( DeletableMutable, 'deletable' )
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' )
434 # Cannot delete immutable attribute
435 with pytest.raises( exceptions.AttributeImmutabilityError ):
436 del DeletableMutable.fixed
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__
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__
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__