Skip to content

Add pyright for static type checking #17744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
19 changes: 18 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ci:
# https://stackoverflow.com/questions/70778806/pre-commit-not-using-virtual-environment .
# Can't run licenseCheck as it relies on telemetry,
# which CI blocks.
skip: [scons-source, checkPot, unitTest, licenseCheck]
skip: [scons-source, checkPot, unitTest, licenseCheck, pyrightLocal]
autoupdate_schedule: monthly
autoupdate_commit_msg: "Pre-commit auto-update"
autofix_commit_msg: "Pre-commit auto-fix"
Expand Down Expand Up @@ -107,6 +107,23 @@ repos:
- id: ruff-format
name: format with ruff

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.394
hooks:
- id: pyright
alias: pyrightLocal
name: Check types with pyright

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.394
hooks:
- id: pyright
alias: pyrightCI
name: Check types with pyright
# use nodejs version of pyright and install requirements.txt for CI
additional_dependencies: ["-rrequirements.txt", "pyright[nodejs]"]
stages: [manual] # Only run from CI manually

- repo: local
hooks:
- id: scons-source
Expand Down
4 changes: 3 additions & 1 deletion projectDocs/testing/automated.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ scons checkPot

Our linting process involves running [Ruff](https://docs.astral.sh/ruff) to pick up Python linting issues and auto-apply fixes where possible.

[pyright](https://microsoft.github.io/pyright/) is used for static type checking.

To run the linter locally:

```cmd
runlint.bat
```

To be warned about linting errors faster, you may wish to integrate Ruff with your IDE or other development tools you are using.
To be warned about linting errors faster, you may wish to integrate Ruff and pyright with your IDE or other development tools you are using.

### Unit Tests

Expand Down
151 changes: 151 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ exclude = [
".tox",
"build",
"output",
".venv",
# When excluding concrete paths relative to a directory,
# not matching multiple folders by name e.g. `__pycache__`,
# paths are relative to the configuration file.
Expand All @@ -59,6 +60,8 @@ exclude = [
"./source/louis",
# #10924: generated by third-party dependencies
"./source/comInterfaces/*",
# Code from add-ons
"./source/userConfig/addons/*",
]

[tool.ruff.format]
Expand Down Expand Up @@ -115,3 +118,151 @@ ignore_packages = [
"wxPython", # wxWindows Library License
"pillow", # PIL Software License
]


[tool.pyright]
venvPath = ".venv"
venv = "."
pythonPlatform = "Windows"
typeCheckingMode = "strict"

include = [
"**/*.py",
"**/*.pyw",
]

exclude = [
"sconstruct",
"*sconscript",
".git",
"__pycache__",
".tox",
"build",
"output",
".venv",
# When excluding concrete paths relative to a directory,
# not matching multiple folders by name e.g. `__pycache__`,
# paths are relative to the configuration file.
"./include/*",
"./miscDeps",
"./source/louis",
# #10924: generated by third-party dependencies
"./source/comInterfaces/*",
# Code from add-ons
"./source/userConfig/addons/*",
]

extraPaths = [
"./source",
"./tests/system/libraries",
"./miscDeps/python",
]

# General config
analyzeUnannotatedFunctions = true
deprecateTypingAliases = true

# Stricter typing
strictParameterNoneValue = true
strictListInference = true
strictDictionaryInference = true
strictSetInference = true

# Compliant rules
reportAssertAlwaysTrue = true
reportAssertTypeFailure = true
reportDuplicateImport = true
reportIncompleteStub = true
reportInconsistentOverload = true
reportInconsistentConstructor = true
reportInvalidStringEscapeSequence = true
reportInvalidStubStatement = true
reportInvalidTypeVarUse = true
reportMatchNotExhaustive = true
reportMissingModuleSource = true
reportMissingImports = true
reportNoOverloadImplementation = true
reportOptionalContextManager = true
reportOverlappingOverload = true
reportPrivateImportUsage = true
reportPropertyTypeMismatch = true
reportSelfClsParameterName = true
reportShadowedImports = true
reportTypeCommentUsage = true
reportTypedDictNotRequiredAccess = true
reportUndefinedVariable = true
reportUnusedExpression = true
reportUnboundVariable = true
reportUnhashable = true
reportUnnecessaryCast = true
reportUnnecessaryContains = true
reportUnnecessaryTypeIgnoreComment = true
reportUntypedClassDecorator = true
reportUntypedFunctionDecorator = true
reportUnusedClass = true
reportUnusedCoroutine = true
reportUnusedExcept = true

# Should switch to true when possible
reportDeprecated = false # 1834 errors

# Can be enabled by generating type stubs for modules via pyright CLI
reportMissingTypeStubs = false

# Bad rules
# These are roughly sorted by compliance to make it easier for devs to focus on enabling them.
# Errors were last checked Feb 2025.
# 1-50 errors
reportUnsupportedDunderAll = false # 2 errors
reportAbstractUsage = false # 3 errors
reportUntypedBaseClass = false # 4 errors
reportOptionalIterable = false # 5 errors
reportCallInDefaultInitializer = false # 6 errors
reportInvalidTypeArguments = false # 7 errors
reportUntypedNamedTuple = false # 11 errors
reportRedeclaration = false # 12 errors
reportOptionalCall = false # 16 errors
reportConstantRedefinition = false # 18 errors
reportWildcardImportFromLibrary = false # 26 errors
reportIncompatibleVariableOverride = false # 28 errors
reportInvalidTypeForm = false # 38 errors


# 50-100 errors
reportGeneralTypeIssues = false # 53 errors
reportOptionalOperand = false # 59 errors
reportUnnecessaryComparison = false # 67 errors
reportFunctionMemberAccess = false # 80 errors
reportUnnecessaryIsInstance = false # 88 errors
reportUnusedFunction = false # 97 errors
reportImportCycles = false # 99 errors
reportUnusedImport = false # 113 errors
reportUnusedVariable = false # 147 errors

# 100-1000 errors
reportOperatorIssue = false # 102 errors
reportAssignmentType = false # 103 errors
reportReturnType = false # 104 errors
reportPossiblyUnboundVariable = false # 126 errors
reportMissingSuperCall = false # 159 errors
reportUninitializedInstanceVariable = false # 179 errors
reportUnknownLambdaType = false # 196 errors
reportMissingTypeArgument = false # 204 errors
reportImplicitStringConcatenation = false # 300+ errors
reportIncompatibleMethodOverride = false # 300+ errors
reportPrivateUsage = false # 900+ errors

# 1000+ errors
reportUnusedCallResult = false # 1000+ errors
reportOptionalSubscript = false # 1000+ errors, mostly failing to recognize config setter/getter
reportCallIssue = false # 1000+ errors, mostly failing to recognize config setter/getter
reportOptionalMemberAccess = false # 1683 errors
reportImplicitOverride = false # 2000+ errors
reportIndexIssue = false # 2000+ errors, mostly failing to recognize config setter/getter
reportAttributeAccessIssue = false # 2000+ errors
reportArgumentType = false # 2000+ errors
reportUnknownParameterType = false # 4000+ errors
reportMissingParameterType = false # 4000+ errors
reportUnknownVariableType = false # 6000+ errors
reportUnknownArgumentType = false # 6000+ errors
reportUnknownMemberType = false # 20000+ errors
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ sphinx_rtd_theme==3.0.1
# Requirements for automated linting
ruff==0.8.1
pre-commit==4.0.1
pyright==1.1.394

# Running automated license checks
licensecheck==2024.3
Expand Down
2 changes: 2 additions & 0 deletions runlint.bat
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ call "%scriptsDir%\venvCmd.bat" ruff check --fix %ruffCheckArgs%
if ERRORLEVEL 1 exit /b %ERRORLEVEL%
call "%scriptsDir%\venvCmd.bat" ruff format %ruffFormatArgs%
if ERRORLEVEL 1 exit /b %ERRORLEVEL%
call "%scriptsDir%\venvCmd.bat" pyright --threads --level warning
if ERRORLEVEL 1 exit /b %ERRORLEVEL%
4 changes: 0 additions & 4 deletions source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1890,8 +1890,6 @@ def _getIA2RelationFirstTarget(
else:
raise TypeError(f"Bad type for 'relationType' arg, got: {type(relationType)}")

relationType = typing.cast(IAccessibleHandler.RelationType, relationType)

try:
# rather than fetch all the relations and querying the type, do that in process for performance reasons
targetsGen = self._getIA2TargetsForRelationsOfType(relationType, maxRelations=1)
Expand Down Expand Up @@ -1935,8 +1933,6 @@ def _getIA2RelationTargetsOfType(
else:
raise TypeError(f"Bad type for 'relationType' arg, got: {type(relationType)}")

relationType = typing.cast(IAccessibleHandler.RelationType, relationType)

try:
# rather than fetch all the relations and querying the type, do that in process for performance reasons
# Bug in Chrome, Chrome does not respect maxRelations param.
Expand Down
2 changes: 1 addition & 1 deletion source/NVDAObjects/IAccessible/adobeAcrobat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from NVDAObjects import NVDAObjectTextInfo
from NVDAObjects.behaviors import EditableText
from comtypes import GUID, COMError, IServiceProvider
from comtypes.gen.AcrobatAccessLib import IAccID, IGetPDDomNode, IPDDomElement
from comtypes.gen.AcrobatAccessLib import IAccID, IGetPDDomNode, IPDDomElement # type: ignore[reportMissingImports]
from logHandler import log

SID_AccID = GUID("{449D454B-1F46-497e-B2B6-3357AED9912B}")
Expand Down
2 changes: 1 addition & 1 deletion source/NVDAObjects/IAccessible/ia2Web.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ class EditorChunk(Ia2Web):

class Math(Ia2Web):
def _get_mathMl(self):
from comtypes.gen.ISimpleDOM import ISimpleDOMNode
from comtypes.gen.ISimpleDOM import ISimpleDOMNode # type: ignore[reportMissingImports]

try:
node = self.IAccessibleObject.QueryInterface(ISimpleDOMNode)
Expand Down
11 changes: 11 additions & 0 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
from ..window import Window


__all__ = [
"ConsoleUIATextInfo",
"ConsoleUIATextInfoWorkaroundEndInclusive",
"WinConsoleUIA",
"consoleUIAWindow",
"findExtraOverlayClasses",
"_DiffBasedWinTerminalUIA",
"_NotificationsBasedWinTerminalUIA",
]


class ConsoleUIATextInfo(UIATextInfo):
"A TextInfo implementation for consoles with an IMPROVED, but not FORMATTED, API level."

Expand Down
2 changes: 2 additions & 0 deletions source/UIAHandler/_remoteOps/instructions/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
InstructionBase,
)

__all__ = ["_TypedInstruction"]


class _TypedInstruction(InstructionBase):
@property
Expand Down
2 changes: 1 addition & 1 deletion source/UIAHandler/_remoteOps/instructions/textRange.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class TextRangeFindAttribute(_TypedInstruction):
def localExecute(self, registers: dict[lowLevel.OperandId, object]):
textRange = cast(UIA.IUIAutomationTextRange, registers[self.target.operandId])
attributeId = cast(int, registers[self.attributeId.operandId])
value = cast(object, registers[self.value.operandId])
value = registers[self.value.operandId]
reverse = cast(bool, registers[self.reverse.operandId])
registers[self.result.operandId] = textRange.FindAttribute(attributeId, value, reverse)

Expand Down
6 changes: 3 additions & 3 deletions source/UIAHandler/_remoteOps/remoteAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def elseBlock(self, silent: bool = False):
if not isinstance(scopeInstructionJustExited, instructions.ForkIfFalse):
raise RuntimeError("Else block not directly preceded by If block")
instructionList = self.rob.getDefaultInstructionList()
ifConditionInstruction = cast(instructions.ForkIfFalse, scopeInstructionJustExited)
ifConditionInstruction = scopeInstructionJustExited
# add a final jump instruction to the previous if block to skip over the else block.
if not silent:
instructionList.addComment("Jump over else block")
Expand Down Expand Up @@ -282,7 +282,7 @@ def forEachNumInRange(
remoteStep = cast(RemoteIntBase, RemoteType).ensureRemote(self.rob, cast(RemoteIntBase, step))
counter = remoteStart.copy()
with self.whileBlock(lambda: counter < remoteStop):
yield cast(RemoteIntBase, counter)
yield counter
counter += remoteStep

@remoteContextManager
Expand Down Expand Up @@ -319,7 +319,7 @@ def catchBlock(self, silent: bool = False):
if not isinstance(scopeInstructionJustExited, instructions.NewTryBlock):
raise RuntimeError("Catch block not directly preceded by Try block")
instructionList = self.rob.getDefaultInstructionList()
tryBlockInstruction = cast(instructions.NewTryBlock, scopeInstructionJustExited)
tryBlockInstruction = scopeInstructionJustExited
# add a final jump instruction to the previous try block to skip over the catch block.
if not silent:
instructionList.addComment("Jump over catch block")
Expand Down
10 changes: 10 additions & 0 deletions source/__builtins__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025 NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

def _(message: str) -> str: ...
def gettext(message: str) -> str: ...
def ngettext(singular: str, plural: str, n: int) -> str: ...
def pgettext(context: str, message: str) -> str: ...
def npgettext(context: str, singular: str, plural: str, n: int) -> str: ...
2 changes: 2 additions & 0 deletions source/addonStore/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

from .models.channel import UpdateChannel

__all__ = ["_AddonStoreSettings"]


@dataclass
class _AddonSettings:
Expand Down
7 changes: 7 additions & 0 deletions source/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
import controlTypes
from NVDAObjects import NVDAObject

__all__ = [
"AnnotationTarget",
"AnnotationOrigin",
"_AnnotationNavigation",
"_AnnotationNavigationNode",
"_AnnotationRolesT",
]

_AnnotationRolesT = Tuple[Optional["controlTypes.Role"]]

Expand Down
6 changes: 6 additions & 0 deletions source/appModules/poedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,9 @@ def reportFocus(self):
tones.beep(550, 50)
case _WindowControlId.NEEDS_WORK_SWITCH:
tones.beep(660, 50)
case _WindowControlId.NOTES_FOR_TRANSLATOR:
pass
case _WindowControlId.TRANSLATOR_COMMENT:
pass
case None:
pass
Loading