From 44fdad2ba196af582dcf7cbf4e1b8afb93031692 Mon Sep 17 00:00:00 2001 From: Thomas Geppert Date: Mon, 13 Jan 2025 08:51:06 +0100 Subject: [PATCH] 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. --- com/win32com/client/__init__.py | 10 +- com/win32com/src/PyRecord.cpp | 156 ++++++++++++++++++++-------- com/win32com/src/include/PyRecord.h | 3 +- com/win32com/src/oleargs.cpp | 2 +- com/win32com/test/testPyComTest.py | 18 +++- 5 files changed, 137 insertions(+), 52 deletions(-) diff --git a/com/win32com/client/__init__.py b/com/win32com/client/__init__.py index 3fdb8d911b..bea4268ee2 100644 --- a/com/win32com/client/__init__.py +++ b/com/win32com/client/__init__.py @@ -507,7 +507,15 @@ def register_record_class(cls): if not issubclass(cls, pythoncom.com_record): raise TypeError("Only subclasses of 'com_record' can be registered.") try: - _ = cls() + TLBID = cls.TLBID + MJVER = cls.MJVER + MNVER = cls.MNVER + LCID = cls.LCID + GUID = cls.GUID + except AttributeError as e: + raise AttributeError(f"Class {cls.__name__} cannot be instantiated.") from e + try: + _ = pythoncom.GetRecordFromGuids(TLBID, MJVER, MNVER, LCID, GUID) except Exception as e: raise TypeError(f"Class {cls.__name__} cannot be instantiated.") from e # Since the class can be instantiated we know that it represents a valid COM Record diff --git a/com/win32com/src/PyRecord.cpp b/com/win32com/src/PyRecord.cpp index b9d7c8b178..1852dff5c2 100644 --- a/com/win32com/src/PyRecord.cpp +++ b/com/win32com/src/PyRecord.cpp @@ -122,7 +122,7 @@ PyObject *PyObject_FromSAFEARRAYRecordInfo(SAFEARRAY *psa) return ret; } // Creates a new Record by TAKING A COPY of the passed record. -PyObject *PyObject_FromRecordInfo(IRecordInfo *ri, void *data, ULONG cbData) +PyObject *PyObject_FromRecordInfo(IRecordInfo *ri, void *data, ULONG cbData, PyTypeObject *type = NULL) { if ((data != NULL && cbData == 0) || (data == NULL && cbData != 0)) return PyErr_Format(PyExc_RuntimeError, "Both or neither data and size must be given"); @@ -147,7 +147,7 @@ PyObject *PyObject_FromRecordInfo(IRecordInfo *ri, void *data, ULONG cbData) delete owner; return PyCom_BuildPyException(hr, ri, IID_IRecordInfo); } - return PyRecord::new_record(ri, owner->data, owner); + return PyRecord::new_record(ri, owner->data, owner, type); } // @pymethod |pythoncom|GetRecordFromGuids|Creates a new record object from the given GUIDs @@ -209,35 +209,37 @@ PyObject *pythoncom_GetRecordFromTypeInfo(PyObject *self, PyObject *args) // This function creates a new 'com_record' instance with placement new. // If the particular Record GUID belongs to a registered subclass // of the 'com_record' base type, it instantiates this subclass. -PyRecord *PyRecord::new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner) +PyRecord *PyRecord::new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner, PyTypeObject *type) /* default: type = NULL */ { GUID structguid; OLECHAR *guidString; PyObject *guidUnicode, *recordType; - // By default we create an instance of the base 'com_record' type. - PyTypeObject *type = &PyRecord::Type; - // Retrieve the GUID of the Record to be created. - HRESULT hr = ri->GetGuid(&structguid); - if (FAILED(hr)) { - PyCom_BuildPyException(hr, ri, IID_IRecordInfo); - return NULL; - } - hr = StringFromCLSID(structguid, &guidString); - if (FAILED(hr)) { - PyCom_BuildPyException(hr); - return NULL; - } - guidUnicode = PyWinCoreString_FromString(guidString); - if (guidUnicode == NULL) { - ::CoTaskMemFree(guidString); - return NULL; - } - recordType = PyDict_GetItem(g_obPyCom_MapRecordGUIDToRecordClass, guidUnicode); - Py_DECREF(guidUnicode); - // If the Record GUID is registered as a subclass of com_record - // we return an object of the subclass type. - if (recordType && PyObject_IsSubclass(recordType, (PyObject *)&PyRecord::Type)) { - type = (PyTypeObject *)recordType; + if (type == NULL) { + // By default we create an instance of the base 'com_record' type. + type = &PyRecord::Type; + // Retrieve the GUID of the Record to be created. + HRESULT hr = ri->GetGuid(&structguid); + if (FAILED(hr)) { + PyCom_BuildPyException(hr, ri, IID_IRecordInfo); + return NULL; + } + hr = StringFromCLSID(structguid, &guidString); + if (FAILED(hr)) { + PyCom_BuildPyException(hr); + return NULL; + } + guidUnicode = PyWinCoreString_FromString(guidString); + if (guidUnicode == NULL) { + ::CoTaskMemFree(guidString); + return NULL; + } + recordType = PyDict_GetItem(g_obPyCom_MapRecordGUIDToRecordClass, guidUnicode); + Py_DECREF(guidUnicode); + // If the Record GUID is registered as a subclass of com_record + // we return an object of the subclass type. + if (recordType && PyObject_IsSubclass(recordType, (PyObject *)&PyRecord::Type)) { + type = (PyTypeObject *)recordType; + } } // Finally allocate the memory for the the appropriate // Record type and construct the instance with placement new. @@ -267,41 +269,103 @@ PyRecord::~PyRecord() PyObject *PyRecord::tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - PyObject *item, *obGuid, *obInfoGuid; + PyObject *item, *guidUnicode; + PyTypeObject *registeredType; int major, minor, lcid; GUID guid, infoGuid; if (type == &PyRecord::Type) - // If the base 'com_record' type was called try to get the - // information required for instance creation from the call parameters. { - if (!PyArg_ParseTuple(args, "OiiiO:__new__", - &obGuid, // @pyparm |iid||The GUID of the type library - &major, // @pyparm int|verMajor||The major version number of the type lib. - &minor, // @pyparm int|verMinor||The minor version number of the type lib. - &lcid, // @pyparm int|lcid||The LCID of the type lib. - &obInfoGuid)) // @pyparm |infoIID||The GUID of the record info in the library + PyErr_SetString(PyExc_TypeError, "Can't instantiate base class com_record. " + "Use the factory function win32com.client.Record instead."); + return NULL; + } + // For subclasses of com_record try to get the record type information from the class variables of the derived type. + else { + if (!(guidUnicode = PyDict_GetItemString(type->tp_dict, "GUID"))) { + PyErr_Format(PyExc_AttributeError, "Missing %s class attribute.", "GUID"); return NULL; - if (!PyWinObject_AsIID(obGuid, &guid)) + } + if (!PyWinObject_AsIID(guidUnicode, &infoGuid)) { + PyErr_Format(PyExc_ValueError, "Invalid value for %s class attribute.", "GUID"); return NULL; - if (!PyWinObject_AsIID(obInfoGuid, &infoGuid)) + } + if (!(item = PyDict_GetItemString(type->tp_dict, "TLBID"))) { + PyErr_Format(PyExc_AttributeError, "Missing %s class attribute.", "TLBID"); return NULL; + } + if (!PyWinObject_AsIID(item, &guid)) { + PyErr_Format(PyExc_ValueError, "Invalid value for %s class attribute.", "TLBID"); + return NULL; + } + if (!(item = PyDict_GetItemString(type->tp_dict, "MJVER"))) { + PyErr_Format(PyExc_AttributeError, "Missing %s class attribute.", "MJVER"); + return NULL; + } + if (((major = PyLong_AsLong(item)) == -1)) { + PyErr_Format(PyExc_ValueError, "Invalid value for %s class attribute.", "MJVER"); + return NULL; + } + if (!(item = PyDict_GetItemString(type->tp_dict, "MNVER"))) { + PyErr_Format(PyExc_AttributeError, "Missing %s class attribute.", "MNVER"); + return NULL; + } + if (((minor = PyLong_AsLong(item)) == -1)) { + PyErr_Format(PyExc_ValueError, "Invalid value for %s class attribute.", "MNVER"); + return NULL; + } + if (!(item = PyDict_GetItemString(type->tp_dict, "LCID"))) { + PyErr_Format(PyExc_AttributeError, "Missing %s class attribute.", "LCID"); + return NULL; + } + if (((lcid = PyLong_AsLong(item)) == -1)) { + PyErr_Format(PyExc_ValueError, "Invalid value for %s class attribute.", "LCID"); + return NULL; + } } - // Otherwise try to get the information from the class variables of the derived type. - else if (!(item = PyDict_GetItemString(type->tp_dict, "GUID")) || !PyWinObject_AsIID(item, &infoGuid) || - !(item = PyDict_GetItemString(type->tp_dict, "TLBID")) || !PyWinObject_AsIID(item, &guid) || - !(item = PyDict_GetItemString(type->tp_dict, "MJVER")) || ((major = PyLong_AsLong(item)) == -1) || - !(item = PyDict_GetItemString(type->tp_dict, "MNVER")) || ((minor = PyLong_AsLong(item)) == -1) || - !(item = PyDict_GetItemString(type->tp_dict, "LCID")) || ((lcid = PyLong_AsLong(item)) == -1)) + // Instances can only be created for registerd subclasses. + registeredType = (PyTypeObject *)PyDict_GetItem(g_obPyCom_MapRecordGUIDToRecordClass, guidUnicode); + if (!(registeredType && type == registeredType)) { + PyErr_Format(PyExc_TypeError, "Can't instantiate class %s because it is not registered.", type->tp_name); return NULL; + } IRecordInfo *ri = NULL; HRESULT hr = GetRecordInfoFromGuids(guid, major, minor, lcid, infoGuid, &ri); if (FAILED(hr)) return PyCom_BuildPyException(hr); - PyObject *ret = PyObject_FromRecordInfo(ri, NULL, 0); + PyObject *ret = PyObject_FromRecordInfo(ri, NULL, 0, type); ri->Release(); return ret; } +int PyRecord::tp_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + PyRecord *pyrec = (PyRecord *)self; + PyObject *obdata = NULL; + if (!PyArg_ParseTuple(args, "|O:__init__", &obdata)) // @pyparm string or buffer|data|None|The raw data to initialize the record with. + return -1; + if (obdata != NULL) { + PyWinBufferView pybuf(obdata, false, false); // None not ok + if (!pybuf.ok()) + return -1; + ULONG cb; + HRESULT hr = pyrec->pri->GetSize(&cb); + if (FAILED(hr)) { + PyCom_BuildPyException(hr, pyrec->pri, IID_IRecordInfo); + return -1; + } + if (pybuf.len() != cb) { + PyErr_Format(PyExc_ValueError, "Expecting a string of %d bytes (got %d)", cb, pybuf.len()); + return -1; + } + hr = pyrec->pri->RecordCopy(pybuf.ptr(), pyrec->pdata); + if (FAILED(hr)) { + PyCom_BuildPyException(hr, pyrec->pri, IID_IRecordInfo); + return -1; + } + } + return 0; +} + PyTypeObject PyRecord::Type = { PYWIN_OBJECT_HEAD "com_record", sizeof(PyRecord), @@ -337,7 +401,7 @@ PyTypeObject PyRecord::Type = { 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ - 0, /* tp_init */ + (initproc)PyRecord::tp_init, /* tp_init */ 0, /* tp_alloc */ (newfunc)PyRecord::tp_new, /* tp_new */ }; diff --git a/com/win32com/src/include/PyRecord.h b/com/win32com/src/include/PyRecord.h index 9b5670b839..28bd26a3d0 100644 --- a/com/win32com/src/include/PyRecord.h +++ b/com/win32com/src/include/PyRecord.h @@ -11,8 +11,9 @@ class PyRecord : public PyObject { PyRecord(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner); ~PyRecord(); - static PyRecord *new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner); + static PyRecord *new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner, PyTypeObject *type = NULL); static PyObject *tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + static int tp_init(PyObject *self, PyObject *args, PyObject *kwds); static void tp_dealloc(PyRecord *ob); static PyObject *getattro(PyObject *self, PyObject *obname); static int setattro(PyObject *self, PyObject *obname, PyObject *v); diff --git a/com/win32com/src/oleargs.cpp b/com/win32com/src/oleargs.cpp index c30c99694c..3800bc39f8 100644 --- a/com/win32com/src/oleargs.cpp +++ b/com/win32com/src/oleargs.cpp @@ -6,7 +6,7 @@ #include "PythonCOM.h" #include "PyRecord.h" -extern PyObject *PyObject_FromRecordInfo(IRecordInfo *, void *, ULONG); +extern PyObject *PyObject_FromRecordInfo(IRecordInfo *, void *, ULONG, PyTypeObject *type = NULL); extern PyObject *PyObject_FromSAFEARRAYRecordInfo(SAFEARRAY *psa); extern BOOL PyObject_AsVARIANTRecordInfo(PyObject *ob, VARIANT *pv); extern BOOL PyRecord_Check(PyObject *ob); diff --git a/com/win32com/test/testPyComTest.py b/com/win32com/test/testPyComTest.py index f53bedcb50..39835803dc 100644 --- a/com/win32com/test/testPyComTest.py +++ b/com/win32com/test/testPyComTest.py @@ -517,9 +517,15 @@ def TestGenerated(): progress("Testing registration of pythoncom.com_record subclasses.") # Instantiating a pythoncom.com_record subclass, which has proper GUID attributes, - # does return an instance of the base class, as long as we have not registered it. - r_base = TestStruct1() - assert type(r_base) is pythoncom.com_record + # does raise a TypeError, as long as we have not registered it. + try: + r_sub = TestStruct1() + except TypeError: + pass + except Exception as e: + raise AssertionError from e + else: + raise AssertionError # Register the subclasses in pythoncom. register_record_class(TestStruct1) register_record_class(ArrayOfStructsTestStruct) @@ -546,6 +552,12 @@ def TestGenerated(): assert type(test_rec) is ArrayOfStructsTestStruct TestArrayOfStructs(o, test_rec) + # Test initialization of registered pythoncom.com_record subclasses. + progress("Testing initialization of pythoncom.com_record subclasses.") + buf = o.GetStruct().__reduce__()[1][5] + test_rec = TestStruct1(buf) + assert test_rec.int_value == 99 and str(test_rec.str_value) == "Hello from C++" + # XXX - this is failing in dynamic tests, but should work fine. i1, i2 = o.GetMultipleInterfaces() # Yay - is now an instance returned!