Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docstr_coverage/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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`"""

Expand Down Expand Up @@ -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()

Expand Down
81 changes: 79 additions & 2 deletions docstr_coverage/printers.py
Original file line number Diff line number Diff line change
@@ -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),
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can remove this comment in the docstring

Expand Down Expand Up @@ -147,3 +164,63 @@ def _print_overall_statistics(self, results):
)

print_line("Total coverage: {:.1f}% - Grade: {}".format(count.coverage(), grade))


class JsonPrinter(Printer):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that the JsonPrinter is primarily used in use cases where the output is interpreted by a machine (which thus allows easy deserialization). We've had such feature requests in the past.

Thus, the printer should provide the maximum amount of available information. The results.to_legacy() only provides an aggregated overview; it would be better to serialize all information present in results (which, as opposed to results.to_legacy(), may also be extended with additional fields in the future).

If that's too much work, we can of course remove the JsonPrinter in this PR and open a dedicated issue/PR for that.

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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am understanding it correctly that this is indeed Github specific (and not plain markdown) because of the icons you use, right? Just asking to double-check to understand why the class is named as it is. Maybe we should call it GithubMarkdownPrinter?

In either case, some docstring could not hurt ;-)

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)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably link to the github readme instead of the RTD (which should be significantly improved or disabled anyways)

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("<details><summary>Additional details and impacted files</summary>")
print_line()
print_line(tabulate(result_table, headers=["Filename", "Needed", "Found", "Missing", "Coverage"], tablefmt="pipe", colalign = ('left','right','right','right','right')))
print_line("</details>")

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"
1 change: 1 addition & 0 deletions docstr_coverage/result_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import enum
import functools
import operator
import os
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

from typing import Optional


Expand Down