Coverage for tests/test_000_accretive/test_010_internals.py: 100%
188 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-10 22:22 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-10 22:22 +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# ruff: noqa: E711,E712
31import pytest
33from platform import python_implementation
34from types import MappingProxyType as DictionaryProxy
36from . import PACKAGE_NAME, cache_import_module
39MODULE_QNAME = f"{PACKAGE_NAME}.__"
40MODULE_ATTRIBUTE_NAMES = (
41 'Absent',
42 'ConcealerExtension',
43 'CoreDictionary',
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._immutability_label in Base._class_behaviors_
146def test_150_internal_object_immutability( ):
147 ''' Instance attributes cannot be modified or deleted. '''
148 class Example( module.InternalObject ):
149 def __init__( self ):
150 # Need to bypass normal setattr to initialize
151 super( module.InternalObject, self ).__setattr__( 'value', 42 )
153 obj = Example( )
154 with pytest.raises( AttributeError ): obj.value = 24
155 with pytest.raises( AttributeError ): obj.new_attr = 'test'
156 with pytest.raises( AttributeError ): del obj.value
159def test_160_falsifier_behavior( ):
160 ''' Falsifier objects are falsey and compare properly. '''
161 class Example( module.Falsifier ): pass
163 obj1 = Example( )
164 obj2 = Example( )
165 assert not obj1
166 assert obj1 == obj1 # pylint: disable=comparison-with-itself
167 assert obj1 != obj2
168 assert obj1 is not True
169 assert obj1 is not False
170 assert obj1 is not None
173def test_170_absent_singleton( ):
174 ''' Absent class produces singleton instance. '''
175 obj1 = module.Absent( )
176 obj2 = module.Absent( )
177 assert obj1 is obj2
178 assert obj1 is module.absent
179 assert not obj1
180 assert obj1 == obj1 # pylint: disable=comparison-with-itself
181 assert obj1 != None # pylint: disable=singleton-comparison
182 assert obj1 != False # pylint: disable=singleton-comparison
185def test_171_absent_type_guard( ):
186 ''' Type guard correctly identifies absent values. '''
187 def example( value: module.Optional[ str ] ) -> str:
188 if not module.is_absent( value ): return value
189 return 'default'
191 assert 'test' == example( 'test' )
192 assert 'default' == example( module.absent )
195def test_200_core_dictionary_instantiation( ):
196 ''' Class instantiates. '''
197 factory = module.CoreDictionary
198 dct1 = factory( )
199 assert isinstance( dct1, factory )
200 dct2 = factory( *dictionary_posargs, **dictionary_nomargs )
201 assert isinstance( dct2, factory )
202 assert 1 == dct2[ 'foo' ]
203 assert 2 == dct2[ 'bar' ]
204 assert dct2[ 'unicorn' ]
205 assert not dct2[ 'orb' ]
206 assert ( 'foo', 'bar', 'unicorn', 'orb' ) == tuple( dct2.keys( ) )
207 assert ( 1, 2, True, False ) == tuple( dct2.values( ) )
210def test_201_core_dictionary_duplication( ):
211 ''' Dictionary is duplicable. '''
212 factory = module.CoreDictionary
213 odct = factory( *dictionary_posargs, **dictionary_nomargs )
214 ddct = odct.copy( )
215 assert odct == ddct
216 odct[ 'baz' ] = 42
217 assert odct != ddct
220def test_210_core_dictionary_entry_accretion( ):
221 ''' Dictionary accretes entries. '''
222 factory = module.CoreDictionary
223 dct = factory( *dictionary_posargs, **dictionary_nomargs )
224 with pytest.raises( exceptions.IndelibleEntryError ):
225 dct[ 'foo' ] = 42
226 with pytest.raises( exceptions.IndelibleEntryError ):
227 del dct[ 'foo' ]
228 dct[ 'baz' ] = 3.1415926535
229 with pytest.raises( exceptions.IndelibleEntryError ):
230 dct[ 'baz' ] = -1
231 with pytest.raises( exceptions.IndelibleEntryError ):
232 del dct[ 'baz' ]
235def test_211_core_dictionary_entry_accretion_via_update( ):
236 ''' Dictionary accretes entries via update. '''
237 factory = module.CoreDictionary
238 dct = factory( )
239 dct.update( *dictionary_posargs, **dictionary_nomargs )
240 with pytest.raises( exceptions.IndelibleEntryError ):
241 dct[ 'foo' ] = 42
242 with pytest.raises( exceptions.IndelibleEntryError ):
243 del dct[ 'foo' ]
244 dct[ 'baz' ] = 3.1415926535
245 with pytest.raises( exceptions.IndelibleEntryError ):
246 dct[ 'baz' ] = -1
247 with pytest.raises( exceptions.IndelibleEntryError ):
248 del dct[ 'baz' ]
251def test_212_core_dictionary_update_validation( ):
252 ''' Dictionary update properly handles various input types. '''
253 factory = module.CoreDictionary
254 dct = factory( )
255 dct.update( { 'a': 1, 'b': 2 } )
256 assert 1 == dct[ 'a' ]
257 dct.update( [ ( 'c', 3 ), ( 'd', 4 ) ] )
258 assert 3 == dct[ 'c' ]
259 dct.update( e = 5, f = 6 )
260 assert 5 == dct[ 'e' ]
261 dct.update( { 'g': 7 }, [ ( 'h', 8 ) ], i = 9 )
262 assert 7 == dct[ 'g' ]
263 assert 8 == dct[ 'h' ]
264 assert 9 == dct[ 'i' ]
265 with pytest.raises( exceptions.IndelibleEntryError ):
266 dct.update( { 'a': 10 } )
269def test_220_core_dictionary_operation_prevention( ):
270 ''' Dictionary cannot perform entry deletions and mutations. '''
271 factory = module.CoreDictionary
272 dct = factory( *dictionary_posargs, **dictionary_nomargs )
273 with pytest.raises( exceptions.InvalidOperationError ):
274 dct.clear( )
275 with pytest.raises( exceptions.InvalidOperationError ):
276 dct.pop( 'foo' )
277 with pytest.raises( exceptions.InvalidOperationError ):
278 dct.pop( 'foo', default = -1 )
279 with pytest.raises( exceptions.InvalidOperationError ):
280 dct.pop( 'baz' )
281 with pytest.raises( exceptions.InvalidOperationError ):
282 dct.popitem( )
285def test_300_fqname_discovery( ):
286 ''' Fully-qualified name of object is discovered. '''
287 assert 'builtins.NoneType' == module.calculate_fqname( None )
288 assert (
289 'builtins.type'
290 == module.calculate_fqname( module.ConcealerExtension ) )
291 obj = module.ConcealerExtension( )
292 assert (
293 f"{MODULE_QNAME}.ConcealerExtension"
294 == module.calculate_fqname( obj ) )
297@pytest.mark.parametrize(
298 'provided, expected',
299 (
300 ( { 'foo': 12 }, ( ) ),
301 ( { '_foo': cache_import_module }, ( ) ),
302 (
303 { name: getattr( module, name )
304 for name in MODULE_ATTRIBUTE_NAMES },
305 MODULE_ATTRIBUTE_NAMES
306 ),
307 )
308)
309def test_400_public_attribute_discovery( provided, expected ):
310 ''' Public attributes are discovered from dictionary. '''
311 assert expected == module.discover_public_attributes( provided )
314def test_500_docstring_generation_argument_acceptance( ):
315 ''' Docstring generator errors on invalid arguments. '''
316 class Foo: pass # pylint: disable=missing-class-docstring
317 with pytest.raises( KeyError ):
318 module.generate_docstring( 1 )
319 with pytest.raises( KeyError ):
320 module.generate_docstring( '8-bit theater' )
321 assert not module.generate_docstring( Foo )
322 assert module.generate_docstring( 'instance attributes accretion' )
323 assert module.generate_docstring( module.Docstring( 'foo bar' ) )
326def test_501_docstring_generation_validity( ):
327 ''' Generated docstrings are correctly formatted. '''
328 from inspect import getdoc
330 class Foo:
331 ''' headline
333 additional information
334 '''
336 docstring_generated = module.generate_docstring(
337 Foo,
338 module.Docstring( 'foo bar' ),
339 'class attributes accretion' )
340 docstring_expected = '\n\n'.join( (
341 getdoc( Foo ),
342 'foo bar',
343 module.generate_docstring( 'class attributes accretion' ) ) )
344 assert docstring_expected == docstring_generated