Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/autotest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15']
architecture: ['x86', 'x64']
support: ['with 3rd parties', 'without 3rd parties']
steps:
Expand All @@ -20,6 +20,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
allow-prereleases: true
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Build and register the OutProc COM server
Expand Down Expand Up @@ -51,7 +52,7 @@ jobs:
strategy:
matrix:
os: [windows-2025, windows-2022]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15']
architecture: ['x86', 'x64']
steps:
- uses: actions/checkout@v6
Expand All @@ -60,6 +61,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
allow-prereleases: true
- name: install comtypes
run: |
pip install --upgrade build
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@

`comtypes` requires Windows and Python 3.9 or later.

- **Note about Python 3.15 and `enum` behavior**
Starting with Python 3.15, the internal handling of `IntFlag`(`Flag`) values is planned to change:
**Negative `IntFlag` members will be reinterpreted by masking them to the defined positive bit domain, instead of keeping their original negative literal values**.
This can affect enumeration types generated by `comtypes` from COM type libraries. Action is needed to maintain literal evaluation.
For details and ongoing discussion, see: [GH-894](https://github.com/enthought/comtypes/issues/894).
- Version <= [1.4.17](https://pypi.org/project/comtypes/1.4.17/) has issues with enum behavior on Python 3.15 as reported in [GH-894](https://github.com/enthought/comtypes/issues/894). Version 1.5.0 will support Python 3.15.
- 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.
Expand Down
16 changes: 0 additions & 16 deletions comtypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,6 @@
import logging
import sys

if sys.version_info >= (3, 15):
import warnings

_PYVER = f"{sys.version_info.major}.{sys.version_info.minor}"
warnings.warn(
(
f"You are running 'comtypes' on Python {_PYVER}, where the behavior of "
"enum types (such as IntFlag) may differ from Python <= 3.14.\n"
f"It is recommended to use a version compatible with Python {_PYVER}.\n"
"See: https://github.com/enthought/comtypes/issues/894"
),
FutureWarning,
stacklevel=2,
)


# HACK: Workaround for projects that depend on this package
# There should be several projects around the world that depend on this package
# and indirectly reference the symbols of `ctypes` from `comtypes`.
Expand Down
20 changes: 20 additions & 0 deletions comtypes/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,26 @@ def test_munged_definitions(self):
self.assertEqual(consts.MSVidCCService.None_, consts.None_)
self.assertEqual(MSVidCtlLib.None_, consts.None_)

def test_enum_base_classes(self):
"""Test that enums with negative values are generated as IntEnum,
and enums with only non-negative values are generated as IntFlag."""
from enum import IntEnum, IntFlag

# MsiInstallState in msi.dll contains negative values, so it should be
# an IntEnum to preserve the values in Python 3.15+.
# See https://github.com/enthought/comtypes/issues/894
msi_module = comtypes.client.GetModule("msi.dll")
MsiInstallState = msi_module.MsiInstallState
self.assertTrue(issubclass(MsiInstallState, IntEnum))
self.assertFalse(issubclass(MsiInstallState, IntFlag))

# OLE_TRISTATE in stdole2.tlb contains only 0, 1, 2, so it can be
# an IntFlag.
stdole_module = comtypes.client.GetModule("stdole2.tlb")
OLE_TRISTATE = stdole_module.OLE_TRISTATE
self.assertTrue(issubclass(OLE_TRISTATE, IntFlag))
self.assertFalse(issubclass(OLE_TRISTATE, IntEnum))


if __name__ == "__main__":
ut.main()
21 changes: 1 addition & 20 deletions comtypes/test/test_util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
import unittest
from ctypes import (
POINTER,
Expand All @@ -14,25 +13,7 @@
sizeof,
)

PY_3_15_ALPHA_BETA = (
sys.version_info.major == 3
and sys.version_info.minor == 15
and sys.version_info.releaselevel in ("alpha", "beta")
)

try:
import comtypes.util
except RuntimeError as e:
SKIP_MSG = (
"Starting from Python 3.15, PyCArgObject layout is changed. "
"See https://github.com/enthought/comtypes/issues/938."
)
if PY_3_15_ALPHA_BETA:

def setUpModule():
raise unittest.SkipTest(SKIP_MSG)
else:
raise e
import comtypes.util
from comtypes import GUID, CoCreateInstance, IUnknown, shelllink


Expand Down
6 changes: 4 additions & 2 deletions comtypes/tools/codegenerator/codegenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,16 @@ def generate_friendly_code(self, modname: str) -> str:
Such as "comtypes.gen.stdole" and "comtypes.gen.Excel".
"""
output = io.StringIO()
print("from enum import IntFlag", file=output)
enumcode, enumbases = self.enums.to_enums()
if enumbases:
print(f"from enum import {', '.join(sorted(list(enumbases)))}", file=output)
print(file=output)
print(f"import {modname} as __wrapper_module__", file=output)
print(self._make_friendly_module_import_part(modname), file=output)
print(file=output)
print(file=output)
if self.enums:
print(self.enums.to_intflags(), file=output)
print(enumcode, file=output)
print(file=output)
print(file=output)
if self.enum_aliases:
Expand Down
36 changes: 29 additions & 7 deletions comtypes/tools/codegenerator/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,12 @@ def add(self, enum_name: str, member_name: str, value: int) -> None:
The 'egg' member of the 'Bar' enumeration is already assigned 4,
but it will be overwritten with 5,
based on the type information.
>>> enums.add('Baz', 'mix', -1)
>>> enums.add('Baz', 'juice', 0)
>>> assert 'Foo' in enums
>>> assert 'Baz' not in enums
>>> print(enums.to_intflags())
>>> assert 'Qux' not in enums
>>> enumcode, enumbases = enums.to_enums()
>>> print(enumcode)
class Foo(IntFlag):
ham = 1
spam = 2
Expand All @@ -194,6 +197,13 @@ class Bar(IntFlag):
bacon = 3
# egg = 4 # duplicated. Perhaps there is a bug in the type library?
egg = 5 # duplicated. Perhaps there is a bug in the type library?
<BLANKLINE>
<BLANKLINE>
class Baz(IntEnum):
mix = -1
juice = 0
>>> sorted(list(enumbases))
['IntEnum', 'IntFlag']
>>> print(enums.to_constants())
# values for enumeration 'Foo'
ham = 1
Expand All @@ -205,6 +215,11 @@ class Bar(IntFlag):
egg = 4 # duplicated within the 'Bar'. Perhaps there is a bug?
egg = 5 # duplicated within the 'Bar'. Perhaps there is a bug?
Bar = c_int # enum
<BLANKLINE>
# values for enumeration 'Baz'
mix = -1
juice = 0
Baz = c_int # enum
"""
members = self.data.setdefault(enum_name, [])
if members:
Expand Down Expand Up @@ -261,11 +276,12 @@ def to_constants(self) -> str:
blocks.append("\n".join(lines))
return "\n\n".join(blocks)

def to_intflags(self) -> str:
blocks = []
def to_enums(self) -> tuple[str, set[str]]:
enumbases: set[str] = set()
blocks: list[str] = []
for enum_name, members in self._iter_items():
has_negative = False
lines = []
lines.append(f"class {enum_name}(IntFlag):")
for member_name, member_value, is_dupl, rest_dupl_count in members:
definition = f"{member_name} = {member_value}"
if is_dupl:
Expand All @@ -279,5 +295,11 @@ def to_intflags(self) -> str:
lines.append(f" {base_line}")
else:
lines.append(f" {definition}")
blocks.append("\n".join(lines))
return "\n\n\n".join(blocks)
if member_value < 0:
has_negative = True
# Preventing the range-masking in Python 3.15
# See https://github.com/enthought/comtypes/issues/894
base_class = "IntEnum" if has_negative else "IntFlag"
enumbases.add(base_class)
blocks.append("\n".join([f"class {enum_name}({base_class}):"] + lines))
return "\n\n\n".join(blocks), enumbases
13 changes: 11 additions & 2 deletions comtypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
byref,
c_byte,
c_char,
c_char_p,
c_double,
c_float,
c_int,
c_long,
c_longdouble,
c_longlong,
c_short,
c_ssize_t,
c_void_p,
cast,
sizeof,
Expand All @@ -32,6 +34,13 @@
_T = TypeVar("_T")
_CT = TypeVar("_CT", bound="_CData")

if sys.version_info >= (3, 15):
_TAG_TYPE = c_char_p
_SIZE_TYPE = c_ssize_t
else:
_TAG_TYPE = c_char
_SIZE_TYPE = c_int


def _calc_offset():
# Internal helper function that calculates where the object
Expand Down Expand Up @@ -103,10 +112,10 @@ class value(Union):
_fields_ = [
("PyObject_HEAD", c_byte * object.__basicsize__),
("pffi_type", c_void_p),
("tag", c_char),
("tag", _TAG_TYPE),
("value", value),
("obj", c_void_p),
("size", c_int),
("size", _SIZE_TYPE),
]

_anonymous_ = ["value"]
Expand Down
29 changes: 28 additions & 1 deletion docs/source/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,8 @@ from COM typelibraries.
abstracted alias ``__wrapper_module__``, also imports interface
classes, coclasses, constants, and structures from the wrapper
module, and defines enumerations from typeinfo of the typelibrary
using `enum.IntFlag <https://docs.python.org/3/library/enum.html#enum.IntFlag>`_.
using `enum.IntFlag <https://docs.python.org/3/library/enum.html#enum.IntFlag>`_
or `enum.IntEnum <https://docs.python.org/3/library/enum.html#enum.IntEnum>`_.
The friendly module can be imported easier than the wrapper
module because the module name is easier to type and read.

Expand Down Expand Up @@ -600,6 +601,20 @@ from COM typelibraries.
+ from comtypes.gen.friendlymodule import __wrapper_module__ as mod
c_int_alias = mod.TheName

*Changed in version 1.5.0*: The friendly module defines
enumerations containing negative member values using
`enum.IntEnum <https://docs.python.org/3/library/enum.html#enum.IntEnum>`_,
while enumerations with only non-negative member values continue to
use `enum.IntFlag <https://docs.python.org/3/library/enum.html#enum.IntFlag>`_.
Prior to this, all enumerations were defined using ``IntFlag``.
However, treating negative values as flags (bitmasks) is
mathematically inconsistent. Furthermore, starting with Python 3.15,
negative members in ``IntFlag`` are reinterpreted by masking them to
the defined positive bit domain instead of keeping their original
negative literal values. Defining enumerations with negative
members as ``IntEnum`` ensures that their literal values (such as
``-1``) are preserved exactly as defined in the type library.

.. py:attribute:: gen_dir

This variable determines the directory where the typelib wrappers
Expand Down Expand Up @@ -656,6 +671,18 @@ friendly module generated by calling the ``GetModule`` function:
<CompareMethod.BinaryCompare: 0>


If an enumeration contains negative member values, it is defined as
an ``IntEnum`` rather than an ``IntFlag``:

.. doctest::

>>> msi = GetModule('msi.dll')
>>> msi.MsiInstallState # an enumeration with negative members
<enum 'MsiInstallState'>
>>> msi.MsiInstallState.msiInstallStateUnknown
<MsiInstallState.msiInstallStateUnknown: -1>


This code snippet could be used to generate the typelib wrapper module
for Scripting Dictionary automatically when your script is run, and
would include the module into the exe-file when the script is frozen
Expand Down
Loading