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
« 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 -*-
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 internals. '''
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
32import pytest
34from platform import python_implementation
35from types import MappingProxyType as DictionaryProxy
37from . import PACKAGE_NAME, cache_import_module
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)
55exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" )
56module = cache_import_module( MODULE_QNAME )
58dictionary_posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': True } )
59dictionary_nomargs = DictionaryProxy( dict( orb = False ) )
61pypy_skip_mark = pytest.mark.skipif(
62 'PyPy' == python_implementation( ),
63 reason = "PyPy handles class cell updates differently"
64)
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 ) ) )
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
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
97 class Example(
98 metaclass = factory, decorators = ( decorator1, decorator2 )
99 ): pass
101 assert 'one' == Example.attr1
102 assert 'two' == Example.attr2 # pylint: disable=no-member
103 with pytest.raises( AttributeError ): Example.attr1 = 'changed'
106def test_113_internal_class_attribute_visibility( ):
107 ''' Class conceals attributes according to visibility rules. '''
108 factory = module.InternalClass
110 class Example( metaclass = factory ):
111 _class_attribute_visibility_includes_ = frozenset( ( '_visible', ) )
112 public = 42
113 _hidden = 24
114 _visible = 12
116 assert ( '_visible', 'public' ) == tuple( sorted( dir( Example ) ) )
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
125 class Example(
126 metaclass = factory, decorators = ( dataclass( slots = True ), )
127 ):
128 field1: str
129 field2: int
131 assert hasattr( Example, '__slots__' )
132 with pytest.raises( AttributeError ): Example.field1 = 'changed'
135def test_115_internal_class_behaviors_extension( ):
136 ''' Class properly extends existing behaviors. '''
137 factory = module.InternalClass
139 class Base( metaclass = factory ):
140 _class_behaviors_ = { 'existing' }
142 assert 'existing' in Base._class_behaviors_
143 assert module.behavior_label in Base._class_behaviors_
146def test_116_internal_class_nested_visibility( ):
147 ''' Class properly handles visibility in nested hierarchies. '''
148 factory = module.InternalClass
150 class Base( metaclass = factory ):
151 _class_attribute_visibility_includes_ = (
152 frozenset( ( '_base_visible', ) ) )
153 _base_visible = 'visible'
154 _base_hidden = 'hidden'
156 class Derived( Base ):
157 _class_attribute_visibility_includes_ = (
158 frozenset( ( '_derived_visible', ) ) )
159 _derived_visible = 'visible'
160 _derived_hidden = 'hidden'
162 assert ( '_base_visible', '_derived_visible' ) == tuple(
163 sorted( name for name in dir( Derived )
164 if name.startswith( '_' ) ) )
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
173 def add_class_var( cls ):
174 cls.class_var = 'added'
175 return cls
177 @dataclass
178 class Mixin:
179 field3: str = 'mixin' # Optional field with default
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'
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'
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 )
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
216def test_160_falsifier_behavior( ):
217 ''' Falsifier objects are falsey and compare properly. '''
218 class Example( module.Falsifier ): pass
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
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
242def test_172_absent_type_guard_edge_cases( ):
243 ''' Type guard handles edge cases properly. '''
245 def example( value: module.Optional[ str ] ) -> str:
246 if not module.is_absent( value ): return value
247 return 'default'
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 )
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( ) )
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 )
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 } )
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
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 )
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' ]
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_
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'
351 assert 'test' == example( 'test' )
352 assert 'default' == example( module.absent )
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 ) )
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 )
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' ) )
396def test_501_docstring_generation_validity( ):
397 ''' Generated docstrings are correctly formatted. '''
398 from inspect import getdoc
400 class Foo:
401 ''' headline
403 additional information
404 '''
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