frigid 2.0__tar.gz → 3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frigid
3
- Version: 2.0
3
+ Version: 3.0
4
4
  Summary: Immutable data structures.
5
5
  Project-URL: Homepage, https://github.com/emcd/python-frigid
6
6
  Project-URL: Documentation, https://emcd.github.io/python-frigid
@@ -87,7 +87,7 @@ Key Features ⭐
87
87
  be modified or removed. Also has variant for validation on initialization.
88
88
  And provides set operations not found on `MappingProxyType
89
89
  <https://docs.python.org/3/library/types.html#types.MappingProxyType>`_.
90
- * 📇 **Immutable Namespace**: Similar to `SimpleNamespace
90
+ * 🗃️ **Immutable Namespace**: Similar to `SimpleNamespace
91
91
  <https://docs.python.org/3/library/types.html#types.SimpleNamespace>`_, but
92
92
  attributes are immutable from creation.
93
93
  * 🧱 **Additional Types**: Classes (including abstract base classes), modules,
@@ -124,12 +124,12 @@ Examples 💡
124
124
  ===============================================================================
125
125
 
126
126
 
127
- Immutable Namespace 📇
127
+ Immutable Namespaces 🗃️
128
128
  -------------------------------------------------------------------------------
129
129
 
130
130
  An immutable namespace, similar to ``types.SimpleNamespace``, is available.
