classicist 1.0.0__tar.gz → 1.0.1__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.
Files changed (45) hide show
  1. classicist-1.0.1/PKG-INFO +501 -0
  2. classicist-1.0.1/README.md +468 -0
  3. {classicist-1.0.0 → classicist-1.0.1}/pyproject.toml +4 -2
  4. {classicist-1.0.0 → classicist-1.0.1}/requirements.development.txt +2 -1
  5. classicist-1.0.1/source/classicist/__init__.py +50 -0
  6. classicist-1.0.1/source/classicist/decorators/__init__.py +20 -0
  7. classicist-1.0.1/source/classicist/decorators/aliased/__init__.py +74 -0
  8. classicist-1.0.1/source/classicist/decorators/annotation/__init__.py +55 -0
  9. classicist-1.0.1/source/classicist/decorators/classproperty/__init__.py +41 -0
  10. classicist-1.0.1/source/classicist/decorators/deprecated/__init__.py +101 -0
  11. {classicist-1.0.0/source/classicist → classicist-1.0.1/source/classicist/decorators/hybridmethod}/__init__.py +2 -63
  12. classicist-1.0.1/source/classicist/decorators/nocache/__init__.py +10 -0
  13. classicist-1.0.1/source/classicist/exceptions/__init__.py +12 -0
  14. classicist-1.0.1/source/classicist/exceptions/decorators/__init__.py +7 -0
  15. classicist-1.0.1/source/classicist/exceptions/decorators/aliased/__init__.py +2 -0
  16. classicist-1.0.1/source/classicist/exceptions/decorators/annotation/__init__.py +2 -0
  17. classicist-1.0.1/source/classicist/exceptions/metaclasses/__init__.py +5 -0
  18. classicist-1.0.1/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +2 -0
  19. classicist-1.0.1/source/classicist/inspector/__init__.py +38 -0
  20. classicist-1.0.1/source/classicist/logging/__init__.py +3 -0
  21. classicist-1.0.1/source/classicist/metaclasses/__init__.py +7 -0
  22. classicist-1.0.1/source/classicist/metaclasses/aliased/__init__.py +40 -0
  23. classicist-1.0.1/source/classicist/metaclasses/shadowproof/__init__.py +36 -0
  24. classicist-1.0.1/source/classicist/version.txt +1 -0
  25. classicist-1.0.1/source/classicist.egg-info/PKG-INFO +501 -0
  26. classicist-1.0.1/source/classicist.egg-info/SOURCES.txt +38 -0
  27. {classicist-1.0.0 → classicist-1.0.1}/source/classicist.egg-info/requires.txt +1 -0
  28. classicist-1.0.1/tests/test_aliased.py +226 -0
  29. classicist-1.0.1/tests/test_annotation.py +100 -0
  30. {classicist-1.0.0 → classicist-1.0.1}/tests/test_classproperty.py +1 -1
  31. classicist-1.0.1/tests/test_deprecated.py +117 -0
  32. classicist-1.0.1/tests/test_shadowproof.py +41 -0
  33. classicist-1.0.0/PKG-INFO +0 -257
  34. classicist-1.0.0/README.md +0 -227
  35. classicist-1.0.0/source/classicist/version.txt +0 -1
  36. classicist-1.0.0/source/classicist.egg-info/PKG-INFO +0 -257
  37. classicist-1.0.0/source/classicist.egg-info/SOURCES.txt +0 -16
  38. {classicist-1.0.0 → classicist-1.0.1}/LICENSE.md +0 -0
  39. {classicist-1.0.0 → classicist-1.0.1}/requirements.distribution.txt +0 -0
  40. {classicist-1.0.0 → classicist-1.0.1}/requirements.txt +0 -0
  41. {classicist-1.0.0 → classicist-1.0.1}/setup.cfg +0 -0
  42. {classicist-1.0.0 → classicist-1.0.1}/source/classicist.egg-info/dependency_links.txt +0 -0
  43. {classicist-1.0.0 → classicist-1.0.1}/source/classicist.egg-info/top_level.txt +0 -0
  44. {classicist-1.0.0 → classicist-1.0.1}/source/classicist.egg-info/zip-safe +0 -0
  45. {classicist-1.0.0 → classicist-1.0.1}/tests/test_hybridmethod.py +0 -0
