Dependencias y firmas
ReflectDependencies transforma los parámetros de un constructor, método o callable en metadatos Argument, agrupados en una Signature. Sirve para analizar handlers, construir herramientas de diagnóstico o comprender cómo Orionis prepara una invocación.
La reflexión no instancia servicios, ejecuta el callable ni valida su payload. La etiqueta resolved describe la clasificación del parámetro; no garantiza que el contenedor pueda construir su valor. Para la inyección habitual consulta Aplicación y Contenedor. El descubrimiento de archivos y la carga de clases se explican en Descubrimiento de módulos.
Importación y primera inspección
Sección titulada «Importación y primera inspección»ReflectDependencies se exporta desde orionis.introspection. Las entidades y el contrato tienen estas importaciones:
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)Todos estos análisis son síncronos, incluso si el objetivo es una función async def: leer su firma no la ejecuta ni requiere await.
Cómo se clasifican los parámetros
Sección titulada «Cómo se clasifican los parámetros»El análisis conserva el orden de declaración y aplica estas reglas, en este orden:
| Parámetro | Clasificación y metadatos |
|---|---|
Nombre self o cls; *args; **kwargs | Se omite. Los nombres self y cls se omiten también en funciones ordinarias. |
| Sin anotación ni valor predeterminado | resolved=False; metadatos de type(typing.Any), no una instancia ni el objeto typing.Any. |
| Con valor predeterminado | resolved=True; type, módulo y nombre provienen de type(default), aunque exista una anotación diferente. |
Tipo de módulo builtins, sin valor predeterminado | resolved=False; por ejemplo int, str o list[int]. |
| Otra anotación, sin valor predeterminado | resolved=True; conserva sus metadatos. Esto incluye clases de aplicación y construcciones de typing que pueden no ser instanciables. |
is_keyword_only=True identifica únicamente los parámetros declarados después de *. Los parámetros anteriores a / y los que admiten tanto posición como nombre se agrupan juntos con False; Argument no conserva una distinción adicional entre ellos.
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 == 5La anotación count: str no convierte 5 a texto. El análisis describe el valor predeterminado existente.
Referencias futuras, genéricos y schemas
Sección titulada «Referencias futuras, genéricos y schemas»Para una anotación de texto sin valor predeterminado, el resolvedor intenta evaluarla en los globals del módulo del callable. Si no puede resolver todas a la vez, intenta cada referencia por separado. Solo sustituye el texto cuando obtiene un objeto que sea una clase (isinstance(resultado, type)). No dispone de los locales arbitrarios de una función que ya terminó.
Una referencia que permanece como texto usa module_name="typing", class_name igual al texto y type=str. Se marca resolved=True, aunque el contenedor la rechazará si no se proporciona un valor explícito. Los alias parametrizados o uniones no reciben tratamiento especial de inyección: por ejemplo list[int] real queda sin resolver por su módulo builtin; int | None real se marca resuelto por sus metadatos, pero eso no selecciona automáticamente una dependencia alternativa.
is_schema=True se establece únicamente cuando la anotación, sin default, es una clase derivada de msgspec.Struct; incluye los schemas de Orionis. No ejecuta validación ni inspecciona las restricciones de sus campos. Una anotación con default sigue la rama de defaults y conserva 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_schemaLas anotaciones de retorno no forman parte de Signature: no hay inferencia ni conversión de la respuesta en este componente.
API de ReflectDependencies
Sección titulada «API de ReflectDependencies»Construcción
Sección titulada «Construcción»ReflectDependencies(target: Any | None = None) -> NoneGuarda target para las siguientes operaciones, sin validarlo ni analizarlo todavía. Puede ser una clase, instancia, función o método según la operación elegida. El default None permite crear el objeto, pero no constituye un objetivo callable útil.
constructorSignature
Sección titulada «constructorSignature»constructorSignature(self) -> SignatureAnaliza target.__init__. No ejecuta el constructor. Para describir una clase sin crear una instancia, pasa directamente la clase:
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 == 3Propaga AttributeError si no se obtiene __init__; los fallos de inspección se presentan como ValueError. Un callable puede tener un __init__ genérico que no represente su llamada: para funciones utiliza callableSignature().
methodSignature
Sección titulada «methodSignature»methodSignature(self, method_name: str) -> SignatureObtiene el atributo con getattr(target, method_name) y analiza su firma. Un nombre ausente provoca AttributeError, no una excepción de reflexión propia. Un atributo no inspeccionable provoca ValueError; un nombre que no sea texto puede provocar TypeError en getattr.
Los métodos ligados excluyen su primer parámetro por la vinculación de Python, aunque el receptor no se llame self. Los classmethod y staticmethod conservan sus argumentos explícitos. En un método ordinario accedido desde la clase, un receptor con nombre distinto de self o cls permanece en la firma.
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
Sección titulada «callableSignature»callableSignature(self) -> SignatureComprueba callable(target) y analiza la firma de llamada que expone Python. Admite funciones, métodos ligados, clases o instancias invocables cuando su firma puede inspeccionarse y el objetivo puede usarse como clave de caché. Para una clase, inspecciona su llamada; no es una operación equivalente a leer explícitamente __init__ en todas las metaclases.
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 == 5Lanza TypeError para objetivos no callable y para objetivos no hashables al acceder a la caché. Si inspect.signature no puede describir la firma, lanza ValueError con la causa original. Inspeccionar min, por ejemplo, falla porque no expone una firma inspeccionable en este runtime. Los decoradores con __wrapped__ y las firmas __signature__ se interpretan según inspect.signature.
Contrato y caché
Sección titulada «Contrato y caché»IReflectDependencies declara las tres operaciones anteriores como abstractas. No es instanciable ni está registrado como servicio nativo para escoger automáticamente un objetivo. El uso directo de ReflectDependencies(objetivo) hace explícito qué se está analizando.
Las firmas de Python y sus metadatos usan dos cachés LRU de 1.024 entradas cada una, locales al proceso. Repetir un objetivo devuelve el mismo objeto Signature, también entre instancias del wrapper. Los métodos ligados usan como clave la función subyacente y el modo ligado; la caché de esos métodos no retiene sus receptores.
No hay clearCache() público en ReflectDependencies. Modificar __annotations__, defaults o __signature__ sobre la misma función después de inspeccionarla puede dejar metadatos obsoletos. Para una definición distinta usa un nuevo objeto función o reinicia el proceso. Los fallos de inspección no se almacenan como resultados correctos.
from orionis.introspection import ReflectDependencies
def cached(limit=25): return limit
first = ReflectDependencies(cached).callableSignature()second = ReflectDependencies(cached).callableSignature()assert first is secondEntidad Argument
Sección titulada «Entidad Argument»Argument es una dataclass frozen=True, slots=True, kw_only=True. Sus nueve campos son argumentos por nombre del constructor; los seis primeros son obligatorios:
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,) -> NoneValidación y mutabilidad
Sección titulada «Validación y mutabilidad»__post_init__(self) -> NoneSe ejecuta automáticamente al construir la entidad. Verifica que module_name, class_name y full_class_path sean str, o lanza TypeError. Rechaza type=None junto con default=None mediante ValueError. No comprueba exhaustivamente name, flags, la correspondencia entre rutas y clases ni todos los objetos de typing.
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")La dataclass genera representación y comparación por campos; su hash solo funciona si todos los valores implicados son hashables. Un default lista no se vuelve inmutable por estar dentro de Argument. La entidad no hereda BaseEntity: no tiene métodos propios toDict() ni getFields().
Copias y protocolos generados de Argument
Sección titulada «Copias y protocolos generados de Argument»dataclasses.replace() y copy.replace() permiten construir otro Argument con campos cambiados y vuelven a ejecutar su validación. La dataclass genera __replace__; por tener slots y ser frozen, también genera __getstate__ y __setstate__ para preservar sus campos al copiar o serializar. Son protocolos estándar de Python, separados de los métodos implementados por Orionis.
La sustitución y copy.copy() son superficiales: conservan el default mutable. copy.deepcopy() copia su contenido. Este ejemplo usa un tipo builtin y un default serializable para comprobar también 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.defaultPoder serializar esta entidad depende de sus valores: una clase local, una función no serializable u otro default pueden impedir pickle.
Los errores de construcción pueden capturarse sin inspeccionar ningún 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")Entidad Signature
Sección titulada «Entidad Signature»Signature es una dataclass frozen con argumentos por nombre. Guarda tres diccionarios obligatorios de Argument:
Signature( *, resolved: dict[str, Argument], unresolved: dict[str, Argument], ordered: dict[str, Argument],) -> NoneEl reflector utiliza las mismas instancias Argument en ordered y su agrupación correspondiente. Al construir una Signature manualmente, el desarrollador proporciona esa coherencia.
Copias y protocolos generados de Signature
Sección titulada «Copias y protocolos generados de Signature»Python genera construcción, representación, comparación, protección de campos frozen y __replace__. Una Signature ordinaria contiene diccionarios, por lo que no es hashable aunque su dataclass genere __hash__. dataclasses.replace() y copy.replace() crean otra entidad validada, pero comparten los mapas que no se sustituyen; no convierten el resultado en un snapshot inmutable.
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")Validación de construcción
Sección titulada «Validación de construcción»__post_init__(self) -> NoneComprueba que cada agrupación sea un dict; de lo contrario lanza TypeError. No valida los tipos de cada entrada ni la consistencia entre las agrupaciones. Aunque no se puedan reasignar sus campos, los diccionarios siguen siendo mutables. Modificar una firma almacenada en caché afecta a inspecciones posteriores del mismo objetivo: utiliza los métodos de copia cuando necesites editar un mapa.
Predicados
Sección titulada «Predicados»
hasParameters(self) -> bool devuelve bool(ordered).
noArgumentsRequired(self) -> bool devuelve not bool(ordered). Su nombre no significa «todos tienen default»: devuelve False aunque cada parámetro conservado sea opcional.
hasUnresolvedArguments(self) -> bool devuelve 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()Acceso a parámetros
Sección titulada «Acceso a parámetros»
getResolved(self) -> dict[str, Argument] devuelve el diccionario resolved original, sin copiarlo.
getUnresolved(self) -> dict[str, Argument] devuelve unresolved por identidad.
getAllOrdered(self) -> dict[str, Argument] devuelve ordered por identidad, conservando la declaración.
getPositionalOnly(self) -> dict[str, Argument] crea un nuevo mapa con is_keyword_only=False. Incluye tanto los parámetros estrictamente posicionales como los posicionales o por nombre; no equivale al kind POSITIONAL_ONLY de inspect.
getKeywordOnly(self) -> dict[str, Argument] crea un nuevo mapa con 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"]Copias e iteración
Sección titulada «Copias e iteración»
toDict(self) -> dict[str, Argument] copia superficialmente ordered. Sobrescribe el método de BaseEntity: no serializa recursivamente los Argument ni produce JSON.
resolvedToDict(self) -> dict[str, Argument] copia superficialmente resolved.
unresolvedToDict(self) -> dict[str, Argument] copia superficialmente unresolved.
arguments(self) -> dict_items[str, Argument] devuelve la vista viva ordered.items(), iterable como pares (nombre, Argument) en orden de declaración.
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),]getFields heredado
Sección titulada «getFields heredado»getFields(self) -> list[dict[str, Any]]Proviene de BaseEntity. Describe los campos de la dataclass Signature, no los parámetros del callable. Cada elemento contiene name, types, default y metadata. Para recorrer parámetros utiliza arguments().
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)Uso con inyección de dependencias
Sección titulada «Uso con inyección de dependencias»Estas utilidades no necesitan iniciar la aplicación. Argument y Signature representan datos que se construyen con valores explícitos. ReflectDependencies necesita un objetivo para cada análisis; no se obtiene un objetivo automáticamente al inyectar el contrato.
Si un componente necesita inspección, puede recibir un servicio de aplicación que construya el reflector con el objetivo seleccionado. Este ejemplo combina inyección en el constructor y en un método usando las APIs reales del contenedor:
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"])Guarda este caso en el directorio de Pruebas de la aplicación. El contenedor construye SignatureReader como dependencia concreta; el reflector solo describe importUsers. No se ejecuta esa función durante la inspección.
Relación con la resolución real
Sección titulada «Relación con la resolución real»El contenedor prepara planes a partir de estos Argument. Al invocar un componente, usa primero los valores explícitos, consulta registros de dependencias y, cuando procede, construye servicios o utiliza defaults. Para un argumento marcado como schema, su flujo de resolución puede obtener el payload de la petición HTTP activa y validarlo; esa lectura y validación pertenecen al contenedor y al sistema de schemas, no a ReflectDependencies.
El constructor del contenedor dispone además de resolución de algunas referencias locales de clase. Que una referencia permanezca pendiente en este reflector no describe necesariamente el resultado final de todos los caminos de invocación del framework. Tampoco resolved=True convierte una unión o cualquier alias de typing en un servicio construible.
Consulta Schemas y validación para el uso de schemas en los contextos admitidos y Aplicación y Contenedor para registros, ámbitos y ejecución de componentes.