classicist 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
classicist/__init__.py CHANGED
@@ -1,118 +1,50 @@
1
- import logging
2
-
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
-
7
- class hybridmethod(object):
8
- """The 'hybridmethod' decorator allows a method to be used as both a class method
9
- and an instance method. The hybridmethod class decorator can wrap methods defined
10
- in classes using the usual @decorator syntax. Methods defined in classes that are
11
- decorated with the @hybridmethod decorator can be accessed as both class methods
12
- and as instance methods, with the first argument passed to the method being the
13
- reference to either the class when the method is called as a class method or to
14
- the instance when the method is called as an instance method.
15
-
16
- A check of the value of the first variable using isinstance(<variable>, <class>) can
17
- be used within a hybrid method to determine if the call was made on an instance of
18
- the class in which case the isinstance() call would evalute to True or if the call
19
- was made on the class itself, in which case isinstance() would evaluate to False.
20
- The variable passed as the first argument to the method may have any name, including
21
- 'self', as in Python, the use of 'self' as the name of the first argument on an
22
- instance method is just customary and the name has no significance like it does in
23
- other languages where the reference to the instance is provided automatically and
24
- may go by 'self', 'this' or something else."""
25
-
26
- def __init__(self, function: callable):
27
- logger.debug(
28
- "%s.__init__(function: %s)",
29
- self.__class__.__name__,
30
- function,
31
- )
32
-
33
- if not callable(function):
34
- raise TypeError(
35
- "The '%s' decorator can only be used to wrap callables!"
36
- % (self.__class__.__name__)
37
- )
38
- elif not type(function).__name__ == "function":
39
- raise TypeError(
40
- "The '%s' decorator can only be used to wrap functions!"
41
- % (self.__class__.__name__)
42
- )
43
-
44
- self.function: callable = function
45
-
46
- def __get__(self, instance, owner) -> callable:
47
- logger.debug(
48
- "%s.__get__(self: %s, instance: %s, owner: %s)",
49
- self.__class__.__name__,
50
- self,
51
- instance,
52
- owner,
53
- )
54
-
55
- if instance is None:
56
- return lambda *args, **kwargs: self.function(owner, *args, **kwargs)
57
- else:
58
- return lambda *args, **kwargs: self.function(instance, *args, **kwargs)
59
-
60
-
61
- class classproperty(property):
62
- """The classproperty decorator transforms a method into a class-level property. This
63
- provides access to the method as if it were a class attribute; this addresses the
64
- removal of support for combining the @classmethod and @property decorators to create
65
- class properties in Python 3.13, a change which was made due to some complexity in
66
- the underlying interpreter implementation."""
67
-
68
- def __init__(self, fget: callable, fset: callable = None, fdel: callable = None):
69
- super().__init__(fget, fset, fdel)
70
-
71
- def __get__(self, instance: object, klass: type = None):
72
- if klass is None:
73
- return self
74
- return self.fget(klass)
75
-
76
- def __set__(self, instance: object, value: object):
77
- # Note that the __set__ descriptor cannot be used on class methods unless
78
- # the class is created with a metaclass that implements this behaviour
79
- raise NotImplemented
80
-
81
- def __delete__(self, instance: object):
82
- # Note that the __delete__ descriptor cannot be used on class methods unless
83
- # the class is created with a metaclass that implements this behaviour
84
- raise NotImplemented
85
-
86
- def __getattr__(self, name: str):
87
- if name in ATTRIBUTES:
88
- return getattr(self.fget, name)
89
- else:
90
- raise AttributeError(
91
- "The classproperty method '%s' does not have an '%s' attribute!"
92
- % (
93
- self.fget.__name__,
94
- name,
95
- )
96
- )
97
-
98
- # # For inspectability, provide access to the underlying function's metadata
99
- # # including __module__, __name__, __qualname__, __doc__, and __annotations__
100
- # @property
101
- # def __module__(self):
102
- # return self.fget.__module__
103
- #
104
- # @property
105
- # def __name__(self):
106
- # return self.fget.__name__
107
- #
108
- # @property
109
- # def __qualname__(self):
110
- # return self.fget.__qualname__
111
- #
112
- # @property
113
- # def __doc__(self):
114
- # return self.fget.__doc__
115
- #
116
- # @property
117
- # def __annotations__(self):
118
- # return self.fget.__annotations__
1
+ # Decorator Classes
2
+ from classicist.decorators import (
3
+ alias,
4
+ annotate,
5
+ annotation,
6
+ annotations,
7
+ classproperty,
8
+ deprecated,
9
+ hybridmethod,
10
+ nocache,
11
+ )
12
+
13
+ # Decorator Helper Methods
14
+ from classicist.decorators import (
15
+ is_aliased,
16
+ aliases,
17
+ is_deprecated,
18
+ )
19
+
20
+ # Meta Classes
21
+ from classicist.metaclasses import (
22
+ aliased,
23
+ shadowproof,
24
+ )
25
+
26
+ # Exception Classes
27
+ from classicist.exceptions import (
28
+ AttributeShadowingError,
29
+ )
30
+
31
+ __all__ = [
32
+ # Decorators
33
+ "alias",
34
+ "annotate",
35
+ "annotation",
36
+ "annotations",
37
+ "classproperty",
38
+ "deprecated",
39
+ "hybridmethod",
40
+ "nocache",
41
+ # Decorator Helper Methods
42
+ "is_aliased",
43
+ "aliases",
44
+ "is_deprecated",
45
+ # Meta Classes
46
+ "aliased",
47
+ "shadowproof",
48
+ # Exception Classes
49
+ "AttributeShadowingError",
50
+ ]
@@ -0,0 +1,20 @@
1
+ from classicist.decorators.aliased import alias, aliases, is_aliased
2
+ from classicist.decorators.annotation import annotate, annotation, annotations
3
+ from classicist.decorators.classproperty import classproperty
4
+ from classicist.decorators.deprecated import deprecated, is_deprecated
5
+ from classicist.decorators.hybridmethod import hybridmethod
6
+ from classicist.decorators.nocache import nocache
7
+
8
+ __all__ = [
9
+ "alias",
10
+ "aliases",
11
+ "annotate",
12
+ "annotation",
13
+ "annotations",
14
+ "classproperty",
15
+ "deprecated",
16
+ "is_aliased",
17
+ "is_deprecated",
18
+ "hybridmethod",
19
+ "nocache",
20
+ ]
@@ -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
+ ]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from classicist.logging import logger
4
+ from classicist.exceptions.decorators.annotation import AnnotationError
5
+
6
+ import builtins
7
+
8
+ logger = logger.getChild(__name__)
9
+
10
+
11
+ def annotate(thing: object, **annotations: dict[str, object]) -> callable:
12
+ """Supports associating arbitrary annotations with the provided code object."""
13
+
14
+ if isinstance(thing, object) and not thing in [None, True, False]:
15
+ if hasattr(thing, "_classicist_annotations") and isinstance(
16
+ thing._classicist_annotations, dict
17
+ ):
18
+ thing._classicist_annotations.update(annotations)
19
+ else:
20
+ try:
21
+ thing._classicist_annotations = annotations
22
+ except AttributeError as exception:
23
+ raise AnnotationError(
24
+ "Cannot assign annotations to an object of type %s: %s!"
25
+ % (builtins.type(thing), str(exception))
26
+ )
27
+
28
+ return thing
29
+
30
+
31
+ def annotation(**annotations: dict[str, object]) -> callable:
32
+ """Supports associating arbitrary annotations with a code object via a decorator."""
33
+
34
+ def decorator(thing: object) -> object:
35
+ return annotate(thing, **annotations)
36
+
37
+ return decorator
38
+
39
+
40
+ def annotations(thing: object, metadata: bool = False) -> dict[str, object] | None:
41
+ """Supports obtaining arbitrary annotations for a code object."""
42
+
43
+ if isinstance(thing, object) and hasattr(thing, "_classicist_annotations"):
44
+ if isinstance(annotations := thing._classicist_annotations, dict):
45
+ if metadata is True:
46
+ annotations["__name__"] = thing.__name__
47
+ annotations["__type__"] = type(thing)
48
+ return annotations
49
+
50
+
51
+ __all__ = [
52
+ "annotate",
53
+ "annotation",
54
+ "annotations",
55
+ ]
@@ -0,0 +1,41 @@
1
+ from classicist.logging import logger
2
+
3
+ logger = logger.getChild(__name__)
4
+
5
+
6
+ class classproperty(property):
7
+ """The classproperty decorator transforms a method into a class-level property. This
8
+ provides access to the method as if it were a class attribute; this addresses the
9
+ removal of support for combining the @classmethod and @property decorators to create
10
+ class properties in Python 3.13, a change which was made due to some complexity in
11
+ the underlying interpreter implementation."""
12
+
13
+ def __init__(self, fget: callable, fset: callable = None, fdel: callable = None):
14
+ super().__init__(fget, fset, fdel)
15
+
16
+ def __get__(self, instance: object, klass: type = None):
17
+ if klass is None:
18
+ return self
19
+ return self.fget(klass)
20
+
21
+ def __set__(self, instance: object, value: object):
22
+ # Note that the __set__ descriptor cannot be used on class methods unless
23
+ # the class is created with a metaclass that implements this behaviour.
24
+ raise NotImplementedError
25
+
26
+ def __delete__(self, instance: object):
27
+ # Note that the __delete__ descriptor cannot be used on class methods unless
28
+ # the class is created with a metaclass that implements this behaviour.
29
+ raise NotImplementedError
30
+
31
+ def __getattr__(self, name: str):
32
+ if hasattr(self.fget, name):
33
+ return getattr(self.fget, name)
34
+ else:
35
+ raise AttributeError(
36
+ "The classproperty method '%s' does not have an '%s' attribute!"
37
+ % (
38
+ self.fget.__name__,
39
+ name,
40
+ )
41
+ )
@@ -0,0 +1,101 @@
1
+ from classicist.logging import logger
2
+ from classicist.decorators.annotation import annotate
3
+
4
+ from functools import wraps, partial
5
+ from datetime import datetime
6
+
7
+ logger = logger.getChild(__name__)
8
+
9
+
10
+ def deprecated(
11
+ thing: object = None,
12
+ /,
13
+ reason: str = None,
14
+ since: str = None,
15
+ removal: str = None,
16
+ replacement: str = None,
17
+ advice: str = None,
18
+ ticket: str = None,
19
+ **annotations: dict[str, object],
20
+ ) -> object:
21
+ """The @deprecated decorator provides support for marking code objects as having
22
+ been deprecated. The decorator also provides support for adding additional arbitrary
23
+ annotations to the object beyond the directly supported annotations."""
24
+
25
+ if reason is None:
26
+ pass
27
+ elif isinstance(reason, str):
28
+ annotations["reason"] = reason
29
+ else:
30
+ raise TypeError(
31
+ "The 'reason' argument, if specified, must have a string value!"
32
+ )
33
+
34
+ if replacement is None:
35
+ pass
36
+ elif isinstance(replacement, str):
37
+ annotations["replacement"] = replacement
38
+ else:
39
+ raise TypeError(
40
+ "The 'replacement' argument, if specified, must have a string value!"
41
+ )
42
+
43
+ if since is None:
44
+ pass
45
+ elif isinstance(since, (str, datetime)):
46
+ annotations["since"] = since
47
+ else:
48
+ raise TypeError(
49
+ "The 'since' argument, if specified, must have a string or datetime value!"
50
+ )
51
+
52
+ if removal is None:
53
+ pass
54
+ elif isinstance(removal, (str, datetime)):
55
+ annotations["removal"] = removal
56
+ else:
57
+ raise TypeError(
58
+ "The 'removal' argument, if specified, must have a string or datetime value!"
59
+ )
60
+
61
+ if advice is None:
62
+ pass
63
+ elif isinstance(advice, str):
64
+ annotations["advice"] = advice
65
+ else:
66
+ raise TypeError(
67
+ "The 'advice' argument, if specified, must have a string value!"
68
+ )
69
+
70
+ if ticket is None:
71
+ pass
72
+ elif isinstance(ticket, str):
73
+ annotations["ticket"] = ticket
74
+ else:
75
+ raise TypeError(
76
+ "The 'ticket' argument, if specified, must have a string value!"
77
+ )
78
+
79
+ if thing is None:
80
+ return partial(deprecated, **annotations)
81
+
82
+ @wraps(thing)
83
+ def decorator() -> object:
84
+ if not hasattr(thing, "_classicist_deprecated"):
85
+ setattr(thing, "_classicist_deprecated", True)
86
+ return annotate(thing, **annotations)
87
+
88
+ return decorator()
89
+
90
+
91
+ def is_deprecated(thing: object) -> bool:
92
+ """The is_deprecated() helper method can be used to determine if an object or class
93
+ has been marked as deprecated."""
94
+
95
+ return getattr(thing, "_classicist_deprecated", False) is True
96
+
97
+
98
+ __all__ = [
99
+ "deprecated",
100
+ "is_deprecated",
101
+ ]
@@ -0,0 +1,57 @@
1
+ from classicist.logging import logger
2
+
3
+ logger = logger.getChild(__name__)
4
+
5
+
6
+ class hybridmethod(object):
7
+ """The 'hybridmethod' decorator allows a method to be used as both a class method
8
+ and an instance method. The hybridmethod class decorator can wrap methods defined
9
+ in classes using the usual @decorator syntax. Methods defined in classes that are
10
+ decorated with the @hybridmethod decorator can be accessed as both class methods
11
+ and as instance methods, with the first argument passed to the method being the
12
+ reference to either the class when the method is called as a class method or to
13
+ the instance when the method is called as an instance method.
14
+
15
+ A check of the value of the first variable using isinstance(<variable>, <class>) can
16
+ be used within a hybrid method to determine if the call was made on an instance of
17
+ the class in which case the isinstance() call would evalute to True or if the call
18
+ was made on the class itself, in which case isinstance() would evaluate to False.
19
+ The variable passed as the first argument to the method may have any name, including
20
+ 'self', as in Python, the use of 'self' as the name of the first argument on an
21
+ instance method is just customary and the name has no significance like it does in
22
+ other languages where the reference to the instance is provided automatically and
23
+ may go by 'self', 'this' or something else."""
24
+
25
+ def __init__(self, function: callable):
26
+ logger.debug(
27
+ "%s.__init__(function: %s)",
28
+ self.__class__.__name__,
29
+ function,
30
+ )
31
+
32
+ if not callable(function):
33
+ raise TypeError(
34
+ "The '%s' decorator can only be used to wrap callables!"
35
+ % (self.__class__.__name__)
36
+ )
37
+ elif not type(function).__name__ == "function":
38
+ raise TypeError(
39
+ "The '%s' decorator can only be used to wrap functions!"
40
+ % (self.__class__.__name__)
41
+ )
42
+
43
+ self.function: callable = function
44
+
45
+ def __get__(self, instance, owner) -> callable:
46
+ logger.debug(
47
+ "%s.__get__(self: %s, instance: %s, owner: %s)",
48
+ self.__class__.__name__,
49
+ self,
50
+ instance,
51
+ owner,
52
+ )
53
+
54
+ if instance is None:
55
+ return lambda *args, **kwargs: self.function(owner, *args, **kwargs)
56
+ else:
57
+ return lambda *args, **kwargs: self.function(instance, *args, **kwargs)
@@ -0,0 +1,10 @@
1
+ from classicist.logging import logger
2
+
3
+ logger = logger.getChild(__name__)
4
+
5
+
6
+ def nocache(function: callable):
7
+ """A no-cache decorator to specifically call out functions and properties that must
8
+ not be cached using the functools.cache decorator or similar."""
9
+
10
+ return function
@@ -0,0 +1,12 @@
1
+ from classicist.exceptions.decorators import (
2
+ AnnotationError,
3
+ )
4
+
5
+ from classicist.exceptions.metaclasses import (
6
+ AttributeShadowingError,
7
+ )
8
+
9
+ __all__ = [
10
+ "AnnotationError",
11
+ "AttributeShadowingError",
12
+ ]
@@ -0,0 +1,7 @@
1
+ from classicist.exceptions.decorators.aliased import AliasError
2
+ from classicist.exceptions.decorators.annotation import AnnotationError
3
+
4
+ __all__ = [
5
+ "AliasError",
6
+ "AnnotationError",
7
+ ]
@@ -0,0 +1,2 @@
1
+ class AliasError(AttributeError):
2
+ pass
@@ -0,0 +1,2 @@
1
+ class AnnotationError(AttributeError):
2
+ pass
@@ -0,0 +1,5 @@
1
+ from classicist.exceptions.metaclasses.shadowproof import AttributeShadowingError
2
+
3
+ __all__ = [
4
+ "AttributeShadowingError",
5
+ ]
@@ -0,0 +1,2 @@
1
+ class AttributeShadowingError(AttributeError):
2
+ pass