diff --git a/README.md b/README.md index 3fad6ff5..43bd7d96 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ `comtypes` allows you to define, call, and implement custom and dispatch-based COM interfaces in pure Python. `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.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. diff --git a/comtypes/__init__.py b/comtypes/__init__.py index 0a3347c5..a415953a 100644 --- a/comtypes/__init__.py +++ b/comtypes/__init__.py @@ -18,6 +18,22 @@ 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`. diff --git a/comtypes/test/test_client.py b/comtypes/test/test_client.py index 991456ba..3a75252c 100644 --- a/comtypes/test/test_client.py +++ b/comtypes/test/test_client.py @@ -269,6 +269,18 @@ def test_progid(self): self.assertEqual(consts.TextCompare, Scripting.TextCompare) self.assertEqual(consts.DatabaseCompare, Scripting.DatabaseCompare) + PY_3_15_ALPHA_BETA = ( + sys.version_info.major == 3 + and sys.version_info.minor == 15 + and sys.version_info.releaselevel in ("alpha", "beta") + ) + ENUMS_MESSAGE = ( + "Starting from Python 3.15, negative members in `IntFlag` may " + "no longer be evaluated as literals.\nWe need to address this before " + "the release. See: https://github.com/enthought/comtypes/issues/894" + ) + + @ut.skipIf(PY_3_15_ALPHA_BETA, ENUMS_MESSAGE) def test_enums_in_friendly_mod(self): comtypes.client.GetModule("scrrun.dll") comtypes.client.GetModule("msi.dll") @@ -287,11 +299,16 @@ def test_enums_in_friendly_mod(self): ), ]: for member in enumtype: - with self.subTest(enumtype=enumtype, member=member): + with self.subTest( + msg=self.ENUMS_MESSAGE, + enumtype=enumtype, + member=member, + ): self.assertIn(member.name, fadic) self.assertEqual(fadic[member.name], member.value) for member_name, member_value in fadic.items(): with self.subTest( + msg=self.ENUMS_MESSAGE, enumtype=enumtype, member_name=member_name, member_value=member_value, diff --git a/comtypes/test/test_outparam.py b/comtypes/test/test_outparam.py index 1650427d..dc7de6c5 100644 --- a/comtypes/test/test_outparam.py +++ b/comtypes/test/test_outparam.py @@ -1,6 +1,14 @@ import logging import unittest -from ctypes import c_wchar, c_wchar_p, cast, memmove, sizeof, wstring_at +from ctypes import ( + c_wchar, + c_wchar_p, + cast, + create_unicode_buffer, + memmove, + sizeof, + wstring_at, +) from unittest.mock import patch from comtypes.malloc import CoGetMalloc, _CoTaskMemAlloc, _CoTaskMemFree @@ -38,10 +46,11 @@ def comstring(text, typ=c_wchar_p): class Test(unittest.TestCase): @patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparam) def test_c_char(self): - ptr = c_wchar_p("abc") - # The normal constructor does not allocate memory using `CoTaskMemAlloc`. - # Therefore, calling the patched `ptr.__ctypes_from_outparam__()` would - # attempt to free invalid memory, potentially leading to a crash. + # Allocate memory from the Python/C runtime heap. + # This ensures the address is valid but "unallocated" from COM. + buf = create_unicode_buffer("abc") + ptr = cast(buf, c_wchar_p) + # Confirm the memory is not managed by the COM task allocator. self.assertEqual(malloc.DidAlloc(ptr), 0) x = comstring("Hello, World") diff --git a/comtypes/test/test_puredispatch.py b/comtypes/test/test_puredispatch.py index 62c3f51e..c4550cb6 100644 --- a/comtypes/test/test_puredispatch.py +++ b/comtypes/test/test_puredispatch.py @@ -83,13 +83,13 @@ def test_product_state(self): ) # There is no product associated with the Null GUID. pdcode = str(GUID()) - expected = msi.MsiInstallState.msiInstallStateUnknown + expected = msi.msiInstallStateUnknown self.assertEqual(expected, inst.ProductState(pdcode)) self.assertEqual(expected, inst.ProductState[pdcode]) # The `ProductState` property is a read-only property. # https://learn.microsoft.com/en-us/windows/win32/msi/installer-productstate-property with self.assertRaises(TypeError): - inst.ProductState[pdcode] = msi.MsiInstallState.msiInstallStateDefault # type: ignore + inst.ProductState[pdcode] = msi.msiInstallStateDefault # type: ignore # NOTE: Named parameters are not yet implemented for the named property. # See https://github.com/enthought/comtypes/issues/371 # TODO: After named parameters are supported, this will become a test to