From e2906a81ee630ce6992d5292e9af27d19a6400b1 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 14 Mar 2024 10:26:31 -0400 Subject: [PATCH] Basic type-checking with mypy and pyright (#2102) --- .github/workflows/main.yml | 40 +++++++++++++++++++- Pythonwin/pywin/framework/stdin.py | 2 +- mypy.ini | 38 +++++++++++++++++++ pyrightconfig.json | 60 ++++++++++++++++++++++++++++++ pywin32_postinstall.py | 23 +++++------- win32/Lib/sspi.py | 2 +- 6 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 mypy.ini create mode 100644 pyrightconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65a8120b15..a0583c3a9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,12 +97,50 @@ jobs: # This job can be run locally with the `format_all.bat` script checkers: - runs-on: ubuntu-latest + runs-on: windows-2019 steps: - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + # This job only needs to target the oldest supported version (black@stable supports Python >=3.8) + python-version: '3.8' - run: pip install isort pycln - run: pycln . --config=pycln.toml --check - run: isort . --diff --check-only - uses: psf/black@stable with: options: "--fast --check --diff --verbose" + + mypy: + runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + # mypy 1.5 dropped support for python 3.7 + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - run: pip install types-regex types-setuptools mypy>=1.5 + - run: mypy . --python-version=${{ matrix.python-version }} + + pyright: + runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + # pyright vendors typeshed, but let's make sure we have the most up to date stubs + - run: pip install types-regex types-setuptools + - uses: jakebailey/pyright-action@v2 + with: + python-version: ${{ matrix.python-version }} + annotate: errors + diff --git a/Pythonwin/pywin/framework/stdin.py b/Pythonwin/pywin/framework/stdin.py index 93f5c02df2..91fe7ef3e1 100644 --- a/Pythonwin/pywin/framework/stdin.py +++ b/Pythonwin/pywin/framework/stdin.py @@ -168,4 +168,4 @@ def fake_input(prompt=None): finally: get_input_line = input else: - sys.stdin = Stdin() + sys.stdin = Stdin() # type: ignore[assignment] # Not an actual TextIO diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..9055b84925 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,38 @@ +[mypy] +show_column_numbers = true +warn_unused_ignores = true +; Target the oldest supported version in editors +python_version = 3.7 + +strict = false +implicit_reexport = true + +; Implicit return types ! +; TODO: turn back check_untyped_defs to true. For now this allows us to +; at least put mypy in place by massively reducing checked code +check_untyped_defs = false +disallow_untyped_calls = false +disallow_untyped_defs = false +disallow_incomplete_defs = false + +; attr-defined: Module has no attribute (modules are dynamic) +; method-assign: Cannot assign to a method (lots of monkey patching) +; name-defined: Name "..." is not defined (dynamic modules will be hard to type without stubs, ie: pythoncom.*, leave undefined/unbound to Flake8/Ruff/pyright) +disable_error_code = attr-defined, method-assign, name-defined +; TODO: adodbapi should be updated and fixed separatly +; Pythonwin/Scintilla is vendored +; Pythonwin/pywin/idle is vendored IDLE extensions predating Python 2.3. They now live in idlelib in https://github.com/python/cpython/tree/main/Lib/idlelib +; Ignoring non-public apis for now +; Duplicate module named "rasutil" and "setup", short-term fix is to ignore +exclude = .*((build|adodbapi|Pythonwin/Scintilla|Pythonwin/pywin/idle|[Tt]est|[Dd]emos?)/.*|rasutil.py|setup.py) + +; C-modules that will need type-stubs +[mypy-adsi.*,dde,exchange,exchdapi,perfmon,servicemanager,win32api,win32clipboard,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,wincerapi,winxpgui,_win32sysloader,_winxptheme] +ignore_missing_imports = True + +; verstamp is installed from win32verstamp.py called in setup.py +; Most of win32com re-exports win32comext +; Test is a local untyped module in win32comext.axdebug +; pywin32_system32 is an empty module created in setup.py to store dlls +[mypy-verstamp,win32com.*,Test,pywin32_system32] +ignore_missing_imports = True diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000000..1dd371af23 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,60 @@ +{ + "typeCheckingMode": "basic", + // Target the oldest supported version in editors + "pythonVersion": "3.7", + // Keep it simple for now by allowing both mypy and pyright to use `type: ignore` + "enableTypeIgnoreComments": true, + // Exclude from scanning when running pyright + "exclude": [ + "build/", + // TODO: adodbapi should be updated and fixed separatly + "adodbapi/", + // Vendored + "Pythonwin/Scintilla/", + // Vendored IDLE extensions predating Python 2.3. They now live in idlelib in https://github.com/python/cpython/tree/main/Lib/idlelib + "Pythonwin/pywin/idle/", + // Ignoring non-public apis for now + "**/Test/", + "**/test/", + "**/Demos/", + "**/demo/", + ], + // Packages that will be accessible globally. + // Setting this makes pyright use the repo's code for those modules instead of typeshed or pywin32 in site-packages + "extraPaths": [ + "com", + "win32/Lib", + "Pythonwin", + ], + // TODO: For now this allows us to at least put pyright in place by massively reducing checked code + // it also reduces issues with the shipped types-pywin32 from typeshed + "reportGeneralTypeIssues": "none", + "reportArgumentType": "none", + "reportAttributeAccessIssue": "none", + // FIXE: These all need to be fixed first and turned back to error + // some of the fixes need to be done in types-pywin32 from typeshed + "reportAssignmentType": "warning", + "reportCallIssue": "warning", + "reportIndexIssue": "warning", + "reportOperatorIssue": "warning", + "reportOptionalCall": "warning", + "reportOptionalIterable": "warning", + "reportOptionalMemberAccess": "warning", + "reportOptionalSubscript": "warning", + // TODO: Leave Unbound/Undefined to its own PR(s) + "reportUnboundVariable": "warning", + "reportUndefinedVariable": "warning", + // Too many dynamically generated modules. This will require type stubs to properly fix. + "reportMissingImports": "warning", + // IDEM, but happens when pywin32 is not in site-packages but module is found from typeshed. + // TODO: Is intended to be fixed with an editable install + // Since we're a library, and not user code, we care less about forgetting to install a dependency, + // as long as we have its stubs. So just disabling for now is fine. + "reportMissingModuleSource": "none", + // External type stubs may not be completable, and this will require type stubs for dynamic modules. + "reportMissingTypeStubs": "information", + // Sometimes used for extra runtime safety + "reportUnnecessaryComparison": "warning", + // Use Flake8/Pycln/Ruff instead + "reportUnusedImport": "none", +} diff --git a/pywin32_postinstall.py b/pywin32_postinstall.py index 86844e95bd..5a4981edc6 100644 --- a/pywin32_postinstall.py +++ b/pywin32_postinstall.py @@ -2,19 +2,14 @@ # # copies pywintypesXX.dll and pythoncomXX.dll into the system directory, # and creates a pth file +import argparse import glob import os import shutil import sys import sysconfig - -try: - import winreg as winreg -except: - import winreg - -# Send output somewhere so it can be found if necessary... -import tempfile +import tempfile # Send output somewhere so it can be found if necessary... +import winreg tee_f = open(os.path.join(tempfile.gettempdir(), "pywin32_postinstall.log"), "w") @@ -44,11 +39,11 @@ def flush(self): # with sys.stdout as None but stderr is hooked up. This work-around allows # bdist_wininst to see the output we write and display it at the end of # the install. -if sys.stdout is None: +if sys.stdout is None: # pyright: ignore[reportUnnecessaryComparison] sys.stdout = sys.stderr -sys.stderr = Tee(sys.stderr) -sys.stdout = Tee(sys.stdout) +sys.stderr = Tee(sys.stderr) # type: ignore[assignment] # Not an actual TextIO +sys.stdout = Tee(sys.stdout) # type: ignore[assignment] # Not an actual TextIO com_modules = [ # module_name, class_names @@ -193,7 +188,9 @@ def LoadSystemModule(lib_dir, modname): loader = importlib.machinery.ExtensionFileLoader(modname, filename) spec = importlib.machinery.ModuleSpec(name=modname, loader=loader, origin=filename) mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) + spec.loader.exec_module( # pyright: ignore[reportOptionalMemberAccess] # We provide the loader, we know it won't be None + mod + ) def SetPyKeyVal(key_name, value_name, value): @@ -697,8 +694,6 @@ def verify_destination(location): def main(): - import argparse - parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description="""A post-install script for the pywin32 extensions. diff --git a/win32/Lib/sspi.py b/win32/Lib/sspi.py index d1e76e7f8b..79afa58393 100644 --- a/win32/Lib/sspi.py +++ b/win32/Lib/sspi.py @@ -372,7 +372,7 @@ def authorize(self, sec_buffer_in): sec_buffer = None client_step = 0 server_step = 0 - while not (sspiclient.authenticated) or len(sec_buffer[0].Buffer): + while not sspiclient.authenticated or (sec_buffer and len(sec_buffer[0].Buffer)): client_step += 1 err, sec_buffer = sspiclient.authorize(sec_buffer) print("Client step %s" % client_step)