Skip to content

Commit f3c7d28

Browse files
authored
Add pyright for static type checking (#17744)
Related to #17301 Summary of the issue: NVDA has no type checking for python. Python is a dynamically typed language meaning typing issues can occur at runtime. Static analysers like `mypy` or `pyright` can be used to avoid typing errors. Description of user facing changes None Description of development approach `pyright` was chosen over `mypy` for performance reasons. Additionally, pyright has better pre-commit integration, and is built-in to VS Code, which is used by most NVDA developers. We are far from compliant with static type checking, so most rules are currently disabled. The intention is to fix compliance and turn on rules over time. Various trivial typing fixes have occurred, particularly: - removing unnecessary casts - fixing type hints - adding relevant type ignore comments - removing redundant type ignore comments - adding type stubs for `gettext` builtins and `_buildVersion` generated build vars - remove duplicate imports - adding `__all__` to mark export for private classes - adding missing `match` cases Testing strategy: - [x] Smoke test NVDA - [x] pyright passing - [x] Automated testing passes Known issues with pull request: We are far from compliant with static type checking, so most rules are currently disabled. The intention is to fix compliance and turn on rules over time.
1 parent 6254b49 commit f3c7d28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+374
-110
lines changed

.pre-commit-config.yaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ci:
99
# https://stackoverflow.com/questions/70778806/pre-commit-not-using-virtual-environment .
1010
# Can't run licenseCheck as it relies on telemetry,
1111
# which CI blocks.
12-
skip: [scons-source, checkPot, unitTest, licenseCheck]
12+
skip: [scons-source, checkPot, unitTest, licenseCheck, pyrightLocal]
1313
autoupdate_schedule: monthly
1414
autoupdate_commit_msg: "Pre-commit auto-update"
1515
autofix_commit_msg: "Pre-commit auto-fix"
@@ -107,6 +107,23 @@ repos:
107107
- id: ruff-format
108108
name: format with ruff
109109

110+
- repo: https://github.com/RobertCraigie/pyright-python
111+
rev: v1.1.394
112+
hooks:
113+
- id: pyright
114+
alias: pyrightLocal
115+
name: Check types with pyright
116+
117+
- repo: https://github.com/RobertCraigie/pyright-python
118+
rev: v1.1.396
119+
hooks:
120+
- id: pyright
121+
alias: pyrightCI
122+
name: Check types with pyright
123+
# use nodejs version of pyright and install requirements.txt for CI
124+
additional_dependencies: ["-rrequirements.txt", "pyright[nodejs]"]
125+
stages: [manual] # Only run from CI manually
126+
110127
- repo: local
111128
hooks:
112129
- id: scons-source

projectDocs/testing/automated.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ scons checkPot
4141

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

44+
[pyright](https://microsoft.github.io/pyright/) is used for static type checking.
45+
4446
To run the linter locally:
4547

4648
```cmd
4749
runlint.bat
4850
```
4951

50-
To be warned about linting errors faster, you may wish to integrate Ruff with your IDE or other development tools you are using.
52+
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.
5153

5254
### Unit Tests
5355

pyproject.toml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ exclude = [
5151
".tox",
5252
"build",
5353
"output",
54+
".venv",
5455
# When excluding concrete paths relative to a directory,
5556
# not matching multiple folders by name e.g. `__pycache__`,
5657
# paths are relative to the configuration file.
@@ -59,6 +60,8 @@ exclude = [
5960
"./source/louis",
6061
# #10924: generated by third-party dependencies
6162
"./source/comInterfaces/*",
63+
# Code from add-ons
64+
"./source/userConfig/addons/*",
6265
]
6366

6467
[tool.ruff.format]
@@ -115,3 +118,154 @@ ignore_packages = [
115118
"wxPython", # wxWindows Library License
116119
"pillow", # PIL Software License
117120
]
121+
122+
123+
[tool.pyright]
124+
venvPath = ".venv"
125+
venv = "."
126+
pythonPlatform = "Windows"
127+
typeCheckingMode = "strict"
128+
129+
include = [
130+
"**/*.py",
131+
"**/*.pyw",
132+
]
133+
134+
exclude = [
135+
"sconstruct",
136+
"*sconscript",
137+
".git",
138+
"__pycache__",
139+
".tox",
140+
"build",
141+
"output",
142+
".venv",
143+
# When excluding concrete paths relative to a directory,
144+
# not matching multiple folders by name e.g. `__pycache__`,
145+
# paths are relative to the configuration file.
146+
"./include/*",
147+
"./miscDeps",
148+
"./source/louis",
149+
# #10924: generated by third-party dependencies
150+
"./source/comInterfaces/*",
151+
# Code from add-ons
152+
"./source/userConfig/addons/*",
153+
]
154+
155+
# Tell pyright where to load python code from
156+
extraPaths = [
157+
"./source",
158+
# Used by system tests
159+
"./tests/system/libraries",
160+
# Used internally in NVDA
161+
"./miscDeps/python",
162+
]
163+
164+
# General config
165+
analyzeUnannotatedFunctions = true
166+
deprecateTypingAliases = true
167+
168+
# Stricter typing
169+
strictParameterNoneValue = true
170+
strictListInference = true
171+
strictDictionaryInference = true
172+
strictSetInference = true
173+
174+
# Compliant rules
175+
reportAssertAlwaysTrue = true
176+
reportAssertTypeFailure = true
177+
reportDuplicateImport = true
178+
reportIncompleteStub = true
179+
reportInconsistentOverload = true
180+
reportInconsistentConstructor = true
181+
reportInvalidStringEscapeSequence = true
182+
reportInvalidStubStatement = true
183+
reportInvalidTypeVarUse = true
184+
reportMatchNotExhaustive = true
185+
reportMissingModuleSource = true
186+
reportMissingImports = true
187+
reportNoOverloadImplementation = true
188+
reportOptionalContextManager = true
189+
reportOverlappingOverload = true
190+
reportPrivateImportUsage = true
191+
reportPropertyTypeMismatch = true
192+
reportSelfClsParameterName = true
193+
reportShadowedImports = true
194+
reportTypeCommentUsage = true
195+
reportTypedDictNotRequiredAccess = true
196+
reportUndefinedVariable = true
197+
reportUnusedExpression = true
198+
reportUnboundVariable = true
199+
reportUnhashable = true
200+
reportUnnecessaryCast = true
201+
reportUnnecessaryContains = true
202+
reportUnnecessaryTypeIgnoreComment = true
203+
reportUntypedClassDecorator = true
204+
reportUntypedFunctionDecorator = true
205+
reportUnusedClass = true
206+
reportUnusedCoroutine = true
207+
reportUnusedExcept = true
208+
209+
# Should switch to true when possible
210+
reportDeprecated = false # 1834 errors
211+
212+
# Can be enabled by generating type stubs for modules via pyright CLI
213+
reportMissingTypeStubs = false
214+
215+
# Bad rules
216+
# These are roughly sorted by compliance to make it easier for devs to focus on enabling them.
217+
# Errors were last checked Feb 2025.
218+
# 1-50 errors
219+
reportUnsupportedDunderAll = false # 2 errors
220+
reportAbstractUsage = false # 3 errors
221+
reportUntypedBaseClass = false # 4 errors
222+
reportOptionalIterable = false # 5 errors
223+
reportCallInDefaultInitializer = false # 6 errors
224+
reportInvalidTypeArguments = false # 7 errors
225+
reportUntypedNamedTuple = false # 11 errors
226+
reportRedeclaration = false # 12 errors
227+
reportOptionalCall = false # 16 errors
228+
reportConstantRedefinition = false # 18 errors
229+
reportWildcardImportFromLibrary = false # 26 errors
230+
reportIncompatibleVariableOverride = false # 28 errors
231+
reportInvalidTypeForm = false # 38 errors
232+
233+
234+
# 50-100 errors
235+
reportGeneralTypeIssues = false # 53 errors
236+
reportOptionalOperand = false # 59 errors
237+
reportUnnecessaryComparison = false # 67 errors
238+
reportFunctionMemberAccess = false # 80 errors
239+
reportUnnecessaryIsInstance = false # 88 errors
240+
reportUnusedFunction = false # 97 errors
241+
reportImportCycles = false # 99 errors
242+
reportUnusedImport = false # 113 errors
243+
reportUnusedVariable = false # 147 errors
244+
245+
# 100-1000 errors
246+
reportOperatorIssue = false # 102 errors
247+
reportAssignmentType = false # 103 errors
248+
reportReturnType = false # 104 errors
249+
reportPossiblyUnboundVariable = false # 126 errors
250+
reportMissingSuperCall = false # 159 errors
251+
reportUninitializedInstanceVariable = false # 179 errors
252+
reportUnknownLambdaType = false # 196 errors
253+
reportMissingTypeArgument = false # 204 errors
254+
reportImplicitStringConcatenation = false # 300+ errors
255+
reportIncompatibleMethodOverride = false # 300+ errors
256+
reportPrivateUsage = false # 900+ errors
257+
258+
# 1000+ errors
259+
reportUnusedCallResult = false # 1000+ errors
260+
reportOptionalSubscript = false # 1000+ errors, mostly failing to recognize config setter/getter
261+
reportCallIssue = false # 1000+ errors, mostly failing to recognize config setter/getter
262+
reportOptionalMemberAccess = false # 1683 errors
263+
reportImplicitOverride = false # 2000+ errors
264+
reportIndexIssue = false # 2000+ errors, mostly failing to recognize config setter/getter
265+
reportAttributeAccessIssue = false # 2000+ errors
266+
reportArgumentType = false # 2000+ errors
267+
reportUnknownParameterType = false # 4000+ errors
268+
reportMissingParameterType = false # 4000+ errors
269+
reportUnknownVariableType = false # 6000+ errors
270+
reportUnknownArgumentType = false # 6000+ errors
271+
reportUnknownMemberType = false # 20000+ errors

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ sphinx_rtd_theme==3.0.1
4545
# Requirements for automated linting
4646
ruff==0.8.1
4747
pre-commit==4.0.1
48+
pyright==1.1.396
4849

4950
# Running automated license checks
5051
licensecheck==2024.3

runlint.bat

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ call "%scriptsDir%\venvCmd.bat" ruff check --fix %ruffCheckArgs%
1414
if ERRORLEVEL 1 exit /b %ERRORLEVEL%
1515
call "%scriptsDir%\venvCmd.bat" ruff format %ruffFormatArgs%
1616
if ERRORLEVEL 1 exit /b %ERRORLEVEL%
17+
call "%scriptsDir%\venvCmd.bat" pyright --threads --level warning
18+
if ERRORLEVEL 1 exit /b %ERRORLEVEL%

source/NVDAObjects/IAccessible/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,8 +1890,6 @@ def _getIA2RelationFirstTarget(
18901890
else:
18911891
raise TypeError(f"Bad type for 'relationType' arg, got: {type(relationType)}")
18921892

1893-
relationType = typing.cast(IAccessibleHandler.RelationType, relationType)
1894-
18951893
try:
18961894
# rather than fetch all the relations and querying the type, do that in process for performance reasons
18971895
targetsGen = self._getIA2TargetsForRelationsOfType(relationType, maxRelations=1)
@@ -1935,8 +1933,6 @@ def _getIA2RelationTargetsOfType(
19351933
else:
19361934
raise TypeError(f"Bad type for 'relationType' arg, got: {type(relationType)}")
19371935

1938-
relationType = typing.cast(IAccessibleHandler.RelationType, relationType)
1939-
19401936
try:
19411937
# rather than fetch all the relations and querying the type, do that in process for performance reasons
19421938
# Bug in Chrome, Chrome does not respect maxRelations param.

source/NVDAObjects/IAccessible/adobeAcrobat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from NVDAObjects import NVDAObjectTextInfo
1414
from NVDAObjects.behaviors import EditableText
1515
from comtypes import GUID, COMError, IServiceProvider
16-
from comtypes.gen.AcrobatAccessLib import IAccID, IGetPDDomNode, IPDDomElement
16+
from comtypes.gen.AcrobatAccessLib import IAccID, IGetPDDomNode, IPDDomElement # type: ignore[reportMissingImports]
1717
from logHandler import log
1818

1919
SID_AccID = GUID("{449D454B-1F46-497e-B2B6-3357AED9912B}")

source/NVDAObjects/IAccessible/ia2Web.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ class EditorChunk(Ia2Web):
330330

331331
class Math(Ia2Web):
332332
def _get_mathMl(self):
333-
from comtypes.gen.ISimpleDOM import ISimpleDOMNode
333+
from comtypes.gen.ISimpleDOM import ISimpleDOMNode # type: ignore[reportMissingImports]
334334

335335
try:
336336
node = self.IAccessibleObject.QueryInterface(ISimpleDOMNode)

source/NVDAObjects/UIA/winConsoleUIA.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
from ..window import Window
2828

2929

30+
__all__ = [
31+
"ConsoleUIATextInfo",
32+
"ConsoleUIATextInfoWorkaroundEndInclusive",
33+
"WinConsoleUIA",
34+
"consoleUIAWindow",
35+
"findExtraOverlayClasses",
36+
"_DiffBasedWinTerminalUIA",
37+
"_NotificationsBasedWinTerminalUIA",
38+
]
39+
40+
3041
class ConsoleUIATextInfo(UIATextInfo):
3142
"A TextInfo implementation for consoles with an IMPROVED, but not FORMATTED, API level."
3243

source/UIAHandler/_remoteOps/instructions/_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
InstructionBase,
1010
)
1111

12+
__all__ = ["_TypedInstruction"]
13+
1214

1315
class _TypedInstruction(InstructionBase):
1416
@property

0 commit comments

Comments
 (0)