Eliminate Workarounds for `POINTER(IUnknown)` by `Intersection` Generic Alias
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 35/100
- issue の種類
- ドキュメント
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- python
調査の方向性
Examine the current if TYPE_CHECKING: workaround in comtypes/_post_coinit/unknwn.py around lines 355-373 to understand the existing hack. Study how POINTER creates pointer types and how interface classes like IUnknown are defined with _cominterface_meta. Investigate adding __pointer_type__ to interface classes and using Intersection from typing-extensions to define ComPointer[T]. Check for type stub tests to validate changes. Done means the workaround is removed and type checkers correctly infer ComPointer[IUnknown] from CreateObject.
索引モデルが issue の本文から書いたものです。
説明
Background and Challenge: The "Intersecting" Dual Nature of COM Pointers
In comtypes, a COM pointer is a complex object that simultaneously exhibits two distinct characteristics at runtime:
- C-Level Pointer Nature:
It manages memory addresses and can be passed directly to C functions as a derivative ofctypes.c_void_por_compointer_base. - COM Interface Nature:
It can directly invoke high-level methods defined by specific interfaces, such asIUnknownorIDispatch(e.g.,AddRef,QueryInterface,GetClassInfo).
Inspecting the MRO (Method Resolution Order) of POINTER, IUnknown, and their underlying metaclasses at runtime reveals how these two characteristics are bound together via dynamic multiple inheritance.
Notably, a POINTER(IUnknown) instance is not a ctypes._Pointer type, whereas POINTER(POINTER(IUnknown)) is a ctypes._Pointer type:
>>> # Python 3.15
>>> from ctypes import POINTER, c_void_p, pointer
>>> import comtypes.client
>>> from comtypes import IUnknown, GUID
>>> from comtypes._post_coinit.unknwn import _compointer_base, _compointer_meta, _cominterface_meta
>>> # metaclass instance = type
>>> isinstance(IUnknown, _compointer_base)
False
>>> isinstance(IUnknown, _compointer_meta)
False
>>> isinstance(IUnknown, _cominterface_meta)
True
>>> isinstance(POINTER(IUnknown), _compointer_base)
False
>>> isinstance(POINTER(IUnknown), _compointer_meta)
True
>>> isinstance(POINTER(IUnknown), _cominterface_meta)
True
>>> # instance
>>> isinstance(IUnknown(), _compointer_base)
False
>>> isinstance(IUnknown(), _compointer_meta)
False
>>> isinstance(IUnknown(), _cominterface_meta)
False
>>> isinstance(IUnknown(), IUnknown)
True
>>> isinstance(POINTER(IUnknown)(), _compointer_base)
True
>>> isinstance(POINTER(IUnknown)(), _compointer_meta)
False
>>> isinstance(POINTER(IUnknown)(), _cominterface_meta)
False
>>> isinstance(POINTER(IUnknown)(), IUnknown)
True
>>> isinstance(POINTER(IUnknown)(), POINTER(IUnknown))
True
>>> isinstance(POINTER(IUnknown)(), _Pointer)
False
>>> isinstance(POINTER(POINTER(IUnknown))(), POINTER(IUnknown))
False
>>> isinstance(POINTER(POINTER(IUnknown))(), _Pointer)
True
>>> # mro
>>> _compointer_base.__mro__
(<class 'comtypes._post_coinit.unknwn._compointer_base'>, <class 'ctypes.c_void_p'>, <class '_ctypes._SimpleCData'>, <class '_ctypes._CData'>, <class 'object'>)
>>> _compointer_meta.__mro__
(<class 'comtypes._post_coinit.unknwn._compointer_meta'>, <class '_ctypes.PyCSimpleType'>, <class '_ctypes.CType_Type'>, <class 'comtypes._post_coinit.unknwn._cominterface_meta'>, <class 'type'>, <class 'object'>)
>>> _cominterface_meta.__mro__
(<class 'comtypes._post_coinit.unknwn._cominterface_meta'>, <class 'type'>, <class 'object'>)
>>> IUnknown.__mro__
(<class 'comtypes._post_coinit.unknwn.IUnknown'>, <class 'object'>)
>>> POINTER(IUnknown).__mro__
(<class 'comtypes._post_coinit.unknwn.POINTER(IUnknown)'>, <class 'comtypes._post_coinit.unknwn.IUnknown'>, <class 'comtypes._post_coinit.unknwn._compointer_base'>, <class 'ctypes.c_void_p'>, <class '_ctypes._SimpleCData'>, <class '_ctypes._CData'>, <class 'object'>)
>>> POINTER(POINTER(IUnknown)).__mro__
(<class 'ctypes.LP_POINTER(IUnknown)'>, <class '_ctypes._Pointer'>, <class '_ctypes._CData'>, <class 'object'>)
>>> type(_compointer_base)
<class 'comtypes._post_coinit.unknwn._compointer_meta'>
>>> type(_compointer_meta)
<class 'type'>
>>> type(_cominterface_meta)
<class 'type'>
>>> type(IUnknown)
<class 'comtypes._post_coinit.unknwn._cominterface_meta'>
>>> type(POINTER(IUnknown))
<class 'comtypes._post_coinit.unknwn._compointer_meta'>
>>> type(POINTER(POINTER(IUnknown)))
<class '_ctypes.PyCPointerType'>
>>> _CData = _SimpleCData.__mro__[:-1][-1]
>>> _CData.__mro__
(<class '_ctypes._CData'>, <class 'object'>)
>>> isinstance(IUnknown(), _CData)
False
>>> isinstance(POINTER(IUnknown)(), _CData)
True
>>> isinstance(POINTER(POINTER(IUnknown))(), _CData)
True
We can confirm that this holds true even for fully functional COM pointers at runtime:
>>> CLSID_CUIAutomation = GUID("{FF48DBA4-60EF-4201-AA87-54103EEF594E}")
>>> punk = CreateObject(CLSID_CUIAutomation, interface=IUnknown)
>>> punk
<POINTER(IUnknown) ptr=... at ...>
>>> isinstance(punk, c_void_p)
True
>>> isinstance(punk, _compointer_base)
True
>>> isinstance(punk, _compointer_meta)
False
>>> isinstance(punk, _cominterface_meta)
False
>>> isinstance(punk, IUnknown)
True
>>> isinstance(punk, POINTER(IUnknown))
True
>>> isinstance(punk, POINTER(POINTER(IUnknown)))
False
>>> ppunk = pointer(punk)
>>> ppunk
<ctypes.LP_POINTER(IUnknown) object at ...>
>>> isinstance(ppunk, _compointer_base)
False
>>> isinstance(ppunk, _compointer_meta)
False
>>> isinstance(ppunk, _cominterface_meta)
False
>>> isinstance(ppunk, IUnknown)
False
>>> isinstance(ppunk, POINTER(IUnknown))
False
>>> isinstance(ppunk, POINTER(POINTER(IUnknown)))
True
Expressing this "dynamic and intersecting nature" in current Python static typing is extremely difficult.
Limitations of Traditional Approaches (Union / Single Inheritance) and Current Workarounds
It is impossible to accurately represent this state using conventional type systems.
- Limitations of Union (
A | B):
Since a Union means "either A or B", it cannot express the intersecting state of "having pointer capabilities (A) and interface methods (B) simultaneously".
Attempting to call an interface method causes the type checker to emit false-positive errors because the pointer type itself does not own those methods. - Limitations of Single Inheritance Trees and Mixins:
To statically define a base class possessing both traits, we need to statically define as many mixin classes as there are derived classes ofIUnknown(hundreds to thousands of COM interfaces).
This is entirely impractical and completely blocks dynamic generic expressions like "a pointer to an arbitrary interfaceT".
Due to these constraints, comtypes currently relies on workarounds using if TYPE_CHECKING: blocks:
At runtime, it is simply an alias for object, but forcing it to inherit from c_void_p strictly during static type checking prevents a flood of false positives when calling POINTER(IUnknown).
However, this is merely an ad-hoc fix that decouples the actual runtime structure (MRO) from static analysis.
Evolution in Python 3.14+ (Leveraging __pointer_type__ and Generics)
By leveraging _CData.__pointer_type__ introduced in Python 3.14+, we now have the foundation to tie interfaces to their respective pointer types using generics.
Note: While the code snippet below follows PEP 695 syntax, the same can be achieved using traditional TypeVar.
# Type stub desired to be added to typeshed
class _HasDunderPointerType[T](Protocol):
__pointer_type__: type[T]
# Existing overloads of `POINTER`...
@overload
def POINTER[T](cls: _HasDunderPointerType[T]) -> type[T]: ...
# Existing overloads of `POINTER`...
If every interface explicitly specifies its pointer type via __pointer_type__ and the POINTER factory overloads accept it, we can at least dynamically and generically communicate "what the pointer type of this interface is" to the type checker.
However, the fundamental problem of how to define the inner contents of T (the actual layout of the pointer) remains unsolved.
Complete Resolution via Intersection and the Need for Adoption
By combining the generic foundation above with Intersection—as discussed in https://github.com/python/typing/issues/213 and https://github.com/CarliJoy/intersection_examples — we can completely eliminate past contradictions and hacks, achieving a static type representation that perfectly matches the runtime behavior.
type ComPointer[T_IUnknown: IUnknown] = Intersection[_compointer_base, T_IUnknown]
This single line declares the coexistence of "the underlying C pointer capability (_compointer_base)" and "any high-level COM interface (T_IUnknown)".
This allows type-safe propagation of return values from dynamic factory methods like CreateObject or QueryInterface:
# _post_coinit/unknwn.py
class IUnknown(object, metaclass=_cominterface_meta): # Workaround is NO more
if TYPE_CHECKING:
# Need to add new type-check only attribute
__pointer_type__: ClassVar[type[ComPointer[Self]]]
punk = CreateObject("AnyProgId", interface=IUnknown)
pc = punk.QueryInterface(IProvideClassInfo)
ti = pc.GetClassInfo()
assert_type(punk, ComPointer[IUnknown])
assert_type(pc, ComPointer[IProvideClassInfo])
assert_type(ti, ComPointer[ITypeInfo])
ppunk = pointer(punk)
assert_type(ppunk, _Pointer[ComPointer[IUnknown]])
This can be verified using ty, which features a pioneering internal implementation of Intersection, confirming that type inference works as intended and statically validates types that precisely match the complex runtime MRO:
https://play.ty.dev/c57737cd-e7e3-4cfc-8e11-4d3e5764750e
Future Outlook
If an experimental version of Intersection is eventually implemented in typing-extensions, we may face some limitations regarding Python versions or supported type checkers, but I would likely adopt code like the above to represent COM pointer types.
In any case, this change is strictly for static typing and is designed to have no impact on runtime behavior.
While complex metaprogramming that relies on runtime annotation inspection might be affected, I am not aware of any projects attempting this with comtypes.
When a clean way to accurately express types becomes available, prioritizing its adoption is my preferred direction.
- 主要言語
- Python
- スター
- 347
- フォーク
- 107
- 平均マージ
- 19時間 36分
- マージ済み PR(30日)
- 17
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
enthought/comtypes のほかの issue
-
good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 1 日以内に返信
-
automation.VARIANT._set_value assumes c_char.value has length 1対応中かも @haoxuanjng-lang が 5 日前に担当しました。 オープンquestion
難易度 3/5 1〜2日 初心者へのやさしさ 58/100
enthought/comtypes#935 · コメント 10 件 ·
メンテナーはふだん 1 日以内に返信
-
ci tests
難易度 3/5 1〜2日 初心者へのやさしさ 62/100
enthought/comtypes#925 · コメント 11 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
enthought/comtypes#873 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
help wanted
難易度 4/5 3〜5日 初心者へのやさしさ 38/100
enthought/comtypes#871 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
enthought/comtypes の issue をすべて見る
似ている issue
-
vis.image() reports "Unsupported number of image channels: 224" for an HxWxC image対応中かも @piyush182004 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
fossasia/visdom#1997 · コメント 3 件 ·
メンテナーはふだん 6 日以内に返信
-
Byse new domainオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
Gujal00/ResolveURL#1525 ·
メンテナーはふだん 1 日以内に返信
-
Serialisation fails with pydantic 2.14: wrap serialisers pass SerializationInfo as index_key対応中かも @hugohills-regnosys が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
finos/rune-python-runtime#45 ·
-
[Bug]: LangGraph Store sync put bypasses namespace validation対応中かも @ktz03 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
volcengine/OpenViking#6050 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
hummingbot/condor#283 ·
メンテナーはふだん 1 日以内に返信