Code Quality¶
This guide covers code quality standards and tools used in hier-config-cli.
Overview¶
hier-config-cli maintains high code quality through:
- Black: Code formatting
- Ruff: Fast Python linting
- mypy: Static type checking
- pytest: Testing framework
- pre-commit: Automated checks
Code Formatting with Black¶
Black is used for consistent code formatting.
Configuration¶
pyproject.toml:
Usage¶
# Format all code
black src/ tests/
# Check formatting without changes
black --check src/ tests/
# Show diff of changes
black --diff src/ tests/
# Format specific file
black src/hier_config_cli/__main__.py
Integration¶
VS Code settings.json:
{
"python.formatting.provider": "black",
"python.formatting.blackArgs": ["--line-length", "100"],
"editor.formatOnSave": true
}
PyCharm: 1. Install Black plugin 2. Settings → Tools → Black → Enable 3. Set line length to 100
Linting with Ruff¶
Ruff is an extremely fast Python linter.
Configuration¶
pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = []
Usage¶
# Lint all code
ruff check src/ tests/
# Fix auto-fixable issues
ruff check --fix src/ tests/
# Show all violations
ruff check --output-format=full src/
# Watch for changes
ruff check --watch src/
Common Rules¶
| Rule | Description | Example |
|---|---|---|
| E501 | Line too long | Lines should be ≤100 chars |
| F401 | Unused import | Remove unused imports |
| F841 | Unused variable | Remove or use variable |
| B006 | Mutable default argument | Use None instead |
| UP006 | Use list instead of List |
Modern type syntax |
Fixing Issues¶
# Before (E501 - line too long)
def long_function_name(param1, param2, param3, param4, param5, param6, param7):
pass
# After
def long_function_name(
param1, param2, param3,
param4, param5, param6,
param7,
):
pass
# Before (F401 - unused import)
import json
import os # Not used
# After
import json
# Before (B006 - mutable default)
def process(items=[]):
pass
# After
def process(items=None):
if items is None:
items = []
Type Checking with mypy¶
mypy provides static type checking.
Configuration¶
pyproject.toml:
[tool.mypy]
python_version = "3.10"
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = false
strict_equality = true
[[tool.mypy.overrides]]
module = [
"hier_config.*",
"click.*",
]
ignore_missing_imports = true
Usage¶
# Type check all code
mypy src/
# Check specific file
mypy src/hier_config_cli/__main__.py
# Show error codes
mypy --show-error-codes src/
# Generate coverage report
mypy --html-report mypy-report src/
Writing Type-Safe Code¶
from pathlib import Path
from typing import Optional, Union, List
from collections.abc import Callable
# Function with type hints
def read_config(
path: Path,
encoding: str = "utf-8",
) -> str:
"""Read configuration file.
Args:
path: Path to config file
encoding: File encoding
Returns:
File contents as string
Raises:
FileNotFoundError: If file doesn't exist
"""
return path.read_text(encoding=encoding)
# Using Optional for nullable values
def find_device(name: str) -> Optional[dict]:
"""Find device by name.
Args:
name: Device name
Returns:
Device dict if found, None otherwise
"""
# Implementation
return None
# Type aliases for clarity
DeviceConfig = dict[str, str]
ConfigList = list[DeviceConfig]
def process_devices(devices: ConfigList) -> None:
"""Process multiple device configurations."""
pass
# Callable types
ProcessFunc = Callable[[str], str]
def apply_transform(data: str, func: ProcessFunc) -> str:
"""Apply transformation function to data."""
return func(data)
Common Type Errors¶
# Error: Incompatible return type
def get_count() -> int:
return "5" # Error: expected int, got str
# Fix
def get_count() -> int:
return 5
# Error: Missing type annotation
def process(data): # Error: missing type hints
return data
# Fix
def process(data: str) -> str:
return data
# Error: Incompatible types in assignment
value: int = "hello" # Error
# Fix
value: str = "hello"
Pre-commit Hooks¶
pre-commit runs checks before each commit.
Installation¶
Configuration¶
.pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args: [--fix]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: [types-pyyaml]
args: [--strict]
Usage¶
# Run on all files
pre-commit run --all-files
# Run specific hook
pre-commit run black --all-files
# Skip hooks for a commit
git commit --no-verify
# Update hooks to latest versions
pre-commit autoupdate
Code Review Checklist¶
Before submitting code for review:
Functionality¶
- [ ] Code works as intended
- [ ] Edge cases are handled
- [ ] Error handling is appropriate
- [ ] No hardcoded values
Code Quality¶
- [ ] Black formatting applied
- [ ] No Ruff violations
- [ ] mypy type checking passes
- [ ] Meaningful variable/function names
- [ ] Code is DRY (Don't Repeat Yourself)
Testing¶
- [ ] Tests are written
- [ ] Tests pass
- [ ] Coverage is adequate
- [ ] Edge cases are tested
Documentation¶
- [ ] Docstrings are present
- [ ] Comments explain "why", not "what"
- [ ] README updated if needed
- [ ] API docs updated if needed
Security¶
- [ ] No secrets in code
- [ ] Input validation present
- [ ] No SQL injection risks
- [ ] No command injection risks
Continuous Integration¶
GitHub Actions Workflow¶
.github/workflows/quality.yml:
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install poetry
poetry install
- name: Check formatting with Black
run: poetry run black --check src/ tests/
- name: Lint with Ruff
run: poetry run ruff check src/ tests/
- name: Type check with mypy
run: poetry run mypy src/
- name: Run tests
run: poetry run pytest --cov=hier_config_cli
- name: Check coverage threshold
run: |
poetry run pytest --cov=hier_config_cli --cov-fail-under=90
Quality Metrics¶
Code Coverage¶
Target: >90% coverage
# Generate coverage report
pytest --cov=hier_config_cli --cov-report=term-missing
# Fail if below threshold
pytest --cov=hier_config_cli --cov-fail-under=90
Type Coverage¶
Target: 100% type coverage
Complexity¶
Keep cyclomatic complexity low:
# Install radon
pip install radon
# Check complexity
radon cc src/ -a
# Grade: A is best, F is worst
# Aim for A or B grades
Best Practices¶
Code Organization¶
# Good: Organized imports
import json
import sys
from pathlib import Path
from typing import Optional
import click
import yaml
from hier_config import HConfig
# Module-level constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
# Functions and classes
def main() -> None:
pass
Error Messages¶
# Good: Clear, actionable error messages
raise click.ClickException(
f"Configuration file not found: {path}\n"
f"Please check the path and try again."
)
# Bad: Vague error message
raise Exception("Error")
Function Length¶
Keep functions focused and concise:
# Good: Single responsibility
def read_file(path: Path) -> str:
"""Read file contents."""
return path.read_text()
def parse_config(content: str) -> dict:
"""Parse configuration content."""
return json.loads(content)
# Bad: Too much in one function
def read_and_parse_and_validate(path: Path) -> dict:
content = path.read_text()
data = json.loads(content)
if not validate(data):
raise ValueError("Invalid")
return data
Documentation¶
# Good: Clear docstring with examples
def calculate_diff(
running: str,
intended: str,
) -> list[str]:
"""Calculate configuration differences.
Args:
running: Running configuration
intended: Intended configuration
Returns:
List of commands needed for remediation
Example:
>>> calculate_diff("hostname old", "hostname new")
['no hostname old', 'hostname new']
"""
pass
Tools Summary¶
| Tool | Purpose | Command |
|---|---|---|
| Black | Code formatting | black src/ tests/ |
| Ruff | Linting | ruff check src/ tests/ |
| mypy | Type checking | mypy src/ |
| pytest | Testing | pytest |
| pre-commit | Automated checks | pre-commit run --all-files |
Next Steps¶
- Review Testing Guide
- Read Contributing Guidelines
- Explore Commands Reference