classicist 1.0.1__py3-none-any.whl → 1.0.2__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/decorators/aliased/__init__.py +117 -14
- classicist/metaclasses/aliased/__init__.py +2 -1
- classicist/version.txt +1 -1
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/METADATA +55 -14
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/RECORD +9 -9
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/WHEEL +1 -1
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/licenses/LICENSE.md +0 -0
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/top_level.txt +0 -0
- {classicist-1.0.1.dist-info → classicist-1.0.2.dist-info}/zip-safe +0 -0
|
@@ -6,20 +6,29 @@ from typing import Callable
|
|
|
6
6
|
from functools import wraps
|
|
7
7
|
|
|
8
8
|
import keyword
|
|
9
|
+
import inspect
|
|
10
|
+
import sys
|
|
9
11
|
|
|
10
12
|
logger = logger.getChild(__name__)
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
def alias(*names: tuple[str]) -> Callable:
|
|
14
|
-
"""Decorator that
|
|
15
|
-
|
|
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."""
|
|
16
25
|
|
|
17
26
|
for name in names:
|
|
18
27
|
if not isinstance(name, str):
|
|
19
28
|
raise AliasError(
|
|
20
29
|
"All @alias decorator name arguments must have a string value; non-string values cannot be used!"
|
|
21
30
|
)
|
|
22
|
-
elif len(name) == 0:
|
|
31
|
+
elif len(name := name.strip()) == 0:
|
|
23
32
|
raise AliasError(
|
|
24
33
|
"All @alias decorator name arguments must be valid Python identifier values; empty strings cannot be used!"
|
|
25
34
|
)
|
|
@@ -32,19 +41,113 @@ def alias(*names: tuple[str]) -> Callable:
|
|
|
32
41
|
f"All @alias decorator name arguments must be valid Python identifier values; reserved keywords, such as '{name}' cannot be used!"
|
|
33
42
|
)
|
|
34
43
|
|
|
35
|
-
def decorator(
|
|
36
|
-
|
|
44
|
+
def decorator(thing: object, *args, **kwargs) -> object:
|
|
45
|
+
nonlocal scope
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
setattr(function, "_classicist_aliases", tuple([*aliases, *names]))
|
|
40
|
-
else:
|
|
41
|
-
setattr(function, "_classicist_aliases", names)
|
|
47
|
+
thing = unwrap(thing)
|
|
42
48
|
|
|
43
|
-
@
|
|
44
|
-
def wrapper(*args, **kwargs):
|
|
45
|
-
return function(*args, **kwargs)
|
|
49
|
+
logger.info(f"@alias({names}) called on {thing}")
|
|
46
50
|
|
|
47
|
-
|
|
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
|
+
)
|
|
48
151
|
|
|
49
152
|
return decorator
|
|
50
153
|
|
|
@@ -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
|
|
classicist/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
classicist/__init__.py,sha256=Rkm1Vx0Z-BHPH1FSzFLrAxwkFEt1xvMxZz-mC8FJSDc,834
|
|
2
|
-
classicist/version.txt,sha256=
|
|
2
|
+
classicist/version.txt,sha256=v-wuNFg62n5q8stzmT-3Wj9xR6bJQ-X_X1xClPxXe5A,5
|
|
3
3
|
classicist/decorators/__init__.py,sha256=wplcs2JnyGMOh72626-x-WyVotVoT7nvk-dpjeTXQ88,600
|
|
4
|
-
classicist/decorators/aliased/__init__.py,sha256=
|
|
4
|
+
classicist/decorators/aliased/__init__.py,sha256=e62ENmJh_kH4Ufh9duAYWg93cfmINIcpzeP0haJo7cw,7132
|
|
5
5
|
classicist/decorators/annotation/__init__.py,sha256=20WwmrXDxT85sItpDiCdC-hZjbyDR6E2mLPMSKQgm8g,1796
|
|
6
6
|
classicist/decorators/classproperty/__init__.py,sha256=ED37_20UeAGKX1ahsv16wTg0JAJT4UBLqiNRbKAp2KE,1646
|
|
7
7
|
classicist/decorators/deprecated/__init__.py,sha256=aAPFQoT-pJf1nauQGiPADkPBREMvEQJaUQc3kjA48Rg,2764
|
|
@@ -16,11 +16,11 @@ classicist/exceptions/metaclasses/shadowproof/__init__.py,sha256=QT-QFRnjlnVexl9
|
|
|
16
16
|
classicist/inspector/__init__.py,sha256=Z8Se9CLkZ4WNRDX9Ui31Kt3BklZMYBpDRo7WWTsCQ_g,1309
|
|
17
17
|
classicist/logging/__init__.py,sha256=LJ-Nih1LacPrO2TvTT6l84So-pgw84AHJ8IhzYKl5rw,57
|
|
18
18
|
classicist/metaclasses/__init__.py,sha256=mYhR5rM7cnJlaNUlgoQWOo44RQSORteRjYd0y0BajxQ,159
|
|
19
|
-
classicist/metaclasses/aliased/__init__.py,sha256=
|
|
19
|
+
classicist/metaclasses/aliased/__init__.py,sha256=grs4Z8dMY6R2fCFn4UCPdCzEJtVaAv-tsWFwY1DCUpI,2033
|
|
20
20
|
classicist/metaclasses/shadowproof/__init__.py,sha256=d55uNjcaCorD3MdDUv7aRVpk2MlE8JzMjint6Vvf_Yc,1492
|
|
21
|
-
classicist-1.0.
|
|
22
|
-
classicist-1.0.
|
|
23
|
-
classicist-1.0.
|
|
24
|
-
classicist-1.0.
|
|
25
|
-
classicist-1.0.
|
|
26
|
-
classicist-1.0.
|
|
21
|
+
classicist-1.0.2.dist-info/licenses/LICENSE.md,sha256=qBmrjPmSCp0YFyaIl2G3FU3rniFD31YC0Yd3MrO1wEg,1070
|
|
22
|
+
classicist-1.0.2.dist-info/METADATA,sha256=g3SDEBecgGq15CnHFUVr2mfyhFNQ3tG752I5OSMaAbk,24825
|
|
23
|
+
classicist-1.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
24
|
+
classicist-1.0.2.dist-info/top_level.txt,sha256=beG3ZuwObnmnY_mgNSN5CaVIWpI2VKszjVdKHPgZBhc,11
|
|
25
|
+
classicist-1.0.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
26
|
+
classicist-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|