classicist 1.0.1__tar.gz → 1.0.2__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.
- {classicist-1.0.1/source/classicist.egg-info → classicist-1.0.2}/PKG-INFO +55 -14
- {classicist-1.0.1 → classicist-1.0.2}/README.md +54 -13
- {classicist-1.0.1 → classicist-1.0.2}/requirements.development.txt +1 -1
- classicist-1.0.2/source/classicist/decorators/aliased/__init__.py +177 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/aliased/__init__.py +2 -1
- classicist-1.0.2/source/classicist/version.txt +1 -0
- {classicist-1.0.1 → classicist-1.0.2/source/classicist.egg-info}/PKG-INFO +55 -14
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/requires.txt +1 -1
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_aliased.py +112 -5
- classicist-1.0.1/source/classicist/decorators/aliased/__init__.py +0 -74
- classicist-1.0.1/source/classicist/version.txt +0 -1
- {classicist-1.0.1 → classicist-1.0.2}/LICENSE.md +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/pyproject.toml +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/requirements.distribution.txt +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/requirements.txt +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/setup.cfg +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/classproperty/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/deprecated/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/nocache/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/inspector/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/logging/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/SOURCES.txt +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/dependency_links.txt +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/top_level.txt +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/zip-safe +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_annotation.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_classproperty.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_deprecated.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_hybridmethod.py +0 -0
- {classicist-1.0.1 → classicist-1.0.2}/tests/test_shadowproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE.md
|
|
23
23
|
Provides-Extra: development
|
|
24
|
-
Requires-Dist: black==
|
|
24
|
+
Requires-Dist: black==26.1.*; extra == "development"
|
|
25
25
|
Requires-Dist: pytest==8.3.*; extra == "development"
|
|
26
26
|
Requires-Dist: pytest-codeblocks==0.17.0; extra == "development"
|
|
27
27
|
Requires-Dist: pyflakes; extra == "development"
|
|
@@ -265,28 +265,67 @@ assert exampleclass.greeting == "goodbye"
|
|
|
265
265
|
|
|
266
266
|
#### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
|
|
267
267
|
|
|
268
|
-
The `@alias` decorator can be used to add
|
|
269
|
-
classes, such that both the original name and any defined
|
|
270
|
-
|
|
271
|
-
of classes as the aliases are created as additional class attributes scoped to the class.
|
|
268
|
+
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
269
|
+
classes, and module-level functions, such that both the original name and any defined
|
|
270
|
+
aliases can be used to access the same code object at runtime.
|
|
272
271
|
|
|
273
|
-
To
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
To alias a class or a module-level function, that is a function defined at the top-level
|
|
273
|
+
of a module file (rather than nested within a function or class), simply decorate the
|
|
274
|
+
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
275
|
+
more name aliases for the code object as one or more string arguments passed into the
|
|
276
|
+
decorator method.
|
|
277
|
+
|
|
278
|
+
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
279
|
+
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
280
|
+
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
281
|
+
the aliases as additional attributes on the class so that the aliased methods are available
|
|
282
|
+
at runtime via both their original name and their aliases.
|
|
278
283
|
|
|
279
284
|
The example below demonstrates adding an alias to a method defined within a class, and
|
|
280
285
|
using the `aliased` metaclass when defining the class to ensure that the alias is parsed
|
|
281
286
|
and translated to an additional class attribute so that the method is accessible via its
|
|
282
287
|
original name and the alias at runtime.
|
|
283
288
|
|
|
289
|
+
If control over the scope is required, the optional `scope` keyword argument can be
|
|
290
|
+
used to specify the scope into which to apply the alias, this must be a reference to
|
|
291
|
+
the globals() or locals() at the point in code where the `@alias()` decorator is used.
|
|
292
|
+
|
|
284
293
|
```python
|
|
285
294
|
from classicist import aliased, alias, is_aliased, aliases
|
|
286
295
|
|
|
287
|
-
|
|
296
|
+
# Define an alias on a module-level method; as this demonstration occurs
|
|
297
|
+
# within the README file which is parsed by and run within an external
|
|
298
|
+
# scope by pytest and pytest-codeblocks, we override the scope within
|
|
299
|
+
# which to apply the alias otherwise the alias would be assigned within
|
|
300
|
+
# an external scope which would prevent the alias from working; however
|
|
301
|
+
# it is rare to need to override the inferred scope, and aliasing of
|
|
302
|
+
# module-level functions defined within actual modules will work normally;
|
|
303
|
+
# for rare cases where overriding scope is necessary the optional `scope`
|
|
304
|
+
# keyword-only argument can be used as shown below.
|
|
305
|
+
@alias("sums", scope=globals())
|
|
306
|
+
def adds(a: int, b: int) -> int:
|
|
307
|
+
return a + b
|
|
308
|
+
|
|
309
|
+
assert globals().get("adds") is adds
|
|
310
|
+
assert globals().get("sums") is sums
|
|
311
|
+
assert adds is sums
|
|
312
|
+
assert adds(1, 2) == 3
|
|
313
|
+
assert sums(1, 2) == 3
|
|
314
|
+
|
|
315
|
+
# Define an alias on a class
|
|
316
|
+
@alias("Color")
|
|
317
|
+
class Colour(object):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
assert Colour is Color
|
|
321
|
+
|
|
322
|
+
# Define an alias on a method defined within a class;
|
|
323
|
+
# this also requires the use of the aliased metaclass
|
|
324
|
+
# which is responsible for adding the aliases within
|
|
325
|
+
# the scope of the class once the class has been parsed
|
|
326
|
+
class Welcome(metaclass=aliased):
|
|
288
327
|
@alias("greet")
|
|
289
|
-
def hello(self, name: str):
|
|
328
|
+
def hello(self, name: str) -> str:
|
|
290
329
|
return f"Hello {name}!"
|
|
291
330
|
|
|
292
331
|
assert is_aliased(Welcome.hello) is True
|
|
@@ -305,7 +344,9 @@ assert welcome.greet("you") == "Hello you!"
|
|
|
305
344
|
|
|
306
345
|
⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
|
|
307
346
|
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.
|
|
347
|
+
alias is specified an `AliasError` exception will be raised at runtime. Furthermore, if
|
|
348
|
+
a name has already been used in the current scope, an `AliasError` exception will be
|
|
349
|
+
raised at runtime.
|
|
309
350
|
|
|
310
351
|
#### Annotation Decorator: Add Arbitrary Annotations to Code Objects
|
|
311
352
|
|
|
@@ -232,28 +232,67 @@ assert exampleclass.greeting == "goodbye"
|
|
|
232
232
|
|
|
233
233
|
#### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
|
|
234
234
|
|
|
235
|
-
The `@alias` decorator can be used to add
|
|
236
|
-
classes, such that both the original name and any defined
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
236
|
+
classes, and module-level functions, such that both the original name and any defined
|
|
237
|
+
aliases can be used to access the same code object at runtime.
|
|
238
|
+
|
|
239
|
+
To alias a class or a module-level function, that is a function defined at the top-level
|
|
240
|
+
of a module file (rather than nested within a function or class), simply decorate the
|
|
241
|
+
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
242
|
+
more name aliases for the code object as one or more string arguments passed into the
|
|
243
|
+
decorator method.
|
|
244
|
+
|
|
245
|
+
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
246
|
+
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
247
|
+
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
248
|
+
the aliases as additional attributes on the class so that the aliased methods are available
|
|
249
|
+
at runtime via both their original name and their aliases.
|
|
245
250
|
|
|
246
251
|
The example below demonstrates adding an alias to a method defined within a class, and
|
|
247
252
|
using the `aliased` metaclass when defining the class to ensure that the alias is parsed
|
|
248
253
|
and translated to an additional class attribute so that the method is accessible via its
|
|
249
254
|
original name and the alias at runtime.
|
|
250
255
|
|
|
256
|
+
If control over the scope is required, the optional `scope` keyword argument can be
|
|
257
|
+
used to specify the scope into which to apply the alias, this must be a reference to
|
|
258
|
+
the globals() or locals() at the point in code where the `@alias()` decorator is used.
|
|
259
|
+
|
|
251
260
|
```python
|
|
252
261
|
from classicist import aliased, alias, is_aliased, aliases
|
|
253
262
|
|
|
254
|
-
|
|
263
|
+
# Define an alias on a module-level method; as this demonstration occurs
|
|
264
|
+
# within the README file which is parsed by and run within an external
|
|
265
|
+
# scope by pytest and pytest-codeblocks, we override the scope within
|
|
266
|
+
# which to apply the alias otherwise the alias would be assigned within
|
|
267
|
+
# an external scope which would prevent the alias from working; however
|
|
268
|
+
# it is rare to need to override the inferred scope, and aliasing of
|
|
269
|
+
# module-level functions defined within actual modules will work normally;
|
|
270
|
+
# for rare cases where overriding scope is necessary the optional `scope`
|
|
271
|
+
# keyword-only argument can be used as shown below.
|
|
272
|
+
@alias("sums", scope=globals())
|
|
273
|
+
def adds(a: int, b: int) -> int:
|
|
274
|
+
return a + b
|
|
275
|
+
|
|
276
|
+
assert globals().get("adds") is adds
|
|
277
|
+
assert globals().get("sums") is sums
|
|
278
|
+
assert adds is sums
|
|
279
|
+
assert adds(1, 2) == 3
|
|
280
|
+
assert sums(1, 2) == 3
|
|
281
|
+
|
|
282
|
+
# Define an alias on a class
|
|
283
|
+
@alias("Color")
|
|
284
|
+
class Colour(object):
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
assert Colour is Color
|
|
288
|
+
|
|
289
|
+
# Define an alias on a method defined within a class;
|
|
290
|
+
# this also requires the use of the aliased metaclass
|
|
291
|
+
# which is responsible for adding the aliases within
|
|
292
|
+
# the scope of the class once the class has been parsed
|
|
293
|
+
class Welcome(metaclass=aliased):
|
|
255
294
|
@alias("greet")
|
|
256
|
-
def hello(self, name: str):
|
|
295
|
+
def hello(self, name: str) -> str:
|
|
257
296
|
return f"Hello {name}!"
|
|
258
297
|
|
|
259
298
|
assert is_aliased(Welcome.hello) is True
|
|
@@ -272,7 +311,9 @@ assert welcome.greet("you") == "Hello you!"
|
|
|
272
311
|
|
|
273
312
|
⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
|
|
274
313
|
other function and method names and aliases cannot be reserved keywords. If an invalid
|
|
275
|
-
alias is specified an `AliasError` exception will be raised at runtime.
|
|
314
|
+
alias is specified an `AliasError` exception will be raised at runtime. Furthermore, if
|
|
315
|
+
a name has already been used in the current scope, an `AliasError` exception will be
|
|
316
|
+
raised at runtime.
|
|
276
317
|
|
|
277
318
|
#### Annotation Decorator: Add Arbitrary Annotations to Code Objects
|
|
278
319
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from classicist.logging import logger
|
|
2
|
+
from classicist.exceptions.decorators.aliased import AliasError
|
|
3
|
+
from classicist.inspector import unwrap
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
|
|
8
|
+
import keyword
|
|
9
|
+
import inspect
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
logger = logger.getChild(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
16
|
+
"""Decorator that applies one or more alias names to a class, function or method.
|
|
17
|
+
The decorator records the assigned aliases on the class, method or function object,
|
|
18
|
+
and where possible creates aliases in the same scope as the original class or module
|
|
19
|
+
level function directly as the decorator call runs. Methods within classes cannot be
|
|
20
|
+
aliased directly by the `@alias` decorator, but instead require the assistance of the
|
|
21
|
+
corresponding `aliased` metaclass that must be specified on the class definition. If
|
|
22
|
+
control over the scope is required, the optional `scope` keyword argument can be used
|
|
23
|
+
to specify the scope into which to apply the alias, this should be a reference to the
|
|
24
|
+
globals() or locals() at the site in code where the `@alias()` decorator is used."""
|
|
25
|
+
|
|
26
|
+
for name in names:
|
|
27
|
+
if not isinstance(name, str):
|
|
28
|
+
raise AliasError(
|
|
29
|
+
"All @alias decorator name arguments must have a string value; non-string values cannot be used!"
|
|
30
|
+
)
|
|
31
|
+
elif len(name := name.strip()) == 0:
|
|
32
|
+
raise AliasError(
|
|
33
|
+
"All @alias decorator name arguments must be valid Python identifier values; empty strings cannot be used!"
|
|
34
|
+
)
|
|
35
|
+
elif not name.isidentifier():
|
|
36
|
+
raise AliasError(
|
|
37
|
+
f"All @alias decorator name arguments must be valid Python identifier values; strings such as '{name}' are not considered valid identifiers by Python!"
|
|
38
|
+
)
|
|
39
|
+
elif keyword.iskeyword(name):
|
|
40
|
+
raise AliasError(
|
|
41
|
+
f"All @alias decorator name arguments must be valid Python identifier values; reserved keywords, such as '{name}' cannot be used!"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def decorator(thing: object, *args, **kwargs) -> object:
|
|
45
|
+
nonlocal scope
|
|
46
|
+
|
|
47
|
+
thing = unwrap(thing)
|
|
48
|
+
|
|
49
|
+
logger.info(f"@alias({names}) called on {thing}")
|
|
50
|
+
|
|
51
|
+
if isinstance(aliases := getattr(thing, "_classicist_aliases", None), tuple):
|
|
52
|
+
setattr(thing, "_classicist_aliases", tuple([*aliases, *names]))
|
|
53
|
+
else:
|
|
54
|
+
setattr(thing, "_classicist_aliases", names)
|
|
55
|
+
|
|
56
|
+
@wraps(thing)
|
|
57
|
+
def wrapper_class(*args, **kwargs):
|
|
58
|
+
return thing # (*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
@wraps(thing)
|
|
61
|
+
def wrapper_method(*args, **kwargs):
|
|
62
|
+
return thing(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
@wraps(thing)
|
|
65
|
+
def wrapper_function(*args, **kwargs):
|
|
66
|
+
return thing # (*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
if inspect.isclass(thing):
|
|
69
|
+
if not scope:
|
|
70
|
+
scope = sys.modules.get(thing.__module__ or "__main__")
|
|
71
|
+
|
|
72
|
+
if isinstance(scope, object):
|
|
73
|
+
for name in names:
|
|
74
|
+
if hasattr(scope, name):
|
|
75
|
+
raise AliasError(
|
|
76
|
+
"Cannot create alias '%s' for %s class in the %s module as an object with that name already exists!"
|
|
77
|
+
% (
|
|
78
|
+
name,
|
|
79
|
+
thing,
|
|
80
|
+
scope,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Create a module-level alias for the class
|
|
85
|
+
if isinstance(scope, dict):
|
|
86
|
+
scope[name] = thing
|
|
87
|
+
else:
|
|
88
|
+
setattr(scope, name, thing)
|
|
89
|
+
|
|
90
|
+
return wrapper_class(*args, **kwargs)
|
|
91
|
+
elif inspect.ismethod(thing) or isinstance(thing, classmethod):
|
|
92
|
+
return wrapper_method
|
|
93
|
+
elif inspect.isfunction(thing):
|
|
94
|
+
if not scope:
|
|
95
|
+
scope = sys.modules.get(thing.__module__ or "__main__")
|
|
96
|
+
|
|
97
|
+
if not scope:
|
|
98
|
+
logger.warning(f"No module found for {thing.__module__}!")
|
|
99
|
+
|
|
100
|
+
for module in sys.modules:
|
|
101
|
+
logger.debug(f" => module => {module}")
|
|
102
|
+
|
|
103
|
+
# The qualified name for module-level functions only contain the name of the
|
|
104
|
+
# function, whereas functions nested within other functions or classes have
|
|
105
|
+
# names comprised of multiple parts separated by the "." character; because
|
|
106
|
+
# it is only currently possible to alias module-level functions, any nested
|
|
107
|
+
# or class methods are ignored during this stage of the aliasing process.
|
|
108
|
+
if len(thing.__qualname__.split(".")) > 1:
|
|
109
|
+
logger.warning(
|
|
110
|
+
"Unable to apply alias to functions defined beyond the top-level of a module: %s!"
|
|
111
|
+
% (thing.__qualname__)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return wrapper_function(*args, **kwargs)
|
|
115
|
+
|
|
116
|
+
# if signature := inspect.signature(thing):
|
|
117
|
+
# if len(parameters := signature.parameters) > 0 and "self" in parameters:
|
|
118
|
+
# return wrapper_function(*args, **kwargs)
|
|
119
|
+
|
|
120
|
+
if isinstance(scope, object):
|
|
121
|
+
# At this point we should only be left with module-level functions to alias
|
|
122
|
+
for name in names:
|
|
123
|
+
# Ensure the scope doesn't already contain an object of the same name
|
|
124
|
+
if hasattr(scope, name):
|
|
125
|
+
raise AliasError(
|
|
126
|
+
"Cannot create alias '%s' for %s function in the %s module as an object with that name already exists!"
|
|
127
|
+
% (
|
|
128
|
+
name,
|
|
129
|
+
thing,
|
|
130
|
+
scope,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
logger.info(f"Added alias '{name}' to {scope}.{thing}")
|
|
135
|
+
|
|
136
|
+
if isinstance(scope, dict):
|
|
137
|
+
scope[name] = thing
|
|
138
|
+
elif isinstance(scope, object):
|
|
139
|
+
setattr(scope, name, thing)
|
|
140
|
+
else:
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"No scope was found or specified for {thing} into which to assign the alias!"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return wrapper_function(*args, **kwargs)
|
|
146
|
+
else:
|
|
147
|
+
raise AliasError(
|
|
148
|
+
"The @alias decorator can only be applied to classes, methods and functions, not %s!"
|
|
149
|
+
% (type(thing))
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return decorator
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_aliased(function: callable) -> bool:
|
|
156
|
+
"""The is_aliased() helper method can be used to determine if a class method has
|
|
157
|
+
been aliased."""
|
|
158
|
+
|
|
159
|
+
function = unwrap(function)
|
|
160
|
+
|
|
161
|
+
return isinstance(getattr(function, "_classicist_aliases", None), tuple)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def aliases(function: callable) -> list[str]:
|
|
165
|
+
"""The aliases() helper method can be used to obtain any class method aliases."""
|
|
166
|
+
|
|
167
|
+
function = unwrap(function)
|
|
168
|
+
|
|
169
|
+
if isinstance(aliases := getattr(function, "_classicist_aliases", None), tuple):
|
|
170
|
+
return list(aliases)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = [
|
|
174
|
+
"alias",
|
|
175
|
+
"is_aliased",
|
|
176
|
+
"aliases",
|
|
177
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from classicist.logging import logger
|
|
2
|
+
from classicist.exceptions.decorators.aliased import AliasError
|
|
2
3
|
|
|
3
4
|
logger = logger.getChild(__name__)
|
|
4
5
|
|
|
@@ -30,7 +31,7 @@ class aliased(type):
|
|
|
30
31
|
if aliases := getattr(value, "_classicist_aliases", None):
|
|
31
32
|
for alias in aliases:
|
|
32
33
|
if hasattr(cls, alias):
|
|
33
|
-
raise
|
|
34
|
+
raise AliasError(
|
|
34
35
|
f"Cannot create alias '{alias}' for method '{name}' as '{cls.__name__}.{alias}' already exists!"
|
|
35
36
|
)
|
|
36
37
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE.md
|
|
23
23
|
Provides-Extra: development
|
|
24
|
-
Requires-Dist: black==
|
|
24
|
+
Requires-Dist: black==26.1.*; extra == "development"
|
|
25
25
|
Requires-Dist: pytest==8.3.*; extra == "development"
|
|
26
26
|
Requires-Dist: pytest-codeblocks==0.17.0; extra == "development"
|
|
27
27
|
Requires-Dist: pyflakes; extra == "development"
|
|
@@ -265,28 +265,67 @@ assert exampleclass.greeting == "goodbye"
|
|
|
265
265
|
|
|
266
266
|
#### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
|
|
267
267
|
|
|
268
|
-
The `@alias` decorator can be used to add
|
|
269
|
-
classes, such that both the original name and any defined
|
|
270
|
-
|
|
271
|
-
of classes as the aliases are created as additional class attributes scoped to the class.
|
|
268
|
+
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
269
|
+
classes, and module-level functions, such that both the original name and any defined
|
|
270
|
+
aliases can be used to access the same code object at runtime.
|
|
272
271
|
|
|
273
|
-
To
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
To alias a class or a module-level function, that is a function defined at the top-level
|
|
273
|
+
of a module file (rather than nested within a function or class), simply decorate the
|
|
274
|
+
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
275
|
+
more name aliases for the code object as one or more string arguments passed into the
|
|
276
|
+
decorator method.
|
|
277
|
+
|
|
278
|
+
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
279
|
+
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
280
|
+
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
281
|
+
the aliases as additional attributes on the class so that the aliased methods are available
|
|
282
|
+
at runtime via both their original name and their aliases.
|
|
278
283
|
|
|
279
284
|
The example below demonstrates adding an alias to a method defined within a class, and
|
|
280
285
|
using the `aliased` metaclass when defining the class to ensure that the alias is parsed
|
|
281
286
|
and translated to an additional class attribute so that the method is accessible via its
|
|
282
287
|
original name and the alias at runtime.
|
|
283
288
|
|
|
289
|
+
If control over the scope is required, the optional `scope` keyword argument can be
|
|
290
|
+
used to specify the scope into which to apply the alias, this must be a reference to
|
|
291
|
+
the globals() or locals() at the point in code where the `@alias()` decorator is used.
|
|
292
|
+
|
|
284
293
|
```python
|
|
285
294
|
from classicist import aliased, alias, is_aliased, aliases
|
|
286
295
|
|
|
287
|
-
|
|
296
|
+
# Define an alias on a module-level method; as this demonstration occurs
|
|
297
|
+
# within the README file which is parsed by and run within an external
|
|
298
|
+
# scope by pytest and pytest-codeblocks, we override the scope within
|
|
299
|
+
# which to apply the alias otherwise the alias would be assigned within
|
|
300
|
+
# an external scope which would prevent the alias from working; however
|
|
301
|
+
# it is rare to need to override the inferred scope, and aliasing of
|
|
302
|
+
# module-level functions defined within actual modules will work normally;
|
|
303
|
+
# for rare cases where overriding scope is necessary the optional `scope`
|
|
304
|
+
# keyword-only argument can be used as shown below.
|
|
305
|
+
@alias("sums", scope=globals())
|
|
306
|
+
def adds(a: int, b: int) -> int:
|
|
307
|
+
return a + b
|
|
308
|
+
|
|
309
|
+
assert globals().get("adds") is adds
|
|
310
|
+
assert globals().get("sums") is sums
|
|
311
|
+
assert adds is sums
|
|
312
|
+
assert adds(1, 2) == 3
|
|
313
|
+
assert sums(1, 2) == 3
|
|
314
|
+
|
|
315
|
+
# Define an alias on a class
|
|
316
|
+
@alias("Color")
|
|
317
|
+
class Colour(object):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
assert Colour is Color
|
|
321
|
+
|
|
322
|
+
# Define an alias on a method defined within a class;
|
|
323
|
+
# this also requires the use of the aliased metaclass
|
|
324
|
+
# which is responsible for adding the aliases within
|
|
325
|
+
# the scope of the class once the class has been parsed
|
|
326
|
+
class Welcome(metaclass=aliased):
|
|
288
327
|
@alias("greet")
|
|
289
|
-
def hello(self, name: str):
|
|
328
|
+
def hello(self, name: str) -> str:
|
|
290
329
|
return f"Hello {name}!"
|
|
291
330
|
|
|
292
331
|
assert is_aliased(Welcome.hello) is True
|
|
@@ -305,7 +344,9 @@ assert welcome.greet("you") == "Hello you!"
|
|
|
305
344
|
|
|
306
345
|
⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
|
|
307
346
|
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.
|
|
347
|
+
alias is specified an `AliasError` exception will be raised at runtime. Furthermore, if
|
|
348
|
+
a name has already been used in the current scope, an `AliasError` exception will be
|
|
349
|
+
raised at runtime.
|
|
309
350
|
|
|
310
351
|
#### Annotation Decorator: Add Arbitrary Annotations to Code Objects
|
|
311
352
|
|
|
@@ -2,29 +2,123 @@ from classicist import aliased, alias, aliases, is_aliased
|
|
|
2
2
|
from classicist.exceptions.decorators.aliased import AliasError
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
import sys
|
|
6
|
+
import types
|
|
7
|
+
import conftest
|
|
8
|
+
|
|
9
|
+
# Obtain a reference to the current module
|
|
10
|
+
module = sys.modules[__name__]
|
|
11
|
+
assert isinstance(module, types.ModuleType)
|
|
12
|
+
assert module.__name__ == __name__
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Define and alias a module-level function for testing below
|
|
16
|
+
@alias("doubled")
|
|
17
|
+
def doubler(value: int) -> int:
|
|
18
|
+
"""Sample method that doubles and returns the provided number."""
|
|
19
|
+
|
|
20
|
+
return value * 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_alias_function():
|
|
24
|
+
"""Test using the @alias decorator on a module-level function."""
|
|
25
|
+
|
|
26
|
+
# Ensure that both the original function "doubler" and its alias exist in the module
|
|
27
|
+
assert hasattr(module, "doubler")
|
|
28
|
+
assert hasattr(module, "doubled")
|
|
29
|
+
|
|
30
|
+
# Ensure that the module attributes directly reference the function objects
|
|
31
|
+
# and ensure that the function names both exist in scope; by ensuring that
|
|
32
|
+
# the aliased name also exists equally in scope and has the same visibility
|
|
33
|
+
# we ensure that the module-level alias is working correctly
|
|
34
|
+
assert getattr(module, "doubler") is doubler
|
|
35
|
+
assert getattr(module, "doubled") is doubled
|
|
36
|
+
|
|
37
|
+
assert doubler is doubled
|
|
38
|
+
|
|
39
|
+
assert doubler(1) == 2
|
|
40
|
+
assert doubled(2) == 4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_alias_function_defined_in_an_imported_module():
|
|
44
|
+
"""Test using the @alias decorator on an imported module-level function."""
|
|
45
|
+
|
|
46
|
+
# Ensure that both the original function "halves" and its alias exist in the module
|
|
47
|
+
assert hasattr(conftest, "halves")
|
|
48
|
+
assert hasattr(conftest, "divide")
|
|
49
|
+
|
|
50
|
+
# Ensure that the module attributes directly reference the function object and
|
|
51
|
+
# that the original and aliased function names both exist in scope; by ensuring
|
|
52
|
+
# that the aliased name exists equally in scope and has the same visibility as
|
|
53
|
+
# the original function object, we confirm that the alias is working correctly
|
|
54
|
+
assert getattr(conftest, "halves") is conftest.halves
|
|
55
|
+
assert getattr(conftest, "divide") is conftest.divide
|
|
56
|
+
|
|
57
|
+
# Ensure that the original and the alias refer to the same object in memory
|
|
58
|
+
assert conftest.halves is conftest.divide
|
|
59
|
+
|
|
60
|
+
# Test the original and aliased function names
|
|
61
|
+
assert conftest.halves(2) == 1
|
|
62
|
+
assert conftest.divide(4) == 2
|
|
63
|
+
|
|
64
|
+
# Ensure that both the original and aliased function can be imported
|
|
65
|
+
from conftest import halves, divide
|
|
66
|
+
|
|
67
|
+
# Ensure that the imported names refer to the same object in memory
|
|
68
|
+
assert conftest.halves is halves
|
|
69
|
+
assert conftest.divide is divide
|
|
70
|
+
|
|
71
|
+
# Test the original and aliased function names
|
|
72
|
+
assert halves(2) == 1
|
|
73
|
+
assert divide(4) == 2
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_alias_class():
|
|
77
|
+
"""Test using the @alias decorator on a class."""
|
|
78
|
+
|
|
79
|
+
@alias("Greeter")
|
|
80
|
+
class Welcome(object):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Ensure that both the original and the aliased names exists
|
|
84
|
+
assert isinstance(Welcome, type)
|
|
85
|
+
assert isinstance(Greeter, type)
|
|
86
|
+
|
|
87
|
+
# Ensure both the original and the aliased names refer to the same object in memory
|
|
88
|
+
assert Welcome is Greeter
|
|
89
|
+
assert Greeter is Welcome
|
|
5
90
|
|
|
6
91
|
|
|
7
92
|
def test_alias_class_method():
|
|
8
93
|
"""Test using the @alias decorator on a method."""
|
|
9
94
|
|
|
95
|
+
# Create a sample class that aliases a method; due to the way that Python classes
|
|
96
|
+
# are parsed by the interpreter, it is necessary to use a metaclass or custom base
|
|
97
|
+
# class to apply the method aliases to the class scope, thus the use of the aliased
|
|
98
|
+
# metaclass in the class definition below; this special metaclass scans the class'
|
|
99
|
+
# methods, looking for any with assigned aliases, and then adds the aliases to the
|
|
100
|
+
# class' scope so that the methods can be accessed both via their original name and
|
|
101
|
+
# any aliases that have been defined; without the metaclass the aliases won't exist.
|
|
10
102
|
class Welcome(metaclass=aliased):
|
|
11
103
|
@alias("sweet", "greet")
|
|
12
104
|
def hello(self, name: str) -> str:
|
|
13
105
|
return f"hello: {name}"
|
|
14
106
|
|
|
107
|
+
# Ensure both the original and the aliased names refer to the same object in memory
|
|
15
108
|
assert Welcome.hello is Welcome.sweet
|
|
16
109
|
assert Welcome.hello is Welcome.greet
|
|
17
110
|
|
|
111
|
+
# Ensure both the original and the aliased names report as having been aliased
|
|
18
112
|
assert is_aliased(Welcome.hello) is True
|
|
19
113
|
assert is_aliased(Welcome.sweet) is True
|
|
20
114
|
assert is_aliased(Welcome.greet) is True
|
|
21
115
|
|
|
22
|
-
|
|
116
|
+
# Check that the class method object reports its aliases correctly
|
|
23
117
|
assert aliases(Welcome.hello) == ["sweet", "greet"]
|
|
24
118
|
|
|
25
119
|
|
|
26
120
|
def test_alias_class_method_property():
|
|
27
|
-
"""Test using the @alias decorator with
|
|
121
|
+
"""Test using the @alias decorator with @property and @classmethod decorators."""
|
|
28
122
|
|
|
29
123
|
class Welcome(metaclass=aliased):
|
|
30
124
|
@property
|
|
@@ -33,14 +127,16 @@ def test_alias_class_method_property():
|
|
|
33
127
|
def hello(self, name: str) -> str:
|
|
34
128
|
return f"hello: {name}"
|
|
35
129
|
|
|
130
|
+
# Ensure both the original and the aliased names refer to the same object in memory
|
|
36
131
|
assert Welcome.hello is Welcome.sweet
|
|
37
132
|
assert Welcome.hello is Welcome.greet
|
|
38
133
|
|
|
134
|
+
# Ensure both the original and the aliased names report as having been aliased
|
|
39
135
|
assert is_aliased(Welcome.hello) is True
|
|
40
136
|
assert is_aliased(Welcome.sweet) is True
|
|
41
137
|
assert is_aliased(Welcome.greet) is True
|
|
42
138
|
|
|
43
|
-
|
|
139
|
+
# Check that the class method object reports its aliases correctly
|
|
44
140
|
assert aliases(Welcome.hello) == ["sweet", "greet"]
|
|
45
141
|
|
|
46
142
|
|
|
@@ -70,23 +166,34 @@ def test_alias_class_method_alias_with_valid_identifier():
|
|
|
70
166
|
assert welcome.hello("me") == "hello: me"
|
|
71
167
|
assert welcome.greet("me") == "hello: me"
|
|
72
168
|
|
|
169
|
+
# Ensure that aliases are inherited by subclasses
|
|
73
170
|
class SubWelcome(Welcome):
|
|
74
171
|
pass
|
|
75
172
|
|
|
173
|
+
# Ensure that aliases are inherited by subclasses
|
|
76
174
|
assert hasattr(SubWelcome, "hello") is True
|
|
77
175
|
assert hasattr(SubWelcome, "greet") is True
|
|
78
176
|
|
|
177
|
+
# Ensure that aliases are inherited by subclass instances
|
|
79
178
|
subwelcome = SubWelcome()
|
|
80
179
|
|
|
180
|
+
# Ensure that aliases are inherited by subclass instances
|
|
81
181
|
assert hasattr(subwelcome, "hello") is True
|
|
82
182
|
assert hasattr(subwelcome, "greet") is True
|
|
83
183
|
|
|
84
184
|
# Ensure that the aliased method functionality operates as expected
|
|
85
185
|
assert subwelcome.hello("me") == "hello: me"
|
|
86
|
-
|
|
87
|
-
# Ensure when the alias hasn't been registered that access raises an AttributeError
|
|
88
186
|
assert subwelcome.greet("me") == "hello: me"
|
|
89
187
|
|
|
188
|
+
# Ensure when an alias hasn't been registered that access raises an AttributeError
|
|
189
|
+
with pytest.raises(AttributeError) as exception:
|
|
190
|
+
assert subwelcome.sweet("me") == "hello: me"
|
|
191
|
+
|
|
192
|
+
assert (
|
|
193
|
+
str(exception)
|
|
194
|
+
== "AttributeError: 'SubWelcome' object has no attribute 'sweet'. Did you mean: 'greet'?"
|
|
195
|
+
)
|
|
196
|
+
|
|
90
197
|
|
|
91
198
|
def test_alias_class_method_with_invalid_identifier():
|
|
92
199
|
"""Test using the @alias decorator with an invalid identifier."""
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
from classicist.logging import logger
|
|
2
|
-
from classicist.exceptions.decorators.aliased import AliasError
|
|
3
|
-
from classicist.inspector import unwrap
|
|
4
|
-
|
|
5
|
-
from typing import Callable
|
|
6
|
-
from functools import wraps
|
|
7
|
-
|
|
8
|
-
import keyword
|
|
9
|
-
|
|
10
|
-
logger = logger.getChild(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def alias(*names: tuple[str]) -> Callable:
|
|
14
|
-
"""Decorator that marks a method with one or more alias names. The decorator does
|
|
15
|
-
not modify the function – it simply records the aliases on the function object."""
|
|
16
|
-
|
|
17
|
-
for name in names:
|
|
18
|
-
if not isinstance(name, str):
|
|
19
|
-
raise AliasError(
|
|
20
|
-
"All @alias decorator name arguments must have a string value; non-string values cannot be used!"
|
|
21
|
-
)
|
|
22
|
-
elif len(name) == 0:
|
|
23
|
-
raise AliasError(
|
|
24
|
-
"All @alias decorator name arguments must be valid Python identifier values; empty strings cannot be used!"
|
|
25
|
-
)
|
|
26
|
-
elif not name.isidentifier():
|
|
27
|
-
raise AliasError(
|
|
28
|
-
f"All @alias decorator name arguments must be valid Python identifier values; strings such as '{name}' are not considered valid identifiers by Python!"
|
|
29
|
-
)
|
|
30
|
-
elif keyword.iskeyword(name):
|
|
31
|
-
raise AliasError(
|
|
32
|
-
f"All @alias decorator name arguments must be valid Python identifier values; reserved keywords, such as '{name}' cannot be used!"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
def decorator(function: Callable) -> Callable:
|
|
36
|
-
function = unwrap(function)
|
|
37
|
-
|
|
38
|
-
if isinstance(aliases := getattr(function, "_classicist_aliases", None), tuple):
|
|
39
|
-
setattr(function, "_classicist_aliases", tuple([*aliases, *names]))
|
|
40
|
-
else:
|
|
41
|
-
setattr(function, "_classicist_aliases", names)
|
|
42
|
-
|
|
43
|
-
@wraps(function)
|
|
44
|
-
def wrapper(*args, **kwargs):
|
|
45
|
-
return function(*args, **kwargs)
|
|
46
|
-
|
|
47
|
-
return wrapper
|
|
48
|
-
|
|
49
|
-
return decorator
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def is_aliased(function: callable) -> bool:
|
|
53
|
-
"""The is_aliased() helper method can be used to determine if a class method has
|
|
54
|
-
been aliased."""
|
|
55
|
-
|
|
56
|
-
function = unwrap(function)
|
|
57
|
-
|
|
58
|
-
return isinstance(getattr(function, "_classicist_aliases", None), tuple)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def aliases(function: callable) -> list[str]:
|
|
62
|
-
"""The aliases() helper method can be used to obtain any class method aliases."""
|
|
63
|
-
|
|
64
|
-
function = unwrap(function)
|
|
65
|
-
|
|
66
|
-
if isinstance(aliases := getattr(function, "_classicist_aliases", None), tuple):
|
|
67
|
-
return list(aliases)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
__all__ = [
|
|
71
|
-
"alias",
|
|
72
|
-
"is_aliased",
|
|
73
|
-
"aliases",
|
|
74
|
-
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/classproperty/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/aliased/__init__.py
RENAMED
|
File without changes
|
{classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|