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.
@@ -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 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."""
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(function: Callable) -> Callable:
36
- function = unwrap(function)
44
+ def decorator(thing: object, *args, **kwargs) -> object:
45
+ nonlocal scope
37
46
 
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)
47
+ thing = unwrap(thing)
42
48
 
43
- @wraps(function)
44
- def wrapper(*args, **kwargs):
45
- return function(*args, **kwargs)
49
+ logger.info(f"@alias({names}) called on {thing}")
46
50
 
47
- return wrapper
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 AttributeError(
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
+ 1.0.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classicist
3
- Version: 1.0.1
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.10.*; extra == "development"
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 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.
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 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.
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
- class Welcome(object, metaclass=aliased):
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=1R5uyUBYVUqEVYpbQC7m71_fVFXjXJAv7aYc2odSlDo,5
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=4g5MBnyypHunvETGLxFHUFr85ZGyTEYkCtL3tG9jtj8,2461
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=82YryLpG-SRmiI6m_Q8pqhHACodoBKyNu0PZ4RMucvE,1973
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.1.dist-info/licenses/LICENSE.md,sha256=qBmrjPmSCp0YFyaIl2G3FU3rniFD31YC0Yd3MrO1wEg,1070
22
- classicist-1.0.1.dist-info/METADATA,sha256=065CjOBWs9ImW8f5Oo3r-e1_Lb_zCppmwNkxqdjUD9g,22994
23
- classicist-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- classicist-1.0.1.dist-info/top_level.txt,sha256=beG3ZuwObnmnY_mgNSN5CaVIWpI2VKszjVdKHPgZBhc,11
25
- classicist-1.0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
26
- classicist-1.0.1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5