The Understand Python API gives scripts access to everything Understand knows about your code — entities, references, metrics, and dependencies — and powers custom checks, graphs, and reports.
Metrics#
A metric is a numeric value about the code such as the lines of code or cyclomatic complexity. Many metrics are built into Understand and additional metrics can be defined through plugins. Metrics are used for many charts, and to create color scales in graphs. They are also displayed in table views such as the Understand GUI’s Entity Locator, allowing for sorting.
Discovery#
Available metrics depend on the target: a Db,
an Ent, or an Arch. For
example, function entities have values for Cyclomatic but file entities,
databases, and architectures only have the aggregate AvgCyclomatic,
MaxCyclomatic, and SumCyclomatic metrics.
Use Metric.list to discover available metrics.
The returned Metric objects provide metadata such
as the id, name,
and description.
List catalog metrics that apply to the project (pass a
Db so availability matches the open database):
import understand
db = understand.open("/path/to/myproject.und")
for metric in understand.Metric.list(db):
print(metric.id(), "-", metric.name())
Generation#
To compute values from scripts, call ent.metric,
arch.metric, or
db.metric for entities, architectures, and
databases respectively. Each metric method has the same signature.
The metric is identified by id (str) or by Metric
objects. Metric can be a single metric or a list.
For historical reasons, integer valued metrics are returned as Python int objects
but real valued metrics are returned as formatted strings. Use the format
argument to control the output format.
Compute every project-level metric at once:
import understand
db = understand.open("/path/to/myproject.und")
metrics = understand.Metric.list(db)
for metric, value in db.metric(metrics).items():
print(f"{metric.id()} = {value}")
Read Cyclomatic complexity for each function-like entity:
import understand
db = understand.open("/path/to/myproject.und")
for func in db.ents("function,method,procedure"):
cyclo = func.metric("Cyclomatic")
if cyclo is not None:
print(f"{func} = {cyclo}")
Plugin writing#
A metric plugin must have the methods ids, name, and value. A single script may define multiple metrics by returning multiple ids from the ids function. At least one test_ function should be defined to indicate when the metric is available.
The parameter metric passed to test_* and value is an
MetricContext object. A metric plugin script
that defines multiple metrics can use the id
method to find the requested metric id. The db
is also available.
The options method can be used to define
and retrieve options through an options object.
Metric options are project specific and configured through the project
configuration dialog. A metric must be enabled to be visible in project
configuration.
Important: If the plugin needs to know what other metrics are available for
an entity, architecture, or database, it must use the metric parameter’s
list method. It is not safe to call any other
methods that list metrics from inside
a metric plugin such as ent.metrics,
arch.metrics,
db.metrics, or
Metric.list. Those metric list functions
may call back into the metrics plugin leading to infinite recursion. It is safe
to retrieve metric values using the normal metric methods
( ent.metric,
arch.metric, or
db.metric).
The sample below reports the number of analysis errors and warnings for a
file, an architecture, or the whole project, and also demonstrates a
per-line metric with test_line/lines.
# A sample metrics plugin.
#
# Reports the number of analysis errors and warnings for a file, a function, an
# architecture, or the whole project, plus a per-line breakdown.
import understand
# This import is part of plugins/Shared
import und_lib.kind_util as kind_util
metDict = {
"CountAnalysisError" : ["Analysis Errors", "The number of analysis errors"],
"CountAnalysisWarning" : ["Analysis Warnings", "The number of analysis warnings"]
}
def ids():
"""
Required, a list of metric ids that this script provides.
"""
return [
"CountAnalysisError",
"CountAnalysisWarning",
]
def name(id):
"""
Required, the name of the metric given by id.
"""
return {
"CountAnalysisError" : "Analysis Errors",
"CountAnalysisWarning": "Analysis Warnings",
}.get(id, "")
def description(id):
"""
Optional, the description of the metric given by id.
"""
if id == "CountAnalysisError":
return """<p>The number of analysis errors</p>
<p>It's important to fix analysis errors to get an accurate project. Check
out the top level "Project" menu's "Improve Project Accuracy" menu for help
fixing analysis errors. There's also a <a
href="https://docs.scitools.com/help/accuracy/accurate-c-cpp-projects.html">
support article ↗</a>.</p>
<p>See also <a href="und://plugin/metric/CountAnalysisWarning">Analysis
Warnings</a> for a count of analysis warnings.</p>
"""
if id == "CountAnalysisWarning":
return """<p>The number of analysis warnings</p>
<p>Check out the top level "Project" menu's "Improve Project Accuracy" menu
for help with analysis accuracy. There's also a <a
href="https://docs.scitools.com/help/accuracy/accurate-c-cpp-projects.html">
support article ↗</a>.</p>
<p>See also <a href="und://plugin/metric/CountAnalysisError">Analysis
Errors</a> for a count of analysis errors.</p>
"""
def tags(id):
"""
Optional, tags to display in the plugin manager.
"""
taglist = [
'Target: Functions',
'Target: Files',
'Target: Architectures',
'Target: Project',
'Language: Any',
'Line Metric',
]
if id == "CountAnalysisError":
taglist.append('Sample Template') # Pick a metric arbitrarily as the official sample
return taglist
def define_options(metric):
"""
Optional, define options using the metric.options() object.
"""
pass
def is_integer(id):
"""
Optional, return True if the metric value is an integer.
If this function it not implemented, it is assumed false, meaning the
value should be represented as a double/float.
"""
return True
# One of the following three test functions should return True.
def test_entity(metric, ent):
"""
Optional, return True if metric can be calculated for the given entity.
"""
return ent.kind().check(kind_util.LEXER_ENTS_KIND_STR)
def test_architecture(metric, arch):
"""
Optional, return True if metric can be calculated for the given architecture.
"""
return True
def test_global(metric, db):
"""
Optional, return True if metric can be calculated for the given database.
"""
return True
def test_available(metric,entkindstr):
"""
Optional, return True if the metric is potentially available.
This is used when there isn't a specific target for the metric, like lists
of metrics available for export, or for a treemap.
Use metric.db() to retrieve the database. If the metric is language specific,
the code might look like this:
return "Ada" in metric.db().language()
entkindstr may be empty. If it is empty, return True as long as the metric
is available for an entity, architecture, or the project as a whole.
If entkindstr is not empty, return True only if the metric is available for
entities matching the provided kind string. Kind checks are performed like
this:
my_kinds = set(understand.Kind.list_entity(myMetricKindString)
test_kinds = set(understand.Kind.list_entity(entkindstr)
return len(my_kinds.intersection(test_kinds)) > 0
"""
# Kind check if requested
if entkindstr:
my_kinds = set(kind_util.LEXER_ENTS_KIND_STR)
test_kinds = set(understand.Kind.list_entity(entkindstr))
return len(my_kinds.intersection(test_kinds)) > 0
# Violation counts are always available
return True
def test_line(metric):
"""
Optional, return True if the metric has values for line.
Values per line are returned as a dictionary from line number to line
value from the lines function (see below).
"""
return True
def value(metric, target):
"""
Required, return the metric value for the target. The target may be
an entity, architecture, or database depending on which test functions
returned True.
"""
viols = []
if isinstance(target, understand.Arch):
for ent in target.ents(True):
viols += ent_violations(ent)
elif isinstance(target, understand.Db):
viols = target.violations()
else:
viols = ent_violations(target)
counts = dict()
for v in viols:
counts[v.check_id()] = counts.get(v.check_id(),0) + 1
if metric.id() == "CountAnalysisError":
return counts.get("UND_ERROR",0)
else:
return counts.get("UND_WARNING",0)
def lines(metric, file):
"""
Optional, return a dictionary from line number to line value.
This method is called if test_line returns True. The dictionary does
not have to include values for every line.
"""
if not file.kind().check("file ~unresolved ~unknown"):
return
id = "UND_WARNING"
if metric.id() == "CountAnalysisError":
id = "UND_ERROR"
linedict = dict()
for v in file.violations():
if v.check_id() == id:
linedict[v.line()] = linedict.get(v.line(),0) + 1
return linedict
def ent_violations(ent):
"""
This is a custom helper for this script: return the violations that apply
to an entity — a file's own violations, or, for a function, the file's
violations restricted to that function's line range.
"""
if ent.kind().check("file ~unresolved ~unknown"):
return ent.violations()
if not ent.kind().check(kind_util.FUNCTION_KIND_STR):
return [] # Violations not supported
defref = ent.ref("begin")
if not defref:
defref = ent.ref("definein, body declarein")
endref = ent.ref("end")
if not defref or not endref or defref.file() != endref.file():
return []
viols = []
for v in defref.file().violations():
if v.line() >= defref.line() and v.line() <= endref.line():
viols.append(v)
return viols