Skip to content

Code generation

Emitter framework

didactic.codegen.Emitter

Bases: Protocol

The protocol every custom emitter implements.

Attributes:

Name Type Description
file_extension str

A class-level attribute naming the canonical filename extension for this emitter ("avsc", "ts", "graphql"). Used by didactic.codegen.write when no explicit filename template is given.

Notes

Implement either or both of emit_class (Model class -> bytes) and emit_instance (Model instance -> bytes). Calls to the missing direction raise NotImplementedError.

emit_class

emit_class(cls: type[Model]) -> bytes

Emit a Model class as bytes (e.g. a schema declaration).

emit_instance

emit_instance(instance: Model) -> bytes

Emit a Model instance as bytes (e.g. a serialised value).

didactic.codegen.IndentWriter

IndentWriter(indent_str: str = '    ')

A small buffer with managed indentation for emitter authors.

Mirrors panproto_protocols::emit::IndentWriter from the panproto Rust crate. Use as the standard helper for emitting nested format text.

Parameters:

Name Type Description Default
indent_str str

The string used for each indent level. Defaults to four spaces.

' '

Examples:

>>> w = IndentWriter()
>>> w.line("class Foo:")
>>> with w.indent():
...     w.line("x: int")
>>> w.text_str()
'class Foo:\\n    x: int\\n'

line

line(text: str = '') -> None

Emit text at the current indent level, followed by \n.

text

text(text: str) -> None

Emit text without trailing newline. No indentation applied.

indent

indent() -> _IndentCtx

Return a context manager that increases the indent level.

text_str

text_str() -> str

Finalise the buffer to a string.

bytes

bytes() -> bytes

Finalise the buffer to UTF-8 bytes.

push_indent

push_indent() -> None

Increase the indent level by one. Prefer with w.indent():.

pop_indent

pop_indent() -> None

Decrease the indent level by one. Prefer with w.indent():.

didactic.codegen.emitter

emitter(name: str) -> Callable[[type[E]], type[E]]

Register a class as the emitter for name.

Parameters:

Name Type Description Default
name str

The target name (e.g. "graphql_lite", "sql_postgres"). Used in Model.emit_as.

required

Returns:

Type Description
decorator

A class decorator that calls register_emitter with an instance of the decorated class.

Examples:

>>> from didactic.codegen import emitter
>>>
>>> @emitter("yaml_compact")
... class YamlCompactEmitter:
...     file_extension = "yaml"
...
...     def emit_class(self, cls):
...         return b""  # implementation

didactic.codegen.register_emitter

register_emitter(name: str, instance: Emitter) -> None

Register instance as the emitter for name.

Parameters:

Name Type Description Default
name str

The target name.

required
instance Emitter

An object that satisfies the Emitter protocol.

required

Raises:

Type Description
TypeError

If instance does not satisfy the protocol.

didactic.codegen.list_emitters

list_emitters() -> list[str]

List every registered emitter name.

JSON Schema

didactic.codegen.json_schema_of

json_schema_of(cls: type[Model]) -> JsonSchemaDoc

Build a JSON Schema (Draft 2020-12) document for cls.

Parameters:

Name Type Description Default
cls type[Model]

A Model subclass.

required

Returns:

Type Description
dict

A JSON Schema object with $schema, title, type, properties, required, and (if any fields have constraints) per-property minimum / maximum / etc.

Examples:

>>> import didactic.api as dx
>>> class User(dx.Model):
...     id: str
...     age: int = 0
>>> schema = json_schema_of(User)
>>> schema["properties"]["id"]["type"]
'string'
>>> "id" in schema["required"]
True
>>> "age" in schema["required"]
False

Bulk export

didactic.codegen.write

write(
    models: Iterable[type[Model]],
    targets: Mapping[str, str],
    *,
    filename: str = "{model_name}.{ext}",
) -> dict[str, Path]

Emit each model under each target, writing files to disk.

Parameters:

Name Type Description Default
models Iterable[type[Model]]

The Model classes to emit.

required
targets Mapping[str, str]

Mapping of target_name -> output_directory. Each directory is created if it does not exist.

required
filename str

A format string for filenames. Available placeholders: {model_name} (the class name as written), {model_snake_case} (snake-case of the class name), and {ext} (the canonical extension for the target).

'{model_name}.{ext}'

Returns:

Type Description
dict

