Architecture
pycmdcheck Architecture
This document describes the internal architecture of pycmdcheck, including the core modules and how they interact.
Module Overview
pycmdcheck/
├── core/ # Core infrastructure
│ ├── runner.py # Check execution engine
│ ├── registry.py # Check registry
│ ├── errors.py # Exception hierarchy
│ ├── subprocess_helpers.py # Command execution
│ ├── file_helpers.py # File I/O utilities
│ ├── ast_helpers.py # AST visitor base classes
│ ├── ast_cache.py # AST caching
│ ├── progress.py # Progress reporting
│ └── check_config.py # Typed check configurations
├── checks/ # Check implementations
│ ├── structure.py # Package structure checks
│ ├── metadata.py # Package metadata checks
│ ├── code_quality/ # Code quality checks (split into submodules)
│ ├── tests.py # Test suite checks
│ └── ...
├── output/ # Output formatters
│ ├── base.py # BaseFormatter abstract class
│ ├── constants.py # Shared constants
│ ├── markdown_utils.py # Markdown utilities
│ └── ...
├── cli/ # CLI commands
│ ├── commands/ # Main commands (check, fix, list, etc.)
│ └── groups/ # Command groups (config, baseline, plugin)
└── fixtures/ # Built-in fixtures
└── builtin.py # pyproject_data, all_py_files, etc.
Core Module Details
Error Handling (core/errors.py)
pycmdcheck uses a structured exception hierarchy:
from pycmdcheck.core.errors import (
PycmdcheckError, # Base exception (exit_code=1)
ConfigError, # Configuration errors (exit_code=2)
PluginError, # Plugin-related errors (exit_code=3)
PluginConflictError, # Two plugins define same check
PluginLoadError, # Failed to load plugin
ProfileError, # Profile-related errors (exit_code=2)
CircularInheritanceError, # Profile inheritance cycle
CheckExecutionError, # Error during check (exit_code=3)
)Exit codes:
| Code | Meaning |
|---|---|
| 0 | Success (all checks passed) |
| 1 | Check failures |
| 2 | Configuration error |
| 3 | Internal/execution error |
All exceptions support an optional hint parameter for user-friendly suggestions:
raise ConfigError(
"Invalid profile 'strict'",
hint="Did you mean 'recommended'? Available profiles: minimal, recommended, strict"
)Command Execution (core/subprocess_helpers.py)
Centralized subprocess execution with consistent error handling:
from pycmdcheck.core.subprocess_helpers import run_command, CommandResult
# Run a command with timeout
result = run_command(
["ruff", "check", "."],
timeout=60,
cwd="/path/to/package",
capture_output=True
)
if result.success:
print(result.stdout)
else:
print(f"Error: {result.stderr}")The CommandResult dataclass provides:
returncode: Process exit codestdout: Standard output (string)stderr: Standard error (string)success: Boolean (returncode == 0)error: Optional error message
File I/O (core/file_helpers.py)
Utilities for reading files and finding Python files:
from pycmdcheck.core.file_helpers import (
read_text, # Read file with encoding handling
parse_python_file, # Parse Python file to AST
parse_python_file_cached, # Cached version
find_python_files, # Find all .py files
is_test_path, # Check if path is a test file
)
# Read a file (returns None on error)
content = read_text(Path("README.md"))
# Parse Python file
result = parse_python_file(Path("src/mypackage/module.py"))
if result.tree:
# AST is available
pass
elif result.error:
print(f"Parse error: {result.error}")
# Find Python files (excludes __pycache__, .venv, etc.)
files = find_python_files(Path("src"))AST Visitor Base Classes (core/ast_helpers.py)
Reusable base classes for AST visitors:
from pycmdcheck.core.ast_helpers import (
BaseASTVisitor, # Static utilities
FileProcessingVisitor, # Tracks file path and issues
ClassTrackingVisitor, # Tracks class context
FunctionTrackingVisitor, # Tracks function context
TypeCheckingAwareVisitor,# Handles TYPE_CHECKING blocks
)Inheritance hierarchy:
BaseASTVisitor (static utilities)
└── FileProcessingVisitor (file_path, locations)
└── ClassTrackingVisitor (current_class, qualified_name)
└── FunctionTrackingVisitor (current_function)
└── TypeCheckingAwareVisitor (in_type_checking)
Example usage:
import ast
from pycmdcheck.core.ast_helpers import ClassTrackingVisitor
class DocstringVisitor(ClassTrackingVisitor):
def __init__(self, file_path: str) -> None:
super().__init__(file_path)
self.missing_docstrings: list[str] = []
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
if self.is_public(node.name):
docstring = self.get_docstring(node)
if not docstring:
full_name = self.qualified_name(node.name)
self.add_location(node.lineno, full_name)
self.generic_visit(node)AST Caching (core/ast_cache.py)
Cache parsed AST modules to avoid repeated parsing:
from pycmdcheck.core.ast_cache import ASTCache, get_default_cache
# Use the default cache
cache = get_default_cache()
tree = cache.get_ast(Path("src/module.py"))
# Or create your own cache
cache = ASTCache()
tree = cache.get_ast(Path("src/module.py"))
# Check if file is cached
if Path("src/module.py") in cache:
print("Already cached")
# Clear cache
cache.clear()Progress Reporting (core/progress.py)
Rich-based progress reporting:
from pycmdcheck.core.progress import ProgressReporter
reporter = ProgressReporter(total=10, verbose=True)
reporter.start()
for check in checks:
reporter.update(check.check_id, "running")
# ... run check ...
reporter.update(check.check_id, "passed")
reporter.finish(duration=1.5)Result Builders (core/result_builders.py)
Fluent builders for constructing CheckResult objects with automatic location limiting:
from pycmdcheck.core.result_builders import (
CheckResultBuilder,
LocationCollector,
collect_locations,
)
# LocationCollector - automatic limiting with truncation tracking
collector = LocationCollector(max_locations=10)
for file, line, name in all_issues:
collector.add(file, line, symbol=name)
# collector.total_count = actual count (may exceed max)
# collector.is_truncated = True if limit hit
# collector.truncation_suffix() = " (showing 10 of 25)"
# CheckResultBuilder - fluent API with check metadata forwarding
result = CheckResultBuilder.for_check(MyCheck) \
.failed(f"Found {collector.total_count} issues") \
.with_locations(collector) \
.with_hint("Fix the issues", show_truncation=True) \
.build()
# Convenience method for common pattern
result = CheckResultBuilder.for_check(MyCheck) \
.failed_with_locations(
items=issues,
message_template="Found {count} issue(s)",
location_fn=lambda x: (x.file, x.line, x.name),
hint="Fix the issues",
max_locations=10,
) \
.build()Typed Check Configuration (core/check_config.py)
Typed dataclasses for check configuration with validation:
from pycmdcheck.core.check_config import (
CheckConfig,
CircularImportCheckConfig,
CyclomaticComplexityConfig,
get_config_class,
parse_check_config,
)
# Parse config from pyproject.toml
config = parse_check_config("DP004", {
"ignore_modules": ["mypackage.compat"],
"max_cycles": 5
})
# Validate configuration
errors = config.validate()
if errors:
for error in errors:
print(f"Config error: {error}")
# Use in a check
cfg = CyclomaticComplexityConfig.from_dict(check_config or {})
max_complexity = cfg.max_complexityOutput Infrastructure
Base Formatter (output/base.py)
Abstract base class for all output formatters:
from pycmdcheck.output.base import BaseFormatter
class MyFormatter(BaseFormatter):
def format(self, results: Results) -> str:
# Implement formatting logic
return "..."Markdown Utilities (output/markdown_utils.py)
from pycmdcheck.output.markdown_utils import (
escape_markdown, # Escape special characters
escape_table_cell, # Escape for table cells
escape_html, # Escape HTML entities
wrap_in_code, # Wrap in backticks
make_link, # Create [text](url)
make_collapsible, # Create <details> block
)CLI Structure
The CLI is organized into commands and groups:
cli/
├── __init__.py # Main entry point (main function)
├── utils.py # Shared utilities
├── commands/
│ ├── check.py # pycmdcheck [PATH]
│ ├── fix.py # pycmdcheck fix
│ ├── list_cmd.py # pycmdcheck list
│ ├── info.py # pycmdcheck info
│ ├── explain.py # pycmdcheck explain
│ ├── remote.py # pycmdcheck remote
│ └── misc.py # Other commands
└── groups/
├── config.py # pycmdcheck config ...
├── baseline.py # pycmdcheck baseline ...
└── plugin.py # pycmdcheck plugin ...
Check Module Structure
Large check modules are split into submodules for maintainability:
checks/code_quality/
├── __init__.py # Re-exports all checks, contains remaining checks
├── ruff.py # RuffCheck
├── mypy.py # MypyCheck
├── docstrings.py # DocstringCoverage, DoctestCheck
└── type_hints.py # HasTypeHints
All checks remain importable from the top level:
# Both work:
from pycmdcheck.checks.code_quality import RuffCheck
from pycmdcheck.checks.code_quality.ruff import RuffCheckWriting New Checks
See the plugin development guide for creating new checks.
Basic pattern:
from pycmdcheck.core import CheckProtocol, CheckResult, CheckStatus
class MyCheck:
check_id = "XX001"
title = "My Check"
description = "Checks something important"
rationale = "This is important because..."
severity = "warning"
group = "my-group"
def check(self, root: Path, fixtures: dict) -> CheckResult:
# Implementation
if problem_found:
return CheckResult(
check_id=self.check_id,
status=CheckStatus.FAILED,
message="Problem description",
hint="How to fix it",
)
return CheckResult(
check_id=self.check_id,
status=CheckStatus.PASSED,
)