diff --git a/.github/issue_template.md b/.github/issue_template.md index 733bf9343..601614eae 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -9,7 +9,7 @@ For all bugs, please provide the following information: ## Expected behavior and actual behavior ## Steps to reproduce the problem - + 1. ... 2. ... diff --git a/.github/workflows/download-arm64-libs.py b/.github/workflows/download-arm64-libs.py index 51c5bcd02..841673f98 100644 --- a/.github/workflows/download-arm64-libs.py +++ b/.github/workflows/download-arm64-libs.py @@ -28,16 +28,16 @@ VERSION += f"-rc{sys.version_info.serial}" URL = f"https://www.nuget.org/api/v2/package/pythonarm64/{VERSION}" -PATH = dest / f"pythonarm64.{VERSION}.zip" +DEST_PATH = dest / f"pythonarm64.{VERSION}.zip" -if PATH.is_file(): - print("Skipping download because", PATH, "exists") +if DEST_PATH.is_file(): + print("Skipping download because", DEST_PATH, "exists") else: print("Downloading", URL) - urlretrieve(URL, PATH) - print("Downloaded", PATH) + urlretrieve(URL, DEST_PATH) + print("Downloaded", DEST_PATH) -with ZipFile(PATH, "r") as zf: +with ZipFile(DEST_PATH, "r") as zf: for name in zf.namelist(): zip_path = pathlib.PurePath(name) if zip_path.parts[:2] == ("tools", "libs"): diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c10850d81..0a7314c70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - # python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + # python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] # architecture: ["x64", "x86"] # free-threaded: [false] include: @@ -56,7 +56,7 @@ jobs: run: | python --version pip --version - pip install --upgrade setuptools wheel + pip install --upgrade setuptools>=74 wheel - name: Build and install run: | @@ -124,7 +124,7 @@ jobs: run: | python --version pip --version - pip install --upgrade setuptools wheel + pip install --upgrade setuptools>=74 wheel - name: Obtain ARM64 library files run: | @@ -172,7 +172,6 @@ jobs: # 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", "3.13"] # steps: # - uses: actions/checkout@v4 @@ -191,7 +190,7 @@ jobs: # strategy: # fail-fast: false # matrix: - # python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + # python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] # steps: # - uses: actions/checkout@v4 # - uses: actions/setup-python@v5 diff --git a/CHANGES.txt b/CHANGES.txt index a88445e9b..d8978afd5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,6 +14,10 @@ https://mhammond.github.io/pywin32_installers.html. Coming in build 309, as yet unreleased -------------------------------------- +* Dropped support for Python 3.7 (#2207, @Avasam) +* Implement record pointers as [in, out] method parameters of a Dispatch Interface (#2310) +* Fix memory leak converting to PyObject from some SAFEARRAY elements (#2316) +* Fix bug where makepy support was unnecessarily generated (#2354, #2353, @geppi) * Fail sooner on invalid `win32timezone.TimeZoneInfo` creation (#2338, @Avasam) * Removed temporary `win32com.server.policy` reexports hack (#2344, @Avasam) Import `DispatcherWin32trace` and `DispatcherTrace` from `win32com.server.dispatcher` instead. diff --git a/Pythonwin/pywin/test/test_pywin.py b/Pythonwin/pywin/test/test_pywin.py index 6cf973a92..49acc9d5e 100644 --- a/Pythonwin/pywin/test/test_pywin.py +++ b/Pythonwin/pywin/test/test_pywin.py @@ -47,8 +47,7 @@ def setUpClass(cls): def _restore_oe(): sys.stdout, sys.stderr = cls.std_oe_orig - if sys.version_info >= (3, 8): - cls.addClassCleanup(_restore_oe) + cls.addClassCleanup(_restore_oe) sys.argv[1:] = ["/new", src_dir + "\\_dbgscript.py"] if not _indebugger: thisApp.InitInstance() @@ -65,7 +64,7 @@ def tearDownClass(cls): win32api.PostQuitMessage() win32gui.PumpWaitingMessages() cls.app.ExitInstance() - sys.stdout, sys.stderr = cls.std_oe_orig # py3.7 + sys.stdout, sys.stderr = cls.std_oe_orig def test_1_pydocs_and_finddlg(self): mf = win32ui.GetMainFrame() diff --git a/build_all.bat b/build_all.bat index 536aee00d..ec8cbbaa2 100644 --- a/build_all.bat +++ b/build_all.bat @@ -1,7 +1,3 @@ -py -3.7-32 setup.py -q build -@if errorlevel 1 goto failed -py -3.7 setup.py -q build -@if errorlevel 1 goto failed py -3.8-32 setup.py -q build @if errorlevel 1 goto failed py -3.8 setup.py -q build diff --git a/build_env.md b/build_env.md index d9c55ac10..4f616bf99 100644 --- a/build_env.md +++ b/build_env.md @@ -149,7 +149,6 @@ from the pywin32 directory. - Update `setuptools` and set the following environment variables to ensure it is used: ```shell - set SETUPTOOLS_USE_DISTUTILS=1 set DISTUTILS_USE_SDK=1 ``` diff --git a/com/TestSources/PyCOMTest/PyCOMImpl.cpp b/com/TestSources/PyCOMTest/PyCOMImpl.cpp index a4616fef0..d58c00f99 100644 --- a/com/TestSources/PyCOMTest/PyCOMImpl.cpp +++ b/com/TestSources/PyCOMTest/PyCOMImpl.cpp @@ -618,6 +618,14 @@ HRESULT CPyCOMTest::GetStruct(TestStruct1 *ret) *ret = r; return S_OK; } + +HRESULT CPyCOMTest::ModifyStruct(TestStruct1 *prec) +{ + prec->int_value = 100; + prec->str_value = SysAllocString(L"Nothing is as constant as change"); + return S_OK; +} + HRESULT CPyCOMTest::DoubleString(BSTR in, BSTR *out) { *out = SysAllocStringLen(NULL, SysStringLen(in) * 2); diff --git a/com/TestSources/PyCOMTest/PyCOMImpl.h b/com/TestSources/PyCOMTest/PyCOMImpl.h index e285268f1..50eb8dd0d 100644 --- a/com/TestSources/PyCOMTest/PyCOMImpl.h +++ b/com/TestSources/PyCOMTest/PyCOMImpl.h @@ -119,6 +119,8 @@ class CPyCOMTest : public IDispatchImpl= (3, 8): - from setuptools.modified import newer - else: - from distutils.dep_util import newer + from setuptools.modified import newer this_dir = os.path.dirname(__file__) idl = os.path.abspath(os.path.join(this_dir, "pippo.idl")) diff --git a/com/win32com/test/testPyComTest.py b/com/win32com/test/testPyComTest.py index 6b9b7f792..dbe933669 100644 --- a/com/win32com/test/testPyComTest.py +++ b/com/win32com/test/testPyComTest.py @@ -198,6 +198,18 @@ def TestCommon(o, is_generated): progress("Checking structs") r = o.GetStruct() assert r.int_value == 99 and str(r.str_value) == "Hello from C++" + # Dynamic does not support struct byref as [ in, out ] parameters + if hasattr(o, "CLSID"): + progress("Checking struct byref as [ in, out ] parameter") + mod_r = o.ModifyStruct(r) + # We expect the input value to stay unchanged + assert r.int_value == 99 and str(r.str_value) == "Hello from C++" + # and the return value to reflect the modifications performed on the COM server side + assert ( + mod_r.int_value == 100 + and str(mod_r.str_value) == "Nothing is as constant as change" + ) + assert o.DoubleString("foo") == "foofoo" progress("Checking var args") diff --git a/make_all.bat b/make_all.bat index c6d6a2b33..05855ddfb 100644 --- a/make_all.bat +++ b/make_all.bat @@ -13,11 +13,6 @@ rem Now the binaries. rem Check /build_env.md#build-environment to make sure you have all the required components installed rem (bdist_wininst needs --target-version to name the installers correctly!) -py -3.7-32 setup.py -q bdist_wininst --skip-build --target-version=3.7 -py -3.7-32 setup.py -q bdist_wheel --skip-build -py -3.7 setup.py -q bdist_wininst --skip-build --target-version=3.7 -py -3.7 setup.py -q bdist_wheel --skip-build - py -3.8-32 setup.py -q bdist_wininst --skip-build --target-version=3.8 py -3.8-32 setup.py -q bdist_wheel --skip-build py -3.8 setup.py -q bdist_wininst --skip-build --target-version=3.8 diff --git a/mypy.ini b/mypy.ini index bd1cd4316..e4676e0c7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,6 @@ [mypy] show_column_numbers = 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 diff --git a/pyrightconfig.json b/pyrightconfig.json index 1721da1a2..d96b3fc9d 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,7 +1,7 @@ { "typeCheckingMode": "basic", // Target the oldest supported version in editors and default CLI - "pythonVersion": "3.7", + "pythonVersion": "3.8", // Keep it simple for now by allowing both mypy and pyright to use `type: ignore` "enableTypeIgnoreComments": true, // Exclude from scanning when running pyright diff --git a/ruff.toml b/ruff.toml index 603043e8f..eab7fcce1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py37" # Target the oldest supported version +target-version = "py38" # Target the oldest supported version in editors and default CLI [lint] select = [ diff --git a/setup.py b/setup.py index ce0120e24..e25465fd7 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ from setuptools.command.build_ext import build_ext from setuptools.command.install import install from setuptools.command.install_lib import install_lib +from setuptools.modified import newer_group from tempfile import gettempdir from typing import Iterable @@ -47,11 +48,6 @@ from distutils._msvccompiler import MSVCCompiler from distutils.command.install_data import install_data -if sys.version_info >= (3, 8): - from setuptools.modified import newer_group -else: - from distutils.dep_util import newer_group - build_id_patch = build_id if not "." in build_id_patch: build_id_patch += ".0" @@ -868,12 +864,6 @@ def run(self): install.run(self) # Custom script we run at the end of installing - this is the same script # run by bdist_wininst - # This child process won't be able to install the system DLLs until our - # process has terminated (as distutils imports win32api!), so we must use - # some 'no wait' executor - spawn seems fine! We pass the PID of this - # process so the child will wait for us. - # XXX - hmm - a closer look at distutils shows it only uses win32api - # if _winreg fails - and this never should. Need to revisit this! # If self.root has a value, it means we are being "installed" into # some other directory than Python itself (eg, into a temp directory # for bdist_wininst to use) - in which case we must *not* run our @@ -885,7 +875,8 @@ def run(self): if not os.path.isfile(filename): raise RuntimeError(f"Can't find '{filename}'") print("Executing post install script...") - # What executable to use? This one I guess. + # As of setuptools>=74.0.0, we no longer need to + # be concerned about distutils calling win32api subprocess.Popen( [ sys.executable, @@ -905,8 +896,8 @@ def install(self): # This is crazy - in setuptools 61.1.0 (and probably some earlier versions), the # install_lib and build comments don't agree on where the .py files to install can # be found, so we end up with a warning logged: - # `warning: my_install_lib: 'build\lib.win-amd64-3.7' does not exist -- no Python modules to install` - # (because they are actually in `build\lib.win-amd64-cpython-37`!) + # `warning: my_install_lib: 'build\lib.win-amd64-3.8' does not exist -- no Python modules to install` + # (because they are actually in `build\lib.win-amd64-cpython-38`!) # It's not an error though, so we end up with .exe installers lacking our lib files! builder = self.get_finalized_command("build") if os.path.isdir(builder.build_platlib) and not os.path.isdir(self.build_dir): @@ -2151,7 +2142,6 @@ def convert_optional_data_files(files): "Intended Audience :: Developers", "License :: OSI Approved :: Python Software Foundation License", "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/win32/Lib/pywin32_bootstrap.py b/win32/Lib/pywin32_bootstrap.py index fbc4f7be4..c9f59a56d 100644 --- a/win32/Lib/pywin32_bootstrap.py +++ b/win32/Lib/pywin32_bootstrap.py @@ -4,8 +4,6 @@ # In short, there's a directory installed by pywin32 named 'pywin32_system32' # with some important DLLs which need to be found by Python when some pywin32 # modules are imported. -# If Python has `os.add_dll_directory()`, we need to call it with this path. -# Otherwise, we add this path to PATH. try: @@ -19,11 +17,5 @@ # https://docs.python.org/3/reference/import.html#__path__ for path in pywin32_system32.__path__: if os.path.isdir(path): - if hasattr(os, "add_dll_directory"): - os.add_dll_directory(path) - # This is to ensure the pywin32 path is in the beginning to find the - # pywin32 DLLs first and prevent other PATH entries to shadow them - elif not os.environ["PATH"].startswith(path): - os.environ["PATH"] = os.environ["PATH"].replace(os.pathsep + path, "") - os.environ["PATH"] = path + os.pathsep + os.environ["PATH"] + os.add_dll_directory(path) break diff --git a/win32/Lib/win32gui_struct.py b/win32/Lib/win32gui_struct.py index 945ededc9..41ef815fd 100644 --- a/win32/Lib/win32gui_struct.py +++ b/win32/Lib/win32gui_struct.py @@ -952,6 +952,8 @@ def UnpackDEV_BROADCAST(lparam): _, _, _, x["unitmask"], x["flags"] = struct.unpack( fmt, buf[: struct.calcsize(fmt)] ) + elif devtype == win32con.DBT_DEVTYP_PORT: + x["name"] = win32gui.PyGetString(lparam + struct.calcsize(hdr_format)) else: raise NotImplementedError("unknown device type %d" % (devtype,)) return DEV_BROADCAST_INFO(devtype, **extra) diff --git a/win32/Lib/win32timezone.py b/win32/Lib/win32timezone.py index 53107542d..fdd71c855 100644 --- a/win32/Lib/win32timezone.py +++ b/win32/Lib/win32timezone.py @@ -642,11 +642,27 @@ def __str__(self): return self.displayName def tzname(self, dt): - winInfo = self.getWinInfo(dt) - if self.dst(dt) == winInfo.daylight_bias: + """ + >>> MST = TimeZoneInfo('Mountain Standard Time') + >>> MST.tzname(datetime.datetime(2003, 8, 2)) + 'Mountain Daylight Time' + >>> MST.tzname(datetime.datetime(2003, 11, 25)) + 'Mountain Standard Time' + """ + + winInfo = self.getWinInfo(dt.year) + if self.dst(dt) == -winInfo.daylight_bias: result = self.daylightName - elif self.dst(dt) == winInfo.standard_bias: + elif self.dst(dt) == -winInfo.standard_bias: result = self.standardName + else: + raise ValueError( + "Unexpected daylight bias", + dt, + self.dst(dt), + winInfo.daylight_bias, + winInfo.standard_bias, + ) return result def getWinInfo(self, targetYear): diff --git a/win32/src/_win32sysloader.cpp b/win32/src/_win32sysloader.cpp index 0a9e4dcac..9a9ef7c27 100644 --- a/win32/src/_win32sysloader.cpp +++ b/win32/src/_win32sysloader.cpp @@ -52,13 +52,7 @@ static PyObject *PyLoadModule(PyObject *self, PyObject *args) if (!modName) return NULL; - // Python 3.7 vs 3.8 use different flags for LoadLibraryEx and we match them. - // See github issue 1787. -#if (PY_VERSION_HEX < 0x03080000) - HINSTANCE hinst = LoadLibraryEx(modName, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); -#else HINSTANCE hinst = LoadLibraryEx(modName, NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); -#endif PyMem_Free(modName); if (hinst == NULL) { Py_INCREF(Py_None); diff --git a/win32/src/win32tsmodule.cpp b/win32/src/win32tsmodule.cpp index c4156f9f5..ddd383dee 100644 --- a/win32/src/win32tsmodule.cpp +++ b/win32/src/win32tsmodule.cpp @@ -409,6 +409,9 @@ static PyObject *PyWTSQuerySessionInformation(PyObject *self, PyObject *args, Py case WTSConnectState: // @flag WTSConnectState|Int, from WTS_CONNECTSTATE_CLASS ret = PyLong_FromLong(*(INT *)buf); break; + case WTSIsRemoteSession: // @flag WTSIsRemoteSession|Boolean + ret = PyBool_FromLong(*(BYTE *)buf); + break; case WTSClientDisplay: { // @flag WTSClientDisplay|Dict containing client's display settings WTS_CLIENT_DISPLAY *wcd = (WTS_CLIENT_DISPLAY *)buf; ret = Py_BuildValue("{s:k, s:k, s:k}", "HorizontalResolution", wcd->HorizontalResolution, @@ -768,6 +771,7 @@ PYWIN_MODULE_INIT_FUNC(win32ts) PyModule_AddIntConstant(module, "WTSClientAddress", WTSClientAddress); PyModule_AddIntConstant(module, "WTSClientDisplay", WTSClientDisplay); PyModule_AddIntConstant(module, "WTSClientProtocolType", WTSClientProtocolType); + PyModule_AddIntConstant(module, "WTSIsRemoteSession", WTSIsRemoteSession); // WTS_CONFIG_CLASS PyModule_AddIntConstant(module, "WTSUserConfigInitialProgram", WTSUserConfigInitialProgram); diff --git a/win32/test/test_win32ts.py b/win32/test/test_win32ts.py new file mode 100644 index 000000000..712b99272 --- /dev/null +++ b/win32/test/test_win32ts.py @@ -0,0 +1,19 @@ +# Tests for win32ts module + +import unittest + +import win32ts + + +class Win32TsTestCase(unittest.TestCase): + def test_is_remote_session(self): + ret = win32ts.WTSQuerySessionInformation( + win32ts.WTS_CURRENT_SERVER_HANDLE, + win32ts.WTS_CURRENT_SESSION, + win32ts.WTSIsRemoteSession, + ) + self.assertIsInstance(ret, bool) + + +if __name__ == "__main__": + unittest.main()