Skip to content

Write a metric plugin (Python)

When Understand's built-in metrics don't measure what you need, write a metric plugin. Once installed, your metric shows up everywhere metrics do — the Metrics Browser, the Metrics Treemap, a graph's color scale, and CSV/HTML exports.

Python only

Metric plugins are Python scripts (.upy). The Perl API is not supported for metrics.

The plugin contract

A metric plugin is a plain Python module — there is no base class. Understand classifies a .upy file as a metric plugin simply because it defines a top-level value function. A metric plugin must define:

Function Returns Purpose
ids() list of strings The metric ID(s) this script provides (one script can define several)
name(id) string Friendly name shown in the UI
value(metric, target) number The metric value for the target
at least one of test_entity / test_architecture / test_global bool Gates whether value is actually called for that target kind (see below)

Optional helpers: description(id) (tooltip / definition text shown in the Plugin Manager — expected to be HTML; plain text containing < or > can break the display), tags(id) (Plugin Manager tags), is_integer(id) (default is a real number), and define_options(metric) (project-configurable options, via metric.options()).

The first argument to value/test_*/define_options is a metric context (MetricContext), not the target itself:

Call Does
metric.id() Which metric ID was requested — needed when one script's ids() returns several
metric.db() The open database
metric.options() Define/look up user options (Options, same object CodeCheck and graph plugins use)
metric.list(target) Look up other built-in metrics safely from inside a plugin (see the warning below)

The target passed to value is an entity, an architecture, or the database, depending on which test_ matched. The full contract, with every function signature, is in the metric plugin API guide and the MetricContext class reference.

Availability: test_entity / test_architecture / test_global

Implement at least one of these three — each is paired with the matching call to value(metric, target) and actually makes the metric computable for that kind of target:

Function Target passed to value
test_entity(metric, ent) The entity
test_architecture(metric, arch) The architecture (a tag)
test_global(metric, db) The database

test_available only controls what's listed — it doesn't make a metric computable

test_available(metric, entkindstr) is a separate, optional function used only where there isn't a specific target yet — populating a column picker, a treemap's metric dropdown, the export dialog's metric list, or Metric.list(entkindstr). Implementing test_available alone is not sufficient: it never causes value to be called. The metric still needs a matching test_entity / test_architecture / test_global to actually run anywhere. entkindstr may be empty (return True if the metric applies to an entity, architecture, or the project generally) or a kind string to intersect against.

test_line is a different mechanism: per-line values

test_line(metric) doesn't pair with value at all — it gates a separate lines(metric, file) function that returns a {line_number: value} dictionary for a file, for metrics that have a value per line rather than one value per entity (e.g. CoverageHits — how many times each line executed). The example below has a working test_line/lines pair. Line metrics aren't shown in the Metrics Browser, the Entity Locator, or exports (those are all entity/architecture/project-scoped) — the one place they surface is coloring a graph edge by a line metric.

Reading other metrics from inside a plugin

To read another metric's value from within a plugin, use the context's metric.list(target) (or ent.metric([...])). Do not call the global Metric.list() / ent.metrics() inside a plugin — that re-enters the metric engine and can recurse.

Example: analysis errors & warnings, with a per-line metric

This trimmed-down version of the shipped plugins/Metric/analysis.upy defines two metrics from one script (CountAnalysisError, CountAnalysisWarning), each available on a file or on the whole project, and both with a per-line breakdown via test_line/lines:

import understand

def ids():
    return ["CountAnalysisError", "CountAnalysisWarning"]

def name(id):
    return "Analysis Errors" if id == "CountAnalysisError" else "Analysis Warnings"

def tags(id):
    return ['Target: Files', 'Target: Project', 'Language: Any', 'Line Metric']

def is_integer(id):
    return True

def test_entity(metric, ent):
    return ent.kind().check("file ~unresolved ~unknown")

def test_global(metric, db):
    return True

def test_line(metric):
    return True

def value(metric, target):
    check_id = "UND_ERROR" if metric.id() == "CountAnalysisError" else "UND_WARNING"
    return sum(1 for v in target.violations() if v.check_id() == check_id)

def lines(metric, file):
    check_id = "UND_ERROR" if metric.id() == "CountAnalysisError" else "UND_WARNING"
    linedict = {}
    for v in file.violations():
        if v.check_id() == check_id:
            linedict[v.line()] = linedict.get(v.line(), 0) + 1
    return linedict

target.violations() works whether target is the file entity or the database, so value doesn't need an isinstance branch here. The full Analysis Errors plugin adds test_architecture and a per-function line-range lookup on top of this — it's also the sample plugin fully annotated in the metric plugin API guide.

Install & run

Install a metric plugin like any other — drag the .upy onto the main window, or add it from the Plugin Manager. Understand recognizes it as a metric (it defines value) and files it under your per-user Metric plugin folder. See Install & run plugins for the directory locations and the Rescan Plugins step to run after editing, and Write a plugin for the plugin model and common gotchas (broken plugins, reloading, the per-interpreter GIL).

Once enabled, the metric shows up in the Metrics Browser, Entity Locator columns, exports, and graph color scales, wherever its test_* functions say it applies.

Performance note

Metrics are computed on a background thread, so a slow plugin won't freeze the UI — but the Metrics Browser calculates all metrics for the selected entity before displaying any, so an expensive metric (e.g. one that shells out to Git) does add latency. Keep value cheap, and gate availability tightly with test_*.

The public plugin repository has more examples to browse. Or, in the Plugin Manager, filter the installed set by the Sample Template tag to start from the recommended one — the plugin above.