diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b4287a8..ea93bd28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,7 +102,10 @@ If any style issues are found, maintainers may ask you to make modifications. ### Pull requests When you have resolved your issue, open a pull request in the `comtypes` repository. Please include the issue number on the PR comment. -When enough PRs have been accepted to resolve the issue, please close the issue or mention it to the person(s) involved. +When enough PRs have been accepted to resolve the issue, please close the issue or mention it to the person(s) involved. +The CI pipeline runs on every pull request to ensure runtime functionality is maintained. It tests integration with optional dependencies such as `numpy` and `pywin32`, measures test coverage. Runtime reliability is **tier 1**. +The CI pipeline also runs static type checking (`mypy`, `pyright`, `ty`) on the installed package. This provides **tier 2** quality assurance for type inference and safety. +For critical vulnerability fixes or bug fixes, releases may proceed in urgent cases if runtime tests pass, even when type checking reports errors. ## Contributing to documentation :books: diff --git a/docs/source/index.rst b/docs/source/index.rst index 13362880..b7c54777 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -29,6 +29,7 @@ Functionalities com_interfaces npsupport threading + type_checking Links diff --git a/docs/source/type_checking.rst b/docs/source/type_checking.rst new file mode 100644 index 00000000..76e86caf --- /dev/null +++ b/docs/source/type_checking.rst @@ -0,0 +1,102 @@ +################################ +Type Checking and Type Inference +################################ + +``comtypes`` generates Python wrapper module files from COM type +libraries. +These generated modules contain extensive type hints which allow +static type checkers to infer the shapes of COM objects, methods, +properties and the behaviour of factories such as ``CreateObject`` +or ``GetModule``. + +.. contents:: + +Basic example – ``Scripting.Dictionary`` +**************************************** + +.. sourcecode:: python + + import sys + from ctypes import POINTER + + from comtypes.client import CreateObject, GetModule + + if sys.version_info >= (3, 11): + from typing import assert_type + else: + from typing_extensions import assert_type + + # Generate/ensure the existence of the ``Scripting`` module. + GetModule("scrrun.dll") + # Must be imported statically; + # The type checker cannot perform static type analysis on the + # ``ModuleType`` instance returned by ``GetModule``. + from comtypes.gen import Scripting + + + dic = CreateObject( + Scripting.Dictionary, interface=Scripting.IDictionary + ) + # At runtime, ``dic`` is a ``POINTER(IDictionary)``. + # It behaves as a subclass of ``IDictionary`` via its metaclass. + # Since capabilities to express dynamic subclasses created by + # ``POINTER`` are not introduced into Python's static type system + # yet, it is typed to return ``IDictionary``. + # The static type checker sees the following type: + # dic: Scripting.IDictionary + assert isinstance(dic, POINTER(Scripting.IDictionary)) + assert isinstance(dic, Scripting.IDictionary) + assert_type(dic, Scripting.IDictionary) + + # Properties are correctly typed + dic.CompareMode = Scripting.TextCompare + # This test verifies that the ``Scripting.Dictionary`` supports + # both subscriptable and callable access patterns, and that the + # static type checker correctly recognizes these operations. + dic["foo"] = 1 + assert dic["foo"] == dic.Item["foo"] == 1 + dic.Add("bar", 2) + assert dic("bar") == dic.Item("bar") == 2 + dic.Item["qux"] = 3 + assert dic("qux") == dic.Item("qux") == 3 + + +Limitations +*********** + +The COM factory is annotated as returning an ``IUnknown``-based +object, although the actual runtime type is the dynamically defined +subclass ``POINTER(IUnknown)``, created through the interaction +between the ``ctypes.POINTER`` factory function and the complex +metaclass of ``IUnknown``. + +It is not annotated as the commonly used ctypes pointer type +``ctypes._Pointer[IUnknown]`` because Python's type system treats +``ctypes._Pointer[CT]`` as a container type rather than as a subtype +of ``CT``. +Additionally, the COM pointer type is dynamically generated by +``ctypes.POINTER``, with its behavior defined by the metaclass of +the COM interface. +In particular, although +``isinstance(ctypes.pointer(ctypes.c_int(1)), ctypes._Pointer)`` +is ``True``, +``isinstance(CreateObject(progid, interface=IUnknown), ctypes._Pointer)`` +is ``False``. Thus, the concrete type of the COM pointer is not +``ctypes._Pointer``. +Furthermore, Python's type system does not currently provide a way +to express the intersection types needed to represent such a +dynamically defined subclass. +As a result, the current Python type system cannot express the +concrete type and type relationship needed for static analysis of +the COM interface methods. + +The recommended typing style in Python is to annotate return values +with the base or abstract interface that describes how the value is +expected to be used, rather than with its concrete runtime type. +For example, if a value is expected to be used only as a sequence, +without adding elements to it, ``collections.abc.Sequence[str]`` is +preferable to ``list[str]``, even when the actual returned value is +a ``list[str]``. +By annotating the return value as ``IUnknown``, this base-class +approach enables static analysis tools to recognize COM interface +methods.