Skip to content

Commit cc9a013

Browse files
bennyrowlandBen Rowland
and
Ben Rowland
authored
Require explicit enabling of numpy interop (#337)
* feat: make numpy interop opt-in This commit removes all automatic imports of numpy, and instead adds a new function "enable_numpy_interop()" to comtypes.npsupport. This function is the only one which actually imports numpy, any attempt to use numpy interop related features without calling "enable_numpy_interop()" first will lead to an ImportError being raised with a message explaining that the function needs to be called before using numpy functionality. Other parts of comtypes wishing to access numpy functions should call comtypes.npsupport.get_numpy() which will return the module. * make npsupport.isndarray raise an Exception if interop not enabled Without numpy interop being enabled, we can't directly check if a variable is an ndarray, but having the __array_interface__ attribute is a fairly good estimator, so if an object with that attribute is passed to isndarray before interop is enabled, a ValueError will be raised prompting the user to call npsupport.enable_numpy_interop(). * make safearray_as_ndarray automatically enable np interop Entering the safearray_as_ndarray context manager will internally call npsupport.enable_numpy_interop(), as it is clear that the user wants to have numpy support. early return from repeated calls to npsupport.enable_numpy_interop() Calling enable_numpy_interop() when interop is already enabled returns at the top of the function. * reorganise tests relating to np interop I have gathered all the numpy related tests into a single test file, test_npsupport.py, which has a couple of different TestCases internally, reflecting the original organisation. test_npsupport will only be run if importing numpy succeeds. I also removed a lot of @Skip decorators relating to numpy dependence, all tests currently pass (or are skipped) with or without numpy being installed (on my system at least, Python 3.9.13 and numpy 1.23.1). * fix syntax errors for older Python versions Remove inline type annotations on a couple of functions in test_npsupport.py and an f-string in npsupport.py * refactor to use a singleton class instead of global variables Modify npsupport to put all functionality inside a class Interop, which exposes public interface methods "enable()", "isndarray()", "isdatetime64" and the properties "numpy", "VARIANT_dtype", "typecodes", "datetime64" and "com_null_date64". A singleton instance of the class (called interop) is created in the npsupport namespace, to use numpy interop its "enable()" method should be called. It is also still valid to use the "safearray_as_ndarray" context manager to enable support as well. Co-authored-by: Ben Rowland <[email protected]>
1 parent 57e4f24 commit cc9a013

File tree

8 files changed

+492
-455
lines changed

8 files changed

+492
-455
lines changed

comtypes/automation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,9 @@ def _set_value(self, value):
284284
com_days = delta.days + (delta.seconds + delta.microseconds * 1e-6) / 86400.
285285
self.vt = VT_DATE
286286
self._.VT_R8 = com_days
287-
elif npsupport.isdatetime64(value):
287+
elif npsupport.interop.isdatetime64(value):
288288
com_days = value - npsupport.com_null_date64
289-
com_days /= npsupport.numpy.timedelta64(1, 'D')
289+
com_days /= npsupport.interop.numpy.timedelta64(1, 'D')
290290
self.vt = VT_DATE
291291
self._.VT_R8 = com_days
292292
elif decimal is not None and isinstance(value, decimal.Decimal):
@@ -308,10 +308,10 @@ def _set_value(self, value):
308308
obj = _midlSAFEARRAY(typ).create(value)
309309
memmove(byref(self._), byref(obj), sizeof(obj))
310310
self.vt = VT_ARRAY | obj._vartype_
311-
elif npsupport.isndarray(value):
311+
elif npsupport.interop.isndarray(value):
312312
# Try to convert a simple array of basic types.
313313
descr = value.dtype.descr[0][1]
314-
typ = npsupport.typecodes.get(descr)
314+
typ = npsupport.interop.typecodes.get(descr)
315315
if typ is None:
316316
# Try for variant
317317
obj = _midlSAFEARRAY(VARIANT).create(value)

comtypes/npsupport.py

Lines changed: 153 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,153 @@
1-
""" Consolidation of numpy support utilities. """
2-
import sys
3-
4-
try:
5-
import numpy
6-
except ImportError:
7-
numpy = None
8-
9-
10-
HAVE_NUMPY = numpy is not None
11-
12-
is_64bits = sys.maxsize > 2**32
13-
14-
15-
def _make_variant_dtype():
16-
""" Create a dtype for VARIANT. This requires support for Unions, which is
17-
available in numpy version 1.7 or greater.
18-
19-
This does not support the decimal type.
20-
21-
Returns None if the dtype cannot be created.
22-
23-
"""
24-
25-
# pointer typecode
26-
ptr_typecode = '<u8' if is_64bits else '<u4'
27-
28-
_tagBRECORD_format = [
29-
('pvRecord', ptr_typecode),
30-
('pRecInfo', ptr_typecode),
31-
]
32-
33-
# overlapping typecodes only allowed in numpy version 1.7 or greater
34-
U_VARIANT_format = dict(
35-
names=[
36-
'VT_BOOL', 'VT_I1', 'VT_I2', 'VT_I4', 'VT_I8', 'VT_INT', 'VT_UI1',
37-
'VT_UI2', 'VT_UI4', 'VT_UI8', 'VT_UINT', 'VT_R4', 'VT_R8', 'VT_CY',
38-
'c_wchar_p', 'c_void_p', 'pparray', 'bstrVal', '_tagBRECORD',
39-
],
40-
formats=[
41-
'<i2', '<i1', '<i2', '<i4', '<i8', '<i4', '<u1', '<u2', '<u4',
42-
'<u8', '<u4', '<f4', '<f8', '<i8', ptr_typecode, ptr_typecode,
43-
ptr_typecode, ptr_typecode, _tagBRECORD_format,
44-
],
45-
offsets=[0] * 19 # This is what makes it a union
46-
)
47-
48-
tagVARIANT_format = [
49-
("vt", '<u2'),
50-
("wReserved1", '<u2'),
51-
("wReserved2", '<u2'),
52-
("wReserved3", '<u2'),
53-
("_", U_VARIANT_format),
54-
]
55-
56-
return numpy.dtype(tagVARIANT_format)
57-
58-
59-
def isndarray(value):
60-
""" Check if a value is an ndarray.
61-
62-
This cannot succeed if numpy is not available.
63-
64-
"""
65-
if not HAVE_NUMPY:
66-
return False
67-
return isinstance(value, numpy.ndarray)
68-
69-
70-
def isdatetime64(value):
71-
""" Check if a value is a datetime64.
72-
73-
This cannot succeed if datetime64 is not available.
74-
75-
"""
76-
if not HAVE_NUMPY:
77-
return False
78-
return isinstance(value, datetime64)
79-
80-
81-
def _check_ctypeslib_typecodes():
82-
import numpy as np
83-
from numpy import ctypeslib
84-
try:
85-
from numpy.ctypeslib import _typecodes
86-
except ImportError:
87-
from numpy.ctypeslib import as_ctypes_type
88-
89-
dtypes_to_ctypes = {}
90-
91-
for tp in set(np.sctypeDict.values()):
92-
try:
93-
ctype_for = as_ctypes_type(tp)
94-
dtypes_to_ctypes[np.dtype(tp).str] = ctype_for
95-
except NotImplementedError:
96-
continue
97-
ctypeslib._typecodes = dtypes_to_ctypes
98-
return ctypeslib._typecodes
99-
100-
101-
com_null_date64 = None
102-
datetime64 = None
103-
VARIANT_dtype = None
104-
typecodes = {}
105-
106-
if HAVE_NUMPY:
107-
typecodes = _check_ctypeslib_typecodes()
108-
# dtype for VARIANT. This allows for packing of variants into an array, and
109-
# subsequent conversion to a multi-dimensional safearray.
110-
try:
111-
VARIANT_dtype = _make_variant_dtype()
112-
except ValueError:
113-
pass
114-
115-
# This simplifies dependent modules
116-
try:
117-
from numpy import datetime64
118-
except ImportError:
119-
pass
120-
else:
121-
try:
122-
# This does not work on numpy 1.6
123-
com_null_date64 = datetime64("1899-12-30T00:00:00", "ns")
124-
except TypeError:
125-
pass
1+
""" Consolidation of numpy support utilities. """
2+
import sys
3+
4+
is_64bits = sys.maxsize > 2**32
5+
6+
7+
class Interop:
8+
""" Class encapsulating all the functionality necessary to allow interop of
9+
comtypes with numpy. Needs to be enabled with the "enable()" method.
10+
"""
11+
def __init__(self):
12+
self.enabled = False
13+
self.VARIANT_dtype = None
14+
self.typecodes = {}
15+
self.datetime64 = None
16+
self.com_null_date64 = None
17+
18+
def _make_variant_dtype(self):
19+
""" Create a dtype for VARIANT. This requires support for Unions, which
20+
is available in numpy version 1.7 or greater.
21+
22+
This does not support the decimal type.
23+
24+
Returns None if the dtype cannot be created.
25+
"""
26+
if not self.enabled:
27+
return None
28+
# pointer typecode
29+
ptr_typecode = '<u8' if is_64bits else '<u4'
30+
31+
_tagBRECORD_format = [
32+
('pvRecord', ptr_typecode),
33+
('pRecInfo', ptr_typecode),
34+
]
35+
36+
# overlapping typecodes only allowed in numpy version 1.7 or greater
37+
U_VARIANT_format = dict(
38+
names=[
39+
'VT_BOOL', 'VT_I1', 'VT_I2', 'VT_I4', 'VT_I8', 'VT_INT',
40+
'VT_UI1', 'VT_UI2', 'VT_UI4', 'VT_UI8', 'VT_UINT', 'VT_R4',
41+
'VT_R8', 'VT_CY', 'c_wchar_p', 'c_void_p', 'pparray',
42+
'bstrVal', '_tagBRECORD',
43+
],
44+
formats=[
45+
'<i2', '<i1', '<i2', '<i4', '<i8', '<i4', '<u1', '<u2', '<u4',
46+
'<u8', '<u4', '<f4', '<f8', '<i8', ptr_typecode, ptr_typecode,
47+
ptr_typecode, ptr_typecode, _tagBRECORD_format,
48+
],
49+
offsets=[0] * 19 # This is what makes it a union
50+
)
51+
52+
tagVARIANT_format = [
53+
("vt", '<u2'),
54+
("wReserved1", '<u2'),
55+
("wReserved2", '<u2'),
56+
("wReserved3", '<u2'),
57+
("_", U_VARIANT_format),
58+
]
59+
60+
return self.numpy.dtype(tagVARIANT_format)
61+
62+
def _check_ctypeslib_typecodes(self):
63+
if not self.enabled:
64+
return {}
65+
import numpy as np
66+
from numpy import ctypeslib
67+
try:
68+
from numpy.ctypeslib import _typecodes
69+
except ImportError:
70+
from numpy.ctypeslib import as_ctypes_type
71+
72+
dtypes_to_ctypes = {}
73+
74+
for tp in set(np.sctypeDict.values()):
75+
try:
76+
ctype_for = as_ctypes_type(tp)
77+
dtypes_to_ctypes[np.dtype(tp).str] = ctype_for
78+
except NotImplementedError:
79+
continue
80+
ctypeslib._typecodes = dtypes_to_ctypes
81+
return dtypes_to_ctypes
82+
83+
def isndarray(self, value):
84+
""" Check if a value is an ndarray.
85+
86+
This cannot succeed if numpy is not available.
87+
88+
"""
89+
if not self.enabled:
90+
if hasattr(value, "__array_interface__"):
91+
raise ValueError(
92+
(
93+
"Argument {0} appears to be a numpy.ndarray, but "
94+
"comtypes numpy support has not been enabled. Please "
95+
"try calling comtypes.npsupport.enable_numpy_interop()"
96+
" before passing ndarrays as parameters."
97+
).format(value)
98+
)
99+
return False
100+
101+
return isinstance(value, self.numpy.ndarray)
102+
103+
def isdatetime64(self, value):
104+
""" Check if a value is a datetime64.
105+
106+
This cannot succeed if datetime64 is not available.
107+
108+
"""
109+
if not self.enabled:
110+
return False
111+
return isinstance(value, self.datetime64)
112+
113+
@property
114+
def numpy(self):
115+
""" The numpy package.
116+
"""
117+
if self.enabled:
118+
import numpy
119+
return numpy
120+
raise ImportError(
121+
"In comtypes>=1.2.0 numpy interop must be explicitly enabled with "
122+
"comtypes.npsupport.enable_numpy_interop before attempting to use "
123+
"numpy features."
124+
)
125+
126+
def enable(self):
127+
""" Enables numpy/comtypes interop.
128+
"""
129+
# don't do this twice
130+
if self.enabled:
131+
return
132+
# first we have to be able to import numpy
133+
import numpy
134+
# if that succeeded we can be enabled
135+
self.enabled = True
136+
self.VARIANT_dtype = self._make_variant_dtype()
137+
self.typecodes = self._check_ctypeslib_typecodes()
138+
try:
139+
from numpy import datetime64
140+
self.datetime64 = datetime64
141+
except ImportError:
142+
self.datetime64 = None
143+
if self.datetime64:
144+
try:
145+
# This does not work on numpy 1.6
146+
self.com_null_date64 = self.datetime64("1899-12-30T00:00:00", "ns")
147+
except TypeError:
148+
self.com_null_date64 = None
149+
150+
151+
interop = Interop()
152+
153+
__all__ = ["interop"]

0 commit comments

Comments
 (0)