CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pylama

Code audit tool for python

Pending
Overview
Eval results
Files

plugin-development.mddocs/

Plugin Development Framework

Framework for creating custom linters and extending pylama functionality. Pylama provides a standardized plugin system that enables integration of any code analysis tool through a consistent interface.

Capabilities

Modern Linter Base Class

Base class for creating new-style linter plugins with context-aware checking.

class LinterV2(Linter):
    """
    Modern linter base class with context-aware checking.
    
    Attributes:
        name: Optional[str] - Unique identifier for the linter
    """
    
    name: Optional[str] = None
    
    def run_check(self, ctx: RunContext):
        """
        Check code using RunContext for error reporting.
        
        Args:
            ctx: RunContext containing file information and error collection
            
        This method should:
        1. Get linter-specific parameters from ctx.get_params(self.name)
        2. Analyze the code in ctx.source or ctx.temp_filename
        3. Report errors using ctx.push(source=self.name, **error_info)
        
        Error info should include:
        - lnum: int - Line number (1-based)
        - col: int - Column number (1-based, default 0)
        - text: str - Error message (stored as message attribute)
        - etype: str - Error type ('E', 'W', 'F', etc.)
        """

Legacy Linter Interface

Base class maintained for backward compatibility with older plugins.

class Linter(metaclass=LinterMeta):
    """
    Legacy linter base class for backward compatibility.
    
    Attributes:
        name: Optional[str] - Unique identifier for the linter
    """
    
    name: Optional[str] = None
    
    @classmethod
    def add_args(cls, parser: ArgumentParser):
        """
        Add linter-specific command line arguments.
        
        Args:
            parser: ArgumentParser to add options to
            
        This method allows linters to register their own command line options
        that will be available in the configuration system.
        """
    
    def run(self, path: str, **meta) -> List[Dict[str, Any]]:
        """
        Legacy linter run method.
        
        Args:
            path: File path to check
            **meta: Additional metadata including 'code' and 'params'
            
        Returns:
            List[Dict]: List of error dictionaries with keys:
                - lnum: int - Line number
                - col: int - Column number (optional)
                - text: str - Error message
                - etype: str - Error type
        """
        return []

Execution Context Management

Context manager for linter execution with resource management and error collection.

