Skip to content

Commit e1aecb2

Browse files
mldangeloclaude
andcommitted
feat: add comprehensive type checking with mypy strict mode and pyright (#20)
- Upgrade mypy to strict mode with additional error codes (ignore-without-code, redundant-cast, truthy-bool, truthy-iterable, unused-awaitable) - Add pyright as a second type checker for comprehensive coverage - Configure pyright in strict mode with practical exceptions for third-party libraries lacking full stubs - Update CI to run both type checkers in parallel via matrix strategy - Fix CompletedProcess generic type parameter in cli.py - Add type: ignore comments for untyped posthog methods in telemetry.py - Update AGENTS.md documentation with new type checking setup The dual type checker approach catches different categories of issues: - mypy: The standard Python type checker, excellent for protocol validation - pyright: Microsoft's type checker, catches additional edge cases Both run in strict mode to enforce rigorous typing standards. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 123088d commit e1aecb2

File tree

6 files changed

+107
-14
lines changed

6 files changed

+107
-14
lines changed

.github/workflows/test.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ jobs:
4242
run: uv run ruff format --check src/
4343

4444
type-check:
45-
name: Type Check
45+
name: Type Check (${{ matrix.type-checker }})
4646
runs-on: ubuntu-latest
4747
timeout-minutes: 10
48+
strategy:
49+
matrix:
50+
type-checker: [mypy, pyright]
4851
steps:
4952
- uses: actions/checkout@v6
5053

@@ -59,8 +62,13 @@ jobs:
5962
run: uv sync --extra dev
6063

6164
- name: Type check with mypy
65+
if: matrix.type-checker == 'mypy'
6266
run: uv run mypy src/promptfoo/
6367

68+
- name: Type check with pyright
69+
if: matrix.type-checker == 'pyright'
70+
run: uv run pyright src/promptfoo/
71+
6472
test:
6573
name: Test (py${{ matrix.python-version }}, ${{ matrix.os }})
6674
runs-on: ${{ matrix.os }}

AGENTS.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ Runs on every PR and push to main:
134134

135135
- **Lint**: Ruff linting (`uv run ruff check src/`)
136136
- **Format Check**: Ruff formatting (`uv run ruff format --check src/`)
137-
- **Type Check**: mypy static analysis (`uv run mypy src/promptfoo/`)
137+
- **Type Check**: Both mypy and pyright in strict mode (run in parallel via matrix)
138+
- `uv run mypy src/promptfoo/` - Standard Python type checker
139+
- `uv run pyright src/promptfoo/` - Microsoft's type checker for additional coverage
138140
- **Unit Tests**: Fast tests with mocked dependencies (`uv run pytest -m 'not smoke'`)
139141
- **Smoke Tests**: Integration tests against real CLI (`uv run pytest tests/smoke/`)
140142
- **Build**: Package build validation
@@ -180,7 +182,9 @@ We use **OpenID Connect (OIDC)** for secure, credential-free PyPI publishing:
180182

181183
- **Linter**: Ruff with extended rule sets (isort, pycodestyle, flake8-bugbear, etc.)
182184
- **Formatter**: Ruff (replaces Black)
183-
- **Type Checker**: mypy with strict settings
185+
- **Type Checkers**: Both **mypy** and **pyright** in strict mode for comprehensive coverage
186+
- **mypy**: The standard Python type checker with strict mode and additional error codes
187+
- **pyright**: Microsoft's fast type checker that catches different issues than mypy
184188
- **Package Manager**: uv (Astral's fast Python package manager)
185189

186190
### Running Checks Locally
@@ -198,9 +202,15 @@ uv run ruff check src/ --fix
198202
# Format code
199203
uv run ruff format src/
200204

201-
# Type check
205+
# Type check with mypy (strict mode)
202206
uv run mypy src/promptfoo/
203207

208+
# Type check with pyright (strict mode)
209+
uv run pyright src/promptfoo/
210+
211+
# Run both type checkers (recommended before PR)
212+
uv run mypy src/promptfoo/ && uv run pyright src/promptfoo/
213+
204214
# Run tests
205215
uv run pytest
206216
```
@@ -330,6 +340,7 @@ git checkout -b feat/my-feature-name
330340
uv run ruff check src/ --fix
331341
uv run ruff format src/
332342
uv run mypy src/promptfoo/
343+
uv run pyright src/promptfoo/
333344
uv run pytest
334345

335346
# 4. Commit with conventional commit message
@@ -357,6 +368,7 @@ git checkout -b fix/bug-description
357368
uv run ruff check src/ --fix
358369
uv run ruff format src/
359370
uv run mypy src/promptfoo/
371+
uv run pyright src/promptfoo/
360372
uv run pytest
361373

362374
# 4. Commit with conventional commit message

pyproject.toml

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies = [
3737
dev = [
3838
"pytest>=8.4.0",
3939
"mypy>=1.16.0",
40+
"pyright>=1.1.400",
4041
"ruff>=0.12.0",
4142
"types-pyyaml>=6.0.0",
4243
]
@@ -92,16 +93,64 @@ quote-style = "double"
9293

9394
[tool.mypy]
9495
python_version = "3.9"
95-
warn_return_any = true
96-
warn_unused_configs = true
97-
warn_redundant_casts = true
96+
# Enable strict mode for comprehensive type checking
97+
strict = true
98+
# Additional strictness beyond --strict
9899
warn_unreachable = true
99-
no_implicit_optional = true
100-
strict_equality = true
100+
enable_error_code = [
101+
"ignore-without-code",
102+
"redundant-cast",
103+
"truthy-bool",
104+
"truthy-iterable",
105+
"unused-awaitable",
106+
]
107+
# Output formatting
101108
show_error_codes = true
109+
show_column_numbers = true
102110
pretty = true
103-
check_untyped_defs = true
104-
disallow_incomplete_defs = true
111+
# Error handling
112+
warn_unused_ignores = true
113+
114+
[[tool.mypy.overrides]]
115+
module = "posthog.*"
116+
ignore_missing_imports = true
117+
118+
[tool.pyright]
119+
pythonVersion = "3.9"
120+
pythonPlatform = "All"
121+
typeCheckingMode = "strict"
122+
include = ["src/promptfoo"]
123+
exclude = ["tests", "**/__pycache__"]
124+
# Report all errors in strict mode
125+
reportMissingImports = true
126+
reportMissingTypeStubs = false # Disable for posthog which lacks full stubs
127+
reportUnusedImport = true
128+
reportUnusedClass = true
129+
reportUnusedFunction = true
130+
reportUnusedVariable = true
131+
reportDuplicateImport = true
132+
reportPrivateUsage = true
133+
reportConstantRedefinition = true
134+
reportIncompatibleMethodOverride = true
135+
reportIncompatibleVariableOverride = true
136+
reportInconsistentConstructor = true
137+
reportOverlappingOverload = true
138+
reportUninitializedInstanceVariable = true
139+
reportCallInDefaultInitializer = true
140+
reportUnnecessaryIsInstance = true
141+
reportUnnecessaryCast = true
142+
reportUnnecessaryComparison = true
143+
reportUnnecessaryContains = true
144+
reportImplicitStringConcatenation = false
145+
reportUnusedCallResult = false
146+
reportUnusedExpression = true
147+
reportUnnecessaryTypeIgnoreComment = false # Allow type: ignore for mypy compatibility
148+
reportMatchNotExhaustive = true
149+
# Relax some strict checks for third-party libraries without full stubs
150+
reportUnknownMemberType = false
151+
reportUnknownArgumentType = false
152+
reportUnknownVariableType = false
153+
reportUnknownParameterType = false
105154

106155
[tool.pytest.ini_options]
107156
testpaths = ["tests"]

src/promptfoo/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def _requires_shell(executable: str) -> bool:
165165
return ext.lower() in _WINDOWS_SHELL_EXTENSIONS
166166

167167

168-
def _run_command(cmd: list[str], env: Optional[dict[str, str]] = None) -> subprocess.CompletedProcess:
168+
def _run_command(cmd: list[str], env: Optional[dict[str, str]] = None) -> subprocess.CompletedProcess[bytes]:
169169
"""Execute a command, handling shell requirements on Windows."""
170170
if _requires_shell(cmd[0]):
171171
return subprocess.run(subprocess.list2cmdline(cmd), shell=True, env=env)

src/promptfoo/telemetry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ def shutdown(self) -> None:
172172
"""Shutdown the telemetry client and flush any pending events."""
173173
if self._client:
174174
try:
175-
self._client.flush()
176-
self._client.shutdown()
175+
self._client.flush() # type: ignore[no-untyped-call]
176+
self._client.shutdown() # type: ignore[no-untyped-call]
177177
except Exception:
178178
pass # Silently fail
179179
finally:

uv.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)