or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-processing.mdconfiguration.mderror-processing.mdindex.mdmain-interface.mdplugin-development.mdpytest-integration.mdvcs-hooks.md

plugin-development.mddocs/

0

# Plugin Development Framework

1

2

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.

3

4

## Capabilities

5

6

### Modern Linter Base Class

7

8

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

9

10

```python { .api }

11

class LinterV2(Linter):

12

"""

13

Modern linter base class with context-aware checking.

14

15

Attributes:

16

name: Optional[str] - Unique identifier for the linter

17

"""

18

19

name: Optional[str] = None

20

21

def run_check(self, ctx: RunContext):

22

"""

23

Check code using RunContext for error reporting.

24

25

Args:

26

ctx: RunContext containing file information and error collection

27

28

This method should:

29

1. Get linter-specific parameters from ctx.get_params(self.name)

30

2. Analyze the code in ctx.source or ctx.temp_filename

31

3. Report errors using ctx.push(source=self.name, **error_info)

32

33

Error info should include:

34

- lnum: int - Line number (1-based)

35

- col: int - Column number (1-based, default 0)

36

- text: str - Error message (stored as message attribute)

37

- etype: str - Error type ('E', 'W', 'F', etc.)

38

"""

39

```

40

41

### Legacy Linter Interface

42

43

Base class maintained for backward compatibility with older plugins.

44

45

```python { .api }

46

class Linter(metaclass=LinterMeta):

47

"""

48

Legacy linter base class for backward compatibility.

49

50

Attributes:

51

name: Optional[str] - Unique identifier for the linter

52

"""

53

54

name: Optional[str] = None

55

56

@classmethod

57

def add_args(cls, parser: ArgumentParser):

58

"""

59

Add linter-specific command line arguments.

60

61

Args:

62

parser: ArgumentParser to add options to

63

64

This method allows linters to register their own command line options

65

that will be available in the configuration system.

66

"""

67

68

def run(self, path: str, **meta) -> List[Dict[str, Any]]:

69

"""

70

Legacy linter run method.

71

72

Args:

73

path: File path to check

74

**meta: Additional metadata including 'code' and 'params'

75

76

Returns:

77

List[Dict]: List of error dictionaries with keys:

78

- lnum: int - Line number

79

- col: int - Column number (optional)

80

- text: str - Error message

81

- etype: str - Error type

82

"""

83

return []

84

```

85

86

### Execution Context Management

87

88

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

89

90

```python { .api }

91

class RunContext:

92

"""

93

Execution context for linter operations with resource management.

94

95

Attributes:

96

errors: List[Error] - Collected errors

97

options: Optional[Namespace] - Configuration options

98

skip: bool - Whether to skip checking this file

99

ignore: Set[str] - Error codes to ignore

100

select: Set[str] - Error codes to select

101

linters: List[str] - Active linters for this file

102

filename: str - Original filename

103

"""

104

105

def __init__(

106

self,

107

filename: str,

108

source: str = None,

109

options: Namespace = None

110

):

111

"""

112

Initialize context for file checking.

113

114

Args:

115

filename: Path to file being checked

116

source: Source code string (if None, reads from file)

117

options: Configuration options

118

"""

119

120

def get_params(self, lname: str) -> Dict[str, Any]:

121

"""

122

Get linter-specific configuration parameters.

123

124

Args:

125

lname: Linter name

126

127

Returns:

128

Dict: Configuration parameters for the linter

129

130

Merges global options with linter-specific settings from

131

configuration sections like [pylama:lintername].

132

"""

133

134

def push(self, source: str, **err_info):

135

"""

136

Add error to the context.

137

138

Args:

139

source: Name of linter reporting the error

140

**err_info: Error information including:

141

- lnum: int - Line number

142

- col: int - Column number (default 0)

143

- text: str - Error message

144

- etype: str - Error type

145

146

Applies filtering based on ignore/select rules and file-specific

147

configuration before adding to errors list.

148

"""

149

150

def __enter__(self) -> 'RunContext':

151

"""Context manager entry."""

152

153

def __exit__(self, exc_type, exc_val, exc_tb):

154

"""Context manager exit with cleanup."""

155

```

156

157

### Plugin Registration

158

159

Automatic plugin registration system using metaclasses.

160

161

```python { .api }

162

class LinterMeta(type):

163

"""

164

Metaclass for automatic linter registration.

165

166

Automatically registers linters in the global LINTERS dictionary

167

when classes are defined with this metaclass.

168

"""

169

170

def __new__(mcs, name, bases, params):

171

"""

172

Register linter class if it has a name attribute.

173

174

Args:

175

name: Class name

176

bases: Base classes

177

params: Class attributes and methods

178

179

Returns:

180

type: Created linter class

181

"""

182

183

LINTERS: Dict[str, Type[LinterV2]] = {}

184

"""Global registry of available linters."""

185

```

186

187

## Built-in Linter Examples

188

189

### Pycodestyle Integration

190

191

```python

192

from pycodestyle import StyleGuide

193

from pylama.lint import LinterV2

194

from pylama.context import RunContext

195

196

class Linter(LinterV2):

197

"""Pycodestyle (PEP8) style checker integration."""

198

199

name = "pycodestyle"

200

201

def run_check(self, ctx: RunContext):

202

# Get linter-specific parameters

203

params = ctx.get_params("pycodestyle")

204

205

# Set max line length from global options

206

if ctx.options:

207

params.setdefault("max_line_length", ctx.options.max_line_length)

208

209

# Create style guide with parameters

210

style = StyleGuide(reporter=CustomReporter, **params)

211

212

# Check the file

213

style.check_files([ctx.temp_filename])

214

```

