Skip to content

Commit

Permalink
Added a method for the 'tp_init' slot of 'com_record' to enable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
geppi committed Jan 13, 2025
1 parent 61662ab commit 44fdad2
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 52 deletions.
10 changes: 9 additions & 1 deletion com/win32com/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
156 changes: 110 additions & 46 deletions com/win32com/src/PyRecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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 <o PyRecord>|pythoncom|GetRecordFromGuids|Creates a new record object from the given GUIDs
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <o PyIID>|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 <o PyIID>|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),
Expand Down Expand Up @@ -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 */
};
Expand Down
3 changes: 2 additions & 1 deletion com/win32com/src/include/PyRecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion com/win32com/src/oleargs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 15 additions & 3 deletions com/win32com/test/testPyComTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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!
Expand Down

0 comments on commit 44fdad2

Please sign in to comment.