dspace-group/dscom

Python (Stub) generation

Opened this issue · 0 comments

What is the goal of this request?

As a python developer I want to use a COM client to develop a tool automation / tool integration. With C++ and .NET I have support for type checking and language server support.

As python developer the type inspection only works at runtime. It would be good to be able to use newer technologies like python type hinting and static type checkers like mypy.

Example C# interface.

Consider the following interface:

    // DLL: Sample.dll
 
    [ComVisible(true)]
    [Guid("8d64afd8-f62e-45a5-92c2-89c3c0d83eb9")]
    [ProgId("SampleComponent.ComponentImplementation")]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IMyInterface))]
    public class SampleComponent: IMyInterface
    {
          public string Version { get; }
  
          public void LongRunningOperation(string valueParameter, int timeout) {  /***/ }

          public string[] GetItems() { return Array.Empty<string>(); }
    }

Feature: Python Dispatch

With this getting into mind, the following python class should be available:

# file sample_component.py

from win32com.client import Dispatch

from typing import TYPECHECKING, cast

if TYPECHECKING:
    import .sample as dispatched

def get_sample_component() -> "dispatched.SampleComponent":
    instance = Dispatch("SampleComponent.ComponentImplementation")
    return cast("dispatched.ComponentImplementation", instance)

Feature: Python Stub Interfaces

In order to be able to check my python code against static python type hints, I need python stub interfaces:

# file sample.pyi

from win32com.client import Dispatch
from collections.abc import Collection

class SampleComponent(Dispatch):
    @property
    def Version(self) -> "str": ...

    def LongRunningOperation(valueParameter: "str", timeout: "str") -> None: ...

    def GetItems() -> "Collection[str]": ...

def get_sample_component() -> "SampleComponent": ...

all_items = [
    "get_sample_component",
    "SampleComponent",
]

__all__ = all_items

del Dispatch
del Collection

Feature: Packaging

In tool chain scenarios it might be wise to pack mulitple stubs / implementations into (hierarchical) packages. Hence the packaging should be available. The package should be selectable, either a namespace package or a classic package.

Example: classic package

from typing import TYPE_CHECKING

import package.sample_component as sc

if TYPE_CHECKING:
    import package.sample as sc_types

component: "sc_types.SampleComponent" = sc.get_sample_component()

print(component.Version)

Example: namespace package

from typing import TYPE_CHECKING

import package.sample_component as sc

if TYPE_CHECKING:
    import package.sample as sc_types

component: "sc_types.SampleComponent" = sc.get_sample_component()

print(component.Version)

The difference lies in the different implementation of the init.py in the files.

Planned implementation

Command-line

It is planned to add a new export command: tlb2pyi

The following options will apply:

--package

A (dot-separated) python package name (defaults to assembly file name)

--shared-file-name

The name of the shared file (defaults to the assembly file name with dots replaced by underscores)

--support-namespace

A boolean flag to support namespace packages (defaults to assembly file name contains a dot)

--output-dir

The output directory to use (defaults to .)

assembly

The dll to dump

Other arguments might be added on requirement.

Implementation

The assembly will be loaded into the context.

For each class flagged with ProgId, a python class will be created inheriting from Dispatch. In addition, a get_ method will be created where Camel Case will be transformed to lower snake case.

All classes, value types, enums and interfaces will be dumped to the shared file.

Enums will derive from enum.IntEnum. Interfaces will be implemented as Protocols. Only public, ComVisible, Non-abstract classed will be exported to type stubs.

Mapping of .NET Types to Python types:

string: str

int: int
short: int (maybe annotated)
sbyte: int (maybe annotated)
long: int (maybe annotated)

uint: int (maybe annotated)
ushort: int (maybe annotated)
byte: int (maybe annotated)
ulong: int (maybe annotated)

float: float
double: float

Guid: TBD

Array: collections.abc.Collection (sized, iterable, container)
List: list
Dictionary: Mapping

Other types: TBD

Limitations

The pystub generator in its first implementation will only consider self-contained assemblies without related TLBs.