-- -- --- --- --- --- --- --- ------- ------- ------- |
"""Annotate source code with metrics."""
-- -- --- --- --- --- --- --- ------- ------- ------- |
from __future__ import annotations
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
import json
-- -- --- --- --- --- --- --- ------- ------- ------- |
import logging
-- -- --- --- --- --- --- --- ------- ------- ------- |
import shutil
-- -- --- --- --- --- --- --- ------- ------- ------- |
from collections import defaultdict
-- -- --- --- --- --- --- --- ------- ------- ------- |
from pathlib import Path
-- -- --- --- --- --- --- --- ------- ------- ------- |
from string import Template
-- -- --- --- --- --- --- --- ------- ------- ------- |
from sys import exit
-- -- --- --- --- --- --- --- ------- ------- ------- |
from typing import Any, Optional
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
import click
-- -- --- --- --- --- --- --- ------- ------- ------- |
import git
-- -- --- --- --- --- --- --- ------- ------- ------- |
from git import Repo
-- -- --- --- --- --- --- --- ------- ------- ------- |
from pygments import highlight
-- -- --- --- --- --- --- --- ------- ------- ------- |
from pygments.formatters import HtmlFormatter, TerminalFormatter
-- -- --- --- --- --- --- --- ------- ------- ------- |
from pygments.lexers import PythonLexer
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
from wily import logger
-- -- --- --- --- --- --- --- ------- ------- ------- |
from wily.archivers import resolve_archiver
-- -- --- --- --- --- --- --- ------- ------- ------- |
from wily.config import DEFAULT_CONFIG_PATH
-- -- --- --- --- --- --- --- ------- ------- ------- |
from wily.config import load as load_config
-- -- --- --- --- --- --- --- ------- ------- ------- |
from wily.state import IndexedRevision, State
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
logger.setLevel(logging.INFO)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
# Maximum values for each metric, i.e. what will result in red background
-- -- --- --- --- --- --- --- ------- ------- ------- |
MAX_DICT = {
-- -- --- --- --- --- --- --- ------- ------- ------- |
"cc_function": 50,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"h1": 40,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"h2": 40,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"N1": 40,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"N2": 40,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"vocabulary": 80,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"length": 80,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"volume": 500,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"effort": 2000,
-- -- --- --- --- --- --- --- ------- ------- ------- |
"difficulty": 40,
-- -- --- --- --- --- --- --- ------- ------- ------- |
}
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
def get_metric_color(val: float, maximum: int = 50, name: Optional[str] = None) -> str:
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
"""
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
Calculate RGB values for a scale from green to red through yellow.
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
:param val: The value to convert to RGB color.
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
:param maximum: The maximum expected value, corresponding to one less than red.
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
:param name: A name to get a maximum value from MAX_DICT.
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
:return: A string of the form `"rgba(X, Y, 0, 0.75)"`.
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
"""
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
if name is not None:
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
maximum = MAX_DICT[name]
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
factor = 2 / maximum
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
red = max(0, min(255, round(factor * 255 * (val - 1))))
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
green = max(0, min(255, round(factor * 255 * (maximum - val + 1))))
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
blue = 0
-- 02 005 013 009 018 018 027 0112.59 0389.73 0003.46 |
return f"rgba{(red, green, blue, 0.75)}"
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
04 -- --- --- --- --- --- --- ------- ------- ------- |
class AnnotatedHTMLFormatter(HtmlFormatter):
04 -- --- --- --- --- --- --- ------- ------- ------- |
"""Annotate and color source code with metric values as HTML."""
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 -- --- --- --- --- --- --- ------- ------- ------- |
halstead_names = (
04 -- --- --- --- --- --- --- ------- ------- ------- |
"h1",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"h2",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"N1",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"N2",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"vocabulary",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"length",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"volume",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"effort",
04 -- --- --- --- --- --- --- ------- ------- ------- |
"difficulty",
04 -- --- --- --- --- --- --- ------- ------- ------- |
)
04 -- --- --- --- --- --- --- ------- ------- ------- |
metric_names = halstead_names + ("cc_function",)
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
def __init__(
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self, metrics: list[dict[int, tuple[str, str]]], **options: Any
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
) -> None:
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
"""Set up the formatter instance with metrics."""
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
super().__init__(**options)
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self.cyclomatic = metrics[0]
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self.halstead = metrics[1]
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
halstead_spans: list[str] = []
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
# These should match the column widths of Halstead metrics in map_halstead_lines
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
empty_halstead_vals = ("---",) * 6 + ("-------",) * 3
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
for name, val in zip(self.halstead_names, empty_halstead_vals):
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
halstead_spans.append(
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
f'<span class="halstead_span {name}_val {name}none">{val} </span>'
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
)
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self.empty_halstead_spans = "".join(halstead_spans)
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
# These should match the column widths of CC metrics in map_cyclomatic_lines
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
empty_cyclomatic_vals = ("--", "--")
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self.empty_cyclomatic_span = (
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
'<span class="cyclomatic_span cc_function_val cc_functionnone" style="background-color: #ffffff;">'
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
f'{" ".join(empty_cyclomatic_vals)} </span>'
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
)
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
# This will be used to create the CSS entries for all classes
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
self.metric_styles: dict[str, str] = {
04 03 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
f"{name}none": "#ffffff" for name in self.metric_names
04 -- 002 006 003 006 008 009 0027.00 0027.00 0001.00 |
}
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def wrap(self, source) -> None:
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""Wrap the ``source`` in custom generators."""
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
output = source
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
output = self.annotate_lines(output)
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
if self.wrapcode:
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
output = self._wrap_code(output)
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
output = self._wrap_pre(output)
04 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
return output
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
def annotate_lines(self, tokensource):
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
"""
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
Add metric annotations from self.cyclomatic and self.halstead.
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
A div is created for each code line, containing spans for each metric
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
value. This div and the spans have associated CSS classes that allow
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
changing the background color of the code to match selected metric
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
values and also hiding unselected metrics.
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
"""
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
for i, (_t, value) in enumerate(tokensource):
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
if not self.cyclomatic: # No metrics
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
yield 1, value
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
continue
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
div_classes = [f"{name}none" for name in self.metric_names]
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
if i in self.cyclomatic: # Line has metric info available
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
cyclomatic = self.get_cyclomatic_content(div_classes, i)
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
halstead = self.get_halstead_content(div_classes, i)
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
yield 1, (
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f'<div class="{" ".join(div_classes)}">'
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f"{cyclomatic}"
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f"{halstead}| {value}</div>"
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
)
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
else: # Line is after last known line, add empty metric spans
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
yield 1, (
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f'<div class="{" ".join(div_classes)}" style="background-color: #ffffff; width: 100%;">'
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f"{self.empty_cyclomatic_span}"
04 05 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
f"{self.empty_halstead_spans}| {value}</div>"
04 -- 002 002 002 003 004 005 0010.00 0015.00 0001.50 |
)
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
def get_halstead_content(self, div_classes: list[str], i: int) -> str:
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
"""
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
Build spans and add styles for Halstead metrics.
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:param div_classes: A list containing CSS class names.
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:param i: Index into self.halstead, corresponding to a source code line.
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:return: A string containing styled spans with Halstead metric values.
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
"""
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
if i not in self.halstead or self.halstead[i][1][1] == "-":
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
# Line is either not known or has empty metric value ("-").
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
halstead = self.empty_halstead_spans
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
else:
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
spans = []
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
for name, val in zip(self.halstead_names, self.halstead[i]):
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
val_ = int(float(val))
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
nameval = f"{name}{val_}"
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
spans.append(
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
f'<span class="halstead_span {name}_val {nameval}">{val} </span>'
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
)
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
if nameval not in self.metric_styles:
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
h = get_metric_color(val_, name=name)
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
self.metric_styles[nameval] = h
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
div_classes.append(f"{nameval}_code")
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
halstead = "".join(spans)
04 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
return halstead
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
def get_cyclomatic_content(self, div_classes: list[str], i: int) -> str:
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
"""
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
Build span and add styles for Cyclomatic Complexity.
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
:param div_classes: A list containing CSS class names.
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
:param i: Index into self.cyclomatic, corresponding to a source code line.
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
:return: A string containing styled spans with Cyclomatic metric values.
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
"""
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
if self.cyclomatic[i][1][1] == "-": # Just use function values for now
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
cyclomatic = self.empty_cyclomatic_span
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
else:
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
val = int(self.cyclomatic[i][1])
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
name = "cc_function"
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
c = get_metric_color(val, name=name)
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
cc_nameval = f"{name}{val}"
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
if cc_nameval not in self.metric_styles:
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
self.metric_styles[cc_nameval] = c
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
div_classes.append(f"{cc_nameval}_code")
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
cyclomatic = (
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
f'<span class="cyclomatic_span cc_function_val {cc_nameval}">'
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
f'{" ".join(self.cyclomatic[i])} </span>'
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
)
04 03 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
return cyclomatic
04 -- --- --- --- --- --- --- ------- ------- ------- |
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
def get_halstead_style_defs(self) -> str:
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""Get additional CSS rules from calculated styles seen."""
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
result = []
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
for name, value in self.metric_styles.items():
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
result.append(f".{name} {{ background-color: {value};}}")
04 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
return "\n" + "\n".join(result)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
03 -- --- --- --- --- --- --- ------- ------- ------- |
class AnnotatedTerminalFormatter(TerminalFormatter):
03 -- --- --- --- --- --- --- ------- ------- ------- |
"""Annotate and source code with metric values to print to terminal."""
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def __init__(self, metrics: dict[int, tuple[str, str]], **options: Any) -> None:
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""Set up the formatter instance with metrics."""
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
super().__init__(**options)
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
self.metrics = metrics
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
def _write_lineno(self, outfile) -> None:
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
"""Write line numbers and metric annotations."""
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
self._lineno += 1
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
metric_values = " ".join(self.metrics.get(self._lineno - 1, ("--", "--")))
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
outfile.write(
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
f"%s%04d: {metric_values} |"
03 03 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
% (self._lineno != 1 and "\n" or "", self._lineno)
-- -- 006 008 006 012 014 018 0068.53 0308.40 0004.50 |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
def last_line(details: dict) -> int:
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
Get the last line from a series of detailed metric entries.
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
:param details: A dict with detailed metric information, with line numbers.
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
:return: The number of the last known line.
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
lineends = []
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
for _name, detail in details.items():
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
endline: int = detail.get("endline", 0)
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
lineends.append(endline)
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
return max(lineends or [0])
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
def map_cyclomatic_lines(details: dict) -> dict[int, tuple[str, str]]:
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
"""
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
Map complexity metric values to lines, for functions/methods and classes.
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
:param details: A dict with detailed metric information, with line numbers.
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
:return: A dict mapping line numbers to Cyclomatic Complexity values.
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
"""
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
last = last_line(details)
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
lines = {i: ("--", "--") for i in range(last + 1)}
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
for _name, detail in details.items():
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
if "is_method" in detail: # It's a function or method
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
for line in range(detail["lineno"] - 1, detail["endline"]):
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
lines[line] = (lines[line][0], f"{detail['complexity']:02d}")
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
else: # It's a class
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
for line in range(detail["lineno"] - 1, detail["endline"]):
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
lines[line] = (f"{detail['complexity']:02d}", lines[line][1])
-- 06 003 006 004 008 009 012 0038.04 0076.08 0002.00 |
return lines
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
def map_halstead_lines(details: dict) -> dict[int, tuple[str, ...]]:
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
"""
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
Map Halstead metric values to lines, for functions.
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
:param details: A dict with detailed metric information, with line numbers.
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
:return: A dict mapping line numbers to Halstead values.
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
"""
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
last = last_line(details)
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
lines = {i: ("---",) * 6 + ("-------",) * 3 for i in range(last + 1)}
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
for _name, detail in details.items():
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
if "lineno" not in detail or detail["lineno"] is None:
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
continue
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
for line in range(detail["lineno"] - 1, detail["endline"]):
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
lines[line] = (
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['h1']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['h2']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['N1']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['N2']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['vocabulary']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['length']:03d}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['volume']:07.2f}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['effort']:07.2f}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
f"{detail['difficulty']:07.2f}",
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
)
-- 06 006 015 008 016 021 024 0105.42 0337.33 0003.20 |
return lines
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
def bulk_annotate(output_dir: Optional[Path] = None) -> None:
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
"""
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
Annotate all Python files found in the index's revisions.
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
:param output_dir: A Path pointing to the directory to output HTML files.
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
"""
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
config = load_config(DEFAULT_CONFIG_PATH)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
state = State(config)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
styles = {}
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
if output_dir is None:
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
output_dir = Path("reports")
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
reports_dir = Path(__file__).parents[1] / output_dir
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
reports_dir.mkdir(exist_ok=True)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
templates_dir = (Path(__file__).parent / "wily" / "templates").resolve()
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
shutil.copyfile(templates_dir / "annotated.js", reports_dir / "annotated.js")
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
css_output = reports_dir / "annotated.css"
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
css_output.unlink(missing_ok=True)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
latest = get_latest_rev(
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
config.cache_path, state.index[state.default_archiver].revision_keys
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
for filename, rev_key in latest.items():
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
try:
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
styles.update(
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
annotate_revision(
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
format="HTML",
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
revision_index=rev_key,
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
path=filename,
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
css=False,
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
output_dir=output_dir,
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
except FileNotFoundError:
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
logger.error(
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
f"Path {filename} not found in current state of git repository."
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
)
-- 04 002 011 007 014 013 021 0077.71 0098.90 0001.27 |
append_css(css_output, styles)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
def get_latest_rev(cache_path: str, revision_keys: list[str]) -> dict[str, str]:
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
"""
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
Get latest known revision for files.
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:param cache_path: The cache path from the config, used to find JSON files.
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:param revision_keys: A list of revision keys.
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
:return: A dict mapping filenames to last known revision.
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
"""
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
latest: dict[str, str] = {}
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
for rev_key in revision_keys:
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
rev_data = Path(cache_path) / "git" / f"{rev_key}.json"
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
as_dict = json.loads(rev_data.read_text())
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
cyclomatic = as_dict["operator_data"]["cyclomatic"]
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
for filename, _data in cyclomatic.items():
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
if filename.endswith(".py") and filename not in latest:
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
latest[filename] = rev_key
-- 05 003 008 004 008 011 012 0041.51 0062.27 0001.50 |
return latest
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
def append_css(css_output: Path, styles: dict[str, str]):
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
Append CSS from a style dict to a CSS file.
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
:param css_output: Path to the output CSS file.
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
:param styles: A dict of single CSS class names and background color values.
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
result = []
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
for name, value in simplify_css(styles).items():
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
result.append(f"{name} {{ background-color: {value};}}")
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
with css_output.open("a") as css:
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
css.write("\n\n" + "\n".join(result))
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def simplify_css(styles: dict[str, str]) -> dict[str, str]:
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
Collapse rules that use the same color to a single line.
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
:param styles: A dict of single CSS class names and background color values.
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
:return: A dict of multiple CSS class names mapping to background color values.
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
colors_to_rules: defaultdict[str, list[str]] = defaultdict(list)
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
for name, color in styles.items():
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
colors_to_rules[color].append(name)
-- 03 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
return {f".{', .'.join(names)}": color for color, names in colors_to_rules.items()}
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
def annotate_revision(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
format: str = "HTML",
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
revision_index: str = "",
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
path: str = "",
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
css: bool = False,
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
output_dir: Optional[Path] = None,
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
) -> dict[str, str]:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
"""
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
Generate annotated files from detailed metric data in a revision.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:param format: Either `HTML` or `CONSOLE`, determines output format.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:param revision_index: A Git revision to annotate at.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:param path: A single filename to annotate.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:param css: Whether to write a CSS file containing styles.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:param output_dir: A Path pointing to the directory to output HTML files.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
:return: A dict mapping CSS class names to color values.
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
"""
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
config = load_config(DEFAULT_CONFIG_PATH)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
state = State(config)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
repo = Repo(config.path)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if not revision_index:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
commit = repo.rev_parse("HEAD")
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
else:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
try:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
commit = repo.rev_parse(revision_index)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
except git.BadName:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.error(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
f"Revision {revision_index} not found in current git repository."
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
exit(1)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
rev = (
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
resolve_archiver(state.default_archiver)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
.archiver_cls(config)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
.find(commit.hexsha)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.debug(f"Resolved {revision_index} to {rev.key} ({rev.message})")
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
try:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
target_revision: IndexedRevision
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
target_revision = state.index[state.default_archiver][commit.hexsha]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
except KeyError:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.error(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
f"Revision {revision_index or 'HEAD'} is not in the cache, make sure you have run wily build."
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
exit(1)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
rev_key = target_revision.revision.key
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
rev_data = Path(config.cache_path) / "git" / f"{rev_key}.json"
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
as_dict = json.loads(rev_data.read_text())
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
cyclomatic = as_dict["operator_data"]["cyclomatic"]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
halstead = as_dict["operator_data"]["halstead"]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if path:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if path not in cyclomatic:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.error(f"Data for file {path} not found on revision {rev_key}.")
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
exit(1)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
else:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
py_files = [path]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
else:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
py_files = [key for key in cyclomatic if key.endswith(".py")]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if not py_files:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.error(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
f"Revision {rev_key} has no files with Cyclomatic Complexity data."
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
exit(1)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if format.lower() == "html":
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.info(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
f"Saving annotated source code for {', '.join(py_files)} at rev {rev_key[:7]}."
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
elif format.lower() == "console":
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
logger.info(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
f"Showing annotated source code for {', '.join(py_files)} at rev {rev_key[:7]}."
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
styles: dict[str, str] = {}
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
for filename in py_files:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
diff = commit.diff(None, filename)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
outdated = False
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if diff and diff[0].change_type in ("M",):
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
outdated = True
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
path_ = Path(filename)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if path_.exists() and not outdated:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
code = path_.read_text()
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
else:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
git_filename = filename.replace("\\", "/")
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
code = repo.git.execute(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
["git", "show", f"{rev_key}:{git_filename}"],
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
as_process=False,
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
stdout_as_string=True,
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
metrics = [
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
map_cyclomatic_lines(cyclomatic[filename]["detailed"]),
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
map_halstead_lines(halstead[filename]["detailed"]),
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
]
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if format.lower() == "html":
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
style = generate_annotated_html(
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
code, filename, metrics, target_revision.revision.key, output_dir
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
styles.update(style)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
elif format.lower() == "console":
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
print_annotated_source(code, metrics[0])
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if format.lower() == "html":
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
reports_dir = Path(__file__).parents[1] / output_dir
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
templates_dir = (Path(__file__).parent / "wily" / "templates").resolve()
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
js_file = reports_dir / "annotated.js"
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if not js_file.exists():
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
shutil.copyfile(templates_dir / "annotated.js", js_file)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
if css:
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
css_output = reports_dir / "annotated.css"
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
append_css(css_output, styles)
-- 22 007 034 022 040 041 062 0332.17 1367.75 0004.12 |
return styles
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def print_annotated_source(code: str, metrics: dict[int, tuple[str, str]]) -> None:
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
Print source annotated with metric to terminal.
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
:param code: The source code to highlight.
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
:param metrics: Map of lines to CC metric values.
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
result = highlight(
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
code,
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
PythonLexer(),
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
AnnotatedTerminalFormatter(
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
linenos=True,
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
metrics=metrics,
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
),
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
)
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
print(result)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
def generate_annotated_html(
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
code: str,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
filename: str,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
metrics: list[dict[int, tuple[str, str]]],
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
key: str,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
output_dir: Optional[Path] = None,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
) -> dict[str, str]:
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
"""
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
Generate an annotated HTML file from source code and metric data.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:param code: The source code to highlight.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:param filename: The filename to display in HTML and base HTML file name on.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:param metrics: Two maps of lines to metric values (CC and Halstead).
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:param key: A Git revision key to display in title.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:param output_dir: A Path pointing to the directory to output HTML files.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
:return: A map of CSS class names to background color values.
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
"""
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
formatter = AnnotatedHTMLFormatter(
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
title=f"CC for {filename} at {key[:7]}",
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
lineanchors="line",
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
anchorlinenos=True,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
filename=filename,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
linenos=True,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
full=False,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
metrics=metrics,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
)
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
result = highlight(
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
code,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
PythonLexer(),
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
formatter=formatter,
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
)
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
if output_dir is None:
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
output_dir = Path("reports")
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
reports_dir = Path(__file__).parents[1] / output_dir
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
reports_dir.mkdir(parents=True, exist_ok=True)
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
htmlname = filename.replace("\\", ".").replace("/", ".")
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
output = reports_dir / f"annotated_{htmlname}.html"
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
logger.info(f"Saving {filename} annotated source code to {output}.")
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
templates_dir = (Path(__file__).parent / "wily" / "templates").resolve()
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
report_template = Template((templates_dir / "annotated_template.html").read_text())
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
result = report_template.safe_substitute(filename=filename, annotated=result)
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
with output.open("w", errors="xmlcharrefreplace") as html:
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
html.write(result)
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
css_output = reports_dir / "annotated.css"
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
if not css_output.exists():
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
with css_output.open("w") as css:
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
css.write(formatter.get_style_defs())
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
if not (reports_dir / "annotated.js").exists():
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
shutil.copyfile(templates_dir / "annotated.js", reports_dir / "annotated.js")
-- 04 003 015 012 022 018 034 0141.78 0311.91 0002.20 |
return formatter.metric_styles
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.group(help="Annotate source files with metric values.")
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def run() -> None:
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
pass
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
@run.command()
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-f",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--format",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default="CONSOLE",
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Save HTML or print to CONSOLE.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
type=click.STRING,
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-r",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--revision",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default="HEAD",
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Annotate with metric values from specific revision.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
type=click.STRING,
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-p",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--path",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default="",
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Path to annotate.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
type=click.Path(),
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-c",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--css/--no-css",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default=True,
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Write CSS file with styles to highlight code and metrics.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-o",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--output",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default="reports",
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Output directory to write files to.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
type=click.Path(path_type=Path),
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
def annotate(format: str, revision: str, path: str, css: bool, output: Path) -> None:
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
"""Generate annotated source for a revision or single file."""
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
if format.lower() not in ("html", "console"):
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
logger.error(f"Format must be HTML or CONSOLE, not {format}.")
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
exit(1)
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
annotate_revision(
-- 02 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
format=format, revision_index=revision, path=path, css=css, output_dir=output
-- -- 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
@run.command("bulk-annotate")
-- -- --- --- --- --- --- --- ------- ------- ------- |
@click.option(
-- -- --- --- --- --- --- --- ------- ------- ------- |
"-o",
-- -- --- --- --- --- --- --- ------- ------- ------- |
"--output",
-- -- --- --- --- --- --- --- ------- ------- ------- |
default="reports",
-- -- --- --- --- --- --- --- ------- ------- ------- |
help="Output directory to write files to.",
-- -- --- --- --- --- --- --- ------- ------- ------- |
type=click.Path(path_type=Path),
-- -- --- --- --- --- --- --- ------- ------- ------- |
)
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
def bulk(output: Path):
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
"""Annotate all Python files from all known revisions."""
-- 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
bulk_annotate(output_dir=output)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
if __name__ == "__main__":
-- -- --- --- --- --- --- --- ------- ------- ------- |
run()