PYTHON
stats.py🐍python
"""
Code statistics commands.
"""
import re
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table
console = Console()
@click.group()
def stats():
"""Code statistics and analysis."""
pass
@stats.command()
@click.argument('path', type=click.Path(exists=True), default='.')
@click.option('--ext', '-e', multiple=True, default=['.py'], help='File extensions')
def lines(path: str, ext: tuple):
"""Count lines of code."""
path = Path(path)
total_files = 0
total_lines = 0
total_code = 0
total_comments = 0
total_blank = 0
# Collect by extension
by_ext = {}
for extension in ext:
if not extension.startswith('.'):
extension = f'.{extension}'
files = list(path.rglob(f'*{extension}'))
for f in files:
if any(p.startswith('.') for p in f.parts):
continue
if 'node_modules' in f.parts or '__pycache__' in f.parts:
continue
try:
content = f.read_text()
file_lines = content.split('\n')
code = 0
comments = 0
blank = 0
for line in file_lines:
stripped = line.strip()
if not stripped:
blank += 1
elif stripped.startswith('#') or stripped.startswith('//'):
comments += 1
else:
code += 1
total_files += 1
total_lines += len(file_lines)
total_code += code
total_comments += comments
total_blank += blank
if extension not in by_ext:
by_ext[extension] = {'files': 0, 'lines': 0}
by_ext[extension]['files'] += 1
by_ext[extension]['lines'] += len(file_lines)
except (UnicodeDecodeError, PermissionError):
continue
# Display results
table = Table(title="Lines of Code")
table.add_column("Metric", style="cyan")
table.add_column("Count", style="green", justify="right")
table.add_row("Files", str(total_files))
table.add_row("Total Lines", str(total_lines))
table.add_row("Code Lines", str(total_code))
table.add_row("Comments", str(total_comments))
table.add_row("Blank Lines", str(total_blank))
console.print(table)
if len(by_ext) > 1:
console.print("\n[bold]By Extension:[/bold]")
for ext_name, data in sorted(by_ext.items()):
console.print(f" {ext_name}: {data['files']} files, {data['lines']} lines")
@stats.command()
@click.argument('path', type=click.Path(exists=True), default='.')
@click.option('--pattern', '-p', default='TODO|FIXME|HACK|XXX', help='Regex pattern')
def todos(path: str, pattern: str):
"""Find TODO comments and similar markers."""
path = Path(path)
regex = re.compile(f'({pattern})', re.IGNORECASE)
findings = []
for f in path.rglob('*.py'):
if any(p.startswith('.') for p in f.parts):
continue
if '__pycache__' in f.parts:
continue
try:
lines = f.read_text().split('\n')
for i, line in enumerate(lines, 1):
match = regex.search(line)
if match:
findings.append({
'file': str(f),
'line': i,
'type': match.group(1).upper(),
'text': line.strip()[:60]
})
except (UnicodeDecodeError, PermissionError):
continue
if not findings:
console.print("[green]No TODOs found![/green]")
return
table = Table(title=f"Found {len(findings)} markers")
table.add_column("Type", style="yellow", width=6)
table.add_column("File", style="cyan")
table.add_column("Line", style="green", justify="right")
table.add_column("Text", style="white")
for f in findings[:50]: # Limit output
table.add_row(f['type'], f['file'], str(f['line']), f['text'])
console.print(table)
if len(findings) > 50:
console.print(f"\n... and {len(findings) - 50} more")
@stats.command()
@click.argument('path', type=click.Path(exists=True), default='.')
def summary(path: str):
"""Show project summary statistics."""
path = Path(path)
# Count files by extension
extensions = {}
for f in path.rglob('*'):
if f.is_file() and not any(p.startswith('.') for p in f.parts):
ext = f.suffix or 'no extension'
extensions[ext] = extensions.get(ext, 0) + 1
# Sort by count
sorted_ext = sorted(extensions.items(), key=lambda x: -x[1])[:15]
table = Table(title="Project Summary")
table.add_column("Extension", style="cyan")
table.add_column("Count", style="green", justify="right")
for ext, count in sorted_ext:
table.add_row(ext, str(count))
console.print(table)
# Check for common project files
console.print("\n[bold]Project Files:[/bold]")
project_files = [
('README.md', 'Documentation'),
('pyproject.toml', 'Python project'),
('package.json', 'Node.js project'),
('Dockerfile', 'Docker support'),
('docker-compose.yml', 'Docker Compose'),
('.gitignore', 'Git configuration'),
('requirements.txt', 'Python dependencies'),
('Makefile', 'Make automation'),
]
for filename, desc in project_files:
if (path / filename).exists():
console.print(f" [green]✓[/green] {filename} ({desc})")