Kida can track which template lines execute during rendering and export coverage data in standard formats (LCOV, Cobertura XML). Coverage collection is opt-in — it has zero overhead when disabled because the compiler-emitted_rc.line = N markers only record hits when a CoverageCollectoris active.
from kida.coverage import CoverageCollector
Quick Start
from kida import Environment, FileSystemLoader
from kida.coverage import CoverageCollector
env = Environment(loader=FileSystemLoader("templates/"))
cov = CoverageCollector()
with cov:
template = env.get_template("page.html")
template.render(title="Hello")
print(cov.summary())
cov.write_lcov("coverage.lcov")
Output:
Template Lines Hit Cover
-----------------------------------------------------------------------
page.html 8 8 100.0%
-----------------------------------------------------------------------
TOTAL 8 8 100.0%
CoverageCollector API
CoverageCollector is the primary interface. It dynamically patches RenderContext.__setattr__while active, so there is zero overhead when no collector is running.
Context Manager
The recommended usage is as a context manager:
with CoverageCollector() as cov:
template.render(**context)
# cov now contains all line hits from the render
Manual Start / Stop
For longer-lived collection (e.g., across an entire test suite):
cov = CoverageCollector()
cov.start()
# ... render many templates ...
cov.stop()
print(cov.summary())
Calling start() on an already-started collector is a no-op. Calling stop() resets the ContextVartoken and decrements the internal reference count.
Raw Data
Access the raw coverage data directly:
cov.data
# {"page.html": {1, 3, 5, 7, 8}, "base.html": {2, 4, 6}}
The data property returns a dict[str, set[int]]mapping template names to sets of executed line numbers.
Clearing Data
Reset collected data without creating a new collector:
cov.clear()
Getting Results
get_results() returns a list of CoverageResultobjects, one per template, sorted by name:
results = cov.get_results()
for r in results:
print(f"{r.template_name}: {r.hit_count}/{r.total_count} ({r.percentage:.1f}%)")
Pass an optional source_mapto provide full line enumeration for accurate miss counting:
source_map = {
"page.html": frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}),
}
results = cov.get_results(source_map=source_map)
for r in results:
print(f"{r.template_name}: {r.missed_count} lines missed")
Without a source_map, total_lines equals executed_lines(100% coverage for all touched templates). This is useful for hit tracking even without full line enumeration.
CoverageResult
| Property | Type | Description |
|---|---|---|
template_name |
str |
Template identifier |
executed_lines |
frozenset[int] |
Line numbers that executed |
total_lines |
frozenset[int] |
All trackable line numbers |
hit_count |
int |
Number of executed lines (property) |
total_count |
int |
Total trackable lines (property) |
missed_count |
int |
Lines not executed (property) |
percentage |
float |
Coverage percentage (property) |
Text Summary
summary()returns a formatted text table of coverage across all templates:
print(cov.summary(source_map=source_map))
If no templates were rendered, summary() returns "No templates rendered.".
LCOV Export
Write coverage data in LCOV tracefile format, compatible withlcov, genhtml, and most CI coverage tools:
cov.write_lcov("coverage.lcov")
Or get the LCOV content as a string:
lcov_string = cov.format_lcov()
The output follows the standard LCOV format:
SF:page.html
DA:1,1
DA:3,1
DA:5,1
LH:3
LF:3
end_of_record
Each DA: line records a line number and its hit count (always 1 for any executed line). LH is the number of lines hit, LFis the total number of lines in the record.
Cobertura Export
Write coverage data in Cobertura XML format, compatible with Jenkins, GitLab CI, and other CI systems:
cov.write_cobertura("coverage.xml")
Or get the XML content as a string:
xml_string = cov.format_cobertura()
The output follows the Cobertura schema. Templates are grouped under a single "templates" package, with each template represented as a class. The root element includes a line-rateattribute with the aggregate coverage ratio.
Thread Safety
CoverageCollector uses ContextVar for data isolation and a threading.Lockfor the global reference count. This means:
- Concurrent renders — each collector tracks only its own renders; parallel test runners or async handlers do not interfere with each other.
- Nested collectors — multiple
CoverageCollectorinstances can be active simultaneously. TheRenderContext.__setattr__patch stays active as long as at least one collector is running (reference-counted via_active_count). - Cleanup — when the last active collector stops, the
__setattr__patch is removed entirely, restoring zero-overhead rendering.
CI Integration Example
pytest + LCOV
import pytest
from kida.coverage import CoverageCollector
@pytest.fixture(scope="session")
def template_coverage():
cov = CoverageCollector()
cov.start()
yield cov
cov.stop()
cov.write_lcov("template-coverage.lcov")
cov.write_cobertura("template-coverage.xml")
def test_homepage(template_coverage, env):
template = env.get_template("home.html")
result = template.render(title="Home")
assert "Home" in result
def test_about(template_coverage, env):
template = env.get_template("about.html")
result = template.render(title="About")
assert "About" in result
GitHub Actions
- name: Run tests with template coverage
run: pytest --tb=short
- name: Upload template coverage
uses: codecov/codecov-action@v4
with:
files: template-coverage.lcov
flags: templates
GitLab CI
test:
script:
- pytest --tb=short
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: template-coverage.xml
See Also
- Profiling — Render-time instrumentation
- Static Analysis — Dependency and purity analysis