config: The Configuration Layer (myform.yaml + [tool.myform] + .py extensions)#

One typed schema with per-format applicability tags, projected two ways: the mdformat-facing MystOptions surface stays authoritative for the plugin facade, while MyformConfig adds the translator-wide knobs (degradation policy, assists, per-target sections). Python extension modules register additional readers, writers, and lowerings through the same registries used internally.

Integration seam for myform.convert: resolve config = MyformConfig.load(target_path), call load_extensions(config) before resolving the target writer (an extension may register the target format itself), then warn_inapplicable(config, dst) before rendering.

This package is PEP 562-lazy: the pydantic schema (options) and the extension loader (extensions) import on first attribute access, so the stdlib-only discovery walk stays importable from cold paths like the CLI’s mdformat fast-path check.

The Configuration Layer (myform.yaml + [tool.myform] + .py extensions).

One typed schema with per-format applicability tags, projected two ways: the mdformat-facing MystOptions surface stays authoritative for the plugin facade, while MyformConfig adds the translator-wide knobs (degradation policy, assists, per-target sections). Python extension modules register additional readers, writers, and lowerings through the same registries used internally.

Integration seam for myform.convert: resolve config = MyformConfig.load(target_path), call load_extensions(config) before resolving the target writer (an extension may register the target format itself), then warn_inapplicable(config, dst) before rendering.

This package is PEP 562-lazy: the pydantic schema (options) and the extension loader (extensions) import on first attribute access, so the stdlib-only discovery walk stays importable from cold paths like the CLI’s mdformat fast-path check.

class myform.config.AssistConfig(*, enabled: bool = False, model: str = 'bulletin', base_url: str = 'http://localhost:4000', api_key_env: Annotated[str, MinLen(min_length=1)] = 'LITELLM_TOKEN', timeout: Annotated[float, Gt(gt=0)] = 5.0, cache: bool = True)#

The assist: section – opt-in, degrade-only LM suggestions (myform.assist).

enabled: bool#

zero network calls when False.

Type:

Enable the gateway-backed assist. Off by default

model: str#

Gateway model alias used for assist requests (a light/cheap tier by default).

base_url: str#

Base URL of the OpenAI-compatible local gateway.

api_key_env: str#

Environment variable carrying the local gateway’s client bearer token.

timeout: float#

Socket timeout for one opt-in gateway request, in seconds.

cache: bool#

Memoize assist responses on disk, keyed by (node hash, target format, model).

class myform.config.DegradeConfig(*, mode: 'annotate' | 'silent' | 'strict' = 'annotate', comments: bool = True)#

The degrade: section – the degradation engine’s policy knobs (myform.degrade).

mode: 'annotate' | 'silent' | 'strict'#

How the degradation engine reacts to a non-NATIVE (APPROX/CARRY/DROP) node.

comments: bool#

Leave a provenance comment (e.g. rST .. myform: ...) for replaced/dropped nodes.

class myform.config.MyformConfig(*, degrade: DegradeConfig = <factory>, assist: AssistConfig = <factory>, myst: MystOptions = <factory>, gfm: dict[str, ~typing.Any]=<factory>, rst: RstConfig = <factory>, typst: TypstConfig = <factory>, extensions: list[str] = <factory>)#

The whole-translator configuration schema (myform.yaml / [tool.myform]).

Instances are frozen and reject unknown top-level keys (once warned about, see from_mapping). Build one with load (file discovery) or from_mapping (a mapping you already parsed), never the bare constructor with untrusted input – the bare constructor skips the warn-on-unknown-key and reuse-MystOptions.from_options behavior.

degrade: DegradeConfig#

consulted for any target whose writer capability table is not all-NATIVE.

Type:

Degradation engine policy. Cross-cutting

assist: AssistConfig#

LM assist policy. Cross-cutting, same reasoning as degrade.

myst: MystOptions#

MyST reader/writer knobs – the mdformat-facing MystOptions surface, reused directly.

gfm: dict[str, Any]#

GFM reader/writer knobs. No typed GfmOptions schema exists yet – mdformat-gfm exposes no analogous [plugin.gfm] surface in this codebase – so this is a raw pass-through mapping merged into the mdformat options for the gfm extension; it becomes a typed model mirroring myst once one lands.

rst: RstConfig#

Sphinx reStructuredText writer knobs (launch-minimal).

typst: TypstConfig#

Typst writer knobs.

extensions: list[str]#

