Skip to content

๐Ÿ“˜ 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.markers to 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.di imports 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:

  1. Callable Markers โ€” Applied to classes, functions, and methods via decorators (e.g., @service(), @bean())
  2. 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

  1. Terminology Confusion โ€” The term "annotation" conflates with Python's @decorator syntax and type hints, creating ambiguity:
  2. "Annotation" in Python typically means __annotations__ dict (type hints)
  3. Our system attaches runtime metadata separately from type hints
  4. This semantic overlap confuses newcomers and makes code intent unclear

  5. Inconsistent API Surface โ€” The system supports two distinct patterns:

  6. Callable markers use decorators and a handler-based registry
  7. Attribute markers use Annotated[] and require reflection over __annotations__
  8. No unified discovery mechanism (users must know which pattern applies)

  9. Architectural Ambiguity โ€” It's unclear what aspects are common and what's pattern-specific:

  10. Both store metadata, but via different mechanisms
  11. Both support discovery, but with different APIs
  12. New contributors must study both patterns to add features

  13. Scalability Concerns โ€” As new marker types emerge (validation, serialization, observability), the architecture doesn't scale cleanly:

  14. No clear extension points
  15. No shared vocabulary for marker semantics (priority, inheritance, composition)
  16. Each pattern reinvents discovery logic

2.2 Goals

  1. Clear Terminology โ€” Establish "marker" as the unambiguous term for runtime metadata, eliminating confusion with Python's type annotation system

  2. Unified Architecture โ€” Define a cohesive internal design that clearly separates:

  3. Common marker infrastructure (storage, registry, discovery)
  4. Callable-specific concerns (decorator handling, function wrapping)
  5. Attribute-specific concerns (type hint inspection, Annotated[] extraction)

  6. Consistent Discovery API โ€” Provide a single, type-safe interface for discovering both callable and attribute markers without branching logic in user code

  7. Complete Framework Migration โ€” Update every framework module, starting with loryth.common.di, to use the new marker system directly without compatibility layers

  8. 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:

  1. Callables โ€” Functions, methods, classes (attached via decorators)
  2. 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.annotations is removed as part of this work; all code must import from loryth.common.markers before release.
  • Every loryth.common.di decorator, 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 extracting Marker instances from Annotated[] 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) == True is used
  • Handler registration order matters; most specific handlers should be registered first
  • If no handler supports a target, TypeError is 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 or None
  • find_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.markers created with all components
  • Marker base class implemented with name validation
  • CallableMarkerHandler and AttributeMarkerHandler protocols defined
  • Concrete handlers for Function, Class, Method, and Annotated Attribute implemented
  • MarkerContext registry 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
  • AutowiredInjector updated to use unified marker discovery
  • All loryth.common.di modules verified to import exclusively from loryth.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.markers module with core Marker class
  • Define CallableMarkerHandler and AttributeMarkerHandler protocols
  • Implement MarkerContext registry 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 AutowiredInjector and any DI helpers to use unified marker discovery
  • Replace every remaining loryth.common.annotations import 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