Map of "{target}/{model_name}" to the absolute Path written.

Raises:

Type Description
LookupError

If Model.emit_as does not know how to emit one of the (model, target) pairs.

Instance-level emit

didactic.codegen.io.emit

emit(protocol: str, instance: Model) -> bytes

Encode instance as bytes in the named protocol.

Parameters:

Name Type Description Default
protocol str

A panproto protocol name (e.g. "avro", "json_schema", "openapi"). Use list_protocols to enumerate.

required
instance Model

A Model instance.

required

Returns:

Type Description
bytes

The encoded bytes.

Raises:

Type Description
IoError

If the codec rejects the instance (mismatch with the Model's Theory, value out of range, etc.).

didactic.codegen.io.parse

parse(
    protocol: str, data: bytes, *, schema: type[M]
) -> M

Decode data from protocol back to a Model instance.

Parameters:

Name Type Description Default
protocol str

A panproto protocol name.

required
data bytes

The encoded bytes.

required
schema type[M]

The expected Model class. The decoded payload is validated against this class.

required

Returns:

Type Description
Model

A new schema instance.

Raises:

Type Description
IoError

If the codec cannot decode data against the schema.

didactic.codegen.io.list_protocols

list_protocols() -> list[str]

List the supported instance-protocol names.

Returns:

Type Description
list of str

Every codec the running panproto build registers, sorted.

didactic.codegen.io.check_round_trip

check_round_trip(protocol: str, instance: Model) -> None

Assert that parse(emit(instance)) == instance for protocol.

Parameters:

Name Type Description Default
protocol str

A panproto protocol name.

required
instance Model

A Model instance. The check round-trips it through the codec and asserts equality.

required

Raises:

Type Description
AssertionError

If the round trip does not produce an equal Model.

IoError

If the codec rejects either direction.

Source-level emit

didactic.codegen.source.emit_pretty

emit_pretty(model: type[Model], *, target: str) -> bytes

Render model as fresh source in the named target language.

Parameters:

Name Type Description Default
model type[Model]

A Model subclass.

required
target str

A panproto grammar name (e.g. "rust", "typescript", "python"). Use available_targets to enumerate.

required

Returns:

Type Description
bytes

Source code that the target grammar accepts as syntactically valid.

Raises:

Type Description
PanprotoError

If the grammar rejects the schema (typically because the grammar lacks a grammar.json for de-novo emission).

Notes

The output is syntactically valid; idiomatic formatting (rustfmt spacing rules, gofmt conventions) is left to a post-processor. Configure one in pyproject.toml under [tool.didactic.emit.targets.{target}.post_process].

didactic.codegen.source.emit

emit(schema: object, *, protocol: str) -> bytes

Re-emit a parse-recovered schema as source bytes.

Parameters:

Name Type Description Default
schema object

A panproto.Schema previously returned by parse. The schema must carry byte-position fragments from the parse step.

required
protocol str

The grammar name to emit under (typically the same as the parse step).

required

Returns:

Type Description
bytes

The reconstructed source.

Notes

Use this for edit pipelines (parse, transform, emit). For fresh emission from a Model class, use emit_pretty.

didactic.codegen.source.parse

parse(
    source: bytes,
    *,
    protocol: str,
    file_path: str = "<source>",
) -> object

Parse source bytes into a panproto.Schema.

Parameters:

Name Type Description Default
source bytes

The source bytes to parse.

required
protocol str

The grammar name (e.g. "rust", "typescript").

required
file_path str

A filename for error messages. Does not need to exist.

'<source>'

Returns:

Type Description
Schema

The parsed schema with byte-position fragments. Pass to emit to re-emit, or to didactic.codegen.source.for_protocol for lens-style round-trip checks.

didactic.codegen.source.available_targets

available_targets() -> list[str]

List every grammar the running panproto build supports.

Returns:

Type Description
list of str

Grammar names, sorted. "rust", "typescript", "go", "python", etc.

didactic.codegen.source.detect_language

detect_language(path: str) -> str | None

Return the grammar name that handles path's extension, if any.

didactic.codegen.source.for_protocol

for_protocol(protocol: str) -> Callable[[bytes], object]

Build a parse function bound to protocol.

Parameters:

Name Type Description Default
protocol str

The grammar name.

required

Returns:

Type Description
Callable

A function that takes source bytes and returns a Schema. Use in lens compositions or as a partial-application helper.