"""Module containing the application abstraction. """
from typing import List, Optional, Union
import argparse
import pathlib
import json
import pprint
import reducto.package as pkg
import reducto.src as src
import reducto.reports as rp
import reducto as rd
[docs]class Reducto:
"""Class defining the package application.
This class represents the reducto application.
Abstracts the application to be used as a command line interface by means of
argparse.ArgumentParser.
The different arguments for the app are defined as private methods of this class.
"""
def __init__(self) -> None: # pragma: no cover, redirects methods
self.parser: argparse.ArgumentParser = argparse.ArgumentParser()
self.args: Optional[argparse.Namespace] = None
# Add arguments
self._add_argument_version()
self._add_argument_target()
self._add_argument_format()
self._add_argument_grouped()
self._add_argument_output_file()
self._add_argument_as_percentage()
def _parse_args(self, argv: Optional[List[str]] = None) -> None: # pragma: no cover
# proxy function to simplify testing
self.args = self.parser.parse_args(argv)
def _add_argument_version(self) -> None: # pragma: no cover
"""Version argument.
Returns the current version of the package.
"""
self.parser.add_argument(
"-v",
"--version",
action="version",
version=f"reducto {rd.__version__}",
help="Show the version of the program.",
)
def _add_argument_target(self) -> None: # pragma: no cover
"""Target argument.
Expects the path pointing to a python package or source file.
"""
self.parser.add_argument(
"target",
type=pathlib.Path,
default=pathlib.Path.cwd(),
help="Path to execute the program into. "
"Must be either a python package (directory containing an __init__.py) "
"or a python source file {SRC.py}",
nargs="?",
)
def _add_argument_format(self) -> None: # pragma: no cover
"""Adds the argument for the type of output format.
The current implementation only allows for raw format (a dict).
Notes
-----
Add redirection to tabulate methods.
"""
self.parser.add_argument(
"-f",
"--format",
type=rp.ReportFormat,
default=rp.ReportFormat.JSON,
choices=list(rp.ReportFormat),
dest="format",
help="Format for the report type.",
)
def _add_argument_grouped(self) -> None: # pragma: no cover
"""Whether to group the package report or not.
Notes
-----
The implementation for the boolean argument is taken from:
https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
"""
help_: str = (
"Return the results separated by "
"source files, or grouped for the "
"whole package. Only used when the "
"target path is a package."
)
grouped_parser = self.parser.add_mutually_exclusive_group(required=False)
grouped_parser.add_argument(
"--grouped", dest="grouped", action="store_true", help=help_
)
grouped_parser.add_argument(
"--ungrouped",
dest="grouped",
action="store_false",
help="Opposite of --grouped.",
)
self.parser.set_defaults(grouped=True)
def _add_argument_output_file(self) -> None: # pragma: no cover
"""Argument to insert the output file (if applies)."""
self.parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
default=None,
help="Full path of the report to be generated. If not"
" given, redirects to stdout.",
)
def _add_argument_exclude(self) -> None: # pragma: no cover
"""Add argument to exclude paths, files, methods (private or dunder)."""
raise NotImplementedError
def _add_argument_as_percentage(self) -> None: # pragma: no cover
"""Add argument to report lines as percentage."""
self.parser.add_argument(
"-p",
"--percentage",
dest="percentage",
action="store_true",
help="Report the number of lines as percentage.",
)
def _report_source_file(self, target: pathlib.Path) -> rp.SourceReportType:
"""Create a report of a single source file.
Parameters
----------
target : pathlib.Path
Path to the source file.
Returns
-------
report : rp.ReportDict
Dict containing the report.
"""
src_file: src.SourceFile = src.SourceFile(target)
reporter: rp.SourceReport = src_file.report()
return reporter.report(
fmt=self.args.format, # type: ignore[union-attr]
is_package=True,
percentage=self.args.percentage, # type: ignore[union-attr]
)
def _report_package(self, target: pathlib.Path) -> rp.PackageReportType:
"""Create a report of a python package.
Parameters
----------
target : pathlib.Path
Path to the package.
Returns
-------
report : rp.ReportPackageDict
Dict containing the report.
"""
package: pkg.Package = pkg.Package(target)
reporter: rp.PackageReport = package.report()
return reporter.report(
fmt=self.args.format, # type: ignore[union-attr]
grouped=self.args.grouped, # type: ignore[union-attr]
percentage=self.args.percentage, # type: ignore[union-attr]
)
[docs] def report(self) -> Union[rp.SourceReportType, rp.PackageReportType]:
"""Detects whether the input target is a file or a directory.
Calls the corresponding method depending on the target.
See Also
--------
run
"""
target: pathlib.Path = self.args.target # type: ignore[union-attr]
if target.is_file():
report: Union[
rp.SourceReportType, rp.PackageReportType
] = self._report_source_file(target)
else:
report = self._report_package(target)
return report
def _write_report(
self, report: Union[rp.SourceReportType, rp.PackageReportType]
) -> None: # pragma: no cover, proxy to json dump
"""Writes the report to a json file.
Parameters
----------
report : Union[rp.ReportDict, rp.ReportPackageDict]
Contains the resulting report.
"""
output_file = self.args.output # type: ignore[union-attr]
if isinstance(report, dict):
with open(output_file, "w") as f:
json.dump(report, f, indent=4)
else:
with open(output_file, "w") as f:
f.write(report)
print(f"Report generated: {output_file}")
[docs] def run(self, argv: Optional[List[str]] = None) -> None:
"""Execute reducto.
The only relevant public method.
Parses the terminal arguments, generates the report
and writes it to a file.
Parameters
----------
argv : Optional[List[str]]
Arguments passed from the terminal.
"""
self._parse_args(argv)
report: Union[rp.SourceReportType, rp.PackageReportType] = self.report()
if self.args.output is not None: # type: ignore[union-attr] # pragma: no cover
# Write file if output is given.
self._write_report(report)
elif self.args.format == rp.ReportFormat.JSON: # type: ignore[union-attr]
# pretty print dict result
pprint.pprint(report)
else: # tabulate results are expected to be printed with print.
print(report)