Skip to content

CI: Add basedpyright type checker - #167

Merged
ModeSevenIndustrialSolutions merged 1 commit into
lfreleng-actions:mainfrom
modeseven-lfreleng-actions:feat/basedpyright-139
Aug 16, 2026
Merged

CI: Add basedpyright type checker#167
ModeSevenIndustrialSolutions merged 1 commit into
lfreleng-actions:mainfrom
modeseven-lfreleng-actions:feat/basedpyright-139

Conversation

@ModeSevenIndustrialSolutions

Copy link
Copy Markdown
Contributor

Summary

Closes #139.

Adds the canonical basedpyright hook from actions-template, a tuned [tool.basedpyright] block in pyproject.toml, and remediates every diagnostic it reports so the hook lands green.

Baseline in the issue was 111 errors / 3262 warnings (default "recommended" mode). Final state is 0 errors / 0 warnings, both in the sandboxed hook environment and locally via uv run basedpyright where third-party imports actually resolve.

Configuration

[tool.basedpyright]
pythonVersion = "3.10"
pythonPlatform = "All"
typeCheckingMode = "standard"
include = ["src", "tests"]

reportMissingImports = false
reportMissingTypeStubs = false
reportUnusedImport = false
reportUnusedVariable = false
  • standard mode scoped to src and tests.
  • reportMissingImports / reportMissingTypeStubs are off because the hook installs basedpyright into an isolated virtualenv without this project's runtime dependencies (pydantic, PyGithub, GitPython, PyYAML, ijson). Same rationale as dependamerge.
  • reportUnusedImport / reportUnusedVariable are off because ruff already covers them (F401, F841).
  • basedpyright==1.39.9 added to the dev extra, pinned to the version the hook is frozen at, so local runs and CI cannot disagree.

Remediation

Every diagnostic turned out to be a latent defect rather than noise.

src/extractors/gerrit.py — stale return type masking a runtime failure

GerritExtractor.extract() was annotated -> GerritMetadata | None, but every code path returns a GerritMetadata — its own inline comment said so ("Always return GerritMetadata object (with empty fields) instead of None"). The stale annotation made main.py:211 look like it could pass None into the non-optional CompleteMetadata.gerrit_environment field, which would raise a Pydantic ValidationError at runtime. Corrected the annotation and folded the explanatory comment into the docstring.

src/config.py — attribute redeclaration contradicting the class-level types

__init__ re-annotated GITHUB_STEP_SUMMARY, GITHUB_EVENT_PATH and GITHUB_ACTOR_ID, shadowing the class-level declarations. The GITHUB_ACTOR_ID case was the harmful one: the inline annotation narrowed it to int | None, contradicting the class-level int | str | None and making the documented "fall back to the raw value on validation failure" path a type error. Dropped the redundant inline annotations so the class-level declarations apply.

tests/test_main.py — ~80 keyword arguments that do not exist on the models

Three fixtures passed arguments such as action, sender_login, protected, author_email, committer_name, timestamp, title, body, state, merged, draft, head_sha, labels, requested_reviewers, assignees, topic, change_owner_name, change_subject and commit_message — none of which are fields on EventMetadata, RefMetadata, CommitMetadata, PullRequestMetadata, ActorMetadata or GerritMetadata.

Pydantic silently discards unknown fields by default, so all of this was dead weight that made the fixtures actively misrepresent the models to anyone reading them. No assertion anywhere referenced the discarded values. Replaced with the real fields, mapping intent across where an equivalent exists (author_nameauthor, head_refsource_branch, base_reftarget_branch) and populating the genuine flags (is_branch_push, is_pull_request, is_default_branch).

Drive-by: ruff was never linting src/

Not strictly in scope, but it sits three lines from the hook being added and is a real coverage hole.

The ruff and ruff-format hooks carried files: ^(scripts|tests|custom_components)/.+\.py$, inherited from an unrelated (Home Assistant style) template. Neither scripts/ nor custom_components/ exists in this repository, so the pattern matched tests/ only and silently excluded all of src/ from pre-commit linting. Removed to match the canonical template. Ruff already passes repository-wide (All checks passed!, 50 files already formatted), so this is a no-op in terms of code changes — it just closes the gap.

Happy to split this out into its own PR if preferred.

Validation

