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:
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:
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_ only makes sense on top of a base, and only over a list.
Two rules are not overrides:
typeis 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.rangeandrangesare one slot. Stating either drops both from the template, so a template'srangeand yourrangesnever 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:
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 |