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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions codelimit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ def list_commands(self, ctx: Context):

@cli.command(help="Check file(s)")
def check(
paths: Annotated[List[Path], typer.Argument(exists=True)],
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
paths: Annotated[List[Path], typer.Argument(exists=True)],
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
):
if exclude:
Configuration.excludes.extend(exclude)
Expand All @@ -41,12 +41,12 @@ def check(

@cli.command(help="Scan a codebase")
def scan(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
):
if exclude:
Configuration.excludes.extend(exclude)
Expand All @@ -55,14 +55,14 @@ def scan(

@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,
totals: Annotated[bool, typer.Option("--totals", help="Only show totals")] = False,
fmt: Annotated[
ReportFormat, typer.Option("--format", help="Output format")
] = ReportFormat.text,
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, totals, fmt)

Expand All @@ -75,15 +75,15 @@ def _version_callback(show: bool):

@cli.callback()
def main(
verbose: Annotated[
Optional[bool], typer.Option("--verbose", "-v", help="Verbose output")
] = False,
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,
version: Annotated[
Optional[bool],
typer.Option(
"--version", "-V", help="Show version", callback=_version_callback
),
] = None,
):
"""Code Limit: Your refactoring alarm."""
if verbose:
Expand Down
17 changes: 10 additions & 7 deletions codelimit/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,49 @@
from pathlib import Path

import typer
from pathspec import PathSpec
from pygments.lexers import get_lexer_for_filename
from pygments.util import ClassNotFound

from codelimit.common.CheckResult import CheckResult
from codelimit.common.Configuration import Configuration
from codelimit.common.Scanner import is_excluded, scan_file
from codelimit.common.lexer_utils import lex
from codelimit.languages import Languages


def check_command(paths: list[Path], quiet: bool):
check_result = CheckResult()
excludes_spec = PathSpec.from_lines("gitignore", Configuration.excludes)
for path in paths:
if path.is_file():
_handle_file_path(path, check_result)
_handle_file_path(path, check_result, excludes_spec)
elif path.is_dir():
for root, dirs, files in os.walk(path.absolute()):
files = [f for f in files if not f[0] == "."]
dirs[:] = [d for d in dirs if not d[0] == "."]
for file in files:
abs_path = Path(os.path.join(root, file))
rel_path = abs_path.relative_to(path.absolute())
if is_excluded(rel_path):
if is_excluded(rel_path, excludes_spec):
continue
check_file(abs_path, check_result)
exit_code = 1 if check_result.unmaintainable > 0 else 0
if (
not quiet
or check_result.hard_to_maintain > 0
or check_result.unmaintainable > 0
not quiet
or check_result.hard_to_maintain > 0
or check_result.unmaintainable > 0
):
check_result.report()
raise typer.Exit(code=exit_code)


def _handle_file_path(path: Path, check_result: CheckResult):
def _handle_file_path(path: Path, check_result: CheckResult, excludes_spec: PathSpec):
if not path.is_absolute():
abs_path = path.absolute().resolve()
try:
rel_path = abs_path.relative_to(Path.cwd())
if is_excluded(rel_path):
if is_excluded(rel_path, excludes_spec):
return
except ValueError:
pass
Expand Down
14 changes: 10 additions & 4 deletions codelimit/commands/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def _report_functions(report: Report, path: Path, full: bool, fmt, console: Cons
if fmt == ReportFormat.markdown:
console.print(_report_functions_markdown(root, report_units), soft_wrap=True)
else:
console.print(_report_functions_text(root, units, report_units, full), soft_wrap=True)
console.print(
_report_functions_text(root, units, report_units, full), soft_wrap=True
)


def get_root(path: Path) -> Path | None:
Expand Down Expand Up @@ -109,17 +111,21 @@ def _report_functions_text(root, units, report_units, full) -> Text:
file_path = unit.file if root is None else root.joinpath(unit.file)
result.append(format_measurement(str(file_path), unit.measurement).append("\n"))
if not full and len(units) > REPORT_LENGTH:
result.append(f"[bold]{len(units) - REPORT_LENGTH} more rows, use --full option to get all rows[/bold]\n")
result.append(
f"[bold]{len(units) - REPORT_LENGTH} more rows, use --full option to get all rows[/bold]\n"
)
return result


def _report_functions_markdown(root: Path | None, report_units: list[ReportUnit]) -> str:
def _report_functions_markdown(
root: Path | 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)
type = '✖' if unit.measurement.value > 60 else '⚠'
type = "✖" if unit.measurement.value > 60 else "⚠"
result += (
f"| {str(file_path)} | {unit.measurement.start.line} | {unit.measurement.start.column} | "
f"{unit.measurement.value} | {type} {unit.measurement.unit_name} |\n"
Expand Down
4 changes: 2 additions & 2 deletions codelimit/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from codelimit.common.report.Report import Report
from codelimit.common.report.ReportReader import ReportReader
from codelimit.github_auth import get_github_token
from codelimit.utils import read_cached_report, upload_report, make_report_path
from codelimit.utils import upload_report, make_report_path


def upload_command(
repository: str, branch: str, report_file: Path, token: str, url: str
repository: str, branch: str, report_file: Path, token: str, url: str
):
if report_file:
report = ReportReader.from_json(report_file.read_text())
Expand Down
32 changes: 10 additions & 22 deletions codelimit/common/Scanner.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fnmatch
import locale
import os
from datetime import datetime
Expand Down Expand Up @@ -86,15 +85,17 @@ def _scan_folder(
cached_report: Union[Report, None] = None,
add_file_entry: Union[Callable[[SourceFileEntry], None], None] = None,
):
gitignore = _read_gitignore(folder)
excludes = Configuration.excludes.copy()
gitignore_excludes = _read_gitignore(folder)
if gitignore_excludes:
excludes.extend(gitignore_excludes)
excludes_spec = PathSpec.from_lines("gitignore", excludes)
for root, dirs, files in os.walk(folder.absolute()):
files = [f for f in files if not f[0] == "."]
dirs[:] = [d for d in dirs if not d[0] == "."]
for file in files:
rel_path = Path(os.path.join(root, file)).relative_to(folder.absolute())
if is_excluded(rel_path) or (
gitignore is not None and is_excluded_by_gitignore(rel_path, gitignore)
):
if is_excluded(rel_path, excludes_spec):
continue
try:
lexer = get_lexer_for_filename(rel_path)
Expand Down Expand Up @@ -178,25 +179,12 @@ def scan_file(tokens: list[Token], language: Language) -> list[Measurement]:
return measurements


def is_excluded(path: Path):
for exclude in Configuration.excludes:
exclude_parts = exclude.split(os.sep)
if len(exclude_parts) == 1:
for part in path.parts:
if fnmatch.fnmatch(part, exclude):
return True
else:
if fnmatch.fnmatch(str(path), exclude):
return True
return False


def _read_gitignore(path: Path) -> PathSpec | None:
def _read_gitignore(path: Path) -> list[str] | None:
gitignore_path = path.joinpath(".gitignore")
if gitignore_path.exists():
return PathSpec.from_lines("gitignore", gitignore_path.read_text().splitlines())
return gitignore_path.read_text().splitlines()
return None


def is_excluded_by_gitignore(path: Path, gitignore: PathSpec):
return gitignore.match_file(path)
def is_excluded(path: Path, spec: PathSpec):
return spec.match_file(path)
2 changes: 1 addition & 1 deletion codelimit/common/gsm/Pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, start: int, automata: DFA):
def consume(self, item) -> State | None:
for transition in self.state.transition:
predicate_id = id(transition[0])
if not predicate_id in self.predicate_map:
if predicate_id not in self.predicate_map:
self.predicate_map[predicate_id] = deepcopy(transition[0])
predicate = self.predicate_map[predicate_id]
if predicate.accept(item):
Expand Down
2 changes: 1 addition & 1 deletion codelimit/common/token_matching/predicate/Balanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ def __hash__(self):
return hash((self.left, self.right, self.depth))

def __str__(self):
return f"<Balanced {self.left} {self.right} {id(self)}>"
return f"<Balanced {self.left} {self.right} {id(self)}>"
2 changes: 1 addition & 1 deletion codelimit/common/token_matching/predicate/Keyword.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def __hash__(self):
return hash(self.keyword)

def __str__(self):
return f'<Keyword {self.keyword}>'
return f"<Keyword {self.keyword}>"
2 changes: 1 addition & 1 deletion codelimit/common/token_matching/predicate/Symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def __hash__(self):
return hash(self.symbol)

def __str__(self):
return f'<Symbol {self.symbol}>'
return f"<Symbol {self.symbol}>"
3 changes: 1 addition & 2 deletions codelimit/common/token_matching/predicate/TokenValue.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ def __eq__(self, other: object) -> bool:
def __hash__(self):
return hash(self.value)


def __str__(self):
return f'<TokenValue {self.value}>'
return f"<TokenValue {self.value}>"
8 changes: 4 additions & 4 deletions codelimit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def read_cached_report(path: Path) -> Optional[Report]:


def upload_report(
report: Report, repository: str, branch: str, url: str, token: str
report: Report, repository: str, branch: str, url: str, token: str
) -> None:
result = api_post_report(report, branch, repository, url, token)
if result.ok:
Expand All @@ -44,9 +44,9 @@ def api_post_report(report, branch, repository, url, token):
f'{{{{"repository": "{repository}", "branch": "{branch}", "report":{{}}}}}}'
)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
) as progress:
progress.add_task(description=f"Uploading report to {url}", total=None)
result = requests.post(
Expand Down
5 changes: 4 additions & 1 deletion tests/commands/test_report.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from codelimit.commands.report import _report_totals_markdown, _report_functions_markdown
from codelimit.commands.report import (
_report_totals_markdown,
_report_functions_markdown,
)
from codelimit.common.LanguageTotals import LanguageTotals
from codelimit.common.Location import Location
from codelimit.common.Measurement import Measurement
Expand Down
37 changes: 15 additions & 22 deletions tests/common/test_Scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
from pathspec import PathSpec

from codelimit.common.Configuration import Configuration
from codelimit.common.Scanner import (
scan_codebase,
is_excluded,
is_excluded_by_gitignore,
)
from codelimit.common.Scanner import scan_codebase, is_excluded
from codelimit.common.source_utils import get_location_range


Expand Down Expand Up @@ -88,30 +84,27 @@ def test_skip_hidden_files():


def test_is_excluded():
assert is_excluded(Path("venv/foo/bar.py"))
assert not is_excluded(Path("foo/bar.py"))
excludes_spec = PathSpec.from_lines("gitignore", Configuration.excludes)

Configuration.excludes = ["output"]
assert is_excluded(Path("venv/foo/bar.py"), excludes_spec)
assert not is_excluded(Path("foo/bar.py"), excludes_spec)

assert is_excluded(Path("output/foo/bar.py"))
assert not is_excluded(Path("venv/foo/bar.py"))
assert not is_excluded(Path("foo/bar.py"))
excludes_spec = PathSpec.from_lines("gitignore", ["output"])

Configuration.excludes = ["foo/bar/*"]
assert is_excluded(Path("output/foo/bar.py"), excludes_spec)
assert not is_excluded(Path("venv/foo/bar.py"), excludes_spec)
assert not is_excluded(Path("foo/bar.py"), excludes_spec)

assert is_excluded(Path("foo/bar/foobar.py"))
excludes_spec = PathSpec.from_lines("gitignore", ["foo/bar/*"])

assert is_excluded(Path("foo/bar/foobar.py"), excludes_spec)

def test_is_excluded_by_gitignore():
Configuration.excludes = ["site/"]
gitignore = PathSpec.from_lines("gitwildmatch", ["site/"])
excludes_spec = PathSpec.from_lines("gitignore", ["site/"])

assert is_excluded_by_gitignore(
Path("site/assets/javascripts/lunr/wordcut.js"), gitignore
)
assert is_excluded(Path("site/assets/javascripts/lunr/wordcut.js"), excludes_spec)

gitignore = PathSpec.from_lines("gitwildmatch", ["!site/"])
excludes_spec = PathSpec.from_lines("gitignore", ["!site/"])

assert not is_excluded_by_gitignore(
Path("site/assets/javascripts/lunr/wordcut.js"), gitignore
assert not is_excluded(
Path("site/assets/javascripts/lunr/wordcut.js"), excludes_spec
)