From 1bd956e804350ad3be2f25886bd3dd3dc092dad5 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:14 +0900 Subject: [PATCH 1/9] test: Add `CoVariantErrorTest` component for `VT_ERROR` `VARIANT` marshalling tests. - Introduce a C++ native COM test double for creating and reading `VT_ERROR` `VARIANT`s. - Ensure test double validates error variant marshalling across Python and COM boundaries. --- source/CppTestSrv/CoVariantErrorTest.cpp | 68 ++++++++++++++++++++++++ source/CppTestSrv/CoVariantErrorTest.h | 20 +++++++ source/CppTestSrv/MAKEFILE | 7 ++- source/CppTestSrv/SERVER.CPP | 7 +++ source/CppTestSrv/SERVER.IDL | 30 +++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 source/CppTestSrv/CoVariantErrorTest.cpp create mode 100644 source/CppTestSrv/CoVariantErrorTest.h 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 ; + } ; } ; From 300240b9740ba2c90742b224d7b670b41235dc97 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:14 +0900 Subject: [PATCH 2/9] feat: handle `VT_ERROR` `VARIANT` without `VariantClear` - Added early return for VT_ERROR in `tagVARIANT.__ctypes_from_outparam__`. - So `scode` is inline value, no extra free needed by caller. - Added test suite `test_variant_error` covering error variant behavior. --- comtypes/automation.py | 7 +++- comtypes/test/test_variant_error.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 comtypes/test/test_variant_error.py diff --git a/comtypes/automation.py b/comtypes/automation.py index acd4e86e..ffee3068 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -533,7 +533,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] @@ -561,6 +560,12 @@ def __getitem__(self, index): def __ctypes_from_outparam__(self): # XXX Manual resource management, because of the VARIANT bug: + if self.vt == VT_ERROR: + # Keep the original VARIANT. Its scode is an inline value, + # not a pointer to an owned resource. + # Letting Python automatically release the VARIANT is enough; + # VariantClear is not needed here or for the caller. + return self result = self.value self.value = None return result diff --git a/comtypes/test/test_variant_error.py b/comtypes/test/test_variant_error.py new file mode 100644 index 00000000..f67cc271 --- /dev/null +++ b/comtypes/test/test_variant_error.py @@ -0,0 +1,50 @@ +"""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 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 + ) + + +if __name__ == "__main__": + unittest.main() From b8413ef16d086c9ff7dc9d393dd947ffc0384bcb Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:14 +0900 Subject: [PATCH 3/9] feat: Support `VT_ERROR` `VARIANT` with `scode` field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `scode` field to `tagVARIANT` (`U_VARIANT2`) to expose error codes directly. - The addition of `scode` does not change the size or memory layout of `tagVARIANT` because it occupies the existing union slot, preserving binary layout. - No backward‑compatible breaking changes; existing code continues to work unchanged. --- comtypes/automation.py | 3 +++ comtypes/test/test_variant.py | 11 ++++++++--- comtypes/test/test_variant_error.py | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/comtypes/automation.py b/comtypes/automation.py index ffee3068..7cf6c5c7 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), @@ -454,6 +455,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._.scode elif vt == VT_BSTR: return self._.bstrVal elif vt == VT_DATE: diff --git a/comtypes/test/test_variant.py b/comtypes/test/test_variant.py index 599cbf49..3820d032 100644 --- a/comtypes/test/test_variant.py +++ b/comtypes/test/test_variant.py @@ -19,7 +19,7 @@ pointer, ) -from comtypes import GUID, IUnknown +from comtypes import GUID, IUnknown, hresult from comtypes.automation import ( DISPPARAMS, VARIANT, @@ -66,8 +66,7 @@ def test_constants(self): missing = VARIANT.missing self.assertEqual(missing.vt, VT_ERROR) - with self.assertRaises(NotImplementedError): - missing.value + self.assertEqual(missing.value, hresult.DISP_E_PARAMNOTFOUND) def test_com_refcounts(self): # typelib for oleaut32 @@ -253,9 +252,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): diff --git a/comtypes/test/test_variant_error.py b/comtypes/test/test_variant_error.py index f67cc271..498c2a4d 100644 --- a/comtypes/test/test_variant_error.py +++ b/comtypes/test/test_variant_error.py @@ -44,6 +44,7 @@ def test_returns_errorcode(self): self.assertEqual( self.server.GetErrorCode(VARIANT.missing), hresult.DISP_E_PARAMNOTFOUND ) + self.assertEqual(VARIANT.missing.value, hresult.DISP_E_PARAMNOTFOUND) if __name__ == "__main__": From 9cc4923080fdd2bd0bf8119addccc3a9be5deec7 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:14 +0900 Subject: [PATCH 4/9] fix: Assert `VT_ERROR` HRESULT handling in `VARIANT`. The relevant `VARIANT` layout uses a `U_VARIANT2(Union)` in which `("VT_I4", c_long)` and `("scode", SCODE)` share the same 32-bit storage. In the cached diff, the code first sets `v.vt = VT_ERROR` to declare that this slot is being used as a `VT_ERROR` variant, and then writes `0x80020004` into the underlying field. This value is `0x80020004` in the raw bit pattern, which is the HRESULT bit pattern for `DISP_E_PARAMNOTFOUND`. In Python, that pattern is represented as `2147549188`, but when read as a signed 32-bit integer it becomes `-2147352572`, which is exactly `hresult.DISP_E_PARAMNOTFOUND`. The key point is that the same 32-bit storage is interpreted differently depending on the active `vt` tag: when the tag is `VT_ERROR`, the value is read back as `scode`. Therefore, the real behavior is: - `v.vt = VT_ERROR` declares that this `VARIANT` is an error value. - `v._.VT_I4 = 0x80020004` writes the HRESULT bit pattern into the shared union storage. - `v.value` reads that same storage as `scode`. - `v.value == hresult.DISP_E_PARAMNOTFOUND` is therefore true. This means the existing code was not wrong to assign the value to `v._.VT_I4`. The issue discussion was based on a misunderstanding of how `VARIANT` stores `VT_ERROR` values, not on a real defect in the code itself. --- comtypes/automation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comtypes/automation.py b/comtypes/automation.py index 7cf6c5c7..10966b2c 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -610,6 +610,7 @@ def ChangeType(self, typecode): VARIANT.missing = v = VARIANT() v.vt = VT_ERROR v._.VT_I4 = 0x80020004 +assert v.value == hresult.DISP_E_PARAMNOTFOUND del v From 546c56c593876c2b3a9df96c8cce24bca2b58cf8 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:15 +0900 Subject: [PATCH 5/9] feat: Add a messsage to the `VARIANT.missing.value` assertion. --- comtypes/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comtypes/automation.py b/comtypes/automation.py index 10966b2c..c9ae2567 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -610,7 +610,7 @@ def ChangeType(self, typecode): VARIANT.missing = v = VARIANT() v.vt = VT_ERROR v._.VT_I4 = 0x80020004 -assert v.value == hresult.DISP_E_PARAMNOTFOUND +assert v.value == hresult.DISP_E_PARAMNOTFOUND, "the shared 32-bit storage problem" del v From 000befd11d48d0d9783c9c8c4bdc2bc95051205f Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:15 +0900 Subject: [PATCH 6/9] test: Add variant error value regression test. --- comtypes/test/test_variant_error.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/comtypes/test/test_variant_error.py b/comtypes/test/test_variant_error.py index 498c2a4d..634441ea 100644 --- a/comtypes/test/test_variant_error.py +++ b/comtypes/test/test_variant_error.py @@ -27,6 +27,23 @@ def _create_server() -> "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.value) + + @unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") class TestGetErrorCode(unittest.TestCase): def setUp(self): From f4aee26ef1115d3f278c7881ac3099bb8a3511e4 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:15 +0900 Subject: [PATCH 7/9] fix: Return `VARIANT` objects for `VT_ERROR` values. Make `VARIANT._get_value()` return the original `VARIANT` for `VT_ERROR`. Do not turn its `scode` into a plain integer. Excel returns `=1/0` as an error cell with `VT_ERROR` and an HRESULT. If `Range.Value()` returns only `-2146826281`, users cannot tell an error code from a real integer. Now `Range.Value()` returns `VARIANT(vt=0xa, -2146826281)`, and `WorksheetFunction.IsError()` returns `True`. The negative number is the signed form of COM HRESULT `0x800A07D7`. It is the error value for Excel `#DIV/0!`. This fixes the old `NotImplementedError` while keeping the error type. Remove the old `VT_ERROR` branch in `__ctypes_from_outparam__()`. `scode` is an inline value, not a pointer, so returning the `VARIANT` keeps the error data. No special resource cleanup is needed. Update the `VARIANT.missing` checks to use `._.scode`, not `.value`. Also update the regression tests to check the error value and the object. --- comtypes/automation.py | 16 +++++++--------- comtypes/test/test_variant.py | 3 ++- comtypes/test/test_variant_error.py | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/comtypes/automation.py b/comtypes/automation.py index c9ae2567..ab2f99d1 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -215,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): @@ -456,7 +460,7 @@ def _get_value(self, dynamic=False): elif vt == VT_BOOL: return self._.VT_BOOL elif vt == VT_ERROR: - return self._.scode + return self elif vt == VT_BSTR: return self._.bstrVal elif vt == VT_DATE: @@ -563,12 +567,6 @@ def __getitem__(self, index): def __ctypes_from_outparam__(self): # XXX Manual resource management, because of the VARIANT bug: - if self.vt == VT_ERROR: - # Keep the original VARIANT. Its scode is an inline value, - # not a pointer to an owned resource. - # Letting Python automatically release the VARIANT is enough; - # VariantClear is not needed here or for the caller. - return self result = self.value self.value = None return result @@ -610,7 +608,7 @@ def ChangeType(self, typecode): VARIANT.missing = v = VARIANT() v.vt = VT_ERROR v._.VT_I4 = 0x80020004 -assert v.value == hresult.DISP_E_PARAMNOTFOUND, "the shared 32-bit storage problem" +assert v._.scode == hresult.DISP_E_PARAMNOTFOUND, "the shared 32-bit storage problem" del v diff --git a/comtypes/test/test_variant.py b/comtypes/test/test_variant.py index 3820d032..d83ba00a 100644 --- a/comtypes/test/test_variant.py +++ b/comtypes/test/test_variant.py @@ -66,7 +66,8 @@ def test_constants(self): missing = VARIANT.missing self.assertEqual(missing.vt, VT_ERROR) - self.assertEqual(missing.value, hresult.DISP_E_PARAMNOTFOUND) + self.assertIs(missing.value, missing) + self.assertEqual(missing._.scode, hresult.DISP_E_PARAMNOTFOUND) def test_com_refcounts(self): # typelib for oleaut32 diff --git a/comtypes/test/test_variant_error.py b/comtypes/test/test_variant_error.py index 634441ea..d4ce555d 100644 --- a/comtypes/test/test_variant_error.py +++ b/comtypes/test/test_variant_error.py @@ -41,7 +41,7 @@ def test_returns_error_code(self): ]: with self.subTest(errcode=errcode): errvar = self.server.CreateVariantError(errcode) - self.assertEqual(errcode, errvar.value) + self.assertEqual(errcode, errvar._.scode) @unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") @@ -61,7 +61,7 @@ def test_returns_errorcode(self): self.assertEqual( self.server.GetErrorCode(VARIANT.missing), hresult.DISP_E_PARAMNOTFOUND ) - self.assertEqual(VARIANT.missing.value, hresult.DISP_E_PARAMNOTFOUND) + self.assertEqual(VARIANT.missing._.scode, hresult.DISP_E_PARAMNOTFOUND) if __name__ == "__main__": From f414b001b613b5641195c38f4c35ca40037dff9e Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 11:35:15 +0900 Subject: [PATCH 8/9] test: Check Excel error cells return `VARIANT` objects. Add an Excel test for a cell with the formula `=1/0`. Excel should return a `VARIANT` with `VT_ERROR`, not a plain integer. This test checks that `Range.Value()` keeps the error type. It also checks that its `scode` is HRESULT `0x800A07D7`. That HRESULT is the signed error value for Excel `#DIV/0!`. The test protects the new behavior from future changes. --- comtypes/test/test_excel.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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]) From 121f60be0bcc5bab1fb741a0f083fa0fa156c8cc Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 12 Oct 2026 12:40:20 +0900 Subject: [PATCH 9/9] test: Cover unsupported `VARIANT` value types. Ensure unsupported `VARIANT` types retain their explicit `NotImplementedError` behavior. --- comtypes/test/test_variant.py | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/comtypes/test/test_variant.py b/comtypes/test/test_variant.py index d83ba00a..5bbdb608 100644 --- a/comtypes/test/test_variant.py +++ b/comtypes/test/test_variant.py @@ -23,24 +23,47 @@ 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 @@ -286,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