style guide
This document outlines the coding standards and best practices for the Lintro project.
Lintro Style Guide
This document outlines the coding standards and best practices for the Lintro project.
Python Code Style
General Guidelines
- Follow PEP 8 for code style
- Use pydoclint for docstring argument checking
- Use Oxfmt for JavaScript/TypeScript formatting
- Use Prettier for CSS, HTML, JSON, YAML, and Markdown formatting
Type Hints
- All functions and methods must include type hints
- All classes must include type hints for attributes
- Prefer using pipe operator (
|) overOptionalfor optional types - Avoid importing from
typingfor built-in types likelist,dict, etc. - Use
Anysparingly and only when absolutely necessary
# ❌ Bad
from typing import Dict, List, Optional
def process_data(data: Optional[List[Dict[str, str]]] = None) -> Optional[Dict[str, str]]:
pass
# ✅ Good
def process_data(data: list[dict[str, str]] | None = None) -> dict[str, str] | None:
pass
Docstrings
- All modules, classes, functions, and methods must have docstrings
- Use Google-style docstrings
- Include parameter descriptions, return value descriptions, and raised exceptions
def calculate_total(items: list[dict[str, float]]) -> float:
"""
Calculate the total value of all items.
Args:
items: List of items with their prices
Returns:
Total value of all items
Raises:
ValueError: If any item has a negative price
"""
pass
Function and Method Definitions
- For function/method declarations with more than 1 parameter, use trailing commas and format with Ruff
- Same applies to function/method calls with multiple arguments
# Function definition with multiple parameters
def complex_function(
param1: str,
param2: int,
param3: bool = False,
) -> str:
pass
# Function call with multiple arguments
result = complex_function(
"value1",
42,
True,
)
Imports
- Use
ruffto automatically sort imports - Group imports in the following order:
- Standard library imports
- Third-party imports
- Local application imports
- Use absolute imports
- Use explicit imports
- Use
from __future__ import annotationsin Python files
# Standard library imports
import os
import sys
from pathlib import Path
# Third-party imports
import click
from tabulate import tabulate
# Local imports
from lintro.tools import Tool
from lintro.utils import format_output
Error Handling
- Use specific exception types rather than catching all exceptions
- Include meaningful error messages
- Use context managers (
withstatements) for resource management
# ❌ Bad
try:
process_file(filename)
except Exception as e:
print(f"Error: {e}")
# ✅ Good
try:
process_file(filename)
except FileNotFoundError:
print(f"File {filename} not found")
except PermissionError:
print(f"Permission denied when accessing {filename}")
Variable Naming
- Use descriptive variable names
- Use
snake_casefor variables, functions, and methods - Use
PascalCasefor classes - Use
UPPER_CASEfor constants
# Constants
MAX_RETRY_COUNT = 5
DEFAULT_TIMEOUT = 30
# Variables
user_input = input("Enter your name: ")
file_path = "/path/to/file.txt"
# Functions
def calculate_total(items):
pass
# Classes
class FileProcessor:
pass
Project Structure
Directory Layout
- Keep related files together
- Use meaningful directory names
- Separate code, tests, and documentation
lintro/
├── __init__.py
├── __main__.py
├── cli.py
├── cli_utils/
│ ├── __init__.py
│ └── commands/
│ ├── check.py
│ ├── config.py
│ ├── format.py
│ ├── init.py
│ ├── list_tools.py
│ ├── test.py
│ └── versions.py
├── config/
│ ├── config_loader.py
│ ├── lintro_config.py
│ └── tool_config_generator.py
├── enums/
│ ├── action.py
│ ├── tool_name.py
│ └── tool_type.py
├── exceptions/
│ └── errors.py
├── formatters/
│ ├── core/
│ ├── styles/
│ └── tools/
├── models/
│ └── core/
├── parsers/
│ ├── actionlint/
│ ├── bandit/
│ ├── black/
│ ├── clippy/
│ ├── pydoclint/
│ ├── hadolint/
│ ├── markdownlint/
│ ├── mypy/
│ ├── oxfmt/
│ ├── oxlint/
│ ├── prettier/
│ ├── pytest/
│ ├── ruff/
│ └── yamllint/
├── tools/
│ ├── core/
│ ├── definitions/
│ │ ├── actionlint.py
│ │ ├── bandit.py
│ │ ├── black.py
│ │ ├── clippy.py
│ │ ├── pydoclint.py
│ │ ├── hadolint.py
│ │ ├── markdownlint.py
│ │ ├── mypy.py
│ │ ├── oxfmt.py
│ │ ├── oxlint.py
│ │ ├── prettier.py
│ │ ├── pytest.py
│ │ ├── ruff.py
│ │ └── yamllint.py
│ └── implementations/
│ ├── pytest/
│ └── ruff/
└── utils/
├── console_logger.py
├── logger_setup.py
├── output_formatting.py
├── result_formatters.py
├── summary_tables.py
└── tool_executor.py
tests/
├── cli/
├── config/
├── formatters/
├── integration/
├── unit/
└── utils/
File Organization
- Each module should have a single, well-defined responsibility
- Keep files to a reasonable size (< 500 lines if possible)
- Use meaningful file names that reflect their contents
Commit Messages
- Use the imperative mood in commit messages
- Start with a prefix indicating the type of change
- Include a brief summary of changes in the first line
- Optionally include a more detailed description in subsequent lines
Prefixes:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code changes that neither fix bugs nor add featuresperf: Performance improvementstest: Adding or modifying testsbuild: Changes to build system or dependenciesci: Changes to CI configuration
feat: add support for mypy integration
- Add MyPyTool class
- Update CLI to include mypy in available tools
- Add tests for mypy integration
Testing
- Write tests for all new features and bug fixes
- Aim for high test coverage (>= 90%)
- Use pytest for testing
- Use fixtures to reduce test code duplication
- Use meaningful test names that describe what is being tested
def test_pydoclint_tool_checks_docstrings_correctly():
# Test implementation
pass
def test_prettier_tool_formats_code_correctly():
# Test implementation
pass
Documentation
- Keep documentation up-to-date with code changes
- Document all public APIs
- Include examples in documentation
- Use Markdown for documentation files
Tool Configuration
When adding new tools to Lintro, ensure they follow these guidelines:
- Inherit from
BaseToolPlugininlintro.plugins.base - Use
@register_tooldecorator fromlintro.plugins.registry - Implement
definitionproperty returningToolDefinition - Implement
check()and optionallyfix()methods - Create a parser in
lintro.parsers.{tool_name}/to parse tool output - Use unified formatter in
lintro.formatters.unified - Include comprehensive docstrings
- Add appropriate tests in
tests/unit/tools/andtests/integration/ - Update documentation
Example tool configuration:
from dataclasses import dataclass
from lintro.enums.tool_type import ToolType
from lintro.models.core.tool_result import ToolResult
from lintro.plugins.base import BaseToolPlugin
from lintro.plugins.protocol import ToolDefinition
from lintro.plugins.registry import register_tool
@register_tool
@dataclass
class ExamplePlugin(BaseToolPlugin):
"""Example tool integration."""
@property
def definition(self) -> ToolDefinition:
"""Return the tool definition."""
return ToolDefinition(
name="example",
description="Example tool for demonstration",
can_fix=False,
tool_type=ToolType.LINTER,
file_patterns=["*.py"],
priority=60,
conflicts_with=[],
native_configs=["pyproject.toml"],
version_command=["example", "--version"],
min_version="1.0.0",
default_options={},
default_timeout=30,
)
def check(self, paths: list[str], options: dict[str, object]) -> ToolResult:
"""Check files with example tool."""
# Implementation here
pass
Code Review
When reviewing code, check for:
- Adherence to this style guide
- Correctness and completeness
- Test coverage
- Documentation
- Performance considerations
- Security considerations
Continuous Integration
All code should pass the following checks before being merged:
- All tests pass
- Code is checked with Lintro
- Test coverage meets minimum threshold
Code Formatting
We use the Lintro tool for code formatting and linting:
Python Code Formatting
-
Use Lintro for checking:
lintro check [PATH] -
Use Lintro for formatting:
lintro format [PATH] -
Format specific files:
lintro format file1.py file2.py -
Format with custom options:
lintro format --tools ruff --core-options "ruff:--line-length=100" [PATH]
Style Guide Project Structure
The current project structure follows a modular design with clear separation of concerns:
py-lintro/
├── lintro/ # Main Python package
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py # Main CLI entry point
│ ├── cli_utils/ # CLI command implementations
│ │ └── commands/
│ │ ├── check.py # Check command
│ │ ├── format.py # Format command
│ │ └── test.py # Test command
│ ├── config/ # Configuration management
│ │ ├── config_loader.py # Loads .lintro-config.yaml
│ │ └── tool_config_generator.py
│ ├── tools/ # Tool implementations
│ │ ├── core/ # Base tool classes
│ │ └── implementations/ # Specific tool implementations
│ │ ├── tool_ruff.py
│ │ ├── tool_black.py
│ │ ├── tool_mypy.py
│ │ └── ...
│ ├── parsers/ # Output parsers for each tool
│ │ ├── ruff/
│ │ ├── black/
│ │ └── ...
│ ├── formatters/ # Output formatters
│ │ ├── styles/ # Output styles (grid, markdown, etc.)
│ │ └── tools/ # Tool-specific formatters
│ └── utils/ # Utility functions
│ ├── tool_executor.py # Main execution logic
│ ├── console_logger.py # Console output handling
│ └── logger_setup.py # Loguru configuration
└── tests/
├── unit/ # Unit tests
├── integration/ # Integration tests
└── ...
Formatter and Output Style Architecture
Overview
Lintro supports flexible, extensible output formatting for all tools. This is achieved by separating:
- Table structure (columns, extraction) per tool
- Output style (plain, markdown, etc.)
This allows:
- Each tool to define its own columns and row extraction logic
- Easy addition of new output styles (Markdown, HTML, JSON, etc.)
- Consistent, DRY, and testable formatting logic
Key Components
1. TableDescriptor (per tool)
- Describes the columns and how to extract them from an issue object.
- Each tool provides its own TableDescriptor if needed.
from lintro.formatters.base_formatter import TableDescriptor
from typing import List
class PydoclintTableDescriptor(TableDescriptor):
def get_columns(self) -> List[str]:
return ["File", "Line", "Code", "Message"]
def get_row(self, issue) -> List[str]:
return [issue.file, str(issue.line), issue.code, issue.message]
2. OutputStyle (per output format)
- Defines how to render a table (columns + rows) as a string.
- Implemented in
lintro/formatters/styles/.
from lintro.formatters.core.output_style import OutputStyle
from typing import List, Any
class MarkdownStyle(OutputStyle):
def format(self, columns: List[str], rows: List[List[Any]]) -> str:
# ...
3. Tool Formatter
- Use the unified formatter for all tools.
- Example usage:
from lintro.formatters.unified import format_issues
# Format issues from any tool using the unified formatter
formatted_output = format_issues(issues, output_format="plain")
How to Add a New Output Style
- Create a new class in
lintro/formatters/styles/inheriting fromOutputStyle. - Implement the
format(columns, rows)method. - Register the new style in the tool’s
STYLE_MAP.
How to Add/Change a Tool’s Table Structure
- Create a new
TableDescriptorfor the tool if needed. - Implement
get_columns()andget_row(issue). - Use this descriptor in the tool’s formatter.
CLI Integration
- The CLI can expose a
--output-styleflag to let users select the output style (e.g.,plain,markdown). - The selected style is passed to the tool formatter, which uses the appropriate OutputStyle.
Example Usage
issues = parse_pydoclint_output(raw_output)
print(format_pydoclint_issues(issues, style="markdown"))
Benefits
- Extensible: Add new styles or tool structures easily
- Consistent: All tools use the same formatting pipeline
- Testable: Each style and descriptor can be unit tested
- Flexible: Tools with different columns or special needs are supported