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 +50 -118
- classicist/decorators/__init__.py +20 -0
- classicist/decorators/aliased/__init__.py +74 -0
- classicist/decorators/annotation/__init__.py +55 -0
- classicist/decorators/classproperty/__init__.py +41 -0
- classicist/decorators/deprecated/__init__.py +101 -0
- classicist/decorators/hybridmethod/__init__.py +57 -0
- classicist/decorators/nocache/__init__.py +10 -0
- classicist/exceptions/__init__.py +12 -0
- classicist/exceptions/decorators/__init__.py +7 -0
- classicist/exceptions/decorators/aliased/__init__.py +2 -0
- classicist/exceptions/decorators/annotation/__init__.py +2 -0
- classicist/exceptions/metaclasses/__init__.py +5 -0
- classicist/exceptions/metaclasses/shadowproof/__init__.py +2 -0
- classicist/inspector/__init__.py +38 -0
- classicist/logging/__init__.py +3 -0
- classicist/metaclasses/__init__.py +7 -0
- classicist/metaclasses/aliased/__init__.py +40 -0
- classicist/metaclasses/shadowproof/__init__.py +36 -0
- classicist/version.txt +1 -1
- classicist-1.0.1.dist-info/METADATA +501 -0
- classicist-1.0.1.dist-info/RECORD +26 -0
- classicist-1.0.0.dist-info/METADATA +0 -257
- classicist-1.0.0.dist-info/RECORD +0 -8
- {classicist-1.0.0.dist-info → classicist-1.0.1.dist-info}/WHEEL +0 -0
- {classicist-1.0.0.dist-info → classicist-1.0.1.dist-info}/licenses/LICENSE.md +0 -0
- {classicist-1.0.0.dist-info → classicist-1.0.1.dist-info}/top_level.txt +0 -0
- {classicist-1.0.0.dist-info → classicist-1.0.1.dist-info}/zip-safe +0 -0
classicist/__init__.py
CHANGED
|
@@ -1,118 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,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,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
|