tdrpa.tdworker 1.2.13.2__py312-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tdrpa/_tdxlwings/__init__.py +193 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/_win32patch.py +90 -0
- tdrpa/_tdxlwings/_xlmac.py +2240 -0
- tdrpa/_tdxlwings/_xlwindows.py +2518 -0
- tdrpa/_tdxlwings/addin/Dictionary.cls +474 -0
- tdrpa/_tdxlwings/addin/IWebAuthenticator.cls +71 -0
- tdrpa/_tdxlwings/addin/WebClient.cls +772 -0
- tdrpa/_tdxlwings/addin/WebHelpers.bas +3203 -0
- tdrpa/_tdxlwings/addin/WebRequest.cls +875 -0
- tdrpa/_tdxlwings/addin/WebResponse.cls +453 -0
- tdrpa/_tdxlwings/addin/xlwings.xlam +0 -0
- tdrpa/_tdxlwings/apps.py +35 -0
- tdrpa/_tdxlwings/base_classes.py +1092 -0
- tdrpa/_tdxlwings/cli.py +1306 -0
- tdrpa/_tdxlwings/com_server.py +385 -0
- tdrpa/_tdxlwings/constants.py +3080 -0
- tdrpa/_tdxlwings/conversion/__init__.py +103 -0
- tdrpa/_tdxlwings/conversion/framework.py +147 -0
- tdrpa/_tdxlwings/conversion/numpy_conv.py +34 -0
- tdrpa/_tdxlwings/conversion/pandas_conv.py +184 -0
- tdrpa/_tdxlwings/conversion/standard.py +321 -0
- tdrpa/_tdxlwings/expansion.py +83 -0
- tdrpa/_tdxlwings/ext/__init__.py +3 -0
- tdrpa/_tdxlwings/ext/sql.py +73 -0
- tdrpa/_tdxlwings/html/xlwings-alert.html +71 -0
- tdrpa/_tdxlwings/js/xlwings.js +577 -0
- tdrpa/_tdxlwings/js/xlwings.ts +729 -0
- tdrpa/_tdxlwings/mac_dict.py +6399 -0
- tdrpa/_tdxlwings/main.py +5205 -0
- tdrpa/_tdxlwings/mistune/__init__.py +63 -0
- tdrpa/_tdxlwings/mistune/block_parser.py +366 -0
- tdrpa/_tdxlwings/mistune/inline_parser.py +216 -0
- tdrpa/_tdxlwings/mistune/markdown.py +84 -0
- tdrpa/_tdxlwings/mistune/renderers.py +220 -0
- tdrpa/_tdxlwings/mistune/scanner.py +121 -0
- tdrpa/_tdxlwings/mistune/util.py +41 -0
- tdrpa/_tdxlwings/pro/__init__.py +40 -0
- tdrpa/_tdxlwings/pro/_xlcalamine.py +536 -0
- tdrpa/_tdxlwings/pro/_xlofficejs.py +146 -0
- tdrpa/_tdxlwings/pro/_xlremote.py +1293 -0
- tdrpa/_tdxlwings/pro/custom_functions_code.js +150 -0
- tdrpa/_tdxlwings/pro/embedded_code.py +60 -0
- tdrpa/_tdxlwings/pro/udfs_officejs.py +549 -0
- tdrpa/_tdxlwings/pro/utils.py +199 -0
- tdrpa/_tdxlwings/quickstart.xlsm +0 -0
- tdrpa/_tdxlwings/quickstart_addin.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_addin_ribbon.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_fastapi/main.py +47 -0
- tdrpa/_tdxlwings/quickstart_fastapi/requirements.txt +3 -0
- tdrpa/_tdxlwings/quickstart_standalone.xlsm +0 -0
- tdrpa/_tdxlwings/reports.py +12 -0
- tdrpa/_tdxlwings/rest/__init__.py +1 -0
- tdrpa/_tdxlwings/rest/api.py +368 -0
- tdrpa/_tdxlwings/rest/serializers.py +103 -0
- tdrpa/_tdxlwings/server.py +14 -0
- tdrpa/_tdxlwings/udfs.py +775 -0
- tdrpa/_tdxlwings/utils.py +777 -0
- tdrpa/_tdxlwings/xlwings-0.31.6.applescript +30 -0
- tdrpa/_tdxlwings/xlwings.bas +2061 -0
- tdrpa/_tdxlwings/xlwings_custom_addin.bas +2042 -0
- tdrpa/_tdxlwings/xlwingslib.cp38-win_amd64.pyd +0 -0
- tdrpa/tdworker/__init__.pyi +12 -0
- tdrpa/tdworker/_clip.pyi +50 -0
- tdrpa/tdworker/_excel.pyi +743 -0
- tdrpa/tdworker/_file.pyi +77 -0
- tdrpa/tdworker/_img.pyi +226 -0
- tdrpa/tdworker/_network.pyi +94 -0
- tdrpa/tdworker/_os.pyi +47 -0
- tdrpa/tdworker/_sp.pyi +21 -0
- tdrpa/tdworker/_w.pyi +129 -0
- tdrpa/tdworker/_web.pyi +995 -0
- tdrpa/tdworker/_winE.pyi +228 -0
- tdrpa/tdworker/_winK.pyi +74 -0
- tdrpa/tdworker/_winM.pyi +117 -0
- tdrpa/tdworker.cp312-win_amd64.pyd +0 -0
- tdrpa_tdworker-1.2.13.2.dist-info/METADATA +38 -0
- tdrpa_tdworker-1.2.13.2.dist-info/RECORD +101 -0
- tdrpa_tdworker-1.2.13.2.dist-info/WHEEL +5 -0
- tdrpa_tdworker-1.2.13.2.dist-info/top_level.txt +1 -0
tdrpa/_tdxlwings/udfs.py
ADDED
@@ -0,0 +1,775 @@
|
|
1
|
+
import asyncio
|
2
|
+
import concurrent
|
3
|
+
import copy
|
4
|
+
import functools
|
5
|
+
import inspect
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
import os.path
|
10
|
+
import re
|
11
|
+
import tempfile
|
12
|
+
import threading
|
13
|
+
from importlib import (
|
14
|
+
import_module,
|
15
|
+
reload,
|
16
|
+
)
|
17
|
+
from random import random
|
18
|
+
|
19
|
+
import pythoncom
|
20
|
+
import pywintypes
|
21
|
+
from win32com.client import Dispatch
|
22
|
+
|
23
|
+
import tdrpa._tdxlwings as xlwings
|
24
|
+
|
25
|
+
from . import Book, LicenseError, Range, __pro__, apps, conversion
|
26
|
+
from .utils import VBAWriter, exception, read_config_sheet
|
27
|
+
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
cache = {}
|
30
|
+
|
31
|
+
com_executor = concurrent.futures.ThreadPoolExecutor(initializer=pythoncom.CoInitialize)
|
32
|
+
|
33
|
+
|
34
|
+
async def async_thread(base, my_has_dynamic_array, func, args, cache_key, expand):
|
35
|
+
try:
|
36
|
+
if expand:
|
37
|
+
stashme = await base.get_formula_array()
|
38
|
+
elif my_has_dynamic_array:
|
39
|
+
stashme = await base.get_formula2()
|
40
|
+
else:
|
41
|
+
stashme = await base.get_formula()
|
42
|
+
|
43
|
+
loop = asyncio.get_running_loop()
|
44
|
+
cache[cache_key] = await loop.run_in_executor(
|
45
|
+
com_executor, functools.partial(func, *args)
|
46
|
+
)
|
47
|
+
|
48
|
+
if expand:
|
49
|
+
await base.set_formula_array(stashme)
|
50
|
+
elif my_has_dynamic_array:
|
51
|
+
await base.set_formula2(stashme)
|
52
|
+
else:
|
53
|
+
await base.set_formula(stashme)
|
54
|
+
except: # noqa: E722
|
55
|
+
exception(logger, "async_thread failed")
|
56
|
+
|
57
|
+
|
58
|
+
async def async_thread_nocaller(
|
59
|
+
func,
|
60
|
+
args,
|
61
|
+
):
|
62
|
+
try:
|
63
|
+
loop = asyncio.get_running_loop()
|
64
|
+
await loop.run_in_executor(com_executor, functools.partial(func, *args))
|
65
|
+
|
66
|
+
except: # noqa: E722
|
67
|
+
exception(logger, "async_thread failed")
|
68
|
+
|
69
|
+
|
70
|
+
def func_sig(f):
|
71
|
+
s = inspect.signature(f)
|
72
|
+
vararg = None
|
73
|
+
args = []
|
74
|
+
defaults = []
|
75
|
+
for p in s.parameters.values():
|
76
|
+
if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
77
|
+
args.append(p.name)
|
78
|
+
if p.default is not inspect.Signature.empty:
|
79
|
+
defaults.append(p.default)
|
80
|
+
elif p.kind is inspect.Parameter.VAR_POSITIONAL:
|
81
|
+
args.append(p.name)
|
82
|
+
vararg = p.name
|
83
|
+
else:
|
84
|
+
raise Exception("xlwings does not support UDFs with keyword arguments")
|
85
|
+
return {"args": args, "defaults": defaults, "vararg": vararg}
|
86
|
+
|
87
|
+
|
88
|
+
def get_category(**func_kwargs):
|
89
|
+
if "category" in func_kwargs:
|
90
|
+
category = func_kwargs.pop("category")
|
91
|
+
if isinstance(category, int):
|
92
|
+
if 1 <= category <= 14:
|
93
|
+
return category
|
94
|
+
raise Exception(
|
95
|
+
"There is only 14 build-in categories available in Excel. "
|
96
|
+
"Please use a string value to specify a custom category."
|
97
|
+
)
|
98
|
+
if isinstance(category, str):
|
99
|
+
return category[:255]
|
100
|
+
raise Exception(
|
101
|
+
"Category {0} should either be a predefined Excel category (int value) "
|
102
|
+
"or a custom one (str value).".format(category)
|
103
|
+
)
|
104
|
+
return "xlwings" # Default category
|
105
|
+
|
106
|
+
|
107
|
+
def get_async_mode(**func_kwargs):
|
108
|
+
if "async_mode" in func_kwargs:
|
109
|
+
value = func_kwargs.pop("async_mode")
|
110
|
+
if value in ["threading"]:
|
111
|
+
return value
|
112
|
+
raise Exception('The only supported async_mode mode is currently "threading".')
|
113
|
+
else:
|
114
|
+
return None
|
115
|
+
|
116
|
+
|
117
|
+
def check_bool(kw, default, **func_kwargs):
|
118
|
+
if kw in func_kwargs:
|
119
|
+
check = func_kwargs.pop(kw)
|
120
|
+
if isinstance(check, bool):
|
121
|
+
return check
|
122
|
+
raise Exception(
|
123
|
+
'{0} only takes boolean values. ("{1}" provided).'.format(kw, check)
|
124
|
+
)
|
125
|
+
return default
|
126
|
+
|
127
|
+
|
128
|
+
def xlfunc(f=None, **kwargs):
|
129
|
+
def inner(f):
|
130
|
+
if not hasattr(f, "__xlfunc__"):
|
131
|
+
xlf = f.__xlfunc__ = {}
|
132
|
+
xlf["name"] = f.__name__
|
133
|
+
xlf["sub"] = False
|
134
|
+
xlargs = xlf["args"] = []
|
135
|
+
xlargmap = xlf["argmap"] = {}
|
136
|
+
sig = func_sig(f)
|
137
|
+
nArgs = len(sig["args"])
|
138
|
+
nDefaults = len(sig["defaults"])
|
139
|
+
nRequiredArgs = nArgs - nDefaults
|
140
|
+
if sig["vararg"] and nDefaults > 0:
|
141
|
+
raise Exception(
|
142
|
+
"xlwings does not support UDFs "
|
143
|
+
"with both optional and variable length arguments"
|
144
|
+
)
|
145
|
+
for vpos, vname in enumerate(sig["args"]):
|
146
|
+
arg_info = {
|
147
|
+
"name": vname,
|
148
|
+
"pos": vpos,
|
149
|
+
"vba": None,
|
150
|
+
"doc": "Positional argument " + str(vpos + 1),
|
151
|
+
"vararg": vname == sig["vararg"],
|
152
|
+
"options": {},
|
153
|
+
}
|
154
|
+
if vpos >= nRequiredArgs:
|
155
|
+
arg_info["optional"] = sig["defaults"][vpos - nRequiredArgs]
|
156
|
+
xlargs.append(arg_info)
|
157
|
+
xlargmap[vname] = xlargs[-1]
|
158
|
+
xlf["ret"] = {
|
159
|
+
"doc": f.__doc__
|
160
|
+
if f.__doc__ is not None
|
161
|
+
else "Python function '"
|
162
|
+
+ f.__name__
|
163
|
+
+ "' defined in '"
|
164
|
+
+ str(f.__code__.co_filename)
|
165
|
+
+ "'.",
|
166
|
+
"options": {},
|
167
|
+
}
|
168
|
+
f.__xlfunc__["category"] = get_category(**kwargs)
|
169
|
+
f.__xlfunc__["call_in_wizard"] = check_bool(
|
170
|
+
"call_in_wizard", default=True, **kwargs
|
171
|
+
)
|
172
|
+
f.__xlfunc__["volatile"] = check_bool("volatile", default=False, **kwargs)
|
173
|
+
f.__xlfunc__["async_mode"] = get_async_mode(**kwargs)
|
174
|
+
return f
|
175
|
+
|
176
|
+
if f is None:
|
177
|
+
return inner
|
178
|
+
else:
|
179
|
+
return inner(f)
|
180
|
+
|
181
|
+
|
182
|
+
def xlsub(f=None, **kwargs):
|
183
|
+
def inner(f):
|
184
|
+
f = xlfunc(**kwargs)(f)
|
185
|
+
f.__xlfunc__["sub"] = True
|
186
|
+
return f
|
187
|
+
|
188
|
+
if f is None:
|
189
|
+
return inner
|
190
|
+
else:
|
191
|
+
return inner(f)
|
192
|
+
|
193
|
+
|
194
|
+
def xlret(convert=None, **kwargs):
|
195
|
+
if convert is not None:
|
196
|
+
kwargs["convert"] = convert
|
197
|
+
|
198
|
+
def inner(f):
|
199
|
+
xlf = xlfunc(f).__xlfunc__
|
200
|
+
xlr = xlf["ret"]
|
201
|
+
xlr["options"].update(kwargs)
|
202
|
+
return f
|
203
|
+
|
204
|
+
return inner
|
205
|
+
|
206
|
+
|
207
|
+
def xlarg(arg, convert=None, **kwargs):
|
208
|
+
if convert is not None:
|
209
|
+
kwargs["convert"] = convert
|
210
|
+
|
211
|
+
def inner(f):
|
212
|
+
xlf = xlfunc(f).__xlfunc__
|
213
|
+
if arg.lstrip("*") not in xlf["argmap"]:
|
214
|
+
raise Exception("Invalid argument name '" + arg + "'.")
|
215
|
+
xla = xlf["argmap"][arg.lstrip("*")]
|
216
|
+
for special in ("vba", "doc"):
|
217
|
+
if special in kwargs:
|
218
|
+
xla[special] = kwargs.pop(special)
|
219
|
+
xla["options"].update(kwargs)
|
220
|
+
return f
|
221
|
+
|
222
|
+
return inner
|
223
|
+
|
224
|
+
|
225
|
+
udf_modules = {}
|
226
|
+
|
227
|
+
RPC_E_SERVERCALL_RETRYLATER = {-2147418111, -2146777998}
|
228
|
+
MAX_BACKOFF_MS = 512
|
229
|
+
|
230
|
+
|
231
|
+
class ComRange(Range):
|
232
|
+
"""
|
233
|
+
A Range subclass that stores the impl as
|
234
|
+
a serialized COM object so it can be passed between
|
235
|
+
threads easily
|
236
|
+
|
237
|
+
https://devblogs.microsoft.com/oldnewthing/20151021-00/?p=91311
|
238
|
+
"""
|
239
|
+
|
240
|
+
def __init__(self, rng):
|
241
|
+
super().__init__(impl=rng.impl)
|
242
|
+
|
243
|
+
self._ser_thread = threading.get_ident()
|
244
|
+
self._ser = pythoncom.CoMarshalInterThreadInterfaceInStream(
|
245
|
+
pythoncom.IID_IDispatch, rng.api
|
246
|
+
)
|
247
|
+
self._ser_resultCLSID = self._impl.api.CLSID
|
248
|
+
|
249
|
+
self._deser_thread = None
|
250
|
+
self._deser = None
|
251
|
+
|
252
|
+
@property
|
253
|
+
def impl(self):
|
254
|
+
if threading.get_ident() == self._ser_thread:
|
255
|
+
return self._impl
|
256
|
+
elif threading.get_ident() == self._deser_thread:
|
257
|
+
return self._deser
|
258
|
+
|
259
|
+
assert self._deser is None, f"already deserialized on {self._deser_thread}"
|
260
|
+
self._deser_thread = threading.get_ident()
|
261
|
+
|
262
|
+
deser = pythoncom.CoGetInterfaceAndReleaseStream(
|
263
|
+
self._ser, pythoncom.IID_IDispatch
|
264
|
+
)
|
265
|
+
dispatch = Dispatch(deser, resultCLSID=self._ser_resultCLSID)
|
266
|
+
|
267
|
+
self._ser = None # single-use
|
268
|
+
self._deser = xlwings._xlwindows.Range(xl=dispatch)
|
269
|
+
|
270
|
+
return self._deser
|
271
|
+
|
272
|
+
def __copy__(self):
|
273
|
+
"""
|
274
|
+
We need to re-serialize the COM object as they're
|
275
|
+
single-use
|
276
|
+
"""
|
277
|
+
return ComRange(self)
|
278
|
+
|
279
|
+
async def _com(self, fn, *args, backoff=1):
|
280
|
+
"""
|
281
|
+
:param backoff: if the call fails, time to wait in ms
|
282
|
+
before the next one. Random exponential backoff to
|
283
|
+
a cap.
|
284
|
+
"""
|
285
|
+
|
286
|
+
loop = asyncio.get_running_loop()
|
287
|
+
|
288
|
+
try:
|
289
|
+
return await loop.run_in_executor(
|
290
|
+
com_executor, functools.partial(fn, copy.copy(self), *args)
|
291
|
+
)
|
292
|
+
except AttributeError:
|
293
|
+
# the Dispatch object that the `com_executor` thread
|
294
|
+
# didn't deserialize properly, as Excel was too busy
|
295
|
+
# to handle the TypeInfo call when requested
|
296
|
+
pass
|
297
|
+
except Exception as e:
|
298
|
+
if getattr(e, "hresult", 0) not in RPC_E_SERVERCALL_RETRYLATER:
|
299
|
+
raise
|
300
|
+
|
301
|
+
await asyncio.sleep(backoff / 1e3)
|
302
|
+
return await self._com(
|
303
|
+
fn, *args, backoff=min(backoff * round(1 + random()), MAX_BACKOFF_MS)
|
304
|
+
)
|
305
|
+
|
306
|
+
async def clear_contents(self):
|
307
|
+
await self._com(lambda rng: rng.impl.clear_contents())
|
308
|
+
|
309
|
+
async def set_formula_array(self, f):
|
310
|
+
await self._com(lambda rng: setattr(rng.impl, "formula_array", f))
|
311
|
+
|
312
|
+
async def set_formula(self, f):
|
313
|
+
await self._com(lambda rng: setattr(rng.impl, "formula", f))
|
314
|
+
|
315
|
+
async def set_formula2(self, f):
|
316
|
+
await self._com(lambda rng: setattr(rng.impl, "formula2", f))
|
317
|
+
|
318
|
+
async def get_shape(self):
|
319
|
+
return await self._com(lambda rng: rng.impl.shape)
|
320
|
+
|
321
|
+
async def get_formula_array(self):
|
322
|
+
return await self._com(lambda rng: rng.impl.formula_array)
|
323
|
+
|
324
|
+
async def get_formula(self):
|
325
|
+
return await self._com(lambda rng: rng.impl.formula)
|
326
|
+
|
327
|
+
async def get_formula2(self):
|
328
|
+
return await self._com(lambda rng: rng.impl.formula2)
|
329
|
+
|
330
|
+
async def get_address(self):
|
331
|
+
return await self._com(lambda rng: rng.impl.address)
|
332
|
+
|
333
|
+
|
334
|
+
async def delayed_resize_dynamic_array_formula(target_range, caller):
|
335
|
+
try:
|
336
|
+
await asyncio.sleep(0.1)
|
337
|
+
|
338
|
+
stashme = await caller.get_formula_array()
|
339
|
+
if not stashme:
|
340
|
+
stashme = await caller.get_formula()
|
341
|
+
|
342
|
+
c_y, c_x = await caller.get_shape()
|
343
|
+
t_y, t_x = await target_range.get_shape()
|
344
|
+
if c_x > t_x or c_y > t_y:
|
345
|
+
await caller.clear_contents()
|
346
|
+
|
347
|
+
# this will call the UDF again (hitting the cache),
|
348
|
+
# but you'll have the right size output this time
|
349
|
+
# (`caller` will be `target_range`). We'll have to
|
350
|
+
# be careful not to block the async loop!
|
351
|
+
await target_range.set_formula_array(stashme)
|
352
|
+
|
353
|
+
except: # noqa: E722
|
354
|
+
exception(logger, "couldn't resize")
|
355
|
+
|
356
|
+
|
357
|
+
def get_udf_module(module_name, xl_workbook):
|
358
|
+
module_info = udf_modules.get(module_name, None)
|
359
|
+
if module_info is not None:
|
360
|
+
module = module_info["module"]
|
361
|
+
# If filetime is None, it's not reloadable
|
362
|
+
if module_info["filetime"] is not None:
|
363
|
+
mtime = os.path.getmtime(module_info["filename"])
|
364
|
+
if mtime != module_info["filetime"]:
|
365
|
+
module = reload(module)
|
366
|
+
module_info["filetime"] = mtime
|
367
|
+
module_info["module"] = module
|
368
|
+
else:
|
369
|
+
# Handle embedded code (Excel only)
|
370
|
+
if xl_workbook:
|
371
|
+
wb = Book(impl=xlwings._xlwindows.Book(Dispatch(xl_workbook)))
|
372
|
+
for sheet in wb.sheets:
|
373
|
+
if sheet.name.endswith(".py") and not __pro__:
|
374
|
+
raise LicenseError("Embedded code requires a valid LICENSE_KEY.")
|
375
|
+
elif sheet.name.endswith(".py") and __pro__:
|
376
|
+
from .pro.embedded_code import dump_embedded_code
|
377
|
+
from .pro.utils import get_embedded_code_temp_dir
|
378
|
+
|
379
|
+
dump_embedded_code(wb, get_embedded_code_temp_dir())
|
380
|
+
|
381
|
+
module = import_module(module_name)
|
382
|
+
filename = os.path.normcase(module.__file__.lower())
|
383
|
+
|
384
|
+
try: # getmtime fails for zip imports and frozen modules
|
385
|
+
mtime = os.path.getmtime(filename)
|
386
|
+
except OSError:
|
387
|
+
mtime = None
|
388
|
+
|
389
|
+
udf_modules[module_name] = {
|
390
|
+
"filename": filename,
|
391
|
+
"filetime": mtime,
|
392
|
+
"module": module,
|
393
|
+
}
|
394
|
+
|
395
|
+
return module
|
396
|
+
|
397
|
+
|
398
|
+
def get_cache_key(func, args, caller):
|
399
|
+
"""only use this if function is called from cells, not VBA"""
|
400
|
+
xw_caller = Range(impl=xlwings._xlwindows.Range(xl=caller))
|
401
|
+
return (
|
402
|
+
func.__name__
|
403
|
+
+ str(args)
|
404
|
+
+ str(xw_caller.sheet.book.app.pid)
|
405
|
+
+ xw_caller.sheet.book.name
|
406
|
+
+ xw_caller.sheet.name
|
407
|
+
+ xw_caller.address.split(":")[0]
|
408
|
+
)
|
409
|
+
|
410
|
+
|
411
|
+
def call_udf(module_name, func_name, args, this_workbook=None, caller=None):
|
412
|
+
"""
|
413
|
+
This method executes the UDF synchronously from the COM server thread
|
414
|
+
"""
|
415
|
+
module = get_udf_module(module_name, this_workbook)
|
416
|
+
func = getattr(module, func_name)
|
417
|
+
func_info = func.__xlfunc__
|
418
|
+
args_info = func_info["args"]
|
419
|
+
ret_info = func_info["ret"]
|
420
|
+
is_dynamic_array = ret_info["options"].get("expand")
|
421
|
+
xw_caller = Range(impl=xlwings._xlwindows.Range(xl=caller))
|
422
|
+
|
423
|
+
# If there is the 'reserved' argument "caller", assign the caller object
|
424
|
+
for info in args_info:
|
425
|
+
if info["name"] == "caller":
|
426
|
+
args = list(args)
|
427
|
+
args[info["pos"]] = ComRange(xw_caller)
|
428
|
+
args = tuple(args)
|
429
|
+
|
430
|
+
writing = func_info.get("writing", None)
|
431
|
+
if writing and writing == xw_caller.address:
|
432
|
+
return func_info["rval"]
|
433
|
+
|
434
|
+
args = list(args)
|
435
|
+
for i, arg in enumerate(args):
|
436
|
+
arg_info = args_info[min(i, len(args_info) - 1)]
|
437
|
+
if isinstance(arg, int) and arg == -2147352572: # missing
|
438
|
+
args[i] = arg_info.get("optional", None)
|
439
|
+
elif xlwings._xlwindows.is_range_instance(arg):
|
440
|
+
args[i] = conversion.read(
|
441
|
+
Range(impl=xlwings._xlwindows.Range(xl=arg)),
|
442
|
+
None,
|
443
|
+
arg_info["options"],
|
444
|
+
)
|
445
|
+
else:
|
446
|
+
args[i] = conversion.read(None, arg, arg_info["options"])
|
447
|
+
if this_workbook:
|
448
|
+
xlwings._xlwindows.BOOK_CALLER = Dispatch(this_workbook)
|
449
|
+
|
450
|
+
from .com_server import loop
|
451
|
+
|
452
|
+
if func_info["async_mode"] and func_info["async_mode"] == "threading":
|
453
|
+
if caller is None:
|
454
|
+
asyncio.run_coroutine_threadsafe(
|
455
|
+
async_thread_nocaller(func, args),
|
456
|
+
loop,
|
457
|
+
)
|
458
|
+
return [[0]]
|
459
|
+
cache_key = get_cache_key(func, args, caller)
|
460
|
+
cached_value = cache.get(cache_key)
|
461
|
+
if (
|
462
|
+
cached_value is not None
|
463
|
+
): # test against None as np arrays don't have a truth value
|
464
|
+
if not is_dynamic_array: # for dynamic arrays, the cache is cleared below
|
465
|
+
del cache[cache_key]
|
466
|
+
ret = cached_value
|
467
|
+
else:
|
468
|
+
ret = [["#N/A waiting..." * xw_caller.columns.count] * xw_caller.rows.count]
|
469
|
+
|
470
|
+
# this does a lot of nested COM calls, so do this all
|
471
|
+
# synchronously on the COM thread until there is async
|
472
|
+
# support for Sheet, Book & App.
|
473
|
+
my_has_dynamic_array = has_dynamic_array(xw_caller.sheet.book.app.pid)
|
474
|
+
|
475
|
+
asyncio.run_coroutine_threadsafe(
|
476
|
+
async_thread(
|
477
|
+
ComRange(xw_caller),
|
478
|
+
my_has_dynamic_array,
|
479
|
+
func,
|
480
|
+
args,
|
481
|
+
cache_key,
|
482
|
+
is_dynamic_array,
|
483
|
+
),
|
484
|
+
loop,
|
485
|
+
)
|
486
|
+
return ret
|
487
|
+
else:
|
488
|
+
if is_dynamic_array:
|
489
|
+
cache_key = get_cache_key(func, args, caller)
|
490
|
+
cached_value = cache.get(cache_key)
|
491
|
+
if cached_value is not None:
|
492
|
+
ret = cached_value
|
493
|
+
else:
|
494
|
+
if inspect.iscoroutinefunction(func):
|
495
|
+
ret = asyncio.run_coroutine_threadsafe(func(*args), loop).result()
|
496
|
+
else:
|
497
|
+
ret = func(*args)
|
498
|
+
cache[cache_key] = ret
|
499
|
+
elif inspect.iscoroutinefunction(func):
|
500
|
+
ret = asyncio.run_coroutine_threadsafe(func(*args), loop).result()
|
501
|
+
else:
|
502
|
+
ret = func(*args)
|
503
|
+
|
504
|
+
xl_result = conversion.write(ret, None, ret_info["options"])
|
505
|
+
|
506
|
+
if is_dynamic_array:
|
507
|
+
current_size = (len(xw_caller.rows), len(xw_caller.columns))
|
508
|
+
result_size = (1, 1)
|
509
|
+
if type(xl_result) is list:
|
510
|
+
result_height = len(xl_result)
|
511
|
+
result_width = result_height and len(xl_result[0])
|
512
|
+
result_size = (max(1, result_height), max(1, result_width))
|
513
|
+
if current_size != result_size:
|
514
|
+
target_range = xw_caller.resize(*result_size)
|
515
|
+
|
516
|
+
asyncio.run_coroutine_threadsafe(
|
517
|
+
delayed_resize_dynamic_array_formula(
|
518
|
+
target_range=ComRange(target_range), caller=ComRange(xw_caller)
|
519
|
+
),
|
520
|
+
loop,
|
521
|
+
)
|
522
|
+
else:
|
523
|
+
del cache[cache_key]
|
524
|
+
|
525
|
+
return xl_result
|
526
|
+
|
527
|
+
|
528
|
+
def generate_vba_wrapper(module_name, module, f, xl_workbook):
|
529
|
+
vba = VBAWriter(f)
|
530
|
+
|
531
|
+
for svar in map(lambda attr: getattr(module, attr), dir(module)):
|
532
|
+
if hasattr(svar, "__xlfunc__"):
|
533
|
+
xlfunc = svar.__xlfunc__
|
534
|
+
fname = xlfunc["name"]
|
535
|
+
call_in_wizard = xlfunc["call_in_wizard"]
|
536
|
+
volatile = xlfunc["volatile"]
|
537
|
+
|
538
|
+
ftype = "Sub" if xlfunc["sub"] else "Function"
|
539
|
+
|
540
|
+
func_sig = ftype + " " + fname + "("
|
541
|
+
|
542
|
+
first = True
|
543
|
+
vararg = ""
|
544
|
+
for arg in xlfunc["args"]:
|
545
|
+
if arg["name"] == "caller":
|
546
|
+
arg[
|
547
|
+
"vba"
|
548
|
+
] = "Nothing" # will be replaced with caller under call_udf
|
549
|
+
if not arg["vba"]:
|
550
|
+
argname = arg["name"]
|
551
|
+
if not first:
|
552
|
+
func_sig += ", "
|
553
|
+
if "optional" in arg:
|
554
|
+
func_sig += "Optional "
|
555
|
+
elif arg["vararg"]:
|
556
|
+
func_sig += "ParamArray "
|
557
|
+
vararg = argname
|
558
|
+
func_sig += argname
|
559
|
+
if arg["vararg"]:
|
560
|
+
func_sig += "()"
|
561
|
+
first = False
|
562
|
+
func_sig += ")"
|
563
|
+
|
564
|
+
with vba.block(func_sig):
|
565
|
+
if ftype == "Function":
|
566
|
+
if not call_in_wizard:
|
567
|
+
vba.writeln(
|
568
|
+
'If (Not Application.CommandBars("Standard")'
|
569
|
+
".Controls(1).Enabled) Then Exit Function"
|
570
|
+
)
|
571
|
+
if volatile:
|
572
|
+
vba.writeln("Application.Volatile")
|
573
|
+
|
574
|
+
if vararg != "":
|
575
|
+
vba.writeln("Dim argsArray() As Variant")
|
576
|
+
non_varargs = [
|
577
|
+
arg["vba"] or arg["name"]
|
578
|
+
for arg in xlfunc["args"]
|
579
|
+
if not arg["vararg"]
|
580
|
+
]
|
581
|
+
vba.writeln(
|
582
|
+
"argsArray = Array(%s)" % tuple({", ".join(non_varargs)})
|
583
|
+
)
|
584
|
+
|
585
|
+
vba.writeln(
|
586
|
+
"ReDim Preserve argsArray(0 to UBound("
|
587
|
+
+ vararg
|
588
|
+
+ ") - LBound("
|
589
|
+
+ vararg
|
590
|
+
+ ") + "
|
591
|
+
+ str(len(non_varargs))
|
592
|
+
+ ")"
|
593
|
+
)
|
594
|
+
vba.writeln(
|
595
|
+
"For k = LBound(" + vararg + ") To UBound(" + vararg + ")"
|
596
|
+
)
|
597
|
+
vba.writeln(
|
598
|
+
"argsArray("
|
599
|
+
+ str(len(non_varargs))
|
600
|
+
+ " + k - LBound("
|
601
|
+
+ vararg
|
602
|
+
+ ")) = "
|
603
|
+
+ argname
|
604
|
+
+ "(k)"
|
605
|
+
)
|
606
|
+
vba.writeln("Next k")
|
607
|
+
|
608
|
+
args_vba = "argsArray"
|
609
|
+
else:
|
610
|
+
args_vba = (
|
611
|
+
"Array("
|
612
|
+
+ ", ".join(arg["vba"] or arg["name"] for arg in xlfunc["args"])
|
613
|
+
+ ")"
|
614
|
+
)
|
615
|
+
|
616
|
+
# Add-ins work with ActiveWorkbook instead of ThisWorkbook
|
617
|
+
vba_workbook = (
|
618
|
+
"ActiveWorkbook"
|
619
|
+
if xl_workbook.Name.endswith(".xlam")
|
620
|
+
else "ThisWorkbook"
|
621
|
+
)
|
622
|
+
|
623
|
+
if ftype == "Sub":
|
624
|
+
with vba.block('#If App = "Microsoft Excel" Then'):
|
625
|
+
vba.writeln(
|
626
|
+
'XLPy.CallUDF "{module_name}", "{fname}", '
|
627
|
+
"{args_vba}, {vba_workbook}, Application.Caller",
|
628
|
+
module_name=module_name,
|
629
|
+
fname=fname,
|
630
|
+
args_vba=args_vba,
|
631
|
+
vba_workbook=vba_workbook,
|
632
|
+
)
|
633
|
+
with vba.block("#Else"):
|
634
|
+
vba.writeln(
|
635
|
+
'XLPy.CallUDF "{module_name}", "{fname}", {args_vba}',
|
636
|
+
module_name=module_name,
|
637
|
+
fname=fname,
|
638
|
+
args_vba=args_vba,
|
639
|
+
)
|
640
|
+
vba.writeln("#End If")
|
641
|
+
else:
|
642
|
+
with vba.block('#If App = "Microsoft Excel" Then'):
|
643
|
+
vba.writeln(
|
644
|
+
"If TypeOf Application.Caller Is Range Then "
|
645
|
+
"On Error GoTo failed"
|
646
|
+
)
|
647
|
+
vba.writeln(
|
648
|
+
'{fname} = XLPy.CallUDF("{module_name}", "{fname}", '
|
649
|
+
"{args_vba}, {vba_workbook}, Application.Caller)",
|
650
|
+
module_name=module_name,
|
651
|
+
fname=fname,
|
652
|
+
args_vba=args_vba,
|
653
|
+
vba_workbook=vba_workbook,
|
654
|
+
)
|
655
|
+
vba.writeln("Exit " + ftype)
|
656
|
+
with vba.block("#Else"):
|
657
|
+
vba.writeln(
|
658
|
+
'{fname} = XLPy.CallUDF("{module_name}", '
|
659
|
+
'"{fname}", {args_vba})',
|
660
|
+
module_name=module_name,
|
661
|
+
fname=fname,
|
662
|
+
args_vba=args_vba,
|
663
|
+
)
|
664
|
+
vba.writeln("Exit " + ftype)
|
665
|
+
vba.writeln("#End If")
|
666
|
+
|
667
|
+
vba.write_label("failed")
|
668
|
+
vba.writeln(fname + " = Err.Description")
|
669
|
+
|
670
|
+
vba.writeln("End " + ftype)
|
671
|
+
vba.writeln("")
|
672
|
+
|
673
|
+
|
674
|
+
def import_udfs(module_names, xl_workbook):
|
675
|
+
module_names = module_names.split(";")
|
676
|
+
|
677
|
+
tf = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
678
|
+
|
679
|
+
vba = VBAWriter(tf.file)
|
680
|
+
|
681
|
+
vba.writeln('Attribute VB_Name = "xlwings_udfs"')
|
682
|
+
|
683
|
+
vba.writeln(
|
684
|
+
"'Autogenerated code by xlwings - changes will be lost with next import!"
|
685
|
+
)
|
686
|
+
vba.writeln(
|
687
|
+
"""#Const App = "Microsoft Excel" 'Adjust when using outside of Excel"""
|
688
|
+
)
|
689
|
+
|
690
|
+
if __pro__:
|
691
|
+
for module_name in module_names:
|
692
|
+
sheet_config = read_config_sheet(
|
693
|
+
Book(impl=xlwings._xlwindows.Book(Dispatch(xl_workbook)))
|
694
|
+
)
|
695
|
+
code_map = sheet_config.get("RELEASE_EMBED_CODE_MAP", "{}")
|
696
|
+
sheetname_to_path = json.loads(code_map)
|
697
|
+
if module_name + ".py" in sheetname_to_path:
|
698
|
+
real_module_name = sheetname_to_path[module_name + ".py"][:-3]
|
699
|
+
module_names.remove(module_name)
|
700
|
+
module_names.append(real_module_name)
|
701
|
+
|
702
|
+
for module_name in module_names:
|
703
|
+
module = get_udf_module(module_name, xl_workbook)
|
704
|
+
generate_vba_wrapper(module_name, module, tf.file, xl_workbook)
|
705
|
+
|
706
|
+
tf.close()
|
707
|
+
|
708
|
+
try:
|
709
|
+
xl_workbook.VBProject.VBComponents.Remove(
|
710
|
+
xl_workbook.VBProject.VBComponents("xlwings_udfs")
|
711
|
+
)
|
712
|
+
except pywintypes.com_error:
|
713
|
+
pass
|
714
|
+
|
715
|
+
try:
|
716
|
+
xl_workbook.VBProject.VBComponents.Import(tf.name)
|
717
|
+
except pywintypes.com_error:
|
718
|
+
# Fallback. Some users get in Excel "Automation error 440" with this traceback
|
719
|
+
# in Python: pywintypes.com_error: (-2147352567, 'Exception occurred.',
|
720
|
+
# (0, None, None, None, 0, -2146827284), None)
|
721
|
+
xl_workbook.Application.Run("ImportXlwingsUdfsModule", tf.name)
|
722
|
+
|
723
|
+
for module_name in module_names:
|
724
|
+
module = get_udf_module(module_name, xl_workbook)
|
725
|
+
for mvar in map(lambda attr: getattr(module, attr), dir(module)):
|
726
|
+
if hasattr(mvar, "__xlfunc__"):
|
727
|
+
xlfunc = mvar.__xlfunc__
|
728
|
+
xlret = xlfunc["ret"]
|
729
|
+
xlargs = xlfunc["args"]
|
730
|
+
fname = xlfunc["name"]
|
731
|
+
fdoc = xlret["doc"][:255]
|
732
|
+
fcategory = xlfunc["category"]
|
733
|
+
|
734
|
+
excel_version = [
|
735
|
+
int(x) for x in re.split("[,\\.]", xl_workbook.Application.Version)
|
736
|
+
]
|
737
|
+
if excel_version[0] >= 14:
|
738
|
+
argdocs = [arg["doc"][:255] for arg in xlargs if not arg["vba"]]
|
739
|
+
xl_workbook.Application.MacroOptions(
|
740
|
+
"'" + xl_workbook.Name + "'!" + fname,
|
741
|
+
Description=fdoc,
|
742
|
+
HasMenu=False,
|
743
|
+
MenuText=None,
|
744
|
+
HasShortcutKey=False,
|
745
|
+
ShortcutKey=None,
|
746
|
+
Category=fcategory,
|
747
|
+
StatusBar=None,
|
748
|
+
HelpContextID=None,
|
749
|
+
HelpFile=None,
|
750
|
+
ArgumentDescriptions=argdocs if argdocs else None,
|
751
|
+
)
|
752
|
+
else:
|
753
|
+
xl_workbook.Application.MacroOptions(
|
754
|
+
"'" + xl_workbook.Name + "'!" + fname, Description=fdoc
|
755
|
+
)
|
756
|
+
|
757
|
+
# try to delete the temp file - doesn't matter too much if it fails
|
758
|
+
try:
|
759
|
+
os.unlink(tf.name)
|
760
|
+
except: # noqa: E722
|
761
|
+
pass
|
762
|
+
msg = f'Imported functions from the following modules: {", ".join(module_names)}'
|
763
|
+
logger.info(msg) if logger.hasHandlers() else print(msg)
|
764
|
+
|
765
|
+
|
766
|
+
@functools.lru_cache(None)
|
767
|
+
def has_dynamic_array(pid):
|
768
|
+
"""This check in this form doesn't work on macOS,
|
769
|
+
that's why it's here and not in utils
|
770
|
+
"""
|
771
|
+
try:
|
772
|
+
apps[pid].api.WorksheetFunction.Unique("dummy")
|
773
|
+
return True
|
774
|
+
except (AttributeError, pywintypes.com_error):
|
775
|
+
return False
|