Coverage for tests/test_000_frigid/test_500_dictionaries.py: 100%
313 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 dictionaries. '''
23# mypy: ignore-errors
24# pylint: disable=attribute-defined-outside-init
25# pylint: disable=invalid-name,magic-value-comparison
26# pylint: disable=pointless-statement,protected-access
27# pylint: disable=too-many-locals,too-many-statements,unnecessary-dunder-call
28# pylint: disable=unused-argument,unused-variable
31import pytest
33from itertools import product
34from types import MappingProxyType as DictionaryProxy
36from . import (
37 MODULES_QNAMES,
38 PACKAGE_NAME,
39 cache_import_module,
40)
43THESE_MODULE_QNAMES = tuple(
44 name for name in MODULES_QNAMES if name.endswith( '.dictionaries' ) )
45THESE_CLASSES_NAMES = ( 'Dictionary', 'ValidatorDictionary' )
46VALIDATOR_NAMES = ( 'ValidatorDictionary', )
48base = cache_import_module( f"{PACKAGE_NAME}.__" )
49exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" )
52def select_arguments( class_name ):
53 ''' Chooses initializer arguments depending on class. '''
54 if class_name in VALIDATOR_NAMES:
55 return ( lambda k, v: isinstance( v, int ), ), { }
56 return ( ), { }
59def select_simple_arguments( class_name ):
60 ''' Choose simple test arguments depending on class. '''
61 posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': True } )
62 nomargs = DictionaryProxy( dict( orb = False ) )
63 if class_name in VALIDATOR_NAMES:
64 posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': 42 } )
65 nomargs = DictionaryProxy( dict( orb = 84 ) )
66 return posargs, nomargs
69@pytest.mark.parametrize(
70 'module_qname, class_name',
71 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
72)
73def test_100_instantiation( module_qname, class_name ):
74 ''' Class instantiates. '''
75 module = cache_import_module( module_qname )
76 factory = getattr( module, class_name )
77 posargs, nomargs = select_arguments( class_name )
78 dct = factory( *posargs, **nomargs )
79 assert isinstance( dct, factory )
80 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
81 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
82 assert isinstance( dct, factory )
83 assert 1 == dct[ 'foo' ]
84 assert 2 == dct[ 'bar' ]
85 assert (
86 dct[ 'unicorn' ] if class_name == 'Dictionary'
87 else dct[ 'unicorn' ] == 42 )
88 assert (
89 not dct[ 'orb' ] if class_name == 'Dictionary'
90 else dct[ 'orb' ] == 84 )
91 assert ( 'bar', 'foo', 'orb', 'unicorn' ) == tuple( sorted( dct.keys( ) ) )
94@pytest.mark.parametrize(
95 'module_qname, class_name',
96 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES )
97)
98def test_102_instantiation( module_qname, class_name ):
99 ''' Validator class instantiates. '''
100 module = cache_import_module( module_qname )
101 factory = getattr( module, class_name )
102 posargs, nomargs = select_arguments( class_name )
103 dct = factory( *posargs, **nomargs )
104 assert isinstance( dct, factory )
105 with pytest.raises( exceptions.EntryValidityError ):
106 dct = factory( *posargs, invalid = 'str' )
109@pytest.mark.parametrize(
110 'module_qname, class_name',
111 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
112)
113def test_110_attribute_immutability( module_qname, class_name ):
114 ''' Dictionary attributes are immutable. '''
115 module = cache_import_module( module_qname )
116 factory = getattr( module, class_name )
117 posargs, nomargs = select_arguments( class_name )
118 obj = factory( *posargs, **nomargs )
119 with pytest.raises( AttributeError ):
120 obj.attr = 42
123@pytest.mark.parametrize(
124 'module_qname, class_name',
125 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
126)
127def test_200_dictionary_entry_immutability( module_qname, class_name ):
128 ''' Dictionary entries are immutable. '''
129 module = cache_import_module( module_qname )
130 factory = getattr( module, class_name )
131 posargs, nomargs = select_arguments( class_name )
132 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
133 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
134 with pytest.raises( exceptions.EntryImmutabilityError ):
135 del dct[ 'foo' ]
136 with pytest.raises( exceptions.EntryImmutabilityError ):
137 dct[ 'foo' ] = 666
138 with pytest.raises( exceptions.EntryImmutabilityError ):
139 dct[ 'baz' ] = 43
142@pytest.mark.parametrize(
143 'module_qname, class_name',
144 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
145)
146def test_160_or_combines_dictionaries( module_qname, class_name ):
147 ''' Dictionary union produces new dictionary with combined entries. '''
148 module = cache_import_module( module_qname )
149 factory = getattr( module, class_name )
150 posargs, nomargs = select_arguments( class_name )
151 # Test union of non-overlapping dictionaries
152 d1 = factory( *posargs, a = 1 )
153 d2 = factory( *posargs, b = 2 )
154 d3 = { 'c': 3, 'd': 4 }
155 # Union with another immutable dictionary
156 d4 = d1 | d2
157 assert isinstance( d4, factory )
158 assert d4 == { 'a': 1, 'b': 2 }
159 # Union with regular dict
160 d5 = d1 | d3
161 assert isinstance( d5, factory )
162 assert d5 == { 'a': 1, 'c': 3, 'd': 4 }
163 # Reverse union with regular dict
164 d6 = d3 | d1
165 assert isinstance( d6, factory )
166 assert d6 == { 'a': 1, 'c': 3, 'd': 4 }
169@pytest.mark.parametrize(
170 'module_qname, class_name',
171 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
172)
173def test_161_or_rejects_invalid_operands( module_qname, class_name ):
174 ''' Dictionary union rejects non-mapping operands. '''
175 module = cache_import_module( module_qname )
176 factory = getattr( module, class_name )
177 posargs, nomargs = select_arguments( class_name )
178 dct = factory( *posargs, **nomargs )
179 assert NotImplemented == dct.__or__( [ ] )
180 assert NotImplemented == dct.__ror__( [ ] )
183@pytest.mark.parametrize(
184 'module_qname, class_name',
185 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
186)
187def test_162_or_prevents_key_conflicts( module_qname, class_name ):
188 ''' Dictionary union raises error on key conflicts. '''
189 module = cache_import_module( module_qname )
190 factory = getattr( module, class_name )
191 posargs, nomargs = select_arguments( class_name )
192 d1 = factory( *posargs, conflict_key = 1, unique1 = 2 )
193 d2 = factory( *posargs, conflict_key = 3, unique2 = 4 )
194 d3 = { 'conflict_key': 2, 'unique3': 5 }
195 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo:
196 d1 | d2
197 assert "entry for 'conflict_key'" in str( excinfo.value )
198 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo:
199 d1 | d3
200 assert "entry for 'conflict_key'" in str( excinfo.value )
201 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo:
202 d3 | d1
203 assert "entry for 'conflict_key'" in str( excinfo.value )
206@pytest.mark.parametrize(
207 'module_qname, class_name',
208 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
209)
210def test_170_and_intersects_mappings( module_qname, class_name ):
211 ''' Dictionary intersection with mapping matches key-value pairs. '''
212 module = cache_import_module( module_qname )
213 factory = getattr( module, class_name )
214 posargs, nomargs = select_arguments( class_name )
215 d1 = factory( *posargs, a = 1, b = 2, c = 3 )
216 d2 = factory( *posargs, a = 1, b = 3, d = 4 )
217 d3 = { 'a': 1, 'c': 3, 'e': 5 }
218 d4 = d1 & d2
219 assert isinstance( d4, factory )
220 assert d4 == { 'a': 1 }
221 d5 = d1 & d3
222 assert isinstance( d5, factory )
223 assert d5 == { 'a': 1, 'c': 3 }
224 d6 = d3 & d1
225 assert isinstance( d6, factory )
226 assert d6 == { 'a': 1, 'c': 3 }
229@pytest.mark.parametrize(
230 'module_qname, class_name',
231 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
232)
233def test_171_and_filters_by_keys( module_qname, class_name ):
234 ''' Dictionary intersection with set filters by keys. '''
235 module = cache_import_module( module_qname )
236 factory = getattr( module, class_name )
237 posargs, nomargs = select_arguments( class_name )
238 d1 = factory( *posargs, a = 1, b = 2, c = 3 )
239 s1 = { 'a', 'b' }
240 d2 = d1 & s1
241 assert isinstance( d2, factory )
242 assert d2 == { 'a': 1, 'b': 2 }
243 d3 = d1 & factory( *posargs, x = 0, a = 9, b = 8 ).keys( )
244 assert isinstance( d3, factory )
245 assert d3 == { 'a': 1, 'b': 2 }
246 d4 = s1 & d1
247 assert isinstance( d4, factory )
248 assert d4 == { 'a': 1, 'b': 2 }
251@pytest.mark.parametrize(
252 'module_qname, class_name',
253 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
254)
255def test_172_and_rejects_invalid_operands( module_qname, class_name ):
256 ''' Dictionary intersection rejects invalid operands. '''
257 module = cache_import_module( module_qname )
258 factory = getattr( module, class_name )
259 posargs, nomargs = select_arguments( class_name )
260 dct = factory( *posargs, **nomargs )
261 assert NotImplemented == dct.__and__( [ ] )
262 assert NotImplemented == dct.__rand__( [ ] )
265@pytest.mark.parametrize(
266 'module_qname, class_name',
267 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES )
268)
269def test_202_validator_dictionary_validation( module_qname, class_name ):
270 ''' Validator dictionary validates entries during creation. '''
271 module = cache_import_module( module_qname )
272 factory = getattr( module, class_name )
273 posargs, nomargs = select_arguments( class_name )
274 with pytest.raises( exceptions.EntryValidityError ):
275 factory( *posargs, invalid = 'str' )
278@pytest.mark.parametrize(
279 'module_qname, class_name',
280 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES )
281)
282def test_203_validator_dictionary_generator_handling(
283 module_qname, class_name
284):
285 ''' Validator dictionary properly handles generator inputs. '''
286 module = cache_import_module( module_qname )
287 factory = getattr( module, class_name )
289 def int_validator( k, v ):
290 return isinstance( v, int )
292 gen = ( ( str( i ), i ) for i in range( 3 ) )
293 dct = factory( int_validator, gen )
294 assert dct == { '0': 0, '1': 1, '2': 2 }
295 gen = ( ( f'g{i}', i ) for i in range( 2 ) )
296 dct = factory(
297 int_validator,
298 gen,
299 { 'm1': 10, 'm2': 20 },
300 k1 = 100,
301 k2 = 200,
302 )
303 assert dct == {
304 'g0': 0, 'g1': 1,
305 'm1': 10, 'm2': 20,
306 'k1': 100, 'k2': 200,
307 }
308 gen = ( ( str( i ), 'bad' if i == 1 else i ) for i in range( 3 ) )
309 with pytest.raises( exceptions.EntryValidityError ):
310 factory( int_validator, gen )
313@pytest.mark.parametrize(
314 'module_qname, class_name',
315 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES )
316)
317def test_204_validator_dictionary_operations_preserve_validation(
318 module_qname, class_name
319):
320 ''' Dictionary operations maintain validation rules. '''
321 module = cache_import_module( module_qname )
322 factory = getattr( module, class_name )
324 def int_validator( k, v ):
325 return isinstance( v, int )
327 d1 = factory( int_validator, a = 1, b = 2 )
328 d2 = factory( int_validator, c = 3, d = 4 ) # No overlapping keys with d1
329 d3 = { 'e': 5, 'f': 6 } # No overlapping keys with d1
330 d4 = d1 & d2
331 assert isinstance( d4, factory )
332 assert d4._validator_ is int_validator
333 assert d4 == { } # No common key-value pairs
334 d5 = d1 & d3
335 assert isinstance( d5, factory )
336 assert d5._validator_ is int_validator
337 assert d5 == { } # No common key-value pairs
338 d6 = d1 | d2
339 assert isinstance( d6, factory )
340 assert d6._validator_ is int_validator
341 assert d6 == { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }
342 d7 = d1.with_data( x = 10, y = 20 )
343 assert isinstance( d7, factory )
344 assert d7._validator_ is int_validator
345 assert d7 == { 'x': 10, 'y': 20 }
346 d8 = d1.copy( )
347 assert isinstance( d8, factory )
348 assert d8._validator_ is int_validator
349 assert d8 == d1
353@pytest.mark.parametrize(
354 'module_qname, class_name',
355 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES )
356)
357def test_205_validator_dictionary_complex_validation(
358 module_qname, class_name
359):
360 ''' Validator dictionary handles complex validation rules. '''
361 module = cache_import_module( module_qname )
362 factory = getattr( module, class_name )
364 def complex_validator( k, v ):
365 return (
366 isinstance( k, str )
367 and isinstance( v, int )
368 and len( k ) == v
369 )
371 d1 = factory( complex_validator, a = 1, bb = 2, ccc = 3 )
372 assert d1 == { 'a': 1, 'bb': 2, 'ccc': 3 }
373 with pytest.raises( exceptions.EntryValidityError ):
374 factory( complex_validator, a = 2 ) # Value doesn't match key length
375 with pytest.raises( exceptions.EntryValidityError ):
376 factory( complex_validator, bb = 'x' ) # Value not int
377 with pytest.raises( exceptions.EntryValidityError ):
378 factory( complex_validator, { 123: 3 } ) # Key not string
379 d2 = factory( complex_validator, bb = 2, xxx = 3 )
380 d3 = d1 & d2
381 assert d3 == { 'bb': 2 }
382 d4 = d1.with_data( zz = 2 )
383 assert d4 == { 'zz': 2 }
386@pytest.mark.parametrize(
387 'module_qname, class_name',
388 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
389)
390def test_220_duplication( module_qname, class_name ):
391 ''' Dictionary is duplicable. '''
392 module = cache_import_module( module_qname )
393 factory = getattr( module, class_name )
394 posargs, nomargs = select_arguments( class_name )
395 odct = factory( *posargs, a = 1, b = 2 )
396 ddct = odct.copy( )
397 assert odct == ddct
398 assert odct is not ddct
401@pytest.mark.parametrize(
402 'module_qname, class_name',
403 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
404)
405def test_221_dictionary_iterability( module_qname, class_name ):
406 ''' Dictionary is iterable. '''
407 module = cache_import_module( module_qname )
408 factory = getattr( module, class_name )
409 posargs, nomargs = select_arguments( class_name )
410 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
411 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
412 assert frozenset( dct.keys( ) ) == frozenset( iter( dct ) )
413 assert tuple( dct.items( ) ) == tuple( zip( dct.keys( ), dct.values( ) ) )
416@pytest.mark.parametrize(
417 'module_qname, class_name',
418 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
419)
420def test_222_dictionary_measurability( module_qname, class_name ):
421 ''' Dictionary is measurable. '''
422 module = cache_import_module( module_qname )
423 factory = getattr( module, class_name )
424 posargs, nomargs = select_arguments( class_name )
425 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
426 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
427 assert len( dct.keys( ) ) == len( dct )
428 assert len( dct.items( ) ) == len( dct )
429 assert len( dct.values( ) ) == len( dct )
432@pytest.mark.parametrize(
433 'module_qname, class_name',
434 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
435)
436def test_225_dictionary_equality( module_qname, class_name ):
437 ''' Dictionary is equivalent to another dictionary with same values. '''
438 module = cache_import_module( module_qname )
439 factory = getattr( module, class_name )
440 posargs, nomargs = select_arguments( class_name )
441 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
442 dct1 = factory( *posargs, *simple_posargs, **simple_nomargs )
443 dct2 = dct1.copy( )
444 dct3 = dict( dct1 )
445 assert dct1 == dct2
446 assert dct2 == dct1
447 assert dct1 == dct3
448 assert dct3 == dct1
449 assert not ( dct1 == -1 ) # pylint: disable=superfluous-parens
450 assert dct1 != -1
451 assert dct1 != ( )
454@pytest.mark.parametrize(
455 'module_qname, class_name',
456 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
457)
458def test_230_string_representation( module_qname, class_name ):
459 ''' Dictionary has expected string representations. '''
460 module = cache_import_module( module_qname )
461 factory = getattr( module, class_name )
462 posargs, nomargs = select_arguments( class_name )
463 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
464 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
465 cdct = dict( dct )
466 assert str( cdct ) == str( dct )
467 assert str( cdct ) in repr( dct )
468 assert base.calculate_fqname( dct ) in repr( dct )
471@pytest.mark.parametrize(
472 'module_qname, class_name',
473 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
474)
475def test_240_dictionary_entry_optional_retrieval( module_qname, class_name ):
476 ''' Default value on optional access of dictionary entry. '''
477 module = cache_import_module( module_qname )
478 factory = getattr( module, class_name )
479 posargs, nomargs = select_arguments( class_name )
480 simple_posargs, simple_nomargs = select_simple_arguments( class_name )
481 dct = factory( *posargs, *simple_posargs, **simple_nomargs )
482 assert None is dct.get( 'baz' )
483 assert -1 == dct.get( 'baz', -1 )
484 assert -1 == dct.get( 'baz', default = -1 )
485 assert 1 == dct.get( 'foo' )
486 assert 1 == dct.get( 'foo', -1 )
489@pytest.mark.parametrize(
490 'module_qname, class_name',
491 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
492)
493def test_250_with_data( module_qname, class_name ):
494 ''' Dictionary creates new instance with different data. '''
495 module = cache_import_module( module_qname )
496 factory = getattr( module, class_name )
497 posargs, nomargs = select_arguments( class_name )
498 d1 = factory( *posargs, a = 1, b = 2 )
499 new_data = { 'c': 3, 'd': 4 }
500 d2 = d1.with_data( new_data )
501 assert isinstance( d2, factory )
502 assert type( d1 ) is type( d2 )
503 assert d1 != d2
504 assert d2 == { 'c': 3, 'd': 4 }
505 if class_name in VALIDATOR_NAMES:
506 with pytest.raises( exceptions.EntryValidityError ):
507 d2 = d1.with_data( invalid = 'str' )
510@pytest.mark.parametrize(
511 'module_qname, class_name',
512 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
513)
514def test_260_subclasses_abc_dictionary( module_qname, class_name ):
515 ''' Subclasses 'collections.abc.Mapping'. '''
516 from collections.abc import Mapping as AbstractDictionary
517 module = cache_import_module( module_qname )
518 factory = getattr( module, class_name )
519 assert issubclass( factory, AbstractDictionary )
522@pytest.mark.parametrize(
523 'module_qname, class_name',
524 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES )
525)
526def test_900_docstring_sanity( module_qname, class_name ):
527 ''' Class has valid docstring. '''
528 module = cache_import_module( module_qname )
529 factory = getattr( module, class_name )
530 assert hasattr( factory, '__doc__' )
531 assert isinstance( factory.__doc__, str )
532 assert factory.__doc__