or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ast-nodes.mdbuild-system.mdcommand-line-tools.mddaemon-mode.mderror-system.mdindex.mdmypyc-compiler.mdplugin-system.mdprogrammatic-api.mdstub-tools.mdtype-system.md

stub-tools.mddocs/

0

# Stub Tools

1

2

Tools for generating and validating Python stub files (.pyi) for type checking without runtime dependencies. These tools enable type checking of untyped libraries and creating type-only interfaces.

3

4

## Capabilities

5

6

### Stub Generation (stubgen)

7

8

Automatically generate Python stub files from source code or runtime inspection.

9

10

```python { .api }

11

def main(args: list[str] | None = None) -> None:

12

"""

13

Main entry point for stub generation.

14

15

Parameters:

16

- args: Command line arguments (uses sys.argv if None)

17

18

Usage:

19

stubgen.main(['mymodule']) # Generate stubs for mymodule

20

"""

21

```

22

23

#### Generation Modes

24

25

**Source-based Generation**: Analyze Python source code to create stubs

26

```bash

27

stubgen mymodule.py # Generate from source file

28

stubgen -p mypackage # Generate from package source

29

```

30

31

**Runtime-based Generation**: Inspect imported modules at runtime

32

```bash

33

stubgen -m requests # Generate from installed module

34

stubgen -m numpy pandas # Generate multiple modules

35

```

36

37

#### Key Features

38

39

- **Preserve public API**: Only exports public names and interfaces

40

- **Type annotation extraction**: Extracts existing type hints from source

41

- **Docstring handling**: Can include or exclude docstrings

42

- **Private member control**: Options for including private definitions

43

- **Cross-references**: Maintains import relationships between modules

44

45

### Stub Testing (stubtest)

46

47

Validate stub files against runtime modules to ensure accuracy and completeness.

48

49

```python { .api }

50

def main(args: list[str] | None = None) -> None:

51

"""

52

Main entry point for stub validation.

53

54

Parameters:

55

- args: Command line arguments (uses sys.argv if None)

56

57

Usage:

58

stubtest.main(['mymodule']) # Test stubs for mymodule

59

"""

60

```

61

62

#### Validation Types

63

64

**Signature Validation**: Check function and method signatures match

65

```bash

66

stubtest mymodule # Basic signature checking

67

stubtest --check-typeddict mymodule # Include TypedDict validation

68

```

69

70

**Completeness Checking**: Ensure all public APIs are stubbed

71

```bash

72

stubtest --ignore-missing-stub mymodule # Allow missing stubs

73

```

74

75

**Runtime Compatibility**: Verify stubs work with actual runtime behavior

76

```bash

77

stubtest --allowlist allowlist.txt mymodule # Use allowlist for known issues

78

```

79

80

## Advanced Usage

81

82

### Programmatic Stub Generation

83

84

```python

85

import subprocess

86

import tempfile

87

import os

88

from pathlib import Path

89

90

class StubGenerator:

91

"""Programmatic interface for stub generation."""

92

93

def __init__(self, output_dir: str = "stubs"):

94

self.output_dir = Path(output_dir)

95

self.output_dir.mkdir(exist_ok=True)

96

97

def generate_from_source(self, source_paths: list[str]) -> bool:

98

"""Generate stubs from source files."""

99

cmd = [

100

'stubgen',

101

'-o', str(self.output_dir),

102

'--include-private',

103

'--no-import'

104

] + source_paths

105

106

result = subprocess.run(cmd, capture_output=True, text=True)

107

return result.returncode == 0

108

109

def generate_from_modules(self, module_names: list[str]) -> bool:

110

"""Generate stubs from installed modules."""

111

cmd = [

112

'stubgen',

113

'-o', str(self.output_dir),

114

'-m'

115

] + module_names

116

117

result = subprocess.run(cmd, capture_output=True, text=True)

118

return result.returncode == 0

119

120

def generate_package_stubs(self, package_name: str) -> bool:

121

"""Generate stubs for entire package."""

122

cmd = [

123

'stubgen',

124

'-o', str(self.output_dir),

125

'-p', package_name

126

]

127

128

result = subprocess.run(cmd, capture_output=True, text=True)

129

return result.returncode == 0

130

131

# Usage

132

generator = StubGenerator("my_stubs")

133

generator.generate_from_modules(['requests', 'numpy'])

134

generator.generate_package_stubs('mypackage')

135

```

136

137

### Programmatic Stub Testing

138

139

