From 153a5fc836cb98fdb887e97254b0ede9975b689d Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 17 Mar 2024 23:56:35 -0400 Subject: [PATCH 1/3] Turn on strict typing in mypy --- .editorconfig | 2 +- .github/workflows/main.yml | 4 +-- com/win32com/__init__.py | 2 +- mypy.ini | 53 ++++++++++++++++++++++++++------------ pyrightconfig.json | 2 +- pywin32_postinstall.py | 4 +-- setup.py | 30 ++++++++++----------- win32/Lib/win32timezone.py | 10 ++++--- 8 files changed, 63 insertions(+), 44 deletions(-) diff --git a/.editorconfig b/.editorconfig index e6c2225313..d3d336adba 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,5 +15,5 @@ max_line_length = 88 # Same as Black [*.md] trim_trailing_whitespace = false -[*.{yaml,yml}] +[*.{yaml,yml,json}] 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 4832d9e1ff..8cd606fe2c 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 9055b84925..ce3d37d82a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,30 +1,46 @@ [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 -; 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/ + ; Pythonwin/pywin/idle is 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/ + ; Duplicate module named "rasutil" and "setup", short-term fix is to ignore + |^win32/scripts/rasutil.py$ + |^win32/Demos/c_extension/setup.py + ; 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,win32clipboard,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,wincerapi,winxpgui,_win32sysloader,_winxptheme] @@ -36,3 +52,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..ca666d62d2 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,6 +1,6 @@ { "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, diff --git a/pywin32_postinstall.py b/pywin32_postinstall.py index 5a4981edc6..039a8fbc7f 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 4181b0f4d0..a7dc77f064 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): @@ -967,22 +971,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, @@ -1146,7 +1145,7 @@ def finalize_options(self): pch_header="PyWinTypes.h", ) -win32_extensions = [pywintypes] +win32_extensions: list[WinExt] = [pywintypes] win32_extensions.append( WinExt_win32( @@ -1288,11 +1287,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, ) @@ -1472,8 +1470,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", @@ -2136,7 +2134,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")] @@ -2151,7 +2149,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 From 41e91fc3101db0f4846b5346983488d83c413c37 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 30 Mar 2024 11:54:17 -0400 Subject: [PATCH 2/3] Use explicit_package_bases --- mypy.ini | 37 +++++++++++++++++++++---------------- pyrightconfig.json | 6 +++--- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/mypy.ini b/mypy.ini index ce3d37d82a..28fe24cca8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,12 +1,21 @@ [mypy] show_column_numbers = true -warn_unused_ignores = true ; Target the oldest supported version in editors and default CLI ; mypy 1.5 dropped support for Python 3.7 python_version = 3.8 strict = true +warn_unused_ignores = 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, ; 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 @@ -26,24 +35,20 @@ disable_error_code = name-defined, ; Cannot assign to a method (we do lots of monkey patching) method-assign, - exclude = (?x)( - ^build/ - ; Vendored - |^Pythonwin/Scintilla/ - ; Pythonwin/pywin/idle is 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/ - ; Duplicate module named "rasutil" and "setup", short-term fix is to ignore - |^win32/scripts/rasutil.py$ - |^win32/Demos/c_extension/setup.py - ; TODO: adodbapi should be updated and fixed separatly - |^adodbapi/ - ; TODO: Ignoring non-public APIs until all public API is typed - |([Tt]est|[Dd]emos?)/ - ) + ^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,win32clipboard,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,wincerapi,winxpgui,_win32sysloader,_winxptheme] +[mypy-adsi.*,dde,exchange,exchdapi,mapi,perfmon,servicemanager,win32api,win32clipboard,win32comext.adsi.adsi,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 diff --git a/pyrightconfig.json b/pyrightconfig.json index ca666d62d2..5ea2fe2796 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -7,17 +7,17 @@ // 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 From ec6dd8f2203b54f1a0a710f982b5b4f17b8d4ace Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 1 Apr 2024 00:42:53 -0400 Subject: [PATCH 3/3] warn_unused_ignores redundant with strict --- mypy.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 22bbd09f23..1c16a96d50 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,6 @@ show_column_numbers = true python_version = 3.8 strict = true -warn_unused_ignores = true implicit_reexport = true ; Necessary to avoid "same module name" issues explicit_package_bases = true