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/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)