classicist 1.0.1__tar.gz → 1.0.3__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.3}/PKG-INFO +64 -20
  2. {classicist-1.0.1 → classicist-1.0.3}/README.md +62 -18
  3. {classicist-1.0.1 → classicist-1.0.3}/requirements.development.txt +1 -1
  4. classicist-1.0.3/source/classicist/decorators/aliased/__init__.py +171 -0
  5. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/metaclasses/aliased/__init__.py +2 -1
  6. classicist-1.0.3/source/classicist/version.txt +1 -0
  7. {classicist-1.0.1 → classicist-1.0.3/source/classicist.egg-info}/PKG-INFO +64 -20
  8. {classicist-1.0.1 → classicist-1.0.3}/source/classicist.egg-info/requires.txt +1 -1
  9. {classicist-1.0.1 → classicist-1.0.3}/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.3}/LICENSE.md +0 -0
  13. {classicist-1.0.1 → classicist-1.0.3}/pyproject.toml +0 -0
  14. {classicist-1.0.1 → classicist-1.0.3}/requirements.distribution.txt +0 -0
  15. {classicist-1.0.1 → classicist-1.0.3}/requirements.txt +0 -0
  16. {classicist-1.0.1 → classicist-1.0.3}/setup.cfg +0 -0
  17. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/__init__.py +0 -0
  18. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/__init__.py +0 -0
  19. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/annotation/__init__.py +0 -0
  20. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/classproperty/__init__.py +0 -0
  21. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/deprecated/__init__.py +0 -0
  22. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
  23. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/decorators/nocache/__init__.py +0 -0
  24. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/__init__.py +0 -0
  25. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/decorators/__init__.py +0 -0
  26. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
  27. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
  28. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
  29. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
  30. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/inspector/__init__.py +0 -0
  31. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/logging/__init__.py +0 -0
  32. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/metaclasses/__init__.py +0 -0
  33. {classicist-1.0.1 → classicist-1.0.3}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
  34. {classicist-1.0.1 → classicist-1.0.3}/source/classicist.egg-info/SOURCES.txt +0 -0
  35. {classicist-1.0.1 → classicist-1.0.3}/source/classicist.egg-info/dependency_links.txt +0 -0
  36. {classicist-1.0.1 → classicist-1.0.3}/source/classicist.egg-info/top_level.txt +0 -0
  37. {classicist-1.0.1 → classicist-1.0.3}/source/classicist.egg-info/zip-safe +0 -0
  38. {classicist-1.0.1 → classicist-1.0.3}/tests/test_annotation.py +0 -0
  39. {classicist-1.0.1 → classicist-1.0.3}/tests/test_classproperty.py +0 -0
  40. {classicist-1.0.1 → classicist-1.0.3}/tests/test_deprecated.py +0 -0
  41. {classicist-1.0.1 → classicist-1.0.3}/tests/test_hybridmethod.py +0 -0
  42. {classicist-1.0.1 → classicist-1.0.3}/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.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
 
@@ -6,7 +6,7 @@ The Classicist library provides several useful decorators and helper methods inc
6
6
  * `@classproperty` – a decorator that allow class methods to be accessed as class properties;
7
7
  * `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
8
8
  * `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
9
- * `@alias` – a decorator that can be used to add aliases to class methods;
9
+ * `@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;
10
10
  * `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
11
11
  * `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
12
12
  being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
@@ -230,30 +230,72 @@ exampleclass.greeting = "goodbye"
230
230
  assert exampleclass.greeting == "goodbye"
231
231
  ```
232
232
 
233
- #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
233
+ #### Alias Decorator & Metaclass: Add Aliases to Classes, Methods & Functions
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.
235
+ The `@alias` decorator can be used to add aliases to classes, methods defined within
236
+ classes, module-level functions, and nested functions when overriding the aliasing scope,
237
+ such that both the original name and any defined aliases can be used to access the same
238
+ code object at runtime.
239
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.
240
+ To alias a class or a module-level function, that is a function defined at the top-level
241
+ of a module file (rather than nested within a function or class), simply decorate the
242
+ class or module-level function with the `@alias(...)` decorator and specify the one or
243
+ more name aliases for the class or function as one or more string arguments passed into
244
+ the decorator method.
245
245
 
