Coverage for sources/accretive/dictionaries.py: 100%
187 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 01:33 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 01:33 +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# pylint: disable=line-too-long
22''' Accretive dictionaries.
24Dictionaries which can grow but never shrink. Once an entry is added, it cannot
25be modified or removed.
27* :py:class:`AbstractDictionary`:
28 Base class defining the accretive dictionary interface. Implementations must
29 provide ``__getitem__``, ``__iter__``, ``__len__``, and storage methods.
31* :py:class:`Dictionary`:
32 Standard implementation of an accretive dictionary. Supports all usual dict
33 operations except those that would modify or remove existing entries.
35* :py:class:`ProducerDictionary`:
36 Automatically generates values for missing keys using a supplied factory
37 function. Similar to :py:class:`collections.defaultdict` but with accretive
38 behavior.
40* :py:class:`ValidatorDictionary`:
41 Validates entries before addition using a supplied predicate function.
43* :py:class:`ProducerValidatorDictionary`:
44 Combines producer and validator behaviors. Generated values must pass
45 validation before being added.
47>>> from accretive import Dictionary
48>>> d = Dictionary( apples = 12, bananas = 6 )
49>>> d[ 'cherries' ] = 42 # Add new entry
50>>> d[ 'apples' ] = 14 # Attempt modification
51Traceback (most recent call last):
52 ...
53accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'apples'.
54>>> del d[ 'bananas' ] # Attempt removal
55Traceback (most recent call last):
56 ...
57accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'bananas'.
59>>> from accretive import ProducerDictionary
60>>> d = ProducerDictionary( list ) # list() called for missing keys
61>>> d[ 'new' ]
62[]
63>>> d[ 'new' ].append( 1 ) # List is mutable, but entry is fixed
64>>> d[ 'new' ] = [ ] # Attempt modification
65Traceback (most recent call last):
66 ...
67accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'new'.
69>>> from accretive import ValidatorDictionary
70>>> d = ValidatorDictionary( lambda k, v: isinstance( v, int ) )
71>>> d[ 'valid' ] = 42 # Passes validation
72>>> d[ 'invalid' ] = 'str' # Fails validation
73Traceback (most recent call last):
74 ...
75accretive.exceptions.EntryValidityError: Cannot add invalid entry with key, 'invalid', and value, 'str', to dictionary.
76'''
77# pylint: enable=line-too-long
80from . import __
81from . import classes as _classes
82from . import objects as _objects
85class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
86 ''' Abstract base class for dictionaries that can grow but not shrink.
88 An accretive dictionary allows new entries to be added but prevents
89 modification or removal of existing entries. This provides a middle
90 ground between immutable and fully mutable mappings.
92 Implementations must provide:
93 - __getitem__, __iter__, __len__
94 - _pre_setitem_ for entry validation/preparation
95 - _store_item_ for storage implementation
96 '''
98 @__.abstract_member_function
99 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
100 raise NotImplementedError # pragma: no coverage
102 @__.abstract_member_function
103 def __len__( self ) -> int:
104 raise NotImplementedError # pragma: no coverage
106 @__.abstract_member_function
107 def __getitem__( self, key: __.H ) -> __.V:
108 raise NotImplementedError # pragma: no coverage
110 def _pre_setitem_( # pylint: disable=no-self-use
111 self, key: __.H, value: __.V
112 ) -> tuple[ __.H, __.V ]:
113 ''' Validates and/or prepares entry before addition.
115 Should raise appropriate exception if entry is invalid.
116 '''
117 return key, value
119 @__.abstract_member_function
120 def _store_item_( self, key: __.H, value: __.V ) -> None:
121 ''' Stores entry in underlying storage. '''
122 raise NotImplementedError # pragma: no coverage
124 def __setitem__( self, key: __.H, value: __.V ) -> None:
125 key, value = self._pre_setitem_( key, value )
126 if key in self:
127 from .exceptions import EntryImmutabilityError
128 raise EntryImmutabilityError( key )
129 self._store_item_( key, value )
131 def __delitem__( self, key: __.H ) -> None:
132 from .exceptions import EntryImmutabilityError
133 raise EntryImmutabilityError( key )
135 def setdefault( self, key: __.H, default: __.V ) -> __.V:
136 ''' Returns value for key, setting it to default if missing. '''
137 try: return self[ key ]
138 except KeyError:
139 self[ key ] = default
140 return default
142 def update(
143 self,
144 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
145 **entries: __.DictionaryNominativeArgument[ __.V ],
146 ) -> __.a.Self:
147 ''' Adds new entries as a batch. Returns self. '''
148 from itertools import chain
149 updates: list[ tuple[ __.H, __.V ] ] = [ ]
150 for indicator, value in chain.from_iterable( map( # type: ignore
151 lambda element: ( # type: ignore
152 element.items( )
153 if isinstance( element, __.cabc.Mapping )
154 else element
155 ),
156 ( *iterables, entries )
157 ) ):
158 indicator_, value_ = (
159 self._pre_setitem_( indicator, value ) ) # type: ignore
160 if indicator_ in self:
161 from .exceptions import EntryImmutabilityError
162 raise EntryImmutabilityError( indicator_ )
163 updates.append( ( indicator_, value_ ) )
164 for indicator, value in updates: self._store_item_( indicator, value )
165 return self
168class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
169 ''' Mix-in providing additional dictionary operations. '''
171 def __init__( self, *posargs: __.a.Any, **nomargs: __.a.Any ) -> None:
172 super( ).__init__( *posargs, **nomargs )
174 def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.a.Self:
175 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
176 result = self.copy( )
177 result.update( other )
178 return result
180 def __ror__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.a.Self:
181 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
182 return self | other
184 def __and__(
185 self,
186 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
187 ) -> __.a.Self:
188 if isinstance( other, __.cabc.Mapping ):
189 return self.with_data(
190 ( key, value ) for key, value in self.items( )
191 if key in other and other[ key ] == value )
192 if isinstance( other, ( __.cabc.Set, __.cabc.KeysView ) ):
193 return self.with_data(
194 ( key, self[ key ] ) for key in self.keys( ) & other )
195 return NotImplemented
197 def __rand__(
198 self,
199 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
200 ) -> __.a.Self:
201 if not isinstance(
202 other, ( __.cabc.Mapping, __.cabc.Set, __.cabc.KeysView )
203 ): return NotImplemented
204 return self & other
206 @__.abstract_member_function
207 def copy( self ) -> __.a.Self:
208 ''' Provides fresh copy of dictionary. '''
209 raise NotImplementedError # pragma: no coverage
211 @__.abstract_member_function
212 def with_data(
213 self,
214 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
215 **entries: __.DictionaryNominativeArgument[ __.V ],
216 ) -> __.a.Self:
217 ''' Creates new dictionary with same behavior but different data. '''
218 raise NotImplementedError # pragma: no coverage
222class _Dictionary(
223 __.CoreDictionary[ __.H, __.V ], metaclass = _classes.Class
224): pass
227class Dictionary( # pylint: disable=eq-without-hash
228 _objects.Object, _DictionaryOperations[ __.H, __.V ]
229):
230 ''' Accretive dictionary. '''
231 # TODO: version 3.0: Do not subclass from 'Object'.
233 __slots__ = ( '_data_', )
235 _data_: _Dictionary[ __.H, __.V ]
237 def __init__(
238 self,
239 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
240 **entries: __.DictionaryNominativeArgument[ __.V ],
241 ) -> None:
242 self._data_ = _Dictionary( *iterables, **entries )
243 super( ).__init__( )
245 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
246 return iter( self._data_ )
248 def __len__( self ) -> int:
249 return len( self._data_ )
251 def __repr__( self ) -> str:
252 return "{fqname}( {contents} )".format(
253 fqname = __.calculate_fqname( self ),
254 contents = str( self._data_ ) )
256 def __str__( self ) -> str:
257 return str( self._data_ )
259 def __contains__( self, key: __.a.Any ) -> bool:
260 return key in self._data_
262 def __getitem__( self, key: __.H ) -> __.V:
263 return self._data_[ key ]
265 def __eq__( self, other: __.a.Any ) -> __.ComparisonResult:
266 if isinstance( other, __.cabc.Mapping ):
267 return self._data_ == other
268 return NotImplemented
270 def __ne__( self, other: __.a.Any ) -> __.ComparisonResult:
271 if isinstance( other, __.cabc.Mapping ):
272 return self._data_ != other
273 return NotImplemented
275 def copy( self ) -> __.a.Self:
276 ''' Provides fresh copy of dictionary. '''
277 return type( self )( self )
279 def get(
280 self, key: __.H, default: __.Optional[ __.V ] = __.absent
281 ) -> __.a.Annotation[
282 __.V,
283 __.a.Doc(
284 'Value of entry, if it exists. '
285 'Else, supplied default value or ``None``.' )
286 ]:
287 ''' Retrieves entry associated with key, if it exists. '''
288 if __.is_absent( default ):
289 return self._data_.get( key ) # type: ignore
290 return self._data_.get( key, default )
292 def keys( self ) -> __.cabc.KeysView[ __.H ]:
293 ''' Provides iterable view over dictionary keys. '''
294 return self._data_.keys( )
296 def items( self ) -> __.cabc.ItemsView[ __.H, __.V ]:
297 ''' Provides iterable view over dictionary items. '''
298 return self._data_.items( )
300 def values( self ) -> __.cabc.ValuesView[ __.V ]:
301 ''' Provides iterable view over dictionary values. '''
302 return self._data_.values( )
304 def with_data(
305 self,
306 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
307 **entries: __.DictionaryNominativeArgument[ __.V ],
308 ) -> __.a.Self:
309 return type( self )( *iterables, **entries )
311 def _store_item_( self, key: __.H, value: __.V ) -> None:
312 self._data_[ key ] = value
314Dictionary.__doc__ = __.generate_docstring(
315 Dictionary, 'dictionary entries accretion' )
316# Register as subclass of Mapping rather than use it as mixin.
317# We directly implement, for the sake of efficiency, the methods which the
318# mixin would provide.
319__.cabc.Mapping.register( Dictionary ) # type: ignore
322class ProducerDictionary( Dictionary[ __.H, __.V ] ):
323 ''' Accretive dictionary with default value for missing entries. '''
325 __slots__ = ( '_producer_', )
327 _producer_: __.DictionaryProducer[ __.V ]
329 def __init__(
330 self,
331 producer: __.DictionaryProducer[ __.V ],
332 /,
333 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
334 **entries: __.DictionaryNominativeArgument[ __.V ],
335 ):
336 # TODO: Validate producer argument.
337 self._producer_ = producer
338 super( ).__init__( *iterables, **entries )
340 def __repr__( self ) -> str:
341 return "{fqname}( {producer}, {contents} )".format(
342 fqname = __.calculate_fqname( self ),
343 producer = self._producer_,
344 contents = str( self._data_ ) )
346 def __getitem__( self, key: __.H ) -> __.V:
347 if key not in self:
348 value = self._producer_( )
349 self[ key ] = value
350 else: value = super( ).__getitem__( key )
351 return value
353 def copy( self ) -> __.a.Self:
354 ''' Provides fresh copy of dictionary. '''
355 dictionary = type( self )( self._producer_ )
356 return dictionary.update( self )
358 def setdefault( self, key: __.H, default: __.V ) -> __.V:
359 ''' Returns value for key, setting it to default if missing. '''
360 if key not in self:
361 self[ key ] = default
362 return default
363 return self[ key ]
365 def with_data(
366 self,
367 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
368 **entries: __.DictionaryNominativeArgument[ __.V ],
369 ) -> __.a.Self:
370 return type( self )( self._producer_, *iterables, **entries )
372ProducerDictionary.__doc__ = __.generate_docstring(
373 ProducerDictionary,
374 'dictionary entries accretion',
375 'dictionary entries production',
376)
379class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
380 ''' Accretive dictionary with validation of new entries. '''
382 __slots__ = ( '_validator_', )
384 _validator_: __.DictionaryValidator[ __.H, __.V ]
386 def __init__(
387 self,
388 validator: __.DictionaryValidator[ __.H, __.V ],
389 /,
390 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
391 **entries: __.DictionaryNominativeArgument[ __.V ],
392 ) -> None:
393 self._validator_ = validator
394 super( ).__init__( *iterables, **entries )
396 def __repr__( self ) -> str:
397 return "{fqname}( {validator}, {contents} )".format(
398 fqname = __.calculate_fqname( self ),
399 validator = self._validator_,
400 contents = str( self._data_ ) )
402 def _pre_setitem_( self, key: __.H, value: __.V ) -> tuple[ __.H, __.V ]:
403 if not self._validator_( key, value ):
404 from .exceptions import EntryValidityError
405 raise EntryValidityError( key, value )
406 return key, value
408 def copy( self ) -> __.a.Self:
409 ''' Provides fresh copy of dictionary. '''
410 dictionary = type( self )( self._validator_ )
411 return dictionary.update( self )
413 def with_data(
414 self,
415 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
416 **entries: __.DictionaryNominativeArgument[ __.V ],
417 ) -> __.a.Self:
418 return type( self )( self._validator_, *iterables, **entries )
420ValidatorDictionary.__doc__ = __.generate_docstring(
421 ValidatorDictionary,
422 'dictionary entries accretion',
423 'dictionary entries validation',
424)
427class ProducerValidatorDictionary( Dictionary[ __.H, __.V ] ):
428 ''' Accretive dictionary with defaults and validation. '''
430 __slots__ = ( '_producer_', '_validator_' )
432 _producer_: __.DictionaryProducer[ __.V ]
433 _validator_: __.DictionaryValidator[ __.H, __.V ]
435 def __init__(
436 self,
437 producer: __.DictionaryProducer[ __.V ],
438 validator: __.DictionaryValidator[ __.H, __.V ],
439 /,
440 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
441 **entries: __.DictionaryNominativeArgument[ __.V ],
442 ) -> None:
443 self._producer_ = producer
444 self._validator_ = validator
445 super( ).__init__( *iterables, **entries )
447 def __repr__( self ) -> str:
448 return "{fqname}( {producer}, {validator}, {contents} )".format(
449 fqname = __.calculate_fqname( self ),
450 producer = self._producer_,
451 validator = self._validator_,
452 contents = str( self._data_ ) )
454 def __getitem__( self, key: __.H ) -> __.V:
455 if key not in self:
456 value = self._producer_( )
457 if not self._validator_( key, value ):
458 from .exceptions import EntryValidityError
459 raise EntryValidityError( key, value )
460 self[ key ] = value
461 else: value = super( ).__getitem__( key )
462 return value
464 def _pre_setitem_( self, key: __.H, value: __.V ) -> tuple[ __.H, __.V ]:
465 if not self._validator_( key, value ):
466 from .exceptions import EntryValidityError
467 raise EntryValidityError( key, value )
468 return key, value
470 def copy( self ) -> __.a.Self:
471 ''' Provides fresh copy of dictionary. '''
472 dictionary = type( self )( self._producer_, self._validator_ )
473 return dictionary.update( self )
475 def setdefault( self, key: __.H, default: __.V ) -> __.V:
476 ''' Returns value for key, setting it to default if missing. '''
477 if key not in self:
478 self[ key ] = default
479 return default
480 return self[ key ]
482 def with_data(
483 self,
484 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
485 **entries: __.DictionaryNominativeArgument[ __.V ],
486 ) -> __.a.Self:
487 return type( self )(
488 self._producer_, self._validator_, *iterables, **entries )
490ProducerValidatorDictionary.__doc__ = __.generate_docstring(
491 ProducerValidatorDictionary,
492 'dictionary entries accretion',
493 'dictionary entries production',
494 'dictionary entries validation',
495)