215

216

### Custom Linter Example

217

218

```python

219

import ast

220

from pylama.lint import LinterV2

221

from pylama.context import RunContext

222

223

class CustomLinter(LinterV2):

224

"""Example custom linter that checks for print statements."""

225

226

name = "no_print"

227

228

def run_check(self, ctx: RunContext):

229

try:

230

# Parse the source code

231

tree = ast.parse(ctx.source, ctx.filename)

232

233

# Walk the AST looking for print calls

234

for node in ast.walk(tree):

235

if (isinstance(node, ast.Call) and

236

isinstance(node.func, ast.Name) and

237

node.func.id == 'print'):

238

239

# Report error

240

ctx.push(

241

source=self.name,

242

lnum=node.lineno,

243

col=node.col_offset,

244

text="NP001 print statement found",

245

etype="W"

246

)

247

248

except SyntaxError as e:

249

# Report syntax error

250

ctx.push(

251

source=self.name,

252

lnum=e.lineno or 1,

253

col=e.offset or 0,

254

text=f"SyntaxError: {e.msg}",

255

etype="E"

256

)

257

```

258

259

## Usage Examples

260

261

### Creating a Custom Linter

262

263

```python

264

import re

265

from pylama.lint import LinterV2

266

from pylama.context import RunContext

267

268

class TodoLinter(LinterV2):

269

"""Linter that finds TODO comments."""

270

271

name = "todo"

272

273

@classmethod

274

def add_args(cls, parser):

275

parser.add_argument(

276

'--todo-keywords',

277

default='TODO,FIXME,XXX',

278

help='Comma-separated list of TODO keywords'

279

)

280

281

def run_check(self, ctx: RunContext):

282

params = ctx.get_params(self.name)

283

keywords = params.get('keywords', 'TODO,FIXME,XXX').split(',')

284

285

pattern = r'\b(' + '|'.join(keywords) + r')\b'

286

287

for i, line in enumerate(ctx.source.splitlines(), 1):

288

if re.search(pattern, line, re.IGNORECASE):

289

ctx.push(

290

source=self.name,

291

lnum=i,

292

col=0,

293

text=f"T001 TODO comment found: {line.strip()}",

294

etype="W"

295

)

296

```

297

298

### Plugin Entry Point

299

300

For external plugins, use setuptools entry points:

301

302

```python

303

# setup.py

304

setup(

305

name='pylama-custom',

306

entry_points={

307

'pylama.linter': [

308

'custom = my_plugin:CustomLinter',

309

],

310

},

311

)

312

```

313

314

### Configuration Integration

315

316

Custom linters automatically integrate with pylama's configuration system:

317

318

```ini

319

# pylama.ini

320

[pylama]

321

linters = pycodestyle,custom

322

323

[pylama:custom]

324

severity = warning

325

max_issues = 10

326

```

327

328

### Testing Custom Linters

329

330

```python

331

import unittest

332

from pylama.context import RunContext

333

from pylama.config import Namespace

334

from my_linter import CustomLinter

335

336

class TestCustomLinter(unittest.TestCase):

337

338

def test_custom_linter(self):

339

# Create test context

340

code = '''

341

def test_function():

342

print("This should trigger our linter")

343

return True

344

'''

345

346

options = Namespace()

347

ctx = RunContext('test.py', source=code, options=options)

348

349

# Run linter

350

linter = CustomLinter()

351

with ctx:

352

linter.run_check(ctx)

353

354

# Check results

355

self.assertEqual(len(ctx.errors), 1)

356

self.assertEqual(ctx.errors[0].source, 'custom')

357

self.assertIn('print statement', ctx.errors[0].text)

358

```

359

360

### Advanced Context Usage

361

362

```python

363

class AdvancedLinter(LinterV2):

364

name = "advanced"

365

366

def run_check(self, ctx: RunContext):

367

# Check if we should skip this file

368

if ctx.skip:

369

return

370

371

# Get configuration

372

params = ctx.get_params(self.name)

373

max_line_length = params.get('max_line_length', 79)

374

375

# Access file information

376

print(f"Checking {ctx.filename}")

377

print(f"Source length: {len(ctx.source)} characters")

378

379

# Check line lengths

380

for i, line in enumerate(ctx.source.splitlines(), 1):

381

if len(line) > max_line_length:

382

# Check if this error should be ignored

383

error_code = "E501"

384

if error_code not in ctx.ignore:

385

ctx.push(

386

source=self.name,

387

lnum=i,

388

col=max_line_length,

389

text=f"{error_code} line too long ({len(line)} > {max_line_length})",

390

etype="E"

391

)

392

```

393

394

## Plugin Discovery

395

396

Pylama automatically discovers and loads plugins through:

397

398

1. **Built-in plugins**: Located in `pylama/lint/` directory

399

2. **Entry point plugins**: Registered via setuptools entry points under `pylama.linter`

400

3. **Import-based discovery**: Uses `pkgutil.walk_packages()` to find linter modules

401

402

```python

403

# Built-in discovery in pylama/lint/__init__.py

404

from pkgutil import walk_packages

405

from importlib import import_module

406

407

# Import all modules in the lint package

408

for _, pname, _ in walk_packages([str(Path(__file__).parent)]):

409

try:

410

import_module(f"{__name__}.{pname}")

411

except ImportError:

412

pass

413

414

# Import entry point plugins

415

from pkg_resources import iter_entry_points

416

for entry in iter_entry_points("pylama.linter"):

417

if entry.name not in LINTERS:

418

try:

419

LINTERS[entry.name] = entry.load()

420

except ImportError:

421

pass

422

```