Skip to content

Commit 876801f

Browse files
jonschzJonathanjunkmd
authored
Fix old index bug in call_with_inout (enthought#473)
* bugfix in `call_with_inout` * minor cleanup * handling the case of no in and no out * Test case for _fix_inout_args * additional cleanup and error handling * code formatting fixed * fix python 3.7 and 3.8 compatibility * Temporary addition of real-world test * code cleanup * intermediate commit, do not review * Refactor of unit test, removing portdevice test * fix global side-effect of other skipped test * Update comtypes/test/test_outparam.py Co-authored-by: Jun Komoda <[email protected]> * work on tests for inout_args and outparam - cleanup for test_outparam.py - improvements to test_inout_args.py - comments on a possible error in _memberspec.py * removing dead code * rename variables and add assertions * pass `MagicMock` instead of `ut.TestCase` * make tests for each argument passing patterns * remove duplicated comments * update test code for readability - remove name from mock - move line breaks to between mock preparations and assertions * split the testcases * add `Test_Error` * minor corrections, remove redundancy, migration - rewrite the permutations test - missing direction and omitted name redundant - migrate autogenerated keywords - TBD: more real life tests * Add tests covering 24 patterns - instead of using `if` statements and `permutations` * update test name * add real world tests, remove old code * formatting issue * Update comtypes/_memberspec.py dict type annotation Co-authored-by: Jun Komoda <[email protected]> * Change missing 'in' or 'out' to be treated as 'in' * Add real-world test: param without 'in' or 'out' * add `contextlib.redirect_stdout` to supress warnings * apply review feedback * update comments * add line breaks to lines longer than 88 characters * Update comtypes/test/test_inout_args.py --------- Co-authored-by: jonschz <[email protected]> Co-authored-by: Jonathan <[email protected]> Co-authored-by: Jun Komoda <[email protected]>
1 parent 07cf96d commit 876801f

File tree

4 files changed

+584
-32
lines changed

4 files changed

+584
-32
lines changed

comtypes/_memberspec.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -151,60 +151,93 @@ def _fix_inout_args(
151151
def call_with_inout(self, *args, **kw):
152152
args = list(args)
153153
# Indexed by order in the output
154-
outargs = {}
154+
outargs: Dict[int, _UnionT[_CData, "ctypes._CArgObject"]] = {}
155155
outnum = 0
156+
param_index = 0
157+
# Go through all expected arguments and match them to the provided arguments.
158+
# `param_index` first counts through the positional and then
159+
# through the keyword arguments.
156160
for i, info in enumerate(paramflags):
157161
direction = info[0]
158-
if direction & 3 == 3:
162+
dir_in = direction & 1 == 1
163+
dir_out = direction & 2 == 2
164+
is_positional = param_index < len(args)
165+
if not (dir_in or dir_out):
166+
# The original code here did not check for this special case and
167+
# effectively treated `(dir_in, dir_out) == (False, False)` and
168+
# `(dir_in, dir_out) == (True, False)` the same.
169+
# In order not to break legacy code we do the same.
170+
# One example of a function that has neither `dir_in` nor `dir_out`
171+
# set is `IMFAttributes.GetString`.
172+
dir_in = True
173+
if dir_in and dir_out:
159174
# This is an [in, out] parameter.
160175
#
161176
# Determine name and required type of the parameter.
162177
name = info[1]
163178
# [in, out] parameters are passed as pointers,
164179
# this is the pointed-to type:
165-
atyp = argtypes[i]._type_
180+
atyp: Type[_CData] = getattr(argtypes[i], "_type_")
166181

167182
# Get the actual parameter, either as positional or
168183
# keyword arg.
169-
try:
170-
try:
171-
v = args[i]
172-
except IndexError:
173-
v = kw[name]
174-
except KeyError:
175-
# no parameter was passed, make an empty one
176-
# of the required type
177-
v = atyp()
178-
else:
179-
# parameter was passed, call .from_param() to
180-
# convert it to a ctypes type.
184+
185+
def prepare_parameter(v):
186+
# parameter was passed, call `from_param()` to
187+
# convert it to a `ctypes` type.
181188
if getattr(v, "_type_", None) is atyp:
182-
# Array of or pointer to type 'atyp' was
183-
# passed, pointer to 'atyp' expected.
189+
# Array of or pointer to type `atyp` was passed,
190+
# pointer to `atyp` expected.
184191
pass
185192
elif type(atyp) is SIMPLETYPE:
186-
# The from_param method of simple types
187-
# (c_int, c_double, ...) returns a byref()
188-
# object which we cannot use since later
189-
# it will be wrapped in a pointer. Simply
190-
# call the constructor with the argument
191-
# in that case.
193+
# The `from_param` method of simple types
194+
# (`c_int`, `c_double`, ...) returns a `byref` object which
195+
# we cannot use since later it will be wrapped in a pointer.
196+
# Simply call the constructor with the argument in that case.
192197
v = atyp(v)
193198
else:
194199
v = atyp.from_param(v)
195200
assert not isinstance(v, BYREFTYPE)
196-
outargs[outnum] = v
197-
outnum += 1
198-
if len(args) > i:
199-
args[i] = v
200-
else:
201+
return v
202+
203+
if is_positional:
204+
v = prepare_parameter(args[param_index])
205+
args[param_index] = v
206+
elif name in kw:
207+
v = prepare_parameter(kw[name])
201208
kw[name] = v
202-
elif direction & 2 == 2:
209+
else:
210+
# no parameter was passed, make an empty one of the required type
211+
# and pass it as a keyword argument
212+
v = atyp()
213+
if name is not None:
214+
kw[name] = v
215+
else:
216+
raise TypeError("Unnamed inout parameters cannot be omitted")
217+
outargs[outnum] = v
218+
if dir_out:
203219
outnum += 1
220+
if dir_in:
221+
param_index += 1
222+
204223
rescode = func(self, *args, **kw)
205224
# If there is only a single output value, then do not expect it to
206225
# be iterable.
226+
227+
# Our interpretation of this code
228+
# (jonschz, junkmd, see https://github.com/enthought/comtypes/pull/473):
229+
# - `outnum` counts the total number of 'out' and 'inout' arguments.
230+
# - `outargs` is a dict consisting of the supplied 'inout' arguments.
231+
# - The call to `func()` returns the 'out' and 'inout' arguments.
232+
# Furthermore, it changes the variables in 'outargs' as a "side effect"
233+
# - In a perfect world, it should be fine to just return `rescode`.
234+
# But we assume there is a reason why the original authors did not do that.
235+
# Instead, they replace the 'inout' variables in `rescode` by those in
236+
# 'outargs', and call `__ctypes_from_outparam__()` on them.
237+
207238
if outnum == 1: # rescode is not iterable
239+
# In this case, it is little faster than creating list with
240+
# `rescode = [rescode]` and getting item with index from the list.
208241
if len(outargs) == 1:
209242
rescode = rescode.__ctypes_from_outparam__()
210243
return rescode

comtypes/test/test_imfattributes.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import contextlib
2+
import unittest as ut
3+
4+
from ctypes import POINTER, pointer, windll
5+
from comtypes import GUID
6+
import comtypes.client
7+
8+
9+
class Test_IMFAttributes(ut.TestCase):
10+
def test_imfattributes(self):
11+
with contextlib.redirect_stdout(None): # supress warnings, see test_client.py
12+
comtypes.client.GetModule("msvidctl.dll")
13+
from comtypes.gen import MSVidCtlLib
14+
15+
imf_attrs = POINTER(MSVidCtlLib.IMFAttributes)()
16+
hres = windll.mfplat.MFCreateAttributes(pointer(imf_attrs), 2)
17+
self.assertEqual(hres, 0)
18+
19+
MF_TRANSCODE_ADJUST_PROFILE = GUID("{9c37c21b-060f-487c-a690-80d7f50d1c72}")
20+
set_int_value = 1
21+
# IMFAttributes.SetUINT32() is an example of a function that has a parameter
22+
# without an `in` or `out` direction; see also test_inout_args.py
23+
imf_attrs.SetUINT32(MF_TRANSCODE_ADJUST_PROFILE, set_int_value)
24+
get_int_value = imf_attrs.GetUINT32(MF_TRANSCODE_ADJUST_PROFILE)
25+
self.assertEqual(set_int_value, get_int_value)
26+
27+
28+
if __name__ == "__main__":
29+
ut.main()

0 commit comments

Comments
 (0)