Code audit tool for python
—
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.
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.)
"""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 []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."""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."""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])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"
)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"
)For external plugins, use setuptools entry points:
# setup.py
setup(
name='pylama-custom',
entry_points={
'pylama.linter': [
'custom = my_plugin:CustomLinter',
],
},
)Custom linters automatically integrate with pylama's configuration system:
# pylama.ini
[pylama]
linters = pycodestyle,custom
[pylama:custom]
severity = warning
max_issues = 10import 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)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"
)Pylama automatically discovers and loads plugins through:
pylama/lint/ directorypylama.linterpkgutil.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:
passInstall with Tessl CLI
npx tessl i tessl/pypi-pylama