guidellm 0.4.0a18__py3-none-any.whl → 0.4.0a155__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 guidellm might be problematic. Click here for more details.
- guidellm/__init__.py +5 -2
- guidellm/__main__.py +451 -252
- guidellm/backends/__init__.py +33 -0
- guidellm/backends/backend.py +110 -0
- guidellm/backends/openai.py +355 -0
- guidellm/backends/response_handlers.py +455 -0
- guidellm/benchmark/__init__.py +53 -39
- guidellm/benchmark/benchmarker.py +148 -317
- guidellm/benchmark/entrypoints.py +466 -128
- guidellm/benchmark/output.py +517 -771
- guidellm/benchmark/profile.py +580 -280
- guidellm/benchmark/progress.py +568 -549
- guidellm/benchmark/scenarios/__init__.py +40 -0
- guidellm/benchmark/scenarios/chat.json +6 -0
- guidellm/benchmark/scenarios/rag.json +6 -0
- guidellm/benchmark/schemas.py +2085 -0
- guidellm/data/__init__.py +28 -4
- guidellm/data/collators.py +16 -0
- guidellm/data/deserializers/__init__.py +53 -0
- guidellm/data/deserializers/deserializer.py +109 -0
- guidellm/data/deserializers/file.py +222 -0
- guidellm/data/deserializers/huggingface.py +94 -0
- guidellm/data/deserializers/memory.py +192 -0
- guidellm/data/deserializers/synthetic.py +346 -0
- guidellm/data/loaders.py +145 -0
- guidellm/data/preprocessors/__init__.py +25 -0
- guidellm/data/preprocessors/formatters.py +412 -0
- guidellm/data/preprocessors/mappers.py +198 -0
- guidellm/data/preprocessors/preprocessor.py +29 -0
- guidellm/data/processor.py +30 -0
- guidellm/data/schemas.py +13 -0
- guidellm/data/utils/__init__.py +10 -0
- guidellm/data/utils/dataset.py +94 -0
- guidellm/data/utils/functions.py +18 -0
- guidellm/extras/__init__.py +4 -0
- guidellm/extras/audio.py +215 -0
- guidellm/extras/vision.py +242 -0
- guidellm/logger.py +2 -2
- guidellm/mock_server/__init__.py +8 -0
- guidellm/mock_server/config.py +84 -0
- guidellm/mock_server/handlers/__init__.py +17 -0
- guidellm/mock_server/handlers/chat_completions.py +280 -0
- guidellm/mock_server/handlers/completions.py +280 -0
- guidellm/mock_server/handlers/tokenizer.py +142 -0
- guidellm/mock_server/models.py +510 -0
- guidellm/mock_server/server.py +168 -0
- guidellm/mock_server/utils.py +302 -0
- guidellm/preprocess/dataset.py +23 -26
- guidellm/presentation/builder.py +2 -2
- guidellm/presentation/data_models.py +25 -21
- guidellm/presentation/injector.py +2 -3
- guidellm/scheduler/__init__.py +65 -26
- guidellm/scheduler/constraints.py +1035 -0
- guidellm/scheduler/environments.py +252 -0
- guidellm/scheduler/scheduler.py +140 -368
- guidellm/scheduler/schemas.py +272 -0
- guidellm/scheduler/strategies.py +519 -0
- guidellm/scheduler/worker.py +391 -420
- guidellm/scheduler/worker_group.py +707 -0
- guidellm/schemas/__init__.py +31 -0
- guidellm/schemas/info.py +159 -0
- guidellm/schemas/request.py +216 -0
- guidellm/schemas/response.py +119 -0
- guidellm/schemas/stats.py +228 -0
- guidellm/{config.py → settings.py} +32 -21
- guidellm/utils/__init__.py +95 -8
- guidellm/utils/auto_importer.py +98 -0
- guidellm/utils/cli.py +46 -2
- guidellm/utils/console.py +183 -0
- guidellm/utils/encoding.py +778 -0
- guidellm/utils/functions.py +134 -0
- guidellm/utils/hf_datasets.py +1 -2
- guidellm/utils/hf_transformers.py +4 -4
- guidellm/utils/imports.py +9 -0
- guidellm/utils/messaging.py +1118 -0
- guidellm/utils/mixins.py +115 -0
- guidellm/utils/pydantic_utils.py +411 -0
- guidellm/utils/random.py +3 -4
- guidellm/utils/registry.py +220 -0
- guidellm/utils/singleton.py +133 -0
- guidellm/{objects → utils}/statistics.py +341 -247
- guidellm/utils/synchronous.py +159 -0
- guidellm/utils/text.py +163 -50
- guidellm/utils/typing.py +41 -0
- guidellm/version.py +1 -1
- {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/METADATA +33 -10
- guidellm-0.4.0a155.dist-info/RECORD +96 -0
- guidellm/backend/__init__.py +0 -23
- guidellm/backend/backend.py +0 -259
- guidellm/backend/openai.py +0 -705
- guidellm/backend/response.py +0 -136
- guidellm/benchmark/aggregator.py +0 -760
- guidellm/benchmark/benchmark.py +0 -837
- guidellm/benchmark/scenario.py +0 -104
- guidellm/data/prideandprejudice.txt.gz +0 -0
- guidellm/dataset/__init__.py +0 -22
- guidellm/dataset/creator.py +0 -213
- guidellm/dataset/entrypoints.py +0 -42
- guidellm/dataset/file.py +0 -92
- guidellm/dataset/hf_datasets.py +0 -62
- guidellm/dataset/in_memory.py +0 -132
- guidellm/dataset/synthetic.py +0 -287
- guidellm/objects/__init__.py +0 -18
- guidellm/objects/pydantic.py +0 -89
- guidellm/request/__init__.py +0 -18
- guidellm/request/loader.py +0 -284
- guidellm/request/request.py +0 -79
- guidellm/request/types.py +0 -10
- guidellm/scheduler/queues.py +0 -25
- guidellm/scheduler/result.py +0 -155
- guidellm/scheduler/strategy.py +0 -495
- guidellm-0.4.0a18.dist-info/RECORD +0 -62
- {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/WHEEL +0 -0
- {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/entry_points.txt +0 -0
- {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/licenses/LICENSE +0 -0
- {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry system for dynamic object registration and discovery.
|
|
3
|
+
|
|
4
|
+
Provides a flexible object registration system with optional auto-discovery
|
|
5
|
+
capabilities through decorators and module imports. Enables dynamic discovery
|
|
6
|
+
and instantiation of implementations based on configuration parameters, supporting
|
|
7
|
+
both manual registration and automatic package-based discovery for extensible
|
|
8
|
+
plugin architectures.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import ClassVar, Generic, TypeVar, cast
|
|
15
|
+
|
|
16
|
+
from guidellm.utils.auto_importer import AutoImporterMixin
|
|
17
|
+
|
|
18
|
+
__all__ = ["RegisterT", "RegistryMixin", "RegistryObjT"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
RegistryObjT = TypeVar("RegistryObjT")
|
|
22
|
+
"""Generic type variable for objects managed by the registry system."""
|
|
23
|
+
RegisterT = TypeVar(
|
|
24
|
+
"RegisterT", bound=type
|
|
25
|
+
) # Must be bound to type to ensure __name__ is available.
|
|
26
|
+
"""Generic type variable for the args and return values within the registry."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RegistryMixin(Generic[RegistryObjT], AutoImporterMixin):
|
|
30
|
+
"""
|
|
31
|
+
Generic mixin for creating object registries with optional auto-discovery.
|
|
32
|
+
|
|
33
|
+
Enables classes to maintain separate registries of objects that can be dynamically
|
|
34
|
+
discovered and instantiated through decorators and module imports. Supports both
|
|
35
|
+
manual registration via decorators and automatic discovery through package scanning
|
|
36
|
+
for extensible plugin architectures.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
::
|
|
40
|
+
class BaseAlgorithm(RegistryMixin):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@BaseAlgorithm.register()
|
|
44
|
+
class ConcreteAlgorithm(BaseAlgorithm):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@BaseAlgorithm.register("custom_name")
|
|
48
|
+
class AnotherAlgorithm(BaseAlgorithm):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Get all registered implementations
|
|
52
|
+
algorithms = BaseAlgorithm.registered_objects()
|
|
53
|
+
|
|
54
|
+
Example with auto-discovery:
|
|
55
|
+
::
|
|
56
|
+
class TokenProposal(RegistryMixin):
|
|
57
|
+
registry_auto_discovery = True
|
|
58
|
+
auto_package = "mypackage.proposals"
|
|
59
|
+
|
|
60
|
+
# Automatically imports and registers decorated objects
|
|
61
|
+
proposals = TokenProposal.registered_objects()
|
|
62
|
+
|
|
63
|
+
:cvar registry: Dictionary mapping names to registered objects
|
|
64
|
+
:cvar registry_auto_discovery: Enable automatic package-based discovery
|
|
65
|
+
:cvar registry_populated: Track whether auto-discovery has completed
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
registry: ClassVar[dict[str, RegistryObjT] | None] = None # type: ignore[misc]
|
|
69
|
+
registry_auto_discovery: ClassVar[bool] = False
|
|
70
|
+
registry_populated: ClassVar[bool] = False
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def register(
|
|
74
|
+
cls, name: str | list[str] | None = None
|
|
75
|
+
) -> Callable[[RegisterT], RegisterT]:
|
|
76
|
+
"""
|
|
77
|
+
Decorator for registering objects with the registry.
|
|
78
|
+
|
|
79
|
+
:param name: Optional name(s) to register the object under.
|
|
80
|
+
If None, uses the object's __name__ attribute
|
|
81
|
+
:return: Decorator function that registers the decorated object
|
|
82
|
+
:raises ValueError: If name is not a string, list of strings, or None
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def _decorator(obj: RegisterT) -> RegisterT:
|
|
86
|
+
cls.register_decorator(obj, name=name)
|
|
87
|
+
return obj
|
|
88
|
+
|
|
89
|
+
return _decorator
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def register_decorator(
|
|
93
|
+
cls, obj: RegisterT, name: str | list[str] | None = None
|
|
94
|
+
) -> RegisterT:
|
|
95
|
+
"""
|
|
96
|
+
Register an object directly with the registry.
|
|
97
|
+
|
|
98
|
+
:param obj: The object to register
|
|
99
|
+
:param name: Optional name(s) to register the object under.
|
|
100
|
+
If None, uses the object's __name__ attribute
|
|
101
|
+
:return: The registered object
|
|
102
|
+
:raises ValueError: If the object is already registered or name is invalid
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
if name is None:
|
|
106
|
+
name = obj.__name__
|
|
107
|
+
elif not isinstance(name, str | list):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"RegistryMixin.register_decorator name must be a string or "
|
|
110
|
+
f"an iterable of strings. Got {name}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if cls.registry is None:
|
|
114
|
+
cls.registry = {}
|
|
115
|
+
|
|
116
|
+
names = [name] if isinstance(name, str) else list(name)
|
|
117
|
+
|
|
118
|
+
for register_name in names:
|
|
119
|
+
if not isinstance(register_name, str):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"RegistryMixin.register_decorator name must be a string or "
|
|
122
|
+
f"a list of strings. Got {register_name}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if register_name in cls.registry:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"RegistryMixin.register_decorator cannot register an object "
|
|
128
|
+
f"{obj} with the name {register_name} because it is already "
|
|
129
|
+
"registered."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
cls.registry[register_name] = cast("RegistryObjT", obj)
|
|
133
|
+
|
|
134
|
+
return obj
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def auto_populate_registry(cls) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Import and register all modules from the auto_package.
|
|
140
|
+
|
|
141
|
+
Automatically called by registered_objects when registry_auto_discovery is True
|
|
142
|
+
to ensure all available implementations are discovered.
|
|
143
|
+
|
|
144
|
+
:return: True if registry was populated, False if already populated
|
|
145
|
+
:raises ValueError: If called when registry_auto_discovery is False
|
|
146
|
+
"""
|
|
147
|
+
if not cls.registry_auto_discovery:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"RegistryMixin.auto_populate_registry() cannot be called "
|
|
150
|
+
"because registry_auto_discovery is set to False. "
|
|
151
|
+
"Set registry_auto_discovery to True to enable auto-discovery."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if cls.registry_populated:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
cls.auto_import_package_modules()
|
|
158
|
+
cls.registry_populated = True
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def registered_objects(cls) -> tuple[RegistryObjT, ...]:
|
|
164
|
+
"""
|
|
165
|
+
Get all registered objects from the registry.
|
|
166
|
+
|
|
167
|
+
Automatically triggers auto-discovery if registry_auto_discovery is enabled
|
|
168
|
+
to ensure all available implementations are included.
|
|
169
|
+
|
|
170
|
+
:return: Tuple of all registered objects including auto-discovered ones
|
|
171
|
+
:raises ValueError: If called before any objects have been registered
|
|
172
|
+
"""
|
|
173
|
+
if cls.registry_auto_discovery:
|
|
174
|
+
cls.auto_populate_registry()
|
|
175
|
+
|
|
176
|
+
if cls.registry is None:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"RegistryMixin.registered_objects() must be called after "
|
|
179
|
+
"registering objects with RegistryMixin.register()."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return tuple(cls.registry.values())
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def is_registered(cls, name: str) -> bool:
|
|
186
|
+
"""
|
|
187
|
+
Check if an object is registered under the given name.
|
|
188
|
+
It matches first by exact name, then by str.lower().
|
|
189
|
+
|
|
190
|
+
:param name: The name to check for registration.
|
|
191
|
+
:return: True if the object is registered, False otherwise.
|
|
192
|
+
"""
|
|
193
|
+
if cls.registry is None:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
return name in cls.registry or name.lower() in [
|
|
197
|
+
key.lower() for key in cls.registry
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_registered_object(cls, name: str) -> RegistryObjT | None:
|
|
202
|
+
"""
|
|
203
|
+
Get a registered object by its name. It matches first by exact name,
|
|
204
|
+
then by str.lower().
|
|
205
|
+
|
|
206
|
+
:param name: The name of the registered object.
|
|
207
|
+
:return: The registered object if found, None otherwise.
|
|
208
|
+
"""
|
|
209
|
+
if cls.registry is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if name in cls.registry:
|
|
213
|
+
return cls.registry[name]
|
|
214
|
+
|
|
215
|
+
name_casefold = name.lower()
|
|
216
|
+
for k, v in cls.registry.items():
|
|
217
|
+
if name_casefold == k.lower():
|
|
218
|
+
return v
|
|
219
|
+
|
|
220
|
+
return None # Not found
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Singleton pattern implementations for ensuring single instance classes.
|
|
3
|
+
|
|
4
|
+
Provides singleton mixins for creating classes that maintain a single instance
|
|
5
|
+
throughout the application lifecycle, with support for both basic and thread-safe
|
|
6
|
+
implementations. These mixins integrate with the scheduler and other system components
|
|
7
|
+
to ensure consistent state management and prevent duplicate resource allocation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
__all__ = ["SingletonMixin", "ThreadSafeSingletonMixin"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SingletonMixin:
|
|
18
|
+
"""
|
|
19
|
+
Basic singleton mixin ensuring single instance per class.
|
|
20
|
+
|
|
21
|
+
Implements the singleton pattern using class variables to control instance
|
|
22
|
+
creation. Subclasses must call super().__init__() for proper initialization
|
|
23
|
+
state management. Suitable for single-threaded environments or when external
|
|
24
|
+
synchronization is provided.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
::
|
|
28
|
+
class ConfigManager(SingletonMixin):
|
|
29
|
+
def __init__(self, config_path: str):
|
|
30
|
+
super().__init__()
|
|
31
|
+
if not self.initialized:
|
|
32
|
+
self.config = load_config(config_path)
|
|
33
|
+
|
|
34
|
+
manager1 = ConfigManager("config.json")
|
|
35
|
+
manager2 = ConfigManager("config.json")
|
|
36
|
+
assert manager1 is manager2
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_singleton_initialized: bool
|
|
40
|
+
_init_lock: threading.Lock
|
|
41
|
+
|
|
42
|
+
def __new__(cls, *args, **kwargs): # noqa: ARG004
|
|
43
|
+
"""
|
|
44
|
+
Create or return the singleton instance.
|
|
45
|
+
|
|
46
|
+
:param args: Positional arguments passed to the constructor
|
|
47
|
+
:param kwargs: Keyword arguments passed to the constructor
|
|
48
|
+
:return: The singleton instance of the class
|
|
49
|
+
"""
|
|
50
|
+
# Use class-specific attribute name to avoid inheritance issues
|
|
51
|
+
attr_name = f"_singleton_instance_{cls.__name__}"
|
|
52
|
+
|
|
53
|
+
if not hasattr(cls, attr_name) or getattr(cls, attr_name) is None:
|
|
54
|
+
instance = super().__new__(cls)
|
|
55
|
+
setattr(cls, attr_name, instance)
|
|
56
|
+
instance._singleton_initialized = False
|
|
57
|
+
return getattr(cls, attr_name)
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
"""Initialize the singleton instance exactly once."""
|
|
61
|
+
if hasattr(self, "_singleton_initialized") and self._singleton_initialized:
|
|
62
|
+
return
|
|
63
|
+
self._singleton_initialized = True
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def initialized(self):
|
|
67
|
+
"""Return True if the singleton has been initialized."""
|
|
68
|
+
return getattr(self, "_singleton_initialized", False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ThreadSafeSingletonMixin(SingletonMixin):
|
|
72
|
+
"""
|
|
73
|
+
Thread-safe singleton mixin with locking mechanisms.
|
|
74
|
+
|
|
75
|
+
Extends SingletonMixin with thread safety using locks to prevent race
|
|
76
|
+
conditions during instance creation in multi-threaded environments. Essential
|
|
77
|
+
for scheduler components and other shared resources accessed concurrently.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
::
|
|
81
|
+
class SchedulerResource(ThreadSafeSingletonMixin):
|
|
82
|
+
def __init__(self):
|
|
83
|
+
super().__init__()
|
|
84
|
+
if not self.initialized:
|
|
85
|
+
self.resource_pool = initialize_resources()
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __new__(cls, *args, **kwargs): # noqa: ARG004
|
|
89
|
+
"""
|
|
90
|
+
Create or return the singleton instance with thread safety.
|
|
91
|
+
|
|
92
|
+
:param args: Positional arguments passed to the constructor
|
|
93
|
+
:param kwargs: Keyword arguments passed to the constructor
|
|
94
|
+
:return: The singleton instance of the class
|
|
95
|
+
"""
|
|
96
|
+
# Use class-specific lock and instance names to avoid inheritance issues
|
|
97
|
+
lock_attr_name = f"_singleton_lock_{cls.__name__}"
|
|
98
|
+
instance_attr_name = f"_singleton_instance_{cls.__name__}"
|
|
99
|
+
|
|
100
|
+
with getattr(cls, lock_attr_name):
|
|
101
|
+
instance_exists = (
|
|
102
|
+
hasattr(cls, instance_attr_name)
|
|
103
|
+
and getattr(cls, instance_attr_name) is not None
|
|
104
|
+
)
|
|
105
|
+
if not instance_exists:
|
|
106
|
+
instance = super(SingletonMixin, cls).__new__(cls)
|
|
107
|
+
setattr(cls, instance_attr_name, instance)
|
|
108
|
+
instance._singleton_initialized = False
|
|
109
|
+
instance._init_lock = threading.Lock()
|
|
110
|
+
return getattr(cls, instance_attr_name)
|
|
111
|
+
|
|
112
|
+
def __init_subclass__(cls, *args, **kwargs):
|
|
113
|
+
super().__init_subclass__(*args, **kwargs)
|
|
114
|
+
lock_attr_name = f"_singleton_lock_{cls.__name__}"
|
|
115
|
+
setattr(cls, lock_attr_name, threading.Lock())
|
|
116
|
+
|
|
117
|
+
def __init__(self):
|
|
118
|
+
"""Initialize the singleton instance with thread-safe initialization."""
|
|
119
|
+
with self._init_lock:
|
|
120
|
+
if hasattr(self, "_singleton_initialized") and self._singleton_initialized:
|
|
121
|
+
return
|
|
122
|
+
self._singleton_initialized = True
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def thread_lock(self):
|
|
126
|
+
"""Return the thread lock for this singleton instance."""
|
|
127
|
+
return getattr(self, "_init_lock", None)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def get_singleton_lock(cls):
|
|
131
|
+
"""Get the class-specific singleton creation lock."""
|
|
132
|
+
lock_attr_name = f"_singleton_lock_{cls.__name__}"
|
|
133
|
+
return getattr(cls, lock_attr_name, None)
|