Contributing to literalizer¶
Contributions to this repository must pass tests and linting.
CI is the canonical source of truth.
Install contribution dependencies¶
Install Python dependencies in a virtual environment.
$ pip install --editable '.[dev]'
Install prek hooks:
$ prek install
Linting¶
Run lint tools either by committing, or with:
$ prek run --all-files --hook-stage pre-commit --verbose
$ prek run --all-files --hook-stage pre-push --verbose
$ prek run --all-files --hook-stage manual --verbose
Running tests¶
$ uv run --extra dev pytest
Changelog entries¶
Describe each user-facing change in a news fragment at newsfragments/<issue>.change.
The .change suffix is the only fragment type towncrier is configured to consume; a fragment with any other suffix (such as .fix or .feature) is silently ignored at release time.
The release workflow assembles the fragments into CHANGELOG.rst and the GitHub release notes, then deletes them.
Do not re-add a fragment that a release has already consumed, for example when merging main into a long-running branch.
Golden case manifests¶
Every directory under tests/integration/cases/ contains a versioned
case.toml. The manifest is the source of truth for the input’s suite,
variant-axis and literalize_call coverage; language capability checks and
expansion remain in the typed Python runner.
An ordinary case participates in the base and combined suites. Its
input.yaml is inferred because it is the directory’s sole input:
schema_version = 1
suites = ["base", "combined"]
A variant-only case declares its specialized owner and the axes that consume it. A suffix distinguishes multiple logical uses of one input:
schema_version = 1
owner = "variant"
[[variants]]
axis = "nested_tuple_strategy"
suffix = "_mixed"
Simple render context also belongs beside the input. This base case replaces null record fields before inference:
schema_version = 1
suites = ["base", "combined"]
[base_context.record_null_substitutions]
replacement = -1
Some runners render one chosen input rather than the whole inventory: the
non-default indent, the bare value at file scope, the pre-indent shapes. The
input declares the part it plays in a roles list, and the runner looks it
up by role, so the directory name stays a single source of truth on disk:
schema_version = 1
suites = ["base", "combined"]
roles = ["indent-input"]
A literalize_call case declares that owner and describes its call in a
[call] table, so the whole case lives in one directory:
schema_version = 1
owner = "literalize-call"
[call]
target_function = "throttler.check"
parameter_names = ["user_id", "ts"]
per_element = true
call_transform = "emit({call})"
transform_stub_names = ["emit"]
requires_call_returns_expression = true
owner = "literalize-call" and a [call] table require each other.
call_transform is a template, not code: it may substitute only {call}
and {zipped}, and any other placeholder fails at load time. Enum- and
type-valued fields are named by string and resolved by the loader:
call_style (keyword, positional, object, or command),
zip_input_format, and the variable_form pair (new or
existing). variant_only = true keeps a case out of the default
per-language call matrix, leaving it to the call-variant suite.
A $ref case declares one of the two ref owners and describes itself in a
[ref] table:
schema_version = 1
owner = "literalize-ref"
[ref]
ref_case_override = "camel"
[ref.value_sources]
my_int = "42"
owner = "literalize-ref" (the case renders with an explicit ref_case)
or owner = "literalize-ref-default" (the case renders without one) and a
[ref] table require each other, so the table is spelled even when it is
empty. ref_key defaults to "$ref". ref_case_override names an
identifier case (snake, camel, pascal, upper_snake, or
kebab) that replaces the language’s default and skips any language whose
supported_ref_cases excludes it. Each [ref.value_sources] entry maps
a ref name to a JSON source that seeds the bound value for that ref.
A case renders under every language unless it narrows. gates names the
property the narrowing follows from, in the same vocabulary the variant axes
and the rejection manifests use, so a language that later gains that property
is covered without editing the manifest:
schema_version = 1
suites = ["base"]
gates = [{ kind = "metadata_field", field = "nested_list_widening", value = "integer_width" }]
A narrowing no property expresses – a syntax quirk one language has, or a
deliberate one-language sample of a rendering that does not vary – names its
languages instead and says in languages_reason which of the two it is:
schema_version = 1
suites = ["base"]
languages = ["Lua"]
languages_reason = "Lua alone closes a long string with the ]] delimiter this input runs up against."
languages and languages_reason require each other, and a case naming
both languages and gates states its narrowing twice and is rejected.
The [call] and [ref] tables narrow the languages they render under the
same way.
Non-default inputs can be explicit with input = "input.toml". Supported
variant context fields are variable_form (new, existing, or
both), collection_layout, pre_indent_level, and
record_null_substitutions. The loader rejects unknown fields or axes,
missing inputs, duplicate logical cases, and duplicate golden targets.
Rejection manifests¶
A rejection that holds for a family of languages – every language with a
json_type refusing a non-string dict key, every language taking
record_shape_names refusing a name that is not PascalCase – is declared
once under tests/errors/rejections/. Each directory holds a
rejection.toml and an expected.toml golden file. The golden
opens with the split of the languages the manifest selected, then groups what
each raised under a table per exception type:
# languages rejecting: 21; languages accepting: 0
[UnrepresentableInputError]
"C[CJSON]" = "C json_type can only represent dict keys as JSON object strings, not int"
A language raising a different exception from the rest of its family therefore appears as its own table rather than as a word buried in a line.
A case is keyed by its language, then by the option member it ran under and the
declared value it substituted, if the manifest varies either. An option member
is named ([CJSON]) and a declared value is quoted (['9Entry']).
The manifest itself declares only what provokes the rejection:
schema_version = 1
summary = """
A JSON value type keys its objects with strings.
"""
exceptions = ["UnrepresentableInputError"]
option = "json_type"
gates = [{ kind = "spec_field_present", field = "json_type" }]
[call]
api = "literalize"
source = "{1: one}"
input_format = "yaml"
gates selects the languages the rejection is claimed for, using the same
vocabulary as the golden suite’s variant axes; a language that later joins
those gates is covered without editing the manifest. A rejection about one
language’s own rendering names it in languages instead. option runs
each language once per member of that option, and values runs a case per
declared value, which any {value} in a constructor argument substitutes.
api is constructor, literalize, or literalize_call, and the
loader rejects an argument the named API does not take. exceptions lists
the exception types any selected language may raise; the golden file records
which one each raised, together with its message.
A language a gate admits that represents the input rather than refusing it is
declared in an accepts entry with the reason it does:
[[accepts]]
languages = ["Rust", "Scala"]
reason = """
Renders tuples alongside the JSON value type rather than instead of it.
"""
An entry there is an assertion rather than a mute: the suite makes the same call for those languages and fails if it stops going through, so a language that starts rejecting cannot sit behind a stale reason.
Documentation¶
Documentation is built on GitHub Pages.
Run the following commands to build and view documentation locally:
$ uv run --extra=dev sphinx-build -M html docs/source docs/build -W
$ python -c 'import os, webbrowser; webbrowser.open("file://" + os.path.abspath("docs/build/html/index.html"))'
Continuous integration¶
Tests are run on GitHub Actions.
The configuration for this is in .github/workflows/.
Performing a release¶
See Release Process.