```python

140

import subprocess

141

import json

142

from pathlib import Path

143

144

class StubTester:

145

"""Programmatic interface for stub testing."""

146

147

def __init__(self, allowlist_file: str | None = None):

148

self.allowlist_file = allowlist_file

149

150

def test_stubs(self, module_names: list[str]) -> dict:

151

"""Test stubs and return results."""

152

cmd = ['stubtest'] + module_names

153

154

if self.allowlist_file:

155

cmd.extend(['--allowlist', self.allowlist_file])

156

157

result = subprocess.run(cmd, capture_output=True, text=True)

158

159

return {

160

'success': result.returncode == 0,

161

'stdout': result.stdout,

162

'stderr': result.stderr,

163

'errors': self.parse_errors(result.stderr)

164

}

165

166

def parse_errors(self, stderr: str) -> list[dict]:

167

"""Parse stubtest error output."""

168

errors = []

169

for line in stderr.strip().split('\n'):

170

if 'error:' in line:

171

# Parse error format: module.function: error: message

172

parts = line.split(':', 2)

173

if len(parts) >= 3:

174

errors.append({

175

'location': parts[0].strip(),

176

'message': parts[2].strip()

177

})

178

return errors

179

180

def generate_allowlist(self, module_names: list[str]) -> str:

181

"""Generate allowlist for current differences."""

182

cmd = ['stubtest', '--generate-allowlist'] + module_names

183

result = subprocess.run(cmd, capture_output=True, text=True)

184

return result.stdout

185

186

# Usage

187

tester = StubTester('allowlist.txt')

188

results = tester.test_stubs(['mymodule'])

189

190

if not results['success']:

191

print(f"Found {len(results['errors'])} stub errors")

192

for error in results['errors']:

193

print(f" {error['location']}: {error['message']}")

194

```

195

196

### Integration with Build Systems

197

198

```python

199

# setup.py integration

200

from setuptools import setup

201

from setuptools.command.build import build

202

import subprocess

203

import os

204

205

class BuildWithStubs(build):

206

"""Custom build command that generates stubs."""

207

208

def run(self):

209

# Run normal build

210

super().run()

211

212

# Generate stubs for the package

213

if self.should_generate_stubs():

214

self.generate_stubs()

215

216

def should_generate_stubs(self) -> bool:

217

"""Check if stubs should be generated."""

218

return os.environ.get('GENERATE_STUBS', '').lower() == 'true'

219

220

def generate_stubs(self):

221

"""Generate stubs for the package."""

222

package_name = 'mypackage' # Replace with actual package name

223

224

print("Generating stub files...")

225

result = subprocess.run([

226

'stubgen',

227

'-o', 'stubs',

228

'-p', package_name

229

], capture_output=True, text=True)

230

231

if result.returncode != 0:

232

print(f"Stub generation failed: {result.stderr}")

233

else:

234

print("Stub files generated successfully")

235

236

setup(

237

name="mypackage",

238

cmdclass={'build': BuildWithStubs},

239

# ... other setup parameters

240

)

241

```

242

243

### CI/CD Integration

244

245

```yaml

246

# GitHub Actions workflow for stub validation

247

name: Validate Stubs

248

249

on: [push, pull_request]

250

251

jobs:

252

test-stubs:

253

runs-on: ubuntu-latest

254

255

steps:

256

- uses: actions/checkout@v2

257

258

- name: Set up Python

259

uses: actions/setup-python@v2

260

with:

261

python-version: 3.11

262

263

- name: Install dependencies

264

run: |

265

pip install mypy

266

pip install -e . # Install the package

267

268

- name: Generate stubs

269

run: |

270

stubgen -o stubs -p mypackage

271

272

- name: Test stubs

273

run: |

274

stubtest --allowlist stubtest.allowlist mypackage

275

276

- name: Upload stub artifacts

277

uses: actions/upload-artifact@v2

278

with:

279

name: stubs

280

path: stubs/

281

```

282

283

## Stub File Examples

284

285

### Basic Stub Structure

286

287

```python

288

# mymodule.pyi - Basic stub file

289

from typing import Any, Optional

290

291

def process_data(data: list[Any], options: Optional[dict[str, Any]] = ...) -> dict[str, Any]: ...

292

293

class DataProcessor:

294

def __init__(self, config: dict[str, Any]) -> None: ...

295

def process(self, data: Any) -> Any: ...

296

@property

297

def status(self) -> str: ...

298

299

ERROR_CODES: dict[str, int]

300

DEFAULT_CONFIG: dict[str, Any]

301

```

302

303

