Coverage for tests/test_000_falsifier/test_011_immutables.py: 100%
200 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 23:26 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 23: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 immutables. '''
24from platform import python_implementation
26import pytest
28from . import PACKAGE_NAME, cache_import_module
31MODULE_QNAME = f"{PACKAGE_NAME}.__.immutables"
32THESE_CLASSES_NAMES = ( 'ImmutableClass', )
34pypy_skip_mark = pytest.mark.skipif(
35 'PyPy' == python_implementation( ),
36 reason = "PyPy handles class cell updates differently" )
39def test_100_concealer_instantiation( ):
40 ''' Concealer extension class instantiates. '''
41 module = cache_import_module( MODULE_QNAME )
42 obj = module.ConcealerExtension( )
43 assert isinstance( obj, module.ConcealerExtension )
44 assert hasattr( obj, '_attribute_visibility_includes_' )
47def test_110_concealer_visibility( ):
48 ''' Concealer extension class conceals attributes according to rules. '''
49 module = cache_import_module( MODULE_QNAME )
51 class Example( module.ConcealerExtension ):
52 _attribute_visibility_includes_ = frozenset( ( '_visible', ) )
54 obj = Example( )
55 obj.public = 42
56 obj._hidden = 24
57 obj._visible = 12
58 assert ( '_visible', 'public' ) == tuple( sorted( dir( obj ) ) )
61@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES )
62def test_200_immutable_class_init( class_name ):
63 ''' Class prevents modification after initialization. '''
64 module = cache_import_module( MODULE_QNAME )
65 factory = getattr( module, class_name )
67 class Example( metaclass = factory ):
68 value = 42
70 with pytest.raises( AttributeError ): Example.value = 24
71 with pytest.raises( AttributeError ): del Example.value
74@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES )
75def test_210_immutable_class_visibility( class_name ):
76 ''' Class conceals attributes according to rules. '''
77 module = cache_import_module( MODULE_QNAME )
78 factory = getattr( module, class_name )
80 class Example( metaclass = factory ):
81 _class_behaviors_ = { 'foobar' }
82 _class_attribute_visibility_includes_ = frozenset( ( '_visible', ) )
83 public = 42
84 _hidden = 24
85 _visible = 12
87 assert ( '_visible', 'public' ) == tuple( sorted( dir( Example ) ) )
90@pypy_skip_mark
91@pytest.mark.parametrize( 'class_name', THESE_CLASSES_NAMES )
92def test_220_immutable_class_decorators( class_name ):
93 ''' Class handles decorators correctly. '''
94 from dataclasses import dataclass
95 module = cache_import_module( MODULE_QNAME )
96 factory = getattr( module, class_name )
98 def add_attr( cls ):
99 cls.added = 'value'
100 return cls
102 class Example(
103 metaclass = factory,
104 decorators = ( dataclass( slots = True ), add_attr )
105 ):
106 field: str = 'test'
108 assert hasattr( Example, '__slots__' )
109 assert 'value' == Example.added
110 with pytest.raises( AttributeError ): Example.added = 'changed'
113@pypy_skip_mark
114def test_221_immutable_class_replacement_super_method( ):
115 ''' ImmutableClass handles class replacement by decorators. '''
116 from dataclasses import dataclass
117 module = cache_import_module( MODULE_QNAME )
118 factory = module.ImmutableClass
120 class Example(
121 metaclass = factory,
122 decorators = ( dataclass( slots = True ), )
123 ):
124 field1: str
125 field2: int
127 def method_with_super( self ):
128 ''' References class cell on CPython. '''
129 super( ).__init__( )
130 return self.__class__.__name__
132 obj = Example( field1 = 'test', field2 = 42 )
133 assert 'Example' == obj.method_with_super( )
134 assert hasattr( Example, '__slots__' )
135 with pytest.raises( AttributeError ):
136 Example.field1 = 'changed'
139@pypy_skip_mark
140def test_222_immutable_class_replacement_super_property( ):
141 ''' ImmutableClass handles class replacement by decorators. '''
142 from dataclasses import dataclass
143 module = cache_import_module( MODULE_QNAME )
144 factory = module.ImmutableClass
146 class Example(
147 metaclass = factory,
148 decorators = ( dataclass( slots = True ), )
149 ):
150 field1: str
151 field2: int
153 @property
154 def prop_with_class( self ):
155 ''' References class cell on CPython. '''
156 return self.__class__.__name__
158 obj = Example( field1 = 'test', field2 = 42 )
159 assert 'Example' == obj.prop_with_class
160 assert hasattr( Example, '__slots__' )
161 with pytest.raises( AttributeError ):
162 Example.field1 = 'changed'
165# pylint: disable=no-member
167def test_300_module_reclassification_by_name( ):
168 ''' Module reclassification works with module name. '''
169 module = cache_import_module( MODULE_QNAME )
170 from types import ModuleType
171 test_module = ModuleType( f"{PACKAGE_NAME}.test" )
172 test_module.__package__ = PACKAGE_NAME
173 from sys import modules
174 modules[ test_module.__name__ ] = test_module
175 module.reclassify_modules( test_module.__name__ )
176 assert isinstance( test_module, module.ImmutableModule )
177 with pytest.raises( AttributeError ):
178 test_module.new_attr = 42
181def test_301_module_reclassification_by_object( ):
182 ''' Module reclassification works with module object. '''
183 module = cache_import_module( MODULE_QNAME )
184 from types import ModuleType
185 test_module = ModuleType( f"{PACKAGE_NAME}.test" )
186 test_module.__package__ = PACKAGE_NAME
187 module.reclassify_modules( test_module )
188 assert isinstance( test_module, module.ImmutableModule )
189 with pytest.raises( AttributeError ):
190 test_module.new_attr = 42
193def test_302_recursive_module_reclassification( ):
194 ''' Recursive module reclassification works. '''
195 module = cache_import_module( MODULE_QNAME )
196 from types import ModuleType
197 root = ModuleType( f"{PACKAGE_NAME}.test" )
198 root.__package__ = PACKAGE_NAME
199 sub1 = ModuleType( f"{PACKAGE_NAME}.test.sub1" )
200 sub2 = ModuleType( f"{PACKAGE_NAME}.test.sub2" )
201 root.sub1 = sub1
202 root.sub2 = sub2
203 module.reclassify_modules( root, recursive = True )
204 assert isinstance( root, module.ImmutableModule )
205 assert isinstance( sub1, module.ImmutableModule )
206 assert isinstance( sub2, module.ImmutableModule )
207 with pytest.raises( AttributeError ):
208 root.new_attr = 42
209 with pytest.raises( AttributeError ):
210 sub1.new_attr = 42
211 with pytest.raises( AttributeError ):
212 sub2.new_attr = 42
215def test_303_module_reclassification_respects_package( ):
216 ''' Module reclassification only affects package modules. '''
217 module = cache_import_module( MODULE_QNAME )
218 from types import ModuleType
219 root = ModuleType( f"{PACKAGE_NAME}.test" )
220 root.__package__ = PACKAGE_NAME
221 external = ModuleType( "other_package.module" )
222 other_pkg = ModuleType( "other_package" )
223 root.external = external
224 root.other_pkg = other_pkg
225 module.reclassify_modules( root, recursive = True )
226 assert isinstance( root, module.ImmutableModule )
227 assert not isinstance( external, module.ImmutableModule )
228 assert not isinstance( other_pkg, module.ImmutableModule )
229 with pytest.raises( AttributeError ):
230 root.new_attr = 42
231 external.new_attr = 42 # Should work
232 assert 42 == external.new_attr
235def test_304_module_reclassification_by_dict( ):
236 ''' Module reclassification works with attribute dictionary. '''
237 module = cache_import_module( MODULE_QNAME )
238 from types import ModuleType
239 m1 = ModuleType( f"{PACKAGE_NAME}.test1" )
240 m2 = ModuleType( f"{PACKAGE_NAME}.test2" )
241 m3 = ModuleType( "other.module" )
242 attrs = {
243 '__package__': PACKAGE_NAME,
244 'module1': m1,
245 'module2': m2,
246 'external': m3,
247 'other': 42,
248 }
249 module.reclassify_modules( attrs )
250 assert isinstance( m1, module.ImmutableModule )
251 assert isinstance( m2, module.ImmutableModule )
252 assert not isinstance( m3, module.ImmutableModule )
253 with pytest.raises( AttributeError ):
254 m1.new_attr = 42
255 with pytest.raises( AttributeError ):
256 m2.new_attr = 42
257 m3.new_attr = 42 # Should work
260def test_305_module_reclassification_requires_package( ):
261 ''' Module reclassification requires package name. '''
262 module = cache_import_module( MODULE_QNAME )
263 from types import ModuleType
264 m1 = ModuleType( f"{PACKAGE_NAME}.test1" )
265 attrs = { 'module1': m1 }
266 module.reclassify_modules( attrs )
267 assert not isinstance( m1, module.ImmutableModule )
268 m1.new_attr = 42
269 assert 42 == m1.new_attr
272def test_306_module_attribute_operations( ):
273 ''' Module prevents attribute deletion and modification. '''
274 module = cache_import_module( MODULE_QNAME )
275 from types import ModuleType
276 test_module = ModuleType( f"{PACKAGE_NAME}.test" )
277 test_module.__package__ = PACKAGE_NAME
278 test_module.existing = 42
279 module.reclassify_modules( test_module )
280 with pytest.raises( AttributeError ) as exc_info:
281 del test_module.existing
282 assert "Cannot delete attribute 'existing'" in str( exc_info.value )
283 assert test_module.__name__ in str( exc_info.value )
284 with pytest.raises( AttributeError ) as exc_info:
285 test_module.existing = 24
286 assert "Cannot assign attribute 'existing'" in str( exc_info.value )
287 assert test_module.__name__ in str( exc_info.value )
288 assert 42 == test_module.existing
290# pylint: enable=no-member
293def test_400_immutable_object_init( ):
294 ''' Object prevents modification after initialization. '''
295 module = cache_import_module( MODULE_QNAME )
297 class Example( module.ImmutableObject ):
298 def __init__( self ):
299 super( module.ImmutableObject, self ).__setattr__( 'value', 42 )
300 super( ).__init__( )
302 obj = Example( )
303 with pytest.raises( AttributeError ): obj.value = 24
304 with pytest.raises( AttributeError ): obj.new_attr = 'test'
305 with pytest.raises( AttributeError ): del obj.value
308def test_500_name_calculation( ):
309 ''' Name calculation functions work correctly. '''
310 module = cache_import_module( MODULE_QNAME )
311 assert 'builtins.NoneType' == module.calculate_fqname( None )
312 assert (
313 'builtins.type'
314 == module.calculate_fqname( module.ConcealerExtension ) )
317@pytest.mark.parametrize(
318 'provided, expected',
319 (
320 ( { 'foo': 12 }, ( ) ),
321 ( { '_foo': cache_import_module }, ( ) ),
322 (
323 { 'public_func': lambda: None },
324 ( 'public_func', )
325 ),
326 )
327)
328def test_600_attribute_discovery( provided, expected ):
329 ''' Public attributes are discovered from dictionary. '''
330 module = cache_import_module( MODULE_QNAME )
331 assert expected == module.discover_public_attributes( provided )