Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ jobs:
run: uv run ruff format --check src/

type-check:
name: Type Check
name: Type Check (${{ matrix.type-checker }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
type-checker: [mypy, pyright]
steps:
- uses: actions/checkout@v6

Expand All @@ -59,8 +62,13 @@ jobs:
run: uv sync --extra dev

- name: Type check with mypy
if: matrix.type-checker == 'mypy'
run: uv run mypy src/promptfoo/

- name: Type check with pyright
if: matrix.type-checker == 'pyright'
run: uv run pyright src/promptfoo/

test:
name: Test (py${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
18 changes: 15 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ Runs on every PR and push to main:

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

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

### Running Checks Locally
Expand All @@ -198,9 +202,15 @@ uv run ruff check src/ --fix
# Format code
uv run ruff format src/

# Type check
# Type check with mypy (strict mode)
uv run mypy src/promptfoo/

# Type check with pyright (strict mode)
uv run pyright src/promptfoo/

# Run both type checkers (recommended before PR)
uv run mypy src/promptfoo/ && uv run pyright src/promptfoo/

# Run tests
uv run pytest
```
Expand Down Expand Up @@ -330,6 +340,7 @@ git checkout -b feat/my-feature-name
uv run ruff check src/ --fix
uv run ruff format src/
uv run mypy src/promptfoo/
uv run pyright src/promptfoo/
uv run pytest

# 4. Commit with conventional commit message
Expand Down Expand Up @@ -357,6 +368,7 @@ git checkout -b fix/bug-description
uv run ruff check src/ --fix
uv run ruff format src/
uv run mypy src/promptfoo/
uv run pyright src/promptfoo/
uv run pytest

# 4. Commit with conventional commit message
Expand Down
63 changes: 56 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies = [
dev = [
"pytest>=8.4.0",
"mypy>=1.16.0",
"pyright>=1.1.400",
"ruff>=0.12.0",
"types-pyyaml>=6.0.0",
]
Expand Down Expand Up @@ -92,16 +93,64 @@ quote-style = "double"

[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true
# Enable strict mode for comprehensive type checking
strict = true
# Additional strictness beyond --strict
warn_unreachable = true
no_implicit_optional = true
strict_equality = true
enable_error_code = [
"ignore-without-code",
"redundant-cast",
"truthy-bool",
"truthy-iterable",
"unused-awaitable",
]
# Output formatting
show_error_codes = true
show_column_numbers = true
pretty = true
check_untyped_defs = true
disallow_incomplete_defs = true
# Error handling
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = "posthog.*"
ignore_missing_imports = true

[tool.pyright]
pythonVersion = "3.9"
pythonPlatform = "All"
typeCheckingMode = "strict"
include = ["src/promptfoo"]
exclude = ["tests", "**/__pycache__"]
# Report all errors in strict mode
reportMissingImports = true
reportMissingTypeStubs = false # Disable for posthog which lacks full stubs
reportUnusedImport = true
reportUnusedClass = true
reportUnusedFunction = true
reportUnusedVariable = true
reportDuplicateImport = true
reportPrivateUsage = true
reportConstantRedefinition = true
reportIncompatibleMethodOverride = true
reportIncompatibleVariableOverride = true
reportInconsistentConstructor = true
reportOverlappingOverload = true
reportUninitializedInstanceVariable = true
reportCallInDefaultInitializer = true
reportUnnecessaryIsInstance = true
reportUnnecessaryCast = true
reportUnnecessaryComparison = true
reportUnnecessaryContains = true
reportImplicitStringConcatenation = false
reportUnusedCallResult = false
reportUnusedExpression = true
reportUnnecessaryTypeIgnoreComment = false # Allow type: ignore for mypy compatibility
reportMatchNotExhaustive = true
# Relax some strict checks for third-party libraries without full stubs
reportUnknownMemberType = false
reportUnknownArgumentType = false
reportUnknownVariableType = false
reportUnknownParameterType = false

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
2 changes: 1 addition & 1 deletion src/promptfoo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def _requires_shell(executable: str) -> bool:
return ext.lower() in _WINDOWS_SHELL_EXTENSIONS


def _run_command(cmd: list[str], env: Optional[dict[str, str]] = None) -> subprocess.CompletedProcess:
def _run_command(cmd: list[str], env: Optional[dict[str, str]] = None) -> subprocess.CompletedProcess[bytes]:
"""Execute a command, handling shell requirements on Windows."""
if _requires_shell(cmd[0]):
return subprocess.run(subprocess.list2cmdline(cmd), shell=True, env=env)
Expand Down
4 changes: 2 additions & 2 deletions src/promptfoo/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ def shutdown(self) -> None:
"""Shutdown the telemetry client and flush any pending events."""
if self._client:
try:
self._client.flush()
self._client.shutdown()
self._client.flush() # type: ignore[no-untyped-call]
self._client.shutdown() # type: ignore[no-untyped-call]
except Exception:
pass # Silently fail
finally:
Expand Down
24 changes: 24 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.