classicist 1.0.0__py3-none-any.whl

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.
classicist/__init__.py ADDED
@@ -0,0 +1,118 @@
1
+ import logging
2
+
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class hybridmethod(object):
8
+ """The 'hybridmethod' decorator allows a method to be used as both a class method
9
+ and an instance method. The hybridmethod class decorator can wrap methods defined
10
+ in classes using the usual @decorator syntax. Methods defined in classes that are
11
+ decorated with the @hybridmethod decorator can be accessed as both class methods
12
+ and as instance methods, with the first argument passed to the method being the
13
+ reference to either the class when the method is called as a class method or to
14
+ the instance when the method is called as an instance method.
15
+
16
+ A check of the value of the first variable using isinstance(<variable>, <class>) can
17
+ be used within a hybrid method to determine if the call was made on an instance of
18
+ the class in which case the isinstance() call would evalute to True or if the call
19
+ was made on the class itself, in which case isinstance() would evaluate to False.
20
+ The variable passed as the first argument to the method may have any name, including
21
+ 'self', as in Python, the use of 'self' as the name of the first argument on an
22
+ instance method is just customary and the name has no significance like it does in
23
+ other languages where the reference to the instance is provided automatically and
24
+ may go by 'self', 'this' or something else."""
25
+
26
+ def __init__(self, function: callable):
27
+ logger.debug(
28
+ "%s.__init__(function: %s)",
29
+ self.__class__.__name__,
30
+ function,
31
+ )
32
+
33
+ if not callable(function):
34
+ raise TypeError(
35
+ "The '%s' decorator can only be used to wrap callables!"
36
+ % (self.__class__.__name__)
37
+ )
38
+ elif not type(function).__name__ == "function":
39
+ raise TypeError(
40
+ "The '%s' decorator can only be used to wrap functions!"
41
+ % (self.__class__.__name__)
42
+ )
43
+
44
+ self.function: callable = function
45
+
46
+ def __get__(self, instance, owner) -> callable:
47
+ logger.debug(
48
+ "%s.__get__(self: %s, instance: %s, owner: %s)",
49
+ self.__class__.__name__,
50
+ self,
51
+ instance,
52
+ owner,
53
+ )
54
+
55
+ if instance is None:
56
+ return lambda *args, **kwargs: self.function(owner, *args, **kwargs)
57
+ else:
58
+ return lambda *args, **kwargs: self.function(instance, *args, **kwargs)
59
+
60
+
61
+ class classproperty(property):
62
+ """The classproperty decorator transforms a method into a class-level property. This
63
+ provides access to the method as if it were a class attribute; this addresses the
64
+ removal of support for combining the @classmethod and @property decorators to create
65
+ class properties in Python 3.13, a change which was made due to some complexity in
66
+ the underlying interpreter implementation."""
67
+
68
+ def __init__(self, fget: callable, fset: callable = None, fdel: callable = None):
69
+ super().__init__(fget, fset, fdel)
70
+
71
+ def __get__(self, instance: object, klass: type = None):
72
+ if klass is None:
73
+ return self
74
+ return self.fget(klass)
75
+
76
+ def __set__(self, instance: object, value: object):
77
+ # Note that the __set__ descriptor cannot be used on class methods unless
78
+ # the class is created with a metaclass that implements this behaviour
79
+ raise NotImplemented
80
+
81
+ def __delete__(self, instance: object):
82
+ # Note that the __delete__ descriptor cannot be used on class methods unless
83
+ # the class is created with a metaclass that implements this behaviour
84
+ raise NotImplemented
85
+
86
+ def __getattr__(self, name: str):
87
+ if name in ATTRIBUTES:
88
+ return getattr(self.fget, name)
89
+ else:
90
+ raise AttributeError(
91
+ "The classproperty method '%s' does not have an '%s' attribute!"
92
+ % (
93
+ self.fget.__name__,
94
+ name,
95
+ )
96
+ )
97
+
98
+ # # For inspectability, provide access to the underlying function's metadata
99
+ # # including __module__, __name__, __qualname__, __doc__, and __annotations__
100
+ # @property
101
+ # def __module__(self):
102
+ # return self.fget.__module__
103
+ #
104
+ # @property
105
+ # def __name__(self):
106
+ # return self.fget.__name__
107
+ #
108
+ # @property
109
+ # def __qualname__(self):
110
+ # return self.fget.__qualname__
111
+ #
112
+ # @property
113
+ # def __doc__(self):
114
+ # return self.fget.__doc__
115
+ #
116
+ # @property
117
+ # def __annotations__(self):
118
+ # return self.fget.__annotations__
classicist/version.txt ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: classicist
3
+ Version: 1.0.0
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
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.md
21
+ Provides-Extra: development
22
+ Requires-Dist: black==24.10.*; extra == "development"
23
+ Requires-Dist: pytest==8.3.*; extra == "development"
24
+ Requires-Dist: pytest-codeblocks==0.17.0; extra == "development"
25
+ Provides-Extra: distribution
26
+ Requires-Dist: build; extra == "distribution"
27
+ Requires-Dist: twine; extra == "distribution"
28
+ Requires-Dist: wheel; extra == "distribution"
29
+ Dynamic: license-file
30
+
31
+ # Classicist: Classy Class Decorators & Extensions
32
+
33
+ The Classicist library provides several useful class decorators for Python class methods
34
+ including a `hybridmethod` decorator that allows methods defined in a class to be used
35
+ both a class method and an instance method, and a `classproperty` decorator that allows
36
+ class methods to be accessed as class properties.
37
+
38
+ The `classicist` library was previously named `hybridmethod` so if a prior version had been
39
+ installed, please update references to the new library name. Installation of the
40
+ library via its old name, `hybridmethod`, will install the new `classicist` library with
41
+ a mapping for backwards compatibility so that code continues to function as before.
42
+
43
+ ### Requirements
44
+
45
+ The Classicist library has been tested with Python 3.9, 3.10, 3.11, 3.12 and 3.13. The library is not compatible with Python 3.8 or earlier.
46
+
47
+ ### Installation
48
+
49
+ The Classicist library is available from PyPI, so may be added to a project's dependencies via its `requirements.txt` file or similar by referencing the Classicist library's name, `classicist`, or the library may be installed directly into your local runtime environment using `pip` via the `pip install` command by entering the following into your shell:
50
+
51
+ $ pip install classicist
52
+
53
+ #### Hybrid Methods
54
+
55
+ The Classicist library provides a `hybridmethod` method decorator that allows methods
56
+ defined in a class to be used as both a class method and an instance method.
57
+
58
+ The `@hybridmethod` decorator provided by the library wraps methods defined in classes
59
+ using the usual `@decorator` syntax. Methods defined in classes that are decorated with
60
+ the `@hybridmethod` decorator can then be accessed as both class methods and as instance
61
+ methods, with the first argument passed to the method being a reference to either the
62
+ class when the method is called as a class method or to the instance when the method is
63
+ called as an instance method.
64
+
65
+ If a class-level property is defined and then an instance-level property is created with
66
+ the same name that shadows the class-level property, the hybrid method can be used to
67
+ interact with both the class-level property and the instance-level property simply based
68
+ on whether the hybrid method was called directly on the class or on an a class instance.
69
+
70
+ If desired, a simple check of the value of the first variable passed to a hybrid method
71
+ using `isinstance(<variable>, <class>)` allows one to determine if the call was made on
72
+ an instance of the class in which case `isinstance()` evaluates to `True` or if the call
73
+ was made on the class itself, in which case `isinstance()` evaluates to `False`.
74
+
75
+ The variable passed as the first argument to the method may have any name, including as
76
+ is common in Python, `self`, although the use of `self` as the name of this argument on
77
+ an instance method is just customary and the name has no significance.
78
+
79
+ If using the `isinstance(<variable>, <class>)` check as described above is used simply
80
+ substitute in the name of the first variable of a hybrid method for `<variable>` and the
81
+ name of the class for `<class>`.
82
+
83
+ #### Hybrid Methods: Usage
84
+
85
+ To use the `hybridmethod` decorator import the decorator from the `classicist` library
86
+ and use it to decorate the class methods you wish to use as both class methods and
87
+ instance methods:
88
+
89
+ ```python
90
+ from classicist import hybridmethod
91
+
92
+ class hybridcollection(object):
93
+ items: list[str] = []
94
+
95
+ def __init__(self):
96
+ # Create an 'items' instance variable; note that this shadows the class variable
97
+ # of the same name which can still be accessed directly via self.__class__.items
98
+ self.items: list[object] = []
99
+
100
+ @hybridmethod
101
+ def add_item(self, item: object):
102
+ # We can use the following line to differentiate between the call being made on
103
+ # an instance or directly on the class; isinstance(self, <class>) returns True
104
+ # if the method was called on an instance of the class, or False if the method
105
+ # was called on the class directly; the 'self' variable will reference either
106
+ # the instance or the class; although 'self' is traditionally used in Python as
107
+ # reference to the instance
108
+ if isinstance(self, hybridcollection):
109
+ self.items.append(item)
110
+ else:
111
+ self.items.append(item)
112
+
113
+ def get_class_items(self) -> list[object]:
114
+ return self.__class__.items
115
+
116
+ def get_instance_items(self) -> list[object]:
117
+ return self.items
118
+
119
+ def get_combined_items(self) -> list[object]:
120
+ return self.__class__.items + self.items
121
+
122
+ hybridcollection.add_item("ABC") # Add an item to the class-level items list
123
+
124
+ collection = hybridcollection()
125
+
126
+ collection.add_item("XYZ") # Add an item to the instance-level items list
127
+
128
+ assert collection.get_class_items() == ["ABC"]
129
+
130
+ assert collection.get_instance_items() == ["XYZ"]
131
+
132
+ assert collection.get_combined_items() == ["ABC", "XYZ"]
133
+ ```
134
+
135
+ #### Class Properties
136
+
137
+ The Classicist library provides a `classproperty` method decorator that allows class
138
+ methods to be accessed as class properties.
139
+
140
+ The `@classproperty` decorator provided by the library wraps methods defined in classes
141
+ using the usual `@decorator` syntax. Methods defined in classes that are decorated with
142
+ the `@classproperty` decorator can then be accessed as though they were real properties
143
+ on the class.
144
+
145
+ The `@classproperty` decorator addresses the removal in Python 3.13 of the prior support
146
+ for combining the `@classmethod` and `@property` decorators to create class properties,
147
+ a change which was made due to complexity in the underlying interpreter implementation.
148
+
149
+ #### Class Properties: Usage
150
+
151
+ To use the `classproperty` decorator import the decorator from the `classicist` library
152
+ and use it to decorate any class methods you wish to access as class properties.
153
+
154
+ ```python
155
+ from classicist import classproperty
156
+
157
+ class exampleclass(object):
158
+ @classproperty
159
+ def greeting(cls) -> str:
160
+ """The 'greeting' class method has been decorated with classproperty so acts as
161
+ a property; here we could do some work to generate a return value."""
162
+ return "hello"
163
+
164
+ assert isinstance(exampleclass, type)
165
+ assert issubclass(exampleclass, exampleclass)
166
+ assert issubclass(exampleclass, object)
167
+
168
+ # We can access `.greeting` as though it was defined as a property:
169
+ # The return value of `.greeting` is indiscernible from the value being returned
170
+ assert isinstance(exampleclass.greeting, str)
171
+ assert exampleclass.greeting == "hello"
172
+ ```
173
+
174
+ ⚠️ An important caveat regarding class properties which applies equally to the method of
175
+ supporting class properties provided by this library, and to class properties which are
176
+ supported natively in Python 3.9 – 3.12 by combining the `@classmethod` and `@property`
177
+ decorators, is that unfortunately unless a custom metaclass is used to intervene, class
178
+ properties can be overwritten by value assignment.
179
+
180
+ This is a result of differences in Python's handling for descriptors between classes and
181
+ instances of classes. For both classes and instances, the `__get__` descriptor is called
182
+ while the `__set__` and `__delete__` descriptor methods will only be called on instances
183
+ such that we have no way to be involved in the property reassignment or deletion process
184
+ as would be the case for properties on instances where we can create our own setter and
185
+ deleter methods in addition to the getter.
186
+
187
+ This caveat can be remedied through a custom metaclass however, which overrides default
188
+ behaviour, and is able to intercept the `__setattr_` and `__delattr__` calls as needed.
189
+
190
+ ```python
191
+ from classicist import classproperty
192
+
193
+ class exampleclass(object):
194
+ @classproperty
195
+ def greeting(cls) -> str:
196
+ # Generate a return value here
197
+ return "hello"
198
+
199
+ # We can access `.greeting` as though it was defined as a property:
200
+ assert exampleclass.greeting == "hello"
201
+
202
+ # Note: The `.greeting` property will be reassigned to the new value, "goodbye":
203
+ exampleclass.greeting = "goodbye"
204
+ assert exampleclass.greeting == "goodbye"
205
+ ```
206
+
207
+ As can be seen with the method of natively supporting class properties, they could also
208
+ have their values reassigned without warning:
209
+
210
+ ```python
211
+ import sys
212
+ import pytest
213
+
214
+ # As Python only natively supported combining @classmethod and @property between version
215
+ # 3.9 and 3.12, the example below is not usable on other versions, such as 3.13+
216
+ if sys.version_info.major == 3 and not (9 <= sys.version_info.minor <= 12):
217
+ pytest.skip("This test can run on Python versions 3.9 – 3.12")
218
+
219
+ class exampleclass(object):
220
+ @classmethod
221
+ @property
222
+ def greeting(cls) -> str:
223
+ # Generate a return value here
224
+ return "hello"
225
+
226
+ # We can access `.greeting` as though it was defined as a property:
227
+ assert exampleclass.greeting == "hello"
228
+
229
+ # Note: The `.greeting` property will be reassigned to the new value, "goodbye":
230
+ exampleclass.greeting = "goodbye"
231
+ assert exampleclass.greeting == "goodbye"
232
+ ```
233
+
234
+ ### Unit Tests
235
+
236
+ The Classicist library includes a suite of comprehensive unit tests which ensure that
237
+ the library functionality operates as expected. The unit tests were developed with and
238
+ are run via `pytest`.
239
+
240
+ To ensure that the unit tests are run within a predictable runtime environment where all of the necessary dependencies are available, a [Docker](https://www.docker.com) image is created within which the tests are run. To run the unit tests, ensure Docker and Docker Compose is [installed](https://docs.docker.com/engine/install/), and perform the following commands, which will build the Docker image via `docker compose build` and then run the tests via `docker compose run` – the output of running the tests will be displayed:
241
+
242
+ ```shell
243
+ $ docker compose build
244
+ $ docker compose run tests
245
+ ```
246
+
247
+ To run the unit tests with optional command line arguments being passed to `pytest`, append the relevant arguments to the `docker compose run tests` command, as follows, for example passing `-vv` to enable verbose output:
248
+
249
+ ```shell
250
+ $ docker compose run tests -vv
251
+ ```
252
+
253
+ See the documentation for [PyTest](https://docs.pytest.org/en/latest/) regarding available optional command line arguments.
254
+
255
+ ### Copyright & License Information
256
+
257
+ Copyright © 2025 Daniel Sissman; licensed under the MIT License.
@@ -0,0 +1,8 @@
1
+ classicist/__init__.py,sha256=_pJXZUnA8U2nAlOZ3qIlX0f1SKlyRrjVQjxX8JRiTCk,4649
2
+ classicist/version.txt,sha256=klIfw8vZZL3J9YSpkbif3apXVO0cyW1tQkRTOGacEwU,5
3
+ classicist-1.0.0.dist-info/licenses/LICENSE.md,sha256=qBmrjPmSCp0YFyaIl2G3FU3rniFD31YC0Yd3MrO1wEg,1070
4
+ classicist-1.0.0.dist-info/METADATA,sha256=f0HnAwDPgxgj4qWsxQWnEcIpcJaX7cKsjQCuRH7RywA,11600
5
+ classicist-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ classicist-1.0.0.dist-info/top_level.txt,sha256=beG3ZuwObnmnY_mgNSN5CaVIWpI2VKszjVdKHPgZBhc,11
7
+ classicist-1.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ classicist-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2025 Daniel Sissman.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ classicist
@@ -0,0 +1 @@
1
+