Introspection¶
Framework and tooling authors can inspect finalized public truth without accessing compiler artifacts.
from talea import Contract, Spec
from talea.introspection import inspect_contract, inspect_spec
class User(Spec):
id: int
spec_info = inspect_spec(User)
contract_info = inspect_contract(Contract(list[User]))
Returned values¶
| Type | Important data |
|---|---|
FieldInfo |
annotation, canonical Schema, required/default/factory state, alias, constraints, metadata, presence |
DerivationInfo |
source Spec, retained/omitted fields, include/exclude selection, partial status, input/output mode |
SpecInfo |
fields, generic identity/arguments, recursion, hooks, serializers, metadata, trust, derivation, reachable representations |
ContractInfo |
annotation, canonical Schema, metadata, supported operation names, reachable representations |
RepresentationInfo |
frozen internal/input/output schemas and direction flags, with no callbacks |
SerializerInfo |
serializer name, target field, declared-output flag, and optional output Schema, with no callback |
For Contract(UserDataclass), ContractInfo.schema is a frozen
DataclassSchema. It exposes exact dataclass type identity, immutable canonical
fields, init/keyword-only/default lifecycle truth, frozen transitive trust,
generic specialization identity, and finite recursive references without
exposing mutable stdlib Field objects.
inspect_spec() accepts a Spec class, including an open generic declaration.
Concrete declarations return a cached immutable SpecInfo; open generics expose
the declaration truth available before specialization. inspect_contract()
accepts a Contract instance and returns a fresh immutable description.
The canonical Schema graph is structural truth and is safe to read. Generated
source, compiled callables, locks, lazy publication state, codec choices, and
resource counters remain intentionally private. Tooling should not infer
semantics from class internals when a public info object or schema node provides
the answer.
SpecInfo.representations and ContractInfo.representations contain each
reachable represented contract once. Their RepresentationInfo values expose
internal, optional input/output, has_loader, and has_dumper. They do
not expose callback objects, callback names, globals, generated source, or
compiler state; mutation cannot alter runtime truth.
SpecInfo.serializers contains one frozen SerializerInfo per effective field
serializer. has_declared_output distinguishes legacy opaque hooks from hooks
whose output_schema is canonical truth. Callback objects, globals, code
objects, generated projectors, and caches are not exposed. Reading this
information does not execute the callback.
For a directional derived Spec, DerivationInfo.mode is "input" or
"output"; it is None for ordinary pick/omit/partial derivation. Consumers
should inspect this provenance instead of inferring semantics from names such
as UserInput.
Framework adapter example¶
The executable example projects a normal account Spec into a smaller framework-owned descriptor, inspects aliases, constraints, metadata, and Sensitive state, then inspects a derived partial and an arbitrary Contract.
"""Framework-facing immutable introspection without annotation reconstruction."""
from dataclasses import FrozenInstanceError
from typing import Annotated, cast
from talea import Alias, Contract, Description, Ge, Sensitive, Spec, Title, derive_spec
from talea.introspection import FieldInfo, inspect_contract, inspect_spec
class Account(
Spec,
metadata=(Title("Account"), Description("Framework-visible account payload.")),
):
account_id: Annotated[int, Alias("id"), Ge(1)]
display_name: Annotated[str, Alias("displayName")]
token: Annotated[str, Sensitive()]
def field_descriptor(field: FieldInfo) -> dict[str, object]:
"""Project public info into one framework's simpler descriptor shape."""
return {
"python_name": field.name,
"external_name": field.alias or field.name,
"required": field.required,
"omittable": field.omittable,
"sensitive": field.sensitive,
"schema_kind": type(field.schema).__name__ if field.schema is not None else None,
}
account_info = inspect_spec(Account)
assert account_info.title == "Account"
assert account_info.description == "Framework-visible account payload."
assert account_info.operations == (
"strict_python",
"external_python",
"json_input",
"python_output",
"json_output",
)
descriptors = [field_descriptor(field) for field in account_info.fields]
assert descriptors[0]["external_name"] == "id"
assert descriptors[0]["required"] is True
assert descriptors[2]["sensitive"] is True
AccountPatch = derive_spec(Account, exclude=("account_id",), partial=True, name="AccountPatch")
patch_info = inspect_spec(AccountPatch)
assert patch_info.presence_aware is True
assert patch_info.derivation is not None
assert patch_info.derivation.source is Account
assert patch_info.derivation.omitted_fields == ("account_id",)
assert all(field.omittable for field in patch_info.fields)
batch = Contract[list[Account]](list[Account])
contract_info = inspect_contract(batch)
assert contract_info.annotation == list[Account]
assert type(contract_info.schema).__name__ == "SequenceSchema"
try:
frozen_attribute = "title"
setattr(account_info, frozen_attribute, "Changed")
except FrozenInstanceError:
pass
else:
raise AssertionError("public introspection must be immutable")
try:
inspect_spec(cast(type[object], dict))
except TypeError as error:
assert "Spec class" in str(error)
else:
raise AssertionError("non-Spec classes must not expose synthetic info")
# Frameworks may project this truth into route parameters, documentation, or
# dependency graphs. They should not mutate it or recover semantics by rereading
# __annotations__, compiler globals, generated functions, or private artifacts.
The framework may copy this information into route metadata, dependency
descriptions, form fields, or documentation. It should preserve the distinction
between canonical Python names and aliases, and between required and
omittable. A partial field can be non-required because it is absent; that does
not imply its value contract accepts None.
Caching, open generics, and recursion¶
Concrete inspect_spec() results are weakly cached by class identity and are
frozen. A framework can retain them without worrying that another consumer will
mutate shared field truth. The cache does not keep an otherwise unreachable
dynamic class alive.
An open generic exposes its parameters and annotations, but a field whose type
depends on a free parameter has schema=None until specialization. Frameworks
must either display that declaration as generic or request a concrete type such
as Page[User]; they must not invent Any runtime semantics. Recursive
contracts expose finite reference nodes, so traversals should track named
identity rather than recursively expanding forever.
Failure and extension rules¶
inspect_spec() rejects non-Spec classes and inspect_contract() rejects
non-Contract objects with TypeError. Introspection does not execute input,
serialization, callbacks, or schema projection. It also does not expose
generated source or compiled callables as public extension points.
A framework needing JSON/OpenAPI should call the projection API instead of
recreating standards semantics from FieldInfo. A runtime validator should use
the Spec/Contract operation or compile from public canonical Schema, not reread
__annotations__. These boundaries preserve one owner for each truth.