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.
Files changed (42) hide show
  1. {classicist-1.0.1/source/classicist.egg-info → classicist-1.0.2}/PKG-INFO +55 -14
  2. {classicist-1.0.1 → classicist-1.0.2}/README.md +54 -13
  3. {classicist-1.0.1 → classicist-1.0.2}/requirements.development.txt +1 -1
  4. classicist-1.0.2/source/classicist/decorators/aliased/__init__.py +177 -0
  5. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/aliased/__init__.py +2 -1
  6. classicist-1.0.2/source/classicist/version.txt +1 -0
  7. {classicist-1.0.1 → classicist-1.0.2/source/classicist.egg-info}/PKG-INFO +55 -14
  8. {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/requires.txt +1 -1
  9. {classicist-1.0.1 → classicist-1.0.2}/tests/test_aliased.py +112 -5
  10. classicist-1.0.1/source/classicist/decorators/aliased/__init__.py +0 -74
  11. classicist-1.0.1/source/classicist/version.txt +0 -1
  12. {classicist-1.0.1 → classicist-1.0.2}/LICENSE.md +0 -0
  13. {classicist-1.0.1 → classicist-1.0.2}/pyproject.toml +0 -0
  14. {classicist-1.0.1 → classicist-1.0.2}/requirements.distribution.txt +0 -0
  15. {classicist-1.0.1 → classicist-1.0.2}/requirements.txt +0 -0
  16. {classicist-1.0.1 → classicist-1.0.2}/setup.cfg +0 -0
  17. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/__init__.py +0 -0
  18. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/__init__.py +0 -0
  19. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/annotation/__init__.py +0 -0
  20. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/classproperty/__init__.py +0 -0
  21. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/deprecated/__init__.py +0 -0
  22. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
  23. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/decorators/nocache/__init__.py +0 -0
  24. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/__init__.py +0 -0
  25. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/__init__.py +0 -0
  26. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
  27. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
  28. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
  29. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
  30. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/inspector/__init__.py +0 -0
  31. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/logging/__init__.py +0 -0
  32. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/__init__.py +0 -0
  33. {classicist-1.0.1 → classicist-1.0.2}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
  34. {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/SOURCES.txt +0 -0
  35. {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/dependency_links.txt +0 -0
  36. {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/top_level.txt +0 -0
  37. {classicist-1.0.1 → classicist-1.0.2}/source/classicist.egg-info/zip-safe +0 -0
  38. {classicist-1.0.1 → classicist-1.0.2}/tests/test_annotation.py +0 -0
  39. {classicist-1.0.1 → classicist-1.0.2}/tests/test_classproperty.py +0 -0
  40. {classicist-1.0.1 → classicist-1.0.2}/tests/test_deprecated.py +0 -0
  41. {classicist-1.0.1 → classicist-1.0.2}/tests/test_hybridmethod.py +0 -0
  42. {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.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
 
@@ -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 method name aliases to methods defined within
236
- classes, such that both the original name and any defined aliases can be used to access
237
- the method at runtime. The `@alias` decorator cannot be used for methods defined outside
238
- of classes as the aliases are created as additional class attributes scoped to the class.
239
-
240
- To use the `@alias` decorator, it is necessary to set the containing class' metaclass to
241
- the `aliased` metaclass provided by the `classicist` library; the metaclass iterates
242
- through the class namespace during parse time and sets up the aliases as additional
243
- attributes on the class so that the aliased methods are available at runtime via both
244
- their original name and their aliases.
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
- class Welcome(object, metaclass=aliased):
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
 
@@ -1,5 +1,5 @@
1
1
  # Classicist Library: Development & Test Dependencies
2
- black==24.10.*
2
+ black==26.1.*
3
3
  pytest==8.3.*
4
4
  pytest-codeblocks==0.17.0
5
5
  pyflakes
@@ -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 AttributeError(
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.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,6 +1,6 @@
1
1
 
2
2
  [development]
3
- black==24.10.*
3
+ black==26.1.*
4
4
  pytest==8.3.*
5
5
  pytest-codeblocks==0.17.0
6
6
  pyflakes
@@ -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
- assert aliases(Welcome.hello) == ["sweet", "greet"]
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 a @property decorator."""
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
- assert aliases(Welcome.hello) == ["sweet", "greet"]
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