classicist 1.0.1__py3-none-any.whl → 1.0.3__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,107 @@ 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.debug(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
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
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
+ # The qualified name for module-level functions only contain the name of the
98
+ # function, whereas functions nested within other functions or classes have
99
+ # names comprised of multiple parts separated by the "." character; because
100
+ # it is only currently possible to alias module-level functions, any nested
101
+ # or class methods are ignored during this stage of the aliasing process.
102
+ if len(thing.__qualname__.split(".")) > 1:
103
+ logger.debug(
104
+ "Unable to apply alias to functions defined beyond the top-level of a module: %s!"
105
+ % (thing.__qualname__)
106
+ )
107
+
108
+ return wrapper_function(*args, **kwargs)
109
+
110
+ # if signature := inspect.signature(thing):
111
+ # if len(parameters := signature.parameters) > 0 and "self" in parameters:
112
+ # return wrapper_function(*args, **kwargs)
113
+
114
+ if isinstance(scope, object):
115
+ # At this point we should only be left with module-level functions to alias
116
+ for name in names:
117
+ # Ensure the scope doesn't already contain an object of the same name
118
+ if hasattr(scope, name):
119
+ raise AliasError(
120
+ "Cannot create alias '%s' for %s function in the %s module as an object with that name already exists!"
121
+ % (
122
+ name,
123
+ thing,
124
+ scope,
125
+ )
126
+ )
127
+
128
+ logger.debug(f"Added alias '{name}' to {scope}.{thing}")
129
+
130
+ if isinstance(scope, dict):
131
+ scope[name] = thing
132
+ elif isinstance(scope, object):
133
+ setattr(scope, name, thing)
134
+ else:
135
+ logger.warning(
136
+ f"No scope was found or specified for {thing} into which to assign aliases!"
137
+ )
138
+
139
+ return wrapper_function(*args, **kwargs)
140
+ else:
141
+ raise AliasError(
142
+ "The @alias decorator can only be applied to classes, methods and functions, not %s!"
143
+ % (type(thing))
144
+ )
48
145
 
49
146
  return decorator
50
147
 
@@ -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.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classicist
3
- Version: 1.0.1
3
+ Version: 1.0.3
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"
@@ -39,7 +39,7 @@ The Classicist library provides several useful decorators and helper methods inc
39
39
  * `@classproperty` – a decorator that allow class methods to be accessed as class properties;
40
40
  * `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
41
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;
42
+ * `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
43
43
  * `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
44
44
  * `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
45
45
  being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
@@ -263,30 +263,72 @@ exampleclass.greeting = "goodbye"
263
263
  assert exampleclass.greeting == "goodbye"
264
264
  ```
265
265
 
266
- #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
266
+ #### Alias Decorator & Metaclass: Add Aliases to Classes, Methods & Functions
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, module-level functions, and nested functions when overriding the aliasing scope,
270
+ such that both the original name and any defined aliases can be used to access the same
271
+ code object at runtime.
272
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.
273
+ To alias a class or a module-level function, that is a function defined at the top-level
274
+ of a module file (rather than nested within a function or class), simply decorate the
275
+ class or module-level function with the `@alias(...)` decorator and specify the one or
276
+ more name aliases for the class or function as one or more string arguments passed into
277
+ the decorator method.
278
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.
279
+ To use the `@alias` decorator on methods defined within a class, it is also necessary to
280
+ set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
281
+ library; the metaclass iterates through the class' namespace during parse time and sets up
282
+ the aliases as additional attributes on the class so that the aliased methods are available
283
+ at runtime via both their original name and any aliases.
284
+
285
+ The examples below demonstrate adding an alias to a module-level function, a class and a
286
+ method defined within a class, and using the `aliased` metaclass when defining a class
287
+ that contains aliased methods to ensure that any aliases are parsed and translated to
288
+ additional class attributes so that the method is accessible via its original name and
289
+ any alias at runtime.
290
+
291
+ If control over the scope is required, usually for nested functions, the optional `scope`
292
+ keyword-only argument can be used to specify the scope into which to apply the alias; this
293
+ must be a reference to `globals()` or `locals()` at the point in code where the `@alias(...)`
294
+ decorator is applied to the nested function.
283
295
 
284
296
  ```python
285
297
  from classicist import aliased, alias, is_aliased, aliases
286
298
 
287
- class Welcome(object, metaclass=aliased):
299
+ # Define an alias on a module-level method; as this demonstration occurs
300
+ # within the README file which is parsed by and run within an external
301
+ # scope by pytest and pytest-codeblocks, we override the scope within
302
+ # which to apply the alias otherwise the alias would be assigned within
303
+ # an external scope which would prevent the alias from working; however
304
+ # it is rare to need to override the inferred scope, and aliasing of
305
+ # module-level functions defined within actual modules will work normally;
306
+ # for rare cases where overriding scope is necessary the optional `scope`
307
+ # keyword-only argument can be used as shown below.
308
+ @alias("sums", scope=globals())
309
+ def adds(a: int, b: int) -> int:
310
+ return a + b
311
+
312
+ assert globals().get("adds") is adds
313
+ assert globals().get("sums") is sums
314
+ assert adds is sums
315
+ assert adds(1, 2) == 3
316
+ assert sums(1, 2) == 3
317
+
318
+ # Define an alias on a class
319
+ @alias("Color")
320
+ class Colour(object):
321
+ pass
322
+
323
+ assert Colour is Color
324
+
325
+ # Define an alias on a method defined within a class;
326
+ # this also requires the use of the aliased metaclass
327
+ # which is responsible for adding the aliases within
328
+ # the scope of the class once the class has been parsed
329
+ class Welcome(metaclass=aliased):
288
330
  @alias("greet")
289
- def hello(self, name: str):
331
+ def hello(self, name: str) -> str:
290
332
  return f"Hello {name}!"
291
333
 
292
334
  assert is_aliased(Welcome.hello) is True
@@ -305,7 +347,9 @@ assert welcome.greet("you") == "Hello you!"
305
347
 
306
348
  ⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
307
349
  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.
350
+ alias is specified an `AliasError` exception will be raised at runtime. Furthermore, if
351
+ a name has already been used in the current scope, an `AliasError` exception will be
352
+ raised at runtime.
309
353
 
310
354
  #### Annotation Decorator: Add Arbitrary Annotations to Code Objects
311
355
 
@@ -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=INLLCW0atBpBQCRtEvB79rjLdD_UgSK3JTLAPUTFwUo,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=vW1P9jbGkOD_m9GvgEXF0Uni-Dt6rfVXDVF5D4wC1GY,6926
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.3.dist-info/licenses/LICENSE.md,sha256=qBmrjPmSCp0YFyaIl2G3FU3rniFD31YC0Yd3MrO1wEg,1070
22
+ classicist-1.0.3.dist-info/METADATA,sha256=iZaQqjD9wd84Bb_Dx_Se-RMYgZTvvf2Hkf2idrnswgk,25128
23
+ classicist-1.0.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
24
+ classicist-1.0.3.dist-info/top_level.txt,sha256=beG3ZuwObnmnY_mgNSN5CaVIWpI2VKszjVdKHPgZBhc,11
25
+ classicist-1.0.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
26
+ classicist-1.0.3.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