150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
"""Implementazione `countings` usando il CLI `pygount` (JSON) con fallback.
|
|
|
|
Questo modulo fornisce `analyze_file_counts` e `analyze_paths`.
|
|
"""
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Iterable, List
|
|
import json
|
|
import subprocess
|
|
import logging
|
|
|
|
try:
|
|
import pygount # type: ignore
|
|
_HAS_PYGOUNT = True
|
|
except Exception:
|
|
_HAS_PYGOUNT = False
|
|
|
|
_LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def _map_pygount_json_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
# Support multiple pygount JSON shapes and key names
|
|
physical = (
|
|
item.get("raw_total_lines")
|
|
or item.get("n_lines")
|
|
or item.get("lines")
|
|
or item.get("raw_lines")
|
|
or item.get("lineCount")
|
|
or item.get("line_count")
|
|
or 0
|
|
)
|
|
code = (
|
|
item.get("code")
|
|
or item.get("n_code")
|
|
or item.get("n_code_lines")
|
|
or item.get("code_lines")
|
|
or item.get("codeCount")
|
|
or item.get("sourceCount")
|
|
or 0
|
|
)
|
|
comment = (
|
|
item.get("comment")
|
|
or item.get("n_comment")
|
|
or item.get("n_comment_lines")
|
|
or item.get("comment_lines")
|
|
or item.get("documentationCount")
|
|
or 0
|
|
)
|
|
blank = (
|
|
item.get("blank")
|
|
or item.get("n_blank")
|
|
or item.get("blank_lines")
|
|
or item.get("emptyCount")
|
|
or item.get("empty_count")
|
|
or 0
|
|
)
|
|
language = item.get("language") or item.get("lang") or item.get("languageName") or "unknown"
|
|
|
|
file_path = (
|
|
item.get("filename")
|
|
or item.get("file")
|
|
or item.get("path")
|
|
or item.get("name")
|
|
or ""
|
|
)
|
|
|
|
return {
|
|
"file": file_path,
|
|
"physical_lines": int(physical),
|
|
"code_lines": int(code),
|
|
"comment_lines": int(comment),
|
|
"blank_lines": int(blank),
|
|
"language": language,
|
|
}
|
|
|
|
|
|
def analyze_file_counts(path: Path) -> Dict[str, Any]:
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"File non trovato: {path}")
|
|
|
|
result: Dict[str, Any] = {
|
|
"file": str(path),
|
|
"physical_lines": 0,
|
|
"code_lines": 0,
|
|
"comment_lines": 0,
|
|
"blank_lines": 0,
|
|
"language": "unknown",
|
|
}
|
|
|
|
if _HAS_PYGOUNT:
|
|
try:
|
|
proc = subprocess.run(["pygount", "--format", "json", str(path)], check=True, capture_output=True, text=True)
|
|
parsed = json.loads(proc.stdout)
|
|
# Support expected JSON shapes from pygount: list or dict with 'files'
|
|
if isinstance(parsed, list) and parsed:
|
|
item = parsed[0]
|
|
result.update(_map_pygount_json_item(item))
|
|
return result
|
|
if isinstance(parsed, dict):
|
|
files = parsed.get("files")
|
|
if files and isinstance(files, list) and files:
|
|
item = files[0]
|
|
result.update(_map_pygount_json_item(item))
|
|
return result
|
|
# If pygount ran but returned no usable data, log stdout/stderr at DEBUG
|
|
_LOG.debug("pygount returned empty or unexpected JSON for %s", path)
|
|
_LOG.debug("pygount stdout:\n%s", proc.stdout)
|
|
_LOG.debug("pygount stderr:\n%s", proc.stderr)
|
|
# force fallback to simple counting
|
|
raise RuntimeError("pygount returned no data")
|
|
except Exception:
|
|
# Log exception and stderr if available, then fall back to simple counting
|
|
try:
|
|
# If proc exists, include its stderr for diagnostics
|
|
if 'proc' in locals():
|
|
_LOG.exception("pygount invocation failed for %s; stderr:\n%s", path, getattr(proc, 'stderr', None))
|
|
else:
|
|
_LOG.exception("pygount invocation failed for %s", path)
|
|
except Exception:
|
|
# ensure we don't break on logging
|
|
pass
|
|
# fall back to simple counting
|
|
pass
|
|
|
|
# Fallback: basic counting
|
|
with path.open("r", errors="ignore") as fh:
|
|
lines = fh.readlines()
|
|
physical = len(lines)
|
|
blanks = sum(1 for l in lines if l.strip() == "")
|
|
code_lines = physical - blanks
|
|
|
|
result.update({
|
|
"physical_lines": physical,
|
|
"code_lines": code_lines,
|
|
"comment_lines": 0,
|
|
"blank_lines": blanks,
|
|
"language": "unknown",
|
|
})
|
|
return result
|
|
|
|
|
|
def analyze_paths(paths: Iterable[Path]) -> List[Dict[str, Any]]:
|
|
results: List[Dict[str, Any]] = []
|
|
for p in paths:
|
|
path = Path(p)
|
|
try:
|
|
results.append(analyze_file_counts(path))
|
|
except Exception as e:
|
|
results.append({"file": str(path), "error": str(e)})
|
|
return results
|