PYTHONPython

stats

real world projects / cli tool / devtools / commands

PYTHON
stats.py🐍
"""
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})")
PreviousNext