246
- The example below demonstrates adding an alias to a method defined within a class, and
247
- using the `aliased` metaclass when defining the class to ensure that the alias is parsed
248
- and translated to an additional class attribute so that the method is accessible via its
249
- original name and the alias at runtime.
246
+ To use the `@alias` decorator on methods defined within a class, it is also necessary to
247
+ set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
248
+ library; the metaclass iterates through the class' namespace during parse time and sets up
249
+ the aliases as additional attributes on the class so that the aliased methods are available
250
+ at runtime via both their original name and any aliases.
251
+
252
+ The examples below demonstrate adding an alias to a module-level function, a class and a
253
+ method defined within a class, and using the `aliased` metaclass when defining a class
254
+ that contains aliased methods to ensure that any aliases are parsed and translated to
255
+ additional class attributes so that the method is accessible via its original name and
256
+ any alias at runtime.
257
+
258
+ If control over the scope is required, usually for nested functions, the optional `scope`
259
+ keyword-only argument can be used to specify the scope into which to apply the alias; this
260
+ must be a reference to `globals()` or `locals()` at the point in code where the `@alias(...)`
261
+ decorator is applied to the nested function.
250
262
 
251
263
  ```python
252
264
  from classicist import aliased, alias, is_aliased, aliases
253
265
 
254
- class Welcome(object, metaclass=aliased):
266
+ # Define an alias on a module-level method; as this demonstration occurs
267
+ # within the README file which is parsed by and run within an external
268
+ # scope by pytest and pytest-codeblocks, we override the scope within
269
+ # which to apply the alias otherwise the alias would be assigned within
270
+ # an external scope which would prevent the alias from working; however
271
+ # it is rare to need to override the inferred scope, and aliasing of
272
+ # module-level functions defined within actual modules will work normally;
273
+ # for rare cases where overriding scope is necessary the optional `scope`
274
+ # keyword-only argument can be used as shown below.
275
+ @alias("sums", scope=globals())
276
+ def adds(a: int, b: int) -> int:
277
+ return a + b
278
+
279
+ assert globals().get("adds") is adds
280
+ assert globals().get("sums") is sums
281
+ assert adds is sums
282
+ assert adds(1, 2) == 3
283
+ assert sums(1, 2) == 3
284
+
285
+ # Define an alias on a class
286
+ @alias("Color")
287
+ class Colour(object):
288
+ pass
289
+
290
+ assert Colour is Color
291
+
292
+ # Define an alias on a method defined within a class;
293
+ # this also requires the use of the aliased metaclass
294
+ # which is responsible for adding the aliases within
295
+ # the scope of the class once the class has been parsed
296
+ class Welcome(metaclass=aliased):
255
297
  @alias("greet")
256
- def hello(self, name: str):
298
+ def hello(self, name: str) -> str:
257
299
  return f"Hello {name}!"
258
300
 
259
301
  assert is_aliased(Welcome.hello) is True
@@ -272,7 +314,9 @@ assert welcome.greet("you") == "Hello you!"
272
314
 
273
315
  ⚠️ Note: Aliases must be valid Python identifiers, following the same rules as for all
274
316
  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.
317
+ alias is specified an `AliasError` exception will be raised at runtime. Furthermore, if
318
+ a name has already been used in the current scope, an `AliasError` exception will be
319
+ raised at runtime.
276
320
 
277
321
  #### Annotation Decorator: Add Arbitrary Annotations to Code Objects
278
322
 
@@ -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,171 @@
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.debug(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
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
+ )
145
+
146
+ return decorator
147
+
148
+
149
+ def is_aliased(function: callable) -> bool:
150
+ """The is_aliased() helper method can be used to determine if a class method has
151
+ been aliased."""
152
+
153
+ function = unwrap(function)
154
+
155
+ return isinstance(getattr(function, "_classicist_aliases", None), tuple)
156
+
157
+
158
+ def aliases(function: callable) -> list[str]:
159
+ """The aliases() helper method can be used to obtain any class method aliases."""
160
+
161
+ function = unwrap(function)
162
+
163
+ if isinstance(aliases := getattr(function, "_classicist_aliases", None), tuple):
164
+ return list(aliases)
165
+
166
+
167
+ __all__ = [
168
+ "alias",
169
+ "is_aliased",
170
+ "aliases",
171
+ ]
@@ -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.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,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