Skip to content

Commit

Permalink
feat: ✨ Extend report command with --totals and --format options (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
robvanderleek authored Nov 30, 2024
1 parent caf1ff0 commit b10c798
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 64 deletions.
52 changes: 27 additions & 25 deletions codelimit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from codelimit.commands import app
from codelimit.commands.check import check_command
from codelimit.commands.report import report_command
from codelimit.commands.report import report_command, ReportFormat
from codelimit.commands.scan import scan_command
from codelimit.common.Configuration import Configuration
from codelimit.version import version
Expand All @@ -25,31 +25,33 @@ def list_commands(self, ctx: Context):

@cli.command(help="Check file(s)")
def check(
paths: Annotated[List[Path], typer.Argument(exists=True)],
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
paths: Annotated[List[Path], typer.Argument(exists=True)],
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
):
check_command(paths, quiet)


@cli.command(help="Scan a codebase")
def scan(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path(".")
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path(".")
):
scan_command(path)


@cli.command(help="Show report for codebase")
def report(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
full: Annotated[bool, typer.Option("--full", help="Show full report")] = False,
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
full: Annotated[bool, typer.Option("--full", help="Show full report")] = False,
totals: Annotated[bool, typer.Option("--totals", help="Only show totals")] = False,
fmt: Annotated[ReportFormat, typer.Option("--format", help="Output format")] = ReportFormat.text
):
report_command(path, full)
report_command(path, full, totals, fmt)


def _version_callback(show: bool):
Expand All @@ -60,18 +62,18 @@ def _version_callback(show: bool):

@cli.callback()
def main(
verbose: Annotated[
Optional[bool], typer.Option("--verbose", "-v", help="Verbose output")
] = False,
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
version: Annotated[
Optional[bool],
typer.Option(
"--version", "-V", help="Show version", callback=_version_callback
),
] = None,
verbose: Annotated[
Optional[bool], typer.Option("--verbose", "-v", help="Verbose output")
] = False,
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
version: Annotated[
Optional[bool],
typer.Option(
"--version", "-V", help="Show version", callback=_version_callback
),
] = None,
):
"""CodeLimit: Your refactoring alarm."""
if verbose:
Expand Down
99 changes: 86 additions & 13 deletions codelimit/commands/report.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,66 @@
from enum import Enum
from pathlib import Path

import typer
from rich import print
from rich.console import Console

from codelimit.common.ScanResultTable import ScanResultTable
from codelimit.common.ScanTotals import ScanTotals
from codelimit.common.report import ReportUnit
from codelimit.common.report.Report import Report
from codelimit.common.utils import format_measurement
from codelimit.utils import read_cached_report

REPORT_LENGTH = 10


def report_command(path: Path, full: bool):
cwd = Path().resolve()
if str(cwd) == str(path.absolute()):
root = None
elif path.absolute().is_relative_to(cwd):
root = path
class ReportFormat(str, Enum):
text = 'text'
markdown = 'markdown'


def report_command(path: Path, full: bool, totals: bool, fmt: ReportFormat):
report = read_report(path)
if totals:
scan_totals = ScanTotals(report.codebase.totals)
if fmt == ReportFormat.markdown:
print(_report_totals_markdown(scan_totals))
else:
_report_totals_text(scan_totals)
else:
root = path.absolute()
report = read_cached_report(path)
if not report:
print("[red]No cached report found in current folder[/red]")
raise typer.Exit(code=1)
_report_units(report, path, full, fmt)


def _report_totals_text(scan_totals: ScanTotals):
table = ScanResultTable(scan_totals)
print(table)


def _report_totals_markdown(st: ScanTotals) -> str:
result = ''
result += '| **Language** | **Files** | **Lines of Code** | **Functions** | ⚠ | ✖ |\n'
result += '| --- | ---: | ---: | ---: | ---: | ---: |\n'
for lt in st.languages_totals():
result += (
f'| {lt.language} | '
f'{lt.files} | '
f'{lt.loc} | '
f'{lt.functions} | '
f'{lt.hard_to_maintain} | '
f'{lt.unmaintainable} |\n'
)
result += (
f'| | '
f'**{st.total_files()}** | '
f'**{st.total_loc()}** | '
f'**{st.total_functions()}** | '
f'**{st.total_hard_to_maintain()}** | '
f'**{st.total_unmaintainable()}** |')
return result


def _report_units(report: Report, path: Path, full: bool, fmt):
units = report.all_report_units_sorted_by_length_asc(30)
if len(units) == 0:
print(
Expand All @@ -33,10 +71,32 @@ def report_command(path: Path, full: bool):
report_units = units
else:
report_units = units[0:REPORT_LENGTH]
print_functions(root, units, report_units, full)
root = get_root(path)
if fmt == ReportFormat.markdown:
_print_functions_markdown(root, report_units)
else:
_print_functions_text(root, units, report_units, full)


def print_functions(root, units, report_units, full):
def get_root(path: Path) -> Path | None:
cwd = Path().resolve()
if str(cwd) == str(path.absolute()):
return None
elif path.absolute().is_relative_to(cwd):
return path
else:
return path.absolute()


def read_report(path: Path) -> Report:
report = read_cached_report(path)
if not report:
print("[red]No cached report found in current folder[/red]")
raise typer.Exit(code=1)
return report


def _print_functions_text(root, units, report_units, full):
stdout = Console()
for unit in report_units:
file_path = unit.file if root is None else root.joinpath(unit.file)
Expand All @@ -47,3 +107,16 @@ def print_functions(root, units, report_units, full):
print(
f"[bold]{len(units) - REPORT_LENGTH} more rows, use --full option to get all rows[/bold]"
)


def _print_functions_markdown(root: str | None, report_units: list[ReportUnit]) -> str:
result = ''
result += '| **File** | **Line** | **Column** | **Length** | **Function** |\n'
result += '| --- | ---: | ---: | ---: | --- |\n'
for unit in report_units:
file_path = unit.file if root is None else root.joinpath(unit.file)
result += (
f'| {str(file_path)} | {unit.measurement.start.line} | {unit.measurement.start.column} | '
f'{unit.measurement.value} | {unit.measurement.unit_name} |\n'
)
return result
5 changes: 5 additions & 0 deletions codelimit/common/Codebase.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from codelimit.common.LanguageTotals import LanguageTotals
from codelimit.common.SourceFileEntry import SourceFileEntry
from codelimit.common.SourceFolder import SourceFolder
from codelimit.common.Measurement import Measurement
Expand All @@ -9,9 +10,13 @@ def __init__(self, root: str):
self.root = root
self.tree = {"./": SourceFolder()}
self.files: dict[str, SourceFileEntry] = {}
self.totals: dict[str, LanguageTotals] = {}

def add_file(self, entry: SourceFileEntry):
self.files[entry.path] = entry
if entry.language not in self.totals:
self.totals[entry.language] = LanguageTotals(entry.language)
self.totals[entry.language].add(entry)
parent_folder = get_parent_folder(entry.path)
if f"{parent_folder}/" not in self.tree:
self.add_folder(parent_folder)
Expand Down
4 changes: 2 additions & 2 deletions codelimit/common/ScanTotals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@


class ScanTotals:
def __init__(self) -> None:
self._languages_totals: dict[str, LanguageTotals] = {}
def __init__(self, language_totals: dict[str, LanguageTotals] | None = None) -> None:
self._languages_totals: dict[str, LanguageTotals] = language_totals if language_totals else {}

def add(self, entry: SourceFileEntry):
if entry.language not in self._languages_totals:
Expand Down
43 changes: 42 additions & 1 deletion codelimit/common/report/ReportWriter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from codelimit.common.CodebseEntry import CodebaseEntry
from codelimit.common.LanguageTotals import LanguageTotals
from codelimit.common.Measurement import Measurement
from codelimit.common.SourceFileEntry import SourceFileEntry
from codelimit.common.SourceFolder import SourceFolder
Expand All @@ -8,6 +9,7 @@
class ReportWriter:
def __init__(self, report: Report, pretty_print=True):
self.report = report
self.totals = report.codebase.totals
self.tree = report.codebase.tree
self.files = report.codebase.files
self.pretty_print = pretty_print
Expand Down Expand Up @@ -51,10 +53,49 @@ def _collection(self, items: list):
def _codebase_to_json(self):
json = ""
json += self._open('"codebase": {')
json += self._collection([self._tree_to_json(), self._measurements_to_json()])
json += self._collection([self._totals_to_json(), self._tree_to_json(), self._measurements_to_json()])
json += self._close("}")
return json

def _totals_to_json(self):
json = ""
json += self._open('"totals": {')
json += self._collection(
[self._totals_item_to_json(k, v) for k, v in self.totals.items()]
)
json += self._close("}")
return json

def _totals_item_to_json(self, name: str, language_totals: LanguageTotals) -> str:
json = ""
json += self._open(f'"{name}": {{')
json += self._collection(
[
self._totals_files_to_json(language_totals),
self._totals_lines_of_code_to_json(language_totals),
self._totals_functions_to_json(language_totals),
self._totals_hard_to_maintain_to_json(language_totals),
self._totals_unmaintainable_to_json(language_totals)
]
)
json += self._close("}")
return json

def _totals_files_to_json(self, language_totals: LanguageTotals):
return self._line(f'"files": {language_totals.files}')

def _totals_lines_of_code_to_json(self, language_totals: LanguageTotals):
return self._line(f'"lines_of_code": {language_totals.loc}')

def _totals_functions_to_json(self, language_totals: LanguageTotals):
return self._line(f'"functions": {language_totals.functions}')

def _totals_hard_to_maintain_to_json(self, language_totals: LanguageTotals):
return self._line(f'"hard_to_maintain": {language_totals.hard_to_maintain}')

def _totals_unmaintainable_to_json(self, language_totals: LanguageTotals):
return self._line(f'"unmaintainable": {language_totals.unmaintainable}')

def _tree_to_json(self):
json = ""
json += self._open('"tree": {')
Expand Down
36 changes: 36 additions & 0 deletions tests/commands/test_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from codelimit.commands.report import _report_totals_markdown, _print_functions_markdown
from codelimit.common.LanguageTotals import LanguageTotals
from codelimit.common.Location import Location
from codelimit.common.Measurement import Measurement
from codelimit.common.ScanTotals import ScanTotals
from codelimit.common.report.ReportUnit import ReportUnit


def test_report_totals_markdown():
python_totals = LanguageTotals("Python")
python_totals.files = 1
python_totals.loc = 2
python_totals.functions = 3
python_totals.hard_to_maintain = 4
python_totals.unmaintainable = 5
st = ScanTotals(
{
"Python": python_totals
}
)
result = _report_totals_markdown(st)
assert result == (
'| **Language** | **Files** | **Lines of Code** | **Functions** | ⚠ | ✖ |\n'
'| --- | ---: | ---: | ---: | ---: | ---: |\n'
'| Python | 1 | 2 | 3 | 4 | 5 |\n'
'| | **1** | **2** | **3** | **4** | **5** |'
)

def test_print_functions_markdown():
result = _print_functions_markdown(None, [ReportUnit('foo.py', Measurement('bar()', Location(1, 1), Location(30, 1), 30))])

assert result == (
'| **File** | **Line** | **Column** | **Length** | **Function** |\n'
'| --- | ---: | ---: | ---: | --- |\n'
'| foo.py | 1 | 1 | 30 | bar() |\n'
)
28 changes: 15 additions & 13 deletions tests/common/test_Codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ def test_codebase_entry_single_file():
writer = ReportWriter(report, False)

assert (
writer.to_json()
== f'{{"version": "{report.version}", "uuid": "{report.uuid}", '
+ '"root": "/", "codebase": {"tree": {"./": {"entries": ["foo.py"], '
+ '"profile": [0, 0, 0, 0]}}, "files": {"foo.py": {"checksum": '
+ '"abcd1234", "language": "Python", "loc": 20, "profile": [0, 0, 0, 0], '
+ '"measurements": []}}}}'
writer.to_json()
== f'{{"version": "{report.version}", "uuid": "{report.uuid}", '
'"root": "/", "codebase": {"totals": {"Python": {"files": 1, "lines_of_code": '
'20, "functions": 0, "hard_to_maintain": 0, "unmaintainable": 0}}, "tree": '
'{"./": {"entries": ["foo.py"], "profile": [0, 0, 0, 0]}}, "files": '
'{"foo.py": {"checksum": "abcd1234", "language": "Python", "loc": 20, '
'"profile": [0, 0, 0, 0], "measurements": []}}}}'
)


Expand All @@ -37,13 +38,14 @@ def test_codebase_entry_single_folder_single_file():
writer = ReportWriter(report, False)

assert (
writer.to_json()
== f'{{"version": "{report.version}", "uuid": "{report.uuid}", '
+ '"root": "/", "codebase": {"tree": {"./": {"entries": '
+ '["foo/"], "profile": [0, 0, 0, 0]}, "foo/": {"entries": ["bar.py"], '
+ '"profile": [0, 0, 0, 0]}}, "files": {"foo/bar.py": {"checksum": '
+ '"abcd1234", "language": "Python", "loc": 20, "profile": [0, 0, 0, 0], '
+ '"measurements": []}}}}'
writer.to_json()
== f'{{"version": "{report.version}", "uuid": "{report.uuid}", '
'"root": "/", "codebase": {"totals": {"Python": {"files": 1, "lines_of_code": '
'20, "functions": 0, "hard_to_maintain": 0, "unmaintainable": 0}}, "tree": '
'{"./": {"entries": ["foo/"], "profile": [0, 0, 0, 0]}, "foo/": {"entries": '
'["bar.py"], "profile": [0, 0, 0, 0]}}, "files": {"foo/bar.py": {"checksum": '
'"abcd1234", "language": "Python", "loc": 20, "profile": [0, 0, 0, 0], '
'"measurements": []}}}}'
)


Expand Down
Loading

0 comments on commit b10c798

Please sign in to comment.