Skip to content

Commit 691f9d7

Browse files
geppiAvasam
andauthored
Implementation of 'com_record' as a subclassable Python type. (#2437)
* Implementation of 'com_record' as a subclassable Python type. 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. * Removed an unnecessary type cast and unreferenced local variables. * Added a global dictionary for 'pythoncom.com_record' subclass registration. * The algorithm that identifies the subclass of 'pythoncom.com_record' from which a COM Record instance will be created now looks up the Record GUID in a global map, to only create instances of subclasses that have explicitely been registered in this map. * Added the function 'register_record_class' to add proper subclasses of 'pythoncom.com_record' to the global map 'pythoncom.RecordClasses'. The function does only add instantiable subclasses of 'pythoncom.com_record' to the map if their GUID is not already contained in the map. * Added a check for the record type name in the 'register_record_class' Python function. Also added another dunder attribute '__record_type_name__' to the 'PyRecord::getattro' function that retrieves the record type name via 'IRecordInfo::GetName'. * Update com/win32com/client/__init__.py Co-authored-by: Avasam <samuel.06@hotmail.com> * Code cleanup. * Reverted the part of commit cd1c2fc that added a check for the record type name in the 'register_record_class' Python function. * Added the dunder attribute '__record_type_guid__' to the 'PyRecord::getattro' function that retrieves the record type GUID via 'IRecordInfo::GetGuid'. * Added a NULL check for 'PyRecord::new_record' before calling 'PyTuple_SET_ITEM' in the 'PyObject_FromSAFEARRAYRecordInfo' function. Also enhanced the treatment of NULL return values in several other places in 'PyRecord.cpp'. * Added tests for the 'pythoncom.com_record' subclassing functionality. * Added a NULL check for 'PyRecord::new_record' before calling 'PyTuple_SET_ITEM' in the 'PyRecord::getattro' function. Also 'PyRecord::new_record' will now raise a 'PyErr_NoMemory' exception when the call to 'tp_alloc' returns NULL. * Added a method for the 'tp_init' slot of 'com_record' to enable instance initialization. Also changed the com_record base class to be only instantiable via the Record factory function and subclasses of com_record only if they are registered. Modified the test in 'testPyComTest.py' to reflect this. Added a test for the initialization of com_record subclasses. * Added a changelog entry. * Added changes from code review. --------- Co-authored-by: Avasam <samuel.06@hotmail.com>
1 parent f587548 commit 691f9d7

8 files changed

Lines changed: 438 additions & 76 deletions

File tree

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ https://mhammond.github.io/pywin32_installers.html .
1414
Coming in build 309, as yet unreleased
1515
--------------------------------------
1616

17+
* Changed the implementation of 'com_record' to a subclassable Python type (2437#, #2361, @geppi)
1718
* Removed param `hIcon` from `win32comext.shell.ShellExecuteEx`. It was unusable since Windows Vista (#2423, @Avasam)
1819
* Fixed `nbios.NCBStruct` packing (#2406, @Avasam)
1920
* Restored axdebug builds on Python 3.10 (#2416, @Avasam)
21+
* Fix for Python 3.12 interpreter crashes when accessing a COM Record field (#2415, @geppi)
2022
* Pythonwin: Bumped Scintilla from 1.77 to 4.4.6. The full changelog can be found here: https://www.scintilla.org/ScintillaHistory.html
2123
* Fixed Pythonwin's editor failing due to invalid regex import (#2419, @Avasam)
2224
* Last error wrongly set by some modules (#2302, @CristiFati)

com/win32com/client/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,47 @@ def Record(name, object):
487487
)
488488

489489

490+
# Registration function for com_record subclasses.
491+
def register_record_class(cls):
492+
"""
493+
Register a subclass of com_record to enable creation of the represented record objects.
494+
495+
A subclass of com_record requires the following class attributes to be instantiable:
496+
497+
TLBID : The GUID of the containing TypeLibrary as a string.
498+
MJVER : The major version number of the TypeLibrary as an integer.
499+
MNVER : The minor version number of the TypeLibrary as an integer.
500+
LCID : The LCID of the TypeLibrary as an integer.
501+
GUID : The GUID of the COM Record as a string.
502+
503+
with GUID strings in {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} notation.
504+
505+
To instantiate such a subclasses it has to be registered via this function.
506+
"""
507+
if not issubclass(cls, pythoncom.com_record):
508+
raise TypeError("Only subclasses of 'com_record' can be registered.")
509+
try:
510+
TLBID = cls.TLBID
511+
MJVER = cls.MJVER
512+
MNVER = cls.MNVER
513+
LCID = cls.LCID
514+
GUID = cls.GUID
515+
except AttributeError as e:
516+
raise AttributeError(f"Class {cls.__name__} cannot be instantiated.") from e
517+
try:
518+
_ = pythoncom.GetRecordFromGuids(TLBID, MJVER, MNVER, LCID, GUID)
519+
except Exception as e:
520+
raise TypeError(f"Class {cls.__name__} cannot be instantiated.") from e
521+
# Since the class can be instantiated we know that it represents a valid COM Record
522+
# in a properly registered TypeLibrary and that it has a 'GUID' class attribute.
523+
if cls.GUID in pythoncom.RecordClasses:
524+
raise ValueError(
525+
f"Record class with same GUID {cls.GUID} "
526+
f"is already registered with name '{pythoncom.RecordClasses[cls.GUID].__name__}'."
527+
)
528+
pythoncom.RecordClasses[cls.GUID] = cls
529+
530+
490531
############################################
491532
# The base of all makepy generated classes
492533
############################################

0 commit comments

Comments
 (0)