Coverage for sources/frigid/dictionaries.py: 100%
102 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 21:52 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 21:52 +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''' Immutable dictionaries.
23 Dictionaries which cannot be modified after creation.
25 .. note::
27 While :py:class:`types.MappingProxyType` also provides a read-only view
28 of a dictionary, it has important differences from
29 :py:class:`Dictionary`:
31 * A ``MappingProxyType`` is a view over a mutable dictionary, so its
32 contents can still change if the underlying dictionary is modified.
33 * ``Dictionary`` owns its data and guarantees that it will never
34 change.
35 * ``Dictionary`` provides set operations (union, intersection) that
36 maintain immutability guarantees.
38 Use ``MappingProxyType`` when you want to expose a read-only view of a
39 dictionary that might need to change. Use ``Dictionary`` when you want
40 to ensure that the data can never change, such as for configuration
41 objects or other cases requiring strong immutability guarantees.
43 * :py:class:`AbstractDictionary`:
44 Base class defining the immutable dictionary interface. Implementations
45 must provide ``__getitem__``, ``__iter__``, and ``__len__``.
47 * :py:class:`Dictionary`:
48 Standard implementation of an immutable dictionary. Supports all usual
49 dict read operations but prevents any modifications.
51 * :py:class:`ValidatorDictionary`:
52 Validates entries before addition using a supplied predicate function.
54 >>> from frigid import Dictionary
55 >>> d = Dictionary( x = 1, y = 2 )
56 >>> d[ 'z' ] = 3 # Attempt to add entry
57 Traceback (most recent call last):
58 ...
59 frigid.exceptions.EntryImmutability: Cannot assign or delete entry for 'z'.
60 >>> d[ 'x' ] = 4 # Attempt modification
61 Traceback (most recent call last):
62 ...
63 frigid.exceptions.EntryImmutability: Cannot assign or delete entry for 'x'.
64 >>> del d[ 'y' ] # Attempt removal
65 Traceback (most recent call last):
66 ...
67 frigid.exceptions.EntryImmutability: Cannot assign or delete entry for 'y'.
68'''
71from . import __
72from . import classes as _classes
75class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
76 ''' Abstract base class for immutable dictionaries.
78 An immutable dictionary prevents modification or removal of entries
79 after creation. This provides a clean interface for dictionaries
80 that should never change.
82 Implementations must provide __getitem__, __iter__, __len__.
83 '''
85 @__.abc.abstractmethod
86 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
87 raise NotImplementedError # pragma: no coverage
89 @__.abc.abstractmethod
90 def __len__( self ) -> int:
91 raise NotImplementedError # pragma: no coverage
93 @__.abc.abstractmethod
94 def __getitem__( self, key: __.H ) -> __.V:
95 raise NotImplementedError # pragma: no coverage
97 def __setitem__( self, key: __.H, value: __.V ) -> None:
98 from .exceptions import EntryImmutability
99 raise EntryImmutability( key )
101 def __delitem__( self, key: __.H ) -> None:
102 from .exceptions import EntryImmutability
103 raise EntryImmutability( key )
106class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
107 ''' Mix-in providing additional dictionary operations. '''
109 # TODO? Common __init__.
111 def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
112 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
113 conflicts = set( self.keys( ) ) & set( other.keys( ) )
114 if conflicts:
115 from .exceptions import EntryImmutability
116 raise EntryImmutability( next( iter( conflicts ) ) )
117 data = dict( self )
118 data.update( other )
119 return self.with_data( data )
121 def __ror__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
122 if not isinstance( other, __.cabc.Mapping ): return NotImplemented
123 return self | other
125 def __and__(
126 self,
127 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
128 ) -> __.typx.Self:
129 if isinstance( other, __.cabc.Mapping ):
130 return self.with_data(
131 ( key, value ) for key, value in self.items( )
132 if key in other and other[ key ] == value )
133 if isinstance( other, ( __.cabc.Set, __.cabc.KeysView ) ):
134 return self.with_data(
135 ( key, self[ key ] ) for key in self.keys( ) & other )
136 return NotImplemented
138 def __rand__(
139 self,
140 other: __.cabc.Set[ __.H ] | __.cabc.Mapping[ __.H, __.V ]
141 ) -> __.typx.Self:
142 if not isinstance(
143 other, ( __.cabc.Mapping, __.cabc.Set, __.cabc.KeysView )
144 ): return NotImplemented
145 return self & other
147 @__.abc.abstractmethod
148 def copy( self ) -> __.typx.Self:
149 ''' Provides fresh copy of dictionary. '''
150 raise NotImplementedError # pragma: no coverage
152 @__.abc.abstractmethod
153 def with_data(
154 self,
155 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
156 **entries: __.DictionaryNominativeArgument[ __.V ],
157 ) -> __.typx.Self:
158 ''' Creates new dictionary with same behavior but different data. '''
159 raise NotImplementedError # pragma: no coverage
162class Dictionary(
163 _DictionaryOperations[ __.H, __.V ],
164 metaclass = _classes.AbstractBaseClass,
165):
166 ''' Immutable dictionary. '''
168 __slots__ = ( '_data_', )
170 _data_: __.ImmutableDictionary[ __.H, __.V ]
171 _dynadoc_fragments_ = ( 'dictionary entries protect', )
173 def __init__(
174 self,
175 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
176 **entries: __.DictionaryNominativeArgument[ __.V ],
177 ) -> None:
178 self._data_ = __.ImmutableDictionary( *iterables, **entries )
179 super( ).__init__( )
181 def __iter__( self ) -> __.cabc.Iterator[ __.H ]:
182 return iter( self._data_ )
184 def __len__( self ) -> int:
185 return len( self._data_ )
187 def __repr__( self ) -> str:
188 return "{fqname}( {contents} )".format(
189 fqname = __.ccutils.qualify_class_name( type( self ) ),
190 contents = self._data_.__repr__( ) )
192 def __str__( self ) -> str:
193 return str( self._data_ )
195 def __contains__( self, key: __.typx.Any ) -> bool:
196 return key in self._data_
198 def __getitem__( self, key: __.H ) -> __.V:
199 return self._data_[ key ]
201 def __eq__( self, other: __.typx.Any ) -> __.ComparisonResult:
202 if isinstance( other, __.cabc.Mapping ):
203 return self._data_ == other
204 return NotImplemented
206 def __ne__( self, other: __.typx.Any ) -> __.ComparisonResult:
207 if isinstance( other, __.cabc.Mapping ):
208 return self._data_ != other
209 return NotImplemented
211 def copy( self ) -> __.typx.Self:
212 ''' Provides fresh copy of dictionary. '''
213 return type( self )( self )
215 def get( # pyright: ignore
216 self, key: __.H, default: __.Absential[ __.V ] = __.absent
217 ) -> __.typx.Annotated[
218 __.V,
219 __.typx.Doc(
220 'Value of entry, if it exists. '
221 'Else, supplied default value or ``None``.' )
222 ]:
223 ''' Retrieves entry associated with key, if it exists. '''
224 if __.is_absent( default ):
225 return self._data_.get( key ) # pyright: ignore
226 return self._data_.get( key, default )
228 def keys( self ) -> __.cabc.KeysView[ __.H ]:
229 ''' Provides iterable view over dictionary keys. '''
230 return self._data_.keys( )
232 def items( self ) -> __.cabc.ItemsView[ __.H, __.V ]:
233 ''' Provides iterable view over dictionary items. '''
234 return self._data_.items( )
236 def values( self ) -> __.cabc.ValuesView[ __.V ]:
237 ''' Provides iterable view over dictionary values. '''
238 return self._data_.values( )
240 def with_data(
241 self,
242 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
243 **entries: __.DictionaryNominativeArgument[ __.V ],
244 ) -> __.typx.Self:
245 return type( self )( *iterables, **entries )
248class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
249 ''' Immutable dictionary with validation of entries on initialization. '''
251 __slots__ = ( '_validator_', )
253 _dynadoc_fragments_ = (
254 'dictionary entries protect', 'dictionary entries validate' )
255 _validator_: __.DictionaryValidator[ __.H, __.V ]
257 def __init__(
258 self,
259 validator: __.DictionaryValidator[ __.H, __.V ],
260 /,
261 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
262 **entries: __.DictionaryNominativeArgument[ __.V ],
263 ) -> None:
264 self._validator_ = validator
265 entries_: list[ tuple[ __.H, __.V ] ] = [ ]
266 from itertools import chain
267 # Collect entries in case an iterable is a generator
268 # which would be consumed during validation, before initialization.
269 for key, value in chain.from_iterable( map( # pyright: ignore
270 lambda element: ( # pyright: ignore
271 element.items( )
272 if isinstance( element, __.cabc.Mapping )
273 else element
274 ),
275 ( *iterables, entries )
276 ) ):
277 if not self._validator_( key, value ): # pyright: ignore
278 from .exceptions import EntryInvalidity
279 raise EntryInvalidity( key, value )
280 entries_.append( ( key, value ) ) # pyright: ignore
281 super( ).__init__( entries_ )
283 def __repr__( self ) -> str:
284 return "{fqname}( {validator}, {contents} )".format(
285 fqname = __.ccutils.qualify_class_name( type( self ) ),
286 validator = self._validator_.__repr__( ),
287 contents = self._data_.__repr__( ) )
289 def copy( self ) -> __.typx.Self:
290 ''' Provides fresh copy of dictionary. '''
291 return type( self )( self._validator_, self )
293 def with_data(
294 self,
295 *iterables: __.DictionaryPositionalArgument[ __.H, __.V ],
296 **entries: __.DictionaryNominativeArgument[ __.V ],
297 ) -> __.typx.Self:
298 ''' Creates new dictionary with same behavior but different data. '''
299 return type( self )( self._validator_, *iterables, **entries )