morphic 0.1.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.
- morphic/__init__.py +7 -0
- morphic/autoenum.py +397 -0
- morphic/registry.py +955 -0
- morphic/typed.py +1327 -0
- morphic-0.1.0.dist-info/METADATA +333 -0
- morphic-0.1.0.dist-info/RECORD +8 -0
- morphic-0.1.0.dist-info/WHEEL +4 -0
- morphic-0.1.0.dist-info/licenses/LICENSE +21 -0
morphic/registry.py
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
"""Registry pattern for automatic class registration and retrieval."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _is_abstract(cls: Type) -> bool:
|
|
8
|
+
"""Check if a class is abstract."""
|
|
9
|
+
return ABC in cls.__bases__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _str_normalize(x: str, remove: Optional[Union[str, Tuple, List, Set]] = (" ", "-", "_")) -> str:
|
|
13
|
+
"""Normalize string by removing specified characters and converting to lowercase."""
|
|
14
|
+
if remove is None:
|
|
15
|
+
remove = set()
|
|
16
|
+
if isinstance(remove, str):
|
|
17
|
+
remove = set(remove)
|
|
18
|
+
|
|
19
|
+
out = str(x)
|
|
20
|
+
if remove:
|
|
21
|
+
for rem in set(remove).intersection(set(out)):
|
|
22
|
+
out = out.replace(rem, "")
|
|
23
|
+
return out.lower()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _as_list(item) -> List:
|
|
27
|
+
"""Convert item to list."""
|
|
28
|
+
if isinstance(item, (list, tuple, set)):
|
|
29
|
+
return list(item)
|
|
30
|
+
return [item]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_set(item) -> Set:
|
|
34
|
+
"""Convert item to set."""
|
|
35
|
+
if isinstance(item, set):
|
|
36
|
+
return item
|
|
37
|
+
if isinstance(item, (list, tuple)):
|
|
38
|
+
return set(item)
|
|
39
|
+
return {item}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Registry(ABC):
|
|
43
|
+
"""
|
|
44
|
+
Inheritance-based class registration system with hierarchical factory pattern.
|
|
45
|
+
|
|
46
|
+
Registry automatically registers classes through inheritance and provides sophisticated factory methods
|
|
47
|
+
for instance creation. Classes automatically register themselves when they inherit from Registry subclasses,
|
|
48
|
+
and the system provides hierarchical factory methods that respect class inheritance relationships.
|
|
49
|
+
|
|
50
|
+
Features:
|
|
51
|
+
- **Automatic Registration**: Classes auto-register when inheriting from Registry
|
|
52
|
+
- **Hierarchical Factory Pattern**: Three-tier intelligent instantiation
|
|
53
|
+
- **Scoped Lookups**: Each class can only instantiate its own subclasses
|
|
54
|
+
- **Class Aliases**: Multiple names for the same class
|
|
55
|
+
- **Custom Registry Keys**: Flexible key systems including tuples
|
|
56
|
+
- **String Normalization**: Case-insensitive, flexible key matching
|
|
57
|
+
- **Registry Inspection**: Query available classes and hierarchies
|
|
58
|
+
|
|
59
|
+
Hierarchical Factory Pattern:
|
|
60
|
+
The Registry provides three intelligent instantiation modes:
|
|
61
|
+
|
|
62
|
+
1. **Direct Instantiation (Concrete Classes)**:
|
|
63
|
+
Concrete classes can be instantiated directly without registry keys
|
|
64
|
+
```python
|
|
65
|
+
dog = Dog.of() # No key needed for concrete classes
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. **Factory Method (Abstract Classes)**:
|
|
69
|
+
Abstract classes require registry keys and search within their hierarchy
|
|
70
|
+
```python
|
|
71
|
+
animal = Animal.of("Dog", name="Buddy") # Key required for abstract classes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. **Hierarchical Scoping**:
|
|
75
|
+
Classes can only instantiate their own subclasses, enforcing hierarchy
|
|
76
|
+
```python
|
|
77
|
+
cat = Cat.of("TabbyCat") # ✅ TabbyCat is subclass of Cat
|
|
78
|
+
# dog = Cat.of("Dog") # ❌ Dog is not subclass of Cat
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Basic Usage:
|
|
82
|
+
```python
|
|
83
|
+
from morphic.registry import Registry
|
|
84
|
+
from abc import ABC, abstractmethod
|
|
85
|
+
|
|
86
|
+
# Base registry class
|
|
87
|
+
class Animal(Registry, ABC):
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def speak(self) -> str:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# Classes automatically register when inheriting
|
|
93
|
+
class Dog(Animal):
|
|
94
|
+
aliases = ["canine", "pup"] # Multiple aliases supported
|
|
95
|
+
|
|
96
|
+
def __init__(self, name: str = "Buddy"):
|
|
97
|
+
self.name = name
|
|
98
|
+
|
|
99
|
+
def speak(self) -> str:
|
|
100
|
+
return f"{self.name} says Woof!"
|
|
101
|
+
|
|
102
|
+
class Cat(Animal):
|
|
103
|
+
aliases = ["feline", "kitty"]
|
|
104
|
+
|
|
105
|
+
def __init__(self, name: str = "Whiskers"):
|
|
106
|
+
self.name = name
|
|
107
|
+
|
|
108
|
+
def speak(self) -> str:
|
|
109
|
+
return f"{self.name} says Meow!"
|
|
110
|
+
|
|
111
|
+
# Hierarchical factory usage examples
|
|
112
|
+
dog = Dog.of(name="Rex") # Direct concrete instantiation
|
|
113
|
+
cat = Animal.of("Cat", name="Fluffy") # Factory with class name
|
|
114
|
+
feline = Animal.of("feline", name="Shadow") # Using alias
|
|
115
|
+
pup = Animal.of("pup", name="Buddy") # Case-insensitive fuzzy matching
|
|
116
|
+
|
|
117
|
+
# String normalization works automatically
|
|
118
|
+
canine = Animal.of("CANINE", name="Max") # Case insensitive
|
|
119
|
+
dog2 = Animal.of("ca-nine", name="Rex") # Handles dashes/spaces
|
|
120
|
+
|
|
121
|
+
print(dog.speak()) # "Rex says Woof!"
|
|
122
|
+
print(cat.speak()) # "Fluffy says Meow!"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Advanced Features:
|
|
126
|
+
```python
|
|
127
|
+
# Class aliases for flexible naming
|
|
128
|
+
class DatabaseConnection(Registry):
|
|
129
|
+
aliases = ["db", "database", "connection"]
|
|
130
|
+
|
|
131
|
+
# Multiple ways to create the same object
|
|
132
|
+
db1 = DatabaseConnection.of() # Direct concrete instantiation
|
|
133
|
+
db2 = DatabaseConnection.of("db", host="remote") # Using alias
|
|
134
|
+
db3 = DatabaseConnection.of("DATABASE") # Case insensitive
|
|
135
|
+
|
|
136
|
+
# Custom registry keys with _registry_keys method
|
|
137
|
+
class HTTPSService(Registry):
|
|
138
|
+
@classmethod
|
|
139
|
+
def _registry_keys(cls):
|
|
140
|
+
return [
|
|
141
|
+
("protocol", "https"), # Tuple key
|
|
142
|
+
("service", "web"), # Another tuple key
|
|
143
|
+
8443, # Numeric key
|
|
144
|
+
"secure_web" # String key
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Create using different key types
|
|
148
|
+
service1 = HTTPSService.of(("protocol", "https")) # Tuple key
|
|
149
|
+
service2 = HTTPSService.of(8443) # Numeric key
|
|
150
|
+
service3 = HTTPSService.of("secure_web") # String key
|
|
151
|
+
|
|
152
|
+
# Registry inspection for discovery
|
|
153
|
+
available_classes = DatabaseConnection.subclasses() # Get all concrete subclasses
|
|
154
|
+
class_by_key = DatabaseConnection.get_subclass("db") # Get class by key
|
|
155
|
+
all_keys = list(DatabaseConnection._registry.keys()) # All registered keys
|
|
156
|
+
|
|
157
|
+
# Error handling with hierarchical scoping
|
|
158
|
+
try:
|
|
159
|
+
service = WrongHierarchy.of("SomeClass") # KeyError with available options
|
|
160
|
+
except KeyError as e:
|
|
161
|
+
print(f"Available in hierarchy: {e}")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Hierarchical Architecture:
|
|
165
|
+
```python
|
|
166
|
+
class Service(Registry, ABC):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
class DataService(Service, ABC):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
class NotificationService(Service, ABC):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
class FileService(DataService):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
class EmailService(NotificationService):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# Hierarchical scoping enforced
|
|
182
|
+
file_svc = DataService.of("FileService") # ✅ Works
|
|
183
|
+
email_svc = NotificationService.of("EmailService") # ✅ Works
|
|
184
|
+
any_svc = Service.of("EmailService") # ✅ Works (common base)
|
|
185
|
+
# email_svc = DataService.of("EmailService") # ❌ Wrong hierarchy
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Configuration Options:
|
|
189
|
+
Control registration behavior with class attributes:
|
|
190
|
+
|
|
191
|
+
- `aliases`: List of alternative names for the class
|
|
192
|
+
- `_allow_multiple_subclasses`: Allow multiple classes per key (default: False)
|
|
193
|
+
- `_allow_subclass_override`: Allow overriding existing registrations (default: False)
|
|
194
|
+
- `_dont_register`: Skip automatic registration (default: False)
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
class FlexibleService(Registry, ABC):
|
|
198
|
+
_allow_multiple_subclasses = True
|
|
199
|
+
_allow_subclass_override = True
|
|
200
|
+
|
|
201
|
+
class SkippedService(Registry):
|
|
202
|
+
_dont_register = True # Won't register itself
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Registry Inspection:
|
|
206
|
+
Query and explore the registry:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Get all subclasses
|
|
210
|
+
animal_types = Animal.subclasses() # Concrete classes only
|
|
211
|
+
all_animals = Animal.subclasses(keep_abstract=True) # Include abstract
|
|
212
|
+
|
|
213
|
+
# Get specific class by key
|
|
214
|
+
dog_class = Animal.get_subclass("Dog") # Returns Dog class
|
|
215
|
+
cat_class = Animal.get_subclass("feline") # Using alias
|
|
216
|
+
|
|
217
|
+
# Check what's available
|
|
218
|
+
available_keys = list(Animal._registry.keys())
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Error Handling:
|
|
222
|
+
The Registry provides clear error messages for common issues:
|
|
223
|
+
|
|
224
|
+
- `TypeError`: Calling Registry.of() directly or abstract class without key
|
|
225
|
+
- `KeyError`: Registry key not found in hierarchy
|
|
226
|
+
- `TypeError`: Multiple subclasses registered to same key (when not allowed)
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
try:
|
|
230
|
+
# Abstract class without key
|
|
231
|
+
animal = Animal.of()
|
|
232
|
+
except TypeError as e:
|
|
233
|
+
print(f"Need registry key: {e}")
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Key not in hierarchy
|
|
237
|
+
dog = Cat.of("Dog")
|
|
238
|
+
except KeyError as e:
|
|
239
|
+
print(f"Wrong hierarchy: {e}")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Performance Notes:
|
|
243
|
+
- Key lookups are O(1) hash table operations
|
|
244
|
+
- String normalization happens once during registration
|
|
245
|
+
- Hierarchical filtering is optimized for inheritance chains
|
|
246
|
+
- Each registry hierarchy maintains its own lookup table
|
|
247
|
+
- Minimal memory overhead per registered class
|
|
248
|
+
|
|
249
|
+
See Also:
|
|
250
|
+
- `of()`: Hierarchical factory method for instance creation
|
|
251
|
+
- `get_subclass()`: Get class by key without creating instance
|
|
252
|
+
- `subclasses()`: Get all registered subclasses
|
|
253
|
+
- `remove_subclass()`: Remove class from registry
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
_registry: ClassVar[Dict[Any, Dict[str, Type]]] = {}
|
|
257
|
+
_registry_base_class: ClassVar[Optional[Type]] = None
|
|
258
|
+
_allow_multiple_subclasses: ClassVar[bool] = False
|
|
259
|
+
_allow_subclass_override: ClassVar[bool] = False
|
|
260
|
+
_dont_register: ClassVar[bool] = False
|
|
261
|
+
aliases: ClassVar[Tuple[str, ...]] = tuple()
|
|
262
|
+
|
|
263
|
+
def __init_subclass__(cls, **kwargs):
|
|
264
|
+
"""Register any subclass with the base class."""
|
|
265
|
+
super().__init_subclass__(**kwargs)
|
|
266
|
+
|
|
267
|
+
if cls in Registry.__subclasses__():
|
|
268
|
+
# Current class is a direct subclass of Registry (base class of hierarchy)
|
|
269
|
+
cls._registry = {}
|
|
270
|
+
cls._registry_base_class = cls
|
|
271
|
+
else:
|
|
272
|
+
# Current class is a subclass of a Registry-subclass
|
|
273
|
+
if not _is_abstract(cls) and not cls._dont_register:
|
|
274
|
+
cls._register_subclass()
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def _register_subclass(cls):
|
|
278
|
+
"""Register this subclass in the registry."""
|
|
279
|
+
keys_to_register = []
|
|
280
|
+
|
|
281
|
+
# Add class name and aliases
|
|
282
|
+
for key in [cls.__name__] + _as_list(cls.aliases) + _as_list(cls._registry_keys()):
|
|
283
|
+
if key is None:
|
|
284
|
+
continue
|
|
285
|
+
elif isinstance(key, str):
|
|
286
|
+
# Case-insensitive matching
|
|
287
|
+
key = _str_normalize(key)
|
|
288
|
+
elif isinstance(key, tuple):
|
|
289
|
+
key = tuple(
|
|
290
|
+
_str_normalize(key_part) if isinstance(key_part, str) else key_part for key_part in key
|
|
291
|
+
)
|
|
292
|
+
keys_to_register.append(key)
|
|
293
|
+
|
|
294
|
+
cls._add_to_registry(keys_to_register, cls)
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def _add_to_registry(cls, keys_to_register: List[Any], subclass: Type):
|
|
298
|
+
"""Add subclass to registry under specified keys."""
|
|
299
|
+
subclass_name = subclass.__name__
|
|
300
|
+
|
|
301
|
+
for k in _as_set(keys_to_register): # Drop duplicates
|
|
302
|
+
if k not in cls._registry:
|
|
303
|
+
cls._registry[k] = {subclass_name: subclass}
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Key already exists in registry
|
|
307
|
+
registered = cls._registry[k]
|
|
308
|
+
registered_names = set(registered.keys())
|
|
309
|
+
|
|
310
|
+
if subclass_name in registered_names and not cls._allow_subclass_override:
|
|
311
|
+
raise KeyError(
|
|
312
|
+
f"A subclass with name '{subclass_name}' is already registered "
|
|
313
|
+
f"against key '{k}' for registry under '{cls._registry_base_class}'; "
|
|
314
|
+
f"overriding subclasses is not permitted."
|
|
315
|
+
)
|
|
316
|
+
elif subclass_name not in registered_names and not cls._allow_multiple_subclasses:
|
|
317
|
+
if len(registered_names) == 0:
|
|
318
|
+
raise ValueError(f"Invalid state: key '{k}' is registered to an empty dict")
|
|
319
|
+
if len(registered_names) > 1:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"Invalid state: _allow_multiple_subclasses is False but multiple subclasses "
|
|
322
|
+
f"are registered against key {k}"
|
|
323
|
+
)
|
|
324
|
+
existing_subclass = next(iter(registered_names))
|
|
325
|
+
raise KeyError(
|
|
326
|
+
f"Key {k} is already registered to subclass {existing_subclass}; "
|
|
327
|
+
f"registering multiple subclasses to the same key is not permitted."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
cls._registry[k] = {
|
|
331
|
+
**registered,
|
|
332
|
+
subclass_name: subclass,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def get_subclass(
|
|
337
|
+
cls,
|
|
338
|
+
key: Any,
|
|
339
|
+
raise_error: bool = True,
|
|
340
|
+
) -> Optional[Union[Type, List[Type]]]:
|
|
341
|
+
"""
|
|
342
|
+
Get registered subclass(es) by key without creating instances.
|
|
343
|
+
|
|
344
|
+
This method performs a global registry lookup (not hierarchical) to find all subclasses
|
|
345
|
+
registered under the given key. Unlike the hierarchical `of()` method, this searches
|
|
346
|
+
the entire registry without scoping restrictions.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
key (Any): Key to look up subclass. Can be:
|
|
350
|
+
- str: Class name or alias (case-insensitive, normalized)
|
|
351
|
+
- tuple: Custom composite key from _registry_keys()
|
|
352
|
+
- Any: Custom key type from _registry_keys()
|
|
353
|
+
raise_error (bool): Whether to raise KeyError if key not found.
|
|
354
|
+
Defaults to True.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
- **Type**: Single subclass if only one is registered under the key
|
|
358
|
+
- **List[Type]**: List of subclasses if multiple are registered (when _allow_multiple_subclasses=True)
|
|
359
|
+
- **None**: If key not found and raise_error=False
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
KeyError: If key not found and raise_error=True
|
|
363
|
+
|
|
364
|
+
Examples:
|
|
365
|
+
```python
|
|
366
|
+
class Animal(Registry, ABC):
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
class Dog(Animal):
|
|
370
|
+
aliases = ["canine", "pup"]
|
|
371
|
+
|
|
372
|
+
class Cat(Animal):
|
|
373
|
+
aliases = ["feline"]
|
|
374
|
+
|
|
375
|
+
# Get class by name
|
|
376
|
+
DogClass = Animal.get_subclass("Dog")
|
|
377
|
+
assert DogClass is Dog
|
|
378
|
+
|
|
379
|
+
# Get class by alias
|
|
380
|
+
CanineClass = Animal.get_subclass("canine")
|
|
381
|
+
assert CanineClass is Dog
|
|
382
|
+
|
|
383
|
+
# Case insensitive
|
|
384
|
+
CatClass = Animal.get_subclass("FELINE")
|
|
385
|
+
assert CatClass is Cat
|
|
386
|
+
|
|
387
|
+
# Handle missing keys
|
|
388
|
+
UnknownClass = Animal.get_subclass("Robot", raise_error=False)
|
|
389
|
+
assert UnknownClass is None
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
Animal.get_subclass("Robot") # raise_error=True by default
|
|
393
|
+
except KeyError:
|
|
394
|
+
print("Robot not found")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Note:
|
|
398
|
+
This method searches the entire registry and does not enforce hierarchical
|
|
399
|
+
scoping like the `of()` method. Use `of()` for hierarchy-aware instantiation.
|
|
400
|
+
"""
|
|
401
|
+
if isinstance(key, str):
|
|
402
|
+
subclasses = cls._registry.get(_str_normalize(key))
|
|
403
|
+
elif isinstance(key, tuple):
|
|
404
|
+
# Normalize tuple keys the same way as during registration
|
|
405
|
+
normalized_key = tuple(
|
|
406
|
+
_str_normalize(key_part) if isinstance(key_part, str) else key_part for key_part in key
|
|
407
|
+
)
|
|
408
|
+
subclasses = cls._registry.get(normalized_key)
|
|
409
|
+
else:
|
|
410
|
+
subclasses = cls._registry.get(key)
|
|
411
|
+
|
|
412
|
+
if subclasses is None:
|
|
413
|
+
if raise_error:
|
|
414
|
+
available_keys = "\n".join(sorted(str(k) for k in cls._registry.keys()))
|
|
415
|
+
raise KeyError(
|
|
416
|
+
f'Could not find subclass of {cls} using key: "{key}" (type={type(key)}). '
|
|
417
|
+
f"Available keys are:\n{available_keys}"
|
|
418
|
+
)
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
if len(subclasses) == 1:
|
|
422
|
+
return next(iter(subclasses.values()))
|
|
423
|
+
return list(subclasses.values())
|
|
424
|
+
|
|
425
|
+
@classmethod
|
|
426
|
+
def subclasses(cls, keep_abstract: bool = False) -> Set[Type]:
|
|
427
|
+
"""
|
|
428
|
+
Get all registered subclasses of this Registry class.
|
|
429
|
+
|
|
430
|
+
Returns all classes that have been registered as subclasses of the calling class.
|
|
431
|
+
This provides a way to discover what implementations are available without needing
|
|
432
|
+
to know their keys in advance.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
keep_abstract (bool): Whether to include abstract classes in the result.
|
|
436
|
+
Defaults to False (only concrete classes returned).
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Set[Type]: Set of registered subclass types. Abstract classes are excluded
|
|
440
|
+
unless keep_abstract=True.
|
|
441
|
+
|
|
442
|
+
Examples:
|
|
443
|
+
```python
|
|
444
|
+
class Animal(Registry, ABC):
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
class Mammal(Animal, ABC): # Abstract intermediate
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
class Dog(Mammal):
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
class Cat(Mammal):
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
class Bird(Animal, ABC): # Abstract intermediate
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
class Parrot(Bird):
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
# Get concrete subclasses only (default)
|
|
463
|
+
concrete_animals = Animal.subclasses()
|
|
464
|
+
assert Dog in concrete_animals
|
|
465
|
+
assert Cat in concrete_animals
|
|
466
|
+
assert Parrot in concrete_animals
|
|
467
|
+
assert Mammal not in concrete_animals # Abstract
|
|
468
|
+
assert Bird not in concrete_animals # Abstract
|
|
469
|
+
|
|
470
|
+
# Include abstract classes
|
|
471
|
+
all_animals = Animal.subclasses(keep_abstract=True)
|
|
472
|
+
assert Mammal in all_animals
|
|
473
|
+
assert Bird in all_animals
|
|
474
|
+
assert Dog in all_animals
|
|
475
|
+
|
|
476
|
+
# Hierarchical subclasses
|
|
477
|
+
mammals = Mammal.subclasses() # Only mammal subclasses
|
|
478
|
+
assert Dog in mammals
|
|
479
|
+
assert Cat in mammals
|
|
480
|
+
assert Parrot not in mammals # Not a mammal
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Use Cases:
|
|
484
|
+
- **Plugin Discovery**: Find all available plugin implementations
|
|
485
|
+
- **Validation**: Check what implementations are registered
|
|
486
|
+
- **Dynamic UI**: Build menus or lists of available options
|
|
487
|
+
- **Testing**: Verify all expected subclasses are registered
|
|
488
|
+
- **Documentation**: Generate lists of available classes
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
# Plugin discovery example
|
|
492
|
+
available_processors = DataProcessor.subclasses()
|
|
493
|
+
for processor_cls in available_processors:
|
|
494
|
+
print(f"Available: {processor_cls.__name__}")
|
|
495
|
+
if hasattr(processor_cls, 'aliases'):
|
|
496
|
+
print(f" Aliases: {processor_cls.aliases}")
|
|
497
|
+
|
|
498
|
+
# Dynamic factory with validation
|
|
499
|
+
def create_safe_processor(processor_type: str, **kwargs):
|
|
500
|
+
available = {cls.__name__.lower(): cls for cls in DataProcessor.subclasses()}
|
|
501
|
+
if processor_type.lower() not in available:
|
|
502
|
+
raise ValueError(f"Unknown processor: {processor_type}. "
|
|
503
|
+
f"Available: {list(available.keys())}")
|
|
504
|
+
return available[processor_type.lower()](**kwargs)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Note:
|
|
508
|
+
This method respects the registry hierarchy - it only returns subclasses of the
|
|
509
|
+
calling class, not subclasses of sibling classes.
|
|
510
|
+
"""
|
|
511
|
+
available_subclasses = set()
|
|
512
|
+
|
|
513
|
+
for registered_dict in cls._registry.values():
|
|
514
|
+
for subclass in registered_dict.values():
|
|
515
|
+
if subclass == cls._registry_base_class:
|
|
516
|
+
continue
|
|
517
|
+
if _is_abstract(subclass) and not keep_abstract:
|
|
518
|
+
continue
|
|
519
|
+
if isinstance(subclass, type) and issubclass(subclass, cls):
|
|
520
|
+
available_subclasses.add(subclass)
|
|
521
|
+
|
|
522
|
+
return available_subclasses
|
|
523
|
+
|
|
524
|
+
@classmethod
|
|
525
|
+
def _get_hierarchical_subclass(cls, registry_key: Any) -> Optional[Union[Type, List[Type]]]:
|
|
526
|
+
"""
|
|
527
|
+
Get subclass by registry_key, but only search within the hierarchy of the calling class.
|
|
528
|
+
|
|
529
|
+
For concrete classes, this can return the class itself if the registry_key matches.
|
|
530
|
+
For abstract classes, this searches only within direct and indirect subclasses.
|
|
531
|
+
"""
|
|
532
|
+
# If the class is concrete (not abstract) and registry_key matches the class name or aliases
|
|
533
|
+
if not _is_abstract(cls):
|
|
534
|
+
# Check if registry_key matches this concrete class
|
|
535
|
+
class_keys = [cls.__name__] + _as_list(cls.aliases) + _as_list(cls._registry_keys())
|
|
536
|
+
|
|
537
|
+
for class_key in class_keys:
|
|
538
|
+
if class_key is None:
|
|
539
|
+
continue
|
|
540
|
+
elif isinstance(class_key, str):
|
|
541
|
+
if _str_normalize(class_key) == _str_normalize(registry_key) if isinstance(registry_key, str) else False:
|
|
542
|
+
return cls
|
|
543
|
+
elif isinstance(class_key, tuple) and isinstance(registry_key, tuple):
|
|
544
|
+
normalized_class_key = tuple(
|
|
545
|
+
_str_normalize(k) if isinstance(k, str) else k for k in class_key
|
|
546
|
+
)
|
|
547
|
+
normalized_registry_key = tuple(
|
|
548
|
+
_str_normalize(k) if isinstance(k, str) else k for k in registry_key
|
|
549
|
+
)
|
|
550
|
+
if normalized_class_key == normalized_registry_key:
|
|
551
|
+
return cls
|
|
552
|
+
elif class_key == registry_key:
|
|
553
|
+
return cls
|
|
554
|
+
|
|
555
|
+
# Search in registry but filter to only subclasses of cls
|
|
556
|
+
matching_subclasses = {}
|
|
557
|
+
|
|
558
|
+
# Normalize the search key
|
|
559
|
+
if isinstance(registry_key, str):
|
|
560
|
+
search_key = _str_normalize(registry_key)
|
|
561
|
+
elif isinstance(registry_key, tuple):
|
|
562
|
+
search_key = tuple(
|
|
563
|
+
_str_normalize(key_part) if isinstance(key_part, str) else key_part for key_part in registry_key
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
search_key = registry_key
|
|
567
|
+
|
|
568
|
+
# Look through registry for matching keys
|
|
569
|
+
registry_entry = cls._registry.get(search_key)
|
|
570
|
+
if registry_entry:
|
|
571
|
+
# Filter to only include subclasses of cls
|
|
572
|
+
for subclass_name, subclass in registry_entry.items():
|
|
573
|
+
if isinstance(subclass, type) and issubclass(subclass, cls) and subclass != cls:
|
|
574
|
+
matching_subclasses[subclass_name] = subclass
|
|
575
|
+
|
|
576
|
+
if not matching_subclasses:
|
|
577
|
+
return None
|
|
578
|
+
|
|
579
|
+
if len(matching_subclasses) == 1:
|
|
580
|
+
return next(iter(matching_subclasses.values()))
|
|
581
|
+
return list(matching_subclasses.values())
|
|
582
|
+
|
|
583
|
+
@classmethod
|
|
584
|
+
def remove_subclass(cls, subclass: Union[Type, str]):
|
|
585
|
+
"""Remove a subclass from the registry."""
|
|
586
|
+
name = subclass if isinstance(subclass, str) else subclass.__name__
|
|
587
|
+
|
|
588
|
+
# Remove from all registry entries and clean up empty dictionaries
|
|
589
|
+
keys_to_remove = []
|
|
590
|
+
for key, registered_dict in cls._registry.items():
|
|
591
|
+
for subclass_name in list(registered_dict.keys()):
|
|
592
|
+
if _str_normalize(subclass_name) == _str_normalize(name):
|
|
593
|
+
registered_dict.pop(subclass_name, None)
|
|
594
|
+
# Mark empty dictionaries for removal
|
|
595
|
+
if not registered_dict:
|
|
596
|
+
keys_to_remove.append(key)
|
|
597
|
+
|
|
598
|
+
# Remove empty registry entries
|
|
599
|
+
for key in keys_to_remove:
|
|
600
|
+
cls._registry.pop(key, None)
|
|
601
|
+
|
|
602
|
+
@classmethod
|
|
603
|
+
def _registry_keys(cls) -> Optional[Union[List[Any], Any]]:
|
|
604
|
+
"""
|
|
605
|
+
Override in subclasses to provide additional registry keys.
|
|
606
|
+
|
|
607
|
+
This method allows classes to register themselves under custom keys beyond their
|
|
608
|
+
class name and aliases. Useful for creating semantic keys, composite keys, or
|
|
609
|
+
domain-specific identifiers.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
- **None**: No additional keys (default)
|
|
613
|
+
- **Any**: Single additional key of any type
|
|
614
|
+
- **List[Any]**: Multiple additional keys of any type
|
|
615
|
+
|
|
616
|
+
Key Types:
|
|
617
|
+
- **str**: Additional string identifiers
|
|
618
|
+
- **tuple**: Composite keys for hierarchical or multi-dimensional lookups
|
|
619
|
+
- **int/float**: Numeric identifiers
|
|
620
|
+
- **Any**: Custom types for domain-specific keys
|
|
621
|
+
|
|
622
|
+
Examples:
|
|
623
|
+
```python
|
|
624
|
+
# String-based semantic keys
|
|
625
|
+
class EmailService(Service):
|
|
626
|
+
@classmethod
|
|
627
|
+
def _registry_keys(cls):
|
|
628
|
+
return ["mail", "smtp", "email-sender"]
|
|
629
|
+
|
|
630
|
+
# Tuple-based composite keys
|
|
631
|
+
class HTTPSService(Service):
|
|
632
|
+
@classmethod
|
|
633
|
+
def _registry_keys(cls):
|
|
634
|
+
return [
|
|
635
|
+
("protocol", "https"),
|
|
636
|
+
("service", "web"),
|
|
637
|
+
("port", 443)
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
# Mixed key types
|
|
641
|
+
class DatabaseService(Service):
|
|
642
|
+
@classmethod
|
|
643
|
+
def _registry_keys(cls):
|
|
644
|
+
return [
|
|
645
|
+
"database", # String key
|
|
646
|
+
("type", "database"), # Tuple key
|
|
647
|
+
5432, # Port number key
|
|
648
|
+
("database", "postgresql") # Specific database type
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
# Dynamic key generation
|
|
652
|
+
class APIService(Service):
|
|
653
|
+
version = "v2"
|
|
654
|
+
endpoint = "users"
|
|
655
|
+
|
|
656
|
+
@classmethod
|
|
657
|
+
def _registry_keys(cls):
|
|
658
|
+
return [
|
|
659
|
+
f"api-{cls.version}",
|
|
660
|
+
f"{cls.endpoint}-service",
|
|
661
|
+
("api", cls.version, cls.endpoint)
|
|
662
|
+
]
|
|
663
|
+
|
|
664
|
+
# Usage with different key types
|
|
665
|
+
email = Service.of("smtp") # String key
|
|
666
|
+
https = Service.of(("protocol", "https")) # Tuple key
|
|
667
|
+
db = Service.of(5432) # Numeric key
|
|
668
|
+
api = Service.of(("api", "v2", "users")) # Complex tuple key
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Best Practices:
|
|
672
|
+
```python
|
|
673
|
+
# Use semantic, domain-appropriate keys
|
|
674
|
+
class PaymentProcessor(Registry):
|
|
675
|
+
@classmethod
|
|
676
|
+
def _registry_keys(cls):
|
|
677
|
+
return [
|
|
678
|
+
"payment",
|
|
679
|
+
("service", "financial"),
|
|
680
|
+
("type", "processor")
|
|
681
|
+
]
|
|
682
|
+
|
|
683
|
+
# Avoid conflicts with likely aliases
|
|
684
|
+
class LoggingService(Registry):
|
|
685
|
+
aliases = ["logger", "log"] # Common aliases
|
|
686
|
+
|
|
687
|
+
@classmethod
|
|
688
|
+
def _registry_keys(cls):
|
|
689
|
+
# Avoid "log" here as it's already in aliases
|
|
690
|
+
return [
|
|
691
|
+
("service", "logging"),
|
|
692
|
+
("type", "audit"),
|
|
693
|
+
"audit-logger" # Specific, unlikely to conflict
|
|
694
|
+
]
|
|
695
|
+
|
|
696
|
+
# Use tuples for hierarchical organization
|
|
697
|
+
class DatabaseConnection(Registry):
|
|
698
|
+
@classmethod
|
|
699
|
+
def _registry_keys(cls):
|
|
700
|
+
return [
|
|
701
|
+
("database", "connection"),
|
|
702
|
+
("storage", "persistent"),
|
|
703
|
+
("type", "relational")
|
|
704
|
+
]
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Note:
|
|
708
|
+
- Keys are automatically normalized for strings (case-insensitive, flexible spacing)
|
|
709
|
+
- Tuple keys have their string elements normalized individually
|
|
710
|
+
- All keys must be hashable (usable as dictionary keys)
|
|
711
|
+
- Keys are registered in addition to class name and aliases
|
|
712
|
+
- Duplicate keys across classes will trigger registration conflicts unless
|
|
713
|
+
`_allow_multiple_subclasses` is True
|
|
714
|
+
"""
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def of(cls, registry_key: Any = None, *args, **kwargs):
|
|
719
|
+
"""
|
|
720
|
+
Hierarchical factory method for creating instances of registered subclasses.
|
|
721
|
+
|
|
722
|
+
This is the core factory method that provides intelligent, hierarchy-aware instance creation.
|
|
723
|
+
The behavior depends on whether the calling class is abstract or concrete, and whether a
|
|
724
|
+
registry_key is provided. The method enforces hierarchical scoping - classes can only
|
|
725
|
+
instantiate their own subclasses.
|
|
726
|
+
|
|
727
|
+
Hierarchical Factory Modes:
|
|
728
|
+
|
|
729
|
+
**1. Direct Instantiation (Concrete Classes):**
|
|
730
|
+
Concrete classes can create instances directly without registry keys:
|
|
731
|
+
```python
|
|
732
|
+
class Dog(Animal): # Concrete class
|
|
733
|
+
def __init__(self, name="Buddy"):
|
|
734
|
+
self.name = name
|
|
735
|
+
|
|
736
|
+
dog = Dog.of() # ✅ Direct instantiation
|
|
737
|
+
dog = Dog.of(name="Rex") # ✅ With constructor args
|
|
738
|
+
dog = Dog.of("Dog", name="Max") # ✅ Also works with matching key
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
**2. Factory Method (Abstract Classes):**
|
|
742
|
+
Abstract classes require registry keys to specify which subclass to create:
|
|
743
|
+
```python
|
|
744
|
+
class Animal(Registry, ABC): # Abstract base
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
dog = Animal.of("Dog", name="Buddy") # ✅ Key required
|
|
748
|
+
cat = Animal.of("Cat", name="Whiskers") # ✅ Key required
|
|
749
|
+
# animal = Animal.of() # ❌ TypeError: key required
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**3. Hierarchical Scoping:**
|
|
753
|
+
Classes can only instantiate their own subclasses, enforcing architecture:
|
|
754
|
+
```python
|
|
755
|
+
class Mammal(Animal, ABC):
|
|
756
|
+
pass
|
|
757
|
+
|
|
758
|
+
class Cat(Mammal, ABC):
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
class Dog(Mammal):
|
|
762
|
+
pass
|
|
763
|
+
|
|
764
|
+
class TabbyCat(Cat):
|
|
765
|
+
pass
|
|
766
|
+
|
|
767
|
+
# Hierarchical restrictions enforced
|
|
768
|
+
tabby = Cat.of("TabbyCat") # ✅ TabbyCat is Cat subclass
|
|
769
|
+
dog = Mammal.of("Dog") # ✅ Dog is Mammal subclass
|
|
770
|
+
any_animal = Animal.of("Dog") # ✅ Dog is Animal subclass
|
|
771
|
+
|
|
772
|
+
# dog = Cat.of("Dog") # ❌ KeyError: Dog not Cat subclass
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
registry_key (Any, optional): Key to look up subclass. Can be:
|
|
777
|
+
- **None**: For concrete classes, instantiates the class directly
|
|
778
|
+
- **str**: Class name or alias (case-insensitive, normalized)
|
|
779
|
+
- **tuple**: Custom composite key from _registry_keys()
|
|
780
|
+
- **Any**: Custom key type from _registry_keys()
|
|
781
|
+
|
|
782
|
+
If None and the class is concrete, instantiates that class directly.
|
|
783
|
+
If None and the class is abstract, raises TypeError.
|
|
784
|
+
|
|
785
|
+
*args: Positional arguments passed to the subclass constructor
|
|
786
|
+
**kwargs: Keyword arguments passed to the subclass constructor
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
Instance of the found subclass or the class itself (for direct instantiation)
|
|
790
|
+
|
|
791
|
+
Raises:
|
|
792
|
+
TypeError: If called on Registry base class directly, or if abstract class
|
|
793
|
+
called without registry_key, or multiple subclasses match key
|
|
794
|
+
KeyError: If registry_key not found in the class's hierarchy
|
|
795
|
+
|
|
796
|
+
Registry Key Types:
|
|
797
|
+
```python
|
|
798
|
+
# String keys (normalized: case-insensitive, space/dash/underscore flexible)
|
|
799
|
+
service = Service.of("EmailService")
|
|
800
|
+
service = Service.of("email-service") # Same as above
|
|
801
|
+
service = Service.of("EMAIL_SERVICE") # Same as above
|
|
802
|
+
|
|
803
|
+
# Alias keys
|
|
804
|
+
class EmailService(Service):
|
|
805
|
+
aliases = ["email", "mail", "smtp"]
|
|
806
|
+
|
|
807
|
+
service = Service.of("email") # Using alias
|
|
808
|
+
|
|
809
|
+
# Tuple keys from _registry_keys()
|
|
810
|
+
class HTTPService(Service):
|
|
811
|
+
@classmethod
|
|
812
|
+
def _registry_keys(cls):
|
|
813
|
+
return [("protocol", "http"), ("service", "web")]
|
|
814
|
+
|
|
815
|
+
service = Service.of(("protocol", "http"))
|
|
816
|
+
service = Service.of(("service", "web"))
|
|
817
|
+
|
|
818
|
+
# Direct instantiation (concrete classes only)
|
|
819
|
+
service = EmailService.of() # No key needed
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
Hierarchical Architecture Example:
|
|
823
|
+
```python
|
|
824
|
+
class Service(Registry, ABC):
|
|
825
|
+
@abstractmethod
|
|
826
|
+
def process(self, data): pass
|
|
827
|
+
|
|
828
|
+
class DataService(Service, ABC):
|
|
829
|
+
'''Base for data processing services'''
|
|
830
|
+
pass
|
|
831
|
+
|
|
832
|
+
class NotificationService(Service, ABC):
|
|
833
|
+
'''Base for notification services'''
|
|
834
|
+
pass
|
|
835
|
+
|
|
836
|
+
class FileService(DataService):
|
|
837
|
+
def process(self, data):
|
|
838
|
+
return f"Processing file: {data}"
|
|
839
|
+
|
|
840
|
+
class EmailService(NotificationService):
|
|
841
|
+
aliases = ["email", "mail"]
|
|
842
|
+
|
|
843
|
+
def process(self, data):
|
|
844
|
+
return f"Sending email: {data}"
|
|
845
|
+
|
|
846
|
+
class SMSService(NotificationService):
|
|
847
|
+
def process(self, data):
|
|
848
|
+
return f"Sending SMS: {data}"
|
|
849
|
+
|
|
850
|
+
# Hierarchical scoping in action
|
|
851
|
+
file_svc = DataService.of("FileService") # ✅ FileService ∈ DataService
|
|
852
|
+
email_svc = NotificationService.of("EmailService") # ✅ EmailService ∈ NotificationService
|
|
853
|
+
mail_svc = NotificationService.of("mail") # ✅ Using alias
|
|
854
|
+
any_svc = Service.of("SMSService") # ✅ SMSService ∈ Service
|
|
855
|
+
|
|
856
|
+
# These fail due to hierarchical restrictions
|
|
857
|
+
# email_svc = DataService.of("EmailService") # ❌ EmailService ∉ DataService
|
|
858
|
+
# file_svc = NotificationService.of("FileService") # ❌ FileService ∉ NotificationService
|
|
859
|
+
|
|
860
|
+
# Direct instantiation for concrete classes
|
|
861
|
+
file_svc = FileService.of() # ✅ Direct concrete instantiation
|
|
862
|
+
email_svc = EmailService.of(smtp_host="smtp.com") # ✅ With constructor args
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
Error Handling:
|
|
866
|
+
```python
|
|
867
|
+
# Registry base class usage
|
|
868
|
+
try:
|
|
869
|
+
Registry.of("SomeClass")
|
|
870
|
+
except TypeError as e:
|
|
871
|
+
# "The 'of' factory method cannot be called directly on Registry class"
|
|
872
|
+
|
|
873
|
+
# Abstract class without key
|
|
874
|
+
try:
|
|
875
|
+
Service.of() # Abstract class, no key
|
|
876
|
+
except TypeError as e:
|
|
877
|
+
# "Cannot instantiate abstract class 'Service' without specifying a registry_key"
|
|
878
|
+
|
|
879
|
+
# Key not found in hierarchy
|
|
880
|
+
try:
|
|
881
|
+
DataService.of("EmailService") # EmailService not in DataService hierarchy
|
|
882
|
+
except KeyError as e:
|
|
883
|
+
# "Could not find subclass of DataService using registry_key: 'EmailService'"
|
|
884
|
+
# Shows available keys in DataService hierarchy
|
|
885
|
+
|
|
886
|
+
# Multiple subclasses (when _allow_multiple_subclasses=True)
|
|
887
|
+
try:
|
|
888
|
+
Service.of("shared_alias") # Multiple classes have same alias
|
|
889
|
+
except TypeError as e:
|
|
890
|
+
# "Cannot instantiate using registry_key 'shared_alias' because multiple subclasses"
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Performance Notes:
|
|
894
|
+
- Registry lookups are O(1) hash table operations
|
|
895
|
+
- Hierarchical filtering is optimized for inheritance chains
|
|
896
|
+
- String normalization is cached during registration
|
|
897
|
+
- Direct instantiation (concrete classes) bypasses registry lookup entirely
|
|
898
|
+
|
|
899
|
+
See Also:
|
|
900
|
+
get_subclass(): Get class by key without creating instance
|
|
901
|
+
subclasses(): Get all registered subclasses in hierarchy
|
|
902
|
+
_get_hierarchical_subclass(): Internal hierarchical lookup method
|
|
903
|
+
"""
|
|
904
|
+
# Prevent calling 'of' directly on Registry class
|
|
905
|
+
if cls is Registry:
|
|
906
|
+
raise TypeError("The 'of' factory method cannot be called directly on Registry class. "
|
|
907
|
+
"It must be called on a subclass of Registry.")
|
|
908
|
+
|
|
909
|
+
# Ensure this is called on a Registry subclass
|
|
910
|
+
if not issubclass(cls, Registry):
|
|
911
|
+
raise TypeError(f"The 'of' method can only be called on Registry subclasses, "
|
|
912
|
+
f"but {cls.__name__} is not a Registry subclass.")
|
|
913
|
+
|
|
914
|
+
# Handle case where no registry_key is provided
|
|
915
|
+
if registry_key is None:
|
|
916
|
+
if not _is_abstract(cls):
|
|
917
|
+
# Concrete class without registry_key - instantiate directly
|
|
918
|
+
return cls(*args, **kwargs)
|
|
919
|
+
else:
|
|
920
|
+
# Abstract class without registry_key - cannot instantiate
|
|
921
|
+
raise TypeError(f"Cannot instantiate abstract class '{cls.__name__}' without specifying "
|
|
922
|
+
f"a registry_key to identify which subclass to create.")
|
|
923
|
+
|
|
924
|
+
# Use hierarchical lookup to find the subclass
|
|
925
|
+
subclass = cls._get_hierarchical_subclass(registry_key)
|
|
926
|
+
|
|
927
|
+
if subclass is None:
|
|
928
|
+
# Build error message showing available keys in this hierarchy
|
|
929
|
+
available_classes = set()
|
|
930
|
+
|
|
931
|
+
# If concrete, the class itself is available
|
|
932
|
+
if not _is_abstract(cls):
|
|
933
|
+
available_classes.add(cls.__name__)
|
|
934
|
+
|
|
935
|
+
# Add subclasses
|
|
936
|
+
for sub in cls.subclasses(keep_abstract=True):
|
|
937
|
+
available_classes.add(sub.__name__)
|
|
938
|
+
if hasattr(sub, 'aliases'):
|
|
939
|
+
available_classes.update(_as_list(sub.aliases))
|
|
940
|
+
|
|
941
|
+
available_keys = sorted(available_classes)
|
|
942
|
+
raise KeyError(f'Could not find subclass of {cls.__name__} using registry_key: "{registry_key}" (type={type(registry_key)}). '
|
|
943
|
+
f"Available keys in this hierarchy are: {available_keys}")
|
|
944
|
+
|
|
945
|
+
# Handle case where multiple subclasses are registered to the same registry_key
|
|
946
|
+
if isinstance(subclass, list):
|
|
947
|
+
if len(subclass) == 1:
|
|
948
|
+
subclass = subclass[0]
|
|
949
|
+
else:
|
|
950
|
+
raise TypeError(f"Cannot instantiate using registry_key '{registry_key}' because multiple subclasses "
|
|
951
|
+
f"are registered: {[sc.__name__ for sc in subclass]}. "
|
|
952
|
+
f"Use a more specific registry_key to select a single subclass.")
|
|
953
|
+
|
|
954
|
+
# Create and return instance
|
|
955
|
+
return subclass(*args, **kwargs)
|