class RunContext:
    """
    Execution context for linter operations with resource management.
    
    Attributes:
        errors: List[Error] - Collected errors
        options: Optional[Namespace] - Configuration options
        skip: bool - Whether to skip checking this file
        ignore: Set[str] - Error codes to ignore
        select: Set[str] - Error codes to select
        linters: List[str] - Active linters for this file
        filename: str - Original filename
    """
    
    def __init__(
        self,
        filename: str,
        source: str = None,
        options: Namespace = None
    ):
        """
        Initialize context for file checking.
        
        Args:
            filename: Path to file being checked
            source: Source code string (if None, reads from file)
            options: Configuration options
        """
    
    def get_params(self, lname: str) -> Dict[str, Any]:
        """
        Get linter-specific configuration parameters.
        
        Args:
            lname: Linter name
            
        Returns:
            Dict: Configuration parameters for the linter
            
        Merges global options with linter-specific settings from
        configuration sections like [pylama:lintername].
        """
    
    def push(self, source: str, **err_info):
        """
        Add error to the context.
        
        Args:
            source: Name of linter reporting the error
            **err_info: Error information including:
                - lnum: int - Line number
                - col: int - Column number (default 0)  
                - text: str - Error message
                - etype: str - Error type
        
        Applies filtering based on ignore/select rules and file-specific
        configuration before adding to errors list.
        """
    
    def __enter__(self) -> 'RunContext':
        """Context manager entry."""
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit with cleanup."""

Plugin Registration

Automatic plugin registration system using metaclasses.

class LinterMeta(type):
    """
    Metaclass for automatic linter registration.
    
    Automatically registers linters in the global LINTERS dictionary
    when classes are defined with this metaclass.
    """
    
    def __new__(mcs, name, bases, params):
        """
        Register linter class if it has a name attribute.
        
        Args:
            name: Class name
            bases: Base classes
            params: Class attributes and methods
            
        Returns:
            type: Created linter class
        """

LINTERS: Dict[str, Type[LinterV2]] = {}
"""Global registry of available linters."""

Built-in Linter Examples

Pycodestyle Integration

from pycodestyle import StyleGuide
from pylama.lint import LinterV2
from pylama.context import RunContext

class Linter(LinterV2):
    """Pycodestyle (PEP8) style checker integration."""
    
    name = "pycodestyle"
    
    def run_check(self, ctx: RunContext):
        # Get linter-specific parameters
        params = ctx.get_params("pycodestyle")
        
        # Set max line length from global options
        if ctx.options:
            params.setdefault("max_line_length", ctx.options.max_line_length)
        
        # Create style guide with parameters
        style = StyleGuide(reporter=CustomReporter, **params)
        
        # Check the file
        style.check_files([ctx.temp_filename])

Custom Linter Example

import ast
from pylama.lint import LinterV2
from pylama.context import RunContext

class CustomLinter(LinterV2):
    """Example custom linter that checks for print statements."""
    
    name = "no_print"
    
    def run_check(self, ctx: RunContext):
        try:
            # Parse the source code
            tree = ast.parse(ctx.source, ctx.filename)
            
            # Walk the AST looking for print calls
            for node in ast.walk(tree):
                if (isinstance(node, ast.Call) and 
                    isinstance(node.func, ast.Name) and 
                    node.func.id == 'print'):
                    
                    # Report error
                    ctx.push(
                        source=self.name,
                        lnum=node.lineno,
                        col=node.col_offset,
                        text="NP001 print statement found",
                        etype="W"
                    )
                    
        except SyntaxError as e:
            # Report syntax error
            ctx.push(
                source=self.name,
                lnum=e.lineno or 1,
                col=e.offset or 0,
                text=f"SyntaxError: {e.msg}",
                etype="E"
            )

Usage Examples

Creating a Custom Linter

import re
from pylama.lint import LinterV2
from pylama.context import RunContext

class TodoLinter(LinterV2):
    """Linter that finds TODO comments."""
    
    name = "todo"
    
    @classmethod
    def add_args(cls, parser):
        parser.add_argument(
            '--todo-keywords',
            default='TODO,FIXME,XXX',
            help='Comma-separated list of TODO keywords'
        )
    
    def run_check(self, ctx: RunContext):
        params = ctx.get_params(self.name)
        keywords = params.get('keywords', 'TODO,FIXME,XXX').split(',')
        
        pattern = r'\b(' + '|'.join(keywords) + r')\b'
        
        for i, line in enumerate(ctx.source.splitlines(), 1):
            if re.search(pattern, line, re.IGNORECASE):
                ctx.push(
                    source=self.name,
                    lnum=i,
                    col=0,
                    text=f"T001 TODO comment found: {line.strip()}",
                    etype="W"
                )

Plugin Entry Point

For external plugins, use setuptools entry points:

# setup.py
setup(
    name='pylama-custom',
    entry_points={
        'pylama.linter': [
            'custom = my_plugin:CustomLinter',
        ],
    },
)

Configuration Integration

Custom linters automatically integrate with pylama's configuration system:

# pylama.ini
[pylama]
linters = pycodestyle,custom

[pylama:custom]
severity = warning
max_issues = 10

Testing Custom Linters

import unittest
from pylama.context import RunContext
from pylama.config import Namespace
from my_linter import CustomLinter

class TestCustomLinter(unittest.TestCase):
    
    def test_custom_linter(self):
        # Create test context
        code = '''
def test_function():
    print("This should trigger our linter")
    return True
'''
        
        options = Namespace()
        ctx = RunContext('test.py', source=code, options=options)
        
        # Run linter
        linter = CustomLinter()
        with ctx:
            linter.run_check(ctx)
        
        # Check results
        self.assertEqual(len(ctx.errors), 1)
        self.assertEqual(ctx.errors[0].source, 'custom')
        self.assertIn('print statement', ctx.errors[0].text)

Advanced Context Usage

class AdvancedLinter(LinterV2):
    name = "advanced"
    
    def run_check(self, ctx: RunContext):
        # Check if we should skip this file
        if ctx.skip:
            return
            
        # Get configuration
        params = ctx.get_params(self.name)
        max_line_length = params.get('max_line_length', 79)
        
        # Access file information
        print(f"Checking {ctx.filename}")
        print(f"Source length: {len(ctx.source)} characters")
        
        # Check line lengths
        for i, line in enumerate(ctx.source.splitlines(), 1):
            if len(line) > max_line_length:
                # Check if this error should be ignored
                error_code = "E501"
                if error_code not in ctx.ignore:
                    ctx.push(
                        source=self.name,
                        lnum=i,
                        col=max_line_length,
                        text=f"{error_code} line too long ({len(line)} > {max_line_length})",
                        etype="E"
                    )

Plugin Discovery

Pylama automatically discovers and loads plugins through:

  1. Built-in plugins: Located in pylama/lint/ directory
  2. Entry point plugins: Registered via setuptools entry points under pylama.linter
  3. Import-based discovery: Uses pkgutil.walk_packages() to find linter modules
# Built-in discovery in pylama/lint/__init__.py
from pkgutil import walk_packages
from importlib import import_module

# Import all modules in the lint package
for _, pname, _ in walk_packages([str(Path(__file__).parent)]):
    try:
        import_module(f"{__name__}.{pname}")
    except ImportError:
        pass

# Import entry point plugins
from pkg_resources import iter_entry_points
for entry in iter_entry_points("pylama.linter"):
    if entry.name not in LINTERS:
        try:
            LINTERS[entry.name] = entry.load()
        except ImportError:
            pass

Install with Tessl CLI

npx tessl i tessl/pypi-pylama

docs

async-processing.md

configuration.md

error-processing.md

index.md

main-interface.md

plugin-development.md

pytest-integration.md

vcs-hooks.md

tile.json