diff --git a/.github/workflows/autofmt.yml b/.github/workflows/autofmt.yml index 6a96677a..bd2c2e4b 100644 --- a/.github/workflows/autofmt.yml +++ b/.github/workflows/autofmt.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install ruff run: pip install ruff==0.6.9 - name: Check format diff --git a/.github/workflows/autotest.yml b/.github/workflows/autotest.yml index 2e7ad7f4..0fb7ac20 100644 --- a/.github/workflows/autotest.yml +++ b/.github/workflows/autotest.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] architecture: ['x86', 'x64'] support: ['with 3rd parties', 'without 3rd parties'] steps: @@ -51,7 +51,7 @@ jobs: strategy: matrix: os: [windows-2025, windows-2022] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] architecture: ['x86', 'x64'] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index fedd4f78..3fad6ff5 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ `comtypes` allows you to define, call, and implement custom and dispatch-based COM interfaces in pure Python. -`comtypes` requires Windows and Python 3.8 or later. +`comtypes` requires Windows and Python 3.9 or later. +- Version [1.4.12](https://pypi.org/project/comtypes/1.4.12/) is the last version to support Python 3.8. - Version <= [1.4.7](https://pypi.org/project/comtypes/1.4.7/) does not work with Python 3.13 as reported in [GH-618](https://github.com/enthought/comtypes/issues/618). Version [1.4.8](https://pypi.org/project/comtypes/1.4.8/) can work with Python 3.13. - Version [1.4.6](https://pypi.org/project/comtypes/1.4.6/) is the last version to support Python 3.7. - Version [1.2.1](https://pypi.org/project/comtypes/1.2.1/) is the last version to support Python 2.7 and 3.3–3.6. -- `comtypes` does not work with Python 3.8.1 as reported in [GH-202](https://github.com/enthought/comtypes/issues/202). This bug has been fixed in Python >= 3.8.2. - Certain `comtypes` functions may not work correctly in Python 3.8 and 3.9 as reported in [GH-212](https://github.com/enthought/comtypes/issues/212). This bug has been fixed in Python >= 3.10.10 and >= 3.11.2. ## Installation diff --git a/comtypes/_meta.py b/comtypes/_meta.py index f3631582..844aecd9 100644 --- a/comtypes/_meta.py +++ b/comtypes/_meta.py @@ -1,4 +1,5 @@ # comtypes._meta helper module +import sys from ctypes import POINTER, c_void_p, cast import comtypes @@ -74,7 +75,7 @@ def __new__(cls, name, bases, namespace): # Depending on a version or revision of Python, this may be essential. return self - PTR = _coclass_pointer_meta( + p = _coclass_pointer_meta( f"POINTER({self.__name__})", (self, c_void_p), { @@ -82,9 +83,12 @@ def __new__(cls, name, bases, namespace): "from_param": classmethod(_coclass_from_param), }, ) - from ctypes import _pointer_type_cache # type: ignore + if sys.version_info >= (3, 14): + self.__pointer_type__ = p + else: + from ctypes import _pointer_type_cache # type: ignore - _pointer_type_cache[self] = PTR + _pointer_type_cache[self] = p return self diff --git a/comtypes/_post_coinit/unknwn.py b/comtypes/_post_coinit/unknwn.py index e11c0e91..f15dbf47 100644 --- a/comtypes/_post_coinit/unknwn.py +++ b/comtypes/_post_coinit/unknwn.py @@ -1,6 +1,7 @@ # https://learn.microsoft.com/en-us/windows/win32/api/unknwn/ import logging +import sys from ctypes import HRESULT, POINTER, byref, c_ulong, c_void_p from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Type, TypeVar @@ -126,9 +127,12 @@ def __new__(cls, name, bases, namespace): {"__com_interface__": self, "_needs_com_addref_": None}, ) - from ctypes import _pointer_type_cache # type: ignore + if sys.version_info >= (3, 14): + self.__pointer_type__ = p + else: + from ctypes import _pointer_type_cache # type: ignore - _pointer_type_cache[self] = p + _pointer_type_cache[self] = p if self._case_insensitive_: _meta_patch.case_insensitive(p) diff --git a/comtypes/test/test_comserver.py b/comtypes/test/test_comserver.py index c4bf0c53..13e4f9fc 100644 --- a/comtypes/test/test_comserver.py +++ b/comtypes/test/test_comserver.py @@ -1,4 +1,5 @@ import doctest +import sys import unittest from ctypes import pointer from typing import Any @@ -140,6 +141,13 @@ class TestInproc_win32com(BaseServerTest, unittest.TestCase): def create_object(self): return Dispatch("TestComServerLib.TestComServer") + if sys.version_info >= (3, 14): + + @unittest.skip("Fails occasionally with a memory leak on INPROC.") + def test_eval(self): + # This test sometimes leaks memory when run as an in-process server. + pass + # These tests make no sense with win32com, override to disable them: @unittest.skip("This test make no sense with win32com.") def test_get_typeinfo(self): diff --git a/comtypes/test/test_excel.py b/comtypes/test/test_excel.py index dfb90a83..16f752c9 100644 --- a/comtypes/test/test_excel.py +++ b/comtypes/test/test_excel.py @@ -125,8 +125,7 @@ def test(self): @unittest.skipIf(IMPORT_FAILED, "This depends on Excel.") @unittest.skipIf( - sys.version_info[:2] == (3, 8) - or sys.version_info[:2] == (3, 9) + sys.version_info[:2] == (3, 9) or (sys.version_info[:2] == (3, 10) and sys.version_info < (3, 10, 10)) or (sys.version_info[:2] == (3, 11) and sys.version_info < (3, 11, 2)), f"This fails in {PY_VER}. See https://github.com/enthought/comtypes/issues/212", diff --git a/comtypes/util.py b/comtypes/util.py index 155021f8..3d4c467a 100644 --- a/comtypes/util.py +++ b/comtypes/util.py @@ -2,6 +2,7 @@ and cast_field(struct, fieldname, fieldtype). """ +import sys from ctypes import ( POINTER, Structure, @@ -15,6 +16,7 @@ c_float, c_int, c_long, + c_longdouble, c_longlong, c_short, c_void_p, @@ -38,17 +40,61 @@ def _calc_offset(): # The definition of PyCArgObject in C code (that is the type of # object that a byref() call returns): class PyCArgObject(Structure): + if sys.version_info >= (3, 14): + # While C compilers automatically determine appropriate + # alignment based on field data types, `ctypes` requires + # explicit control over memory layout. + # + # `_pack_ = 8` ensures 8-byte alignment for fields. + # + # This works on both 32-bit and 64-bit systems: + # - On 64-bit systems, this matches the natural alignment + # for pointers. + # - On 32-bit systems, this is more strict than necessary + # (4-byte would be enough), but still produces the + # correct memory layout with proper padding. + # + # With `_pack_`, `ctypes` will automatically add padding + # here to ensure proper alignment of the `value` field + # after the `tag` and after the `size`. + _pack_ = 8 + else: + # No special packing needed for Python 3.13 and earlier + # because the default alignment works fine for the legacy + # structure. + pass + class value(Union): - _fields_ = [ - ("c", c_char), - ("h", c_short), - ("i", c_int), - ("l", c_long), - ("q", c_longlong), - ("d", c_double), - ("f", c_float), - ("p", c_void_p), - ] + if sys.version_info >= (3, 14): + # In Python 3.14, the tagPyCArgObject structure was + # modified to better support complex types. + _fields_ = [ + ("c", c_char), + ("b", c_char), + ("h", c_short), + ("i", c_int), + ("l", c_long), + ("q", c_longlong), + ("g", c_longdouble), + ("d", c_double), + ("f", c_float), + ("p", c_void_p), + # arrays for real and imaginary of complex + ("D", c_double * 2), + ("F", c_float * 2), + ("G", c_longdouble * 2), + ] + else: + _fields_ = [ + ("c", c_char), + ("h", c_short), + ("i", c_int), + ("l", c_long), + ("q", c_longlong), + ("d", c_double), + ("f", c_float), + ("p", c_void_p), + ] # # Thanks to Lenard Lindstrom for this tip: @@ -66,9 +112,13 @@ class value(Union): _anonymous_ = ["value"] # additional checks to make sure that everything works as expected - - if sizeof(PyCArgObject) != type(byref(c_int())).__basicsize__: - raise RuntimeError("sizeof(PyCArgObject) invalid") + expected_size = type(byref(c_int())).__basicsize__ + actual_size = sizeof(PyCArgObject) + if actual_size != expected_size: + raise RuntimeError( + f"sizeof(PyCArgObject) mismatch: expected {expected_size}, " + f"got {actual_size}." + ) obj = c_int() ref = byref(obj) diff --git a/setup.cfg b/setup.cfg index 7008bacb..bb98b1c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules [options] -python_requires = >=3.8 +python_requires = >=3.9 packages = comtypes