### Advanced Stub Features

304

305

```python

306

# advanced.pyi - Advanced stub features

307

from typing import Generic, TypeVar, Protocol, overload

308

from typing_extensions import ParamSpec, TypeVarTuple

309

310

T = TypeVar('T')

311

P = ParamSpec('P')

312

Ts = TypeVarTuple('Ts')

313

314

class Container(Generic[T]):

315

def __init__(self, item: T) -> None: ...

316

def get(self) -> T: ...

317

def set(self, item: T) -> None: ...

318

319

class Callable(Protocol[P, T]):

320

def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ...

321

322

@overload

323

def convert(value: int) -> str: ...

324

@overload

325

def convert(value: str) -> int: ...

326

def convert(value: int | str) -> str | int: ...

327

328

# Variadic generic function

329

def process_multiple(*args: *Ts) -> tuple[*Ts]: ...

330

```

331

332

## Best Practices

333

334

### Stub Maintenance Workflow

335

336

```python

337

class StubMaintenanceWorkflow:

338

"""Automated workflow for stub maintenance."""

339

340

def __init__(self, package_name: str):

341

self.package_name = package_name

342

self.stubs_dir = Path("stubs")

343

344

def update_stubs(self) -> bool:

345

"""Update stubs and validate them."""

346

# 1. Regenerate stubs

347

if not self.regenerate_stubs():

348

return False

349

350

# 2. Test against runtime

351

test_results = self.test_stubs()

352

if not test_results['success']:

353

# 3. Update allowlist if needed

354

self.update_allowlist(test_results['errors'])

355

356

# 4. Final validation

357

return self.validate_stubs()

358

359

def regenerate_stubs(self) -> bool:

360

"""Regenerate stub files."""

361

cmd = [

362

'stubgen',

363

'-o', str(self.stubs_dir),

364

'-p', self.package_name,

365

'--include-private'

366

]

367

368

result = subprocess.run(cmd, capture_output=True)

369

return result.returncode == 0

370

371

def test_stubs(self) -> dict:

372

"""Test stub accuracy."""

373

tester = StubTester()

374

return tester.test_stubs([self.package_name])

375

376

def update_allowlist(self, errors: list[dict]):

377

"""Update allowlist based on test errors."""

378

allowlist_entries = []

379

380

for error in errors:

381

if self.should_allow_error(error):

382

allowlist_entries.append(error['location'])

383

384

# Write updated allowlist

385

with open('stubtest.allowlist', 'w') as f:

386

f.write('\n'.join(allowlist_entries))

387

388

def should_allow_error(self, error: dict) -> bool:

389

"""Determine if error should be allowlisted."""

390

message = error['message'].lower()

391

392

# Common patterns to allowlist

393

allowlist_patterns = [

394

'runtime argument', # Runtime-only arguments

395

'is not present at runtime', # Stub-only definitions

396

'incompatible default', # Different default values

397

]

398

399

return any(pattern in message for pattern in allowlist_patterns)

400

401

def validate_stubs(self) -> bool:

402

"""Final validation of stub files."""

403

# Run mypy on stub files themselves

404

cmd = ['mypy', str(self.stubs_dir)]

405

result = subprocess.run(cmd, capture_output=True)

406

return result.returncode == 0

407

408

# Usage

409

workflow = StubMaintenanceWorkflow('mypackage')

410

success = workflow.update_stubs()

411

```

412

413

### Quality Assurance

414

415

```python

416

def validate_stub_quality(stub_file: Path) -> dict:

417

"""Validate stub file quality and completeness."""

418

with open(stub_file) as f:

419

content = f.read()

420

421

issues = []

422

423

# Check for common issues

424

if 'Any' in content:

425

any_count = content.count('Any')

426

if any_count > 10: # Threshold for too many Any types

427

issues.append(f"Excessive use of Any ({any_count} occurrences)")

428

429

if '...' not in content:

430

issues.append("Missing ellipsis in function bodies")

431

432

# Check for proper imports

433

required_imports = []

434

if 'Optional[' in content and 'from typing import' not in content:

435

required_imports.append('Optional')

436

437

if required_imports:

438

issues.append(f"Missing imports: {', '.join(required_imports)}")

439

440

return {

441

'file': str(stub_file),

442

'issues': issues,

443

'quality_score': max(0, 100 - len(issues) * 10)

444

}

445

446

# Usage

447

quality_report = validate_stub_quality(Path('stubs/mymodule.pyi'))

448

print(f"Quality score: {quality_report['quality_score']}/100")

449

```