From 2b3b1a0bef1947d4d3a783b1bb2a1612652e2746 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 11 Apr 2024 14:19:49 -0400 Subject: [PATCH] Turn on strict typing in mypy (#2211) --- .editorconfig | 2 +- .github/workflows/main.yml | 4 +-- com/win32com/__init__.py | 2 +- mypy.ini | 61 +++++++++++++++++++++++++++----------- pyrightconfig.json | 8 ++--- pywin32_postinstall.py | 4 +-- setup.py | 30 +++++++++---------- win32/Lib/win32timezone.py | 10 ++++--- 8 files changed, 72 insertions(+), 49 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0534fe90db..1b4b1d2642 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,5 +15,5 @@ max_line_length = 88 # Same as Black [*.md] trim_trailing_whitespace = false -[*.{yaml,yml,toml}] +[*.{yaml,yml,json,toml}] indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f861a1e9a6..0f25111235 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -116,14 +116,14 @@ jobs: strategy: fail-fast: false matrix: - # mypy 1.5 dropped support for python 3.7 + # 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: pip install types-regex types-setuptools mypy==1.9 - run: mypy . --python-version=${{ matrix.python-version }} pyright: diff --git a/com/win32com/__init__.py b/com/win32com/__init__.py index 44ca3c2073..56d1b815cc 100644 --- a/com/win32com/__init__.py +++ b/com/win32com/__init__.py @@ -9,7 +9,7 @@ import win32api # flag if we are in a "frozen" build. -_frozen = getattr(sys, "frozen", 1 == 0) +_frozen = getattr(sys, "frozen", False) # pythoncom dumbly defaults this to zero - we believe sys.frozen over it. if _frozen and not getattr(pythoncom, "frozen", 0): pythoncom.frozen = sys.frozen diff --git a/mypy.ini b/mypy.ini index bf38efcf24..7f0506d85f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,33 +1,53 @@ [mypy] show_column_numbers = true -warn_unused_ignores = true -; Target the oldest supported version in editors -python_version = 3.7 +; Target the oldest supported version in editors and default CLI +; mypy 1.5 dropped support for Python 3.7 +python_version = 3.8 -strict = false +strict = true implicit_reexport = true +; Necessary to avoid "same module name" issues +explicit_package_bases = true +; Must specify top-level packages and scripts folders for mypy to work with explicit_package_bases +mypy_path = + $MYPY_CONFIG_FILE_DIR/com, + $MYPY_CONFIG_FILE_DIR/win32/Lib, + $MYPY_CONFIG_FILE_DIR/Pythonwin, + $MYPY_CONFIG_FILE_DIR/AutoDuck, + $MYPY_CONFIG_FILE_DIR/win32/scripts/VersionStamp, -; 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 +; TODO: Gradually type classes and functions until we can 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 +; Implicit return types ! 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) +disable_error_code = + ; Module has no attribute; (Dynamic modules will be hard to type without first-party stubs, ie: pythoncom.*) + attr-defined, + ; Class cannot subclass "..." (has type "Any"); (IDEM) + ; TODO: Use typeshed's types-pywin32 stubs after a few more fixes there + misc, + ; Name "..." is not defined; (IDEM, leave undefined/unbound to Flake8/Ruff/pyright) + name-defined, + ; Cannot assign to a method (we do lots of monkey patching) + method-assign, +exclude = (?x)( + ^build/ + ; Vendored + | ^Pythonwin/Scintilla/ + ; Forked IDLE extensions predating Python 2.3. They now live in idlelib in https://github.com/python/cpython/tree/main/Lib/idlelib + | ^Pythonwin/pywin/idle/ + ; TODO: adodbapi should be updated and fixed separatly + | ^adodbapi/ + ; TODO: Ignoring non-public APIs until all public API is typed + | ([Tt]est|[Dd]emos?)/ + ) ; C-modules that will need type-stubs -[mypy-adsi.*,dde,exchange,exchdapi,perfmon,servicemanager,win32api,win32console,win32clipboard,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,_win32sysloader,_winxptheme] +[mypy-adsi.*,dde,exchange,exchdapi,mapi,perfmon,servicemanager,win32api,win32console,win32clipboard,win32comext.adsi.adsi,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,_win32sysloader,_winxptheme] ignore_missing_imports = True ; verstamp is installed from win32verstamp.py called in setup.py @@ -36,3 +56,8 @@ ignore_missing_imports = True ; pywin32_system32 is an empty module created in setup.py to store dlls [mypy-verstamp,win32com.*,Test,pywin32_system32] ignore_missing_imports = True + +; Distutils being removed from stdlib currently causes some issues on Python 3.12 +; https://github.com/mhammond/pywin32/issues/2119 +[mypy-distutils.*] +ignore_missing_imports = True diff --git a/pyrightconfig.json b/pyrightconfig.json index 1dd371af23..5ea2fe2796 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,23 +1,23 @@ { "typeCheckingMode": "basic", - // Target the oldest supported version in editors + // Target the oldest supported version in editors and default CLI "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 + // Forked 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/", + // TODO: adodbapi should be updated and fixed separately + "adodbapi/", ], // 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 diff --git a/pywin32_postinstall.py b/pywin32_postinstall.py index 1b3f9ca57d..b1b9cdd214 100644 --- a/pywin32_postinstall.py +++ b/pywin32_postinstall.py @@ -188,9 +188,7 @@ 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( # pyright: ignore[reportOptionalMemberAccess] # We provide the loader, we know it won't be None - mod - ) + loader.exec_module(mod) def SetPyKeyVal(key_name, value_name, value): diff --git a/setup.py b/setup.py index f5b378f588..851a6889e1 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + build_id = "306" # may optionally include a ".{patchno}" suffix. __doc__ = """This is a distutils setup-script for the pywin32 extensions. @@ -44,6 +46,8 @@ from setuptools.command.install import install from setuptools.command.install_lib import install_lib +from distutils import ccompiler +from distutils._msvccompiler import MSVCCompiler from distutils.command.install_data import install_data if sys.version_info >= (3, 8): @@ -936,22 +940,17 @@ def my_new_compiler(**kw): # No way to cleanly wedge our compiler sub-class in. -from distutils import ccompiler -from distutils._msvccompiler import MSVCCompiler - orig_new_compiler = ccompiler.new_compiler -ccompiler.new_compiler = my_new_compiler - -base_compiler = MSVCCompiler +ccompiler.new_compiler = my_new_compiler # type: ignore[assignment] # Assuming the caller will always use only kwargs -class my_compiler(base_compiler): +class my_compiler(MSVCCompiler): # Just one GUIDS.CPP and it gives trouble on mainwin too. Maybe I # should just rename the file, but a case-only rename is likely to be # worse! This can probably go away once we kill the VS project files # though, as we can just specify the lowercase name in the module def. - _cpp_extensions = base_compiler._cpp_extensions + [".CPP"] - src_extensions = base_compiler.src_extensions + [".CPP"] + _cpp_extensions = MSVCCompiler._cpp_extensions + [".CPP"] + src_extensions = MSVCCompiler.src_extensions + [".CPP"] def link( self, @@ -1112,7 +1111,7 @@ def finalize_options(self): pch_header="PyWinTypes.h", ) -win32_extensions = [pywintypes] +win32_extensions: list[WinExt] = [pywintypes] win32_extensions.append( WinExt_win32( @@ -1254,11 +1253,10 @@ def finalize_options(self): windows_h_ver = info[2] if len(info) > 3: sources = info[3].split() - extra_compile_args = [] ext = WinExt_win32( name, libraries=lib_names, - extra_compile_args=extra_compile_args, + extra_compile_args=[], windows_h_version=windows_h_ver, sources=sources, ) @@ -1425,8 +1423,8 @@ def finalize_options(self): base_address=dll_base_address, ) dll_base_address += 0x80000 # pythoncom is large! -com_extensions = [pythoncom] -com_extensions += [ +com_extensions = [ + pythoncom, WinExt_win32com( "adsi", libraries="ACTIVEDS ADSIID user32 advapi32", @@ -2089,7 +2087,7 @@ def finalize_options(self): swig_include_files = "mapilib adsilib".split() -def expand_modules(module_dir: Union[str, os.PathLike]): +def expand_modules(module_dir: Union[str, os.PathLike[str]]): """Helper to allow our script specifications to include wildcards.""" return [str(path.with_suffix("")) for path in Path(module_dir).rglob("*.py")] @@ -2104,7 +2102,7 @@ def convert_data_files(files: Iterable[str]): for file in files: file = os.path.normpath(file) if file.find("*") >= 0: - files_use = ( + files_use = tuple( str(path) for path in Path(file).parent.rglob(os.path.basename(file)) # We never want CVS diff --git a/win32/Lib/win32timezone.py b/win32/Lib/win32timezone.py index dc41514e03..d5605a511b 100644 --- a/win32/Lib/win32timezone.py +++ b/win32/Lib/win32timezone.py @@ -1,4 +1,5 @@ # -*- coding: UTF-8 -*- +from __future__ import annotations """ win32timezone: @@ -231,7 +232,6 @@ datetime.datetime(2011, 11, 6, 1, 0, tzinfo=TimeZoneInfo('Pacific Standard Time')) """ -from __future__ import annotations import datetime import logging @@ -240,6 +240,7 @@ import struct import winreg from itertools import count +from typing import Dict import win32api @@ -792,8 +793,8 @@ def get_sorted_time_zones(key=None): return zones -class _RegKeyDict(dict): - def __init__(self, key): +class _RegKeyDict(Dict[str, int]): + def __init__(self, key: winreg.HKEYType): dict.__init__(self) self.key = key self.__load_values() @@ -907,7 +908,8 @@ def resolveMUITimeZone(spec): # from jaraco.util.dictlib 5.3.1 -class RangeMap(dict): +# TODO: Update to implementation in jaraco.collections +class RangeMap(dict): # type: ignore[type-arg] # Source code is untyped :/ TODO: Add generics! """ A dictionary-like object that uses the keys as bounds for a range. Inclusion of the value for that range is determined by the