Coverage for sources/frigid/dictionaries.py: 100%
104 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 03:33 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 03: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''' Immutable dictionaries.
24 Dictionaries which cannot be modified after creation.
26 .. note::
28 While :py:class:`types.MappingProxyType` also provides a read-only view
29 of a dictionary, it has important differences from
30 :py:class:`Dictionary`:
32 * A ``MappingProxyType`` is a view over a mutable dictionary, so its
33 contents can still change if the underlying dictionary is modified.
34 * ``Dictionary`` owns its data and guarantees that it will never
35 change.
36 * ``Dictionary`` provides set operations (union, intersection) that
37 maintain immutability guarantees.
39 Use ``MappingProxyType`` when you want to expose a read-only view of a
40 dictionary that might need to change. Use ``Dictionary`` when you want
41 to ensure that the data can never change, such as for configuration
42 objects or other cases requiring strong immutability guarantees.
44 * :py:class:`AbstractDictionary`:
45 Base class defining the immutable dictionary interface. Implementations
46 must provide ``__getitem__``, ``__iter__``, and ``__len__``.
48 * :py:class:`Dictionary`:
49 Standard implementation of an immutable dictionary. Supports all usual
50 dict read operations but prevents any modifications.
52 * :py:class:`ValidatorDictionary`:
53 Validates entries before addition using a supplied predicate function.
55 >>> from frigid import Dictionary
56 >>> d = Dictionary( x = 1, y = 2 )
57 >>> d[ 'z' ] = 3 # Attempt to add entry
58 Traceback (most recent call last):
59 ...
60 frigid.exceptions.EntryImmutabilityError: Cannot assign or delete entry for 'z'.
61 >>> d[ 'x' ] = 4 # Attempt modification
62 Traceback (most recent call last):
63 ...
64 frigid.exceptions.EntryImmutabilityError: Cannot assign or delete entry for 'x'.
65 >>> del d[ 'y' ] # Attempt removal
66 Traceback (most recent call last):
67 ...
68 frigid.exceptions.EntryImmutabilityError: Cannot assign or delete entry for 'y'.
69'''
70# pylint: enable=line-too-long
73from . import __
74from . import classes as _classes
75from . import objects as _objects
78class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
79 ''' Abstract base class for immutable dictionaries.
81 An immutable dictionary prevents modification or removal of entries
82 after creation. This provides a clean interface for dictionaries
83 that should never change.
85 Implementations must provide __getitem__, __iter__, __len__.
86 '''
88 @__.abc.abstractmethod
89 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
90 raise NotImplementedError # pragma: no coverage
92 @__.abc.abstractmethod
93 def __len__( self ) -> int:
94 raise NotImplementedError # pragma: no coverage
96 @__.abc.abstractmethod
97 def __getitem__( self, key: __.H ) -> __.V:
98 raise NotImplementedError # pragma: no coverage
100 def __setitem__( self, key: __.H, value: __.V ) -> None:
101 from .exceptions import EntryImmutabilityError
102 raise EntryImmutabilityError( key )
104 def __delitem__( self, key: __.H ) -> None:
105 from .exceptions import EntryImmutabilityError
106 raise EntryImmutabilityError( key )
109class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
110 ''' Mix-in providing additional dictionary operations. '''
112 # TODO? Common __init__.
114 def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
115 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
116 conflicts = set( self.keys( ) ) & set( other.keys( ) )
117 if conflicts:
118 from .exceptions import EntryImmutabilityError
119 raise EntryImmutabilityError( next( iter( conflicts ) ) )
120 data = dict( self )
121 data.update( other )
122 return self.with_data( data )
124 def __ror__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
125 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
126 return self | other
128 def __and__(
129 self,
130 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
131 ) -> __.typx.Self:
132 if isinstance( other, __.cabc.Mapping ):
133 return self.with_data(
134 ( key, value ) for key, value in self.items( )
135 if key in other and other[ key ] == value )
136 if isinstance( other, ( __.cabc.Set, __.cabc.KeysView ) ):
137 return self.with_data(
138 ( key, self[ key ] ) for key in self.keys( ) & other )
139 return NotImplemented
141 def __rand__(
142 self,
143 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
144 ) -> __.typx.Self:
145 if not isinstance(
146 other, ( __.cabc.Mapping, __.cabc.Set, __.cabc.KeysView )
147 ): return NotImplemented
148 return self & other
150 @__.abc.abstractmethod
151 def copy( self ) -> __.typx.Self:
152 ''' Provides fresh copy of dictionary. '''
153 raise NotImplementedError # pragma: no coverage
155 @__.abc.abstractmethod
156 def with_data(
157 self,
158 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
159 **entries: __.DictionaryNominativeArgument[ __.V ],
160 ) -> __.typx.Self:
161 ''' Creates new dictionary with same behavior but different data. '''
162 raise NotImplementedError # pragma: no coverage
165class _Dictionary(
166 __.ImmutableDictionary[ __.H, __.V ], metaclass = _classes.Class
167): pass
170class Dictionary( # pylint: disable=eq-without-hash
171 _objects.Object, _DictionaryOperations[ __.H, __.V ]
172):
173 ''' Immutable dictionary. '''
174 # TODO: version 2.0: Do not subclass from 'Object'.
176 __slots__ = ( '_data_', )
178 _data_: _Dictionary[ __.H, __.V ]
180 def __init__(
181 self,
182 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
183 **entries: __.DictionaryNominativeArgument[ __.V ],
184 ) -> None:
185 self._data_ = _Dictionary( *iterables, **entries )
186 super( ).__init__( )
188 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
189 return iter( self._data_ )
191 def __len__( self ) -> int:
192 return len( self._data_ )
194 def __repr__( self ) -> str:
195 return "{fqname}( {contents} )".format(
196 fqname = __.calculate_fqname( self ),
197 contents = self._data_.__repr__( ) )
199 def __str__( self ) -> str:
200 return str( self._data_ )
202 def __contains__( self, key: __.typx.Any ) -> bool:
203 return key in self._data_
205 def __getitem__( self, key: __.H ) -> __.V:
206 return self._data_[ key ]
208 def __eq__( self, other: __.typx.Any ) -> __.ComparisonResult:
209 if isinstance( other, __.cabc.Mapping ):
210 return self._data_ == other
211 return NotImplemented
213 def __ne__( self, other: __.typx.Any ) -> __.ComparisonResult:
214 if isinstance( other, __.cabc.Mapping ):
215 return self._data_ != other
216 return NotImplemented
218 def copy( self ) -> __.typx.Self:
219 ''' Provides fresh copy of dictionary. '''
220 return type( self )( self )
222 def get(
223 self, key: __.H, default: __.Absential[ __.V ] = __.absent
224 ) -> __.typx.Annotated[
225 __.V,
226 __.typx.Doc(
227 'Value of entry, if it exists. '
228 'Else, supplied default value or ``None``.' )
229 ]:
230 ''' Retrieves entry associated with key, if it exists. '''
231 if __.is_absent( default ):
232 return self._data_.get( key ) # type: ignore
233 return self._data_.get( key, default )
235 def keys( self ) -> __.cabc.KeysView[ __.H ]:
236 ''' Provides iterable view over dictionary keys. '''
237 return self._data_.keys( )
239 def items( self ) -> __.cabc.ItemsView[ __.H, __.V ]:
240 ''' Provides iterable view over dictionary items. '''
241 return self._data_.items( )
243 def values( self ) -> __.cabc.ValuesView[ __.V ]:
244 ''' Provides iterable view over dictionary values. '''
245 return self._data_.values( )
247 def with_data(
248 self,
249 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
250 **entries: __.DictionaryNominativeArgument[ __.V ],
251 ) -> __.typx.Self:
252 return type( self )( *iterables, **entries )
254Dictionary.__doc__ = __.generate_docstring(
255 Dictionary, 'dictionary entries immutability' )
258class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
259 ''' Immutable dictionary with validation of entries on initialization. '''
261 __slots__ = ( '_validator_', )
263 _validator_: __.DictionaryValidator[ __.H, __.V ]
265 def __init__(
266 self,
267 validator: __.DictionaryValidator[ __.H, __.V ],
268 /,
269 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
270 **entries: __.DictionaryNominativeArgument[ __.V ],
271 ) -> None:
272 self._validator_ = validator
273 entries_: list[ tuple[ __.H, __.V ] ] = [ ]
274 from itertools import chain
275 # Collect entries in case an iterable is a generator
276 # which would be consumed during validation, before initialization.
277 for key, value in chain.from_iterable( map( # type: ignore
278 lambda element: ( # type: ignore
279 element.items( )
280 if isinstance( element, __.cabc.Mapping )
281 else element
282 ),
283 ( *iterables, entries )
284 ) ):
285 if not self._validator_( key, value ): # type: ignore
286 from .exceptions import EntryValidityError
287 raise EntryValidityError( key, value )
288 entries_.append( ( key, value ) ) # type: ignore
289 super( ).__init__( entries_ )
291 def __repr__( self ) -> str:
292 return "{fqname}( {validator}, {contents} )".format(
293 fqname = __.calculate_fqname( self ),
294 validator = self._validator_.__repr__( ),
295 contents = self._data_.__repr__( ) )
297 def copy( self ) -> __.typx.Self:
298 ''' Provides fresh copy of dictionary. '''
299 return type( self )( self._validator_, self )
301 def with_data(
302 self,
303 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
304 **entries: __.DictionaryNominativeArgument[ __.V ],
305 ) -> __.typx.Self:
306 ''' Creates new dictionary with same behavior but different data. '''
307 return type( self )( self._validator_, *iterables, **entries )
309ValidatorDictionary.__doc__ = __.generate_docstring(
310 ValidatorDictionary,
311 'dictionary entries immutability',
312 'dictionary entries validation',
313)