Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of 'com_record' as a subclassable Python type. #2437

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

geppi
Copy link
Contributor

@geppi geppi commented Dec 13, 2024

A 'tp_new' method was added to the 'PyTypeObject' slots of 'com_record'. Replacement new is now used to create an instance of a 'com_record' type, i.e. in 'tp_new' as well as in the factory functions. The 'tp_dealloc' method explicitely calls the destructor before finally calling 'tp_free'.

Records returned from a call to a COM method do get the subclass type according to the GUID in the COM RecordInfo. If no subclass with that GUID is found, the generic 'com_record' base class is used.

The algorithm that retrieves the list of subclasses of 'com_record' only uses methods and data structures of the public Python API. This is important because in Python 3.12 the type of the 'tp_subclasses' slot of a 'PyTypeObject' was changed to 'void*' to indicate that for some types it does not hold a valid 'PyObject*'.

The 'PyRecord_Check' function was modified to test if the object is an instance of 'com_record' or a derived type.

The implementation does not break existing code.
It is not required to define 'com_record' subclasses, i.e. it is possible to work with the generic 'com_record' type as before using the factory function.

Resolves #2361

@geppi
Copy link
Contributor Author

geppi commented Dec 13, 2024

This PR addresses issue #2361 and will allow to create subclasses of com_record.

It does enable typing of interface methods that expect a particular structure as a parameter.

To define a particular subclass of com_record it is required to specify these class attributes:

  • TLBID : the UUID of the Type Library where the record is defined
  • MJVER : the major version number of the Type Library
  • MNVER : the minor version number of the Type Library
  • LCID : the LCID of the Type Library
  • GUID : the GUID of the record structure from the Type Library

As an example with the following Type Library IDL:

[
  uuid(E6F07342-C1F7-4E4E-B021-11BBD54B9F37),
  version(2.3),
  helpstring("Example Type Library"),
]
library ExampleLib
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

	
    typedef [uuid(9F2C4E2E-2C5C-4F39-9FDB-840A1E08B165)]
    struct tagT_ENTITY {
        long parent;
        BSTR name;
        BSTR description;
    } T_ENTITY;

	
    typedef [uuid(B1461DD4-8C86-4627-B444-3D833C980111)]
    struct tagT_LINE {            
        double startX;
        double startY;
        double startZ;
        double endX;
        double endY;
        double endZ;
    } T_LINE;

	....

    interface IExample : IDispatch {
	
	[id(0x00000001)]
        HRESULT AddLine(
                        [in, out] T_ENTITY* ent, 
                        [in, out] T_LINE* line, 
                        [out, retval] long* retval);
        [id(0x00000002)]
        HRESULT GetLine(
                        [in] long n, 
                        [in, out] T_ENTITY* ent, 
                        [in, out] T_LINE* line);
		....
	}
....
}

the record class definitions for T_ENTITY and T_LINE will take the following form:

import pythoncom

class SomeTlibRecordMeta(type):

	def __new__(cls, name, bases, namespace):
		namespace['TLBID'] = '{E6F07342-C1F7-4E4E-B021-11BBD54B9F37}'
		namespace['MJVER'] = 2
		namespace['MNVER'] = 3
		namespace['LCID'] = 0
		return type.__new__(cls, name, bases, namespace)


class T_ENTITY(pythoncom.com_record, metaclass=SomeTlibRecordMeta):

	GUID = '{9F2C4E2E-2C5C-4F39-9FDB-840A1E08B165}'
	__slots__ = tuple()
	parent: int
	name: str
	description: str


class T_LINE(pythoncom.com_record, metaclass=SomeTlibRecordMeta):
	
	GUID = '{B1461DD4-8C86-4627-B444-3D833C980111}'
	__slots__ = tuple()
	startX: float
	startY: float
	startZ: float
	endX: float
	endY: float
	endZ: float

The metaclass in the above code is only used to avoid retyping the TLBID, MJVER, MNVER and LCID of the Type Library in every class definition for records from the same Type Library.

The subclasses provide valuable information for type hints.
In the future it would be possible to extend makepy to automatically generate those type hints, like:

def AddLine(self, ent:T_ENTITY=defaultNamedNotOptArg, line:T_LINE=defaultNamedNotOptArg) -> tuple[int, T_ENTITY, T_LINE]:
	return self._ApplyTypes_(643, 1, (3, 0), ((16420, 3), (16420, 3)), 'AddLine', None, ent, line)

def GetLine(self, n:int=defaultNamedNotOptArg, ent:T_ENTITY=defaultNamedNotOptArg, line:T_LINE=defaultNamedNotOptArg) -> tuple[T_ENTITY, T_LINE]:
	return self._ApplyTypes_(645, 1, (24, 0), ((3, 1), (16420, 3), (16420, 3)), 'GetLine', None, n, ent, line)

With the above class definitions, new instances of a particular record type can simply be created with e.g. line = T_LINE() or as before this PR with:

from win32com.client import gencache, Record

app = gencache.EnsureDispatch('Example.Application')
line = Record("T_LINE", app)

This PR does not break any existing code because the com_record base type is always used as the fallback type, if no particular subclass with a matching COM record GUID has been defined.

On the other hand, COM records returned by a call to a COM interface method will receive the proper subclass type, if their RecordInfo does match the GUID of a com_record subclass.

A 'tp_new' method was added to the 'PyTypeObject' slots of 'com_record'.
Replacement new is now used to create an instance of a 'com_record' type,
i.e. in 'tp_new' as well as in the factory functions. The 'tp_dealloc'
method explicitely calls the destructor before finally calling 'tp_free'.

Records returned from a call to a COM method do get the subclass type
according to the GUID in the COM RecordInfo. If no subclass with that
GUID is found, the generic 'com_record' base class is used.

The algorithm that retrieves the list of subclasses of 'com_record'
only uses methods and data structures of the public Python API.
This is important because in Python 3.12 the type of the 'tp_subclasses'
slot of a 'PyTypeObject' was changed to 'void*' to indicate that for some
types it does not hold a valid 'PyObject*'.

The 'PyRecord_Check' function was modified to test if the object is an
instance of 'com_record' or a derived type.

The implementation does not break existing code.
It is not required to define 'com_record' subclasses, i.e. it is possible
to work with the generic 'com_record' type as before using the factory
function.
@geppi geppi force-pushed the record_subclasses branch from 0c591f8 to ebfaa62 Compare December 13, 2024 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enable creating subclasses of com_record
1 participant