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
9 changes: 6 additions & 3 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 All @@ -71,14 +73,15 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15']
type-checker: ['ty', 'mypy', 'pyright']
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Build sdist
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
2 changes: 1 addition & 1 deletion comtypes/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ def ChangeType(self, typecode):
VARIANT.empty = VARIANT()
VARIANT.missing = v = VARIANT()
v.vt = VT_ERROR
v._.VT_I4 = 0x80020004
v._.VT_I4 = hresult.DISP_E_PARAMNOTFOUND
del v


Expand Down
65 changes: 5 additions & 60 deletions comtypes/logutil.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# logutil.py
import functools
import logging
import warnings
from collections.abc import Callable
from ctypes import WinDLL
from ctypes.wintypes import LPCSTR, LPCWSTR

Expand All @@ -19,65 +18,11 @@
class NTDebugHandler(logging.Handler):
def emit(
self,
record,
writeW=_OutputDebugStringW,
):
record: logging.LogRecord,
writeW: Callable[[str], None] = _OutputDebugStringW,
) -> None:
text = self.format(record)
writeW(text + "\n")


logging.NTDebugHandler = NTDebugHandler


def deprecated(reason: str):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(reason, category=DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)

return wrapper

return decorator


@deprecated("Deprecated. See https://github.com/enthought/comtypes/issues/920.")
def setup_logging(*pathnames):
import configparser

parser = configparser.ConfigParser()
parser.optionxform = str # use case sensitive option names!

parser.read(pathnames)

DEFAULTS = {
"handler": "StreamHandler()",
"format": "%(levelname)s:%(name)s:%(message)s",
"level": "WARNING",
}

def get(section, option):
try:
return parser.get(section, option, True)
except (configparser.NoOptionError, configparser.NoSectionError):
return DEFAULTS[option]

levelname = get("logging", "level")
format = get("logging", "format")
handlerclass = get("logging", "handler")

# convert level name to level value
level = getattr(logging, levelname)
# create the handler instance
handler = eval(handlerclass, vars(logging))
formatter = logging.Formatter(format)
handler.setFormatter(formatter)
logging.root.addHandler(handler)
logging.root.setLevel(level)

try:
for name, value in parser.items("logging.levels", True):
value = getattr(logging, value)
logging.getLogger(name).setLevel(value)
except configparser.NoSectionError:
pass
logging.NTDebugHandler = NTDebugHandler # type: ignore
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()
16 changes: 1 addition & 15 deletions comtypes/test/test_logutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Union as _UnionT

from comtypes.client._events import SECURITY_ATTRIBUTES
from comtypes.logutil import NTDebugHandler, deprecated
from comtypes.logutil import NTDebugHandler
from comtypes.logutil import (
_OutputDebugStringW as OutputDebugStringW,
)
Expand All @@ -21,20 +21,6 @@
from ctypes import _CArgObject, _Pointer


class Test_deprecated(ut.TestCase):
def test_warning_is_raised(self):
reason_text = "This is deprecated."

@deprecated(reason_text)
def test_func():
return "success"

with self.assertWarns(DeprecationWarning) as cm:
result = test_func()
self.assertEqual(result, "success")
self.assertEqual(reason_text, str(cm.warning))


_kernel32 = WinDLL("kernel32", use_last_error=True)

# https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventw
Expand Down
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
Loading
Loading