Coverage for sources/accretive/dictionaries.py: 100%
189 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:40 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:40 +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.
24 Dictionaries which can grow but never shrink. Once an entry is added, it
25 cannot be modified or removed.
27 * :py:class:`AbstractDictionary`:
28 Base class defining the accretive dictionary interface. Implementations
29 must provide ``__getitem__``, ``__iter__``, ``__len__``, and storage
30 methods.
32 * :py:class:`Dictionary`:
33 Standard implementation of an accretive dictionary. Supports all usual
34 dict operations except those that would modify or remove existing
35 entries.
37 * :py:class:`ProducerDictionary`:
38 Automatically generates values for missing keys using a supplied factory
39 function. Similar to :py:class:`collections.defaultdict` but with
40 accretive behavior.
42 * :py:class:`ValidatorDictionary`:
43 Validates entries before addition using a supplied predicate function.
45 * :py:class:`ProducerValidatorDictionary`:
46 Combines producer and validator behaviors. Generated values must pass
47 validation before being added.
49 >>> from accretive import Dictionary
50 >>> d = Dictionary( apples = 12, bananas = 6 )
51 >>> d[ 'cherries' ] = 42 # Add new entry
52 >>> d[ 'apples' ] = 14 # Attempt modification
53 Traceback (most recent call last):
54 ...
55 accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'apples'.
56 >>> del d[ 'bananas' ] # Attempt removal
57 Traceback (most recent call last):
58 ...
59 accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'bananas'.
61 >>> from accretive import ProducerDictionary
62 >>> d = ProducerDictionary( list ) # list() called for missing keys
63 >>> d[ 'new' ]
64 []
65 >>> d[ 'new' ].append( 1 ) # List is mutable, but entry is fixed
66 >>> d[ 'new' ] = [ ] # Attempt modification
67 Traceback (most recent call last):
68 ...
69 accretive.exceptions.EntryImmutabilityError: Cannot alter or remove existing entry for 'new'.
71 >>> from accretive import ValidatorDictionary
72 >>> d = ValidatorDictionary( lambda k, v: isinstance( v, int ) )
73 >>> d[ 'valid' ] = 42 # Passes validation
74 >>> d[ 'invalid' ] = 'str' # Fails validation
75 Traceback (most recent call last):
76 ...
77 accretive.exceptions.EntryValidityError: Cannot add invalid entry with key, 'invalid', and value, 'str', to dictionary.
78'''
79# pylint: enable=line-too-long
82from . import __
83from . import classes as _classes
86class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
87 ''' Abstract base class for dictionaries that can grow but not shrink.
89 An accretive dictionary allows new entries to be added but prevents
90 modification or removal of existing entries. This provides a middle
91 ground between immutable and fully mutable mappings.
93 Implementations must provide:
94 - __getitem__, __iter__, __len__
95 - _pre_setitem_ for entry validation/preparation
96 - _store_item_ for storage implementation
97 '''
99 @__.abc.abstractmethod
100 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
101 raise NotImplementedError # pragma: no coverage
103 @__.abc.abstractmethod
104 def __len__( self ) -> int:
105 raise NotImplementedError # pragma: no coverage
107 @__.abc.abstractmethod
108 def __getitem__( self, key: __.H ) -> __.V:
109 raise NotImplementedError # pragma: no coverage
111 def _pre_setitem_( # pylint: disable=no-self-use
112 self, key: __.H, value: __.V
113 ) -> tuple[ __.H, __.V ]:
114 ''' Validates and/or prepares entry before addition.
116 Should raise appropriate exception if entry is invalid.
117 '''
118 return key, value
120 @__.abc.abstractmethod
121 def _store_item_( self, key: __.H, value: __.V ) -> None:
122 ''' Stores entry in underlying storage. '''
123 raise NotImplementedError # pragma: no coverage
125 def __setitem__( self, key: __.H, value: __.V ) -> None:
126 key, value = self._pre_setitem_( key, value )
127 if key in self:
128 from .exceptions import EntryImmutabilityError
129 raise EntryImmutabilityError( key )
130 self._store_item_( key, value )
132 def __delitem__( self, key: __.H ) -> None:
133 from .exceptions import EntryImmutabilityError
134 raise EntryImmutabilityError( key )
136 def setdefault( self, key: __.H, default: __.V ) -> __.V:
137 ''' Returns value for key, setting it to default if missing. '''
138 try: return self[ key ]
139 except KeyError:
140 self[ key ] = default
141 return default
143 def update(
144 self,
145 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
146 **entries: __.DictionaryNominativeArgument[ __.V ],
147 ) -> __.typx.Self:
148 ''' Adds new entries as a batch. Returns self. '''
149 from itertools import chain
150 updates: list[ tuple[ __.H, __.V ] ] = [ ]
151 for indicator, value in chain.from_iterable( map( # type: ignore
152 lambda element: ( # type: ignore
153 element.items( )
154 if isinstance( element, __.cabc.Mapping )
155 else element
156 ),
157 ( *iterables, entries )
158 ) ):
159 indicator_, value_ = (
160 self._pre_setitem_( indicator, value ) ) # type: ignore
161 if indicator_ in self:
162 from .exceptions import EntryImmutabilityError
163 raise EntryImmutabilityError( indicator_ )
164 updates.append( ( indicator_, value_ ) )
165 for indicator, value in updates: self._store_item_( indicator, value )
166 return self
169class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
170 ''' Mix-in providing additional dictionary operations. '''
172 def __init__(
173 self, *posargs: __.typx.Any, **nomargs: __.typx.Any
174 ) -> None:
175 super( ).__init__( *posargs, **nomargs )
177 def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
178 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
179 conflicts = set( self.keys( ) ) & set( other.keys( ) )
180 if conflicts:
181 from .exceptions import EntryImmutabilityError
182 raise EntryImmutabilityError( next( iter( conflicts ) ) )
183 data = dict( self )
184 data.update( other )
185 return self.with_data( data )
187 def __ror__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
188 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
189 return self | other
191 def __and__(
192 self,
193 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
194 ) -> __.typx.Self:
195 if isinstance( other, __.cabc.Mapping ):
196 return self.with_data( # pyright: ignore
197 ( key, value ) for key, value in self.items( )
198 if key in other and other[ key ] == value )
199 if isinstance( other, ( __.cabc.Set, __.cabc.KeysView ) ):
200 return self.with_data( # pyright: ignore
201 ( key, self[ key ] ) for key in self.keys( ) & other )
202 return NotImplemented
204 def __rand__(
205 self,
206 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
207 ) -> __.typx.Self:
208 if not isinstance(
209 other, ( __.cabc.Mapping, __.cabc.Set, __.cabc.KeysView )
210 ): return NotImplemented
211 return self & other
213 @__.abc.abstractmethod
214 def copy( self ) -> __.typx.Self:
215 ''' Provides fresh copy of dictionary. '''
216 raise NotImplementedError # pragma: no coverage
218 @__.abc.abstractmethod
219 def with_data(
220 self,
221 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
222 **entries: __.DictionaryNominativeArgument[ __.V ],
223 ) -> __.typx.Self:
224 ''' Creates new dictionary with same behavior but different data. '''
225 raise NotImplementedError # pragma: no coverage
228class _Dictionary(
229 __.AccretiveDictionary[ __.H, __.V ], metaclass = _classes.Class
230): pass
233class Dictionary( # pylint: disable=eq-without-hash
234 _DictionaryOperations[ __.H, __.V ]
235):
236 ''' Accretive dictionary. '''
238 __slots__ = ( '_data_', )
240 _data_: _Dictionary[ __.H, __.V ]
242 def __init__(
243 self,
244 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
245 **entries: __.DictionaryNominativeArgument[ __.V ],
246 ) -> None:
247 self._data_ = _Dictionary( *iterables, **entries )
248 super( ).__init__( )
250 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
251 return iter( self._data_ )
253 def __len__( self ) -> int:
254 return len( self._data_ )
256 def __repr__( self ) -> str:
257 return "{fqname}( {contents} )".format(
258 fqname = __.calculate_fqname( self ),
259 contents = str( self._data_ ) )
261 def __str__( self ) -> str:
262 return str( self._data_ )
264 def __contains__( self, key: __.typx.Any ) -> bool:
265 return key in self._data_
267 def __getitem__( self, key: __.H ) -> __.V:
268 return self._data_[ key ]
270 def __eq__( self, other: __.typx.Any ) -> __.ComparisonResult:
271 if isinstance( other, __.cabc.Mapping ):
272 return self._data_ == other
273 return NotImplemented
275 def __ne__( self, other: __.typx.Any ) -> __.ComparisonResult:
276 if isinstance( other, __.cabc.Mapping ):
277 return self._data_ != other
278 return NotImplemented
280 def copy( self ) -> __.typx.Self:
281 ''' Provides fresh copy of dictionary. '''
282 return type( self )( self )
284 def get(
285 self, key: __.H, default: __.Absential[ __.V ] = __.absent
286 ) -> __.typx.Annotated[
287 __.V,
288 __.typx.Doc(
289 'Value of entry, if it exists. '
290 'Else, supplied default value or ``None``.' )
291 ]:
292 ''' Retrieves entry associated with key, if it exists. '''
293 if __.is_absent( default ):
294 return self._data_.get( key ) # pyright: ignore
295 return self._data_.get( key, default )
297 def keys( self ) -> __.cabc.KeysView[ __.H ]:
298 ''' Provides iterable view over dictionary keys. '''
299 return self._data_.keys( )
301 def items( self ) -> __.cabc.ItemsView[ __.H, __.V ]:
302 ''' Provides iterable view over dictionary items. '''
303 return self._data_.items( )
305 def values( self ) -> __.cabc.ValuesView[ __.V ]:
306 ''' Provides iterable view over dictionary values. '''
307 return self._data_.values( )
309 def with_data(
310 self,
311 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
312 **entries: __.DictionaryNominativeArgument[ __.V ],
313 ) -> __.typx.Self:
314 return type( self )( *iterables, **entries )
316 def _store_item_( self, key: __.H, value: __.V ) -> None:
317 self._data_[ key ] = value
319Dictionary.__doc__ = __.generate_docstring(
320 Dictionary, 'dictionary entries accretion' )
323class ProducerDictionary( Dictionary[ __.H, __.V ] ):
324 ''' Accretive dictionary with default value for missing entries. '''
326 __slots__ = ( '_producer_', )
328 _producer_: __.DictionaryProducer[ __.V ]
330 def __init__(
331 self,
332 producer: __.DictionaryProducer[ __.V ],
333 /,
334 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
335 **entries: __.DictionaryNominativeArgument[ __.V ],
336 ):
337 # TODO: Validate producer argument.
338 self._producer_ = producer
339 super( ).__init__( *iterables, **entries )
341 def __repr__( self ) -> str:
342 return "{fqname}( {producer}, {contents} )".format(
343 fqname = __.calculate_fqname( self ),
344 producer = self._producer_,
345 contents = str( self._data_ ) )
347 def __getitem__( self, key: __.H ) -> __.V:
348 if key not in self:
349 value = self._producer_( )
350 self[ key ] = value
351 else: value = super( ).__getitem__( key )
352 return value
354 def copy( self ) -> __.typx.Self:
355 ''' Provides fresh copy of dictionary. '''
356 dictionary = type( self )( self._producer_ )
357 return dictionary.update( self )
359 def setdefault( self, key: __.H, default: __.V ) -> __.V:
360 ''' Returns value for key, setting it to default if missing. '''
361 if key not in self:
362 self[ key ] = default
363 return default
364 return self[ key ]
366 def with_data(
367 self,
368 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
369 **entries: __.DictionaryNominativeArgument[ __.V ],
370 ) -> __.typx.Self:
371 return type( self )( self._producer_, *iterables, **entries )
373ProducerDictionary.__doc__ = __.generate_docstring(
374 ProducerDictionary,
375 'dictionary entries accretion',
376 'dictionary entries production',
377)
380class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
381 ''' Accretive dictionary with validation of new entries. '''
383 __slots__ = ( '_validator_', )
385 _validator_: __.DictionaryValidator[ __.H, __.V ]
387 def __init__(
388 self,
389 validator: __.DictionaryValidator[ __.H, __.V ],
390 /,
391 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
392 **entries: __.DictionaryNominativeArgument[ __.V ],
393 ) -> None:
394 self._validator_ = validator
395 super( ).__init__( *iterables, **entries )
397 def __repr__( self ) -> str:
398 return "{fqname}( {validator}, {contents} )".format(
399 fqname = __.calculate_fqname( self ),
400 validator = self._validator_,
401 contents = str( self._data_ ) )
403 def _pre_setitem_( self, key: __.H, value: __.V ) -> tuple[ __.H, __.V ]:
404 if not self._validator_( key, value ):
405 from .exceptions import EntryValidityError
406 raise EntryValidityError( key, value )
407 return key, value
409 def copy( self ) -> __.typx.Self:
410 ''' Provides fresh copy of dictionary. '''
411 dictionary = type( self )( self._validator_ )
412 return dictionary.update( self )
414 def with_data(
415 self,
416 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
417 **entries: __.DictionaryNominativeArgument[ __.V ],
418 ) -> __.typx.Self:
419 return type( self )( self._validator_, *iterables, **entries )
421ValidatorDictionary.__doc__ = __.generate_docstring(
422 ValidatorDictionary,
423 'dictionary entries accretion',
424 'dictionary entries validation',
425)
428class ProducerValidatorDictionary( Dictionary[ __.H, __.V ] ):
429 ''' Accretive dictionary with defaults and validation. '''
431 __slots__ = ( '_producer_', '_validator_' )
433 _producer_: __.DictionaryProducer[ __.V ]
434 _validator_: __.DictionaryValidator[ __.H, __.V ]
436 def __init__(
437 self,
438 producer: __.DictionaryProducer[ __.V ],
439 validator: __.DictionaryValidator[ __.H, __.V ],
440 /,
441 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
442 **entries: __.DictionaryNominativeArgument[ __.V ],
443 ) -> None:
444 self._producer_ = producer
445 self._validator_ = validator
446 super( ).__init__( *iterables, **entries )
448 def __repr__( self ) -> str:
449 return "{fqname}( {producer}, {validator}, {contents} )".format(
450 fqname = __.calculate_fqname( self ),
451 producer = self._producer_,
452 validator = self._validator_,
453 contents = str( self._data_ ) )
455 def __getitem__( self, key: __.H ) -> __.V:
456 if key not in self:
457 value = self._producer_( )
458 if not self._validator_( key, value ):
459 from .exceptions import EntryValidityError
460 raise EntryValidityError( key, value )
461 self[ key ] = value
462 else: value = super( ).__getitem__( key )
463 return value
465 def _pre_setitem_( self, key: __.H, value: __.V ) -> tuple[ __.H, __.V ]:
466 if not self._validator_( key, value ):
467 from .exceptions import EntryValidityError
468 raise EntryValidityError( key, value )
469 return key, value
471 def copy( self ) -> __.typx.Self:
472 ''' Provides fresh copy of dictionary. '''
473 dictionary = type( self )( self._producer_, self._validator_ )
474 return dictionary.update( self )
476 def setdefault( self, key: __.H, default: __.V ) -> __.V:
477 ''' Returns value for key, setting it to default if missing. '''
478 if key not in self:
479 self[ key ] = default
480 return default
481 return self[ key ]
483 def with_data(
484 self,
485 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
486 **entries: __.DictionaryNominativeArgument[ __.V ],
487 ) -> __.typx.Self:
488 return type( self )(
489 self._producer_, self._validator_, *iterables, **entries )
491ProducerValidatorDictionary.__doc__ = __.generate_docstring(
492 ProducerValidatorDictionary,
493 'dictionary entries accretion',
494 'dictionary entries production',
495 'dictionary entries validation',
496)