def rank(config, path, metric, revision_index, limit, threshold, descending): """ Rank command ordering files, methods or functions using metrics. :param config: The configuration :type config: :class:'wily.config.WilyConfig' :param path: The path to the file :type path ''str'' :param metric: Name of the metric to report on :type metric: ''str'' :param revision_index: Version of git repository to revert to. :type revision_index: ``str`` :param limit: Limit the number of items in the table :type limit: ``int`` :param threshold: For total values beneath the threshold return a non-zero exit code :type threshold: ``int`` :return: Sorted table of all files in path, sorted in order of metric. """ logger.debug("Running rank command") data = [] operator, metric = resolve_metric_as_tuple(metric) operator = operator.name state = State(config) if not revision_index: target_revision = state.index[state.default_archiver].last_revision else: rev = resolve_archiver( state.default_archiver).cls(config).find(revision_index) logger.debug(f"Resolved {revision_index} to {rev.key} ({rev.message})") try: target_revision = state.index[state.default_archiver][rev.key] except KeyError: logger.error( f"Revision {revision_index} is not in the cache, make sure you have run wily build." ) exit(1) logger.info( f"-----------Rank for {metric.description} for {format_revision(target_revision.revision.key)} by {target_revision.revision.author_name} on {format_date(target_revision.revision.date)}.------------" ) if path is None: files = target_revision.get_paths(config, state.default_archiver, operator) logger.debug(f"Analysing {files}") else: # Resolve target paths when the cli has specified --path if config.path != DEFAULT_PATH: targets = [str(Path(config.path) / Path(path))] else: targets = [path] # Expand directories to paths files = [ os.path.relpath(fn, config.path) for fn in radon.cli.harvest.iter_filenames(targets) ] logger.debug(f"Targeting - {files}") for item in files: for archiver in state.archivers: try: logger.debug( f"Fetching metric {metric.name} for {operator} in {str(item)}" ) val = target_revision.get(config, archiver, operator, str(item), metric.name) value = val data.append((item, value)) except KeyError: logger.debug(f"Could not find file {item} in index") # Sort by ideal value data = sorted(data, key=op.itemgetter(1), reverse=descending) if limit: data = data[:limit] # Tack on the total row at the end total = metric.aggregate(rev[1] for rev in data) data.append(["Total", total]) headers = ("File", metric.description) print( tabulate.tabulate(headers=headers, tabular_data=data, tablefmt=DEFAULT_GRID_STYLE)) if threshold and total < threshold: logger.error( f"Total value below the specified threshold: {total} < {threshold}" ) exit(1)
def metric_parts(metric): """Convert a metric name into the operator and metric names.""" operator, met = resolve_metric_as_tuple(metric) return operator.name, met.name
def report( config, path, metrics, n, output, include_message=False, format=ReportFormat.CONSOLE, console_format=None, ): """ Show information about the cache and runtime. :param config: The configuration :type config: :class:`wily.config.WilyConfig` :param path: The path to the file :type path: ``str`` :param metrics: Name of the metric to report on :type metrics: ``str`` :param n: Number of items to list :type n: ``int`` :param output: Output path :type output: ``Path`` :param include_message: Include revision messages :type include_message: ``bool`` :param format: Output format :type format: ``ReportFormat`` :param console_format: Grid format style for tabulate :type console_format: ``str`` """ logger.debug("Running report command") logger.info(f"-----------History for {metrics}------------") data = [] metric_metas = [] for metric in metrics: operator, metric = resolve_metric_as_tuple(metric) key = metric.name operator = operator.name # Set the delta colors depending on the metric type if metric.measure == MetricType.AimHigh: good_color = 32 bad_color = 31 elif metric.measure == MetricType.AimLow: good_color = 31 bad_color = 32 elif metric.measure == MetricType.Informational: good_color = 33 bad_color = 33 metric_meta = { "key": key, "operator": operator, "good_color": good_color, "bad_color": bad_color, "title": metric.description, "type": metric.type, } metric_metas.append(metric_meta) state = State(config) for archiver in state.archivers: # We have to do it backwards to get the deltas between releases history = state.index[archiver].revisions[:n][::-1] last = {} for rev in history: vals = [] for meta in metric_metas: try: logger.debug( f"Fetching metric {meta['key']} for {meta['operator']} in {path}" ) val = rev.get(config, archiver, meta["operator"], path, meta["key"]) last_val = last.get(meta["key"], None) # Measure the difference between this value and the last if meta["type"] in (int, float): if last_val: delta = val - last_val else: delta = 0 last[meta["key"]] = val else: # TODO : Measure ranking increases/decreases for str types? delta = 0 if delta == 0: delta_col = delta elif delta < 0: delta_col = f"\u001b[{meta['good_color']}m{delta:n}\u001b[0m" else: delta_col = f"\u001b[{meta['bad_color']}m+{delta:n}\u001b[0m" if meta["type"] in (int, float): k = f"{val:n} ({delta_col})" else: k = f"{val}" except KeyError as e: k = f"Not found {e}" vals.append(k) if include_message: data.append(( format_revision(rev.revision.key), rev.revision.message[:MAX_MESSAGE_WIDTH], rev.revision.author_name, format_date(rev.revision.date), *vals, )) else: data.append(( format_revision(rev.revision.key), rev.revision.author_name, format_date(rev.revision.date), *vals, )) descriptions = [meta["title"] for meta in metric_metas] if include_message: headers = ("Revision", "Message", "Author", "Date", *descriptions) else: headers = ("Revision", "Author", "Date", *descriptions) if format == ReportFormat.HTML: if output.is_file and output.suffix == ".html": report_path = output.parents[0] report_output = output else: report_path = output report_output = output.joinpath("index.html") report_path.mkdir(exist_ok=True, parents=True) templates_dir = (Path(__file__).parents[1] / "templates").resolve() report_template = Template( (templates_dir / "report_template.html").read_text()) table_headers = "".join([f"<th>{header}</th>" for header in headers]) table_content = "" for line in data[::-1]: table_content += "<tr>" for element in line: element = element.replace("[32m", "<span class='green-color'>") element = element.replace("[31m", "<span class='red-color'>") element = element.replace("[33m", "<span class='orange-color'>") element = element.replace("[0m", "</span>") table_content += f"<td>{element}</td>" table_content += "</tr>" report_template = report_template.safe_substitute( headers=table_headers, content=table_content) with report_output.open("w") as output: output.write(report_template) try: copytree(str(templates_dir / "css"), str(report_path / "css")) except FileExistsError: pass logger.info(f"wily report was saved to {report_path}") else: print( # But it still makes more sense to show the newest at the top, so reverse again tabulate.tabulate(headers=headers, tabular_data=data[::-1], tablefmt=console_format))
def report( config: WilyConfig, path: Path, metrics: str, n: int, output: Path, include_message: bool = False, format: ReportFormat = ReportFormat.CONSOLE, console_format: str = None, ) -> None: """ Show information about the cache and runtime. :param config: The configuration :type config: :class:`wily.config.WilyConfig` :param path: The path to the file :type path: ``str`` :param metrics: Name of the metric to report on :type metrics: ``str`` :param n: Number of items to list :type n: ``int`` :param output: Output path :type output: ``Path`` :param include_message: Include revision messages :type include_message: ``bool`` :param format: Output format :type format: ``ReportFormat`` :param console_format: Grid format style for tabulate :type console_format: ``str`` """ logger.debug("Running report command") logger.info(f"-----------History for {metrics}------------") data = [] metric_metas = [] for metric in metrics: operator, metric = resolve_metric_as_tuple(metric) # Set the delta colors depending on the metric type metric_meta = { "key": metric.name, "operator": operator.name, "title": metric.description, "type": metric.type, "measure": metric.measure, } metric_metas.append(metric_meta) state = State(config) for archiver in state.archivers: history = state.index[archiver].revisions[:n][::-1] last = {} for rev in history: vals = [] for meta in metric_metas: try: logger.debug( f"Fetching metric {meta['key']} for {meta['operator']} in {path}" ) val = rev.get(config, archiver, meta["operator"], path, meta["key"]) last_val = last.get(meta["key"], None) # Measure the difference between this value and the last if meta["type"] in (int, float): delta = val - last_val if last_val else 0 change = delta elif last_val: delta = ord(last_val) - ord( val) if last_val != val else 1 change = last_val else: delta = 1 change = val last[meta["key"]] = val if delta == 0: delta_col = delta elif delta < 0: delta_col = _plant_delta_color( BAD_COLORS[meta["measure"]], change) else: delta_col = _plant_delta_color( GOOD_COLORS[meta["measure"]], change) k = _plant_delta(val, delta_col) except KeyError as e: k = f"Not found {e}" vals.append(k) if include_message: data.append(( format_revision(rev.revision.key), rev.revision.message[:MAX_MESSAGE_WIDTH], rev.revision.author_name, format_date(rev.revision.date), *vals, )) else: data.append(( format_revision(rev.revision.key), rev.revision.author_name, format_date(rev.revision.date), *vals, )) descriptions = [meta["title"] for meta in metric_metas] if include_message: headers = (_("Revision"), _("Message"), _("Author"), _("Date"), *descriptions) else: headers = (_("Revision"), _("Author"), _("Date"), *descriptions) if format in FORMAT_MAP: FORMAT_MAP[format](path, output, data, headers) return print( tabulate.tabulate(headers=headers, tabular_data=data[::-1], tablefmt=console_format))