aspyx 1.3.0__py3-none-any.whl → 1.4.0__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.
Potentially problematic release.
This version of aspyx might be problematic. Click here for more details.
- aspyx/__init__.py +0 -0
- aspyx/di/aop/aop.py +112 -87
- aspyx/di/di.py +125 -24
- aspyx/di/threading/synchronized.py +8 -5
- aspyx/exception/__init__.py +10 -0
- aspyx/exception/exception_manager.py +168 -0
- aspyx/reflection/reflection.py +37 -1
- aspyx/threading/__init__.py +10 -0
- aspyx/threading/thread_local.py +47 -0
- aspyx/{di/util → util}/stringbuilder.py +11 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.0.dist-info}/METADATA +180 -58
- aspyx-1.4.0.dist-info/RECORD +25 -0
- aspyx-1.3.0.dist-info/RECORD +0 -20
- /aspyx/{di/util → util}/__init__.py +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.0.dist-info}/WHEEL +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.0.dist-info}/top_level.txt +0 -0
aspyx/di/di.py
CHANGED
|
@@ -5,14 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import inspect
|
|
7
7
|
import logging
|
|
8
|
+
import importlib
|
|
9
|
+
import pkgutil
|
|
10
|
+
import sys
|
|
8
11
|
|
|
9
12
|
from abc import abstractmethod, ABC
|
|
10
13
|
from enum import Enum
|
|
11
14
|
import threading
|
|
12
|
-
from operator import truediv
|
|
13
15
|
from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable, TypedDict
|
|
14
16
|
|
|
15
|
-
from aspyx.
|
|
17
|
+
from aspyx.util import StringBuilder
|
|
16
18
|
from aspyx.reflection import Decorators, TypeDescriptor, DecoratorDescriptor
|
|
17
19
|
|
|
18
20
|
T = TypeVar("T")
|
|
@@ -44,9 +46,9 @@ class DIRegistrationException(DIException):
|
|
|
44
46
|
|
|
45
47
|
class ProviderCollisionException(DIRegistrationException):
|
|
46
48
|
def __init__(self, message: str, *providers: AbstractInstanceProvider):
|
|
47
|
-
|
|
49
|
+
super().__init__(message)
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
self.providers = providers
|
|
50
52
|
|
|
51
53
|
def __str__(self):
|
|
52
54
|
return f"[{self.args[0]} {self.providers[1].location()} collides with {self.providers[0].location()}"
|
|
@@ -159,6 +161,13 @@ class SingletonScopeInstanceProvider(InstanceProvider):
|
|
|
159
161
|
def create(self, environment: Environment, *args):
|
|
160
162
|
return SingletonScope()
|
|
161
163
|
|
|
164
|
+
class EnvironmentScopeInstanceProvider(InstanceProvider):
|
|
165
|
+
def __init__(self):
|
|
166
|
+
super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request") # TODO?
|
|
167
|
+
|
|
168
|
+
def create(self, environment: Environment, *args):
|
|
169
|
+
return EnvironmentScope()
|
|
170
|
+
|
|
162
171
|
class RequestScopeInstanceProvider(InstanceProvider):
|
|
163
172
|
def __init__(self):
|
|
164
173
|
super().__init__(RequestScopeInstanceProvider, RequestScope, False, "singleton")
|
|
@@ -166,7 +175,6 @@ class RequestScopeInstanceProvider(InstanceProvider):
|
|
|
166
175
|
def create(self, environment: Environment, *args):
|
|
167
176
|
return RequestScope()
|
|
168
177
|
|
|
169
|
-
|
|
170
178
|
class AmbiguousProvider(AbstractInstanceProvider):
|
|
171
179
|
"""
|
|
172
180
|
An AmbiguousProvider covers all cases, where fetching a class would lead to an ambiguity exception.
|
|
@@ -591,7 +599,7 @@ class Providers:
|
|
|
591
599
|
Providers.check.clear()
|
|
592
600
|
|
|
593
601
|
@classmethod
|
|
594
|
-
def filter(cls, environment: Environment) -> Dict[Type,AbstractInstanceProvider]:
|
|
602
|
+
def filter(cls, environment: Environment, provider_filter: Callable) -> Dict[Type,AbstractInstanceProvider]:
|
|
595
603
|
cache: Dict[Type,AbstractInstanceProvider] = {}
|
|
596
604
|
|
|
597
605
|
context: ConditionContext = {
|
|
@@ -610,12 +618,18 @@ class Providers:
|
|
|
610
618
|
if result is not None:
|
|
611
619
|
raise ProviderCollisionException(f"type {clazz.__name__} already registered", result, provider)
|
|
612
620
|
|
|
613
|
-
|
|
614
|
-
result = provider
|
|
621
|
+
result = provider
|
|
615
622
|
|
|
616
623
|
return result
|
|
617
624
|
|
|
618
625
|
def provider_applies(provider: AbstractInstanceProvider) -> bool:
|
|
626
|
+
# is it in the right module?
|
|
627
|
+
|
|
628
|
+
if not provider_filter(provider):
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
# check conditionals
|
|
632
|
+
|
|
619
633
|
descriptor = TypeDescriptor.for_type(provider.get_host())
|
|
620
634
|
if descriptor.has_decorator(conditional):
|
|
621
635
|
conditions: list[Condition] = [*descriptor.get_decorator(conditional).args]
|
|
@@ -658,7 +672,7 @@ class Providers:
|
|
|
658
672
|
|
|
659
673
|
# filter conditional providers and fill base classes as well
|
|
660
674
|
|
|
661
|
-
for provider_type,
|
|
675
|
+
for provider_type, _ in Providers.providers.items():
|
|
662
676
|
matching_provider = filter_type(provider_type)
|
|
663
677
|
if matching_provider is not None:
|
|
664
678
|
cache_provider_for_type(matching_provider, provider_type)
|
|
@@ -677,7 +691,11 @@ class Providers:
|
|
|
677
691
|
|
|
678
692
|
# and resolve
|
|
679
693
|
|
|
680
|
-
|
|
694
|
+
providers = result
|
|
695
|
+
if environment.parent is not None:
|
|
696
|
+
providers = providers | environment.parent.providers # add parent providers
|
|
697
|
+
|
|
698
|
+
provider_context = Providers.ResolveContext(providers)
|
|
681
699
|
for provider in mapped.values():
|
|
682
700
|
provider.resolve(provider_context)
|
|
683
701
|
provider_context.next() # clear dependencies
|
|
@@ -894,22 +912,35 @@ class Environment:
|
|
|
894
912
|
|
|
895
913
|
self.features = features
|
|
896
914
|
self.providers: Dict[Type, AbstractInstanceProvider] = {}
|
|
915
|
+
self.instances = []
|
|
897
916
|
self.lifecycle_processors: list[LifecycleProcessor] = []
|
|
898
917
|
|
|
899
918
|
if self.parent is not None:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
919
|
+
# inherit providers from parent
|
|
920
|
+
|
|
921
|
+
for provider_type, inherited_provider in self.parent.providers.items():
|
|
922
|
+
if inherited_provider.get_scope() == "environment":
|
|
923
|
+
# replace with own environment instance provider
|
|
924
|
+
self.providers[provider_type] = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
|
|
925
|
+
else:
|
|
926
|
+
self.providers[provider_type] = inherited_provider
|
|
927
|
+
|
|
928
|
+
# inherit processors as is unless they have an environment scope
|
|
929
|
+
|
|
930
|
+
for processor in self.parent.lifecycle_processors:
|
|
931
|
+
if self.providers[type(processor)].get_scope() != "environment":
|
|
932
|
+
self.lifecycle_processors.append(processor)
|
|
933
|
+
else:
|
|
934
|
+
# create and remember
|
|
935
|
+
self.lifecycle_processors.append(self.get(type(processor)))
|
|
936
|
+
else:
|
|
903
937
|
self.providers[SingletonScope] = SingletonScopeInstanceProvider()
|
|
904
938
|
self.providers[RequestScope] = RequestScopeInstanceProvider()
|
|
905
|
-
|
|
906
|
-
self.instances = []
|
|
939
|
+
self.providers[EnvironmentScope] = EnvironmentScopeInstanceProvider()
|
|
907
940
|
|
|
908
941
|
Environment.instance = self
|
|
909
942
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
overall_providers = Providers.filter(self)
|
|
943
|
+
prefix_list : list[str] = []
|
|
913
944
|
|
|
914
945
|
loaded = set()
|
|
915
946
|
|
|
@@ -918,6 +949,36 @@ class Environment:
|
|
|
918
949
|
|
|
919
950
|
self.providers[type] = provider
|
|
920
951
|
|
|
952
|
+
def get_type_package(type: Type):
|
|
953
|
+
module_name = type.__module__
|
|
954
|
+
module = sys.modules[module_name]
|
|
955
|
+
|
|
956
|
+
return module.__package__
|
|
957
|
+
|
|
958
|
+
def import_package(name: str):
|
|
959
|
+
"""Import a package and all its submodules recursively."""
|
|
960
|
+
package = importlib.import_module(name)
|
|
961
|
+
results = {name: package}
|
|
962
|
+
|
|
963
|
+
if hasattr(package, '__path__'): # it's a package, not a single file
|
|
964
|
+
for finder, name, ispkg in pkgutil.walk_packages(package.__path__, prefix=package.__name__ + "."):
|
|
965
|
+
try:
|
|
966
|
+
loaded = sys.modules
|
|
967
|
+
|
|
968
|
+
if loaded.get(name, None) is None:
|
|
969
|
+
Environment.logger.debug("import module %s", name)
|
|
970
|
+
|
|
971
|
+
submodule = importlib.import_module(name)
|
|
972
|
+
results[name] = submodule
|
|
973
|
+
else:
|
|
974
|
+
# skip import
|
|
975
|
+
results[name] = loaded[name]
|
|
976
|
+
|
|
977
|
+
except Exception as e:
|
|
978
|
+
Environment.logger.info("failed to import module %s due to %s", name, str(e))
|
|
979
|
+
|
|
980
|
+
return results
|
|
981
|
+
|
|
921
982
|
def load_environment(env: Type):
|
|
922
983
|
if env not in loaded:
|
|
923
984
|
Environment.logger.debug("load environment %s", env.__qualname__)
|
|
@@ -930,24 +991,43 @@ class Environment:
|
|
|
930
991
|
if decorator is None:
|
|
931
992
|
raise DIRegistrationException(f"{env.__name__} is not an environment class")
|
|
932
993
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
994
|
+
# package
|
|
995
|
+
|
|
996
|
+
package_name = get_type_package(env)
|
|
936
997
|
|
|
937
998
|
# recursion
|
|
938
999
|
|
|
939
1000
|
for import_environment in decorator.args[0] or []:
|
|
940
1001
|
load_environment(import_environment)
|
|
941
1002
|
|
|
1003
|
+
# import package
|
|
1004
|
+
|
|
1005
|
+
if package_name is not None and len(package_name) > 0: # files outside of a package return None pr ""
|
|
1006
|
+
import_package(package_name)
|
|
1007
|
+
|
|
942
1008
|
# filter and load providers according to their module
|
|
943
1009
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1010
|
+
module_prefix = package_name
|
|
1011
|
+
if len(module_prefix) == 0:
|
|
1012
|
+
module_prefix = env.__module__
|
|
1013
|
+
|
|
1014
|
+
prefix_list.append(module_prefix)
|
|
1015
|
+
|
|
947
1016
|
# go
|
|
948
1017
|
|
|
949
1018
|
load_environment(env)
|
|
950
1019
|
|
|
1020
|
+
# filter according to the prefix list
|
|
1021
|
+
|
|
1022
|
+
def filter_provider(provider: AbstractInstanceProvider) -> bool:
|
|
1023
|
+
for prefix in prefix_list:
|
|
1024
|
+
if provider.get_host().__module__.startswith(prefix):
|
|
1025
|
+
return True
|
|
1026
|
+
|
|
1027
|
+
return False
|
|
1028
|
+
|
|
1029
|
+
self.providers.update(Providers.filter(self, filter_provider))
|
|
1030
|
+
|
|
951
1031
|
# construct eager objects for local providers
|
|
952
1032
|
|
|
953
1033
|
for provider in set(self.providers.values()):
|
|
@@ -959,6 +1039,14 @@ class Environment:
|
|
|
959
1039
|
for instance in self.instances:
|
|
960
1040
|
self.execute_processors(Lifecycle.ON_RUNNING, instance)
|
|
961
1041
|
|
|
1042
|
+
def is_registered_type(self, type: Type) -> bool:
|
|
1043
|
+
provider = self.providers.get(type, None)
|
|
1044
|
+
return provider is not None and not isinstance(provider, AmbiguousProvider)
|
|
1045
|
+
|
|
1046
|
+
def registered_types(self, predicate: Callable[[Type], bool]) -> list[Type]:
|
|
1047
|
+
return [provider.get_type() for provider in self.providers.values()
|
|
1048
|
+
if predicate(provider.get_type())]
|
|
1049
|
+
|
|
962
1050
|
# internal
|
|
963
1051
|
|
|
964
1052
|
def has_feature(self, feature: str) -> bool:
|
|
@@ -1297,6 +1385,19 @@ class SingletonScope(Scope):
|
|
|
1297
1385
|
|
|
1298
1386
|
return self.value
|
|
1299
1387
|
|
|
1388
|
+
@scope("environment", register=False)
|
|
1389
|
+
class EnvironmentScope(SingletonScope):
|
|
1390
|
+
# properties
|
|
1391
|
+
|
|
1392
|
+
__slots__ = [
|
|
1393
|
+
]
|
|
1394
|
+
|
|
1395
|
+
# constructor
|
|
1396
|
+
|
|
1397
|
+
def __init__(self):
|
|
1398
|
+
super().__init__()
|
|
1399
|
+
|
|
1400
|
+
|
|
1300
1401
|
@scope("thread")
|
|
1301
1402
|
class ThreadScope(Scope):
|
|
1302
1403
|
__slots__ = [
|
|
@@ -20,9 +20,7 @@ def synchronized():
|
|
|
20
20
|
return decorator
|
|
21
21
|
|
|
22
22
|
@advice
|
|
23
|
-
class SynchronizeAdvice
|
|
24
|
-
__slots__ = ("locks")
|
|
25
|
-
|
|
23
|
+
class SynchronizeAdvice:
|
|
26
24
|
# constructor
|
|
27
25
|
|
|
28
26
|
def __init__(self):
|
|
@@ -41,6 +39,11 @@ class SynchronizeAdvice():
|
|
|
41
39
|
# around
|
|
42
40
|
|
|
43
41
|
@around(methods().decorated_with(synchronized))
|
|
44
|
-
def
|
|
42
|
+
def synchronize_sync(self, invocation: Invocation):
|
|
43
|
+
with self.get_lock(invocation.args[0]):
|
|
44
|
+
return invocation.proceed()
|
|
45
|
+
|
|
46
|
+
@around(methods().decorated_with(synchronized).that_are_async())
|
|
47
|
+
async def synchronize_async(self, invocation: Invocation):
|
|
45
48
|
with self.get_lock(invocation.args[0]):
|
|
46
|
-
return invocation.
|
|
49
|
+
return await invocation.proceed_async()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception handling code
|
|
3
|
+
"""
|
|
4
|
+
from threading import RLock
|
|
5
|
+
from typing import Any, Callable, Dict, Optional, Type
|
|
6
|
+
|
|
7
|
+
from aspyx.di import injectable, Environment, inject_environment, on_running
|
|
8
|
+
from aspyx.reflection import Decorators, TypeDescriptor
|
|
9
|
+
from aspyx.threading import ThreadLocal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def exception_handler():
|
|
13
|
+
"""
|
|
14
|
+
This annotation is used to mark classes that container handlers for exceptions
|
|
15
|
+
"""
|
|
16
|
+
def decorator(cls):
|
|
17
|
+
Decorators.add(cls, exception_handler)
|
|
18
|
+
|
|
19
|
+
ExceptionManager.exception_handler_classes.append(cls)
|
|
20
|
+
|
|
21
|
+
return cls
|
|
22
|
+
|
|
23
|
+
return decorator
|
|
24
|
+
|
|
25
|
+
def handle():
|
|
26
|
+
"""
|
|
27
|
+
Any method annotated with @handle will be registered as an exception handler method.
|
|
28
|
+
"""
|
|
29
|
+
def decorator(func):
|
|
30
|
+
Decorators.add(func, handle)
|
|
31
|
+
return func
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
class ErrorContext():
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
class Handler:
|
|
39
|
+
# constructor
|
|
40
|
+
|
|
41
|
+
def __init__(self, type_: Type, instance: Any, handler: Callable):
|
|
42
|
+
self.type = type_
|
|
43
|
+
self.instance = instance
|
|
44
|
+
self.handler = handler
|
|
45
|
+
|
|
46
|
+
def handle(self, exception: BaseException):
|
|
47
|
+
self.handler(self.instance, exception)
|
|
48
|
+
|
|
49
|
+
class Chain:
|
|
50
|
+
# constructor
|
|
51
|
+
|
|
52
|
+
def __init__(self, handler: Handler, next: Optional[Handler] = None):
|
|
53
|
+
self.handler = handler
|
|
54
|
+
self.next = next
|
|
55
|
+
|
|
56
|
+
# public
|
|
57
|
+
|
|
58
|
+
def handle(self, exception: BaseException):
|
|
59
|
+
self.handler.handle(exception)
|
|
60
|
+
|
|
61
|
+
class Invocation:
|
|
62
|
+
def __init__(self, exception: Exception, chain: Chain):
|
|
63
|
+
self.exception = exception
|
|
64
|
+
self.chain = chain
|
|
65
|
+
self.current = self.chain
|
|
66
|
+
|
|
67
|
+
@injectable()
|
|
68
|
+
class ExceptionManager:
|
|
69
|
+
"""
|
|
70
|
+
An exception manager collects all registered handlers and is able to handle an exception
|
|
71
|
+
by dispatching it to the most applicable handler ( according to mro )
|
|
72
|
+
"""
|
|
73
|
+
# static data
|
|
74
|
+
|
|
75
|
+
exception_handler_classes = []
|
|
76
|
+
|
|
77
|
+
invocation = ThreadLocal()
|
|
78
|
+
|
|
79
|
+
# class methods
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def proceed(cls):
|
|
83
|
+
invocation = cls.invocation.get()
|
|
84
|
+
|
|
85
|
+
invocation.current = invocation.current.next
|
|
86
|
+
if invocation.current is not None:
|
|
87
|
+
invocation.current.handle(invocation.exception)
|
|
88
|
+
|
|
89
|
+
# constructor
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.environment : Optional[Environment] = None
|
|
93
|
+
self.handler : list[Handler] = []
|
|
94
|
+
self.cache: Dict[Type, Chain] = {}
|
|
95
|
+
self.lock = RLock()
|
|
96
|
+
self.current_context: Optional[ErrorContext] = None
|
|
97
|
+
|
|
98
|
+
# internal
|
|
99
|
+
|
|
100
|
+
@inject_environment()
|
|
101
|
+
def set_environment(self, environment: Environment):
|
|
102
|
+
self.environment = environment
|
|
103
|
+
|
|
104
|
+
@on_running()
|
|
105
|
+
def setup(self):
|
|
106
|
+
for handler_class in self.exception_handler_classes:
|
|
107
|
+
type_descriptor = TypeDescriptor.for_type(handler_class)
|
|
108
|
+
instance = self.environment.get(handler_class)
|
|
109
|
+
|
|
110
|
+
# analyze methods
|
|
111
|
+
|
|
112
|
+
for method in type_descriptor.get_methods():
|
|
113
|
+
if method.has_decorator(handle):
|
|
114
|
+
if len(method.param_types) == 1:
|
|
115
|
+
exception_type = method.param_types[0]
|
|
116
|
+
|
|
117
|
+
self.handler.append(Handler(
|
|
118
|
+
exception_type,
|
|
119
|
+
instance,
|
|
120
|
+
method.method,
|
|
121
|
+
))
|
|
122
|
+
else:
|
|
123
|
+
print(f"handler {method.method} expected to have one parameter")
|
|
124
|
+
|
|
125
|
+
def get_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
126
|
+
chain = self.cache.get(clazz, None)
|
|
127
|
+
if chain is None:
|
|
128
|
+
with self.lock:
|
|
129
|
+
chain = self.cache.get(clazz, None)
|
|
130
|
+
if chain is None:
|
|
131
|
+
chain = self.compute_handlers(clazz)
|
|
132
|
+
self.cache[clazz] = chain
|
|
133
|
+
|
|
134
|
+
return chain
|
|
135
|
+
|
|
136
|
+
def compute_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
137
|
+
mro = clazz.mro()
|
|
138
|
+
|
|
139
|
+
chain = []
|
|
140
|
+
|
|
141
|
+
for type in mro:
|
|
142
|
+
handler = next((handler for handler in self.handler if handler.type is type), None)
|
|
143
|
+
if handler:
|
|
144
|
+
chain.append(Chain(handler))
|
|
145
|
+
|
|
146
|
+
# chain
|
|
147
|
+
|
|
148
|
+
for i in range(0, len(chain) - 2):
|
|
149
|
+
chain[i].next = chain[i + 1]
|
|
150
|
+
|
|
151
|
+
if len(chain) > 0:
|
|
152
|
+
return chain[0]
|
|
153
|
+
else:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def handle(self, exception: Exception):
|
|
157
|
+
"""
|
|
158
|
+
handle an exception by invoking the most applicable handler ( according to mro )
|
|
159
|
+
:param exception: the exception
|
|
160
|
+
"""
|
|
161
|
+
chain = self.get_handlers(type(exception))
|
|
162
|
+
if chain is not None:
|
|
163
|
+
|
|
164
|
+
self.invocation.set(Invocation(exception, chain))
|
|
165
|
+
try:
|
|
166
|
+
chain.handle(exception)
|
|
167
|
+
finally:
|
|
168
|
+
self.invocation.clear()
|
aspyx/reflection/reflection.py
CHANGED
|
@@ -12,6 +12,9 @@ from weakref import WeakKeyDictionary
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DecoratorDescriptor:
|
|
15
|
+
"""
|
|
16
|
+
A DecoratorDescriptor covers the decorator - a callable - and the passed arguments
|
|
17
|
+
"""
|
|
15
18
|
__slots__ = [
|
|
16
19
|
"decorator",
|
|
17
20
|
"args"
|
|
@@ -29,13 +32,17 @@ class Decorators:
|
|
|
29
32
|
Utility class that caches decorators ( Python does not have a feature for this )
|
|
30
33
|
"""
|
|
31
34
|
@classmethod
|
|
32
|
-
def add(cls, func, decorator, *args):
|
|
35
|
+
def add(cls, func, decorator: Callable, *args):
|
|
33
36
|
decorators = getattr(func, '__decorators__', None)
|
|
34
37
|
if decorators is None:
|
|
35
38
|
setattr(func, '__decorators__', [DecoratorDescriptor(decorator, *args)])
|
|
36
39
|
else:
|
|
37
40
|
decorators.append(DecoratorDescriptor(decorator, *args))
|
|
38
41
|
|
|
42
|
+
@classmethod
|
|
43
|
+
def has_decorator(cls, func, callable: Callable) -> bool:
|
|
44
|
+
return any(decorator.decorator is callable for decorator in Decorators.get(func))
|
|
45
|
+
|
|
39
46
|
@classmethod
|
|
40
47
|
def get(cls, func) -> list[DecoratorDescriptor]:
|
|
41
48
|
return getattr(func, '__decorators__', [])
|
|
@@ -69,10 +76,34 @@ class TypeDescriptor:
|
|
|
69
76
|
|
|
70
77
|
# public
|
|
71
78
|
|
|
79
|
+
def get_name(self) -> str:
|
|
80
|
+
"""
|
|
81
|
+
return the method name
|
|
82
|
+
:return: the method name
|
|
83
|
+
"""
|
|
84
|
+
return self.method.__name__
|
|
85
|
+
|
|
86
|
+
def get_doc(self, default = "") -> str:
|
|
87
|
+
"""
|
|
88
|
+
return the method docstring
|
|
89
|
+
:param default: the default if no docstring is found
|
|
90
|
+
:return: the docstring
|
|
91
|
+
"""
|
|
92
|
+
return self.method.__doc__ or default
|
|
93
|
+
|
|
72
94
|
def is_async(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
return true if the method is asynchronous
|
|
97
|
+
:return: async flag
|
|
98
|
+
"""
|
|
73
99
|
return inspect.iscoroutinefunction(self.method)
|
|
74
100
|
|
|
75
101
|
def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
|
|
102
|
+
"""
|
|
103
|
+
return the DecoratorDescriptor - if any - associated with the passed Callable
|
|
104
|
+
:param decorator:
|
|
105
|
+
:return: the DecoratorDescriptor or None
|
|
106
|
+
"""
|
|
76
107
|
for dec in self.decorators:
|
|
77
108
|
if dec.decorator is decorator:
|
|
78
109
|
return dec
|
|
@@ -80,6 +111,11 @@ class TypeDescriptor:
|
|
|
80
111
|
return None
|
|
81
112
|
|
|
82
113
|
def has_decorator(self, decorator: Callable) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
return True if the method is decorated with the decorator
|
|
116
|
+
:param decorator: the decorator callable
|
|
117
|
+
:return: True if the method is decorated with the decorator
|
|
118
|
+
"""
|
|
83
119
|
for dec in self.decorators:
|
|
84
120
|
if dec.decorator is decorator:
|
|
85
121
|
return True
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Some threading related utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional, TypeVar, Generic
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
class ThreadLocal(Generic[T]):
|
|
11
|
+
"""
|
|
12
|
+
A thread local value holder
|
|
13
|
+
"""
|
|
14
|
+
# constructor
|
|
15
|
+
|
|
16
|
+
def __init__(self, default_factory: Optional[Callable[[], T]] = None):
|
|
17
|
+
self.local = threading.local()
|
|
18
|
+
self.factory = default_factory
|
|
19
|
+
|
|
20
|
+
# public
|
|
21
|
+
|
|
22
|
+
def get(self) -> Optional[T]:
|
|
23
|
+
"""
|
|
24
|
+
return the current value or invoke the optional factory to compute one
|
|
25
|
+
:return: the value associated with the current thread
|
|
26
|
+
"""
|
|
27
|
+
if not hasattr(self.local, "value"):
|
|
28
|
+
if self.factory is not None:
|
|
29
|
+
self.local.value = self.factory()
|
|
30
|
+
else:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return self.local.value
|
|
34
|
+
|
|
35
|
+
def set(self, value: T) -> None:
|
|
36
|
+
"""
|
|
37
|
+
set a value in the current thread
|
|
38
|
+
:param value: the value
|
|
39
|
+
"""
|
|
40
|
+
self.local.value = value
|
|
41
|
+
|
|
42
|
+
def clear(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
clear the value in the current thread
|
|
45
|
+
"""
|
|
46
|
+
if hasattr(self.local, "value"):
|
|
47
|
+
del self.local.value
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Utility class for Java lovers
|
|
3
3
|
"""
|
|
4
4
|
class StringBuilder:
|
|
5
|
+
"""
|
|
6
|
+
A StringBuilder is used to build a string by multiple append calls.
|
|
7
|
+
"""
|
|
5
8
|
___slots__ = ("_parts",)
|
|
6
9
|
|
|
7
10
|
# constructor
|
|
@@ -12,6 +15,11 @@ class StringBuilder:
|
|
|
12
15
|
# public
|
|
13
16
|
|
|
14
17
|
def append(self, s: str) -> "StringBuilder":
|
|
18
|
+
"""
|
|
19
|
+
append a string to the end of the string builder
|
|
20
|
+
:param s: the string
|
|
21
|
+
:return: self
|
|
22
|
+
"""
|
|
15
23
|
self._parts.append(str(s))
|
|
16
24
|
|
|
17
25
|
return self
|
|
@@ -23,6 +31,9 @@ class StringBuilder:
|
|
|
23
31
|
return self
|
|
24
32
|
|
|
25
33
|
def clear(self):
|
|
34
|
+
"""
|
|
35
|
+
clear the content
|
|
36
|
+
"""
|
|
26
37
|
self._parts.clear()
|
|
27
38
|
|
|
28
39
|
# object
|