dotted module paths or .py file paths. See myform.config.extensions.load_extensions.

Type:

Extension entries

classmethod from_mapping(raw: Mapping[str, Any]) → Self#

Build a validated MyformConfig from a raw config mapping (parsed YAML/TOML).

Same philosophy as MystOptions.from_options at both this and each section’s level: None values are dropped so field defaults apply; an unknown top-level section is warned about and ignored (never aborts a run over a stray/typo’d key); a wrong value type still raises, since that is a genuine misconfiguration. Only sections present in raw are passed to the constructor, so model_fields_set on the result exactly reflects what the config provided – warn_inapplicable depends on this.

classmethod load(start: Path) → Self#

Discover the nearest myform config upward from start and build a validated instance.

Precedence within one directory: myform.yaml, then .myform.yaml, then [tool.myform] in pyproject.toml (a pyproject.toml without that table does not count as “found”). The nearest directory holding any of the three wins over a farther one, regardless of which file type it is. Nothing found by the filesystem root -> defaults.

classmethod discover(start: Path) → tuple[dict[str, Any], Path | None]#

Walk upward from start and return the raw config mapping plus its source path.

start may be the target file itself or its containing directory. Returns ({}, None) when no config file is found. Exposed separately from load so discovery precedence is directly testable without also exercising validation. The walk itself lives in myform.config.discovery.discover_config, shared with the CLI’s fast-path check.

classmethod json_schema() → dict[str, Any]#

Return the JSON Schema for the whole MyformConfig (editor autocomplete/validation).

class myform.config.Registries(Reader: type[Reader], Writer: type[Writer], degrade: LoweringRegistry)#

The registry namespace handed to an extension’s register(registries) hook.

Reader and Writer are the exact registry classes this package’s own reader/writer modules decorate through (@Reader.register / @Writer.register); an extension registers exactly the same way. degrade is the exact process-wide lowering registry consulted by lower.

class myform.config.RstConfig(*, heading_underline_ladder: Annotated[str, MinLen(min_length=1)] = '#*=-^"')#

The rst: section – Sphinx reStructuredText writer knobs (launch-minimal).

heading_underline_ladder: str#

Underline characters for nested heading levels, outermost first (docutils convention).

class myform.config.TypstConfig(*, prelude: 'none' | 'myform' = 'none', postformat: 'off' | 'auto' | 'require' = 'off')#

The typst: section – Typst writer knobs (see the Typst-ecosystem memo, spec §8).

prelude: 'none' | 'myform'#

Emit calls into the small shipped myform.typ prelude for MyST-only constructs.

postformat: 'off' | 'auto' | 'require'#

Shell out to typstyle for final normalization, if present.

myform.config.discover_config(start: Path) → tuple[Path, dict[str, Any] | None] | None#

Walk upward from start and return the winning config source, or None.

start may be the target file itself or its containing directory. Precedence within one directory: myform.yaml, then .myform.yaml, then [tool.myform] in pyproject.toml (a pyproject.toml without that table does not count as “found”). The nearest directory holding any of the three wins over a farther one, regardless of which file type it is.

The result is a (path, raw) pair. For a pyproject.toml winner raw is the parsed [tool.myform] table – read here, exactly once, so no check-then-act re-read can observe a different file. For a YAML winner raw is None and the caller parses the file through its own YAML layer.

myform.config.load_extensions(config: MyformConfig) → None#

Import and register every config.extensions entry.

Parameters:

config – The loaded MyformConfig; MyformConfig.extensions entries are dotted module paths (e.g. mypackage.myform_ext) or .py file paths (e.g. ./myform_ext.py).

Raises:
  • ImportError – An entry could not be imported (bad path, bad module, syntax error).

  • AttributeError – A successfully imported module has no callable register.

myform.config.warn_inapplicable(config: MyformConfig, target_format: str) → None#

Warn for every explicitly-configured section that cannot affect target_format.

Consults only the top-level sections the config actually set (config.model_fields_set, populated by MyformConfig.from_mapping to exactly mirror what was in the file) against each field’s declared applies_to tag. Cross-cutting sections (DegradeConfig, AssistConfig, extensions) are tagged with ALL_FORMATS and never warn. This never raises – a mistagged option is a loud log line, not a crashed render (the dredge-note contract).

Parameters:
  • config – A MyformConfig built via load/from_mapping (so model_fields_set is meaningful; a bare MyformConfig() has an empty set and never warns).

  • target_format – The format name being written (e.g. 'gfm').