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.