Skip to content

Template library

Most projects silence the same tools in the same ways, so most tingle.toml files start out re-typing the same regexes. A metric can instead name a template and state only what it wants differently:

[[metrics]]
base = "tingle.builtins.ruff.noqa_comment"
extra_ignore_lines = ['# @generated']

That is a regex_count over #\s*noqa, named noqa-comment, grouped under linting, described in the report — with one line of local knowledge laid over it.

See what is on offer:

tingle library                 # the bundled pack
tingle library mycorp_metrics  # somebody else's
tingle library --expand        # each one as the config it stands for
tingle add --base tingle.builtins.mypy.type_ignore_comment

tingle add takes a metric type or a --base, and both together only when the base is a mixin — which states no type, so the entry has to:

tingle add regex_count '#\s*noqa:' --base generated

What base may name

A dotted base is a Python import path, its last component the attribute: tingle.builtins.ruff.noqa_comment is noqa_comment in tingle.builtins.ruff. The bundled pack is reached exactly like any other package — tingle imports it by name rather than knowing it from the inside, so a template you publish works the way the shipped ones do.

A bare base names a local template declared in the config file. That is the whole distinction: a dot means an import.

Warning

A dotted base imports and runs Python. Running tingle in a repository is trusting that repository's config the way running pytest trusts its conftest.py.

Overriding

Every key a metric states wins over the template's. There is no deep merge: a list replaces.

[[metrics]]
base = "tingle.builtins.ruff.noqa_comment"
name = "noqa-in-tests"     # so the same template can be used twice
range = "tests"
group = "test-debt"
ignore_lines = ['# checked']   # replaces whatever the template had

To add to a template's list instead of replacing it, prefix the param with extra_:

extra_ignore_lines = ['# @generated']   # the template's, then this

extra_ only makes sense on top of a base, and only over a list.

Two rules are not overrides:

  • type is fixed. A template that states a type owns it, because its params were written for that type. A template that states no type is a mixin, and the metric supplies one.
  • range and ranges are one slot. Stating either drops both from the template, so a template's range and your ranges never arrive together.

Local templates

A [templates.<name>] table is a template the config file declares itself. Its table key names it; the name inside is only the default metric name, exactly as for an imported one.

This is where a shared ignore set or a shared pair of ranges lives — a mixin with no type, used by metrics of different types:

[templates.generated]
ignore_lines = ['# @generated', '# AUTOGENERATED']

[[metrics]]
base = "generated"
name = "noqa-comment"
type = "regex_count"
pattern = '#\s*noqa:'

[[metrics]]
base = "generated"
name = "any-uses"
type = "symbol_uses"
symbol = "typing.Any"

A local template may itself carry a base, imported or local, so a project can narrow a shipped template once and use the result everywhere:

[templates.our-noqa]
base = "tingle.builtins.ruff.noqa_comment"
extra_ignore_lines = ['# @generated']

A base that leads back to itself is a config error, not a hang.

A local template's fields are checked at the metric that uses them, by the same validator every metric goes through — and the complaint names the base the field came from. A template nothing uses is not checked, because there is nothing yet for it to be wrong about.

Publishing your own

A template pack is a Python package holding MetricTemplate instances. It needs tingle and nothing else:

# mycorp_metrics/django.py
from tingle.pacts.config import MetricTemplate

legacy_orm = MetricTemplate(
    type="symbol_uses",
    name="legacy-orm-uses",
    group="migration",
    description="Calls into the ORM wrapper the strangler fig is replacing.",
    params={"symbol": "mycorp.legacy.orm.query"},
)

Used as base = "mycorp_metrics.django.legacy_orm". A pack may nest as deeply as it likes — base = "mycorp_metrics.django.orm.legacy" — and tingle library lists what it finds all the way down. Where a module declares __all__, that is taken as the list of templates it publishes; without one, every public name holding a MetricTemplate is published.

Compose in Python rather than with base — a packaged template has no base of its own:

from dataclasses import replace

strict_legacy_orm = replace(
    legacy_orm,
    name="legacy-orm-strict",
    params={"symbol": "mycorp.legacy.orm.raw_query"},
)

Every loaded template is verified before use: it must be a MetricTemplate exactly (not a subclass), its fields must hold what they claim, its type must be one tingle knows, and its params must be strings, numbers, booleans or lists of those. Its params are copied on load, so a pack cannot change a metric's definition after the config has read it. tingle library lists the ones that pass and reports the rest, so one bad template in somebody else's pack does not hide the good ones.

Templates should not state a range. Range names belong to the project using them.

Pinning a template

The bundled templates are versioned with tingle, so an upgrade can change what one measures and move a number that nothing in your repository touched. Their names, types and meanings are treated as public API and changes are recorded in the changelog — but if you would rather not follow one at all, print what it stands for and paste that in place of the base line:

tingle library tingle.builtins --expand

The bundled pack

tingle library is the authority; in outline:

Pack What it counts
black # fmt comments
codespell # codespell:ignore comments
coverage # pragma comments
import_linter commented-out contracts, ignore_imports
mypy # type: ignore, overrides, disabled codes, disallow_* holes
pylint # pylint: comments, rcfile and pyproject disables
pytest skip and xfail marks
python typing.Any, typing.cast, TODO/FIXME, long files
ruff # noqa, # ruff: ignore/disable, file exemptions, ignores
taplo # taplo: comments
unittest_mock ANY placeholders, patch uses