Coverage for sources/frigid/dictionaries.py: 100%
105 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# 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:
86 - __getitem__, __iter__, __len__
87 '''
89 @__.abc.abstractmethod
90 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
91 raise NotImplementedError # pragma: no coverage
93 @__.abc.abstractmethod
94 def __len__( self ) -> int:
95 raise NotImplementedError # pragma: no coverage
97 @__.abc.abstractmethod
98 def __getitem__( self, key: __.H ) -> __.V:
99 raise NotImplementedError # pragma: no coverage
101 def __setitem__( self, key: __.H, value: __.V ) -> None:
102 from .exceptions import EntryImmutabilityError
103 raise EntryImmutabilityError( key )
105 def __delitem__( self, key: __.H ) -> None:
106 from .exceptions import EntryImmutabilityError
107 raise EntryImmutabilityError( key )
110class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
111 ''' Mix-in providing additional dictionary operations. '''
113 def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
114 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
115 conflicts = set( self.keys( ) ) & set( other.keys( ) )
116 if conflicts:
117 from .exceptions import EntryImmutabilityError
118 raise EntryImmutabilityError( next( iter( conflicts ) ) )
119 data = dict( self )
120 data.update( other )
121 return self.with_data( data )
123 def __ror__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
124 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
125 return self | other
127 def __and__(
128 self,
129 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
130 ) -> __.typx.Self:
131 if isinstance( other, __.cabc.Mapping ):
132 return self.with_data(
133 ( key, value ) for key, value in self.items( )
134 if key in other and other[ key ] == value )
135 if isinstance( other, ( __.cabc.Set, __.cabc.KeysView ) ):
136 return self.with_data(
137 ( key, self[ key ] ) for key in self.keys( ) & other )
138 return NotImplemented
140 def __rand__(
141 self,
142 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
143 ) -> __.typx.Self:
144 if not isinstance(
145 other, ( __.cabc.Mapping, __.cabc.Set, __.cabc.KeysView )
146 ): return NotImplemented
147 return self & other
149 @__.abc.abstractmethod
150 def copy( self ) -> __.typx.Self:
151 ''' Provides fresh copy of dictionary. '''
152 raise NotImplementedError # pragma: no coverage
154 @__.abc.abstractmethod
155 def with_data(
156 self,
157 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
158 **entries: __.DictionaryNominativeArgument[ __.V ],
159 ) -> __.typx.Self:
160 ''' Creates new dictionary with same behavior but different data. '''
161 raise NotImplementedError # pragma: no coverage
164class _Dictionary(
165 __.ImmutableDictionary[ __.H, __.V ], metaclass = _classes.Class
166): pass
169class Dictionary( # pylint: disable=eq-without-hash
170 _objects.Object, _DictionaryOperations[ __.H, __.V ]
171):
172 ''' Immutable dictionary. '''
174 __slots__ = ( '_data_', )
176 _data_: _Dictionary[ __.H, __.V ]
178 def __init__(
179 self,
180 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
181 **entries: __.DictionaryNominativeArgument[ __.V ],
182 ) -> None:
183 self._data_ = _Dictionary( *iterables, **entries )
184 super( ).__init__( )
186 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
187 return iter( self._data_ )
189 def __len__( self ) -> int:
190 return len( self._data_ )
192 def __repr__( self ) -> str:
193 return "{fqname}( {contents} )".format(
194 fqname = __.calculate_fqname( self ),
195 contents = self._data_.__repr__( ) )
197 def __str__( self ) -> str:
198 return str( self._data_ )
200 def __contains__( self, key: __.typx.Any ) -> bool:
201 return key in self._data_
203 def __getitem__( self, key: __.H ) -> __.V:
204 return self._data_[ key ]
206 def __eq__( self, other: __.typx.Any ) -> __.ComparisonResult:
207 if isinstance( other, __.cabc.Mapping ):
208 return self._data_ == other
209 return NotImplemented
211 def __ne__( self, other: __.typx.Any ) -> __.ComparisonResult:
212 if isinstance( other, __.cabc.Mapping ):
213 return self._data_ != other
214 return NotImplemented
216 def copy( self ) -> __.typx.Self:
217 ''' Provides fresh copy of dictionary. '''
218 return type( self )( self )
220 def get(
221 self, key: __.H, default: __.Absential[ __.V ] = __.absent
222 ) -> __.typx.Annotated[
223 __.V,
224 __.typx.Doc(
225 'Value of entry, if it exists. '
226 'Else, supplied default value or ``None``.' )
227 ]:
228 ''' Retrieves entry associated with key, if it exists. '''
229 if __.is_absent( default ):
230 return self._data_.get( key ) # type: ignore
231 return self._data_.get( key, default )
233 def keys( self ) -> __.cabc.KeysView[ __.H ]:
234 ''' Provides iterable view over dictionary keys. '''
235 return self._data_.keys( )
237 def items( self ) -> __.cabc.ItemsView[ __.H, __.V ]:
238 ''' Provides iterable view over dictionary items. '''
239 return self._data_.items( )
241 def values( self ) -> __.cabc.ValuesView[ __.V ]:
242 ''' Provides iterable view over dictionary values. '''
243 return self._data_.values( )
245 def with_data(
246 self,
247 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
248 **entries: __.DictionaryNominativeArgument[ __.V ],
249 ) -> __.typx.Self:
250 return type( self )( *iterables, **entries )
252Dictionary.__doc__ = __.generate_docstring(
253 Dictionary, 'dictionary entries immutability' )
254# Register as subclass of Mapping rather than use it as mixin.
255# We directly implement, for the sake of efficiency, the methods which the
256# mixin would provide.
257__.cabc.Mapping.register( Dictionary ) # type: ignore
260class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
261 ''' Immutable dictionary with validation of entries on initialization. '''
263 __slots__ = ( '_validator_', )
265 _validator_: __.DictionaryValidator[ __.H, __.V ]
267 def __init__(
268 self,
269 validator: __.DictionaryValidator[ __.H, __.V ],
270 /,
271 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
272 **entries: __.DictionaryNominativeArgument[ __.V ],
273 ) -> None:
274 self._validator_ = validator
275 entries_: list[ tuple[ __.H, __.V ] ] = [ ]
276 from itertools import chain
277 # Collect entries in case an iterable is a generator
278 # which would be consumed during validation, before initialization.
279 for key, value in chain.from_iterable( map( # type: ignore
280 lambda element: ( # type: ignore
281 element.items( )
282 if isinstance( element, __.cabc.Mapping )
283 else element
284 ),
285 ( *iterables, entries )
286 ) ):
287 if not self._validator_( key, value ): # type: ignore
288 from .exceptions import EntryValidityError
289 raise EntryValidityError( key, value )
290 entries_.append( ( key, value ) ) # type: ignore
291 super( ).__init__( entries_ )
293 def __repr__( self ) -> str:
294 return "{fqname}( {validator}, {contents} )".format(
295 fqname = __.calculate_fqname( self ),
296 validator = self._validator_.__repr__( ),
297 contents = self._data_.__repr__( ) )
299 def copy( self ) -> __.typx.Self:
300 ''' Provides fresh copy of dictionary. '''
301 return type( self )( self._validator_, self )
303 def with_data(
304 self,
305 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
306 **entries: __.DictionaryNominativeArgument[ __.V ],
307 ) -> __.typx.Self:
308 ''' Creates new dictionary with same behavior but different data. '''
309 return type( self )( self._validator_, *iterables, **entries )
311ValidatorDictionary.__doc__ = __.generate_docstring(
312 ValidatorDictionary,
313 'dictionary entries immutability',
314 'dictionary entries validation',
315)