diff --git a/comtypes/automation.py b/comtypes/automation.py index acd4e86e..ab2f99d1 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -170,6 +170,7 @@ class _tagBRECORD(Structure): ("VT_UI4", c_ulong), ("VT_UI8", c_ulonglong), ("VT_UINT", c_uint), + ("scode", SCODE), ("VT_R4", c_float), ("VT_R8", c_double), ("VT_CY", c_longlong), @@ -214,7 +215,11 @@ def __repr__(self): return "VARIANT.empty" elif self is type(self).missing: return "VARIANT.missing" - return f"VARIANT(vt=0x{self.vt:x}, {self.value!r})" + elif self.vt == VT_ERROR: + value = self._.scode + else: + value = self.value + return f"VARIANT(vt=0x{self.vt:x}, {value!r})" @classmethod def from_param(cls, value): @@ -454,6 +459,8 @@ def _get_value(self, dynamic=False): return self._.VT_R8 elif vt == VT_BOOL: return self._.VT_BOOL + elif vt == VT_ERROR: + return self elif vt == VT_BSTR: return self._.bstrVal elif vt == VT_DATE: @@ -533,7 +540,6 @@ def __getitem__(self, index): return v.value # these are missing: - # getter[VT_ERROR] # getter[VT_ARRAY] # getter[VT_BYREF|VT_UI1] # getter[VT_BYREF|VT_I2] @@ -602,6 +608,7 @@ def ChangeType(self, typecode): VARIANT.missing = v = VARIANT() v.vt = VT_ERROR v._.VT_I4 = 0x80020004 +assert v._.scode == hresult.DISP_E_PARAMNOTFOUND, "the shared 32-bit storage problem" del v diff --git a/comtypes/test/test_excel.py b/comtypes/test/test_excel.py index 16f752c9..aabbba13 100644 --- a/comtypes/test/test_excel.py +++ b/comtypes/test/test_excel.py @@ -3,6 +3,7 @@ import unittest from typing import ClassVar +from comtypes import automation, hresult from comtypes.client import CreateObject, GetModule ################################################################ @@ -119,6 +120,14 @@ def test(self): sh.Range[sh.Cells.Item[1, 1], sh.Cells.Item[3, 3]].Copy(sh.Cells.Item[4, 1]) sh.Range[sh.Cells.Item[4, 1], sh.Cells.Item[6, 3]].Select() + # Error Variant + err_rng = sh.Range("A1") + err_rng.Formula = "=1/0" + self.assertIsInstance(err_rng.Value(), automation.VARIANT) # type: ignore + self.assertEqual( # type: ignore + err_rng.Value()._.scode, hresult.signed32bithex_to_int("0x800A07D7") + ) + PY_VER = "Python {0}.{1}.{2}".format(*sys.version_info[:3]) diff --git a/comtypes/test/test_variant.py b/comtypes/test/test_variant.py index 599cbf49..5bbdb608 100644 --- a/comtypes/test/test_variant.py +++ b/comtypes/test/test_variant.py @@ -19,28 +19,51 @@ pointer, ) -from comtypes import GUID, IUnknown +from comtypes import GUID, IUnknown, hresult from comtypes.automation import ( DISPPARAMS, VARIANT, + VT_BLOB, + VT_BLOB_OBJECT, VT_BSTR, VT_BYREF, + VT_CARRAY, + VT_CF, + VT_CLSID, VT_CY, VT_DATE, VT_DECIMAL, VT_EMPTY, VT_ERROR, + VT_FILETIME, + VT_HRESULT, VT_I1, VT_I2, VT_I4, VT_I8, + VT_ILLEGALMASKED, + VT_INT_PTR, + VT_LPSTR, + VT_LPWSTR, VT_NULL, + VT_PTR, VT_R4, VT_R8, + VT_RESERVED, + VT_SAFEARRAY, + VT_STORAGE, + VT_STORED_OBJECT, + VT_STREAM, + VT_STREAMED_OBJECT, VT_UI1, VT_UI2, VT_UI4, VT_UI8, + VT_UINT_PTR, + VT_USERDEFINED, + VT_VECTOR, + VT_VERSIONED_STREAM, + VT_VOID, ) from comtypes.test.find_memleak import find_memleak from comtypes.typeinfo import LoadRegTypeLib @@ -66,8 +89,8 @@ def test_constants(self): missing = VARIANT.missing self.assertEqual(missing.vt, VT_ERROR) - with self.assertRaises(NotImplementedError): - missing.value + self.assertIs(missing.value, missing) + self.assertEqual(missing._.scode, hresult.DISP_E_PARAMNOTFOUND) def test_com_refcounts(self): # typelib for oleaut32 @@ -253,9 +276,15 @@ def test_repr(self): self.assertEqual( repr(VARIANT(byref(c_int(42)))), "VARIANT(vt=0x4003, byref(42))" ) + + def test_repr_error(self): self.assertEqual(repr(VARIANT.empty), "VARIANT.empty") self.assertEqual(repr(VARIANT.null), "VARIANT.null") self.assertEqual(repr(VARIANT.missing), "VARIANT.missing") + v = VARIANT() + v.vt = VT_ERROR + v._.scode = hresult.DISP_E_MEMBERNOTFOUND + self.assertEqual(repr(v), f"VARIANT(vt=0xa, {hresult.DISP_E_MEMBERNOTFOUND})") class ArrayTest(unittest.TestCase): @@ -280,6 +309,46 @@ def test_int(self): self.assertEqual(v.value, (1, 1, 1, 1)) +class UnsupportedVariantTestCase(unittest.TestCase): + def _create_variant(self, vt: int) -> VARIANT: + variant = VARIANT() + variant.vt = vt + return variant + + def test_value_unsupported_types(self): + unsupported_types = ( + VT_VOID, + VT_HRESULT, + VT_PTR, + VT_SAFEARRAY, + VT_CARRAY, + VT_USERDEFINED, + VT_LPSTR, + VT_LPWSTR, + VT_INT_PTR, + VT_UINT_PTR, + VT_FILETIME, + VT_BLOB, + VT_STREAM, + VT_STORAGE, + VT_STREAMED_OBJECT, + VT_STORED_OBJECT, + VT_BLOB_OBJECT, + VT_CF, + VT_CLSID, + VT_VERSIONED_STREAM, + VT_VECTOR, + VT_RESERVED, + VT_ILLEGALMASKED, + ) + for vt in unsupported_types: + variant = self._create_variant(vt) + with self.subTest(vt=vt), self.assertRaises(NotImplementedError): + variant.value + # Supress `Exception ignored while calling deallocator: ` + variant.vt = VT_EMPTY + + ################################################################ def run_test(rep, msg, func=None, previous={}, results={}): # items = [None] * rep diff --git a/comtypes/test/test_variant_error.py b/comtypes/test/test_variant_error.py new file mode 100644 index 00000000..d4ce555d --- /dev/null +++ b/comtypes/test/test_variant_error.py @@ -0,0 +1,68 @@ +"""VT_ERROR VARIANT round-trip tests via the native C++ COM test server.""" + +import unittest +from _ctypes import COMError + +from comtypes import CLSCTX_LOCAL_SERVER, hresult +from comtypes.automation import VARIANT +from comtypes.client import CreateObject, GetModule + +try: + GetModule(("{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}", 1, 0, 0)) + from comtypes.gen.ComtypesCppTestSrvLib import ( + CoVariantErrorTest, + IVariantErrorTest, + ) + + IMPORT_SERVER_FAILED = False +except (ImportError, OSError): + IMPORT_SERVER_FAILED = True + + +def _create_server() -> "IVariantErrorTest": + return CreateObject( + CoVariantErrorTest, + clsctx=CLSCTX_LOCAL_SERVER, + interface=IVariantErrorTest, + ) + + +@unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") +class TestCreateVariantError(unittest.TestCase): + def setUp(self): + self.server = _create_server() + + def test_returns_error_code(self): + for errcode in [ + hresult.DISP_E_TYPEMISMATCH, + hresult.DISP_E_BADINDEX, + hresult.DISP_E_UNKNOWNNAME, + hresult.DISP_E_OVERFLOW, + ]: + with self.subTest(errcode=errcode): + errvar = self.server.CreateVariantError(errcode) + self.assertEqual(errcode, errvar._.scode) + + +@unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") +class TestGetErrorCode(unittest.TestCase): + def setUp(self): + self.server = _create_server() + + def test_raises_type_mismatch(self): + """Passing a NON VT_ERROR VARIANT raises COMError(DISP_E_TYPEMISMATCH).""" + for value in [VARIANT(123), VARIANT("hello"), VARIANT()]: + with self.subTest(value=value): + with self.assertRaises(COMError) as cm: + self.server.GetErrorCode(value) + self.assertEqual(cm.exception.hresult, hresult.DISP_E_TYPEMISMATCH) + + def test_returns_errorcode(self): + self.assertEqual( + self.server.GetErrorCode(VARIANT.missing), hresult.DISP_E_PARAMNOTFOUND + ) + self.assertEqual(VARIANT.missing._.scode, hresult.DISP_E_PARAMNOTFOUND) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/CppTestSrv/CoVariantErrorTest.cpp b/source/CppTestSrv/CoVariantErrorTest.cpp new file mode 100644 index 00000000..0a36c0d9 --- /dev/null +++ b/source/CppTestSrv/CoVariantErrorTest.cpp @@ -0,0 +1,68 @@ +// CoVariantErrorTest.cpp +// Test double for VT_ERROR VARIANT marshalling. +// +// CreateVariantError: constructs a VARIANT entirely in native C++ — sets vt +// to VT_ERROR and scode to errorCode directly. This matches what VBA's +// CVErr() produces. +// GetErrorCode: reads the SCODE from a VT_ERROR VARIANT and returns it as a +// signed LONG. This matches what Excel VBA's CLng(myErrCell.Value). +#include +#include "CoVariantErrorTest.h" + + +HRESULT CVariantErrorTest::CreateInstance(IUnknown* pUnknownOuter, + CUnknown** ppNewComponent) +{ + if (pUnknownOuter != NULL) + { + return CLASS_E_NOAGGREGATION ; + } + *ppNewComponent = new CVariantErrorTest(pUnknownOuter) ; + return S_OK ; +} + +HRESULT __stdcall CVariantErrorTest::NondelegatingQueryInterface(const IID& iid, + void** ppv) +{ + if (iid == IID_IVariantErrorTest) + { + return FinishQI(static_cast(this), ppv) ; + } + return CUnknown::NondelegatingQueryInterface(iid, ppv) ; +} + +// CreateVariantError +// Sets *result to VT_ERROR with the given SCODE. +HRESULT __stdcall CVariantErrorTest::CreateVariantError(LONG errorCode, VARIANT* result) +{ + if (result == NULL) + { + return E_POINTER ; + } + + VariantInit(result) ; + + result->vt = VT_ERROR ; + result->scode = static_cast(errorCode) ; + + return S_OK ; +} + +// GetErrorCode +// Extracts the SCODE from a VT_ERROR VARIANT. +// Returns DISP_E_TYPEMISMATCH if the variant is not VT_ERROR. +HRESULT __stdcall CVariantErrorTest::GetErrorCode(VARIANT value, LONG* errorCode) +{ + if (errorCode == NULL) + { + return E_POINTER ; + } + + if (value.vt != VT_ERROR) + { + return DISP_E_TYPEMISMATCH ; + } + + *errorCode = value.scode ; + return S_OK ; +} diff --git a/source/CppTestSrv/CoVariantErrorTest.h b/source/CppTestSrv/CoVariantErrorTest.h new file mode 100644 index 00000000..6eacaf41 --- /dev/null +++ b/source/CppTestSrv/CoVariantErrorTest.h @@ -0,0 +1,20 @@ +// CoVariantErrorTest.h +// Test double for VT_ERROR VARIANT marshalling. +#include "CUnknown.h" +#include "Iface.h" + +class CVariantErrorTest : public CUnknown, public IVariantErrorTest +{ +public: + static HRESULT CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) ; + +private: + DECLARE_IUNKNOWN + virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; + + // Interface IVariantErrorTest + virtual HRESULT __stdcall CreateVariantError(LONG errorCode, VARIANT* result) ; + virtual HRESULT __stdcall GetErrorCode(VARIANT value, LONG* errorCode) ; + + CVariantErrorTest(IUnknown* pUnknownOuter) : CUnknown(pUnknownOuter) {} ; +} ; diff --git a/source/CppTestSrv/MAKEFILE b/source/CppTestSrv/MAKEFILE index 0c5c6afd..13d66b1c 100644 --- a/source/CppTestSrv/MAKEFILE +++ b/source/CppTestSrv/MAKEFILE @@ -41,7 +41,7 @@ guids.obj : guids.c # Component/server source files # -server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h +server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoVariantErrorTest.h cl $(CPP_FLAGS) server.cpp CoComtypesDispRecordParamTest.obj : CoComtypesDispRecordParamTest.cpp \ @@ -60,6 +60,10 @@ CoStringSafearrayInsideVariantTest.obj : CoStringSafearrayInsideVariantTest.cpp CoStringSafearrayInsideVariantTest.h iface.h registry.h CUnknown.h cl $(CPP_FLAGS) CoStringSafearrayInsideVariantTest.cpp +CoVariantErrorTest.obj : CoVariantErrorTest.cpp \ + CoVariantErrorTest.h iface.h CUnknown.h + cl $(CPP_FLAGS) CoVariantErrorTest.cpp + # # Helper classes # @@ -91,6 +95,7 @@ SERVER_OBJS = Server.obj \ CoComtypesDispSafearrayParamTest.obj \ CoNumericSafearrayTest.obj \ CoStringSafearrayInsideVariantTest.obj \ + CoVariantErrorTest.obj \ Registry.obj \ Cfactory.obj \ Cunknown.obj \ diff --git a/source/CppTestSrv/SERVER.CPP b/source/CppTestSrv/SERVER.CPP index 5b04fb61..90b30795 100644 --- a/source/CppTestSrv/SERVER.CPP +++ b/source/CppTestSrv/SERVER.CPP @@ -12,6 +12,7 @@ #include "CoComtypesDispSafearrayParamTest.h" #include "CoNumericSafearrayTest.h" #include "CoStringSafearrayInsideVariantTest.h" +#include "CoVariantErrorTest.h" /////////////////////////////////////////////////////////// @@ -61,6 +62,12 @@ CFactoryData g_FactoryDataArray[] = L"Comtypes.CoStringSafearrayInsideVariantTest.1", L"Comtypes.CoStringSafearrayInsideVariantTest", &LIBID_ComtypesCppTestSrvLib, + NULL, 0}, + {&CLSID_CoVariantErrorTest, CVariantErrorTest::CreateInstance, + L"Comtypes component for VT_ERROR VARIANT marshalling tests", + L"Comtypes.VariantErrorTest.1", + L"Comtypes.VariantErrorTest", + &LIBID_ComtypesCppTestSrvLib, NULL, 0} } ; int g_cFactoryDataEntries diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index 25e86444..30f26ee5 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -97,6 +97,25 @@ dispinterface IDispSafearrayParamTest interface IDualSafearrayParamTest; } ; +// Interface IVariantErrorTest +// Regression test double for VT_ERROR VARIANT marshalling (issues #325, #821). +// CreateVariantError constructs the VARIANT entirely in native C++ code: +// it sets vt = VT_ERROR and scode = errorCode directly without any +// Python reflection or type-library inference, matching what VBA's CVErr() +// produces. +// GetErrorCode reads the SCODE back out, matching VBA's CLng() on an error +// cell, and returns DISP_E_TYPEMISMATCH for any non-VT_ERROR VARIANT. +[ + object, + uuid(A4421E3A-E17C-496E-B2DF-002C20FEC215), + oleautomation +] +interface IVariantErrorTest : IUnknown +{ + HRESULT CreateVariantError([in] LONG errorCode, [out, retval] VARIANT* result); + HRESULT GetErrorCode([in] VARIANT value, [out, retval] LONG* errorCode); +} ; + // // Component and type library descriptions @@ -183,4 +202,15 @@ library ComtypesCppTestSrvLib { [default] interface IStringSafearrayInsideVariantTest ; } ; + + // CoVariantErrorTest + // Test double for VT_ERROR VARIANT round-trip (issues #325, #821). + [ + uuid(3AD33CCB-69A9-4FFD-A190-468A255D6479), + helpstring("Comtypes component for VT_ERROR VARIANT marshalling tests.") + ] + coclass CoVariantErrorTest + { + [default] interface IVariantErrorTest ; + } ; } ;