classicist 1.0.0__py3-none-any.whl → 1.0.1__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,74 @@
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
+ ]
@@ -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
@@ -0,0 +1,38 @@
1
+ from classicist.logging import logger
2
+
3
+ import sys
4
+
5
+ logger = logger.getChild(__name__)
6
+
7
+
8
+ def unwrap(function: callable) -> callable:
9
+ """Support unwrapping methods decorated with @property and other descriptor protocol
10
+ decorators such as @classmethod and @staticmethod as well as function decorators
11
+ that follow best-practice and have a __wrapped__ attribute referencing the original
12
+ function, so that the original function can be found by unwrapping via the chain of
13
+ the __wrapped__ and fget attributes.
14
+
15
+ This implementation is based on the standard library's inspect.unwrap() method."""
16
+
17
+ original: callable = function
18
+
19
+ functionids: dict[id, callable] = {id(function): function}
20
+
21
+ recursion_limit: int = sys.getrecursionlimit()
22
+
23
+ while (w := hasattr(function, "__wrapped__")) or (d := hasattr(function, "fget")):
24
+ if w is True:
25
+ function = getattr(function, "__wrapped__")
26
+ elif d is True:
27
+ function = getattr(function, "fget")
28
+
29
+ functionid: int = id(function)
30
+
31
+ if (functionid in functionids) or (len(functionids) >= recursion_limit):
32
+ raise ValueError(
33
+ "Found wrapper loop while unwrapping {!r}!".format(original)
34
+ )
35
+
36
+ functionids[functionid] = function
37
+
38
+ return function
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("classicist")
@@ -0,0 +1,7 @@
1
+ from classicist.metaclasses.aliased import aliased
2
+ from classicist.metaclasses.shadowproof import shadowproof
3
+
4
+ __all__ = [
5
+ "aliased",
6
+ "shadowproof",
7
+ ]
@@ -0,0 +1,40 @@
1
+ from classicist.logging import logger
2
+
3
+ logger = logger.getChild(__name__)
4
+
5
+
6
+ class aliased(type):
7
+ """Metaclass that looks for methods that have been decorated with @alias(...) and
8
+ automatically creates the corresponding aliases for those methods on the class."""
9
+
10
+ def __new__(cls: object, name: str, bases: tuple[object], namespace: dict):
11
+ # Create the class first
12
+ cls = super().__new__(cls, name, bases, namespace)
13
+
14
+ # Walk through the class body (namespace) and install the aliases
15
+ for name, value in namespace.items():
16
+ original: object = value
17
+
18
+ # If a function has been wrapped by a well behaved decorator, unwrap it, to
19
+ # get to the original function, and thus to the alias annotation we need to
20
+ # create the function aliases in the class; without access to the annotation
21
+ # the aliases cannot be created, so any decorators used should follow best
22
+ # practice and apply the __wrapped__ attribute to point back to the wrapped
23
+ # function using functools.wraps or similar or use property getter practice:
24
+ while (w := hasattr(value, "__wrapped__")) or (p := hasattr(value, "fget")):
25
+ if w is True: # Get the original wrapped function
26
+ value = getattr(value, "__wrapped__")
27
+ elif p is True: # Get the original property function
28
+ value = getattr(value, "fget")
29
+
30
+ if aliases := getattr(value, "_classicist_aliases", None):
31
+ for alias in aliases:
32
+ if hasattr(cls, alias):
33
+ raise AttributeError(
34
+ f"Cannot create alias '{alias}' for method '{name}' as '{cls.__name__}.{alias}' already exists!"
35
+ )
36
+
37
+ # The alias points to the original function or property accessor
38
+ setattr(cls, alias, original)
39
+
40
+ return cls