From 0156125b1457e68199d56d2ef77b30072698dc8b Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Sat, 14 Sep 2024 03:07:51 +0300 Subject: [PATCH 01/12] First version of refactored printers. --- docstr_coverage/cli.py | 3 +- docstr_coverage/coverage.py | 2 +- docstr_coverage/printers.py | 279 ++++++++++++++++++++++++------------ tests/test_coverage.py | 4 +- 4 files changed, 190 insertions(+), 98 deletions(-) diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index a19cfb4..c2fcdff 100644 --- a/docstr_coverage/cli.py +++ b/docstr_coverage/cli.py @@ -327,8 +327,7 @@ def execute(paths, **kwargs): # Calculate docstring coverage 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) + LegacyPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config).print() file_results, total_results = results.to_legacy() diff --git a/docstr_coverage/coverage.py b/docstr_coverage/coverage.py index 7cb3869..0458056 100644 --- a/docstr_coverage/coverage.py +++ b/docstr_coverage/coverage.py @@ -213,7 +213,7 @@ def get_docstring_coverage( ignore_names=ignore_names, ) results = analyze(filenames, ignore_config) - LegacyPrinter(verbosity=verbose, ignore_config=ignore_config).print(results) + LegacyPrinter(results, verbosity=verbose, ignore_config=ignore_config).print() return results.to_legacy() diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 8ef21e2..78c93a3 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -1,9 +1,11 @@ """All logic used to print a recorded ResultCollection to stdout. Currently, this module is in BETA and its interface may change in future versions.""" +from dataclasses import dataclass import logging +from abc import ABC, abstractmethod from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.result_collection import FileStatus +from docstr_coverage.result_collection import FileStatus, ResultCollection _GRADES = ( ("AMAZING! Your docstrings are truly a wonder to behold!", 100), @@ -22,128 +24,219 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") -def print_line(line=""): - """Prints `line` - - Parameters - ---------- - line: String - The text to print""" - logger.info(line) +# TODO: 1st - collect data in structure; 2nd - print (with selected printer) info + + +@dataclass(frozen=True) +class IgnoredNode: + """Data Structure for nodes that was ignored in checking.""" + identifier: str + reason: str + + +@dataclass(frozen=True) +class FileCoverageStat: + """Data Structure of coverage info about one file.""" + path: str + is_empty: bool + nodes_with_docstring: tuple[str, ...] + nodes_without_docstring: tuple[str, ...] + ignored_nodes: tuple[IgnoredNode, ...] + needed: int + found: int + missing: int + coverage: float + + +@dataclass(frozen=True) +class OverallCoverageStat: + """Data Structure of coverage statistic""" + num_empty_files: int + num_files: int + is_skip_magic: bool + is_skip_file_docstring: bool + is_skip_init: bool + is_skip_class_def: bool + is_skip_private: bool + files_info: tuple[FileCoverageStat, ...] + needed: int + found: int + missing: int + total_coverage: float + grade: str + + +class Printer(ABC): + """Base abstract superclass for printing coverage results.""" + + def __init__( + self, + results: ResultCollection, + verbosity: int, + ignore_config: IgnoreConfig = IgnoreConfig(), + ): + self.verbosity = verbosity + self.ignore_config = ignore_config + self.overall_coverage_info = self._collect_info(results) + def _collect_info(self, results: ResultCollection) -> OverallCoverageStat: + """Collecting info data for later printing""" + count = results.count_aggregate() -class LegacyPrinter: - """Printing functionality consistent with the original early-versions docstr-coverage outputs. + return OverallCoverageStat( + num_empty_files=count.num_empty_files, + num_files=count.num_files, + is_skip_magic=self.ignore_config.skip_magic, + is_skip_file_docstring=self.ignore_config.skip_file_docstring, + is_skip_init=self.ignore_config.skip_init, + is_skip_class_def=self.ignore_config.skip_class_def, + is_skip_private=self.ignore_config.skip_private, + files_info=tuple( + self._collect_file_coverage_info(file_path, file_info) + for file_path, file_info in results.files() + ), + needed=count.needed, + found=count.found, + missing=count.missing, + total_coverage=count.coverage(), + grade=next( + message for message, grade_threshold in _GRADES + if grade_threshold <= count.coverage() + ), + ) - In future versions, the interface of this class will be refined and an abstract superclass - will be extracted. Thus, coding against the current interface will require refactorings with - future versions of docstr-coverage.""" + def _collect_file_coverage_info(self, file_path, file_info) -> FileCoverageStat: + count = file_info.count_aggregate() - def __init__(self, verbosity: int, ignore_config: IgnoreConfig = IgnoreConfig()): - self.verbosity = verbosity - self.ignore_config = ignore_config + return FileCoverageStat( + path=file_path, + is_empty=file_info.status == FileStatus.EMPTY, + nodes_with_docstring=tuple( + expected_docstring.node_identifier + for expected_docstring in file_info._expected_docstrings + if expected_docstring.has_docstring and not expected_docstring.ignore_reason + ), + nodes_without_docstring=tuple( + expected_docstring.node_identifier + for expected_docstring in file_info._expected_docstrings + if not expected_docstring.has_docstring and not expected_docstring.ignore_reason + ), + ignored_nodes=tuple( + IgnoredNode( + identifier=expected_docstring.node_identifier, + reason=expected_docstring.ignore_reason, + ) + for expected_docstring in file_info._expected_docstrings + if expected_docstring.ignore_reason is not None + ), + needed=count.needed, + found=count.found, + missing=count.missing, + coverage=count.coverage(), + ) - def print(self, results): + @abstractmethod + def print(self) -> None: """Prints a provided set of results to stdout. Parameters ---------- results: ResultCollection The information about docstr presence to be printed to stdout.""" - if self.verbosity >= 2: - self._print_file_statistics(results) - if self.verbosity >= 1: - self._print_overall_statistics(results) - - def _print_file_statistics(self, results): - """Prints the file specific information to stdout. - - Parameters - ---------- - results: ResultCollection - The information about docstr presence to be printed to stdout.""" - for file_path, file in results.files(): - if self.verbosity < 4 and file.count_aggregate().missing == 0: - # Don't print fully documented files - continue + pass - # File Header - print_line('\nFile: "{}"'.format(file_path)) - - # List of missing docstrings - if self.verbosity >= 3: - if file.status == FileStatus.EMPTY and self.verbosity > 3: - print_line(" - File is empty") - for expected_docstr in file._expected_docstrings: - if expected_docstr.has_docstring and self.verbosity > 3: - print_line( - " - Found docstring for `{0}`".format(expected_docstr.node_identifier) - ) - elif expected_docstr.ignore_reason and self.verbosity > 3: - print_line( - " - Ignored `{0}`: reason: `{1}`".format( - expected_docstr.node_identifier, expected_docstr.ignore_reason - ) - ) - elif not expected_docstr.has_docstring and not expected_docstr.ignore_reason: - if expected_docstr.node_identifier == "module docstring": - print_line(" - No module docstring") - else: - print_line( - " - No docstring for `{0}`".format(expected_docstr.node_identifier) - ) - # Statistics - count = file.count_aggregate() - print_line( - " Needed: %s; Found: %s; Missing: %s; Coverage: %.1f%%" - % ( - count.needed, - count.found, - count.missing, - count.coverage(), - ), - ) - print_line() - print_line() +class LegacyPrinter(Printer): + """Print in simple format to output""" - def _print_overall_statistics(self, results): - """Prints overall results (aggregated over all files) to stdout. + def _print_line(self, line: str = ""): + """Prints `line` Parameters ---------- - results: ResultCollection - The information about docstr presence to be printed to stdout.""" - count = results.count_aggregate() + line: String + The text to print""" + logger.info(line) + + def print(self): + if self.verbosity >= 2: + self._print_files_statistic() + if self.verbosity >= 1: + self._print_overall_statistics() + def _print_overall_statistics(self): postfix = "" - if count.num_empty_files > 0: - postfix = " (%s files are empty)" % count.num_empty_files - if self.ignore_config.skip_magic: + if self.overall_coverage_info.num_empty_files > 0: + postfix = " (%s files are empty)" % self.overall_coverage_info.num_empty_files + if self.overall_coverage_info.is_skip_magic: postfix += " (skipped all non-init magic methods)" - if self.ignore_config.skip_file_docstring: + if self.overall_coverage_info.is_skip_file_docstring: postfix += " (skipped file-level docstrings)" - if self.ignore_config.skip_init: + if self.overall_coverage_info.is_skip_init: postfix += " (skipped __init__ methods)" - if self.ignore_config.skip_class_def: + if self.overall_coverage_info.is_skip_class_def: postfix += " (skipped class definitions)" - if self.ignore_config.skip_private: + if self.overall_coverage_info.is_skip_private: postfix += " (skipped private methods)" - if count.num_files > 1: - print_line("Overall statistics for %s files%s:" % (count.num_files, postfix)) + if self.overall_coverage_info.num_files > 1: + self._print_line("Overall statistics for %s files%s:" % ( + self.overall_coverage_info.num_files, + postfix, + )) else: - print_line("Overall statistics%s:" % postfix) + self._print_line("Overall statistics%s:" % postfix) - print_line( + self._print_line( "Needed: {} - Found: {} - Missing: {}".format( - count.needed, count.found, count.missing + self.overall_coverage_info.needed, + self.overall_coverage_info.found, + self.overall_coverage_info.missing, ), ) - # Calculate Total Grade - grade = next( - message for message, grade_threshold in _GRADES if grade_threshold <= count.coverage() + self._print_line( + "Total coverage: {:.1f}% - Grade: {}".format( + self.overall_coverage_info.total_coverage, + self.overall_coverage_info.grade, + ) ) - print_line("Total coverage: {:.1f}% - Grade: {}".format(count.coverage(), grade)) + def _print_files_statistic(self): + for file_info in self.overall_coverage_info.files_info: + + if self.verbosity < 4 and file_info.missing == 0: + continue + + self._print_line('\nFile: "{0}"'.format(file_info.path)) + if self.verbosity >= 3: + if file_info.is_empty and self.verbosity > 3: + self._print_line(" - File is empty") + for node_identifier in file_info.nodes_with_docstring: + if self.verbosity > 3: + self._print_line(" - Found docstring for `{0}`".format(node_identifier)) + for ignored_node in file_info.ignored_nodes: + if self.verbosity > 3: + self._print_line( + " - Ignored `{0}`: reason: `{1}`".format( + ignored_node.identifier, + ignored_node.reason, + ) + ) + for node_identifier in file_info.nodes_without_docstring: + if node_identifier == "module docstring": + self._print_line(" - No module docstring") + else: + self._print_line(" - No docstring for `{0}`".format(node_identifier)) + + self._print_line( + " Needed: %s; Found: %s; Missing: %s; Coverage: %.1f%%" + % ( + file_info.needed, + file_info.found, + file_info.missing, + file_info.coverage, + ) + ) + self._print_line() + self._print_line() diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 53ebdde..d40e803 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -160,7 +160,7 @@ def test_should_report_when_no_docs_in_a_file(): def test_logging_empty_file(caplog, expected): with caplog.at_level(logging.DEBUG): result = analyze([EMPTY_FILE_PATH]) - LegacyPrinter(verbosity=4).print(result) + LegacyPrinter(result, verbosity=4).print() _file_results, _total_results = result.to_legacy() if platform.system() == "Windows": @@ -242,7 +242,7 @@ def test_logging_partially_documented_file(caplog, expected, verbose, ignore_nam ignore_config = IgnoreConfig(ignore_names=ignore_names) with caplog.at_level(logging.DEBUG): result = analyze([PARTLY_DOCUMENTED_FILE_PATH], ignore_config=ignore_config) - LegacyPrinter(verbosity=verbose, ignore_config=ignore_config).print(result) + LegacyPrinter(result, verbosity=verbose, ignore_config=ignore_config).print() if platform.system() == "Windows": assert [m.replace("\\", "/") for m in caplog.messages] == expected From 298c27902be56478840c0485b69f2af089b75ad5 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Sat, 14 Sep 2024 14:26:19 +0300 Subject: [PATCH 02/12] Add docstrings. Fix naming. Little refactoring. --- docstr_coverage/printers.py | 151 +++++++++++++++++++++++++----------- 1 file changed, 104 insertions(+), 47 deletions(-) diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 78c93a3..3cc24fb 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -1,11 +1,11 @@ """All logic used to print a recorded ResultCollection to stdout. Currently, this module is in BETA and its interface may change in future versions.""" -from dataclasses import dataclass import logging from abc import ABC, abstractmethod +from dataclasses import dataclass from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.result_collection import FileStatus, ResultCollection +from docstr_coverage.result_collection import File, FileStatus, ResultCollection _GRADES = ( ("AMAZING! Your docstrings are truly a wonder to behold!", 100), @@ -24,12 +24,10 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") -# TODO: 1st - collect data in structure; 2nd - print (with selected printer) info - - @dataclass(frozen=True) class IgnoredNode: """Data Structure for nodes that was ignored in checking.""" + identifier: str reason: str @@ -37,6 +35,7 @@ class IgnoredNode: @dataclass(frozen=True) class FileCoverageStat: """Data Structure of coverage info about one file.""" + path: str is_empty: bool nodes_with_docstring: tuple[str, ...] @@ -51,6 +50,7 @@ class FileCoverageStat: @dataclass(frozen=True) class OverallCoverageStat: """Data Structure of coverage statistic""" + num_empty_files: int num_files: int is_skip_magic: bool @@ -67,7 +67,10 @@ class OverallCoverageStat: class Printer(ABC): - """Base abstract superclass for printing coverage results.""" + """Base abstract superclass for printing coverage results. + + It provides parsers of coverage results into data structures and abstract methods for + implementing printers, file writers or whatever in heir classes.""" def __init__( self, @@ -75,12 +78,33 @@ def __init__( verbosity: int, ignore_config: IgnoreConfig = IgnoreConfig(), ): + """ + Parameters + ---------- + results: ResultCollection + Coverage analyze results. + verbosity: int + Verbosity identifier. + ignore_config: IgnoreConfig + Config with ignoring setups. + """ self.verbosity = verbosity self.ignore_config = ignore_config - self.overall_coverage_info = self._collect_info(results) + self.overall_coverage_stat = self._collect_overall_coverage_stat(results) + + def _collect_overall_coverage_stat(self, results: ResultCollection) -> OverallCoverageStat: + """Collecting overall coverage statistic. - def _collect_info(self, results: ResultCollection) -> OverallCoverageStat: - """Collecting info data for later printing""" + Parameters + ---------- + results : ResultCollection + Result of coverage analyze. + + Returns + ------- + OverallCoverageStat + Data structure with coverage statistic. + """ count = results.count_aggregate() return OverallCoverageStat( @@ -92,7 +116,7 @@ def _collect_info(self, results: ResultCollection) -> OverallCoverageStat: is_skip_class_def=self.ignore_config.skip_class_def, is_skip_private=self.ignore_config.skip_private, files_info=tuple( - self._collect_file_coverage_info(file_path, file_info) + self._collect_file_coverage_stat(file_path, file_info) for file_path, file_info in results.files() ), needed=count.needed, @@ -100,12 +124,27 @@ def _collect_info(self, results: ResultCollection) -> OverallCoverageStat: missing=count.missing, total_coverage=count.coverage(), grade=next( - message for message, grade_threshold in _GRADES + message + for message, grade_threshold in _GRADES if grade_threshold <= count.coverage() ), ) - def _collect_file_coverage_info(self, file_path, file_info) -> FileCoverageStat: + def _collect_file_coverage_stat(self, file_path: str, file_info: File) -> FileCoverageStat: + """Collecting coverage statistic for one file. + + Parameters + ---------- + file_path: str + Path to checking file + file_info: File + Info about docstring in one file. + + Returns + ------- + FileCoverageStat + Data structure with file coverage statistic. + """ count = file_info.count_aggregate() return FileCoverageStat( @@ -137,12 +176,20 @@ def _collect_file_coverage_info(self, file_path, file_info) -> FileCoverageStat: @abstractmethod def print(self) -> None: - """Prints a provided set of results to stdout. - - Parameters - ---------- - results: ResultCollection - The information about docstr presence to be printed to stdout.""" + """Providing how to print coverage results. + + In heir classes you can use `overall_coverage_stat` and `verbosity` attribute to create + special result printers. The `verbosity` points on what info will be displayed. Here the + rules: + * `verbosity` equal `1` - prints all overall coverage statistic. + * `verbosity` equal `2` - prints all overall coverage statistic and *needed*, *found*, + *missing* and *coverage* file statistics. + * `verbosity` equal `3` - prints all overall coverage statistic, information about *nodes + without docstrings*, *needed*, *found*, *missing* and *coverage* file statistics. + * `verbosity` equal `4` - prints all overall coverage statistic, information about *empty + files*, *nodes with docstrings*, *ignored nodes*, *nodes without docstrings*, *needed*, + *found*, *missing* and *coverage* file statistics. + """ pass @@ -165,64 +212,73 @@ def print(self): self._print_overall_statistics() def _print_overall_statistics(self): + """Printing overall coverage statistics.""" postfix = "" - if self.overall_coverage_info.num_empty_files > 0: - postfix = " (%s files are empty)" % self.overall_coverage_info.num_empty_files - if self.overall_coverage_info.is_skip_magic: + if self.overall_coverage_stat.num_empty_files > 0: + postfix = " (%s files are empty)" % self.overall_coverage_stat.num_empty_files + if self.overall_coverage_stat.is_skip_magic: postfix += " (skipped all non-init magic methods)" - if self.overall_coverage_info.is_skip_file_docstring: + if self.overall_coverage_stat.is_skip_file_docstring: postfix += " (skipped file-level docstrings)" - if self.overall_coverage_info.is_skip_init: + if self.overall_coverage_stat.is_skip_init: postfix += " (skipped __init__ methods)" - if self.overall_coverage_info.is_skip_class_def: + if self.overall_coverage_stat.is_skip_class_def: postfix += " (skipped class definitions)" - if self.overall_coverage_info.is_skip_private: + if self.overall_coverage_stat.is_skip_private: postfix += " (skipped private methods)" - if self.overall_coverage_info.num_files > 1: - self._print_line("Overall statistics for %s files%s:" % ( - self.overall_coverage_info.num_files, - postfix, - )) + if self.overall_coverage_stat.num_files > 1: + self._print_line( + "Overall statistics for %s files%s:" + % ( + self.overall_coverage_stat.num_files, + postfix, + ) + ) else: self._print_line("Overall statistics%s:" % postfix) self._print_line( "Needed: {} - Found: {} - Missing: {}".format( - self.overall_coverage_info.needed, - self.overall_coverage_info.found, - self.overall_coverage_info.missing, + self.overall_coverage_stat.needed, + self.overall_coverage_stat.found, + self.overall_coverage_stat.missing, ), ) self._print_line( "Total coverage: {:.1f}% - Grade: {}".format( - self.overall_coverage_info.total_coverage, - self.overall_coverage_info.grade, + self.overall_coverage_stat.total_coverage, + self.overall_coverage_stat.grade, ) ) def _print_files_statistic(self): - for file_info in self.overall_coverage_info.files_info: - + """Print coverage file statistic.""" + for file_info in self.overall_coverage_stat.files_info: if self.verbosity < 4 and file_info.missing == 0: continue self._print_line('\nFile: "{0}"'.format(file_info.path)) - if self.verbosity >= 3: - if file_info.is_empty and self.verbosity > 3: + + if self.verbosity > 3: + if file_info.is_empty: self._print_line(" - File is empty") for node_identifier in file_info.nodes_with_docstring: - if self.verbosity > 3: - self._print_line(" - Found docstring for `{0}`".format(node_identifier)) + self._print_line( + " - Found docstring for `{0}`".format( + node_identifier, + ) + ) for ignored_node in file_info.ignored_nodes: - if self.verbosity > 3: - self._print_line( - " - Ignored `{0}`: reason: `{1}`".format( - ignored_node.identifier, - ignored_node.reason, - ) + self._print_line( + " - Ignored `{0}`: reason: `{1}`".format( + ignored_node.identifier, + ignored_node.reason, ) + ) + + if self.verbosity >= 3: for node_identifier in file_info.nodes_without_docstring: if node_identifier == "module docstring": self._print_line(" - No module docstring") @@ -238,5 +294,6 @@ def _print_files_statistic(self): file_info.coverage, ) ) + self._print_line() self._print_line() From 7671497c63bbca1bae4ed237305054b81e6dfb41 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Sat, 14 Sep 2024 22:03:58 +0300 Subject: [PATCH 03/12] Add markdown output. Add little tests. --- docstr_coverage/cli.py | 18 +++- docstr_coverage/printers.py | 164 ++++++++++++++++++++++++++++++++++++ tests/test_coverage.py | 45 +++++++++- 3 files changed, 222 insertions(+), 5 deletions(-) diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index c2fcdff..4007c45 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 LegacyPrinter, MarkdownPrinter 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", "markdown"]), + default="text", + help="Formatting style of the output (text, markdown)", + show_default=True, + metavar="FORMAT", +) def execute(paths, **kwargs): """Measure docstring coverage for `PATHS`""" @@ -327,7 +336,12 @@ def execute(paths, **kwargs): # Calculate docstring coverage show_progress = not kwargs["percentage_only"] results = analyze(all_paths, ignore_config=ignore_config, show_progress=show_progress) - LegacyPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config).print() + + if kwargs["output"] == "markdown": + printer = MarkdownPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config) + else: + printer = LegacyPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config) + printer.print() file_results, total_results = results.to_legacy() diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 3cc24fb..787b5ed 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -297,3 +297,167 @@ def _print_files_statistic(self): self._print_line() self._print_line() + + +class MarkdownPrinter(Printer): + + def _print_line(self, line: str = ""): + """Prints `line` + + Parameters + ---------- + line: String + The text to print""" + logger.info(line) + + def _print_title_line(self, content: str | int | float): + """Print Markdown 2nd title (`## ...`). + + Parameters + ---------- + content : str | int | float + Title content + """ + logger.info("## {}".format(content)) + + def _print_table( + self, + cols: tuple[str, ...], + rows: tuple[tuple[str | int | float], ...], + ): + """Print markdown table. + + Using: + >>> self._print_table( + ... cols=("Needed", "Found", "Missing"), + ... vals=( + ... (10, 20, "65.5%"), + ... (30, 40, "99.9%") + ... ) + ... ) + | Needed | Found | Missing | + |---|---|---| + | 10 | 20 | 65.5% | + | 30 | 40 | 99.9% | + + Parameters + ---------- + cols: tuple[str, ...] + Table columns + rows: tuple[tuple[str | int | float], ...] + Column values + """ + assert all(len(v) == len(cols) for v in rows), "Col num not equal to cols value" + + col_line = "" + for col in cols: + col_line += "| {} ".format(col) + self._print_line(col_line + "|") + + sep_line = "" + for _ in range(len(cols)): + sep_line += "|---" + self._print_line(sep_line + "|") + + for row in rows: + row_line = "" + for value in row: + row_line += "| {} ".format(value) + self._print_line(row_line + "|") + + def print(self) -> None: + if self.verbosity >= 2: + self._print_files_statistic() + if self.verbosity >= 1: + self._print_overall_statistics() + + def _print_files_statistic(self): + for file_info in self.overall_coverage_stat.files_info: + if self.verbosity < 4 and file_info.missing == 0: + continue + + self._print_line('**File**: `{0}`'.format(file_info.path)) + + if self.verbosity > 3: + if file_info.is_empty: + self._print_line("- File is empty") + for node_identifier in file_info.nodes_with_docstring: + self._print_line( + "- Found docstring for `{0}`".format( + node_identifier, + ) + ) + for ignored_node in file_info.ignored_nodes: + self._print_line( + "- Ignored `{0}`: reason: `{1}`".format( + ignored_node.identifier, + ignored_node.reason, + ) + ) + + if self.verbosity >= 3: + for node_identifier in file_info.nodes_without_docstring: + if node_identifier == "module docstring": + self._print_line("- No module docstring") + else: + self._print_line("- No docstring for `{0}`".format(node_identifier)) + + self._print_line() + + self._print_table( + ("Needed", "Found", "Missing", "Coverage"), + (( + file_info.needed, + file_info.found, + file_info.missing, + "{:.1f}%".format(file_info.coverage), + ),) + ) + self._print_line() + + self._print_line() + + def _print_overall_statistics(self): + self._print_title_line("Overall statistic") + + if self.overall_coverage_stat.num_files > 1: + self._print_line("Files number: **{}**".format(self.overall_coverage_stat.num_files)) + self._print_line() + self._print_line( + "Total coverage: **{:.1f}%**".format( + self.overall_coverage_stat.total_coverage, + ) + ) + self._print_line() + self._print_line( + "Grade: **{}**".format(self.overall_coverage_stat.grade) + ) + + if self.overall_coverage_stat.num_empty_files > 0: + self._print_line("- %s files are empty" % self.overall_coverage_stat.num_empty_files) + + if self.overall_coverage_stat.is_skip_magic: + self._print_line("- skipped all non-init magic methods") + + if self.overall_coverage_stat.is_skip_file_docstring: + self._print_line("- skipped file-level docstrings") + + if self.overall_coverage_stat.is_skip_init: + self._print_line("- skipped __init__ methods") + + if self.overall_coverage_stat.is_skip_class_def: + self._print_line("- skipped class definitions") + + if self.overall_coverage_stat.is_skip_private: + self._print_line("- skipped private methods") + + self._print_line() + + self._print_table( + ("Needed", "Found", "Missing"), + (( + self.overall_coverage_stat.needed, + self.overall_coverage_stat.found, + self.overall_coverage_stat.missing, + ),) + ) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index d40e803..5de4ec0 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -6,7 +6,7 @@ from docstr_coverage import analyze from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.printers import _GRADES, LegacyPrinter +from docstr_coverage.printers import _GRADES, LegacyPrinter, MarkdownPrinter SAMPLES_DIRECTORY = os.path.join("tests", "sample_files", "subdir_a") EMPTY_FILE_PATH = os.path.join(SAMPLES_DIRECTORY, "empty_file.py") @@ -157,7 +157,7 @@ def test_should_report_when_no_docs_in_a_file(): ) ], ) -def test_logging_empty_file(caplog, expected): +def test_legacy_printer_logging_empty_file(caplog, expected): with caplog.at_level(logging.DEBUG): result = analyze([EMPTY_FILE_PATH]) LegacyPrinter(result, verbosity=4).print() @@ -169,6 +169,45 @@ def test_logging_empty_file(caplog, expected): assert caplog.messages == expected +@pytest.mark.parametrize( + ["expected"], + [ + ( + [ + '**File**: `tests/sample_files/subdir_a/empty_file.py`', + "- File is empty", + "", + "| Needed | Found | Missing | Coverage |", + "|---|---|---|---|", + "| 0 | 0 | 0 | 100.0% |", + "", + "", + "## Overall statistic", + "", + "Total coverage: **100.0%**", + "", + "Grade: **" + _GRADES[0][0] + "**", + "- 1 files are empty", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 0 | 0 | 0 |", + ], + ) + ], +) +def test_markdown_printer_logging_empty_file(caplog, expected): + with caplog.at_level(logging.DEBUG): + result = analyze([EMPTY_FILE_PATH]) + MarkdownPrinter(result, verbosity=4).print() + _file_results, _total_results = result.to_legacy() + + if platform.system() == "Windows": + assert [m.replace("\\", "/") for m in caplog.messages] == expected + else: + assert caplog.messages == expected + + @pytest.mark.parametrize( ["expected", "verbose", "ignore_names"], [ @@ -238,7 +277,7 @@ def test_logging_empty_file(caplog, expected): ), ], ) -def test_logging_partially_documented_file(caplog, expected, verbose, ignore_names): +def test_legacy_printer_logging_partially_documented_file(caplog, expected, verbose, ignore_names): ignore_config = IgnoreConfig(ignore_names=ignore_names) with caplog.at_level(logging.DEBUG): result = analyze([PARTLY_DOCUMENTED_FILE_PATH], ignore_config=ignore_config) From c5105ee266f958074084bc42b26feac7ecb327e4 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Sat, 14 Sep 2024 22:13:44 +0300 Subject: [PATCH 04/12] Add output args description in README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2b1490d..c2ca295 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,9 @@ docstr-coverage some_project/src #### Options +- _--output=\, -o \_ - Set output style (default text) + - text - Output in simple style. + - markdown - Output in Markdown notation. - _--skip-magic, -m_ - Ignore all magic methods (except `__init__`) - _--skip-init, -i_ - Ignore all `__init__` methods - _--skip-file-doc, -f_ - Ignore module docstrings (at the top of files) From e3862aa31bf0b0bf7dde99a0cfe906c5bb91df61 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Mon, 14 Oct 2024 19:49:57 +0300 Subject: [PATCH 05/12] Add flags report format and output type. Refactor legacy and markdown printer. Change a little output style. Add more tests and fix some of them. --- docstr_coverage/cli.py | 27 +- docstr_coverage/coverage.py | 2 +- docstr_coverage/printers.py | 663 +++++++++++++++++++----------------- tests/test_coverage.py | 204 ++++++++++- 4 files changed, 564 insertions(+), 332 deletions(-) diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index 4007c45..231606b 100644 --- a/docstr_coverage/cli.py +++ b/docstr_coverage/cli.py @@ -264,9 +264,18 @@ def _assert_valid_key_value(k, v): @click.option( "-o", "--output", + type=click.Choice(["stdout", "file"]), + default="stdout", + help="Format of output", + show_default=True, + metavar="FORMAT", +) +@click.option( + "-r", + "--format", type=click.Choice(["text", "markdown"]), default="text", - help="Formatting style of the output (text, markdown)", + help="Format of output", show_default=True, metavar="FORMAT", ) @@ -337,11 +346,21 @@ def execute(paths, **kwargs): show_progress = not kwargs["percentage_only"] results = analyze(all_paths, ignore_config=ignore_config, show_progress=show_progress) - if kwargs["output"] == "markdown": + report_format = kwargs["format"] + if report_format == "markdown": printer = MarkdownPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config) - else: + elif report_format == "text": printer = LegacyPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config) - printer.print() + else: + raise SystemError("Unknown report format: {0}".format(report_format)) + + output_type = kwargs["output"] + if output_type == "file": + printer.save_to_file() + elif output_type == "stdout": + printer.print_to_stdout() + else: + raise SystemError("Unknown output type: {0}".format(output_type)) file_results, total_results = results.to_legacy() diff --git a/docstr_coverage/coverage.py b/docstr_coverage/coverage.py index 0458056..792f1e4 100644 --- a/docstr_coverage/coverage.py +++ b/docstr_coverage/coverage.py @@ -213,7 +213,7 @@ def get_docstring_coverage( ignore_names=ignore_names, ) results = analyze(filenames, ignore_config) - LegacyPrinter(results, verbosity=verbose, ignore_config=ignore_config).print() + LegacyPrinter(results, verbosity=verbose, ignore_config=ignore_config).print_to_stdout() return results.to_legacy() diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 787b5ed..26cfbba 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -3,9 +3,10 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import Optional, Union from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.result_collection import File, FileStatus, ResultCollection +from docstr_coverage.result_collection import FileStatus, ResultCollection _GRADES = ( ("AMAZING! Your docstrings are truly a wonder to behold!", 100), @@ -34,43 +35,53 @@ class IgnoredNode: @dataclass(frozen=True) class FileCoverageStat: - """Data Structure of coverage info about one file.""" + """Data Structure of coverage info about one file. - path: str - is_empty: bool - nodes_with_docstring: tuple[str, ...] - nodes_without_docstring: tuple[str, ...] - ignored_nodes: tuple[IgnoredNode, ...] - needed: int + For `verbosity` with value: + * `2` - Fields `coverage`, `found`, `missing`, `needed` and `path`. + * `3` - Fields with `verbosity` `2` and `nodes_without_docstring`. + * `4` - Fields with `verbosity` `3` and `is_empty`, `nodes_with_docstring`, + `ignored_nodes` + """ + + coverage: float found: int missing: int - coverage: float + needed: int + path: str + ignored_nodes: tuple[IgnoredNode, ...] + is_empty: Union[bool, None] + nodes_with_docstring: Union[tuple[str, ...], None] + nodes_without_docstring: Union[tuple[str, ...], None] @dataclass(frozen=True) class OverallCoverageStat: - """Data Structure of coverage statistic""" + """Data Structure of coverage statistic.""" - num_empty_files: int - num_files: int - is_skip_magic: bool + found: int + grade: str + is_skip_class_def: bool is_skip_file_docstring: bool is_skip_init: bool - is_skip_class_def: bool + is_skip_magic: bool is_skip_private: bool - files_info: tuple[FileCoverageStat, ...] - needed: int - found: int missing: int + needed: int + num_empty_files: int + num_files: int total_coverage: float - grade: str class Printer(ABC): """Base abstract superclass for printing coverage results. - It provides parsers of coverage results into data structures and abstract methods for - implementing printers, file writers or whatever in heir classes.""" + It provides coverage results in data structures (`OverallCoverageStat`, `FileCoverageStat` and + `IgnoredNode`) and abstract methods for implementing type of displaying and saving in file + statistic data. + + In heir classes you can use `overall_coverage_stat` and `overall_files_coverage_stat` + attributes. Depends of given `verbosity` some data can be `None`.""" def __init__( self, @@ -90,370 +101,340 @@ def __init__( """ self.verbosity = verbosity self.ignore_config = ignore_config - self.overall_coverage_stat = self._collect_overall_coverage_stat(results) + self.results = results + self.__overall_coverage_stat = None + self.__overall_files_coverage_stat = None + + @property + def overall_coverage_stat(self) -> OverallCoverageStat | float: + """Getting full coverage statistic. + + For `verbosity` with value: + * `0` - Only `total_coverage` value returning. + * `1` - All fields, except `files_info`. + * `2` - All fields.""" + if self.__overall_coverage_stat is None: + count = self.results.count_aggregate() + + if self.verbosity >= 1: + + self.__overall_coverage_stat = OverallCoverageStat( + found=count.found, + grade=next( + message + for message, grade_threshold in _GRADES + if grade_threshold <= count.coverage() + ), + is_skip_class_def=self.ignore_config.skip_class_def, + is_skip_file_docstring=self.ignore_config.skip_file_docstring, + is_skip_init=self.ignore_config.skip_init, + is_skip_magic=self.ignore_config.skip_magic, + is_skip_private=self.ignore_config.skip_private, + missing=count.missing, + needed=count.needed, + num_empty_files=count.num_empty_files, + num_files=count.num_files, + total_coverage=count.coverage(), + ) - def _collect_overall_coverage_stat(self, results: ResultCollection) -> OverallCoverageStat: - """Collecting overall coverage statistic. + else: + self.__overall_coverage_stat = count.coverage() - Parameters - ---------- - results : ResultCollection - Result of coverage analyze. + return self.__overall_coverage_stat - Returns - ------- - OverallCoverageStat - Data structure with coverage statistic. - """ - count = results.count_aggregate() - - return OverallCoverageStat( - num_empty_files=count.num_empty_files, - num_files=count.num_files, - is_skip_magic=self.ignore_config.skip_magic, - is_skip_file_docstring=self.ignore_config.skip_file_docstring, - is_skip_init=self.ignore_config.skip_init, - is_skip_class_def=self.ignore_config.skip_class_def, - is_skip_private=self.ignore_config.skip_private, - files_info=tuple( - self._collect_file_coverage_stat(file_path, file_info) - for file_path, file_info in results.files() - ), - needed=count.needed, - found=count.found, - missing=count.missing, - total_coverage=count.coverage(), - grade=next( - message - for message, grade_threshold in _GRADES - if grade_threshold <= count.coverage() - ), - ) + @property + def overall_files_coverage_stat(self) -> Union[list[FileCoverageStat], None]: + """Getting coverage statistics for files. - def _collect_file_coverage_stat(self, file_path: str, file_info: File) -> FileCoverageStat: - """Collecting coverage statistic for one file. - - Parameters - ---------- - file_path: str - Path to checking file - file_info: File - Info about docstring in one file. + For `verbosity` with value: + * `2` - Fields `coverage`, `found`, `missing`, `needed` and `path`. + * `3` - Fields with `verbosity` `2` and `nodes_without_docstring`. + * `4` - Fields with `verbosity` `3` and `is_empty`, `nodes_with_docstring`, + `ignored_nodes` Returns ------- - FileCoverageStat - Data structure with file coverage statistic. - """ - count = file_info.count_aggregate() - - return FileCoverageStat( - path=file_path, - is_empty=file_info.status == FileStatus.EMPTY, - nodes_with_docstring=tuple( - expected_docstring.node_identifier - for expected_docstring in file_info._expected_docstrings - if expected_docstring.has_docstring and not expected_docstring.ignore_reason - ), - nodes_without_docstring=tuple( - expected_docstring.node_identifier - for expected_docstring in file_info._expected_docstrings - if not expected_docstring.has_docstring and not expected_docstring.ignore_reason - ), - ignored_nodes=tuple( - IgnoredNode( - identifier=expected_docstring.node_identifier, - reason=expected_docstring.ignore_reason, + list[FileCoverageStat] + Coverage info about all checked files.""" + if self.__overall_files_coverage_stat is None and self.verbosity >= 2: + + overall_files_coverage_stat: list[FileCoverageStat] = [] + for file_path, file_info in self.results.files(): + + if self.verbosity >= 3: + nodes_without_docstring = tuple( + expected_docstring.node_identifier + for expected_docstring in file_info._expected_docstrings + if not expected_docstring.has_docstring and + not expected_docstring.ignore_reason + ) + else: + nodes_without_docstring = None + + if self.verbosity >= 4: + is_empty = file_info.status == FileStatus.EMPTY + nodes_with_docstring = tuple( + expected_docstring.node_identifier + for expected_docstring in file_info._expected_docstrings + if expected_docstring.has_docstring and + not expected_docstring.ignore_reason + ) + ignored_nodes = tuple( + IgnoredNode( + identifier=expected_docstring.node_identifier, + reason=expected_docstring.ignore_reason, + ) + for expected_docstring in file_info._expected_docstrings + if expected_docstring.ignore_reason is not None + ) + else: + is_empty = None + nodes_with_docstring = None + ignored_nodes = None + + count = file_info.count_aggregate() + overall_files_coverage_stat.append( + FileCoverageStat( + coverage=count.coverage(), + found=count.found, + missing=count.missing, + needed=count.needed, + path=file_path, + ignored_nodes=ignored_nodes, + is_empty=is_empty, + nodes_with_docstring=nodes_with_docstring, + nodes_without_docstring=nodes_without_docstring, + ) ) - for expected_docstring in file_info._expected_docstrings - if expected_docstring.ignore_reason is not None - ), - needed=count.needed, - found=count.found, - missing=count.missing, - coverage=count.coverage(), - ) + self.__overall_files_coverage_stat = overall_files_coverage_stat + + return self.__overall_files_coverage_stat @abstractmethod - def print(self) -> None: - """Providing how to print coverage results. - - In heir classes you can use `overall_coverage_stat` and `verbosity` attribute to create - special result printers. The `verbosity` points on what info will be displayed. Here the - rules: - * `verbosity` equal `1` - prints all overall coverage statistic. - * `verbosity` equal `2` - prints all overall coverage statistic and *needed*, *found*, - *missing* and *coverage* file statistics. - * `verbosity` equal `3` - prints all overall coverage statistic, information about *nodes - without docstrings*, *needed*, *found*, *missing* and *coverage* file statistics. - * `verbosity` equal `4` - prints all overall coverage statistic, information about *empty - files*, *nodes with docstrings*, *ignored nodes*, *nodes without docstrings*, *needed*, - *found*, *missing* and *coverage* file statistics. + def print_to_stdout(self) -> None: + """Providing how to print coverage results.""" + pass + + @abstractmethod + def save_to_file(self, path: Optional[str] = None) -> None: + """Providing how to save coverage results in file. + + Parameters + ---------- + path: str | None + Path to file with coverage results. """ pass class LegacyPrinter(Printer): - """Print in simple format to output""" + """Printer for legacy format.""" - def _print_line(self, line: str = ""): - """Prints `line` + def print_to_stdout(self) -> None: + for line in self._generate_string().split("\n"): + logger.info(line) - Parameters - ---------- - line: String - The text to print""" - logger.info(line) - - def print(self): - if self.verbosity >= 2: - self._print_files_statistic() - if self.verbosity >= 1: - self._print_overall_statistics() - - def _print_overall_statistics(self): - """Printing overall coverage statistics.""" - postfix = "" - if self.overall_coverage_stat.num_empty_files > 0: - postfix = " (%s files are empty)" % self.overall_coverage_stat.num_empty_files - if self.overall_coverage_stat.is_skip_magic: - postfix += " (skipped all non-init magic methods)" - if self.overall_coverage_stat.is_skip_file_docstring: - postfix += " (skipped file-level docstrings)" - if self.overall_coverage_stat.is_skip_init: - postfix += " (skipped __init__ methods)" - if self.overall_coverage_stat.is_skip_class_def: - postfix += " (skipped class definitions)" - if self.overall_coverage_stat.is_skip_private: - postfix += " (skipped private methods)" + def save_to_file(self, path: Optional[str] = None) -> None: + if path is None: + path = "./coverage-results.txt" + with open(path, "w") as wf: + wf.write(self._generate_string()) - if self.overall_coverage_stat.num_files > 1: - self._print_line( - "Overall statistics for %s files%s:" - % ( - self.overall_coverage_stat.num_files, - postfix, - ) - ) - else: - self._print_line("Overall statistics%s:" % postfix) - - self._print_line( - "Needed: {} - Found: {} - Missing: {}".format( - self.overall_coverage_stat.needed, - self.overall_coverage_stat.found, - self.overall_coverage_stat.missing, - ), - ) + def _generate_string(self) -> str: + final_string = "" - self._print_line( - "Total coverage: {:.1f}% - Grade: {}".format( - self.overall_coverage_stat.total_coverage, - self.overall_coverage_stat.grade, - ) - ) + if self.overall_files_coverage_stat is not None: + final_string += self._generate_file_stat_string() + final_string += "\n" + final_string += self._generate_overall_stat_string() - def _print_files_statistic(self): - """Print coverage file statistic.""" - for file_info in self.overall_coverage_stat.files_info: - if self.verbosity < 4 and file_info.missing == 0: - continue - - self._print_line('\nFile: "{0}"'.format(file_info.path)) - - if self.verbosity > 3: - if file_info.is_empty: - self._print_line(" - File is empty") - for node_identifier in file_info.nodes_with_docstring: - self._print_line( - " - Found docstring for `{0}`".format( - node_identifier, - ) + return final_string + + def _generate_file_stat_string(self): + final_string = "" + for file_coverage_stat in self.overall_files_coverage_stat: + + file_string = 'File: "{0}"\n'.format(file_coverage_stat.path) + + if file_coverage_stat.is_empty is not None and file_coverage_stat.is_empty is True: + file_string += " - File is empty\n" + + if file_coverage_stat.nodes_with_docstring is not None: + for node_identifier in file_coverage_stat.nodes_with_docstring: + file_string += " - Found docstring for `{0}`\n".format( + node_identifier, ) - for ignored_node in file_info.ignored_nodes: - self._print_line( - " - Ignored `{0}`: reason: `{1}`".format( - ignored_node.identifier, - ignored_node.reason, - ) + + if file_coverage_stat.ignored_nodes is not None: + for ignored_node in file_coverage_stat.ignored_nodes: + file_string += " - Ignored `{0}`: reason: `{1}`\n".format( + ignored_node.identifier, + ignored_node.reason, ) - if self.verbosity >= 3: - for node_identifier in file_info.nodes_without_docstring: + if file_coverage_stat.nodes_without_docstring is not None: + for node_identifier in file_coverage_stat.nodes_without_docstring: if node_identifier == "module docstring": - self._print_line(" - No module docstring") + file_string += " - No module docstring\n" else: - self._print_line(" - No docstring for `{0}`".format(node_identifier)) - - self._print_line( - " Needed: %s; Found: %s; Missing: %s; Coverage: %.1f%%" - % ( - file_info.needed, - file_info.found, - file_info.missing, - file_info.coverage, - ) + file_string += " - No docstring for `{0}`\n".format(node_identifier) + + file_string += " Needed: %s; Found: %s; Missing: %s; Coverage: %.1f%%" % ( + file_coverage_stat.needed, + file_coverage_stat.found, + file_coverage_stat.missing, + file_coverage_stat.coverage, ) - self._print_line() - self._print_line() + final_string += "\n" + file_string + "\n" + return final_string -class MarkdownPrinter(Printer): + def _generate_overall_stat_string(self) -> str: + if isinstance(self.overall_coverage_stat, float): + return str(self.overall_coverage_stat) - def _print_line(self, line: str = ""): - """Prints `line` + prefix = "" - Parameters - ---------- - line: String - The text to print""" - logger.info(line) + if self.overall_coverage_stat.num_empty_files > 0: + prefix = " (%s files are empty)" % self.overall_coverage_stat.num_empty_files - def _print_title_line(self, content: str | int | float): - """Print Markdown 2nd title (`## ...`). + if self.overall_coverage_stat.is_skip_magic: + prefix += " (skipped all non-init magic methods)" - Parameters - ---------- - content : str | int | float - Title content - """ - logger.info("## {}".format(content)) + if self.overall_coverage_stat.is_skip_file_docstring: + prefix += " (skipped file-level docstrings)" - def _print_table( - self, - cols: tuple[str, ...], - rows: tuple[tuple[str | int | float], ...], - ): - """Print markdown table. + if self.overall_coverage_stat.is_skip_init: + prefix += " (skipped __init__ methods)" - Using: - >>> self._print_table( - ... cols=("Needed", "Found", "Missing"), - ... vals=( - ... (10, 20, "65.5%"), - ... (30, 40, "99.9%") - ... ) - ... ) - | Needed | Found | Missing | - |---|---|---| - | 10 | 20 | 65.5% | - | 30 | 40 | 99.9% | + if self.overall_coverage_stat.is_skip_class_def: + prefix += " (skipped class definitions)" - Parameters - ---------- - cols: tuple[str, ...] - Table columns - rows: tuple[tuple[str | int | float], ...] - Column values - """ - assert all(len(v) == len(cols) for v in rows), "Col num not equal to cols value" + if self.overall_coverage_stat.is_skip_private: + prefix += " (skipped private methods)" - col_line = "" - for col in cols: - col_line += "| {} ".format(col) - self._print_line(col_line + "|") + final_string = "" - sep_line = "" - for _ in range(len(cols)): - sep_line += "|---" - self._print_line(sep_line + "|") + if self.overall_coverage_stat.num_files > 1: + final_string += "Overall statistics for %s files%s:\n" % ( + self.overall_coverage_stat.num_files, + prefix, + ) + else: + final_string += "Overall statistics%s:\n" % prefix - for row in rows: - row_line = "" - for value in row: - row_line += "| {} ".format(value) - self._print_line(row_line + "|") - - def print(self) -> None: - if self.verbosity >= 2: - self._print_files_statistic() - if self.verbosity >= 1: - self._print_overall_statistics() - - def _print_files_statistic(self): - for file_info in self.overall_coverage_stat.files_info: - if self.verbosity < 4 and file_info.missing == 0: - continue - - self._print_line('**File**: `{0}`'.format(file_info.path)) - - if self.verbosity > 3: - if file_info.is_empty: - self._print_line("- File is empty") - for node_identifier in file_info.nodes_with_docstring: - self._print_line( - "- Found docstring for `{0}`".format( - node_identifier, - ) + final_string += "Needed: {} - Found: {} - Missing: {}\n".format( + self.overall_coverage_stat.needed, + self.overall_coverage_stat.found, + self.overall_coverage_stat.missing, + ) + + final_string += "Total coverage: {:.1f}% - Grade: {}".format( + self.overall_coverage_stat.total_coverage, + self.overall_coverage_stat.grade, + ) + + return final_string + + +class MarkdownPrinter(LegacyPrinter): + """Printer for Markdown format.""" + + def save_to_file(self, path: Optional[str] = None) -> None: + if path is None: + path = "./coverage-results.md" + with open(path, "w") as wf: + wf.write(self._generate_string()) + + def _generate_file_stat_string(self) -> str: + final_string = "" + for file_coverage_stat in self.overall_files_coverage_stat: + + file_string = '**File**: `{0}`\n'.format(file_coverage_stat.path) + + if file_coverage_stat.is_empty is not None and file_coverage_stat.is_empty is True: + file_string += "- File is empty\n" + + if file_coverage_stat.nodes_with_docstring is not None: + for node_identifier in file_coverage_stat.nodes_with_docstring: + file_string += "- Found docstring for `{0}`\n".format( + node_identifier, ) - for ignored_node in file_info.ignored_nodes: - self._print_line( - "- Ignored `{0}`: reason: `{1}`".format( - ignored_node.identifier, - ignored_node.reason, - ) + + if file_coverage_stat.ignored_nodes is not None: + for ignored_node in file_coverage_stat.ignored_nodes: + file_string += "- Ignored `{0}`: reason: `{1}`\n".format( + ignored_node.identifier, + ignored_node.reason, ) - if self.verbosity >= 3: - for node_identifier in file_info.nodes_without_docstring: + if file_coverage_stat.nodes_without_docstring is not None: + for node_identifier in file_coverage_stat.nodes_without_docstring: if node_identifier == "module docstring": - self._print_line("- No module docstring") + file_string += "- No module docstring\n" else: - self._print_line("- No docstring for `{0}`".format(node_identifier)) + file_string += "- No docstring for `{0}`\n".format(node_identifier) - self._print_line() + file_string += "\n" - self._print_table( + file_string += self._generate_markdown_table( ("Needed", "Found", "Missing", "Coverage"), (( - file_info.needed, - file_info.found, - file_info.missing, - "{:.1f}%".format(file_info.coverage), + file_coverage_stat.needed, + file_coverage_stat.found, + file_coverage_stat.missing, + "{:.1f}%".format(file_coverage_stat.coverage), ),) ) - self._print_line() - self._print_line() + if final_string == "": + final_string += file_string + "\n" + else: + final_string += "\n" + file_string + "\n" + + return final_string + "\n" - def _print_overall_statistics(self): - self._print_title_line("Overall statistic") + def _generate_overall_stat_string(self) -> str: + if isinstance(self.overall_coverage_stat, float): + return str(self.overall_coverage_stat) + + final_string = "## Overall statistics\n" if self.overall_coverage_stat.num_files > 1: - self._print_line("Files number: **{}**".format(self.overall_coverage_stat.num_files)) - self._print_line() - self._print_line( - "Total coverage: **{:.1f}%**".format( - self.overall_coverage_stat.total_coverage, - ) - ) - self._print_line() - self._print_line( - "Grade: **{}**".format(self.overall_coverage_stat.grade) + final_string += "Files number: **{}**\n".format(self.overall_coverage_stat.num_files) + + final_string += "\n" + + final_string += "Total coverage: **{:.1f}%**\n".format( + self.overall_coverage_stat.total_coverage, ) + final_string += "\n" + + final_string += "Grade: **{}**\n".format(self.overall_coverage_stat.grade) + if self.overall_coverage_stat.num_empty_files > 0: - self._print_line("- %s files are empty" % self.overall_coverage_stat.num_empty_files) + final_string += "- %s files are empty\n" % self.overall_coverage_stat.num_empty_files if self.overall_coverage_stat.is_skip_magic: - self._print_line("- skipped all non-init magic methods") + final_string += "- skipped all non-init magic methods\n" if self.overall_coverage_stat.is_skip_file_docstring: - self._print_line("- skipped file-level docstrings") + final_string += "- skipped file-level docstrings\n" if self.overall_coverage_stat.is_skip_init: - self._print_line("- skipped __init__ methods") + final_string += "- skipped __init__ methods\n" if self.overall_coverage_stat.is_skip_class_def: - self._print_line("- skipped class definitions") + final_string += "- skipped class definitions\n" if self.overall_coverage_stat.is_skip_private: - self._print_line("- skipped private methods") + final_string += "- skipped private methods\n" - self._print_line() + final_string += "\n" - self._print_table( + final_string += self._generate_markdown_table( ("Needed", "Found", "Missing"), (( self.overall_coverage_stat.needed, @@ -461,3 +442,55 @@ def _print_overall_statistics(self): self.overall_coverage_stat.missing, ),) ) + + return final_string + + def _generate_markdown_table( + self, + cols: tuple[str, ...], + rows: tuple[tuple[Union[str, int, float]], ...], + ) -> str: + """Generate markdown table. + + Using: + >>> self._generate_markdown_table( + ... cols=("Needed", "Found", "Missing"), + ... vals=( + ... (10, 20, "65.5%"), + ... (30, 40, "99.9%") + ... ) + ... ) + | Needed | Found | Missing | + |---|---|---| + | 10 | 20 | 65.5% | + | 30 | 40 | 99.9% | + + Parameters + ---------- + cols: tuple[str, ...] + Table columns + rows: tuple[tuple[Union[str, int, float]], ...] + Column values + + Returns + ------- + str + Generated table. + """ + assert all(len(v) == len(cols) for v in rows), "Col num not equal to cols value" + final_string = "" + + for col in cols: + final_string += "| {} ".format(col) + final_string += "|\n" + + for _ in range(len(cols)): + final_string += "|---" + final_string += "|\n" + + for row in rows: + for value in row: + final_string += "| {} ".format(value) + final_string += "|" + + return final_string diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 5de4ec0..0fd48cf 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -145,11 +145,11 @@ def test_should_report_when_no_docs_in_a_file(): [ ( [ - '\nFile: "tests/sample_files/subdir_a/empty_file.py"', + "", + 'File: "tests/sample_files/subdir_a/empty_file.py"', " - File is empty", " Needed: 0; Found: 0; Missing: 0; Coverage: 100.0%", "", - "", "Overall statistics (1 files are empty):", "Needed: 0 - Found: 0 - Missing: 0", "Total coverage: 100.0% - Grade: " + _GRADES[0][0], @@ -160,7 +160,7 @@ def test_should_report_when_no_docs_in_a_file(): def test_legacy_printer_logging_empty_file(caplog, expected): with caplog.at_level(logging.DEBUG): result = analyze([EMPTY_FILE_PATH]) - LegacyPrinter(result, verbosity=4).print() + LegacyPrinter(result, verbosity=4).print_to_stdout() _file_results, _total_results = result.to_legacy() if platform.system() == "Windows": @@ -169,6 +169,31 @@ def test_legacy_printer_logging_empty_file(caplog, expected): assert caplog.messages == expected +@pytest.mark.parametrize( + ["expected"], + [ + ( + [ + "\n", + 'File: "tests/sample_files/subdir_a/empty_file.py"\n', + " - File is empty\n", + " Needed: 0; Found: 0; Missing: 0; Coverage: 100.0%\n", + "\n", + "Overall statistics (1 files are empty):\n", + "Needed: 0 - Found: 0 - Missing: 0\n", + "Total coverage: 100.0% - Grade: " + _GRADES[0][0], + ], + ) + ], +) +def test_legacy_save_to_file_printer_empty_file(tmpdir, expected): + path = tmpdir.join("coverage-result.txt") + result = analyze([EMPTY_FILE_PATH]) + LegacyPrinter(result, verbosity=4).save_to_file(path.strpath) + + assert path.readlines() == expected + + @pytest.mark.parametrize( ["expected"], [ @@ -182,7 +207,7 @@ def test_legacy_printer_logging_empty_file(caplog, expected): "| 0 | 0 | 0 | 100.0% |", "", "", - "## Overall statistic", + "## Overall statistics", "", "Total coverage: **100.0%**", "", @@ -199,7 +224,7 @@ def test_legacy_printer_logging_empty_file(caplog, expected): def test_markdown_printer_logging_empty_file(caplog, expected): with caplog.at_level(logging.DEBUG): result = analyze([EMPTY_FILE_PATH]) - MarkdownPrinter(result, verbosity=4).print() + MarkdownPrinter(result, verbosity=4).print_to_stdout() _file_results, _total_results = result.to_legacy() if platform.system() == "Windows": @@ -208,18 +233,53 @@ def test_markdown_printer_logging_empty_file(caplog, expected): assert caplog.messages == expected +@pytest.mark.parametrize( + ["expected"], + [ + ( + [ + '**File**: `tests/sample_files/subdir_a/empty_file.py`\n', + "- File is empty\n", + "\n", + "| Needed | Found | Missing | Coverage |\n", + "|---|---|---|---|\n", + "| 0 | 0 | 0 | 100.0% |\n", + "\n", + "\n", + "## Overall statistics\n", + "\n", + "Total coverage: **100.0%**\n", + "\n", + "Grade: **" + _GRADES[0][0] + "**\n", + "- 1 files are empty\n", + "\n", + "| Needed | Found | Missing |\n", + "|---|---|---|\n", + "| 0 | 0 | 0 |", + ], + ) + ], +) +def test_markdown_save_to_file_printer_empty_file(tmpdir, expected): + path = tmpdir.join("coverage-result.md") + result = analyze([EMPTY_FILE_PATH]) + MarkdownPrinter(result, verbosity=4).save_to_file(path.strpath) + + assert path.readlines() == expected + + @pytest.mark.parametrize( ["expected", "verbose", "ignore_names"], [ ( [ - '\nFile: "tests/sample_files/subdir_a/partly_documented_file.py"', + "", + 'File: "tests/sample_files/subdir_a/partly_documented_file.py"', " - No module docstring", " - No docstring for `foo`", " - No docstring for `bar`", " Needed: 4; Found: 1; Missing: 3; Coverage: 25.0%", "", - "", "Overall statistics:", "Needed: 4 - Found: 1 - Missing: 3", "Total coverage: 25.0% - Grade: " + _GRADES[6][0], @@ -229,14 +289,14 @@ def test_markdown_printer_logging_empty_file(caplog, expected): ), ( [ - '\nFile: "tests/sample_files/subdir_a/partly_documented_file.py"', + "", + 'File: "tests/sample_files/subdir_a/partly_documented_file.py"', " - No module docstring", " - No docstring for `FooBar.__init__`", " - No docstring for `foo`", " - No docstring for `bar`", " Needed: 5; Found: 1; Missing: 4; Coverage: 20.0%", "", - "", "Overall statistics:", "Needed: 5 - Found: 1 - Missing: 4", "Total coverage: 20.0% - Grade: " + _GRADES[7][0], @@ -246,9 +306,9 @@ def test_markdown_printer_logging_empty_file(caplog, expected): ), ( [ - '\nFile: "tests/sample_files/subdir_a/partly_documented_file.py"', - " Needed: 5; Found: 1; Missing: 4; Coverage: 20.0%", "", + 'File: "tests/sample_files/subdir_a/partly_documented_file.py"', + " Needed: 5; Found: 1; Missing: 4; Coverage: 20.0%", "", "Overall statistics:", "Needed: 5 - Found: 1 - Missing: 4", @@ -281,7 +341,127 @@ def test_legacy_printer_logging_partially_documented_file(caplog, expected, verb ignore_config = IgnoreConfig(ignore_names=ignore_names) with caplog.at_level(logging.DEBUG): result = analyze([PARTLY_DOCUMENTED_FILE_PATH], ignore_config=ignore_config) - LegacyPrinter(result, verbosity=verbose, ignore_config=ignore_config).print() + LegacyPrinter(result, verbosity=verbose, ignore_config=ignore_config).print_to_stdout() + + if platform.system() == "Windows": + assert [m.replace("\\", "/") for m in caplog.messages] == expected + else: + assert caplog.messages == expected + + +@pytest.mark.parametrize( + ["expected", "verbose", "ignore_names"], + [ + ( + [ + '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "- No module docstring", + "- No docstring for `foo`", + "- No docstring for `bar`", + "", + "| Needed | Found | Missing | Coverage |", + "|---|---|---|---|", + "| 4 | 1 | 3 | 25.0% |", + "", + "", + "## Overall statistics", + "", + "Total coverage: **25.0%**", + "", + "Grade: **" + _GRADES[6][0] + "**", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 4 | 1 | 3 |", + ], + 3, + ([".*", "__.+__"],), + ), + ( + [ + '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "- No module docstring", + "- No docstring for `FooBar.__init__`", + "- No docstring for `foo`", + "- No docstring for `bar`", + "", + "| Needed | Found | Missing | Coverage |", + "|---|---|---|---|", + "| 5 | 1 | 4 | 20.0% |", + "", + "", + "## Overall statistics", + "", + "Total coverage: **20.0%**", + "", + "Grade: **" + _GRADES[7][0] + "**", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 5 | 1 | 4 |", + ], + 3, + (), + ), + ( + [ + '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "", + "| Needed | Found | Missing | Coverage |", + "|---|---|---|---|", + "| 5 | 1 | 4 | 20.0% |", + "", + "", + "## Overall statistics", + "", + "Total coverage: **20.0%**", + "", + "Grade: **" + _GRADES[7][0] + "**", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 5 | 1 | 4 |", + ], + 2, + (), + ), + ( + [ + "## Overall statistics", + "", + "Total coverage: **20.0%**", + "", + "Grade: **" + _GRADES[7][0] + "**", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 5 | 1 | 4 |", + ], + 1, + (), + ), + ( + [ + "## Overall statistics", + "", + "Total coverage: **0.0%**", + "", + "Grade: **" + _GRADES[9][0] + "**", + "", + "| Needed | Found | Missing |", + "|---|---|---|", + "| 1 | 0 | 1 |", + ], + 1, + ([".*", ".*"],), # ignore all, except module + ), + ], +) +def test_markdown_printer_logging_partially_documented_file(caplog, expected, verbose, ignore_names): + ignore_config = IgnoreConfig(ignore_names=ignore_names) + with caplog.at_level(logging.DEBUG): + result = analyze([PARTLY_DOCUMENTED_FILE_PATH], ignore_config=ignore_config) + MarkdownPrinter(result, verbosity=verbose, ignore_config=ignore_config).print_to_stdout() if platform.system() == "Windows": assert [m.replace("\\", "/") for m in caplog.messages] == expected From 940a31b2346db4097254828965532d3a7ec8b4f4 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Mon, 14 Oct 2024 19:56:35 +0300 Subject: [PATCH 06/12] Upd README options describing section. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2ca295..dc5e8ba 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,10 @@ docstr-coverage some_project/src #### Options -- _--output=\, -o \_ - Set output style (default text) +- _--output=\, -o \_ - Set the output target (default stdout) + - stdout - Output to standard STDOUT. + - file - Save output to file. +- _--format=\, -r \_ - Set output style (default text) - text - Output in simple style. - markdown - Output in Markdown notation. - _--skip-magic, -m_ - Ignore all magic methods (except `__init__`) From 62ee8b3180d0cd77a2b1c73052117c2a647bbc14 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Mon, 14 Oct 2024 19:59:25 +0300 Subject: [PATCH 07/12] Fix output. --- docstr_coverage/printers.py | 2 +- tests/test_coverage.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 26cfbba..f92a9dc 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -288,7 +288,7 @@ def _generate_file_stat_string(self): final_string += "\n" + file_string + "\n" - return final_string + return final_string + "\n" def _generate_overall_stat_string(self) -> str: if isinstance(self.overall_coverage_stat, float): diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 0fd48cf..c498cc3 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -150,6 +150,7 @@ def test_should_report_when_no_docs_in_a_file(): " - File is empty", " Needed: 0; Found: 0; Missing: 0; Coverage: 100.0%", "", + "", "Overall statistics (1 files are empty):", "Needed: 0 - Found: 0 - Missing: 0", "Total coverage: 100.0% - Grade: " + _GRADES[0][0], @@ -179,6 +180,7 @@ def test_legacy_printer_logging_empty_file(caplog, expected): " - File is empty\n", " Needed: 0; Found: 0; Missing: 0; Coverage: 100.0%\n", "\n", + "\n", "Overall statistics (1 files are empty):\n", "Needed: 0 - Found: 0 - Missing: 0\n", "Total coverage: 100.0% - Grade: " + _GRADES[0][0], @@ -280,6 +282,7 @@ def test_markdown_save_to_file_printer_empty_file(tmpdir, expected): " - No docstring for `bar`", " Needed: 4; Found: 1; Missing: 3; Coverage: 25.0%", "", + "", "Overall statistics:", "Needed: 4 - Found: 1 - Missing: 3", "Total coverage: 25.0% - Grade: " + _GRADES[6][0], @@ -297,6 +300,7 @@ def test_markdown_save_to_file_printer_empty_file(tmpdir, expected): " - No docstring for `bar`", " Needed: 5; Found: 1; Missing: 4; Coverage: 20.0%", "", + "", "Overall statistics:", "Needed: 5 - Found: 1 - Missing: 4", "Total coverage: 20.0% - Grade: " + _GRADES[7][0], @@ -310,6 +314,7 @@ def test_markdown_save_to_file_printer_empty_file(tmpdir, expected): 'File: "tests/sample_files/subdir_a/partly_documented_file.py"', " Needed: 5; Found: 1; Missing: 4; Coverage: 20.0%", "", + "", "Overall statistics:", "Needed: 5 - Found: 1 - Missing: 4", "Total coverage: 20.0% - Grade: " + _GRADES[7][0], From 4059d627b94320c5d86ff6304971dd23385bd4ad Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Mon, 14 Oct 2024 20:15:40 +0300 Subject: [PATCH 08/12] Fix code typing and styling. --- docstr_coverage/printers.py | 37 ++++++++++++++++++++----------------- tests/test_coverage.py | 14 ++++++++------ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index f92a9dc..df68e2f 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -106,7 +106,7 @@ def __init__( self.__overall_files_coverage_stat = None @property - def overall_coverage_stat(self) -> OverallCoverageStat | float: + def overall_coverage_stat(self) -> Union[OverallCoverageStat, float]: """Getting full coverage statistic. For `verbosity` with value: @@ -165,8 +165,8 @@ def overall_files_coverage_stat(self) -> Union[list[FileCoverageStat], None]: nodes_without_docstring = tuple( expected_docstring.node_identifier for expected_docstring in file_info._expected_docstrings - if not expected_docstring.has_docstring and - not expected_docstring.ignore_reason + if not expected_docstring.has_docstring + and not expected_docstring.ignore_reason ) else: nodes_without_docstring = None @@ -176,8 +176,7 @@ def overall_files_coverage_stat(self) -> Union[list[FileCoverageStat], None]: nodes_with_docstring = tuple( expected_docstring.node_identifier for expected_docstring in file_info._expected_docstrings - if expected_docstring.has_docstring and - not expected_docstring.ignore_reason + if expected_docstring.has_docstring and not expected_docstring.ignore_reason ) ignored_nodes = tuple( IgnoredNode( @@ -221,7 +220,7 @@ def save_to_file(self, path: Optional[str] = None) -> None: Parameters ---------- - path: str | None + path: Union[str, None] Path to file with coverage results. """ pass @@ -351,7 +350,7 @@ def _generate_file_stat_string(self) -> str: final_string = "" for file_coverage_stat in self.overall_files_coverage_stat: - file_string = '**File**: `{0}`\n'.format(file_coverage_stat.path) + file_string = "**File**: `{0}`\n".format(file_coverage_stat.path) if file_coverage_stat.is_empty is not None and file_coverage_stat.is_empty is True: file_string += "- File is empty\n" @@ -380,12 +379,14 @@ def _generate_file_stat_string(self) -> str: file_string += self._generate_markdown_table( ("Needed", "Found", "Missing", "Coverage"), - (( - file_coverage_stat.needed, - file_coverage_stat.found, - file_coverage_stat.missing, - "{:.1f}%".format(file_coverage_stat.coverage), - ),) + ( + ( + file_coverage_stat.needed, + file_coverage_stat.found, + file_coverage_stat.missing, + "{:.1f}%".format(file_coverage_stat.coverage), + ), + ), ) if final_string == "": @@ -435,13 +436,15 @@ def _generate_overall_stat_string(self) -> str: final_string += "\n" final_string += self._generate_markdown_table( - ("Needed", "Found", "Missing"), - (( + ("Needed", "Found", "Missing"), + ( + ( self.overall_coverage_stat.needed, self.overall_coverage_stat.found, self.overall_coverage_stat.missing, - ),) - ) + ), + ), + ) return final_string diff --git a/tests/test_coverage.py b/tests/test_coverage.py index c498cc3..c4d66d7 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -201,7 +201,7 @@ def test_legacy_save_to_file_printer_empty_file(tmpdir, expected): [ ( [ - '**File**: `tests/sample_files/subdir_a/empty_file.py`', + "**File**: `tests/sample_files/subdir_a/empty_file.py`", "- File is empty", "", "| Needed | Found | Missing | Coverage |", @@ -240,7 +240,7 @@ def test_markdown_printer_logging_empty_file(caplog, expected): [ ( [ - '**File**: `tests/sample_files/subdir_a/empty_file.py`\n', + "**File**: `tests/sample_files/subdir_a/empty_file.py`\n", "- File is empty\n", "\n", "| Needed | Found | Missing | Coverage |\n", @@ -359,7 +359,7 @@ def test_legacy_printer_logging_partially_documented_file(caplog, expected, verb [ ( [ - '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "**File**: `tests/sample_files/subdir_a/partly_documented_file.py`", "- No module docstring", "- No docstring for `foo`", "- No docstring for `bar`", @@ -384,7 +384,7 @@ def test_legacy_printer_logging_partially_documented_file(caplog, expected, verb ), ( [ - '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "**File**: `tests/sample_files/subdir_a/partly_documented_file.py`", "- No module docstring", "- No docstring for `FooBar.__init__`", "- No docstring for `foo`", @@ -410,7 +410,7 @@ def test_legacy_printer_logging_partially_documented_file(caplog, expected, verb ), ( [ - '**File**: `tests/sample_files/subdir_a/partly_documented_file.py`', + "**File**: `tests/sample_files/subdir_a/partly_documented_file.py`", "", "| Needed | Found | Missing | Coverage |", "|---|---|---|---|", @@ -462,7 +462,9 @@ def test_legacy_printer_logging_partially_documented_file(caplog, expected, verb ), ], ) -def test_markdown_printer_logging_partially_documented_file(caplog, expected, verbose, ignore_names): +def test_markdown_printer_logging_partially_documented_file( + caplog, expected, verbose, ignore_names +): ignore_config = IgnoreConfig(ignore_names=ignore_names) with caplog.at_level(logging.DEBUG): result = analyze([PARTLY_DOCUMENTED_FILE_PATH], ignore_config=ignore_config) From 32efe47c1afe02243fe98959c250d333aa7f8cb2 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Fri, 18 Oct 2024 23:23:23 +0300 Subject: [PATCH 09/12] Fix type hint for python3.8 compatibility --- docstr_coverage/printers.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index df68e2f..240c6e1 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -3,7 +3,7 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional, Union +from typing import List, Optional, Tuple, Union from docstr_coverage.ignore_config import IgnoreConfig from docstr_coverage.result_collection import FileStatus, ResultCollection @@ -49,10 +49,10 @@ class FileCoverageStat: missing: int needed: int path: str - ignored_nodes: tuple[IgnoredNode, ...] + ignored_nodes: Tuple[IgnoredNode, ...] is_empty: Union[bool, None] - nodes_with_docstring: Union[tuple[str, ...], None] - nodes_without_docstring: Union[tuple[str, ...], None] + nodes_with_docstring: Union[Tuple[str, ...], None] + nodes_without_docstring: Union[Tuple[str, ...], None] @dataclass(frozen=True) @@ -143,7 +143,7 @@ def overall_coverage_stat(self) -> Union[OverallCoverageStat, float]: return self.__overall_coverage_stat @property - def overall_files_coverage_stat(self) -> Union[list[FileCoverageStat], None]: + def overall_files_coverage_stat(self) -> Union[List[FileCoverageStat], None]: """Getting coverage statistics for files. For `verbosity` with value: @@ -154,11 +154,11 @@ def overall_files_coverage_stat(self) -> Union[list[FileCoverageStat], None]: Returns ------- - list[FileCoverageStat] + List[FileCoverageStat] Coverage info about all checked files.""" if self.__overall_files_coverage_stat is None and self.verbosity >= 2: - overall_files_coverage_stat: list[FileCoverageStat] = [] + overall_files_coverage_stat: List[FileCoverageStat] = [] for file_path, file_info in self.results.files(): if self.verbosity >= 3: @@ -450,8 +450,8 @@ def _generate_overall_stat_string(self) -> str: def _generate_markdown_table( self, - cols: tuple[str, ...], - rows: tuple[tuple[Union[str, int, float]], ...], + cols: Tuple[str, ...], + rows: Tuple[Tuple[Union[str, int, float]], ...], ) -> str: """Generate markdown table. @@ -470,9 +470,9 @@ def _generate_markdown_table( Parameters ---------- - cols: tuple[str, ...] + cols: Tuple[str, ...] Table columns - rows: tuple[tuple[Union[str, int, float]], ...] + rows: Tuple[Tuple[Union[str, int, float]], ...] Column values Returns From 6fb7404ed619a2c4035095fd04bba914e36a5b52 Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Tue, 22 Oct 2024 21:35:23 +0300 Subject: [PATCH 10/12] Fix windows tests compatibility. --- tests/test_coverage.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index c4d66d7..248fa27 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -193,7 +193,11 @@ def test_legacy_save_to_file_printer_empty_file(tmpdir, expected): result = analyze([EMPTY_FILE_PATH]) LegacyPrinter(result, verbosity=4).save_to_file(path.strpath) - assert path.readlines() == expected + lines = path.readlines() + if platform.system() == "Windows": + assert [m.replace("\\", "/") for m in lines] == expected + else: + assert lines == expected @pytest.mark.parametrize( @@ -267,7 +271,11 @@ def test_markdown_save_to_file_printer_empty_file(tmpdir, expected): result = analyze([EMPTY_FILE_PATH]) MarkdownPrinter(result, verbosity=4).save_to_file(path.strpath) - assert path.readlines() == expected + lines = path.readlines() + if platform.system() == "Windows": + assert [m.replace("\\", "/") for m in lines] == expected + else: + assert lines == expected @pytest.mark.parametrize( From d321905b78b796c693f0ba380e00f27da820570f Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Tue, 22 Oct 2024 22:20:17 +0300 Subject: [PATCH 11/12] Add more type hints. Minor fix. --- docstr_coverage/cli.py | 4 +-- docstr_coverage/printers.py | 62 ++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index 231606b..357721e 100644 --- a/docstr_coverage/cli.py +++ b/docstr_coverage/cli.py @@ -346,7 +346,7 @@ def execute(paths, **kwargs): show_progress = not kwargs["percentage_only"] results = analyze(all_paths, ignore_config=ignore_config, show_progress=show_progress) - report_format = kwargs["format"] + report_format: str = kwargs["format"] if report_format == "markdown": printer = MarkdownPrinter(results, verbosity=kwargs["verbose"], ignore_config=ignore_config) elif report_format == "text": @@ -354,7 +354,7 @@ def execute(paths, **kwargs): else: raise SystemError("Unknown report format: {0}".format(report_format)) - output_type = kwargs["output"] + output_type: str = kwargs["output"] if output_type == "file": printer.save_to_file() elif output_type == "stdout": diff --git a/docstr_coverage/printers.py b/docstr_coverage/printers.py index 240c6e1..99b46a6 100644 --- a/docstr_coverage/printers.py +++ b/docstr_coverage/printers.py @@ -6,7 +6,12 @@ from typing import List, Optional, Tuple, Union from docstr_coverage.ignore_config import IgnoreConfig -from docstr_coverage.result_collection import FileStatus, ResultCollection +from docstr_coverage.result_collection import ( + AggregatedCount, + File, + FileStatus, + ResultCollection, +) _GRADES = ( ("AMAZING! Your docstrings are truly a wonder to behold!", 100), @@ -49,10 +54,10 @@ class FileCoverageStat: missing: int needed: int path: str - ignored_nodes: Tuple[IgnoredNode, ...] - is_empty: Union[bool, None] - nodes_with_docstring: Union[Tuple[str, ...], None] - nodes_without_docstring: Union[Tuple[str, ...], None] + ignored_nodes: Optional[Tuple[IgnoredNode, ...]] + is_empty: Optional[Union[bool]] + nodes_with_docstring: Optional[Tuple[str, ...]] + nodes_without_docstring: Optional[Tuple[str, ...]] @dataclass(frozen=True) @@ -99,11 +104,11 @@ def __init__( ignore_config: IgnoreConfig Config with ignoring setups. """ - self.verbosity = verbosity - self.ignore_config = ignore_config - self.results = results - self.__overall_coverage_stat = None - self.__overall_files_coverage_stat = None + self.verbosity: int = verbosity + self.ignore_config: IgnoreConfig = ignore_config + self.results: ResultCollection = results + self.__overall_coverage_stat: Optional[Union[OverallCoverageStat, float]] = None + self.__overall_files_coverage_stat: Optional[List[FileCoverageStat]] = None @property def overall_coverage_stat(self) -> Union[OverallCoverageStat, float]: @@ -114,7 +119,7 @@ def overall_coverage_stat(self) -> Union[OverallCoverageStat, float]: * `1` - All fields, except `files_info`. * `2` - All fields.""" if self.__overall_coverage_stat is None: - count = self.results.count_aggregate() + count: AggregatedCount = self.results.count_aggregate() if self.verbosity >= 1: @@ -143,7 +148,7 @@ def overall_coverage_stat(self) -> Union[OverallCoverageStat, float]: return self.__overall_coverage_stat @property - def overall_files_coverage_stat(self) -> Union[List[FileCoverageStat], None]: + def overall_files_coverage_stat(self) -> Optional[List[FileCoverageStat]]: """Getting coverage statistics for files. For `verbosity` with value: @@ -157,10 +162,16 @@ def overall_files_coverage_stat(self) -> Union[List[FileCoverageStat], None]: List[FileCoverageStat] Coverage info about all checked files.""" if self.__overall_files_coverage_stat is None and self.verbosity >= 2: - overall_files_coverage_stat: List[FileCoverageStat] = [] for file_path, file_info in self.results.files(): + file_path: str + file_info: File + nodes_without_docstring: Optional[Tuple[str, ...]] + is_empty: Optional[bool] + nodes_with_docstring: Optional[Tuple[str, ...]] + ignored_nodes: Optional[Tuple[IgnoredNode, ...]] + if self.verbosity >= 3: nodes_without_docstring = tuple( expected_docstring.node_identifier @@ -220,7 +231,7 @@ def save_to_file(self, path: Optional[str] = None) -> None: Parameters ---------- - path: Union[str, None] + path: Optional[str] Path to file with coverage results. """ pass @@ -240,7 +251,7 @@ def save_to_file(self, path: Optional[str] = None) -> None: wf.write(self._generate_string()) def _generate_string(self) -> str: - final_string = "" + final_string: str = "" if self.overall_files_coverage_stat is not None: final_string += self._generate_file_stat_string() @@ -250,10 +261,10 @@ def _generate_string(self) -> str: return final_string def _generate_file_stat_string(self): - final_string = "" + final_string: str = "" for file_coverage_stat in self.overall_files_coverage_stat: - file_string = 'File: "{0}"\n'.format(file_coverage_stat.path) + file_string: str = 'File: "{0}"\n'.format(file_coverage_stat.path) if file_coverage_stat.is_empty is not None and file_coverage_stat.is_empty is True: file_string += " - File is empty\n" @@ -293,10 +304,10 @@ def _generate_overall_stat_string(self) -> str: if isinstance(self.overall_coverage_stat, float): return str(self.overall_coverage_stat) - prefix = "" + prefix: str = "" if self.overall_coverage_stat.num_empty_files > 0: - prefix = " (%s files are empty)" % self.overall_coverage_stat.num_empty_files + prefix += " (%s files are empty)" % self.overall_coverage_stat.num_empty_files if self.overall_coverage_stat.is_skip_magic: prefix += " (skipped all non-init magic methods)" @@ -313,7 +324,7 @@ def _generate_overall_stat_string(self) -> str: if self.overall_coverage_stat.is_skip_private: prefix += " (skipped private methods)" - final_string = "" + final_string: str = "" if self.overall_coverage_stat.num_files > 1: final_string += "Overall statistics for %s files%s:\n" % ( @@ -347,10 +358,10 @@ def save_to_file(self, path: Optional[str] = None) -> None: wf.write(self._generate_string()) def _generate_file_stat_string(self) -> str: - final_string = "" + final_string: str = "" for file_coverage_stat in self.overall_files_coverage_stat: - file_string = "**File**: `{0}`\n".format(file_coverage_stat.path) + file_string: str = "**File**: `{0}`\n".format(file_coverage_stat.path) if file_coverage_stat.is_empty is not None and file_coverage_stat.is_empty is True: file_string += "- File is empty\n" @@ -400,7 +411,7 @@ def _generate_overall_stat_string(self) -> str: if isinstance(self.overall_coverage_stat, float): return str(self.overall_coverage_stat) - final_string = "## Overall statistics\n" + final_string: str = "## Overall statistics\n" if self.overall_coverage_stat.num_files > 1: final_string += "Files number: **{}**\n".format(self.overall_coverage_stat.num_files) @@ -480,8 +491,9 @@ def _generate_markdown_table( str Generated table. """ - assert all(len(v) == len(cols) for v in rows), "Col num not equal to cols value" - final_string = "" + if not all(len(v) == len(cols) for v in rows): + raise ValueError("Col num not equal to cols value") + final_string: str = "" for col in cols: final_string += "| {} ".format(col) From 6f94034d0533c72eb4c6032b8cadc6f8a38c837d Mon Sep 17 00:00:00 2001 From: skv0zsneg Date: Tue, 10 Dec 2024 23:12:27 +0300 Subject: [PATCH 12/12] Fix lib args naming. Upd README. --- README.md | 4 ++-- docstr_coverage/cli.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dc5e8ba..1974a6e 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ docstr-coverage some_project/src #### Options -- _--output=\, -o \_ - Set the output target (default stdout) +- _--destination=\, -dst \_ - Set the results output destination (default stdout) - stdout - Output to standard STDOUT. - file - Save output to file. -- _--format=\, -r \_ - Set output style (default text) +- _--format=\, -frm \_ - Set output style (default text) - text - Output in simple style. - markdown - Output in Markdown notation. - _--skip-magic, -m_ - Ignore all magic methods (except `__init__`) diff --git a/docstr_coverage/cli.py b/docstr_coverage/cli.py index 357721e..d2b4762 100644 --- a/docstr_coverage/cli.py +++ b/docstr_coverage/cli.py @@ -262,16 +262,16 @@ def _assert_valid_key_value(k, v): help="Deprecated. Use json config (--config / -C) instead", ) @click.option( - "-o", - "--output", + "-dst", + "--destination", type=click.Choice(["stdout", "file"]), default="stdout", - help="Format of output", + help="Results output destination", show_default=True, - metavar="FORMAT", + metavar="DESTINATION", ) @click.option( - "-r", + "-frm", "--format", type=click.Choice(["text", "markdown"]), default="text", @@ -354,13 +354,13 @@ def execute(paths, **kwargs): else: raise SystemError("Unknown report format: {0}".format(report_format)) - output_type: str = kwargs["output"] - if output_type == "file": + destination: str = kwargs["destination"] + if destination == "file": printer.save_to_file() - elif output_type == "stdout": + elif destination == "stdout": printer.print_to_stdout() else: - raise SystemError("Unknown output type: {0}".format(output_type)) + raise SystemError("Unknown output type: {0}".format(destination)) file_results, total_results = results.to_legacy()