Dependencies and signatures
ReflectDependencies turns the parameters of a constructor, method or callable into Argument metadata, grouped in a Signature. Use it to analyze handlers, build diagnostics or understand how Orionis prepares an invocation.
Reflection does not instantiate services, execute the callable or validate its payload. The resolved flag describes a parameter’s classification; it does not guarantee that the container can construct its value. For routine injection, see Application and Container. File discovery and class loading have their own guide in Module discovery.
Imports and first inspection
Section titled “Imports and first inspection”ReflectDependencies is exported from orionis.introspection. Import the entities and contract from their defining modules:
from orionis.introspection import ReflectDependenciesfrom orionis.introspection.dependencies.contracts.reflection import IReflectDependenciesfrom orionis.introspection.dependencies.entities.argument import Argumentfrom orionis.introspection.dependencies.entities.signature import Signature
class Repository: pass
def handler(repository: Repository, user_id: int, *, limit=25): return repository, user_id, limit
signature = ReflectDependencies(handler).callableSignature()assert isinstance(signature, Signature)assert isinstance(signature.ordered["repository"], Argument)assert list(signature.resolved) == ["repository", "limit"]assert list(signature.unresolved) == ["user_id"]assert list(signature.ordered) == ["repository", "user_id", "limit"]assert signature.ordered["limit"].is_keyword_onlyassert isinstance(ReflectDependencies(handler), IReflectDependencies)All these inspections are synchronous, including when the target is an async def function: reading its signature neither executes it nor requires await.
Parameter classification
Section titled “Parameter classification”Analysis preserves declaration order and applies these rules in sequence:
| Parameter | Classification and metadata |
|---|---|
Named self or cls; *args; **kwargs | Omitted. The names self and cls are also omitted in ordinary functions. |
| No annotation and no default | resolved=False; metadata describes type(typing.Any), not an instance or the typing.Any object. |
| Has a default | resolved=True; type, module and class name come from type(default), even when another type was annotated. |
Type from module builtins, without a default | resolved=False; for example int, str or list[int]. |
| Other annotation without a default | resolved=True; its metadata is preserved. This includes application classes and typing constructs that may not be instantiable. |
is_keyword_only=True identifies only parameters declared after *. Parameters before / and those accepting either position or name are grouped together with False; Argument preserves no further distinction between them.
from orionis.introspection import ReflectDependencies
def options(required: int, /, ordinary: str, *items, count: str = 5, **extras): return required, ordinary, items, count, extras
signature = ReflectDependencies(options).callableSignature()assert list(signature.ordered) == ["required", "ordinary", "count"]assert list(signature.getPositionalOnly()) == ["required", "ordinary"]assert list(signature.getKeywordOnly()) == ["count"]assert signature.resolved["count"].type is intassert signature.resolved["count"].default == 5The count: str annotation does not convert 5 to text. Inspection describes the existing default value.
Forward references, generics and schemas
Section titled “Forward references, generics and schemas”For a string annotation without a default, the resolver attempts evaluation in the callable’s module globals. If resolving all hints at once fails, it tries each reference separately. It replaces the string only when the result is a class object (isinstance(result, type)). It does not have arbitrary local variables from a function that has already returned.
A reference that remains a string uses module_name="typing", the annotation text as class_name, and type=str. It is marked resolved=True, although the container will reject it unless an explicit value is supplied. Parameterized aliases and unions receive no special injection selection: an actual list[int] stays unresolved because its module is builtin; an actual int | None is marked resolved from its metadata, but this does not automatically select an alternative dependency.
is_schema=True is set only when the annotation, without a default, is a class derived from msgspec.Struct; this includes Orionis schemas. It does not perform validation or inspect field constraints. An annotated parameter with a default follows the default-value branch and keeps is_schema=False.
import inspectfrom orionis.introspection import ReflectDependenciesfrom orionis.schemas import Schema
class Repository: pass
class CreateUser(Schema): name: str
def action( payload: CreateUser, repository: "Repository", unknown: "UnavailableService", rows: list[int], optional: Repository = None,): return payload, repository, unknown, rows, optional
signature = ReflectDependencies(action).callableSignature()assert signature.ordered["payload"].is_schemaassert signature.ordered["repository"].type is Repositoryassert signature.ordered["repository"].default is inspect.Parameter.emptyassert signature.ordered["unknown"].type is strassert signature.ordered["unknown"].module_name == "typing"assert signature.ordered["unknown"].resolvedassert not signature.ordered["rows"].resolvedassert signature.ordered["optional"].type is type(None)assert signature.ordered["optional"].default is Noneassert not signature.ordered["optional"].is_schemaReturn annotations are not part of Signature: this component performs no response inference or conversion.
ReflectDependencies API
Section titled “ReflectDependencies API”Construction
Section titled “Construction”ReflectDependencies(target: Any | None = None) -> NoneStores target for later operations without validating or inspecting it yet. It can be a class, instance, function or method depending on the selected operation. The None default allows constructing the wrapper, but does not provide a useful callable target.
constructorSignature
Section titled “constructorSignature”constructorSignature(self) -> SignatureInspects target.__init__ without running the constructor. To describe a class without creating an instance, pass the class itself:
from orionis.introspection import ReflectDependencies
class Report: def __init__(self, title: str, *, attempts=3): self.title = title self.attempts = attempts
signature = ReflectDependencies(Report).constructorSignature()assert list(signature.ordered) == ["title", "attempts"]assert not signature.ordered["title"].resolvedassert signature.ordered["attempts"].default == 3Propagates AttributeError if __init__ cannot be retrieved; inspection failures become ValueError. A callable may have a generic __init__ that does not represent its call: use callableSignature() for functions.
methodSignature
Section titled “methodSignature”methodSignature(self, method_name: str) -> SignatureRetrieves the attribute with getattr(target, method_name) and inspects its signature. A missing name raises AttributeError, rather than a custom reflection exception. An uninspectable attribute raises ValueError; a non-string name may raise TypeError in getattr.
Bound methods omit their first parameter according to Python binding, even if the receiver is not named self. Class methods and static methods preserve their explicit parameters. For an ordinary method accessed on the class, a receiver named something other than self or cls stays in the signature.
from orionis.introspection import ReflectDependencies
class Actions: def handle(receiver, item: int, *, limit=10): return item, limit
@classmethod def byClass(cls, page=1): return page
@staticmethod def byStatic(page=1): return page
bound = ReflectDependencies(Actions()).methodSignature("handle")unbound = ReflectDependencies(Actions).methodSignature("handle")assert list(bound.ordered) == ["item", "limit"]assert list(unbound.ordered) == ["receiver", "item", "limit"]for target in (Actions, Actions()): reflection = ReflectDependencies(target) assert list(reflection.methodSignature("byClass").ordered) == ["page"] assert list(reflection.methodSignature("byStatic").ordered) == ["page"]callableSignature
Section titled “callableSignature”callableSignature(self) -> SignatureChecks callable(target) and analyzes the call signature exposed by Python. Accepts functions, bound methods, classes and callable instances when their signatures can be inspected and the target can be used as a cache key. For a class, it inspects its call; this is not equivalent to explicitly reading __init__ for every metaclass.
from orionis.introspection import ReflectDependencies
async def fetch(item_id: int, *, timeout=5): return item_id, timeout
signature = ReflectDependencies(fetch).callableSignature()assert signature.ordered["item_id"].type is intassert signature.ordered["timeout"].default == 5Raises TypeError for non-callable targets and for unhashable targets when accessing the cache. If inspect.signature cannot describe the signature, raises ValueError with the original cause. Inspecting min, for example, fails because it exposes no inspectable signature in this runtime. Decorators exposing __wrapped__ and custom __signature__ values follow inspect.signature behavior.
Contract and caching
Section titled “Contract and caching”IReflectDependencies declares the three operations above as abstract methods. It cannot be instantiated and is not registered as a native service that automatically selects a target. Direct ReflectDependencies(target) construction makes the inspected target explicit.
Python signatures and dependency metadata use two 1,024-entry LRU caches local to the process. Inspecting the same target returns the same Signature object, even across different wrapper instances. Bound methods use the underlying function and binding mode as the key; those cached methods do not retain their receivers.
ReflectDependencies has no public clearCache(). Changing __annotations__, defaults or __signature__ on the same function after inspection can leave stale metadata. For a different definition, use a new function object or restart the process. Inspection failures are not stored as successful results.
from orionis.introspection import ReflectDependencies
def cached(limit=25): return limit
first = ReflectDependencies(cached).callableSignature()second = ReflectDependencies(cached).callableSignature()assert first is secondArgument entity
Section titled “Argument entity”Argument is a frozen=True, slots=True, kw_only=True dataclass. Its nine fields are keyword arguments to the constructor; the first six are required:
Argument( *, name: str, resolved: bool, module_name: str, class_name: str, type: type[Any], full_class_path: str, is_keyword_only: bool = False, is_schema: bool = False, default: Any | None = None,) -> NoneValidation and mutability
Section titled “Validation and mutability”__post_init__(self) -> NoneRuns automatically when constructing the entity. Checks that module_name, class_name and full_class_path are strings, raising TypeError otherwise. Rejects type=None together with default=None using ValueError. It does not exhaustively validate name, flags, correspondence between paths and classes or all typing objects.
from dataclasses import FrozenInstanceErrorfrom orionis.introspection.dependencies.entities.argument import Argument
argument = Argument( name="limit", resolved=True, module_name="builtins", class_name="int", type=int, full_class_path="builtins.int", default=25,)assert argument.default == 25assert argument.is_keyword_only is Falseassert argument.is_schema is Falsetry: argument.name = "other"except FrozenInstanceError: passelse: raise AssertionError("Argument must reject field reassignment")The dataclass generates representation and field-based equality; hashing works only if all participating values are hashable. A list default does not become immutable inside an Argument. The entity does not inherit BaseEntity: it has no toDict() or getFields() methods of its own.
Argument copies and generated protocols
Section titled “Argument copies and generated protocols”dataclasses.replace() and copy.replace() construct another Argument with changed fields and run its validation again. The dataclass generates __replace__; because it is frozen and uses slots, it also generates __getstate__ and __setstate__ to preserve fields during copying or serialization. These are standard Python protocols, separate from Orionis-implemented methods.
Replacement and copy.copy() are shallow: they retain the mutable default. copy.deepcopy() copies its contents. This example uses a builtin type and a serializable default to also verify pickle:
import copyimport picklefrom dataclasses import replacefrom orionis.introspection.dependencies.entities.argument import Argument
original = Argument( name="tags", resolved=True, module_name="builtins", class_name="list", type=list, full_class_path="builtins.list", default=["reports"],)renamed = replace(original, name="labels")replaced = copy.replace(original, name="categories")assert renamed.name == "labels" and replaced.name == "categories"assert renamed.default is original.defaultassert copy.copy(original).default is original.defaultassert copy.deepcopy(original).default is not original.defaultrestored = pickle.loads(pickle.dumps(original))assert restored == original and restored is not originalassert restored.default is not original.defaultSerialization depends on field values: local classes, non-serializable functions or other defaults may prevent pickle from working.
Construction errors can be handled without inspecting a callable:
from orionis.introspection.dependencies.entities.argument import Argumentfrom orionis.introspection.dependencies.entities.signature import Signature
try: Argument( name="invalid", resolved=False, module_name="builtins", class_name="int", type=None, full_class_path="builtins.int", )except ValueError: passelse: raise AssertionError("A missing type and default must fail")
try: Signature(resolved=[], unresolved={}, ordered={})except TypeError: passelse: raise AssertionError("Signature groups must be dictionaries")Signature entity
Section titled “Signature entity”Signature is a frozen dataclass with keyword arguments. It stores three required dictionaries of Argument objects:
Signature( *, resolved: dict[str, Argument], unresolved: dict[str, Argument], ordered: dict[str, Argument],) -> NoneReflection puts the same Argument instances in ordered and their corresponding group. When constructing Signature manually, the developer provides that consistency.
Signature copies and generated protocols
Section titled “Signature copies and generated protocols”Python generates construction, representation, equality, frozen-field protection and __replace__. An ordinary Signature contains dictionaries, so it is not hashable even though its dataclass generates __hash__. dataclasses.replace() and copy.replace() create another validated entity, but share maps that are not replaced; they do not create an immutable snapshot.
import copyimport picklefrom dataclasses import replacefrom orionis.introspection.dependencies.entities.signature import Signature
original = Signature(resolved={}, unresolved={}, ordered={})assert replace(original).ordered is original.orderedassert copy.replace(original).resolved is original.resolvedassert copy.copy(original).unresolved is original.unresolvedassert copy.deepcopy(original).ordered is not original.orderedrestored = pickle.loads(pickle.dumps(original))assert restored == original and restored.ordered is not original.orderedtry: hash(original)except TypeError: passelse: raise AssertionError("Dictionary-backed signatures are not hashable")Construction validation
Section titled “Construction validation”__post_init__(self) -> NoneChecks that each group is a dict, raising TypeError otherwise. It does not validate individual entry types or consistency between groups. Fields cannot be reassigned, but the dictionaries remain mutable. Changing a cached signature affects later inspections of the same target: use the copying methods when you need to edit a map.
Predicates
Section titled “Predicates”
hasParameters(self) -> bool returns bool(ordered).
noArgumentsRequired(self) -> bool returns not bool(ordered). Its name does not mean “all parameters have defaults”: it returns False even when every retained parameter is optional.
hasUnresolvedArguments(self) -> bool returns bool(unresolved).
from orionis.introspection import ReflectDependencies
def empty(): return None
def defaults(limit=25): return limit
first = ReflectDependencies(empty).callableSignature()second = ReflectDependencies(defaults).callableSignature()assert not first.hasParameters()assert first.noArgumentsRequired()assert not first.hasUnresolvedArguments()assert second.hasParameters()assert not second.noArgumentsRequired()assert not second.hasUnresolvedArguments()Accessing parameters
Section titled “Accessing parameters”
getResolved(self) -> dict[str, Argument] returns the original resolved dictionary without copying.
getUnresolved(self) -> dict[str, Argument] returns unresolved by identity.
getAllOrdered(self) -> dict[str, Argument] returns ordered by identity, preserving declaration order.
getPositionalOnly(self) -> dict[str, Argument] creates a new map with is_keyword_only=False. It includes both strictly positional and positional-or-keyword parameters; it is not equivalent to the inspect kind POSITIONAL_ONLY.
getKeywordOnly(self) -> dict[str, Argument] creates a new map with is_keyword_only=True.
from orionis.introspection import ReflectDependencies
def search(term: str, *, limit=25): return term, limit
signature = ReflectDependencies(search).callableSignature()assert signature.getResolved() is signature.resolvedassert signature.getUnresolved() is signature.unresolvedassert signature.getAllOrdered() is signature.orderedassert list(signature.getPositionalOnly()) == ["term"]assert list(signature.getKeywordOnly()) == ["limit"]assert signature.getKeywordOnly()["limit"] is signature.ordered["limit"]Copies and iteration
Section titled “Copies and iteration”
toDict(self) -> dict[str, Argument] shallow-copies ordered. Overrides BaseEntity.toDict: it does not recursively serialize Argument objects or produce JSON.
resolvedToDict(self) -> dict[str, Argument] shallow-copies resolved.
unresolvedToDict(self) -> dict[str, Argument] shallow-copies unresolved.
arguments(self) -> dict_items[str, Argument] returns the live ordered.items() view, iterable as (name, Argument) pairs in declaration order.
from orionis.introspection import ReflectDependencies
def search(term: str, *, limit=25): return term, limit
signature = ReflectDependencies(search).callableSignature()all_copy = signature.toDict()resolved_copy = signature.resolvedToDict()unresolved_copy = signature.unresolvedToDict()assert all_copy is not signature.orderedassert resolved_copy is not signature.resolvedassert unresolved_copy is not signature.unresolvedassert all_copy["limit"] is signature.ordered["limit"]all_copy.pop("term")assert "term" in signature.orderedassert [(name, argument.resolved) for name, argument in signature.arguments()] == [ ("term", False), ("limit", True),]Inherited getFields
Section titled “Inherited getFields”getFields(self) -> list[dict[str, Any]]Inherited from BaseEntity. Describes the dataclass fields of Signature, not the callable’s parameters. Each entry contains name, types, default and metadata. Use arguments() to iterate parameters.
from orionis.introspection.dependencies.entities.signature import Signature
signature = Signature(resolved={}, unresolved={}, ordered={})fields = signature.getFields()assert [field["name"] for field in fields] == ["resolved", "unresolved", "ordered"]assert all(set(field) == {"name", "types", "default", "metadata"} for field in fields)Dependency injection
Section titled “Dependency injection”These utilities do not require application startup. Argument and Signature are data records constructed with explicit values. ReflectDependencies needs a target for each inspection; injecting its contract does not automatically provide a target.
A component that needs inspection can receive an application service that constructs the reflector for its selected target. This example combines constructor and method injection with the container’s real APIs:
from orionis.container.container import Containerfrom orionis.introspection import ReflectDependenciesfrom orionis.introspection.dependencies.entities.signature import Signaturefrom orionis.test import TestCase
class SignatureReader: def inspect(self, target) -> Signature: return ReflectDependencies(target).callableSignature()
class ImportService: def __init__(self, reader: SignatureReader): self.reader = reader
def describe(self, reader: SignatureReader, *, target): return reader.inspect(target)
def importUsers(source: str, *, batch_size=100): return source, batch_size
class SignatureInjectionTest(TestCase): async def testConstructorAndMethodInjection(self): container = Container() service = await container.make(ImportService) self.assertIsInstance(service.reader, SignatureReader) signature = await container.call(service, "describe", target=importUsers) self.assertEqual(list(signature.ordered), ["source", "batch_size"])Save this case in the application’s Testing directory. The container constructs SignatureReader as a concrete dependency; reflection only describes importUsers. The function is not executed during inspection.
Relationship to actual resolution
Section titled “Relationship to actual resolution”The container prepares plans using these Argument objects. When invoking a component it uses explicit values first, checks dependency registrations, and constructs services or applies defaults as appropriate. For a schema parameter, its resolution path may read the active HTTP request’s payload and validate it; that reading and validation belong to the container and schema system, rather than ReflectDependencies.
The container’s constructor path also resolves some class-local references. A reference that remains pending in this reflector does not necessarily describe the final result of every framework invocation path. Likewise, resolved=True does not turn a union or arbitrary typing alias into a constructible service.
See Schemas and validation for schema usage in supported contexts, and Application and Container for registration, scopes and component invocation.