Skip to content

Commit 2429d34

Browse files
authored
Support Python 3.14 and drop Python 3.8. (#839)
* Add a version bridge to `_post_coinit/unknwn.py` to accommodate the changes in `ctypes` in Python 3.14, including the deprecation of `_pointer_type_cache` and the introduction of the `__pointer_type__` attribute protocol. * Add a version bridge to `_meta.py` to accommodate the changes in `ctypes` in Python 3.14, including the deprecation of `_pointer_type_cache` and the introduction of the `__pointer_type__` attribute protocol. * Improve error message for `PyCArgObject` size check. * Update PyCArgObject.value for Python 3.14 compatibility. * Set packing for `PyCArgObject` on Python 3.14+. In Python 3.14, changes to the underlying C structures in `ctypes` require more explicit control over memory layout. This change enforces 8-byte alignment, which correctly pads the structure and ensures that the `value` field is accessed at the correct offset. This is critical for preventing memory corruption and maintaining compatibility on both 32-bit and 64-bit systems. * Update the autotest GHA workflow. * Remove `allow-prereleases: true` from `autotest.yml`. * Drop Python 3.8 support. * Add the version bridge for `test_comserver.TestInproc_win32com.test_eval`.
1 parent 2dc690a commit 2429d34

File tree

9 files changed

+91
-26
lines changed

9 files changed

+91
-26
lines changed

.github/workflows/autofmt.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Set up Python
1313
uses: actions/setup-python@v5
1414
with:
15-
python-version: 3.8
15+
python-version: 3.9
1616
- name: Install ruff
1717
run: pip install ruff==0.6.9
1818
- name: Check format

.github/workflows/autotest.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
matrix:
1212
os: [windows-latest]
13-
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
13+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1414
architecture: ['x86', 'x64']
1515
support: ['with 3rd parties', 'without 3rd parties']
1616
steps:
@@ -51,7 +51,7 @@ jobs:
5151
strategy:
5252
matrix:
5353
os: [windows-2025, windows-2022]
54-
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
54+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
5555
architecture: ['x86', 'x64']
5656
steps:
5757
- uses: actions/checkout@v4

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
`comtypes` allows you to define, call, and implement custom and dispatch-based COM interfaces in pure Python.
1313

14-
`comtypes` requires Windows and Python 3.8 or later.
14+
`comtypes` requires Windows and Python 3.9 or later.
15+
- Version [1.4.12](https://pypi.org/project/comtypes/1.4.12/) is the last version to support Python 3.8.
1516
- 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.
1617
- Version [1.4.6](https://pypi.org/project/comtypes/1.4.6/) is the last version to support Python 3.7.
1718
- 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.
18-
- `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.
1919
- 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.
2020

2121
## Installation

comtypes/_meta.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# comtypes._meta helper module
2+
import sys
23
from ctypes import POINTER, c_void_p, cast
34

45
import comtypes
@@ -74,17 +75,20 @@ def __new__(cls, name, bases, namespace):
7475
# Depending on a version or revision of Python, this may be essential.
7576
return self
7677

77-
PTR = _coclass_pointer_meta(
78+
p = _coclass_pointer_meta(
7879
f"POINTER({self.__name__})",
7980
(self, c_void_p),
8081
{
8182
"__ctypes_from_outparam__": _wrap_coclass,
8283
"from_param": classmethod(_coclass_from_param),
8384
},
8485
)
85-
from ctypes import _pointer_type_cache # type: ignore
86+
if sys.version_info >= (3, 14):
87+
self.__pointer_type__ = p
88+
else:
89+
from ctypes import _pointer_type_cache # type: ignore
8690

87-
_pointer_type_cache[self] = PTR
91+
_pointer_type_cache[self] = p
8892

8993
return self
9094

comtypes/_post_coinit/unknwn.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# https://learn.microsoft.com/en-us/windows/win32/api/unknwn/
22

33
import logging
4+
import sys
45
from ctypes import HRESULT, POINTER, byref, c_ulong, c_void_p
56
from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Type, TypeVar
67

@@ -126,9 +127,12 @@ def __new__(cls, name, bases, namespace):
126127
{"__com_interface__": self, "_needs_com_addref_": None},
127128
)
128129

129-
from ctypes import _pointer_type_cache # type: ignore
130+
if sys.version_info >= (3, 14):
131+
self.__pointer_type__ = p
132+
else:
133+
from ctypes import _pointer_type_cache # type: ignore
130134

131-
_pointer_type_cache[self] = p
135+
_pointer_type_cache[self] = p
132136

133137
if self._case_insensitive_:
134138
_meta_patch.case_insensitive(p)

comtypes/test/test_comserver.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import doctest
2+
import sys
23
import unittest
34
from ctypes import pointer
45
from typing import Any
@@ -140,6 +141,13 @@ class TestInproc_win32com(BaseServerTest, unittest.TestCase):
140141
def create_object(self):
141142
return Dispatch("TestComServerLib.TestComServer")
142143

144+
if sys.version_info >= (3, 14):
145+
146+
@unittest.skip("Fails occasionally with a memory leak on INPROC.")
147+
def test_eval(self):
148+
# This test sometimes leaks memory when run as an in-process server.
149+
pass
150+
143151
# These tests make no sense with win32com, override to disable them:
144152
@unittest.skip("This test make no sense with win32com.")
145153
def test_get_typeinfo(self):

comtypes/test/test_excel.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,7 @@ def test(self):
125125

126126
@unittest.skipIf(IMPORT_FAILED, "This depends on Excel.")
127127
@unittest.skipIf(
128-
sys.version_info[:2] == (3, 8)
129-
or sys.version_info[:2] == (3, 9)
128+
sys.version_info[:2] == (3, 9)
130129
or (sys.version_info[:2] == (3, 10) and sys.version_info < (3, 10, 10))
131130
or (sys.version_info[:2] == (3, 11) and sys.version_info < (3, 11, 2)),
132131
f"This fails in {PY_VER}. See https://github.com/enthought/comtypes/issues/212",

comtypes/util.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
and cast_field(struct, fieldname, fieldtype).
33
"""
44

5+
import sys
56
from ctypes import (
67
POINTER,
78
Structure,
@@ -15,6 +16,7 @@
1516
c_float,
1617
c_int,
1718
c_long,
19+
c_longdouble,
1820
c_longlong,
1921
c_short,
2022
c_void_p,
@@ -38,17 +40,61 @@ def _calc_offset():
3840
# The definition of PyCArgObject in C code (that is the type of
3941
# object that a byref() call returns):
4042
class PyCArgObject(Structure):
43+
if sys.version_info >= (3, 14):
44+
# While C compilers automatically determine appropriate
45+
# alignment based on field data types, `ctypes` requires
46+
# explicit control over memory layout.
47+
#
48+
# `_pack_ = 8` ensures 8-byte alignment for fields.
49+
#
50+
# This works on both 32-bit and 64-bit systems:
51+
# - On 64-bit systems, this matches the natural alignment
52+
# for pointers.
53+
# - On 32-bit systems, this is more strict than necessary
54+
# (4-byte would be enough), but still produces the
55+
# correct memory layout with proper padding.
56+
#
57+
# With `_pack_`, `ctypes` will automatically add padding
58+
# here to ensure proper alignment of the `value` field
59+
# after the `tag` and after the `size`.
60+
_pack_ = 8
61+
else:
62+
# No special packing needed for Python 3.13 and earlier
63+
# because the default alignment works fine for the legacy
64+
# structure.
65+
pass
66+
4167
class value(Union):
42-
_fields_ = [
43-
("c", c_char),
44-
("h", c_short),
45-
("i", c_int),
46-
("l", c_long),
47-
("q", c_longlong),
48-
("d", c_double),
49-
("f", c_float),
50-
("p", c_void_p),
51-
]
68+
if sys.version_info >= (3, 14):
69+
# In Python 3.14, the tagPyCArgObject structure was
70+
# modified to better support complex types.
71+
_fields_ = [
72+
("c", c_char),
73+
("b", c_char),
74+
("h", c_short),
75+
("i", c_int),
76+
("l", c_long),
77+
("q", c_longlong),
78+
("g", c_longdouble),
79+
("d", c_double),
80+
("f", c_float),
81+
("p", c_void_p),
82+
# arrays for real and imaginary of complex
83+
("D", c_double * 2),
84+
("F", c_float * 2),
85+
("G", c_longdouble * 2),
86+
]
87+
else:
88+
_fields_ = [
89+
("c", c_char),
90+
("h", c_short),
91+
("i", c_int),
92+
("l", c_long),
93+
("q", c_longlong),
94+
("d", c_double),
95+
("f", c_float),
96+
("p", c_void_p),
97+
]
5298

5399
#
54100
# Thanks to Lenard Lindstrom for this tip:
@@ -66,9 +112,13 @@ class value(Union):
66112
_anonymous_ = ["value"]
67113

68114
# additional checks to make sure that everything works as expected
69-
70-
if sizeof(PyCArgObject) != type(byref(c_int())).__basicsize__:
71-
raise RuntimeError("sizeof(PyCArgObject) invalid")
115+
expected_size = type(byref(c_int())).__basicsize__
116+
actual_size = sizeof(PyCArgObject)
117+
if actual_size != expected_size:
118+
raise RuntimeError(
119+
f"sizeof(PyCArgObject) mismatch: expected {expected_size}, "
120+
f"got {actual_size}."
121+
)
72122

73123
obj = c_int()
74124
ref = byref(obj)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ classifiers =
1818
Topic :: Software Development :: Libraries :: Python Modules
1919

2020
[options]
21-
python_requires = >=3.8
21+
python_requires = >=3.9
2222

2323
packages =
2424
comtypes

0 commit comments

Comments
 (0)