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 __init__(self, config, archiver=None): """ Instantiate a new process state. :param config: The wily configuration. :type config: :class:`WilyConfig` :param archiver: The archiver (optional). :type archiver: :class:`wily.archivers.Archiver` """ if archiver: self.archivers = [archiver.name] else: self.archivers = cache.list_archivers(config) logger.debug( f"Initialised state indexes for archivers {self.archivers}") self.config = config self.index = {} for archiver in self.archivers: self.index[archiver] = Index(self.config, resolve_archiver(archiver)) self.default_archiver = self.archivers[0]
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 diff(config, files, metrics, changes_only=True, detail=True, revision=None): """ Show the differences in metrics for each of the files. :param config: The wily configuration :type config: :namedtuple:`wily.config.WilyConfig` :param files: The files to compare. :type files: ``list`` of ``str`` :param metrics: The metrics to measure. :type metrics: ``list`` of ``str`` :param changes_only: Only include changes files in output. :type changes_only: ``bool`` :param detail: Show details (function-level) :type detail: ``bool`` :param revision: Compare with specific revision :type revision: ``str`` """ config.targets = files files = list(files) state = State(config) # Resolve target paths when the cli has specified --path if config.path != DEFAULT_PATH: targets = [str(Path(config.path) / Path(file)) for file in files] else: targets = files # 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}") if not revision: target_revision = state.index[state.default_archiver].last_revision else: rev = resolve_archiver( state.default_archiver).cls(config).find(revision) logger.debug(f"Resolved {revision} to {rev.key} ({rev.message})") try: target_revision = state.index[state.default_archiver][rev.key] except KeyError: logger.error( f"Revision {revision} is not in the cache, make sure you have run wily build." ) exit(1) logger.info( f"Comparing current with {format_revision(target_revision.revision.key)} by {target_revision.revision.author_name} on {format_date(target_revision.revision.date)}." ) # Convert the list of metrics to a list of metric instances operators = {resolve_operator(metric.split(".")[0]) for metric in metrics} metrics = [(metric.split(".")[0], resolve_metric(metric)) for metric in metrics] results = [] # Build a set of operators with multiprocessing.Pool(processes=len(operators)) as pool: operator_exec_out = pool.starmap(run_operator, [(operator, None, config, targets) for operator in operators]) data = {} for operator_name, result in operator_exec_out: data[operator_name] = result # Write a summary table extra = [] for operator, metric in metrics: if detail and resolve_operator(operator).level == OperatorLevel.Object: for file in files: try: extra.extend([ f"{file}:{k}" for k in data[operator][file]["detailed"].keys() if k != metric.name and isinstance( data[operator][file]["detailed"][k], dict) ]) except KeyError: logger.debug(f"File {file} not in cache") logger.debug("Cache follows -- ") logger.debug(data[operator]) files.extend(extra) logger.debug(files) for file in files: metrics_data = [] has_changes = False for operator, metric in metrics: try: current = target_revision.get(config, state.default_archiver, operator, file, metric.name) except KeyError: current = "-" try: new = get_metric(data, operator, file, metric.name) except KeyError: new = "-" if new != current: has_changes = True if metric.type in (int, float) and new != "-" and current != "-": if current > new: metrics_data.append( "{0:n} -> \u001b[{2}m{1:n}\u001b[0m".format( current, new, BAD_COLORS[metric.measure])) elif current < new: metrics_data.append( "{0:n} -> \u001b[{2}m{1:n}\u001b[0m".format( current, new, GOOD_COLORS[metric.measure])) else: metrics_data.append("{0:n} -> {1:n}".format(current, new)) else: if current == "-" and new == "-": metrics_data.append("-") else: metrics_data.append("{0} -> {1}".format(current, new)) if has_changes or not changes_only: results.append((file, *metrics_data)) else: logger.debug(metrics_data) descriptions = [metric.description for operator, metric in metrics] headers = ("File", *descriptions) if len(results) > 0: print( # But it still makes more sense to show the newest at the top, so reverse again tabulate.tabulate(headers=headers, tabular_data=results, tablefmt=DEFAULT_GRID_STYLE))