class MaintainabilityIndexOperator(BaseOperator): """MI Operator.""" name = "maintainability" defaults = { "exclude": None, "ignore": None, "min": "A", "max": "C", "multi": True, "show": False, "sort": False, "include_ipynb": True, "ipynb_cells": True, } metrics = ( Metric("rank", _("Maintainability Ranking"), str, MetricType.Informational, mode), Metric("mi", _("Maintainability Index"), float, MetricType.AimHigh, statistics.mean), ) default_metric_index = 1 # MI def __init__(self, config, targets): """ Instantiate a new MI operator. :param config: The wily configuration. :type config: :class:`WilyConfig` """ # TODO : Import config from wily.cfg logger.debug(f"Using {targets} with {self.defaults} for MI metrics") self.harvester = harvesters.MIHarvester(targets, config=Config(**self.defaults)) def run(self, module, options): """ Run the operator. :param module: The target module path. :type module: ``str`` :param options: Any runtime options. :type options: ``dict`` :return: The operator results. :rtype: ``dict`` """ logger.debug("Running maintainability harvester") results = {} for filename, metrics in dict(self.harvester.results).items(): results[filename] = {"total": metrics} return results
def handle_no_cache(context): """Handle lack-of-cache error, prompt user for index process.""" logger.error( _("Could not locate wily cache, the cache is required to provide insights.") ) p = input(_("Do you want to run setup and index your project now? [y/N]")) if p.lower() != "y": exit(1) else: revisions = input(_("How many previous git revisions do you want to index? : ")) revisions = int(revisions) path = input(_("Path to your source files; comma-separated for multiple: ")) paths = path.split(",") context.invoke(build, max_revisions=revisions, targets=paths, operators=None)
def clean(ctx, yes): """Clear the .wily/ folder.""" config = ctx.obj["CONFIG"] if not exists(config): logger.info(_("Wily cache does not exist, nothing to remove.")) exit(0) if not yes: p = input(_("Are you sure you want to delete wily cache? [y/N]")) if p.lower() != "y": exit(0) from wily.cache import clean clean(config)
def build(ctx, max_revisions, targets, operators, archiver): """Build the wily cache.""" config = ctx.obj["CONFIG"] from wily.commands.build import build if max_revisions: logger.debug(f"Fixing revisions to {max_revisions}") config.max_revisions = max_revisions if operators: logger.debug(f"Fixing operators to {operators}") config.operators = operators.strip().split(",") if archiver: logger.debug(f"Fixing archiver to {archiver}") config.archiver = archiver if targets: logger.debug(f"Fixing targets to {targets}") config.targets = targets build( config=config, archiver=resolve_archiver(config.archiver), operators=resolve_operators(config.operators), ) logger.info( _("Completed building wily history, run `wily report <file>` or `wily index` to see more." ))
def get_default_metrics(config): """ Get the default metrics for a configuration. :param config: The configuration :type config: :class:`wily.config.WilyConfig` :return: Return the list of default metrics in this index :rtype: ``list`` of ``str`` """ archivers = list_archivers(config) default_metrics = [] for archiver in archivers: index = get_archiver_index(config, archiver) if len(index) == 0: logger.warning( _("No records found in the index, no metrics available")) return [] operators = index[0]["operators"] for operator in operators: o = resolve_operator(operator) if o.cls.default_metric_index is not None: metric = o.cls.metrics[o.cls.default_metric_index] default_metrics.append("{0}.{1}".format( o.cls.name, metric.name)) return default_metrics
:rtype: ``dict`` """ raise NotImplementedError from wily.operators.cyclomatic import CyclomaticComplexityOperator from wily.operators.maintainability import MaintainabilityIndexOperator from wily.operators.raw import RawMetricsOperator from wily.operators.halstead import HalsteadOperator """Type for an operator.""" Operator = namedtuple("Operator", "name cls description level") OPERATOR_CYCLOMATIC = Operator( name="cyclomatic", cls=CyclomaticComplexityOperator, description=_("Cyclomatic Complexity of modules"), level=OperatorLevel.Object, ) OPERATOR_RAW = Operator( name="raw", cls=RawMetricsOperator, description=_("Raw Python statistics"), level=OperatorLevel.File, ) OPERATOR_MAINTAINABILITY = Operator( name="maintainability", cls=MaintainabilityIndexOperator, description=_("Maintainability index (lines of code and branching)"), level=OperatorLevel.File,
class HalsteadOperator(BaseOperator): """Halstead Operator.""" name = "halstead" defaults = { "exclude": None, "ignore": None, "min": "A", "max": "C", "multi": True, "show": False, "sort": False, "by_function": True, "include_ipynb": True, "ipynb_cells": True, } metrics = ( Metric("h1", _("Unique Operands"), int, MetricType.AimLow, sum), Metric("h2", _("Unique Operators"), int, MetricType.AimLow, sum), Metric("N1", _("Number of Operands"), int, MetricType.AimLow, sum), Metric("N2", _("Number of Operators"), int, MetricType.AimLow, sum), Metric( "vocabulary", _("Unique vocabulary (h1 + h2)"), int, MetricType.AimLow, sum ), Metric("length", _("Length of application"), int, MetricType.AimLow, sum), Metric("volume", _("Code volume"), float, MetricType.AimLow, sum), Metric("difficulty", _("Difficulty"), float, MetricType.AimLow, sum), Metric("effort", _("Effort"), float, MetricType.AimLow, sum), ) default_metric_index = 0 # MI def __init__(self, config, targets): """ Instantiate a new HC operator. :param config: The wily configuration. :type config: :class:`WilyConfig` """ # TODO : Import config from wily.cfg logger.debug(f"Using {targets} with {self.defaults} for HC metrics") self.harvester = harvesters.HCHarvester(targets, config=Config(**self.defaults)) def run(self, module, options): """ Run the operator. :param module: The target module path. :type module: ``str`` :param options: Any runtime options. :type options: ``dict`` :return: The operator results. :rtype: ``dict`` """ logger.debug("Running halstead harvester") results = {} for filename, details in dict(self.harvester.results).items(): results[filename] = {"detailed": {}, "total": {}} for instance in details: if isinstance(instance, list): for item in instance: function, report = item results[filename]["detailed"][function] = self._report_to_dict( report ) else: if isinstance(instance, str) and instance == "error": logger.debug( f"Failed to run Halstead harvester on {filename} : {details['error']}" ) continue results[filename]["total"] = self._report_to_dict(instance) return results def _report_to_dict(self, report): return { "h1": report.h1, "h2": report.h2, "N1": report.N1, "N2": report.N2, "vocabulary": report.vocabulary, "volume": report.volume, "length": report.length, "effort": report.effort, "difficulty": report.difficulty, }
"""Main command line.""" import click import traceback from pathlib import Path from wily import logger, __version__, WILY_LOG_NAME from wily.archivers import resolve_archiver from wily.cache import exists, get_default_metrics from wily.config import DEFAULT_CONFIG_PATH, DEFAULT_GRID_STYLE from wily.config import load as load_config from wily.helper.custom_enums import ReportFormat from wily.operators import resolve_operators from wily.lang import _ version_text = _("Version: ") + __version__ + "\n\n" help_header = version_text + _( """\U0001F98A Inspect and search through the complexity of your source code. To get started, run setup: $ wily setup To reindex any changes in your source code: $ wily build <src> Then explore basic metrics with: $ wily report <file> You can also graph specific metrics in a browser with:
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`` """ metrics = sorted(metrics) 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: increase_color = ANSI_GREEN decrease_color = ANSI_RED elif metric.measure == MetricType.AimLow: increase_color = ANSI_RED decrease_color = ANSI_GREEN elif metric.measure == MetricType.Informational: increase_color = ANSI_YELLOW decrease_color = ANSI_YELLOW else: increase_color = ANSI_YELLOW decrease_color = ANSI_YELLOW metric_meta = { "key": key, "operator": operator, "increase_color": increase_color, "decrease_color": decrease_color, "title": metric.description, "type": metric.type, } 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): 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['decrease_color']}m{delta:n}\u001b[0m" else: delta_col = f"\u001b[{meta['increase_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( tabulate.tabulate( headers=headers, tabular_data=data[::-1], tablefmt=console_format ) )
class CyclomaticComplexityOperator(BaseOperator): """Cyclomatic complexity operator.""" name = "cyclomatic" defaults = { "exclude": None, "ignore": None, "min": "A", "max": "F", "no_assert": True, "show_closures": False, "order": radon.complexity.SCORE, "include_ipynb": True, "ipynb_cells": True, } metrics = ( Metric( "complexity", _("Cyclomatic Complexity"), float, MetricType.AimLow, statistics.mean, ), ) default_metric_index = 0 # MI def __init__(self, config, targets): """ Instantiate a new Cyclomatic Complexity operator. :param config: The wily configuration. :type config: :class:`WilyConfig` """ # TODO: Import config for harvester from .wily.cfg logger.debug(f"Using {targets} with {self.defaults} for CC metrics") self.harvester = harvesters.CCHarvester(targets, config=Config(**self.defaults)) def run(self, module, options): """ Run the operator. :param module: The target module path. :type module: ``str`` :param options: Any runtime options. :type options: ``dict`` :return: The operator results. :rtype: ``dict`` """ logger.debug("Running CC harvester") results = {} for filename, details in dict(self.harvester.results).items(): results[filename] = {"detailed": {}, "total": {}} total = 0 # running CC total for instance in details: if isinstance(instance, Class): i = self._dict_from_class(instance) elif isinstance(instance, Function): i = self._dict_from_function(instance) else: if isinstance(instance, str) and instance == "error": logger.debug( f"Failed to run CC harvester on {filename} : {details['error']}" ) continue else: logger.warning( f"Unexpected result from Radon : {instance} of {type(instance)}. Please report on Github." ) continue results[filename]["detailed"][i["fullname"]] = i del i["fullname"] total += i["complexity"] results[filename]["total"]["complexity"] = total return results @staticmethod def _dict_from_function(l): return { "name": l.name, "is_method": l.is_method, "classname": l.classname, "closures": l.closures, "complexity": l.complexity, "fullname": l.fullname, "loc": l.endline - l.lineno, } @staticmethod def _dict_from_class(l): return { "name": l.name, "inner_classes": l.inner_classes, "real_complexity": l.real_complexity, "complexity": l.complexity, "fullname": l.fullname, "loc": l.endline - l.lineno, }
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))