@@ -0,0 +1,501 @@
1
+ Metadata-Version: 2.4
2
+ Name: classicist
3
+ Version: 1.0.1
4
+ Summary: Classy class decorators for Python.
5
+ Author: Daniel Sissman
6
+ License-Expression: MIT
7
+ Project-URL: documentation, https://github.com/bluebinary/classicist/blob/main/README.md
8
+ Project-URL: changelog, https://github.com/bluebinary/classicist/blob/main/CHANGELOG.md
9
+ Project-URL: repository, https://github.com/bluebinary/classicist
10
+ Project-URL: issues, https://github.com/bluebinary/classicist/issues
11
+ Project-URL: homepage, https://github.com/bluebinary/classicist
12
+ Keywords: decorator,hybrid method,class method,instance method,class property,class properties,annotations,deprecations,aliases,shadow proofing
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.md
23
+ Provides-Extra: development
24
+ Requires-Dist: black==24.10.*; extra == "development"
25
+ Requires-Dist: pytest==8.3.*; extra == "development"
26
+ Requires-Dist: pytest-codeblocks==0.17.0; extra == "development"
27
+ Requires-Dist: pyflakes; extra == "development"
28
+ Provides-Extra: distribution
29
+ Requires-Dist: build; extra == "distribution"
30
+ Requires-Dist: twine; extra == "distribution"
31
+ Requires-Dist: wheel; extra == "distribution"
32
+ Dynamic: license-file
33
+
34
+ # Classicist: Classy Class Decorators & Extensions
35
+
36
+ The Classicist library provides several useful decorators and helper methods including:
37
+
38
+ * `@hybridmethod` – a decorator that allows methods to be used both as class methods and as instance methods;
39
+ * `@classproperty` – a decorator that allow class methods to be accessed as class properties;
40
+ * `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
41
+ * `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
42
+ * `@alias` – a decorator that can be used to add aliases to class methods;
43
+ * `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
44
+ * `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
45
+ being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
46
+
47
+ The `classicist` library was previously named `hybridmethod` so if a prior version had
48
+ been installed, please update references to the new library name. Installation of the
49
+ library via its old name, `hybridmethod`, will install the new `classicist` library with
50
+ a mapping for backwards compatibility so that code continues to function as before.
51
+
52
+ ### Requirements
53
+
54
+ The Classicist library has been tested with Python 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14.
55
+ The library is not compatible with Python 3.8 or earlier.
56
+
57
+ ### Installation
58
+
59
+ The Classicist library is available from PyPI, so may be added to a project's dependencies
60
+ via its `requirements.txt` file or similar by referencing the Classicist library's name,
61
+ `classicist`, or the library may be installed directly into your local runtime environment
62
+ using `pip` via the `pip install` command by entering the following into your shell:
63
+
64
+ $ pip install classicist
65
+
66
+ #### Hybrid Methods
67
+
68
+ The Classicist library provides a `@hybridmethod` method decorator that allows methods
69
+ defined in a class to be used as both class methods and as instance methods.
70
+
71
+ The `@hybridmethod` decorator provided by the library wraps methods defined in classes
72
+ using the usual `@` decorator syntax. Methods defined in classes that are decorated with
73
+ the `@hybridmethod` decorator can then be accessed as both class methods and as instance
74
+ methods, with the first argument passed to the method being a reference to either the
75
+ class when the method is called as a class method or to the instance when the method is
76
+ called as an instance method.
77
+
78
+ If a class-level property is defined and then an instance-level property is created with
79
+ the same name that shadows the class-level property, the hybrid method can be used to
80
+ interact with both the class-level property and the instance-level property simply based
81
+ on whether the hybrid method was called directly on the class or on an a class instance.
82
+
83
+ If desired, a simple check of the value of the first variable passed to a hybrid method
84
+ using `isinstance(<variable>, <class>)` allows one to determine if the call was made on
85
+ an instance of the class in which case `isinstance()` evaluates to `True` or if the call
86
+ was made on the class itself, in which case `isinstance()` evaluates to `False`.
87
+
88
+ The variable passed as the first argument to the method may have any name, including as
89
+ is common in Python, `self`, although the use of `self` as the name of this argument on
90
+ an instance method is just customary and the name has no significance.
91
+
92
+ If using the `isinstance(<variable>, <class>)` check as described above, substitute in
93
+ the name of the first argument variable of a hybrid method for the `<variable>` place
94
+ holder and the name of the class for the `<class>` place holder.
95
+
96
+ ##### Hybrid Methods: Usage
97
+
98
+ To use the `@hybridmethod` decorator import the decorator from the `classicist` library
99
+ and use it to decorate the class methods you wish to use as both class methods and
100
+ instance methods:
101
+
102
+ ```python
103
+ from classicist import hybridmethod
104
+
105
+ class hybridcollection(object):
106
+ """An example class to demonstrate one possible use of a hybridmethod; here we have
107
+ a list maintained at the class-level, accessible by all class instances as well as
108
+ available directly on the class itself, as well as instance-level lists maintained
109
+ individually by each instance of the class. The hybridmethod decorator allows the
110
+ same methods to operate on the lists, affecting the relevant list, either the class
111
+ or instance level list, based on whether the call was made directly on the class or
112
+ if the call was made on an instance of the class."""
113
+
114
+ items: list[str] = []
115
+
116
+ def __init__(self):
117
+ # Create an 'items' instance variable; note that this shadows the class variable
118
+ # of the same name which can still be accessed directly via self.__class__.items
119
+ self.items: list[object] = []
120
+
121
+ @hybridmethod
122
+ def add_item(self, item: object):
123
+ # We can use the following line to differentiate between the call being made on
124
+ # an instance or directly on the class; isinstance(self, <class>) returns True
125
+ # if the method was called on an instance of the class, or False if the method
126
+ # was called on the class directly; the 'self' variable will reference either
127
+ # the instance or the class; although 'self' is traditionally used in Python as
128
+ # reference to the instance
129
+ if isinstance(self, hybridcollection):
130
+ self.items.append(item)
131
+ else:
132
+ self.items.append(item)
133
+
134
+ def get_class_items(self) -> list[object]:
135
+ return self.__class__.items
136
+
137
+ def get_instance_items(self) -> list[object]:
138
+ return self.items
139
+
140
+ def get_combined_items(self) -> list[object]:
141
+ return self.__class__.items + self.items
142
+
143
+ hybridcollection.add_item("ABC") # Add an item to the class-level items list
144
+
145
+ collection = hybridcollection()
146
+
147
+ collection.add_item("XYZ") # Add an item to the instance-level items list
148
+
149
+ assert collection.get_class_items() == ["ABC"]
150
+
151
+ assert collection.get_instance_items() == ["XYZ"]
152
+
153
+ assert collection.get_combined_items() == ["ABC", "XYZ"]
154
+ ```
155
+
156
+ #### Class Properties
157
+
158
+ The Classicist library provides a `@classproperty` method decorator that allows class
159
+ methods to be accessed as class properties.
160
+
161
+ The `@classproperty` decorator provided by the library wraps methods defined in classes
162
+ using the usual `@` decorator syntax. Methods defined in classes that are decorated with
163
+ the `@classproperty` decorator can then be accessed as though they are real properties
164
+ on the class.
165
+
166
+ The `@classproperty` decorator addresses the removal in Python 3.13 of the prior support
167
+ for combining the `@classmethod` and `@property` decorators to create class properties;
168
+ a change which was made due to complexity in the underlying interpreter implementation.
169
+
170
+ ##### Class Properties: Usage
171
+
172
+ To use the `@classproperty` decorator import the decorator from the `classicist` library
173
+ and use it to decorate any class methods you wish to access as class properties.
174
+
175
+ ```python
176
+ from classicist import classproperty
177
+
178
+ class exampleclass(object):
179
+ @classproperty
180
+ def greeting(cls) -> str:
181
+ """The 'greeting' class method has been decorated with classproperty so acts as
182
+ a property; we can do some potentially complex work to compute return value."""
183
+ return "hello"
184
+
185
+ assert isinstance(exampleclass, type)
186
+ assert issubclass(exampleclass, exampleclass)
187
+ assert issubclass(exampleclass, object)
188
+
189
+ # We can now access `.greeting` as though it was defined as a property.
190
+ # The return value of `.greeting` is indiscernible from the value being returned
191
+ assert isinstance(exampleclass.greeting, str)
192
+ assert exampleclass.greeting == "hello"
193
+ ```
194
+
195
+ ⚠️ An important caveat regarding class properties which applies equally to the method of
196
+ supporting class properties provided by this library, and to class properties which are
197
+ supported natively in Python 3.9 – 3.12 by combining the `@classmethod` and `@property`
198
+ decorators, is that unfortunately unless a custom metaclass is used to intervene, class
199
+ properties can be overwritten by value assignment, just like regular attributes can be.
200
+
201
+ This is a result of differences in Python's handling for descriptors between classes and
202
+ instances of classes. For both classes and instances, the `__get__` descriptor is called
203
+ while the `__set__` and `__delete__` descriptor methods will only be called on instances
204
+ such that we have no way to be involved in the property reassignment or deletion process
205
+ as would be the case for properties on instances where we can create our own setter and
206
+ deleter methods in addition to the getter.
207
+
208
+ This caveat can be remedied through a custom metaclass however, which overrides default
209
+ behaviour, and is able to intercept the `__setattr__` and `__delattr__` calls as needed.
210
+
211
+ The two code samples below illustrate the creation of a class property, `greeting`, via
212
+ this library's `@classproperty` decorator, and compares this to a class property created
213
+ natively in supported versions of Python by combining the `@classmethod` and `@property`
214
+ decorators. The code samples then highlight the possibility in both cases of overwriting
215
+ a class property by assigning a new value. The class property will be overwritten due to
216
+ standard attribute assignment behaviour. As such, whether using natively supported class
217
+ properties created by combining the `@classmethod` and `@property` decorators in Python
218
+ versions that support such class properties, or if using the `@classproperty` decorator
219
+ offered by this library, one must be mindful that a class property can be overwritten by
220
+ value assignment, unless one uses a custom metaclass to prevent such behaviour:
221
+
222
+ ```python
223
+ from classicist import classproperty
224
+
225
+ class exampleclass(object):
226
+ @classproperty
227
+ def greeting(cls) -> str:
228
+ # Generate a return value here
229
+ return "hello"
230
+
231
+ # We can access `.greeting` as though it was defined as a property:
232
+ assert exampleclass.greeting == "hello"
233
+
234
+ # Note: The `.greeting` property will be reassigned to the new value, "goodbye":
235
+ exampleclass.greeting = "goodbye"
236
+ assert exampleclass.greeting == "goodbye"
237
+ ```
238
+
239
+ As can be seen with the method of natively supporting class properties, class properties
240
+ can also have their values reassigned without warning in just the same way:
241
+
242
+ ```python
243
+ import sys
244
+ import pytest
245
+
246
+ # As Python only natively supported combining @classmethod and @property between version
247
+ # 3.9 and 3.12, the example below is not usable on other versions, such as 3.13+
248
+ if (sys.version_info.major == 3) and not (9 <= sys.version_info.minor <= 12):
249
+ pytest.skip("This test can only run on Python version 3.9 – 3.12")
250
+
251
+ class exampleclass(object):
252
+ @classmethod
253
+ @property
254
+ def greeting(cls) -> str:
255
+ # Generate a return value here
256
+ return "hello"
257
+
258
+ # We can access `.greeting` as though it was defined as a property:
259
+ assert exampleclass.greeting == "hello"
260
+
261
+ # Note: The `.greeting` property will be reassigned to the new value, "goodbye":
262
+ exampleclass.greeting = "goodbye"
263
+ assert exampleclass.greeting == "goodbye"
264
+ ```
265
+
266
+ #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
267
+
268
+ The `@alias` decorator can be used to add method name aliases to methods defined within
269
+ classes, such that both the original name and any defined aliases can be used to access
270
+ the method at runtime. The `@alias` decorator cannot be used for methods defined outside
271
+ of classes as the aliases are created as additional class attributes scoped to the class.
272
+
273
+ To use the `@alias` decorator, it is necessary to set the containing class' metaclass to
274
+ the `aliased` metaclass provided by the `classicist` library; the metaclass iterates
275
+ through the class namespace during parse time and sets up the aliases as additional
276
+ attributes on the class so that the aliased methods are available at runtime via both
277
+ their original name and their aliases.
278
+
279
+ The example below demonstrates adding an alias to a method defined within a class, and
280
+ using the `aliased` metaclass when defining the class to ensure that the alias is parsed
281
+ and translated to an additional class attribute so that the method is accessible via its
282
+ original name and the alias at runtime.
283
+
284
+ ```python
285
+ from classicist import aliased, alias, is_aliased, aliases
286
+
287
+ class Welcome(object, metaclass=aliased):
288
+ @alias("greet")
289
+ def hello(self, name: str):
290
+ return f"Hello {name}!"
291
+
292
+ assert is_aliased(Welcome.hello) is True
293
+
294
+ assert aliases(Welcome.hello) == ["greet"]
295
+
296
+ assert Welcome.hello is Welcome.greet
297
+
298
+ welcome = Welcome()
299
+
300
+ assert isinstance(welcome, Welcome)
301
+
302
+ assert welcome.hello("you") == "Hello you!"
303
+ assert welcome.greet("you") == "Hello you!"
304
+ ```
305
+
306
+ ⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
307
+ other function and method names and aliases cannot be reserved keywords. If an invalid
308
+ alias is specified an `AliasError` exception will be raised at runtime.
309
+
310
+ #### Annotation Decorator: Add Arbitrary Annotations to Code Objects
311
+
312
+ The `@annotation` decorator can be used to assign arbitrary annotations to mutable code
313
+ objects including classes, methods, functions and most objects, with the exception of
314
+ immutable objects that do not allow their attributes to be modified. The annotations
315
+ can be used for any purpose, such as to assist with generating documentation for the
316
+ annotated code objects, or for storing addition metadata on the code objects themselves
317
+ which can be accessed later.
318
+
319
+ Annotations applied to a code object using the `@annotation` decorator can be accessed via
320
+ the `annotations()` helper method which provides easy access to the assigned annotations:
321
+
322
+ ```python
323
+ from classicist import annotation, annotations
324
+
325
+ class Test(object):
326
+ @annotation(added="01/12/2026")
327
+ def new(self):
328
+ pass
329
+
330
+ assert annotations(Test.new) == dict(added="01/12/2026")
331
+ ```
332
+
333
+ #### Deprecation Decorator: Mark Functions and Methods as Deprecated
334
+
335
+ The `@deprecated` decorator can be used to mark code objects such as methods and functions
336
+ as deprecated and for checking deprecated status of such objects via the `is_deprecated`
337
+ helper method.
338
+
339
+ The `@deprecated` decorator and `is_deprecated` helper method can be used as follows:
340
+
341
+ ```python
342
+ from classicist import deprecated, is_deprecated
343
+
344
+ class Test(object):
345
+ @deprecated
346
+ def old(self):
347
+ pass
348
+
349
+ def new(self):
350
+ pass
351
+
352
+ assert is_deprecated(Test.old) is True
353
+ assert is_deprecated(Test.new) is False
354
+ ```
355
+
356
+ One can also add arbitrary annotations via the `@deprecated` decorator, specifying each
357
+ annotation as a keyword argument. The `@deprecated` decorator supports several optional
358
+ annotations by default, and these can be used to note common attributes of a deprecation
359
+ including when the deprecation began, the reason for the deprecation, when the deprecated
360
+ code will be removed, a reference to its replacement functionality (if applicable), and
361
+ advice on the replacement functionality's use, and a reference to ticket (if applicable)
362
+ tracking the deprecation. These default annotations may be specified by using the following
363
+ keyword arguments on the `@deprecated` decorator:
364
+
365
+ * `reason` (`str`) – The optional `reason` keyword argument can be used to specify a
366
+ reason note for the deprecation which can be useful for users to understand the change
367
+ and can also be obtained from the deprecation annotation for use in documentation.
368
+
369
+ * `since` (`str` | `datetime.datetime`) – The optional `since` keyword argument can be
370
+ used to specify when the date for when the deprecation began; the argument can accept
371
+ a string formatted date or a `datetime.datetime` instance. The `since` value serves to
372
+ note when the deprecation began which can be useful in cases where there is a standard
373
+ deprecation window of say six-twelve months before deprecated code is removed. The date
374
+ is visible in the deprecation annotation and can also be obtained for use in documentation.
375
+
376
+ * `removal` (`str` | `datetime.datetime`) – The optional `removal` keyword argument can be
377
+ used to specify when the date for when the deprecated code will be removed; the argument
378
+ can accept a string formatted date or a `datetime.datetime` instance. The `removal` value
379
+ serves to note when the deprecation began which can be useful in cases where there is a
380
+ standard deprecation window of say six-twelve months before deprecated code is removed.
381
+ The date is visible at the site of the deprecation and can also be obtained for use in
382
+ documentation.
383
+
384
+ * `replacement` (`str`) – The optional `replacement` keyword argument can be used to
385
+ specify a note about the replacement functionality (if applicable) that can be used
386
+ instead of the deprecated functionality. The replacement note is visible at the site
387
+ of the deprecation and can also be obtained for use in documentation.
388
+
389
+ * `advice` (`str`) – The optional `advice` keyword argument can be used to specify any
390
+ relevant advice about the replacement functionality (if applicable) that can be used
391
+ instead of the deprecated functionality. The advice note is visible at the site of the
392
+ deprecation and can also be obtained for use in documentation.
393
+
394
+ * `ticket` (`str`) – The optional `ticket` keyword argument can be used to specify a
395
+ reference to a ticket number or a ticket URL that is being used to track the deprecation.
396
+ The ticket value is visible at the site of the deprecation and can also be obtained for
397
+ use in documentation.
398
+
399
+ In addition to the default annotations, any other desired annotation can be added to via
400
+ the `@deprecated` decorator by specifying it as an additional keyword argument value. All
401
+ keyword argument values must be valid keyword argument identifiers and not be reserved words.
402
+
403
+ ```python
404
+ from classicist import deprecated, is_deprecated, annotations
405
+
406
+ class Test(object):
407
+ @deprecated(since="01/01/2026")
408
+ def old(self):
409
+ pass
410
+
411
+ def new(self):
412
+ pass
413
+
414
+ assert is_deprecated(Test.old) is True
415
+ assert is_deprecated(Test.new) is False
416
+
417
+ # The annotations can be obtained and accessed by using the `annotations` helper method:
418
+ assert annotations(Test.old) == dict(since="01/01/2026")
419
+ ```
420
+
421
+ #### No Cache Decorator: Mark Functions and Methods as "Not Cacheable"
422
+
423
+ The `@nocache` decorator can be used to mark functions and methods as not being suitable
424
+ for caching via say `functools.cache`.
425
+
426
+ ⚠️ Note: The `@nocache` decorator does not prevent caching via mechanisms such as the
427
+ `functools.cache` decorator, but rather acts as a clear note directly in code that the
428
+ function or method should not be cached via such means.
429
+
430
+ The `@nocache` decorator can be used as follows:
431
+
432
+ ```python
433
+ from classicist import nocache
434
+
435
+ class Test(object):
436
+ @nocache
437
+ def computation(self) -> int:
438
+ pass
439
+ ```
440
+
441
+ #### ShadowProof: Attribute Shadowing Protection Metaclass
442
+
443
+ The `shadowproof` metaclass can be used to protect classes and subclasses from attribute
444
+ -shadowing. The issue is usually caused by a subclass unintentionally redefining or
445
+ overwriting an attribute value that has been inherited from a superclass and can
446
+ otherwise be quite difficult to debug, as it may lead to unexpected behaviour in either
447
+ the superclass or subclass without an immediately obvious cause. Python does not issue
448
+ any warnings or raise any errors when most attributes are overwritten, aside from special
449
+ cases mostly in the standard library on immutable objects. The `shadowproof` metaclass
450
+ helps solve this issue by raising an `AttributeShadowingError` when this happens.
451
+
452
+ To use the `shadowproof` metaclass to protect a class and its subclasses, implement code
453
+ similar to the following, by importing the `shadowproof` metaclass and assigning it as
454
+ the metaclass for the class and subclasses you want to protect:
455
+
456
+ ```python
457
+ from classicist import shadowproof, AttributeShadowingError
458
+
459
+ class Test(object, metaclass=shadowproof):
460
+ example: int = 123
461
+
462
+ try:
463
+ class SubTest(Test):
464
+ example: str = "hello"
465
+ except AttributeShadowingError as exception:
466
+ # The AttributeShadowingError is expected as the `example` attribute was modified!
467
+ pass
468
+ ```
469
+
470
+ ### Unit Tests
471
+
472
+ The Classicist library includes a suite of comprehensive unit tests which ensure that
473
+ the library functionality operates as expected. The unit tests were developed with and
474
+ are run via `pytest`.
475
+
476
+ To ensure that the unit tests are run within a predictable runtime environment where all
477
+ of the necessary dependencies are available, a [Docker](https://www.docker.com) image is
478
+ created within which the tests are run. To run the unit tests, ensure Docker and Docker
479
+ Compose is [installed](https://docs.docker.com/engine/install/), and perform the following
480
+ commands, which will build the Docker image via `docker compose build` and then run the
481
+ tests via `docker compose run` – the output of running the tests will be displayed:
482
+
483
+ ```shell
484
+ $ docker compose build
485
+ $ docker compose run tests
486
+ ```
487
+
488
+ To run the unit tests with optional command line arguments being passed to `pytest`, append
489
+ the relevant arguments to the `docker compose run tests` command, as follows, for example
490
+ passing `-vv` to enable verbose output:
491
+
492
+ ```shell
493
+ $ docker compose run tests -vv
494
+ ```
495
+
496
+ See the documentation for [PyTest](https://docs.pytest.org/en/latest/) regarding available
497
+ optional command line arguments.
498
+
499
+ ### Copyright & License Information
500
+
501
+ Copyright © 2025-2026 Daniel Sissman; licensed under the MIT License.