Check Result
prek run --all-files all hooks pass
prek run basedpyright --all-files Passed
uv run basedpyright (deps resolved) 0 errors, 0 warnings, 0 notes
uv run pytest 535 passed
uv run ruff check . All checks passed
uv run ruff format --check . 50 files already formatted

The hook was also verified to actually fail rather than silently pass in the sandboxed environment, by temporarily introducing a reportReturnType violation and confirming it was caught.

No workflow files were modified, so no Zizmor audit was required.

Acceptance criteria

  • [tool.basedpyright] block present in pyproject.toml
  • basedpyright hook added, preserving existing config (skip: list, hook args and per-hook customizations left intact)
  • prek run basedpyright --all-files reports 0 errors
  • CI green

Note: this is independent of #166 (the GitPython/Dependabot security work) and branches from main, so the two should not conflict.

Adds the canonical basedpyright hook from actions-template, a tuned
[tool.basedpyright] block in pyproject.toml, and remediates every
diagnostic it reports so the hook lands green.

Configuration uses typeCheckingMode = "standard" scoped to src and
tests. reportMissingImports and reportMissingTypeStubs are disabled
because the hook runs basedpyright in an isolated virtualenv without
this project's runtime dependencies. reportUnusedImport and
reportUnusedVariable are disabled as ruff already covers them (F401,
F841). basedpyright is pinned in the dev extra to the version the
hook is frozen at, so local runs and CI cannot disagree.

The diagnostics were latent defects rather than noise:

- GerritExtractor.extract() was annotated as returning
  GerritMetadata | None, but every code path returns a
  GerritMetadata; its own comment said as much. The stale annotation
  made main.py look like it could pass None into the non-optional
  CompleteMetadata.gerrit_environment field, which would raise a
  Pydantic ValidationError at runtime. Corrected the annotation and
  folded the explanatory comment into the docstring.

- Config re-annotated GITHUB_STEP_SUMMARY, GITHUB_EVENT_PATH and
  GITHUB_ACTOR_ID inside __init__, shadowing the class-level
  declarations. The GITHUB_ACTOR_ID redeclaration narrowed the type
  to int | None, contradicting the class-level int | str | None and
  making the documented "raw value on validation failure" fallback a
  type error. Dropped the redundant inline annotations.

- Three fixtures in test_main.py passed roughly 80 keyword arguments
  that do not exist on the models: action, sender_login, protected,
  author_email, title, labels, topic, change_subject and others.
  Pydantic silently discards unknown fields, so these were dead
  weight that made the fixtures misrepresent the models. Replaced
  them with the real fields, mapping intent across where an
  equivalent exists: author_name to author, head_ref to
  source_branch, base_ref to target_branch.

Separately, the ruff and ruff-format hooks carried a files pattern
of ^(scripts|tests|custom_components)/.+\.py$ inherited from an
unrelated template. Neither scripts/ nor custom_components/ exists
here, so the pattern silently excluded all of src/ from pre-commit
linting. Removed it to match the canonical template; ruff already
passes repository-wide.

Closes: lfreleng-actions#139

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
@ModeSevenIndustrialSolutions
ModeSevenIndustrialSolutions requested review from a team and a balanced review from Copilot August 15, 2026 13:48
@github-actions github-actions Bot added the CI CI and tests updates label Aug 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds basedpyright type checking and resolves existing diagnostics while preserving runtime behavior.

Changes:

  • Adds pinned basedpyright configuration, dependency, lock data, and pre-commit hook.
  • Corrects stale type annotations and invalid test fixture fields.
  • Expands Ruff pre-commit coverage to src/.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
.pre-commit-config.yaml Adds basedpyright and broadens Ruff coverage.
pyproject.toml Configures and pins basedpyright.
uv.lock Locks basedpyright and Node.js dependency.
src/config.py Removes contradictory instance annotations.
src/extractors/gerrit.py Corrects the extractor return type and documentation.
tests/test_main.py Aligns fixtures with actual model fields.
tests/test_commit_extractor.py Narrows the optional message before measuring length.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ModeSevenIndustrialSolutions
ModeSevenIndustrialSolutions merged commit a31da44 into lfreleng-actions:main Aug 16, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI CI and tests updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add basedpyright pre-commit hook to match canonical linting

3 participants