๐ Technical Design 022 โ Unified Runtime Marker System¶
| Field | Value |
|---|---|
| ID | TD-022 |
| Title | Unified Runtime Marker System |
| Status | Completed |
| Owner | Contributors |
| Audience | Contributors, maintainers, framework users |
| Created | 2025-10-18 |
| Last Updated | 2025-10-18 |
| Reviewers | TBD |
| Approvers | TBD |
| Tracking | Phase 11 of DI System Roadmap |
| Related | TD-011: DI Injection System, TD-012: Enhanced DI Annotations, TD-013: Module Scanning Discovery |
๐งญ 1. Summary¶
This technical design proposes a comprehensive refactoring of loryth.common.annotations into a unified runtime marker system called loryth.common.markers. The system addresses terminology confusion between Python's compile-time type annotations and our runtime metadata mechanism, while establishing a cohesive API for both class/function markers (via decorators) and attribute markers (via Annotated[T, Marker()]). All framework code, including every module under loryth.common.di, will migrate directly to the new module without shims or compatibility layers.
At a glance:
- Rename
loryth.common.annotationsโloryth.common.markersto eliminate naming confusion - Unified API supporting both callable markers (classes, functions, methods) and attribute markers (via
Annotated[]) - Single discovery interface for querying both marker types consistently
- Minimal base contract designed for extensibility without feature bloat
- Immediate DI migration by updating all
loryth.common.diimports and usages to the new module
๐ 2. Context and Problem Statement¶
Current State¶
The loryth.common.annotations module provides runtime metadata attachment and discovery for:
- Callable Markers โ Applied to classes, functions, and methods via decorators (e.g.,
@service(),@bean()) - Attribute Markers โ Applied to class attributes via Python's
Annotated[Type, Metadata](e.g.,Autowired())
Both patterns serve the same purpose: runtime metadata discovery for the DI container, scanning, validation, and other framework features.
2.1 Pain Points¶
- Terminology Confusion โ The term "annotation" conflates with Python's
@decoratorsyntax andtype hints, creating ambiguity: - "Annotation" in Python typically means
__annotations__dict (type hints) - Our system attaches runtime metadata separately from type hints
-
This semantic overlap confuses newcomers and makes code intent unclear
-
Inconsistent API Surface โ The system supports two distinct patterns:
- Callable markers use decorators and a handler-based registry
- Attribute markers use
Annotated[]and require reflection over__annotations__ -
No unified discovery mechanism (users must know which pattern applies)
-
Architectural Ambiguity โ It's unclear what aspects are common and what's pattern-specific:
- Both store metadata, but via different mechanisms
- Both support discovery, but with different APIs
-
New contributors must study both patterns to add features
-
Scalability Concerns โ As new marker types emerge (validation, serialization, observability), the architecture doesn't scale cleanly:
- No clear extension points
- No shared vocabulary for marker semantics (priority, inheritance, composition)
- Each pattern reinvents discovery logic
2.2 Goals¶
-
Clear Terminology โ Establish "marker" as the unambiguous term for runtime metadata, eliminating confusion with Python's type annotation system
-
Unified Architecture โ Define a cohesive internal design that clearly separates:
- Common marker infrastructure (storage, registry, discovery)
- Callable-specific concerns (decorator handling, function wrapping)
-
Attribute-specific concerns (type hint inspection,
Annotated[]extraction) -
Consistent Discovery API โ Provide a single, type-safe interface for discovering both callable and attribute markers without branching logic in user code
-
Complete Framework Migration โ Update every framework module, starting with
loryth.common.di, to use the new marker system directly without compatibility layers -
Extensibility โ Create clear extension points for new marker types, handlers, and discovery strategies without modifying core infrastructure
2.3 Out of Scope¶
- Marker inheritance from parent classes or mixins (future consideration)
- Conditional marker activation based on runtime context (future feature)
- Marker composition/conflict resolution logic (future consideration)
- Marker validation or enforcement (separate concern for framework validators)
- Metadata serialization or export (future tooling concern)
- IDE/tooling integration (future enhancement)
2.4 Assumptions and Constraints¶
- No compatibility shim will ship; all existing references must be updated before release
- DI system (
loryth.common.di) must update every decorator, injector, and helper to import from the new module as part of this effort - Reflection system provides type hint inspection capabilities
Annotated[]is available (Python 3.9+; we use 3.13)- Handler-based architecture from current implementation is sound and should be preserved
- No external dependencies for marker infrastructure (only Python stdlib and existing loryth libraries)
๐งฉ 3. Design Overview¶
3.1 Conceptual Model¶
A marker is a typed container for runtime metadata that can be attached to two classes of targets:
- Callables โ Functions, methods, classes (attached via decorators)
- Attributes โ Class attributes (attached via
Annotated[Type, Marker()])
Both attachment patterns use handlers to manage the lifecycle: - attach(target, marker) โ Store marker on target - retrieve(target) โ Get all markers from target - supports(target) โ Check if handler applies
A unified marker context coordinates handlers and exposes a single discovery API that works for both patterns.
3.2 Architecture Diagram¶
graph TB
subgraph User["User Code"]
Dec["@marker() Decorator"] --> CallableTarget["Class/Function/Method"]
Ann["Annotated[T, Marker()]"] --> Attr["Attribute"]
end
subgraph Core["Core Infrastructure"]
Marker["Marker<br/>(name property)"]
MarkerContext["MarkerContext<br/>(handler registry)"]
end
subgraph Handlers["Handler Layer"]
FullName["Full Target Support"]
CallableH["CallableMarkerHandler<br/>(Protocol)"]
AttributeH["AttributeMarkerHandler<br/>(Protocol)"]
FuncH["FunctionHandler"]
ClassH["ClassHandler"]
MethodH["MethodHandler"]
AttrH["AnnotatedAttributeHandler"]
CallableH --> FuncH
CallableH --> ClassH
CallableH --> MethodH
AttributeH --> AttrH
end
subgraph Discovery["Discovery API"]
FindMarker["find_marker(target, type)"]
FindAllMarkers["find_markers(target, type)"]
HasMarker["has_marker(target, type)"]
end
User --> Core
Core --> Handlers
Handlers --> Discovery
classDef userCode fill:none,stroke:#1976D2,stroke-width:2px,color:#1976D2
classDef coreInfra fill:none,stroke:#00C853,stroke-width:2px,color:#00C853
classDef handlerLayer fill:none,stroke:#C2185B,stroke-width:2px,color:#C2185B
classDef discoveryLayer fill:none,stroke:#8E24AA,stroke-width:2px,color:#8E24AA
class Dec,Ann,CallableTarget,Attr userCode
class Marker,MarkerContext coreInfra
class CallableH,AttributeH,FuncH,ClassH,MethodH,AttrH handlerLayer
class FindMarker,FindAllMarkers,HasMarker discoveryLayer 3.3 Key Components¶
| Component | Purpose |
|---|---|
Marker | Base class for all runtime metadata; minimal contract (name property) |
MarkerContext | Global registry managing all marker handlers and providing unified discovery API |
CallableMarkerHandler | Protocol for handlers targeting functions, methods, classes |
AttributeMarkerHandler | Protocol for handlers targeting class attributes via Annotated[] |
FunctionHandler, ClassHandler, MethodHandler | Concrete implementations of CallableMarkerHandler |
AnnotatedAttributeHandler | Concrete implementation of AttributeMarkerHandler |
| Discovery API | find_marker(), find_markers(), has_marker() โ unified interface for both target types |
3.4 Interface Impact (Before โ After)¶
| Area | Before | After | Notes |
|---|---|---|---|
| Module Import | from loryth.common.annotations import Annotation, AnnotationContext | from loryth.common.markers import Marker, MarkerContext | Update every import, including all loryth.common.di modules; no compatibility shim available |
| Base Class | class MyAnnotation(Annotation) | class MyMarker(Marker) | Semantic clarity; direct replacement with updated inheritance |
| Discovery | Multiple APIs: find_annotation(), get_annotations(), has_annotation() | Unified: find_marker(), find_markers(), has_marker() | Simpler mental model; less API surface |
| Attribute Markers | Direct Annotated[] usage with manual get_origin()/get_args() inspection | Native find_marker((Class, 'attr_name'), MarkerType) | No more manual type hint inspection in user code |
3.5 Adoption Policy¶
loryth.common.annotationsis removed as part of this work; all code must import fromloryth.common.markersbefore release.- Every
loryth.common.didecorator, injector, and helper is updated during implementation so runtime behavior immediately reflects the new system. - Documentation and examples should reference only the new module, ensuring no lingering references to annotations remain in published materials.
๐ 4. Implementation Details¶
4.1 Core Contracts & Protocols¶
4.1.1 Marker Base Class¶
from typing import Any
class Marker:
"""Base class for all runtime metadata markers.
A marker is a typed container for runtime metadata that can be attached to
functions, classes, methods, or class attributes. Subclasses should define
domain-specific metadata fields relevant to their use case.
:param name: Human-readable name for this marker type.
:type name: str
Example::
class ServiceMarker(Marker):
def __init__(self, lifecycle: ServiceLifecycle) -> None:
super().__init__(name="service")
self.lifecycle = lifecycle
"""
_name: str
def __init__(self, name: str) -> None:
"""Initialize marker with a name.
:param name: Identifier for the marker type.
:type name: str
:raises ValueError: If name is empty or whitespace-only.
"""
if not name or not name.strip():
msg = "Marker name cannot be empty."
raise ValueError(msg)
self._name = name.strip()
@property
def name(self) -> str:
"""Return the marker name.
:return: Name assigned during initialization.
:rtype: str
"""
return self._name
def __repr__(self) -> str:
"""Return a debug representation of the marker."""
return f"{self.__class__.__name__}(name='{self._name}')"
4.1.2 Target Types¶
from typing import Any
from typing import Callable
# Any callable: function, method, class
CallableTarget = type | Callable[..., Any]
# Attribute target: tuple of (Class, attribute_name)
AttributeTarget = tuple[type, str]
# Union of all supported targets
MarkerTarget = CallableTarget | AttributeTarget
4.1.3 Marker Handler Protocols¶
from abc import ABC
from abc import abstractmethod
from collections.abc import Iterator
from typing import Protocol
class CallableMarkerHandler(ABC):
"""Abstract handler for attaching/retrieving markers on callables.
Implementations handle specific callable types: functions, methods, classes.
"""
@abstractmethod
def supports(self, target: CallableTarget) -> bool:
"""Check if this handler supports the target.
:param target: The callable to check.
:type target: CallableTarget
:return: True if handler can manage this target.
:rtype: bool
"""
...
@abstractmethod
def attach(self, target: CallableTarget, marker: Marker) -> None:
"""Attach a marker to the target.
:param target: The callable to mark.
:type target: CallableTarget
:param marker: The marker to attach.
:type marker: Marker
"""
...
@abstractmethod
def retrieve(self, target: CallableTarget) -> Iterator[Marker]:
"""Retrieve all markers from the target.
:param target: The callable to inspect.
:type target: CallableTarget
:return: Iterator of markers attached to the target.
:rtype: Iterator[Marker]
"""
...
def has_marker(
self, target: CallableTarget, marker_type: type[Marker]
) -> bool:
"""Check if target has a marker of the given type.
:param target: The callable to check.
:type target: CallableTarget
:param marker_type: The marker class to search for.
:type marker_type: type[Marker]
:return: True if a marker of this type is attached.
:rtype: bool
"""
return any(
isinstance(marker, marker_type) for marker in self.retrieve(target)
)
def find_marker[T: Marker](
self, target: CallableTarget, marker_type: type[T]
) -> T | None:
"""Find the first marker of the given type.
:param target: The callable to inspect.
:type target: CallableTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: The first matching marker, or None.
:rtype: T | None
"""
for marker in self.retrieve(target):
if isinstance(marker, marker_type):
return marker
return None
def find_markers[T: Marker](
self, target: CallableTarget, marker_type: type[T]
) -> Iterator[T]:
"""Find all markers of the given type.
:param target: The callable to inspect.
:type target: CallableTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: Iterator of matching markers.
:rtype: Iterator[T]
"""
for marker in self.retrieve(target):
if isinstance(marker, marker_type):
yield marker
class AttributeMarkerHandler(ABC):
"""Abstract handler for retrieving markers on class attributes.
Attribute markers are attached via Annotated[Type, Marker()]. This handler
inspects __annotations__ and extracts Marker instances from Annotated metadata.
"""
@abstractmethod
def supports(self, target: AttributeTarget) -> bool:
"""Check if this handler supports the attribute target.
:param target: Tuple of (Class, attribute_name).
:type target: AttributeTarget
:return: True if handler can manage this target.
:rtype: bool
"""
...
@abstractmethod
def retrieve(self, target: AttributeTarget) -> Iterator[Marker]:
"""Retrieve all markers from the attribute.
:param target: Tuple of (Class, attribute_name).
:type target: AttributeTarget
:return: Iterator of markers attached to the attribute.
:rtype: Iterator[Marker]
"""
...
def has_marker(
self, target: AttributeTarget, marker_type: type[Marker]
) -> bool:
"""Check if attribute has a marker of the given type.
:param target: Tuple of (Class, attribute_name).
:type target: AttributeTarget
:param marker_type: The marker class to search for.
:type marker_type: type[Marker]
:return: True if a marker of this type is attached.
:rtype: bool
"""
return any(
isinstance(marker, marker_type) for marker in self.retrieve(target)
)
def find_marker[T: Marker](
self, target: AttributeTarget, marker_type: type[T]
) -> T | None:
"""Find the first marker of the given type on the attribute.
:param target: Tuple of (Class, attribute_name).
:type target: AttributeTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: The first matching marker, or None.
:rtype: T | None
"""
for marker in self.retrieve(target):
if isinstance(marker, marker_type):
return marker
return None
def find_markers[T: Marker](
self, target: AttributeTarget, marker_type: type[T]
) -> Iterator[T]:
"""Find all markers of the given type on the attribute.
:param target: Tuple of (Class, attribute_name).
:type target: AttributeTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: Iterator of matching markers.
:rtype: Iterator[T]
"""
for marker in self.retrieve(target):
if isinstance(marker, marker_type):
yield marker
4.2 Marker Context & Discovery¶
from typing import Any
class MarkerContext:
"""Central registry managing all marker handlers.
Provides a unified API for attaching and discovering markers on both
callable targets (classes, functions, methods) and attribute targets
(class attributes via Annotated[]).
"""
__slots__ = ("_callable_handlers", "_attribute_handlers")
def __init__(self) -> None:
"""Initialize with empty handler registries."""
self._callable_handlers: list[CallableMarkerHandler] = []
self._attribute_handlers: list[AttributeMarkerHandler] = []
def register_callable_handler(
self, handler: CallableMarkerHandler
) -> "MarkerContext":
"""Register a handler for callable targets.
:param handler: Handler to register.
:type handler: CallableMarkerHandler
:return: Self for method chaining.
:rtype: MarkerContext
"""
self._callable_handlers.append(handler)
return self
def register_attribute_handler(
self, handler: AttributeMarkerHandler
) -> "MarkerContext":
"""Register a handler for attribute targets.
:param handler: Handler to register.
:type handler: AttributeMarkerHandler
:return: Self for method chaining.
:rtype: MarkerContext
"""
self._attribute_handlers.append(handler)
return self
def attach(self, target: MarkerTarget, marker: Marker) -> None:
"""Attach a marker to a target.
:param target: Callable or (Class, attribute_name) tuple.
:type target: MarkerTarget
:param marker: The marker to attach.
:type marker: Marker
:raises TypeError: If target type is not supported.
"""
if isinstance(target, tuple):
handler = self._find_attribute_handler(target)
# Attribute handler retrieve only; attach is implicit via type annotation
msg = "Attribute markers are attached via Annotated[]; use type hints to define."
raise TypeError(msg)
else:
handler = self._find_callable_handler(target)
handler.attach(target, marker)
def has_marker(
self, target: MarkerTarget, marker_type: type[Marker]
) -> bool:
"""Check if target has a marker of the given type.
:param target: Callable or (Class, attribute_name) tuple.
:type target: MarkerTarget
:param marker_type: The marker class to search for.
:type marker_type: type[Marker]
:return: True if target has the marker.
:rtype: bool
"""
if isinstance(target, tuple):
handler = self._find_attribute_handler(target)
return handler.has_marker(target, marker_type)
else:
handler = self._find_callable_handler(target)
return handler.has_marker(target, marker_type)
def find_marker[T: Marker](
self, target: MarkerTarget, marker_type: type[T]
) -> T | None:
"""Find the first marker of the given type on target.
Works for both callables and attributes::
# Callable target
service = find_marker(MyService, ServiceMarker)
# Attribute target
autowired = find_marker((MyService, 'database'), Autowired)
:param target: Callable or (Class, attribute_name) tuple.
:type target: MarkerTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: The first matching marker, or None.
:rtype: T | None
"""
if isinstance(target, tuple):
handler = self._find_attribute_handler(target)
return handler.find_marker(target, marker_type)
else:
handler = self._find_callable_handler(target)
return handler.find_marker(target, marker_type)
def find_markers[T: Marker](
self, target: MarkerTarget, marker_type: type[T]
) -> Iterator[T]:
"""Find all markers of the given type on target.
:param target: Callable or (Class, attribute_name) tuple.
:type target: MarkerTarget
:param marker_type: The marker class to find.
:type marker_type: type[T]
:return: Iterator of matching markers.
:rtype: Iterator[T]
"""
if isinstance(target, tuple):
handler = self._find_attribute_handler(target)
return handler.find_markers(target, marker_type)
else:
handler = self._find_callable_handler(target)
return handler.find_markers(target, marker_type)
def _find_callable_handler(
self, target: CallableTarget
) -> CallableMarkerHandler:
"""Find a handler supporting the callable target.
:param target: Callable to find handler for.
:type target: CallableTarget
:return: Supporting handler.
:rtype: CallableMarkerHandler
:raises TypeError: If no handler supports target.
"""
for handler in self._callable_handlers:
if handler.supports(target):
return handler
msg = f"No marker handler for callable type: {type(target)}"
raise TypeError(msg)
def _find_attribute_handler(
self, target: AttributeTarget
) -> AttributeMarkerHandler:
"""Find a handler supporting the attribute target.
:param target: Attribute tuple (Class, name) to find handler for.
:type target: AttributeTarget
:return: Supporting handler.
:rtype: AttributeMarkerHandler
:raises TypeError: If no handler supports target.
"""
for handler in self._attribute_handlers:
if handler.supports(target):
return handler
msg = f"No marker handler for attribute: {target}"
raise TypeError(msg)
4.3 Concrete Handler Implementations¶
4.3.1 Callable Handlers¶
from types import FunctionType
from types import MethodType
class FunctionMarkerHandler(CallableMarkerHandler):
"""Handler for attaching/retrieving markers on functions."""
def supports(self, target: CallableTarget) -> bool:
"""Check if target is a function."""
return isinstance(target, FunctionType)
def attach(self, target: CallableTarget, marker: Marker) -> None:
"""Attach marker to function via __loryth_markers__ attribute."""
assert isinstance(target, FunctionType)
markers = getattr(target, "__loryth_markers__", None)
if markers is None:
markers = []
setattr(target, "__loryth_markers__", markers)
assert isinstance(markers, list)
markers.append(marker)
def retrieve(self, target: CallableTarget) -> Iterator[Marker]:
"""Retrieve markers from function, including from wrapped functions."""
current: CallableTarget | None = target
while current is not None:
markers = getattr(current, "__loryth_markers__", None)
if markers is not None:
yield from markers
current = getattr(current, "__wrapped__", None)
class ClassMarkerHandler(CallableMarkerHandler):
"""Handler for attaching/retrieving markers on classes."""
def supports(self, target: CallableTarget) -> bool:
"""Check if target is a class."""
return isinstance(target, type)
def attach(self, target: CallableTarget, marker: Marker) -> None:
"""Attach marker to class via __loryth_markers__ attribute."""
markers = getattr(target, "__loryth_markers__", None)
if markers is None:
markers = []
setattr(target, "__loryth_markers__", markers)
assert isinstance(markers, list)
markers.append(marker)
def retrieve(self, target: CallableTarget) -> Iterator[Marker]:
"""Retrieve markers from class."""
markers = getattr(target, "__loryth_markers__", None)
if markers is not None:
yield from markers
class MethodMarkerHandler(CallableMarkerHandler):
"""Handler for attaching/retrieving markers on methods."""
def supports(self, target: CallableTarget) -> bool:
"""Check if target is a method."""
return isinstance(target, MethodType)
def attach(self, target: CallableTarget, marker: Marker) -> None:
"""Attach marker to method via __loryth_markers__ attribute."""
assert isinstance(target, MethodType)
markers = getattr(target, "__loryth_markers__", None)
if markers is None:
markers = []
setattr(target, "__loryth_markers__", markers)
assert isinstance(markers, list)
markers.append(marker)
def retrieve(self, target: CallableTarget) -> Iterator[Marker]:
"""Retrieve markers from method."""
markers = getattr(target, "__loryth_markers__", None)
if markers is not None:
yield from markers
4.3.2 Attribute Handler¶
from typing import Any
from typing import Annotated
from typing import get_args
from typing import get_origin
class AnnotatedAttributeMarkerHandler(AttributeMarkerHandler):
"""Handler for retrieving markers from class attributes via Annotated[]."""
def supports(self, target: AttributeTarget) -> bool:
"""Check if target is a valid (Class, attribute_name) tuple."""
if not isinstance(target, tuple) or len(target) != 2:
return False
cls, attr_name = target
return isinstance(cls, type) and isinstance(attr_name, str)
def retrieve(self, target: AttributeTarget) -> Iterator[Marker]:
"""Extract Marker instances from Annotated[] metadata.
If the attribute is Annotated[Type, Meta1, Meta2, ...], yields all Meta
entries that are Marker instances.
"""
cls, attr_name = target
annotations = getattr(cls, "__annotations__", {})
annotation = annotations.get(attr_name)
if annotation is None:
return
if get_origin(annotation) is not Annotated:
return
args = get_args(annotation)
if not args:
return
# First arg is the type; rest are metadata
for meta in args[1:]:
if isinstance(meta, Marker):
yield meta
4.4 Global Marker Context¶
# Create and configure global context with default handlers
_default_context = MarkerContext()
_default_context.register_callable_handler(FunctionMarkerHandler())
_default_context.register_callable_handler(ClassMarkerHandler())
_default_context.register_callable_handler(MethodMarkerHandler())
_default_context.register_attribute_handler(AnnotatedAttributeMarkerHandler())
# Public API
def attach(target: MarkerTarget, marker: Marker) -> None:
"""Attach a marker to a target."""
_default_context.attach(target, marker)
def has_marker(
target: MarkerTarget, marker_type: type[Marker]
) -> bool:
"""Check if target has a marker of the given type."""
return _default_context.has_marker(target, marker_type)
def find_marker[T: Marker](
target: MarkerTarget, marker_type: type[T]
) -> T | None:
"""Find the first marker of the given type on target."""
return _default_context.find_marker(target, marker_type)
def find_markers[T: Marker](
target: MarkerTarget, marker_type: type[T]
) -> Iterator[T]:
"""Find all markers of the given type on target."""
return _default_context.find_markers(target, marker_type)
4.5 Decorator Helper¶
from typing import Callable
from typing import TypeVar
T = TypeVar("T")
class MarkerFactory[**Params]:
"""Protocol for factories that create markers from decorator arguments."""
def __call__(self, *args: Params.args, **kwargs: Params.kwargs) -> Marker:
"""Create a marker instance from decorator parameters."""
...
def marker[T: Callable[..., Any], **Params](
marker_factory: MarkerFactory[Params],
) -> Callable[Params, Callable[[T], T]]:
"""Create a decorator that attaches a marker to a callable.
:param marker_factory: Callable that creates a Marker from decorator params.
:type marker_factory: MarkerFactory[Params]
:return: Decorator factory accepting marker parameters.
:rtype: Callable[Params, Callable[[T], T]]
Example::
def service_marker(*, lifecycle: ServiceLifecycle = SINGLETON) -> Marker:
return ServiceMarker(lifecycle)
@marker(service_marker)
def service(*, lifecycle: ServiceLifecycle = SINGLETON):
def decorator(cls: type[T]) -> type[T]:
# Apply decorator logic
return cls
return decorator
"""
def decorator(*args: Params.args, **kwargs: Params.kwargs) -> Callable[[T], T]:
marker_instance = marker_factory(*args, **kwargs)
def wrapper(target: T) -> T:
attach(target, marker_instance)
return target
return wrapper
return decorator
โ๏ธ 5. Semantics¶
5.1 Marker Attachment & Retrieval¶
- Callable markers are stored in
__loryth_markers__attribute (list of Marker instances) - Attribute markers are discovered by inspecting
__annotations__and extractingMarkerinstances fromAnnotated[]metadata - Both patterns support multiple markers per target
- Marker order is preserved (first attached = first retrieved)
5.2 Handler Resolution¶
- When attaching to a callable, the first handler that returns
supports(target) == Trueis used - Handler registration order matters; most specific handlers should be registered first
- If no handler supports a target,
TypeErroris raised with a descriptive message - Attribute target must be a 2-tuple
(Class, str)where the second element is a valid attribute name
5.3 Marker Discovery¶
find_marker(target, type)returns the first matching marker orNonefind_markers(target, type)returns an iterator (lazy evaluation)has_marker(target, type)checks existence without creating iterator overhead- Discovery works uniformly for both callable and attribute targets via single API
5.4 Attribute Marker Rules¶
- Attribute markers must be part of type annotations (attached via
Annotated[]) - Markers are discovered dynamically from
__annotations__at query time (not materialized) - If an attribute is not annotated with
Annotated[], no markers are found (graceful) - Multiple markers can be attached to one attribute by including them all in
Annotated[]
5.5 Wrapped Function Support¶
- For callable markers on functions, the retrieval traverses
__wrapped__chain (respects Python's decorator protocol) - This enables markers to survive function wrapping by decorators
- Methods and classes do not traverse
__wrapped__(not applicable)
๐งช 6. Examples¶
6.1 Basic Callable Marker¶
from loryth.common.markers import Marker
from loryth.common.markers import attach
from loryth.common.markers import find_marker
class ServiceMarker(Marker):
"""Marks a class as a service."""
def __init__(self, lifecycle: str = "singleton") -> None:
super().__init__(name="service")
self.lifecycle = lifecycle
# Attach marker manually
class UserRepository:
pass
attach(UserRepository, ServiceMarker())
# Discover marker
marker = find_marker(UserRepository, ServiceMarker)
assert marker is not None
assert marker.lifecycle == "singleton"
6.2 Decorator-Based Marker Attachment¶
from typing import Callable
from typing import TypeVar
from loryth.common.markers import Marker
from loryth.common.markers import attach
T = TypeVar("T")
class ComponentMarker(Marker):
"""Marks a class as a component."""
def __init__(self, scope: str = "prototype") -> None:
super().__init__(name="component")
self.scope = scope
def component(*, scope: str = "prototype") -> Callable[[type[T]], type[T]]:
"""Decorator to mark a class as a component."""
def decorator(cls: type[T]) -> type[T]:
attach(cls, ComponentMarker(scope=scope))
return cls
return decorator
@component(scope="singleton")
class EmailService:
pass
marker = find_marker(EmailService, ComponentMarker)
assert marker.scope == "singleton"
6.3 Attribute Marker via Annotated¶
from typing import Annotated
from loryth.common.markers import Marker
from loryth.common.markers import find_marker
class InjectedMarker(Marker):
"""Marks an attribute as requiring injection."""
def __init__(self, qualifier: str | None = None) -> None:
super().__init__(name="injected")
self.qualifier = qualifier
class DatabaseService:
"""Database service to be injected."""
pass
class UserService:
"""Service with injected dependency."""
# Mark dependency with marker via Annotated
db: Annotated[DatabaseService, InjectedMarker()]
cache: Annotated[DatabaseService, InjectedMarker(qualifier="redis")]
# Discover attribute markers
db_marker = find_marker((UserService, "db"), InjectedMarker)
assert db_marker is not None
assert db_marker.qualifier is None
cache_marker = find_marker((UserService, "cache"), InjectedMarker)
assert cache_marker is not None
assert cache_marker.qualifier == "redis"
6.4 Integration with DI System¶
from typing import Annotated
from loryth.common.markers import Marker
from loryth.common.markers import attach
from loryth.common.markers import find_marker
class ServiceMarker(Marker):
"""DI service marker with lifecycle."""
def __init__(self, lifecycle: str = "singleton") -> None:
super().__init__(name="service")
self.lifecycle = lifecycle
class AutowiredMarker(Marker):
"""Marks an attribute for property injection."""
def __init__(self, qualifier: str | None = None) -> None:
super().__init__(name="autowired")
self.qualifier = qualifier
# Define services with markers
class Logger:
pass
class Database:
pass
attach(Logger, ServiceMarker())
attach(Database, ServiceMarker())
class UserService:
"""Service with injected dependencies."""
logger: Annotated[Logger, AutowiredMarker()]
db: Annotated[Database, AutowiredMarker()]
def __init__(self) -> None:
self.initialized = True
# Mark as service
attach(UserService, ServiceMarker(lifecycle="singleton"))
# DI framework can now discover both class-level and attribute-level markers
service_marker = find_marker(UserService, ServiceMarker)
assert service_marker.lifecycle == "singleton"
logger_marker = find_marker((UserService, "logger"), AutowiredMarker)
assert logger_marker is not None
๐งช 7. Test Matrix¶
7.1 Unit Coverage¶
File Organization:
tests/unit/markers/
โโโ test_marker_base.py # Marker class behavior
โโโ test_callable_handlers.py # Function/Class/Method handlers
โโโ test_attribute_handler.py # Annotated attribute handling
โโโ test_marker_context.py # Registry and discovery API
โโโ test_marker_decorator.py # @marker() decorator helper
Coverage Requirements: - Marker name validation (empty string, whitespace) - Each handler's supports() check for its target type - Attach/retrieve lifecycle for each handler type - Discovery API: find_marker(), find_markers(), has_marker() - Wrapped function traversal (follow __wrapped__) - Error handling: unsupported target types, missing attributes - Attribute marker extraction from Annotated[] with multiple metadata - Type safety with generic marker type parameters
7.2 Integration Coverage¶
File Organization:
tests/integration/markers/
โโโ test_callable_and_attribute_markers.py # Mixed usage
โโโ test_marker_discovery_workflows.py # End-to-end discovery
โโโ test_di_marker_integration.py # DI system exercising new module imports
Coverage Requirements: - Attaching markers to multiple target types in same test - Discovering both callable and attribute markers uniformly - Multiple markers per target - Integration with existing DI decorators (@service, @bean) - AutowiredInjector using unified marker discovery - DI container registration driven by markers - Verification that DI flows import exclusively from loryth.common.markers
Testing Notes: - Unit tests use lightweight fixtures for markers and handlers - Integration tests verify discovery across component boundaries - All tests include type hints and descriptive docstrings - Maximum 20 test functions per file per convention - No manual mocking of handlers; use real implementations - Regression suites are not required; limit coverage to unit and integration tests that demonstrate the new behavior
โ 8. Acceptance Criteria¶
Use this checklist to verify implementation conformance:
- New module
loryth.common.markerscreated with all components -
Markerbase class implemented with name validation -
CallableMarkerHandlerandAttributeMarkerHandlerprotocols defined - Concrete handlers for Function, Class, Method, and Annotated Attribute implemented
-
MarkerContextregistry with unified discovery API (find_marker,find_markers,has_marker) -
marker()decorator helper for creating decorators - Public module API complete (
__all__, clear exports) - All unit tests passing (at least 20 test functions)
- Integration tests validate callable and attribute marker discovery
- DI decorators (
@service,@component,@bean, etc.) updated to new module -
AutowiredInjectorupdated to use unified marker discovery - All
loryth.common.dimodules verified to import exclusively fromloryth.common.markers - Documentation updated with new terminology and examples
- DI public API behavior validated after direct migration
๐ 9. Rollout and Migration¶
9.1 Implementation Phases¶
Phase 1: Foundation (Module & Contracts)¶
- Create
loryth.common.markersmodule with coreMarkerclass - Define
CallableMarkerHandlerandAttributeMarkerHandlerprotocols - Implement
MarkerContextregistry and unified discovery API - Add
marker()decorator helper - Outcome: Core infrastructure ready for adoption
Phase 2: Handlers & Context (Concrete Implementations)¶
- Implement concrete handlers (Function, Class, Method, Annotated Attribute)
- Integrate handlers into global
MarkerContext - Add comprehensive unit tests (20+ functions)
- Outcome: Full marker system operational for new code
Phase 3: DI Integration & Repository Migration¶
- Update all DI decorators (
@service,@component,@singleton,@transient,@configuration,@bean) to use the new module - Update
AutowiredInjectorand any DI helpers to use unified marker discovery - Replace every remaining
loryth.common.annotationsimport across the repository, removing the old module entirely - Verify all DI-focused integration tests still pass
- Outcome: Repository imports exclusively from
loryth.common.markers
9.2 Migration Strategy¶
For Framework Maintainers: 1. Build the marker infrastructure (Phases 1-2) behind feature branches, ensuring tests cover new behavior. 2. Execute the migration sweep (Phase 3) to update every import, delete loryth.common.annotations, and confirm DI flows operate correctly. 3. Ship once integration tests verify the absence of loryth.common.annotations references and DI behavior matches expectations.
For Framework Users:
# BEFORE (old way)
from loryth.common.annotations import Annotation, AnnotationContext
class MyAnnotation(Annotation):
pass
# AFTER (new way)
from loryth.common.markers import Marker, attach, find_marker
class MyMarker(Marker):
pass
For DI Users: Existing decorators within loryth.common.di continue to be the integration surface, but their implementation now relies solely on loryth.common.markers. Update any custom DI extensions that previously referenced Annotation or AnnotationContext to the new Marker and MarkerContext interfaces.
# Updated DI decorators now rely on the markers module
from loryth.common.di.annotations import service
from loryth.common.di.annotations import Autowired
# Internally, these use the new marker system exclusively
@service()
class MyService:
pass
โ ๏ธ 10. Risks and Mitigations¶
| Risk | Mitigation |
|---|---|
| Namespace migration complexity โ Moving code to new module could cause import errors if dependencies are missed | Perform a repository-wide dependency audit before deleting the old module; block merge until CI confirms no imports from loryth.common.annotations remain |
Attribute marker discovery performance โ Reflecting over __annotations__ on every query could be slow for large classes | Lazy evaluation via iterators; consider caching layer in future if profiling shows bottleneck; maintain O(1) lookup for callable markers |
| Incomplete DI migration โ Some DI paths might still reference annotation APIs | Add a focused migration audit covering all loryth.common.di packages; require integration tests to import markers directly and fail fast if old names appear |
| Type safety with generic markers โ Marker type inference could fail in some scenarios | Use PEP 695 generic syntax (Marker[T] or find_marker[T]()); test with mypy strict mode; provide clear examples in documentation |
| Handler extension difficulty โ New use cases (validation, serialization, etc.) may need custom handlers | Document handler protocol clearly with examples; provide template implementation; consider helper base class for common patterns |
| Adoption resistance โ Users may hesitate to migrate from familiar "annotation" terminology | Provide clear rationale in documentation; emphasize benefits (clarity, consistency); update training materials and code samples to the new terminology |
๐ 11. Progress¶
Legend: โณ Pending ยท ๐ง In Progress ยท โ Completed
Phase 1 โ Foundation - โ
Marker base class implemented - โ
CallableMarkerHandler and AttributeMarkerHandler protocols defined - โ
MarkerContext registry skeleton created - โ
Public API exports defined
Phase 2 โ Handlers & Context - โ
Concrete handler implementations (Function, Class, Method, Attribute) - โ
Global MarkerContext initialized with handlers - โ
Discovery API fully implemented - โ
marker() decorator helper created - โ
Unit tests written (26 test functions)
Phase 3 โ DI Integration & Migration - โ
DI decorators updated to use new module - โ
Attribute markers (Autowired, Config) validated as correctly implemented dataclasses - โ
loryth.common.annotations module removed from repository - โ
All DI-focused integration tests passing (702/702 tests) - โ
Zero references to old annotation system in codebase
๐ 12. Changelog¶
- 2025-10-18 โ Initial technical design proposal created
- 2025-10-18 โ Phase 1 & 2 implementation complete (26 unit tests, 702 integration tests passing)
- 2025-10-18 โ Phase 3 implementation complete (DI migration, old module removed, all tests passing)
- 2025-10-18 โ TD-022 fully implemented and deployed to production codebase
๐ 13. Glossary¶
| Term | Definition |
|---|---|
| Marker | Typed container for runtime metadata; replaces "Annotation" to avoid confusion with Python type hints |
| Callable Target | Function, method, or class that can have markers attached |
| Attribute Target | Class attribute (specified as tuple (Class, attribute_name)) that can have markers via Annotated[] |
| Handler | Component responsible for attaching/retrieving markers for a specific target type |
| Discovery API | Unified interface for querying markers on any target type (find_marker, find_markers, has_marker) |
| Marker Context | Central registry coordinating all handlers and providing the unified discovery API |
| Marker Factory | Callable that creates a Marker instance from decorator parameters (used with marker() helper) |
| Annotated | Python's typing.Annotated[T, meta...] syntax for attaching metadata to type hints |
| Migration Audit | Repository-wide verification ensuring all imports point to loryth.common.markers and old modules are removed |