diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index a19cfb4..80bbef9 100644 --- a/docstr_coverage/cli.py +++ b/docstr_coverage/cli.py @@ -13,7 +13,7 @@ from docstr_coverage.config_file import set_config_defaults from docstr_coverage.coverage import analyze from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.printers import LegacyPrinter +from docstr_coverage.printers import GitHubCommentPrinter, JsonPrinter, LegacyPrinter def do_include_filepath(filepath: str, exclude_re: Optional["re.Pattern"]) -> bool: @@ -261,6 +261,15 @@ def _assert_valid_key_value(k, v): default=".docstr_coverage", help="Deprecated. Use json config (--config / -C) instead", ) +@click.option( + "-o", + "--output", + type=click.Choice(["text", "json", "github-comment"]), + default="text", + help="Formatting style of the output (text, json, github-comment)", + show_default=True, + metavar="FORMAT", +) def execute(paths, **kwargs): """Measure docstring coverage for `PATHS`""" @@ -328,7 +337,14 @@ def execute(paths, **kwargs): show_progress = not kwargs["percentage_only"] results = analyze(all_paths, ignore_config=ignore_config, show_progress=show_progress) - LegacyPrinter(verbosity=kwargs["verbose"], ignore_config=ignore_config).print(results) + if kwargs["output"] == "json": + printer = JsonPrinter() + elif kwargs["output"] == "github-comment": + printer = GitHubCommentPrinter(verbosity=kwargs["verbose"], fail_under=kwargs["fail_under"]) + else: + printer = LegacyPrinter(verbosity=kwargs["verbose"], ignore_config=ignore_config) + + printer.print(results) file_results, total_results = results.to_legacy() diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 8ef21e2..a4f4f2a 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -1,9 +1,14 @@ """All logic used to print a recorded ResultCollection to stdout. Currently, this module is in BETA and its interface may change in future versions.""" +import json import logging +import os +from abc import ABC, abstractmethod +from math import floor from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.result_collection import FileStatus +from docstr_coverage.result_collection import FileStatus, ResultCollection +from tabulate import tabulate _GRADES = ( ("AMAZING! Your docstrings are truly a wonder to behold!", 100), @@ -32,7 +37,19 @@ def print_line(line=""): logger.info(line) -class LegacyPrinter: +class Printer(ABC): + @abstractmethod + def print(self, results: ResultCollection): + """Prints a provided set of results to stdout. + + Parameters + ---------- + results: ResultCollection + The information about docstr presence to be printed to stdout.""" + raise NotImplementedError() + + +class LegacyPrinter(Printer): """Printing functionality consistent with the original early-versions docstr-coverage outputs. In future versions, the interface of this class will be refined and an abstract superclass @@ -147,3 +164,63 @@ def _print_overall_statistics(self, results): ) print_line("Total coverage: {:.1f}% - Grade: {}".format(count.coverage(), grade)) + + +class JsonPrinter(Printer): + def print(self, results): + """Prints a provided set of results to stdout in format JSON. + + Parameters + ---------- + results: ResultCollection + The information about docstr presence to be printed to stdout.""" + + file_results, total_results = results.to_legacy() + print(json.dumps({"files": file_results, "result": total_results})) + + +class GitHubCommentPrinter(Printer): + def __init__(self, verbosity: int, fail_under: int): + self.verbosity = verbosity + self.fail_under = fail_under + + def print(self, results: ResultCollection): + """Prints a provided set of results to stdout in format JSON. + + Parameters + ---------- + results: ResultCollection + The information about docstr presence to be printed to stdout.""" + file_results, total_results = results.to_legacy() + + success = total_results["coverage"] >= self.fail_under + + print_line("## [:books: docstr_coverage](https://docstr-coverage.readthedocs.io/en/latest/api_essentials.html) status: %s **%s**" % (self._status_icon(success), self._status_text(success))) + print_line() + print_line("Overall coverage: **`%s%%`** (required **`%s%%`**)" % (floor(total_results["coverage"]), floor(self.fail_under))) + print_line() + + result_table = [] + for file_path, file in results.files(): + if self.verbosity < 4 and file.count_aggregate().missing == 0: + # Don't print fully documented files + continue + + count = file.count_aggregate() + + row = [] + row.append("%s `%s`" % (self._status_icon(count.coverage() >= self.fail_under), os.path.relpath(file_path, start=os.curdir))) + row += [count.needed, count.found, count.missing, "%.1f%%" % count.coverage()] + + result_table.append(row) + + print_line("
Additional details and impacted files") + print_line() + print_line(tabulate(result_table, headers=["Filename", "Needed", "Found", "Missing", "Coverage"], tablefmt="pipe", colalign = ('left','right','right','right','right'))) + print_line("
") + + def _status_icon(self, success: bool) -> str: + return ":white_check_mark:" if success else ":x:" + + def _status_text(self, success: bool) -> str: + return "SUCCESS" if success else "FAILED" \ No newline at end of file diff --git a/docstr_coverage/result_collection.py b/docstr_coverage/result_collection.py index 44edbcd..29f0648 100644 --- a/docstr_coverage/result_collection.py +++ b/docstr_coverage/result_collection.py @@ -5,6 +5,7 @@ import enum import functools import operator +import os from typing import Optional