131
- This namespace is initialized from keyword arguments and becomes completely
132
- immutable. (Keyword arguments shown below; see documentation for additional
131
+ This namespace can be initialized from multiple iterables and from keyword
132
+ arguments. (Keyword arguments shown below; see documentation for additional
133
133
  forms of initialization.)
134
134
 
135
135
  >>> from frigid import Namespace
@@ -146,13 +146,13 @@ frigid.exceptions.AttributeImmutabilityError: Cannot delete attribute 'apples'.
146
146
  frigid.namespaces.Namespace( apples = 12, bananas = 6 )
147
147
 
148
148
 
149
- Immutable Dictionary 📖
149
+ Immutable Dictionaries 📖
150
150
  -------------------------------------------------------------------------------
151
151
 
152
- An immutable dictionary, similar to ``dict``, is available. This dictionary is
153
- initialized from iterables and keyword arguments and becomes completely
154
- immutable. (Keyword arguments shown below; see documentation for additional
155
- forms of initialization.)
152
+ An immutable dictionary, similar to ``dict``, is available. This dictionary can
153
+ be initialized from multiple iterables and from keyword arguments. (Keyword
154
+ arguments shown below; see documentation for additional forms of
155
+ initialization.)
156
156
 
157
157
  >>> from frigid import Dictionary
158
158
  >>> dct = Dictionary( apples = 12, bananas = 6)
@@ -168,6 +168,28 @@ frigid.exceptions.EntryImmutabilityError: Cannot delete entry for 'bananas'.
168
168
  frigid.dictionaries.Dictionary( {'apples': 12, 'bananas': 6} )
169
169
 
170
170
 
171
+ Immutable Objects 🧱
172
+ -------------------------------------------------------------------------------
173
+
174
+ The ``immutable`` decorator can be applied to any class to make its instances fully immutable after initialization.
175
+
176
+ >>> from frigid import immutable
177
+ >>> @immutable
178
+ ... class Config:
179
+ ... def __init__( self, debug = False ):
180
+ ... self.debug = debug
181
+ ...
182
+ >>> config = Config( debug = True )
183
+ >>> config.verbose = True # ❌ Attempted addition raises error
184
+ Traceback (most recent call last):
185
+ ...
186
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'verbose'.
187
+ >>> config.debug = False # ❌ Attempted reassignment raises error
188
+ Traceback (most recent call last):
189
+ ...
190
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'debug'.
191
+
192
+
171
193
  Use Cases 🎯
172
194
  ===============================================================================
173
195
 
@@ -58,7 +58,7 @@ Key Features ⭐
58
58
  be modified or removed. Also has variant for validation on initialization.
59
59
  And provides set operations not found on `MappingProxyType
60
60
  <https://docs.python.org/3/library/types.html#types.MappingProxyType>`_.
61
- * 📇 **Immutable Namespace**: Similar to `SimpleNamespace
61
+ * 🗃️ **Immutable Namespace**: Similar to `SimpleNamespace
62
62
  <https://docs.python.org/3/library/types.html#types.SimpleNamespace>`_, but
63
63
  attributes are immutable from creation.
64
64
  * 🧱 **Additional Types**: Classes (including abstract base classes), modules,
@@ -95,12 +95,12 @@ Examples 💡
95
95
  ===============================================================================
96
96
 
97
97
 
98
- Immutable Namespace 📇
98
+ Immutable Namespaces 🗃️
99
99
  -------------------------------------------------------------------------------
100
100
 
101
101
  An immutable namespace, similar to ``types.SimpleNamespace``, is available.
102
- This namespace is initialized from keyword arguments and becomes completely
103
- immutable. (Keyword arguments shown below; see documentation for additional
102
+ This namespace can be initialized from multiple iterables and from keyword
103
+ arguments. (Keyword arguments shown below; see documentation for additional
104
104
  forms of initialization.)
105
105
 
106
106
  >>> from frigid import Namespace
@@ -117,13 +117,13 @@ frigid.exceptions.AttributeImmutabilityError: Cannot delete attribute 'apples'.
117
117
  frigid.namespaces.Namespace( apples = 12, bananas = 6 )
118
118
 
119
119
 
120
- Immutable Dictionary 📖
120
+ Immutable Dictionaries 📖
121
121
  -------------------------------------------------------------------------------
122
122
 
123
- An immutable dictionary, similar to ``dict``, is available. This dictionary is
124
- initialized from iterables and keyword arguments and becomes completely
125
- immutable. (Keyword arguments shown below; see documentation for additional
126
- forms of initialization.)
123
+ An immutable dictionary, similar to ``dict``, is available. This dictionary can
124
+ be initialized from multiple iterables and from keyword arguments. (Keyword
125
+ arguments shown below; see documentation for additional forms of
126
+ initialization.)
127
127
 
128
128
  >>> from frigid import Dictionary
129
129
  >>> dct = Dictionary( apples = 12, bananas = 6)
@@ -139,6 +139,28 @@ frigid.exceptions.EntryImmutabilityError: Cannot delete entry for 'bananas'.
139
139
  frigid.dictionaries.Dictionary( {'apples': 12, 'bananas': 6} )
140
140
 
141
141
 
142
+ Immutable Objects 🧱
143
+ -------------------------------------------------------------------------------
144
+
145
+ The ``immutable`` decorator can be applied to any class to make its instances fully immutable after initialization.
146
+
147
+ >>> from frigid import immutable
148
+ >>> @immutable
149
+ ... class Config:
150
+ ... def __init__( self, debug = False ):
151
+ ... self.debug = debug
152
+ ...
153
+ >>> config = Config( debug = True )
154
+ >>> config.verbose = True # ❌ Attempted addition raises error
155
+ Traceback (most recent call last):
156
+ ...
157
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'verbose'.
158
+ >>> config.debug = False # ❌ Attempted reassignment raises error
159
+ Traceback (most recent call last):
160
+ ...
161
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'debug'.
162
+
163
+
142
164
  Use Cases 🎯
143
165
  ===============================================================================
144
166
 
@@ -1,3 +1,6 @@
1
+ # vim: set filetype=toml fileencoding=utf-8:
2
+ # -*- mode: toml ; coding: utf-8 -*-
3
+
1
4
  [build-system]
2
5
  requires = [
3
6
  'hatchling',
@@ -14,6 +17,9 @@ requires-python = '>= 3.10'
14
17
  dependencies = [
15
18
  'absence',
16
19
  'typing-extensions',
20
+ # --- BEGIN: Injected by Copier ---
21
+
22
+ # --- END: Injected by Copier ---
17
23
  ]
18
24
  classifiers = [ # https://pypi.org/classifiers
19
25
  'Development Status :: 5 - Production/Stable',
@@ -52,7 +58,7 @@ branch = true
52
58
  command_line = '-m pytest' # TODO? '--fail-under'
53
59
  data_file = '.auxiliary/caches/pytest/coverage.sqlite3'
54
60
  parallel = true
55
- source = [ 'sources', 'tests' ]
61
+ source = [ 'sources' ]
56
62
  [tool.coverage.html]
57
63
  directory = '.auxiliary/artifacts/coverage-pytest'
58
64
  [tool.coverage.xml]
@@ -62,10 +68,20 @@ output = '.auxiliary/artifacts/coverage-pytest/coverage.xml'
62
68
  [tool.hatch.build]
63
69
  directory = '.auxiliary/artifacts/hatch-build'
64
70
  [tool.hatch.build.targets.sdist]
65
- only-include = [ 'sources/frigid' ]
71
+ only-include = [
72
+ 'sources/frigid',
73
+ # --- BEGIN: Injected by Copier ---
74
+
75
+ # --- END: Injected by Copier ---
76
+ ]
66
77
  strict-naming = false
67
78
  [tool.hatch.build.targets.wheel]
68
- only-include = [ 'sources/frigid' ]
79
+ only-include = [
80
+ 'sources/frigid',
81
+ # --- BEGIN: Injected by Copier ---
82
+
83
+ # --- END: Injected by Copier ---
84
+ ]
69
85
  strict-naming = false
70
86
  [tool.hatch.build.targets.wheel.sources]
71
87
  'sources/frigid' = 'frigid'
@@ -79,6 +95,7 @@ dependencies = [
79
95
  'coverage[toml]',
80
96
  'furo',
81
97
  'icecream',
98
+ 'isort',
82
99
  'packaging',
83
100
  'pre-commit',
84
101
  'pylint',
@@ -93,8 +110,16 @@ dependencies = [
93
110
  'towncrier',
94
111
  'tryceratops',
95
112
  'yapf',
113
+ # --- BEGIN: Injected by Copier ---
114
+
115
+
116
+
117
+ # --- END: Injected by Copier ---
96
118
  ]
97
119
  post-install-commands = [
120
+ # --- BEGIN: Injected by Copier ---
121
+
122
+ # --- END: Injected by Copier ---
98
123
  ]
99
124
  [tool.hatch.envs.develop.env-vars]
100
125
  PYTHONUNBUFFERED = 'TRUE' # TODO: Only for coverage/pytest.
@@ -117,8 +142,10 @@ linters = [
117
142
  sources/frigid''',
118
143
  ]
119
144
  packagers = [
120
- 'hatch build',
121
- # TODO? Sign packages.
145
+ '''hatch build''',
146
+ # --- BEGIN: Injected by Copier ---
147
+
148
+ # --- END: Injected by Copier ---
122
149
  ]
123
150
  testers = [
124
151
  'coverage erase',
@@ -242,9 +269,12 @@ ignored-parents = [
242
269
  'builtins.TypeError',
243
270
  'builtins.ValueError',
244
271
  'builtins.type',
245
- 'frigid.__.ClassConcealerExtension',
272
+ 'frigid.__.exceptions.Omnierror',
273
+ 'frigid.__.exceptions.Omniexception',
246
274
  'frigid.objects.Class',
247
275
  'frigid.objects.Object',
276
+ 'typing.Protocol',
277
+ 'typing_extensions.Protocol',
248
278
  # --- BEGIN: Injected by Copier ---
249
279
  'frigid.exceptions.Omnierror',
250
280
  'frigid.exceptions.Omniexception',
@@ -296,7 +326,9 @@ disable = [
296
326
  'multiple-statements',
297
327
  'reimported',
298
328
  'too-few-public-methods',
329
+ 'too-many-positional-arguments', # 'too-many-arguments' is good enough
299
330
  'ungrouped-imports',
331
+ 'unused-wildcard-import',
300
332
  'use-dict-literal',
301
333
  'using-exception-groups-in-unsupported-version',
302
334
  'wrong-import-order',
@@ -304,7 +336,7 @@ disable = [
304
336
  ]
305
337
  # TODO: Latest 'per-file-ignores' code may supports dicts and lists in addition to strings.
306
338
  per-file-ignores = '''
307
- /tests/:attribute-defined-outside-init,comparison-with-itself,magic-value-comparison,missing-class-docstring,missing-function-docstring,protected-access,singleton-comparison,superfluous-parens,unexpected-keyword-arg
339
+ /tests/:attribute-defined-outside-init,comparison-with-itself,magic-value-comparison,missing-class-docstring,missing-function-docstring,protected-access,singleton-comparison,superfluous-parens,too-many-try-statements,unexpected-keyword-arg
308
340
  __init__\.py:unused-import
309
341
  ''' # Note: Paths are regexes.
310
342
  [tool.pylint.refactoring]
@@ -355,6 +387,7 @@ reportMatchNotExhaustive = true
355
387
  reportMissingImports = true
356
388
  reportMissingTypeStubs = true
357
389
  reportMissingTypeArgument = true
390
+ reportPossiblyUnboundVariable = false # Covered by other linters.
358
391
  reportPrivateImportUsage = false # Covered by other linters.
359
392
  reportPrivateUsage = false # Covered by other linters.
360
393
  reportSelfClsParameterName = false # Too opinionated.
@@ -380,8 +413,8 @@ stubPath = 'sources/frigid/_typedecls'
380
413
  # attribute on protected modules. Instead, we use Sphinx to run doctests.
381
414
  minversion = '8.1'
382
415
  addopts = '--capture=no --exitfirst -rfE'
383
- testpaths = [ 'tests', 'sources' ]
384
- python_files = [ '*.py' ]
416
+ testpaths = [ 'tests' ]
417
+ python_files = [ 'test_*.py' ]
385
418
  python_functions = [ 'test_[0-9][0-9][0-9]_*' ]
386
419
  cache_dir = '.auxiliary/caches/pytest'
387
420
 
@@ -480,4 +513,4 @@ split_penalty_after_opening_bracket = 100 # default: 300
480
513
  #split_penalty_excess_character = 7000
481
514
  #split_penalty_for_added_line_split = 30
482
515
  #split_penalty_import_names = 0
483
- #split_penalty_logical_operator = 300
516
+ #split_penalty_logical_operator = 300
@@ -24,6 +24,10 @@
24
24
  # ruff: noqa: F401
25
25
 
26
26
 
27
+ # TODO: Consider a dictionary factory to allow 'mutables' closure
28
+ # to be referenced in the '__setitem__' and '__delitem__' methods.
29
+
30
+
27
31
  from __future__ import annotations
28
32
 
29
33
  from . import imports as __
@@ -66,11 +70,11 @@ class ImmutableDictionary(
66
70
  self._behaviors_.add( _immutables.behavior_label )
67
71
 
68
72
  def __delitem__( self, key: _H ) -> None:
69
- from ..exceptions import EntryImmutabilityError
73
+ from .exceptions import EntryImmutabilityError
70
74
  raise EntryImmutabilityError( key )
71
75
 
72
76
  def __setitem__( self, key: _H, value: _V ) -> None:
73
- from ..exceptions import EntryImmutabilityError
77
+ from .exceptions import EntryImmutabilityError
74
78
  default: set[ str ] = set( )
75
79
  if _immutables.behavior_label in getattr(
76
80
  self, '_behaviors_', default
@@ -80,7 +84,7 @@ class ImmutableDictionary(
80
84
 
81
85
  def clear( self ) -> __.typx.Never:
82
86
  ''' Raises exception. Cannot clear immutable entries. '''
83
- from ..exceptions import OperationInvalidity
87
+ from .exceptions import OperationInvalidity
84
88
  raise OperationInvalidity( 'clear' )
85
89
 
86
90
  def copy( self ) -> __.typx.Self:
@@ -91,12 +95,12 @@ class ImmutableDictionary(
91
95
  self, key: _H, default: __.Absential[ _V ] = __.absent
92
96
  ) -> __.typx.Never:
93
97
  ''' Raises exception. Cannot pop immutable entry. '''
94
- from ..exceptions import OperationInvalidity
98
+ from .exceptions import OperationInvalidity
95
99
  raise OperationInvalidity( 'pop' )
96
100
 
97
101
  def popitem( self ) -> __.typx.Never:
98
102
  ''' Raises exception. Cannot pop immutable entry. '''
99
- from ..exceptions import OperationInvalidity
103
+ from .exceptions import OperationInvalidity
100
104
  raise OperationInvalidity( 'popitem' )
101
105
 
102
106
  def update( # type: ignore
@@ -105,5 +109,5 @@ class ImmutableDictionary(
105
109
  **entries: __.DictionaryNominativeArgument[ _V ],
106
110
  ) -> None:
107
111
  ''' Raises exception. Cannot perform mass update. '''
108
- from ..exceptions import OperationInvalidity
112
+ from .exceptions import OperationInvalidity
109
113
  raise OperationInvalidity( 'update' )
@@ -0,0 +1,53 @@
1
+ # vim: set filetype=python fileencoding=utf-8:
2
+ # -*- coding: utf-8 -*-
3
+
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
+ #============================================================================#
19
+
20
+
21
+ ''' Family of exceptions for package internals. '''
22
+
23
+
24
+ from __future__ import annotations
25
+
26
+ from . import imports as __
27
+ from . import immutables as _immutables
28
+
29
+
30
+ class Omniexception( _immutables.ImmutableObject, BaseException ):
31
+ ''' Base for all exceptions raised internally. '''
32
+
33
+ _attribute_visibility_includes_: __.cabc.Collection[ str ] = (
34
+ frozenset( ( '__cause__', '__context__', ) ) )
35
+
36
+
37
+ class Omnierror( Omniexception, Exception ):
38
+ ''' Base for error exceptions raised internally. '''
39
+
40
+
41
+ class EntryImmutabilityError( Omnierror, TypeError ):
42
+ ''' Attempt to update or remove immutable dictionary entry. '''
43
+
44
+ def __init__( self, indicator: __.cabc.Hashable ) -> None:
45
+ super( ).__init__(
46
+ f"Cannot alter or remove existing entry for {indicator!r}." )
47
+
48
+
49
+ class OperationInvalidity( Omnierror, RuntimeError, TypeError ):
50
+ ''' Attempt to perform invalid operation. '''
51
+
52
+ def __init__( self, name: str ) -> None:
53
+ super( ).__init__( f"Operation {name!r} is not valid on this object." )
@@ -28,12 +28,16 @@ from __future__ import annotations
28
28
 
29
29
  import abc
30
30
  import collections.abc as cabc
31
+ import dataclasses as dcls
31
32
  import types
32
33
 
33
34
  import typing_extensions as typx
35
+ # --- BEGIN: Injected by Copier ---
34
36
 
37
+ # --- END: Injected by Copier ---
35
38
 
36
- from absence import Absential, absent, is_absent
39
+
40
+ from absence import AbsentSingleton, Absential, absent, is_absent
37
41
 
38
42
 
39
43
  C = typx.TypeVar( 'C' ) # Class
@@ -20,19 +20,12 @@
20
20
 
21
21
  ''' Immutable data structures. '''
22
22
 
23
- # ruff: noqa: F401,F403
24
-
25
23
 
26
24
  from . import __
27
- from . import classes
28
- from . import dictionaries
29
- from . import exceptions
30
- from . import installers
31
- from . import modules
32
- from . import namespaces
33
- from . import objects
34
25
  from . import qaliases
35
- from . import sequences
26
+ # --- BEGIN: Injected by Copier ---
27
+ from . import exceptions
28
+ # --- END: Injected by Copier ---
36
29
 
37
30
  from .classes import *
38
31
  from .dictionaries import *
@@ -43,7 +36,7 @@ from .objects import *
43
36
  from .sequences import *
44
37
 
45
38
 
46
- __version__ = '2.0'
39
+ __version__ = '3.0'
47
40
 
48
41
 
49
42
  _attribute_visibility_includes_ = frozenset( ( '__version__', ) )
@@ -22,7 +22,8 @@
22
22
  ''' Immutable classes.
23
23
 
24
24
  Provides metaclasses for creating classes with immutable attributes. Once a
25
- class is initialized, its attributes cannot be reassigned or deleted.
25
+ class is initialized, new attributes may not be assigned to it and its
26
+ existing attributes cannot be reassigned or deleted.
26
27
 
27
28
  The implementation includes:
28
29
 
@@ -33,11 +34,7 @@
33
34
  * ``ProtocolClass``: Metaclass for protocol classes; derived from
34
35
  :py:class:`typing.Protocol`.
35
36
 
36
- These metaclasses are particularly useful for:
37
-
38
- * Creating classes with constant class attributes
39
- * Defining stable abstract base classes
40
- * Building protocol classes with fixed interfaces
37
+ Additionally, metaclasses for dataclasses are provided as a convenience.
41
38
 
42
39
  >>> from frigid import Class
43
40
  >>> class Example( metaclass = Class ):
@@ -106,6 +103,67 @@ Class.__doc__ = __.generate_docstring(
106
103
  'class attributes immutability' )
107
104
 
108
105
 
106
+ @__.typx.dataclass_transform( kw_only_default = True )
107
+ class Dataclass( Class ):
108
+ ''' Immutable dataclass factory. '''
109
+
110
+ def __new__( # pylint: disable=too-many-arguments
111
+ clscls: type[ Dataclass ],
112
+ name: str,
113
+ bases: tuple[ type, ... ],
114
+ namespace: dict[ str, __.typx.Any ], *,
115
+ decorators: ClassDecorators = ( ),
116
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
117
+ mutables: __.cabc.Collection[ str ] = ( ),
118
+ **args: __.typx.Any
119
+ ) -> Dataclass:
120
+ decorators_ = (
121
+ __.dcls.dataclass( kw_only = True, slots = True ),
122
+ *decorators )
123
+ return Class.__new__( # pyright: ignore
124
+ clscls, name, bases, namespace,
125
+ decorators = decorators_,
126
+ docstring = docstring,
127
+ mutables = mutables,
128
+ **args )
129
+
130
+ Dataclass.__doc__ = __.generate_docstring(
131
+ Dataclass,
132
+ 'description of class factory class',
133
+ 'class attributes immutability' )
134
+
135
+
136
+ @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
137
+ class CompleteDataclass( Class ):
138
+ ''' Immutable dataclass factory.
139
+
140
+ Dataclasses from this factory produce immutable instances. '''
141
+ def __new__( # pylint: disable=too-many-arguments
142
+ clscls: type[ CompleteDataclass ],
143
+ name: str,
144
+ bases: tuple[ type, ... ],
145
+ namespace: dict[ str, __.typx.Any ], *,
146
+ decorators: ClassDecorators = ( ),
147
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
148
+ mutables: __.cabc.Collection[ str ] = ( ),
149
+ **args: __.typx.Any
150
+ ) -> CompleteDataclass:
151
+ decorators_ = (
152
+ __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
153
+ *decorators )
154
+ return Class.__new__( # pyright: ignore
155
+ clscls, name, bases, namespace,
156
+ decorators = decorators_,
157
+ docstring = docstring,
158
+ mutables = mutables,
159
+ **args )
160
+
161
+ CompleteDataclass.__doc__ = __.generate_docstring(
162
+ CompleteDataclass,
163
+ 'description of class factory class',
164
+ 'class attributes immutability' )
165
+
166
+
109
167
  class ABCFactory( __.abc.ABCMeta ):
110
168
  ''' Immutable abstract base class factory. '''
111
169
 
@@ -185,6 +243,70 @@ ProtocolClass.__doc__ = __.generate_docstring(
185
243
  'class attributes immutability' )
186
244
 
187
245
 
246
+ # pylint: disable=bad-classmethod-argument,no-self-argument
247
+ @__.typx.dataclass_transform( kw_only_default = True )
248
+ class ProtocolDataclass( ProtocolClass ):
249
+ ''' Immutable protocol dataclass factory. '''
250
+ def __new__( # pylint: disable=too-many-arguments
251
+ clscls: type[ ProtocolDataclass ],
252
+ name: str,
253
+ bases: tuple[ type, ... ],
254
+ namespace: dict[ str, __.typx.Any ], *,
255
+ decorators: ClassDecorators = ( ),
256
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
257
+ mutables: __.cabc.Collection[ str ] = ( ),
258
+ **args: __.typx.Any
259
+ ) -> ProtocolDataclass:
260
+ decorators_ = (
261
+ __.dcls.dataclass( kw_only = True, slots = True ),
262
+ *decorators )
263
+ return ProtocolClass.__new__( # pyright: ignore
264
+ clscls, name, bases, namespace,
265
+ decorators = decorators_,
266
+ docstring = docstring,
267
+ mutables = mutables,
268
+ **args )
269
+ # pylint: enable=bad-classmethod-argument,no-self-argument
270
+
271
+ ProtocolDataclass.__doc__ = __.generate_docstring(
272
+ ProtocolDataclass,
273
+ 'description of class factory class',
274
+ 'class attributes immutability' )
275
+
276
+
277
+ # pylint: disable=bad-classmethod-argument,no-self-argument
278
+ @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
279
+ class CompleteProtocolDataclass( ProtocolClass ):
280
+ ''' Immutable protocol dataclass factory.
281
+
282
+ Dataclasses from this factory produce immutable instances. '''
283
+ def __new__( # pylint: disable=too-many-arguments
284
+ clscls: type[ CompleteProtocolDataclass ],
285
+ name: str,
286
+ bases: tuple[ type, ... ],
287
+ namespace: dict[ str, __.typx.Any ], *,
288
+ decorators: ClassDecorators = ( ),
289
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
290
+ mutables: __.cabc.Collection[ str ] = ( ),
291
+ **args: __.typx.Any
292
+ ) -> CompleteProtocolDataclass:
293
+ decorators_ = (
294
+ __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
295
+ *decorators )
296
+ return ProtocolClass.__new__( # pyright: ignore
297
+ clscls, name, bases, namespace,
298
+ decorators = decorators_,
299
+ docstring = docstring,
300
+ mutables = mutables,
301
+ **args )
302
+ # pylint: enable=bad-classmethod-argument,no-self-argument
303
+
304
+ CompleteProtocolDataclass.__doc__ = __.generate_docstring(
305
+ CompleteProtocolDataclass,
306
+ 'description of class factory class',
307
+ 'class attributes immutability' )
308
+
309
+
188
310
  def _accumulate_mutables(
189
311
  class_: type, mutables: __.cabc.Collection[ str ]
190
312
  ) -> frozenset[ str ]:
@@ -82,8 +82,7 @@ class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
82
82
  after creation. This provides a clean interface for dictionaries
83
83
  that should never change.
84
84
 
85
- Implementations must provide:
86
- - __getitem__, __iter__, __len__
85
+ Implementations must provide __getitem__, __iter__, __len__.
87
86
  '''
88
87
 
89
88
  @__.abc.abstractmethod
@@ -110,6 +109,8 @@ class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
110
109
  class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
111
110
  ''' Mix-in providing additional dictionary operations. '''
112
111
 
112
+ # TODO? Common __init__.
113
+
113
114
  def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
114
115
  if not isinstance( other, __.cabc.Mapping ): return NotImplemented
115
116
  conflicts = set( self.keys( ) ) & set( other.keys( ) )
@@ -170,6 +171,7 @@ class Dictionary( # pylint: disable=eq-without-hash
170
171
  _objects.Object, _DictionaryOperations[ __.H, __.V ]
171
172
  ):
172
173
  ''' Immutable dictionary. '''
174
+ # TODO: version 2.0: Do not subclass from 'Object'.
173
175
 
174
176
  __slots__ = ( '_data_', )
175
177
 
@@ -251,10 +253,6 @@ class Dictionary( # pylint: disable=eq-without-hash
251
253
 
252
254
  Dictionary.__doc__ = __.generate_docstring(
253
255
  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
258
256
 
259
257
 
260
258
  class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
@@ -23,11 +23,6 @@
23
23
  Provides a hierarchy of exceptions that are raised when immutability is
24
24
  violated. The hierarchy is designed to allow both specific and general
25
25
  exception handling.
26
-
27
- * ``Omniexception``: Base for all package exceptions
28
- * ``Omnierror``: Base for all package errors
29
- * ``AttributeImmutabilityError``: Raised for attribute modification
30
- * ``EntryImmutabilityError``: Raised for dictionary entry modification
31
26
  '''
32
27
 
33
28
 
@@ -59,7 +54,7 @@ class DecoratorCompatibilityError( Omnierror, TypeError ):
59
54
  def __init__( self, class_name: str, method_name: str ) -> None:
60
55
  # TODO: Use helper function to extract class name from class.
61
56
  super( ).__init__(
62
- f"Cannot apply immutable decorator to {class_name!r} "
57
+ f"Cannot apply immutability decorator to {class_name!r} "
63
58
  f"because it defines {method_name!r}.")
64
59
 
65
60
 
@@ -68,7 +63,7 @@ class EntryImmutabilityError( Omnierror, TypeError ):
68
63
 
69
64
  def __init__( self, key: __.cabc.Hashable ) -> None:
70
65
  super( ).__init__(
71
- f"Cannot assign or delete entry for {key!r}." )
66
+ f"Cannot add, alter, or remove entry for {key!r}." )
72
67
 
73
68
 
74
69
  class EntryValidityError( Omnierror, ValueError ):
@@ -80,10 +75,3 @@ class EntryValidityError( Omnierror, ValueError ):
80
75
  super( ).__init__(
81
76
  f"Cannot add invalid entry with key, {indicator!r}, "
82
77
  f"and value, {value!r}, to dictionary." )
83
-
84
-
85
- class OperationInvalidity( Omnierror, RuntimeError, TypeError ):
86
- ''' Attempt to perform invalid operation. '''
87
-
88
- def __init__( self, name: str ) -> None:
89
- super( ).__init__( f"Operation {name!r} is not valid on this object." )
@@ -66,18 +66,15 @@ def reclassify_modules(
66
66
  ) -> None:
67
67
  ''' Reclassifies modules to be immutable.
68
68
 
69
- This function converts existing modules to immutable modules, enforcing
70
- attribute immutability after conversion. It can operate on individual
71
- modules or entire package hierarchies.
69
+ Can operate on individual modules or entire package hierarchies.
72
70
 
73
71
  Notes
74
72
  -----
75
73
  * Only converts modules within the same package to prevent unintended
76
- modifications to external modules
74
+ modifications to external modules.
77
75
  * When used with a dictionary, converts any module objects found as
78
- values if they belong to the same package
79
- * Module conversion is permanent for the runtime session
80
- * Has no effect on already-immutable modules
76
+ values if they belong to the same package.
77
+ * Has no effect on already-immutable modules.
81
78
  '''
82
79
  from inspect import ismodule
83
80
  from sys import modules
@@ -0,0 +1,190 @@
1
+ # vim: set filetype=python fileencoding=utf-8:
2
+ # -*- coding: utf-8 -*-
3
+
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
+ #============================================================================#
19
+
20
+
21
+ # ruff: noqa: F811
22
+
23
+
24
+ # pylint: disable=line-too-long
25
+ ''' Immutable objects.
26
+
27
+ Provides a base class and decorator for creating objects with immutable
28
+ attributes. Once an object is initialized, its attributes cannot be modified
29
+ or deleted.
30
+
31
+ >>> from frigid import Object
32
+ >>> class Point( Object ):
33
+ ... def __init__( self, x, y ):
34
+ ... self.x = x
35
+ ... self.y = y
36
+ ... super( ).__init__( )
37
+ ...
38
+ >>> obj = Point( 1, 2 ) # Initialize with attributes
39
+ >>> obj.z = 3 # Attempt to add attribute
40
+ Traceback (most recent call last):
41
+ ...
42
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.
43
+ >>> obj.x = 4 # Attempt modification
44
+ Traceback (most recent call last):
45
+ ...
46
+ frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
47
+
48
+ The `immutable` decorator can be used to make any class immutable:
49
+
50
+ >>> from frigid import immutable
51
+ >>> @immutable
52
+ ... class Config:
53
+ ... def __init__( self, verbose = False ):
54
+ ... self.verbose = verbose
55
+ ...
56
+ >>> config = Config( verbose = True )
57
+ >>> config.verbose = False # Attempt to modify attribute
58
+ Traceback (most recent call last):
59
+ ...
60
+ frigid.exceptions.AttributeImmutabilityError: ...
61
+ '''
62
+ # pylint: enable=line-too-long
63
+
64
+
65
+ from . import __
66
+
67
+
68
+ _behavior = 'immutability'
69
+
70
+
71
+ def _check_behavior( obj: object ) -> bool:
72
+ behaviors: __.cabc.MutableSet[ str ]
73
+ if _check_dict( obj ):
74
+ attributes = getattr( obj, '__dict__' )
75
+ behaviors = attributes.get( '_behaviors_', set( ) )
76
+ else: behaviors = getattr( obj, '_behaviors_', set( ) )
77
+ return _behavior in behaviors
78
+
79
+
80
+ def _check_dict( obj: object ) -> bool:
81
+ # Return False even if '__dict__' in '__slots__'.
82
+ if hasattr( obj, '__slots__' ): return False
83
+ return hasattr( obj, '__dict__' )
84
+
85
+
86
+ @__.typx.overload
87
+ def immutable( # pragma: no branch
88
+ class_: type[ __.C ], *,
89
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
90
+ mutables: __.cabc.Collection[ str ] = ( )
91
+ ) -> type[ __.C ]: ...
92
+
93
+
94
+ @__.typx.overload
95
+ def immutable( # pragma: no branch
96
+ class_: __.AbsentSingleton, *,
97
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
98
+ mutables: __.cabc.Collection[ str ] = ( )
99
+ ) -> __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]: ...
100
+
101
+
102
+ def immutable( # pylint: disable=too-complex,too-many-statements
103
+ class_: __.Absential[ type[ __.C ] ] = __.absent, *,
104
+ docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
105
+ mutables: __.cabc.Collection[ str ] = ( )
106
+ ) -> __.typx.Union[
107
+ type[ __.C ], __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]
108
+ ]:
109
+ ''' Decorator which makes class immutable after initialization.
110
+
111
+ Cannot be applied to classes which define their own __setattr__
112
+ or __delattr__ methods.
113
+
114
+ This decorator can be used in different ways:
115
+
116
+ 1. Simple decorator:
117
+
118
+ >>> @immutable
119
+ ... class Config:
120
+ ... pass
121
+
122
+ 2. With parameters:
123
+
124
+ >>> @immutable( mutables = ( 'version', ) )
125
+ ... class Config:
126
+ ... pass
127
+ '''
128
+ def decorator( cls: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-many-statements
129
+ if not __.is_absent( docstring ): cls.__doc__ = docstring
130
+ for method in ( '__setattr__', '__delattr__' ):
131
+ if method in cls.__dict__:
132
+ from .exceptions import DecoratorCompatibilityError
133
+ raise DecoratorCompatibilityError( cls.__name__, method )
134
+ original_init = next(
135
+ base.__dict__[ '__init__' ] for base in cls.__mro__
136
+ if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
137
+ mutables_ = frozenset( mutables )
138
+
139
+ def __init__(
140
+ self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
141
+ ) -> None:
142
+ original_init( self, *posargs, **nomargs )
143
+ behaviors: __.cabc.MutableSet[ str ]
144
+ if _check_dict( self ):
145
+ attributes = getattr( self, '__dict__' )
146
+ behaviors = attributes.get( '_behaviors_', set( ) )
147
+ if not behaviors: attributes[ '_behaviors_' ] = behaviors
148
+ else:
149
+ behaviors = getattr( self, '_behaviors_', set( ) )
150
+ if not behaviors: setattr( self, '_behaviors_', behaviors )
151
+ behaviors.add( _behavior )
152
+
153
+ def __delattr__( self: object, name: str ) -> None:
154
+ if name in mutables_:
155
+ super( cls, self ).__delattr__( name )
156
+ return
157
+ if _check_behavior( self ): # pragma: no branch
158
+ from .exceptions import AttributeImmutabilityError
159
+ raise AttributeImmutabilityError( name )
160
+ super( cls, self ).__delattr__( name ) # pragma: no cover
161
+
162
+ def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
163
+ if name in mutables_:
164
+ super( cls, self ).__setattr__( name, value )
165
+ return
166
+ if _check_behavior( self ):
167
+ from .exceptions import AttributeImmutabilityError
168
+ raise AttributeImmutabilityError( name )
169
+ super( cls, self ).__setattr__( name, value )
170
+
171
+ cls.__init__ = __init__
172
+ cls.__delattr__ = __delattr__
173
+ cls.__setattr__ = __setattr__
174
+ return cls
175
+
176
+ if not __.is_absent( class_ ): return decorator( class_ )
177
+ return decorator # No class to decorate; keyword arguments only.
178
+
179
+
180
+ @immutable
181
+ class Object:
182
+ ''' Immutable objects. '''
183
+
184
+ __slots__ = ( '__dict__', '_behaviors_' )
185
+
186
+ def __repr__( self ) -> str:
187
+ return "{fqname}( )".format( fqname = __.calculate_fqname( self ) )
188
+
189
+ Object.__doc__ = __.generate_docstring(
190
+ Object, 'instance attributes immutability' )
@@ -42,9 +42,13 @@
42
42
 
43
43
  from . import __
44
44
  from .classes import (
45
- ABCFactory as ImmutableABCFactory,
46
- Class as ImmutableClass,
47
- ProtocolClass as ImmutableProtocolClass,
45
+ ABCFactory as ImmutableABCFactory,
46
+ Class as ImmutableClass,
47
+ CompleteDataclass as ImmutableCompleteDataclass,
48
+ CompleteProtocolDataclass as ImmutableCompleteProtocolDataclass,
49
+ Dataclass as ImmutableDataclass,
50
+ ProtocolClass as ImmutableProtocolClass,
51
+ ProtocolDataclass as ImmutableProtocolDataclass,
48
52
  )
49
53
  from .dictionaries import (
50
54
  AbstractDictionary as AbstractImmutableDictionary,
@@ -60,5 +64,5 @@ from .namespaces import (
60
64
  )
61
65
  from .objects import (
62
66
  Object as ImmutableObject,
63
- immutable,
67
+ immutable,
64
68
  )
@@ -1,129 +0,0 @@
1
- # vim: set filetype=python fileencoding=utf-8:
2
- # -*- coding: utf-8 -*-
3
-
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
- #============================================================================#
19
-
20
-
21
- # pylint: disable=line-too-long
22
- ''' Immutable objects.
23
-
24
- Provides a base class and decorator for creating objects with immutable
25
- attributes. Once an object is initialized, its attributes cannot be modified
26
- or deleted.
27
-
28
- The implementation uses a special dictionary type for attribute storage that
29
- enforces immutability. This makes it suitable for:
30
-
31
- * Configuration objects
32
- * Value objects
33
- * Immutable data containers
34
- * Objects requiring attribute stability
35
-
36
- >>> from frigid import Object
37
- >>> class Point( Object ):
38
- ... def __init__( self, x, y ):
39
- ... self.x = x
40
- ... self.y = y
41
- ... super( ).__init__( )
42
- ...
43
- >>> obj = Point( 1, 2 ) # Initialize with attributes
44
- >>> obj.z = 3 # Attempt to add attribute
45
- Traceback (most recent call last):
46
- ...
47
- frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.
48
- >>> obj.x = 4 # Attempt modification
49
- Traceback (most recent call last):
50
- ...
51
- frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
52
- '''
53
- # pylint: enable=line-too-long
54
-
55
-
56
- from . import __
57
-
58
-
59
- def _check_behavior( obj: object ) -> bool:
60
- behaviors: __.cabc.MutableSet[ str ]
61
- if _check_dict( obj ):
62
- attributes = getattr( obj, '__dict__' )
63
- behaviors = attributes.get( '_behaviors_', set( ) )
64
- else: behaviors = getattr( obj, '_behaviors_', set( ) )
65
- return __.behavior_label in behaviors
66
-
67
-
68
- def _check_dict( obj: object ) -> bool:
69
- # Return False even if '__dict__' in '__slots__'.
70
- if hasattr( obj, '__slots__' ): return False
71
- return hasattr( obj, '__dict__' )
72
-
73
-
74
- def immutable( class_: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-complex
75
- ''' Decorator which makes class immutable after initialization.
76
-
77
- Cannot be applied to classes which define their own __setattr__
78
- or __delattr__ methods.
79
- '''
80
- for method in ( '__setattr__', '__delattr__' ):
81
- if method in class_.__dict__:
82
- from .exceptions import DecoratorCompatibilityError
83
- raise DecoratorCompatibilityError( class_.__name__, method )
84
- original_init = next(
85
- base.__dict__[ '__init__' ] for base in class_.__mro__
86
- if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
87
-
88
- def __init__(
89
- self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
90
- ) -> None:
91
- # TODO: Use accretive set for behaviors.
92
- original_init( self, *posargs, **nomargs )
93
- behaviors: __.cabc.MutableSet[ str ]
94
- if _check_dict( self ):
95
- attributes = getattr( self, '__dict__' )
96
- behaviors = attributes.get( '_behaviors_', set( ) )
97
- if not behaviors: attributes[ '_behaviors_' ] = behaviors
98
- setattr( self, '__dict__', __.ImmutableDictionary( attributes ) )
99
- else:
100
- behaviors = getattr( self, '_behaviors_', set( ) )
101
- if not behaviors: setattr( self, '_behaviors_', behaviors )
102
- behaviors.add( __.behavior_label )
103
-
104
- def __delattr__( self: object, name: str ) -> None:
105
- if _check_behavior( self ):
106
- from .exceptions import AttributeImmutabilityError
107
- raise AttributeImmutabilityError( name )
108
- super( class_, self ).__delattr__( name )
109
-
110
- def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
111
- if _check_behavior( self ):
112
- from .exceptions import AttributeImmutabilityError
113
- raise AttributeImmutabilityError( name )
114
- super( class_, self ).__setattr__( name, value )
115
-
116
- class_.__init__ = __init__
117
- class_.__delattr__ = __delattr__
118
- class_.__setattr__ = __setattr__
119
- return class_
120
-
121
-
122
- @immutable
123
- class Object:
124
- ''' Immutable objects. '''
125
-
126
- __slots__ = ( '__dict__', '_behaviors_' )
127
-
128
- Object.__doc__ = __.generate_docstring(
129
- Object, 'instance attributes immutability' )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes