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
@@ -0,0 +1,2518 @@
|
|
1
|
+
import atexit
|
2
|
+
import locale
|
3
|
+
import os
|
4
|
+
import subprocess
|
5
|
+
import sys
|
6
|
+
from shlex import split
|
7
|
+
|
8
|
+
# Hack to find pythoncom.dll - needed for some distribution/setups (includes seemingly
|
9
|
+
# unused import win32api) E.g. if python is started with the full path outside of the
|
10
|
+
# python path, then it almost certainly fails
|
11
|
+
cwd = os.getcwd()
|
12
|
+
if not hasattr(sys, "frozen"):
|
13
|
+
# cx_Freeze etc. will fail here otherwise
|
14
|
+
os.chdir(sys.exec_prefix)
|
15
|
+
# Since Python 3.8, pywintypes needs to be imported before win32api or you get
|
16
|
+
# ImportError: DLL load failed while importing win32api: The specified module could not
|
17
|
+
# be found.
|
18
|
+
# See: https://stackoverflow.com/questions/58805040/pywin32-226-and-virtual-environments
|
19
|
+
# Seems to be required even with pywin32 227
|
20
|
+
import pywintypes
|
21
|
+
import win32api
|
22
|
+
import win32con
|
23
|
+
|
24
|
+
os.chdir(cwd)
|
25
|
+
|
26
|
+
import ctypes
|
27
|
+
import datetime as dt
|
28
|
+
import numbers
|
29
|
+
import types
|
30
|
+
from ctypes import PyDLL, byref, oledll, py_object, windll
|
31
|
+
from pathlib import Path
|
32
|
+
from warnings import warn
|
33
|
+
|
34
|
+
import pythoncom
|
35
|
+
|
36
|
+
# Patching CoClassBaseClass, see https://github.com/xlwings/xlwings/issues/1789
|
37
|
+
import win32com.client
|
38
|
+
|
39
|
+
from ._win32patch import CoClassBaseClass
|
40
|
+
|
41
|
+
win32com.client.CoClassBaseClass = CoClassBaseClass
|
42
|
+
# End Patch
|
43
|
+
|
44
|
+
import win32gui
|
45
|
+
import win32process
|
46
|
+
import win32timezone
|
47
|
+
from win32com.client import (
|
48
|
+
CDispatch,
|
49
|
+
CoClassBaseClass,
|
50
|
+
Dispatch,
|
51
|
+
DispatchBaseClass,
|
52
|
+
DispatchEx,
|
53
|
+
)
|
54
|
+
|
55
|
+
import tdrpa._tdxlwings as xlwings
|
56
|
+
|
57
|
+
from . import base_classes, constants, utils
|
58
|
+
from .constants import (
|
59
|
+
ColorIndex,
|
60
|
+
DeleteShiftDirection,
|
61
|
+
FileFormat,
|
62
|
+
FixedFormatType,
|
63
|
+
HtmlType,
|
64
|
+
InsertFormatOrigin,
|
65
|
+
InsertShiftDirection,
|
66
|
+
ListObjectSourceType,
|
67
|
+
SourceType,
|
68
|
+
UpdateLinks,
|
69
|
+
)
|
70
|
+
from .utils import (
|
71
|
+
col_name,
|
72
|
+
fullname_url_to_local_path,
|
73
|
+
hex_to_rgb,
|
74
|
+
int_to_rgb,
|
75
|
+
np_datetime_to_datetime,
|
76
|
+
read_config_sheet,
|
77
|
+
rgb_to_int,
|
78
|
+
)
|
79
|
+
|
80
|
+
# Optional imports
|
81
|
+
try:
|
82
|
+
import pandas as pd
|
83
|
+
except ImportError:
|
84
|
+
pd = None
|
85
|
+
try:
|
86
|
+
import numpy as np
|
87
|
+
except ImportError:
|
88
|
+
np = None
|
89
|
+
try:
|
90
|
+
from PIL import ImageGrab
|
91
|
+
except ImportError:
|
92
|
+
PIL = None
|
93
|
+
|
94
|
+
|
95
|
+
time_types = (dt.date, dt.datetime, pywintypes.TimeType)
|
96
|
+
if np:
|
97
|
+
time_types = time_types + (np.datetime64,)
|
98
|
+
|
99
|
+
|
100
|
+
N_COM_ATTEMPTS = 0 # 0 means try indefinitely
|
101
|
+
BOOK_CALLER = None
|
102
|
+
missing = object()
|
103
|
+
|
104
|
+
|
105
|
+
@atexit.register
|
106
|
+
def cleanup():
|
107
|
+
"""Clear up any zombie processes"""
|
108
|
+
try:
|
109
|
+
Apps.cleanup()
|
110
|
+
except: # noqa: E722
|
111
|
+
pass
|
112
|
+
|
113
|
+
|
114
|
+
class COMRetryMethodWrapper:
|
115
|
+
def __init__(self, method):
|
116
|
+
self.__method = method
|
117
|
+
|
118
|
+
def __call__(self, *args, **kwargs):
|
119
|
+
n_attempt = 1
|
120
|
+
while True:
|
121
|
+
try:
|
122
|
+
v = self.__method(*args, **kwargs)
|
123
|
+
if isinstance(v, (CDispatch, CoClassBaseClass, DispatchBaseClass)):
|
124
|
+
return COMRetryObjectWrapper(v)
|
125
|
+
elif isinstance(v, types.MethodType):
|
126
|
+
return COMRetryMethodWrapper(v)
|
127
|
+
else:
|
128
|
+
return v
|
129
|
+
except pywintypes.com_error as e:
|
130
|
+
if (
|
131
|
+
not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS
|
132
|
+
) and e.hresult == -2147418111:
|
133
|
+
n_attempt += 1
|
134
|
+
continue
|
135
|
+
else:
|
136
|
+
raise
|
137
|
+
except AttributeError:
|
138
|
+
if not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS:
|
139
|
+
n_attempt += 1
|
140
|
+
continue
|
141
|
+
else:
|
142
|
+
raise
|
143
|
+
|
144
|
+
|
145
|
+
class ExcelBusyError(Exception):
|
146
|
+
def __init__(self):
|
147
|
+
super(ExcelBusyError, self).__init__("Excel application is not responding")
|
148
|
+
|
149
|
+
|
150
|
+
class COMRetryObjectWrapper:
|
151
|
+
def __init__(self, inner):
|
152
|
+
object.__setattr__(self, "_inner", inner)
|
153
|
+
|
154
|
+
def __repr__(self):
|
155
|
+
return repr(self._inner)
|
156
|
+
|
157
|
+
def __setattr__(self, key, value):
|
158
|
+
n_attempt = 1
|
159
|
+
while True:
|
160
|
+
try:
|
161
|
+
return setattr(self._inner, key, value)
|
162
|
+
except pywintypes.com_error as e:
|
163
|
+
hresult, msg, exc, arg = e.args
|
164
|
+
if exc:
|
165
|
+
wcode, source, text, help_file, help_id, scode = exc
|
166
|
+
else:
|
167
|
+
wcode, source, text, help_file, help_id, scode = ( # noqa: F841
|
168
|
+
None,
|
169
|
+
None,
|
170
|
+
None,
|
171
|
+
None,
|
172
|
+
None,
|
173
|
+
None,
|
174
|
+
)
|
175
|
+
# -2147352567 is the error you get when clicking into cells. If we
|
176
|
+
# wouldn't check for scode, actions like renaming a sheet with >31
|
177
|
+
# characters would be tried forever, causing xlwings to hang (they
|
178
|
+
# also have hresult -2147352567).
|
179
|
+
if (
|
180
|
+
(not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS)
|
181
|
+
and e.hresult in [-2147418111, -2147352567]
|
182
|
+
and scode in [None, -2146777998]
|
183
|
+
):
|
184
|
+
n_attempt += 1
|
185
|
+
continue
|
186
|
+
else:
|
187
|
+
raise
|
188
|
+
except AttributeError:
|
189
|
+
if not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS:
|
190
|
+
n_attempt += 1
|
191
|
+
continue
|
192
|
+
else:
|
193
|
+
raise
|
194
|
+
|
195
|
+
def __getattr__(self, item):
|
196
|
+
n_attempt = 1
|
197
|
+
while True:
|
198
|
+
try:
|
199
|
+
v = getattr(self._inner, item)
|
200
|
+
if isinstance(v, (CDispatch, CoClassBaseClass, DispatchBaseClass)):
|
201
|
+
return COMRetryObjectWrapper(v)
|
202
|
+
elif isinstance(v, types.MethodType):
|
203
|
+
return COMRetryMethodWrapper(v)
|
204
|
+
else:
|
205
|
+
return v
|
206
|
+
except pywintypes.com_error as e:
|
207
|
+
if (
|
208
|
+
not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS
|
209
|
+
) and e.hresult == -2147418111:
|
210
|
+
n_attempt += 1
|
211
|
+
continue
|
212
|
+
else:
|
213
|
+
raise
|
214
|
+
except AttributeError:
|
215
|
+
# pywin32 reacts incorrectly to RPC_E_CALL_REJECTED (i.e. assumes
|
216
|
+
# attribute doesn't exist, thus not allowing to distinguish between
|
217
|
+
# cases where attribute really doesn't exist or error is only being
|
218
|
+
# thrown because the COM RPC server is busy). Here we try to test to
|
219
|
+
# see what's going on really
|
220
|
+
try:
|
221
|
+
self._oleobj_.GetIDsOfNames(0, item)
|
222
|
+
except pythoncom.ole_error as e:
|
223
|
+
if e.hresult != -2147418111: # RPC_E_CALL_REJECTED
|
224
|
+
# attribute probably really doesn't exist
|
225
|
+
raise
|
226
|
+
if not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS:
|
227
|
+
n_attempt += 1
|
228
|
+
continue
|
229
|
+
else:
|
230
|
+
raise ExcelBusyError()
|
231
|
+
|
232
|
+
def __call__(self, *args, **kwargs):
|
233
|
+
n_attempt = 1
|
234
|
+
for i in range(N_COM_ATTEMPTS + 1):
|
235
|
+
try:
|
236
|
+
v = self._inner(*args, **kwargs)
|
237
|
+
if isinstance(v, (CDispatch, CoClassBaseClass, DispatchBaseClass)):
|
238
|
+
return COMRetryObjectWrapper(v)
|
239
|
+
elif isinstance(v, types.MethodType):
|
240
|
+
return COMRetryMethodWrapper(v)
|
241
|
+
else:
|
242
|
+
return v
|
243
|
+
except pywintypes.com_error as e:
|
244
|
+
if (
|
245
|
+
not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS
|
246
|
+
) and e.hresult == -2147418111:
|
247
|
+
n_attempt += 1
|
248
|
+
continue
|
249
|
+
else:
|
250
|
+
raise
|
251
|
+
except AttributeError:
|
252
|
+
if not N_COM_ATTEMPTS or n_attempt < N_COM_ATTEMPTS:
|
253
|
+
n_attempt += 1
|
254
|
+
continue
|
255
|
+
else:
|
256
|
+
raise
|
257
|
+
|
258
|
+
def __iter__(self):
|
259
|
+
for v in self._inner:
|
260
|
+
if isinstance(v, (CDispatch, CoClassBaseClass, DispatchBaseClass)):
|
261
|
+
yield COMRetryObjectWrapper(v)
|
262
|
+
else:
|
263
|
+
yield v
|
264
|
+
|
265
|
+
|
266
|
+
# Constants
|
267
|
+
OBJID_NATIVEOM = -16
|
268
|
+
|
269
|
+
|
270
|
+
class _GUID(ctypes.Structure):
|
271
|
+
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/
|
272
|
+
# ms-dtyp/49e490b8-f972-45d6-a3a4-99f924998d97
|
273
|
+
_fields_ = [
|
274
|
+
("Data1", ctypes.c_ulong),
|
275
|
+
("Data2", ctypes.c_ushort),
|
276
|
+
("Data3", ctypes.c_ushort),
|
277
|
+
("Data4", ctypes.c_byte * 8),
|
278
|
+
]
|
279
|
+
|
280
|
+
|
281
|
+
_IDISPATCH_GUID = _GUID()
|
282
|
+
oledll.ole32.CLSIDFromString(
|
283
|
+
"{00020400-0000-0000-C000-000000000046}", byref(_IDISPATCH_GUID)
|
284
|
+
)
|
285
|
+
|
286
|
+
|
287
|
+
def accessible_object_from_window(hwnd):
|
288
|
+
# ptr is a pointer to an IDispatch:
|
289
|
+
# https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-idispatch
|
290
|
+
# We don't bother using ctypes.POINTER(comtypes.automation.IDispatch)()
|
291
|
+
# because we won't dereference the pointer except through pywin32's
|
292
|
+
# pythoncom.PyCom_PyObjectFromIUnknown below in get_xl_app_from_hwnd().
|
293
|
+
ptr = ctypes.c_void_p()
|
294
|
+
res = oledll.oleacc.AccessibleObjectFromWindow( # noqa: F841
|
295
|
+
hwnd, OBJID_NATIVEOM, byref(_IDISPATCH_GUID), byref(ptr)
|
296
|
+
)
|
297
|
+
return ptr
|
298
|
+
|
299
|
+
|
300
|
+
def _checkDblpas(hwnd):
|
301
|
+
childWindow=win32gui.FindWindowEx(hwnd, 0, None, None)
|
302
|
+
if childWindow == 0:
|
303
|
+
return 0
|
304
|
+
pid = win32process.GetWindowThreadProcessId(childWindow)[1]
|
305
|
+
xldeskHwnd=win32gui.FindWindowEx(childWindow, 0, 'XLDESK', None)
|
306
|
+
if(xldeskHwnd==0):
|
307
|
+
xldeskHwnd=win32gui.FindWindowEx(hwnd, 0, 'XLDESK', None)
|
308
|
+
if xldeskHwnd==0:
|
309
|
+
handle = _checkDblpas(childWindow)
|
310
|
+
if handle == 0:
|
311
|
+
handlenext = windll.user32.GetWindow(childWindow,2)
|
312
|
+
if handlenext != 0:
|
313
|
+
while(1):
|
314
|
+
handletmp = _checkDblpas(handlenext)
|
315
|
+
if handletmp == 0:
|
316
|
+
handlenext = windll.user32.GetWindow(handlenext,2)
|
317
|
+
else:
|
318
|
+
return handletmp
|
319
|
+
else:
|
320
|
+
return 0
|
321
|
+
else:
|
322
|
+
return handle
|
323
|
+
else:
|
324
|
+
excel7Hwnd=win32gui.FindWindowEx(xldeskHwnd, 0, 'EXCEL7', None)
|
325
|
+
if excel7Hwnd:
|
326
|
+
return excel7Hwnd
|
327
|
+
|
328
|
+
|
329
|
+
def is_hwnd_xl_app(hwnd):
|
330
|
+
try:
|
331
|
+
child_hwnd = win32gui.FindWindowEx(hwnd, 0, "XLDESK", None)
|
332
|
+
child_hwnd = win32gui.FindWindowEx(child_hwnd, 0, "EXCEL7", None)
|
333
|
+
if child_hwnd == 0:
|
334
|
+
child_hwnd = _checkDblpas(hwnd)
|
335
|
+
ptr = accessible_object_from_window(child_hwnd) # noqa: F841
|
336
|
+
return True
|
337
|
+
except WindowsError:
|
338
|
+
return False
|
339
|
+
except pywintypes.error:
|
340
|
+
return False
|
341
|
+
|
342
|
+
|
343
|
+
_PyCom_PyObjectFromIUnknown = PyDLL(pythoncom.__file__).PyCom_PyObjectFromIUnknown
|
344
|
+
_PyCom_PyObjectFromIUnknown.restype = py_object
|
345
|
+
|
346
|
+
|
347
|
+
def get_xl_app_from_hwnd(hwnd):
|
348
|
+
pythoncom.CoInitialize()
|
349
|
+
child_hwnd = win32gui.FindWindowEx(hwnd, 0, "XLDESK", None)
|
350
|
+
child_hwnd = win32gui.FindWindowEx(child_hwnd, 0, "EXCEL7", None)
|
351
|
+
if child_hwnd == 0:
|
352
|
+
child_hwnd = _checkDblpas(hwnd)
|
353
|
+
ptr = accessible_object_from_window(child_hwnd)
|
354
|
+
p = _PyCom_PyObjectFromIUnknown(ptr, byref(_IDISPATCH_GUID), True)
|
355
|
+
disp = COMRetryObjectWrapper(Dispatch(p))
|
356
|
+
return disp.Application
|
357
|
+
|
358
|
+
|
359
|
+
def get_excel_hwnds():
|
360
|
+
pythoncom.CoInitialize()
|
361
|
+
hwnd = windll.user32.GetTopWindow(None)
|
362
|
+
pids = set()
|
363
|
+
while hwnd:
|
364
|
+
try:
|
365
|
+
# Apparently, this fails on some systems when Excel is closed
|
366
|
+
child_hwnd = win32gui.FindWindowEx(hwnd, 0, "XLDESK", None)
|
367
|
+
if child_hwnd:
|
368
|
+
child_hwnd = win32gui.FindWindowEx(child_hwnd, 0, "EXCEL7", None)
|
369
|
+
if child_hwnd:
|
370
|
+
pid = win32process.GetWindowThreadProcessId(hwnd)[1]
|
371
|
+
if pid not in pids:
|
372
|
+
pids.add(pid)
|
373
|
+
yield hwnd
|
374
|
+
|
375
|
+
child_hwnd = win32gui.FindWindowEx(hwnd, 0, 'XLMAIN', None)
|
376
|
+
if child_hwnd == 0 and win32gui.GetClassName(hwnd) == 'XLMAIN':
|
377
|
+
child_hwnd = hwnd
|
378
|
+
if child_hwnd:
|
379
|
+
child_hwnd = _checkDblpas(hwnd)
|
380
|
+
if child_hwnd:
|
381
|
+
pid = win32process.GetWindowThreadProcessId(hwnd)[1]
|
382
|
+
yield hwnd
|
383
|
+
|
384
|
+
except pywintypes.error:
|
385
|
+
pass
|
386
|
+
|
387
|
+
hwnd = windll.user32.GetWindow(hwnd, 2) # 2 = next window according to Z-order
|
388
|
+
|
389
|
+
|
390
|
+
def get_xl_apps():
|
391
|
+
for hwnd in get_excel_hwnds():
|
392
|
+
try:
|
393
|
+
yield get_xl_app_from_hwnd(hwnd)
|
394
|
+
except ExcelBusyError:
|
395
|
+
pass
|
396
|
+
except WindowsError:
|
397
|
+
# This happens if the bare Excel Application is open without Workbook, i.e.,
|
398
|
+
# there's no 'EXCEL7' child hwnd that would be necessary for a connection
|
399
|
+
pass
|
400
|
+
|
401
|
+
|
402
|
+
def is_range_instance(xl_range):
|
403
|
+
pyid = getattr(xl_range, "_oleobj_", None)
|
404
|
+
if pyid is None:
|
405
|
+
return False
|
406
|
+
return xl_range._oleobj_.GetTypeInfo().GetTypeAttr().iid == pywintypes.IID(
|
407
|
+
"{00020846-0000-0000-C000-000000000046}"
|
408
|
+
)
|
409
|
+
# return pyid.GetTypeInfo().GetDocumentation(-1)[0] == 'Range'
|
410
|
+
|
411
|
+
|
412
|
+
def _com_time_to_datetime(com_time, datetime_builder):
|
413
|
+
return datetime_builder(
|
414
|
+
month=com_time.month,
|
415
|
+
day=com_time.day,
|
416
|
+
year=com_time.year,
|
417
|
+
hour=com_time.hour,
|
418
|
+
minute=com_time.minute,
|
419
|
+
second=com_time.second,
|
420
|
+
microsecond=com_time.microsecond,
|
421
|
+
tzinfo=None,
|
422
|
+
)
|
423
|
+
|
424
|
+
|
425
|
+
def _datetime_to_com_time(dt_time):
|
426
|
+
"""
|
427
|
+
This function is a modified version from Pyvot (https://pypi.python.org/pypi/Pyvot)
|
428
|
+
and subject to the following copyright:
|
429
|
+
|
430
|
+
Copyright (c) Microsoft Corporation.
|
431
|
+
|
432
|
+
This source code is subject to terms and conditions of the Apache License,
|
433
|
+
Version 2.0. A copy of the license can be found in the LICENSE.txt file at the root
|
434
|
+
of this distribution. If you cannot locate the Apache License, Version 2.0, please
|
435
|
+
send an email to vspython@microsoft.com. By using this source code in any fashion,
|
436
|
+
you are agreeing to be bound by the terms of the Apache License, Version 2.0.
|
437
|
+
|
438
|
+
You must not remove this notice, or any other, from this software.
|
439
|
+
|
440
|
+
"""
|
441
|
+
# Convert date to datetime
|
442
|
+
if pd and isinstance(dt_time, type(pd.NaT)):
|
443
|
+
return ""
|
444
|
+
if np:
|
445
|
+
if type(dt_time) is np.datetime64:
|
446
|
+
dt_time = np_datetime_to_datetime(dt_time)
|
447
|
+
|
448
|
+
if type(dt_time) is dt.date:
|
449
|
+
dt_time = dt.datetime(
|
450
|
+
dt_time.year,
|
451
|
+
dt_time.month,
|
452
|
+
dt_time.day,
|
453
|
+
tzinfo=win32timezone.TimeZoneInfo.utc(),
|
454
|
+
)
|
455
|
+
|
456
|
+
# pywintypes has its time type inherit from datetime.
|
457
|
+
# For some reason, though it accepts plain datetimes, they must have a timezone set.
|
458
|
+
# See http://docs.activestate.com/activepython/2.7/pywin32/html/win32/help/py3k.html
|
459
|
+
# We replace no timezone -> UTC to allow round-trips in the naive case
|
460
|
+
if pd and isinstance(dt_time, pd.Timestamp):
|
461
|
+
# Otherwise pandas prints ignored exceptions on Python 3
|
462
|
+
dt_time = dt_time.to_pydatetime()
|
463
|
+
# We don't use pytz.utc to get rid of additional dependency
|
464
|
+
# Don't do any timezone transformation: simply cutoff the tz info
|
465
|
+
# If we don't reset it first, it gets transformed into UTC before sending to Excel
|
466
|
+
dt_time = dt_time.replace(tzinfo=None)
|
467
|
+
dt_time = dt_time.replace(tzinfo=win32timezone.TimeZoneInfo.utc())
|
468
|
+
|
469
|
+
return dt_time
|
470
|
+
|
471
|
+
|
472
|
+
cell_errors = {
|
473
|
+
-2146826281: "#DIV/0!",
|
474
|
+
-2146826246: "#N/A",
|
475
|
+
-2146826259: "#NAME?",
|
476
|
+
-2146826288: "#NULL!",
|
477
|
+
-2146826252: "#NUM!",
|
478
|
+
-2146826265: "#REF!",
|
479
|
+
-2146826273: "#VALUE!",
|
480
|
+
}
|
481
|
+
|
482
|
+
|
483
|
+
def _clean_value_data_element(
|
484
|
+
value, datetime_builder, empty_as, number_builder, err_to_str
|
485
|
+
):
|
486
|
+
if value in ("", None):
|
487
|
+
return empty_as
|
488
|
+
elif isinstance(value, time_types):
|
489
|
+
return _com_time_to_datetime(value, datetime_builder)
|
490
|
+
elif number_builder is not None and isinstance(value, float):
|
491
|
+
value = number_builder(value)
|
492
|
+
elif isinstance(value, int) and value in cell_errors:
|
493
|
+
if err_to_str:
|
494
|
+
return cell_errors[value]
|
495
|
+
else:
|
496
|
+
return None
|
497
|
+
return value
|
498
|
+
|
499
|
+
|
500
|
+
class Engine:
|
501
|
+
@property
|
502
|
+
def apps(self):
|
503
|
+
return Apps()
|
504
|
+
|
505
|
+
@property
|
506
|
+
def name(self):
|
507
|
+
return "excel"
|
508
|
+
|
509
|
+
@property
|
510
|
+
def type(self):
|
511
|
+
return "desktop"
|
512
|
+
|
513
|
+
@staticmethod
|
514
|
+
def prepare_xl_data_element(x, date_format):
|
515
|
+
if isinstance(x, time_types):
|
516
|
+
return _datetime_to_com_time(x)
|
517
|
+
elif pd and pd.isna(x):
|
518
|
+
return ""
|
519
|
+
elif np and isinstance(x, (np.floating, float)) and np.isnan(x):
|
520
|
+
return ""
|
521
|
+
elif np and isinstance(x, np.number):
|
522
|
+
return float(x)
|
523
|
+
elif x is None:
|
524
|
+
return ""
|
525
|
+
else:
|
526
|
+
return x
|
527
|
+
|
528
|
+
@staticmethod
|
529
|
+
def clean_value_data(data, datetime_builder, empty_as, number_builder, err_to_str):
|
530
|
+
return [
|
531
|
+
[
|
532
|
+
_clean_value_data_element(
|
533
|
+
c, datetime_builder, empty_as, number_builder, err_to_str
|
534
|
+
)
|
535
|
+
for c in row
|
536
|
+
]
|
537
|
+
for row in data
|
538
|
+
]
|
539
|
+
|
540
|
+
|
541
|
+
engine = Engine()
|
542
|
+
|
543
|
+
|
544
|
+
class Apps(base_classes.Apps):
|
545
|
+
def keys(self):
|
546
|
+
k = []
|
547
|
+
for hwnd in get_excel_hwnds():
|
548
|
+
k.append(App(xl=hwnd).pid)
|
549
|
+
return k
|
550
|
+
|
551
|
+
def add(self, spec=None, add_book=None, xl=None, visible=None):
|
552
|
+
return App(spec=spec, add_book=add_book, xl=xl, visible=visible)
|
553
|
+
|
554
|
+
@staticmethod
|
555
|
+
def cleanup():
|
556
|
+
res = subprocess.run(
|
557
|
+
split('tasklist /FI "IMAGENAME eq EXCEL.exe"'),
|
558
|
+
stdout=subprocess.PIPE,
|
559
|
+
stderr=subprocess.STDOUT,
|
560
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
561
|
+
encoding=locale.getpreferredencoding(),
|
562
|
+
)
|
563
|
+
|
564
|
+
all_pids = set()
|
565
|
+
for line in res.stdout.splitlines()[3:]:
|
566
|
+
# Ignored if there's no processes as it prints only 1 line
|
567
|
+
_, pid, _, _, _, _ = line.split()
|
568
|
+
all_pids.add(int(pid))
|
569
|
+
|
570
|
+
active_pids = {app.pid for app in xlwings.apps}
|
571
|
+
zombie_pids = all_pids - active_pids
|
572
|
+
|
573
|
+
for pid in zombie_pids:
|
574
|
+
subprocess.run(
|
575
|
+
split(f"taskkill /PID {pid} /F"),
|
576
|
+
stdout=subprocess.PIPE,
|
577
|
+
stderr=subprocess.STDOUT,
|
578
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
579
|
+
encoding=locale.getpreferredencoding(),
|
580
|
+
)
|
581
|
+
|
582
|
+
def __iter__(self):
|
583
|
+
for hwnd in get_excel_hwnds():
|
584
|
+
yield App(xl=hwnd)
|
585
|
+
|
586
|
+
def __len__(self):
|
587
|
+
return len(list(get_excel_hwnds()))
|
588
|
+
|
589
|
+
def __getitem__(self, pid):
|
590
|
+
for hwnd in get_excel_hwnds():
|
591
|
+
app = App(xl=hwnd)
|
592
|
+
if app.pid == pid:
|
593
|
+
return app
|
594
|
+
raise KeyError("Could not find an Excel instance with this PID.")
|
595
|
+
|
596
|
+
|
597
|
+
class App(base_classes.App):
|
598
|
+
def __init__(self, spec=None, add_book=True, xl=None, visible=None):
|
599
|
+
# visible is only required on mac
|
600
|
+
pythoncom.CoInitialize()
|
601
|
+
if spec is not None:
|
602
|
+
warn("spec is ignored on Windows.")
|
603
|
+
if xl is None:
|
604
|
+
# new instance
|
605
|
+
self._xl = COMRetryObjectWrapper(DispatchEx("Excel.Application"))
|
606
|
+
if add_book:
|
607
|
+
self._xl.Workbooks.Add()
|
608
|
+
self._hwnd = None
|
609
|
+
elif isinstance(xl, int):
|
610
|
+
self._xl = None
|
611
|
+
self._hwnd = xl
|
612
|
+
else:
|
613
|
+
self._xl = xl
|
614
|
+
self._hwnd = None
|
615
|
+
self._pid = self.pid
|
616
|
+
|
617
|
+
@property
|
618
|
+
def xl(self):
|
619
|
+
if self._xl is None:
|
620
|
+
self._xl = get_xl_app_from_hwnd(self._hwnd)
|
621
|
+
return self._xl
|
622
|
+
|
623
|
+
@xl.setter
|
624
|
+
def xl(self, value):
|
625
|
+
self._xl = value
|
626
|
+
|
627
|
+
api = xl
|
628
|
+
|
629
|
+
@property
|
630
|
+
def engine(self):
|
631
|
+
return engine
|
632
|
+
|
633
|
+
@property
|
634
|
+
def selection(self):
|
635
|
+
try:
|
636
|
+
_ = (
|
637
|
+
self.xl.Selection.Address
|
638
|
+
) # Force exception outside of the retry wrapper e.g., if chart is selected
|
639
|
+
return Range(xl=self.xl.Selection)
|
640
|
+
except pywintypes.com_error:
|
641
|
+
return None
|
642
|
+
|
643
|
+
def activate(self, steal_focus=False):
|
644
|
+
# makes the Excel instance the foreground Excel instance,
|
645
|
+
# but not the foreground desktop app if the current foreground
|
646
|
+
# app isn't already an Excel instance
|
647
|
+
hwnd = windll.user32.GetForegroundWindow()
|
648
|
+
if steal_focus or is_hwnd_xl_app(hwnd):
|
649
|
+
windll.user32.SetForegroundWindow(self.xl.Hwnd)
|
650
|
+
else:
|
651
|
+
windll.user32.SetWindowPos(self.xl.Hwnd, hwnd, 0, 0, 0, 0, 0x1 | 0x2 | 0x10)
|
652
|
+
|
653
|
+
@property
|
654
|
+
def visible(self):
|
655
|
+
return self.xl.Visible
|
656
|
+
|
657
|
+
@visible.setter
|
658
|
+
def visible(self, visible):
|
659
|
+
self.xl.Visible = visible
|
660
|
+
|
661
|
+
def quit(self):
|
662
|
+
self.xl.DisplayAlerts = False
|
663
|
+
self.xl.Quit()
|
664
|
+
self.xl = None
|
665
|
+
try:
|
666
|
+
Apps.cleanup()
|
667
|
+
except: # noqa: E722
|
668
|
+
pass
|
669
|
+
|
670
|
+
def kill(self):
|
671
|
+
PROCESS_TERMINATE = 1
|
672
|
+
handle = win32api.OpenProcess(PROCESS_TERMINATE, False, self._pid)
|
673
|
+
win32api.TerminateProcess(handle, -1)
|
674
|
+
win32api.CloseHandle(handle)
|
675
|
+
try:
|
676
|
+
Apps.cleanup()
|
677
|
+
except: # noqa: E722
|
678
|
+
pass
|
679
|
+
|
680
|
+
@property
|
681
|
+
def screen_updating(self):
|
682
|
+
return self.xl.ScreenUpdating
|
683
|
+
|
684
|
+
@screen_updating.setter
|
685
|
+
def screen_updating(self, value):
|
686
|
+
self.xl.ScreenUpdating = value
|
687
|
+
|
688
|
+
@property
|
689
|
+
def display_alerts(self):
|
690
|
+
return self.xl.DisplayAlerts
|
691
|
+
|
692
|
+
@display_alerts.setter
|
693
|
+
def display_alerts(self, value):
|
694
|
+
self.xl.DisplayAlerts = value
|
695
|
+
|
696
|
+
@property
|
697
|
+
def enable_events(self):
|
698
|
+
return self.xl.EnableEvents
|
699
|
+
|
700
|
+
@enable_events.setter
|
701
|
+
def enable_events(self, value):
|
702
|
+
self.xl.EnableEvents = value
|
703
|
+
|
704
|
+
@property
|
705
|
+
def interactive(self):
|
706
|
+
return self.xl.Interactive
|
707
|
+
|
708
|
+
@interactive.setter
|
709
|
+
def interactive(self, value):
|
710
|
+
self.xl.Interactive = value
|
711
|
+
|
712
|
+
@property
|
713
|
+
def startup_path(self):
|
714
|
+
return self.xl.StartupPath
|
715
|
+
|
716
|
+
@property
|
717
|
+
def calculation(self):
|
718
|
+
return calculation_i2s[self.xl.Calculation]
|
719
|
+
|
720
|
+
@calculation.setter
|
721
|
+
def calculation(self, value):
|
722
|
+
self.xl.Calculation = calculation_s2i[value]
|
723
|
+
|
724
|
+
def calculate(self):
|
725
|
+
self.xl.Calculate()
|
726
|
+
|
727
|
+
@property
|
728
|
+
def version(self):
|
729
|
+
return self.xl.Version
|
730
|
+
|
731
|
+
@property
|
732
|
+
def books(self):
|
733
|
+
return Books(xl=self.xl.Workbooks, app=self)
|
734
|
+
|
735
|
+
@property
|
736
|
+
def hwnd(self):
|
737
|
+
if self._hwnd is None:
|
738
|
+
self._hwnd = self._xl.Hwnd
|
739
|
+
return self._hwnd
|
740
|
+
|
741
|
+
@property
|
742
|
+
def path(self):
|
743
|
+
return self.xl.Path
|
744
|
+
|
745
|
+
@property
|
746
|
+
def pid(self):
|
747
|
+
return win32process.GetWindowThreadProcessId(self.hwnd)[1]
|
748
|
+
|
749
|
+
def run(self, macro, args):
|
750
|
+
return self.xl.Run(macro, *args)
|
751
|
+
|
752
|
+
@property
|
753
|
+
def status_bar(self):
|
754
|
+
return self.xl.StatusBar
|
755
|
+
|
756
|
+
@status_bar.setter
|
757
|
+
def status_bar(self, value):
|
758
|
+
self.xl.StatusBar = value
|
759
|
+
|
760
|
+
@property
|
761
|
+
def cut_copy_mode(self):
|
762
|
+
modes = {2: "cut", 1: "copy"}
|
763
|
+
return modes.get(self.xl.CutCopyMode)
|
764
|
+
|
765
|
+
@cut_copy_mode.setter
|
766
|
+
def cut_copy_mode(self, value):
|
767
|
+
self.xl.CutCopyMode = value
|
768
|
+
|
769
|
+
def alert(self, prompt, title, buttons, mode, callback):
|
770
|
+
buttons_dict = {
|
771
|
+
None: win32con.MB_OK,
|
772
|
+
"ok": win32con.MB_OK,
|
773
|
+
"ok_cancel": win32con.MB_OKCANCEL,
|
774
|
+
"yes_no": win32con.MB_YESNO,
|
775
|
+
"yes_no_cancel": win32con.MB_YESNOCANCEL,
|
776
|
+
}
|
777
|
+
modes = {
|
778
|
+
"info": win32con.MB_ICONINFORMATION,
|
779
|
+
"critical": win32con.MB_ICONWARNING,
|
780
|
+
}
|
781
|
+
style = buttons_dict[buttons]
|
782
|
+
if mode:
|
783
|
+
style += modes[mode]
|
784
|
+
rv = win32api.MessageBox(
|
785
|
+
self.hwnd,
|
786
|
+
"" if prompt is None else prompt,
|
787
|
+
"" if title is None else title,
|
788
|
+
style,
|
789
|
+
)
|
790
|
+
return_values = {1: "ok", 2: "cancel", 6: "yes", 7: "no"}
|
791
|
+
return return_values[rv]
|
792
|
+
|
793
|
+
|
794
|
+
class Books(base_classes.Books):
|
795
|
+
def __init__(self, xl, app):
|
796
|
+
self.xl = xl
|
797
|
+
self.app = app
|
798
|
+
|
799
|
+
@property
|
800
|
+
def api(self):
|
801
|
+
return self.xl
|
802
|
+
|
803
|
+
@property
|
804
|
+
def active(self):
|
805
|
+
return Book(self.xl.Application.ActiveWorkbook)
|
806
|
+
|
807
|
+
def __call__(self, name_or_index):
|
808
|
+
try:
|
809
|
+
return Book(xl=self.xl(name_or_index))
|
810
|
+
except pywintypes.com_error:
|
811
|
+
raise KeyError(name_or_index)
|
812
|
+
|
813
|
+
def __len__(self):
|
814
|
+
return self.xl.Count
|
815
|
+
|
816
|
+
def add(self):
|
817
|
+
return Book(xl=self.xl.Add())
|
818
|
+
|
819
|
+
def open(
|
820
|
+
self,
|
821
|
+
fullname,
|
822
|
+
update_links=None,
|
823
|
+
read_only=None,
|
824
|
+
format=None,
|
825
|
+
password=None,
|
826
|
+
write_res_password=None,
|
827
|
+
ignore_read_only_recommended=None,
|
828
|
+
origin=None,
|
829
|
+
delimiter=None,
|
830
|
+
editable=None,
|
831
|
+
notify=None,
|
832
|
+
converter=None,
|
833
|
+
add_to_mru=None,
|
834
|
+
local=None,
|
835
|
+
corrupt_load=None,
|
836
|
+
):
|
837
|
+
# update_links: According to VBA docs, only constants 0 and 3 are supported
|
838
|
+
if update_links:
|
839
|
+
update_links = UpdateLinks.xlUpdateLinksAlways
|
840
|
+
# Workbooks.Open params are position only on pywin32
|
841
|
+
return Book(
|
842
|
+
xl=self.xl.Open(
|
843
|
+
fullname,
|
844
|
+
update_links,
|
845
|
+
read_only,
|
846
|
+
format,
|
847
|
+
password,
|
848
|
+
write_res_password,
|
849
|
+
ignore_read_only_recommended,
|
850
|
+
origin,
|
851
|
+
delimiter,
|
852
|
+
editable,
|
853
|
+
notify,
|
854
|
+
converter,
|
855
|
+
add_to_mru,
|
856
|
+
local,
|
857
|
+
corrupt_load,
|
858
|
+
)
|
859
|
+
)
|
860
|
+
|
861
|
+
def __iter__(self):
|
862
|
+
for xl in self.xl:
|
863
|
+
yield Book(xl=xl)
|
864
|
+
|
865
|
+
|
866
|
+
class Book(base_classes.Book):
|
867
|
+
def __init__(self, xl):
|
868
|
+
self.xl = xl
|
869
|
+
|
870
|
+
@property
|
871
|
+
def api(self):
|
872
|
+
return self.xl
|
873
|
+
|
874
|
+
def json(self):
|
875
|
+
raise NotImplementedError()
|
876
|
+
|
877
|
+
@property
|
878
|
+
def name(self):
|
879
|
+
return self.xl.Name
|
880
|
+
|
881
|
+
@property
|
882
|
+
def sheets(self):
|
883
|
+
return Sheets(xl=self.xl.Worksheets)
|
884
|
+
|
885
|
+
@property
|
886
|
+
def app(self):
|
887
|
+
return App(xl=self.xl.Application)
|
888
|
+
|
889
|
+
def close(self):
|
890
|
+
self.xl.Close(SaveChanges=False)
|
891
|
+
|
892
|
+
def save(self, path=None, password=None):
|
893
|
+
saved_path = self.xl.Path
|
894
|
+
source_ext = os.path.splitext(self.name)[1] if saved_path else None
|
895
|
+
target_ext = os.path.splitext(path)[1] if path else ".xlsx"
|
896
|
+
if saved_path and source_ext == target_ext:
|
897
|
+
file_format = self.xl.FileFormat
|
898
|
+
else:
|
899
|
+
ext_to_file_format = {
|
900
|
+
".xlsx": FileFormat.xlOpenXMLWorkbook,
|
901
|
+
".xlsm": FileFormat.xlOpenXMLWorkbookMacroEnabled,
|
902
|
+
".xlsb": FileFormat.xlExcel12,
|
903
|
+
".xltm": FileFormat.xlOpenXMLTemplateMacroEnabled,
|
904
|
+
".xltx": FileFormat.xlOpenXMLTemplateMacroEnabled,
|
905
|
+
".xlam": FileFormat.xlOpenXMLAddIn,
|
906
|
+
".xls": FileFormat.xlWorkbookNormal,
|
907
|
+
".xlt": FileFormat.xlTemplate,
|
908
|
+
".xla": FileFormat.xlAddIn,
|
909
|
+
".html": FileFormat.xlHtml,
|
910
|
+
}
|
911
|
+
file_format = ext_to_file_format[target_ext]
|
912
|
+
if (saved_path != "") and (path is None):
|
913
|
+
# Previously saved: Save under existing name
|
914
|
+
self.xl.Save()
|
915
|
+
elif (
|
916
|
+
(saved_path != "") and (path is not None) and (os.path.split(path)[0] == "")
|
917
|
+
):
|
918
|
+
# Save existing book under new name in cwd if no path has been provided
|
919
|
+
path = os.path.join(os.getcwd(), path)
|
920
|
+
self.xl.SaveAs(
|
921
|
+
os.path.realpath(path), FileFormat=file_format, Password=password
|
922
|
+
)
|
923
|
+
elif (saved_path == "") and (path is None):
|
924
|
+
# Previously unsaved: Save under current name in current working directory
|
925
|
+
path = os.path.join(os.getcwd(), self.xl.Name + ".xlsx")
|
926
|
+
alerts_state = self.xl.Application.DisplayAlerts
|
927
|
+
self.xl.Application.DisplayAlerts = False
|
928
|
+
self.xl.SaveAs(
|
929
|
+
os.path.realpath(path), FileFormat=file_format, Password=password
|
930
|
+
)
|
931
|
+
self.xl.Application.DisplayAlerts = alerts_state
|
932
|
+
elif path:
|
933
|
+
# Save under new name/location
|
934
|
+
alerts_state = self.xl.Application.DisplayAlerts
|
935
|
+
self.xl.Application.DisplayAlerts = False
|
936
|
+
self.xl.SaveAs(
|
937
|
+
os.path.realpath(path), FileFormat=file_format, Password=password
|
938
|
+
)
|
939
|
+
self.xl.Application.DisplayAlerts = alerts_state
|
940
|
+
|
941
|
+
@property
|
942
|
+
def fullname(self):
|
943
|
+
if "://" in self.xl.FullName:
|
944
|
+
config = read_config_sheet(xlwings.Book(impl=self))
|
945
|
+
return fullname_url_to_local_path(
|
946
|
+
url=self.xl.FullName,
|
947
|
+
sheet_onedrive_consumer_config=config.get("ONEDRIVE_CONSUMER_WIN"),
|
948
|
+
sheet_onedrive_commercial_config=config.get("ONEDRIVE_COMMERCIAL_WIN"),
|
949
|
+
sheet_sharepoint_config=config.get("SHAREPOINT_WIN"),
|
950
|
+
)
|
951
|
+
else:
|
952
|
+
return self.xl.FullName
|
953
|
+
|
954
|
+
@property
|
955
|
+
def names(self):
|
956
|
+
return Names(xl=self.xl.Names)
|
957
|
+
|
958
|
+
def activate(self):
|
959
|
+
self.xl.Activate()
|
960
|
+
|
961
|
+
def to_pdf(self, path, quality):
|
962
|
+
self.xl.ExportAsFixedFormat(
|
963
|
+
Type=FixedFormatType.xlTypePDF,
|
964
|
+
Filename=path,
|
965
|
+
Quality=quality_types[quality],
|
966
|
+
IncludeDocProperties=True,
|
967
|
+
IgnorePrintAreas=False,
|
968
|
+
OpenAfterPublish=False,
|
969
|
+
)
|
970
|
+
|
971
|
+
|
972
|
+
class Sheets(base_classes.Sheets):
|
973
|
+
def __init__(self, xl):
|
974
|
+
self.xl = xl
|
975
|
+
|
976
|
+
@property
|
977
|
+
def api(self):
|
978
|
+
return self.xl
|
979
|
+
|
980
|
+
@property
|
981
|
+
def active(self):
|
982
|
+
return Sheet(self.xl.Parent.ActiveSheet)
|
983
|
+
|
984
|
+
def __call__(self, name_or_index):
|
985
|
+
return Sheet(xl=self.xl(name_or_index))
|
986
|
+
|
987
|
+
def __len__(self):
|
988
|
+
return self.xl.Count
|
989
|
+
|
990
|
+
def __iter__(self):
|
991
|
+
for xl in self.xl:
|
992
|
+
yield Sheet(xl=xl)
|
993
|
+
|
994
|
+
def add(self, before=None, after=None, name=None):
|
995
|
+
if before:
|
996
|
+
sheet = Sheet(xl=self.xl.Add(Before=before.xl))
|
997
|
+
if name is not None:
|
998
|
+
sheet.name = name
|
999
|
+
return sheet
|
1000
|
+
elif after:
|
1001
|
+
# Hack, since "After" is broken in certain environments
|
1002
|
+
# see: http://code.activestate.com/lists/python-win32/11554/
|
1003
|
+
count = self.xl.Count
|
1004
|
+
new_sheet_index = after.xl.Index + 1
|
1005
|
+
if new_sheet_index > count:
|
1006
|
+
xl_sheet = self.xl.Add(Before=after.xl)
|
1007
|
+
self.xl(self.xl.Count).Move(Before=self.xl(self.xl.Count - 1))
|
1008
|
+
self.xl(self.xl.Count).Activate()
|
1009
|
+
else:
|
1010
|
+
xl_sheet = self.xl.Add(Before=self.xl(after.xl.Index + 1))
|
1011
|
+
sheet = Sheet(xl=xl_sheet)
|
1012
|
+
if name is not None:
|
1013
|
+
sheet.name = name
|
1014
|
+
return sheet
|
1015
|
+
else:
|
1016
|
+
sheet = Sheet(xl=self.xl.Add())
|
1017
|
+
if name is not None:
|
1018
|
+
sheet.name = name
|
1019
|
+
return sheet
|
1020
|
+
|
1021
|
+
|
1022
|
+
class Sheet(base_classes.Sheet):
|
1023
|
+
def __init__(self, xl):
|
1024
|
+
self.xl = xl
|
1025
|
+
|
1026
|
+
@property
|
1027
|
+
def api(self):
|
1028
|
+
return self.xl
|
1029
|
+
|
1030
|
+
@property
|
1031
|
+
def name(self):
|
1032
|
+
return self.xl.Name
|
1033
|
+
|
1034
|
+
@name.setter
|
1035
|
+
def name(self, value):
|
1036
|
+
self.xl.Name = value
|
1037
|
+
|
1038
|
+
@property
|
1039
|
+
def names(self):
|
1040
|
+
return Names(xl=self.xl.Names)
|
1041
|
+
|
1042
|
+
@property
|
1043
|
+
def book(self):
|
1044
|
+
return Book(xl=self.xl.Parent)
|
1045
|
+
|
1046
|
+
@property
|
1047
|
+
def index(self):
|
1048
|
+
return self.xl.Index
|
1049
|
+
|
1050
|
+
def range(self, arg1, arg2=None):
|
1051
|
+
if isinstance(arg1, Range):
|
1052
|
+
xl1 = arg1.xl
|
1053
|
+
elif isinstance(arg1, tuple):
|
1054
|
+
if len(arg1) == 4:
|
1055
|
+
row, col, nrows, ncols = arg1
|
1056
|
+
return Range(xl=(self.xl, row, col, nrows, ncols))
|
1057
|
+
if 0 in arg1:
|
1058
|
+
raise IndexError(
|
1059
|
+
"Attempted to access 0-based Range. "
|
1060
|
+
"xlwings/Excel Ranges are 1-based."
|
1061
|
+
)
|
1062
|
+
xl1 = self.xl.Cells(arg1[0], arg1[1])
|
1063
|
+
elif isinstance(arg1, numbers.Number) and isinstance(arg2, numbers.Number):
|
1064
|
+
xl1 = self.xl.Cells(arg1, arg2)
|
1065
|
+
arg2 = None
|
1066
|
+
else:
|
1067
|
+
xl1 = self.xl.Range(arg1)
|
1068
|
+
|
1069
|
+
if arg2 is None:
|
1070
|
+
return Range(xl=xl1)
|
1071
|
+
|
1072
|
+
if isinstance(arg2, Range):
|
1073
|
+
xl2 = arg2.xl
|
1074
|
+
elif isinstance(arg2, tuple):
|
1075
|
+
if 0 in arg2:
|
1076
|
+
raise IndexError(
|
1077
|
+
"Attempted to access 0-based Range. "
|
1078
|
+
"xlwings/Excel Ranges are 1-based."
|
1079
|
+
)
|
1080
|
+
xl2 = self.xl.Cells(arg2[0], arg2[1])
|
1081
|
+
else:
|
1082
|
+
xl2 = self.xl.Range(arg2)
|
1083
|
+
|
1084
|
+
return Range(xl=self.xl.Range(xl1, xl2))
|
1085
|
+
|
1086
|
+
@property
|
1087
|
+
def cells(self):
|
1088
|
+
return Range(xl=self.xl.Cells)
|
1089
|
+
|
1090
|
+
def activate(self):
|
1091
|
+
return self.xl.Activate()
|
1092
|
+
|
1093
|
+
def select(self):
|
1094
|
+
return self.xl.Select()
|
1095
|
+
|
1096
|
+
def clear_contents(self):
|
1097
|
+
self.xl.Cells.ClearContents()
|
1098
|
+
|
1099
|
+
def clear_formats(self):
|
1100
|
+
self.xl.Cells.ClearFormats()
|
1101
|
+
|
1102
|
+
def clear(self):
|
1103
|
+
self.xl.Cells.Clear()
|
1104
|
+
|
1105
|
+
def autofit(self, axis=None):
|
1106
|
+
if axis == "rows" or axis == "r":
|
1107
|
+
self.xl.Rows.AutoFit()
|
1108
|
+
elif axis == "columns" or axis == "c":
|
1109
|
+
self.xl.Columns.AutoFit()
|
1110
|
+
elif axis is None:
|
1111
|
+
self.xl.Rows.AutoFit()
|
1112
|
+
self.xl.Columns.AutoFit()
|
1113
|
+
|
1114
|
+
def delete(self):
|
1115
|
+
app = self.xl.Parent.Application
|
1116
|
+
alerts_state = app.DisplayAlerts
|
1117
|
+
app.DisplayAlerts = False
|
1118
|
+
self.xl.Delete()
|
1119
|
+
app.DisplayAlerts = alerts_state
|
1120
|
+
|
1121
|
+
def copy(self, before, after):
|
1122
|
+
if before:
|
1123
|
+
before = before.xl
|
1124
|
+
if after:
|
1125
|
+
after = after.xl
|
1126
|
+
self.xl.Copy(Before=before, After=after)
|
1127
|
+
|
1128
|
+
@property
|
1129
|
+
def charts(self):
|
1130
|
+
return Charts(xl=self.xl.ChartObjects())
|
1131
|
+
|
1132
|
+
@property
|
1133
|
+
def shapes(self):
|
1134
|
+
return Shapes(xl=self.xl.Shapes)
|
1135
|
+
|
1136
|
+
@property
|
1137
|
+
def tables(self):
|
1138
|
+
return Tables(xl=self.xl.ListObjects)
|
1139
|
+
|
1140
|
+
@property
|
1141
|
+
def pictures(self):
|
1142
|
+
return Pictures(xl=self.xl.Pictures())
|
1143
|
+
|
1144
|
+
@property
|
1145
|
+
def used_range(self):
|
1146
|
+
return Range(xl=self.xl.UsedRange)
|
1147
|
+
|
1148
|
+
@property
|
1149
|
+
def visible(self):
|
1150
|
+
return self.xl.Visible
|
1151
|
+
|
1152
|
+
@visible.setter
|
1153
|
+
def visible(self, value):
|
1154
|
+
self.xl.Visible = value
|
1155
|
+
|
1156
|
+
@property
|
1157
|
+
def page_setup(self):
|
1158
|
+
return PageSetup(self.xl.PageSetup)
|
1159
|
+
|
1160
|
+
def to_html(self, path):
|
1161
|
+
if not Path(path).is_absolute():
|
1162
|
+
path = Path(".").resolve() / path
|
1163
|
+
source_cell2 = self.used_range.address.split(":")
|
1164
|
+
if len(source_cell2) == 2:
|
1165
|
+
source = f"A1:{source_cell2[1]}"
|
1166
|
+
else:
|
1167
|
+
source = f"A1:{source_cell2[0]}"
|
1168
|
+
self.book.xl.PublishObjects.Add(
|
1169
|
+
SourceType=SourceType.xlSourceRange,
|
1170
|
+
Filename=path,
|
1171
|
+
Sheet=self.name,
|
1172
|
+
Source=source,
|
1173
|
+
HtmlType=HtmlType.xlHtmlStatic,
|
1174
|
+
).Publish(True)
|
1175
|
+
html_file = Path(path)
|
1176
|
+
content = html_file.read_text()
|
1177
|
+
html_file.write_text(
|
1178
|
+
content.replace(
|
1179
|
+
"align=center x:publishsource=", "align=left x:publishsource="
|
1180
|
+
)
|
1181
|
+
)
|
1182
|
+
|
1183
|
+
|
1184
|
+
class Range(base_classes.Range):
|
1185
|
+
def __init__(self, xl):
|
1186
|
+
if isinstance(xl, tuple):
|
1187
|
+
self._coords = xl
|
1188
|
+
self._xl = missing
|
1189
|
+
else:
|
1190
|
+
self._coords = missing
|
1191
|
+
self._xl = xl
|
1192
|
+
|
1193
|
+
@property
|
1194
|
+
def xl(self):
|
1195
|
+
if self._xl is missing:
|
1196
|
+
xl_sheet, row, col, nrows, ncols = self._coords
|
1197
|
+
if nrows and ncols:
|
1198
|
+
self._xl = xl_sheet.Range(
|
1199
|
+
xl_sheet.Cells(row, col),
|
1200
|
+
xl_sheet.Cells(row + nrows - 1, col + ncols - 1),
|
1201
|
+
)
|
1202
|
+
else:
|
1203
|
+
self._xl = None
|
1204
|
+
return self._xl
|
1205
|
+
|
1206
|
+
@property
|
1207
|
+
def coords(self):
|
1208
|
+
if self._coords is missing:
|
1209
|
+
self._coords = (
|
1210
|
+
self.xl.Worksheet,
|
1211
|
+
self.xl.Row,
|
1212
|
+
self.xl.Column,
|
1213
|
+
self.xl.Rows.Count,
|
1214
|
+
self.xl.Columns.Count,
|
1215
|
+
)
|
1216
|
+
return self._coords
|
1217
|
+
|
1218
|
+
@property
|
1219
|
+
def api(self):
|
1220
|
+
return self.xl
|
1221
|
+
|
1222
|
+
@property
|
1223
|
+
def sheet(self):
|
1224
|
+
return Sheet(xl=self.coords[0])
|
1225
|
+
|
1226
|
+
def __len__(self):
|
1227
|
+
return (self.xl and self.xl.Count) or 0
|
1228
|
+
|
1229
|
+
@property
|
1230
|
+
def row(self):
|
1231
|
+
return self.coords[1]
|
1232
|
+
|
1233
|
+
@property
|
1234
|
+
def column(self):
|
1235
|
+
return self.coords[2]
|
1236
|
+
|
1237
|
+
@property
|
1238
|
+
def shape(self):
|
1239
|
+
return self.coords[3], self.coords[4]
|
1240
|
+
|
1241
|
+
@property
|
1242
|
+
def raw_value(self):
|
1243
|
+
if self.xl is not None:
|
1244
|
+
return self.xl.Value
|
1245
|
+
else:
|
1246
|
+
return None
|
1247
|
+
|
1248
|
+
@raw_value.setter
|
1249
|
+
def raw_value(self, data):
|
1250
|
+
if self.xl is not None:
|
1251
|
+
self.xl.Value = data
|
1252
|
+
|
1253
|
+
def clear_contents(self):
|
1254
|
+
if self.xl is not None:
|
1255
|
+
self.xl.ClearContents()
|
1256
|
+
|
1257
|
+
def clear_formats(self):
|
1258
|
+
self.xl.ClearFormats()
|
1259
|
+
|
1260
|
+
def clear(self):
|
1261
|
+
if self.xl is not None:
|
1262
|
+
self.xl.Clear()
|
1263
|
+
|
1264
|
+
@property
|
1265
|
+
def formula(self):
|
1266
|
+
if self.xl is not None:
|
1267
|
+
return self.xl.Formula
|
1268
|
+
else:
|
1269
|
+
return None
|
1270
|
+
|
1271
|
+
@formula.setter
|
1272
|
+
def formula(self, value):
|
1273
|
+
if self.xl is not None:
|
1274
|
+
self.xl.Formula = value
|
1275
|
+
|
1276
|
+
@property
|
1277
|
+
def formula2(self):
|
1278
|
+
if self.xl is not None:
|
1279
|
+
return self.xl.Formula2
|
1280
|
+
else:
|
1281
|
+
return None
|
1282
|
+
|
1283
|
+
@formula2.setter
|
1284
|
+
def formula2(self, value):
|
1285
|
+
if self.xl is not None:
|
1286
|
+
self.xl.Formula2 = value
|
1287
|
+
|
1288
|
+
def end(self, direction):
|
1289
|
+
direction = directions_s2i.get(direction, direction)
|
1290
|
+
return Range(xl=self.xl.End(direction))
|
1291
|
+
|
1292
|
+
@property
|
1293
|
+
def formula_array(self):
|
1294
|
+
if self.xl is not None:
|
1295
|
+
return self.xl.FormulaArray
|
1296
|
+
else:
|
1297
|
+
return None
|
1298
|
+
|
1299
|
+
@formula_array.setter
|
1300
|
+
def formula_array(self, value):
|
1301
|
+
if self.xl is not None:
|
1302
|
+
self.xl.FormulaArray = value
|
1303
|
+
|
1304
|
+
@property
|
1305
|
+
def font(self):
|
1306
|
+
return Font(self, self.xl.Font)
|
1307
|
+
|
1308
|
+
@property
|
1309
|
+
def column_width(self):
|
1310
|
+
if self.xl is not None:
|
1311
|
+
return self.xl.ColumnWidth
|
1312
|
+
else:
|
1313
|
+
return 0
|
1314
|
+
|
1315
|
+
@column_width.setter
|
1316
|
+
def column_width(self, value):
|
1317
|
+
if self.xl is not None:
|
1318
|
+
self.xl.ColumnWidth = value
|
1319
|
+
|
1320
|
+
@property
|
1321
|
+
def row_height(self):
|
1322
|
+
if self.xl is not None:
|
1323
|
+
return self.xl.RowHeight
|
1324
|
+
else:
|
1325
|
+
return 0
|
1326
|
+
|
1327
|
+
@row_height.setter
|
1328
|
+
def row_height(self, value):
|
1329
|
+
if self.xl is not None:
|
1330
|
+
self.xl.RowHeight = value
|
1331
|
+
|
1332
|
+
@property
|
1333
|
+
def width(self):
|
1334
|
+
if self.xl is not None:
|
1335
|
+
return self.xl.Width
|
1336
|
+
else:
|
1337
|
+
return 0
|
1338
|
+
|
1339
|
+
@property
|
1340
|
+
def height(self):
|
1341
|
+
if self.xl is not None:
|
1342
|
+
return self.xl.Height
|
1343
|
+
else:
|
1344
|
+
return 0
|
1345
|
+
|
1346
|
+
@property
|
1347
|
+
def left(self):
|
1348
|
+
if self.xl is not None:
|
1349
|
+
return self.xl.Left
|
1350
|
+
else:
|
1351
|
+
return 0
|
1352
|
+
|
1353
|
+
@property
|
1354
|
+
def top(self):
|
1355
|
+
if self.xl is not None:
|
1356
|
+
return self.xl.Top
|
1357
|
+
else:
|
1358
|
+
return 0
|
1359
|
+
|
1360
|
+
@property
|
1361
|
+
def number_format(self):
|
1362
|
+
if self.xl is not None:
|
1363
|
+
return self.xl.NumberFormat
|
1364
|
+
else:
|
1365
|
+
return ""
|
1366
|
+
|
1367
|
+
@number_format.setter
|
1368
|
+
def number_format(self, value):
|
1369
|
+
if self.xl is not None:
|
1370
|
+
self.xl.NumberFormat = value
|
1371
|
+
|
1372
|
+
def get_address(self, row_absolute, col_absolute, external):
|
1373
|
+
if self.xl is not None:
|
1374
|
+
return self.xl.GetAddress(row_absolute, col_absolute, 1, external)
|
1375
|
+
else:
|
1376
|
+
raise NotImplementedError()
|
1377
|
+
|
1378
|
+
@property
|
1379
|
+
def address(self):
|
1380
|
+
if self.xl is not None:
|
1381
|
+
return self.xl.Address
|
1382
|
+
else:
|
1383
|
+
_, row, col, nrows, ncols = self.coords
|
1384
|
+
return "$%s$%s{%sx%s}" % (col_name(col), str(row), nrows, ncols)
|
1385
|
+
|
1386
|
+
@property
|
1387
|
+
def current_region(self):
|
1388
|
+
if self.xl is not None:
|
1389
|
+
return Range(xl=self.xl.CurrentRegion)
|
1390
|
+
else:
|
1391
|
+
return self
|
1392
|
+
|
1393
|
+
def autofit(self, axis=None):
|
1394
|
+
if self.xl is not None:
|
1395
|
+
if axis == "rows" or axis == "r":
|
1396
|
+
self.xl.Rows.AutoFit()
|
1397
|
+
elif axis == "columns" or axis == "c":
|
1398
|
+
self.xl.Columns.AutoFit()
|
1399
|
+
elif axis is None:
|
1400
|
+
self.xl.Columns.AutoFit()
|
1401
|
+
self.xl.Rows.AutoFit()
|
1402
|
+
|
1403
|
+
def insert(self, shift=None, copy_origin=None):
|
1404
|
+
shifts = {
|
1405
|
+
"down": InsertShiftDirection.xlShiftDown,
|
1406
|
+
"right": InsertShiftDirection.xlShiftToRight,
|
1407
|
+
None: None,
|
1408
|
+
}
|
1409
|
+
copy_origins = {
|
1410
|
+
"format_from_left_or_above": InsertFormatOrigin.xlFormatFromLeftOrAbove,
|
1411
|
+
"format_from_right_or_below": InsertFormatOrigin.xlFormatFromRightOrBelow,
|
1412
|
+
}
|
1413
|
+
self.xl.Insert(Shift=shifts[shift], CopyOrigin=copy_origins[copy_origin])
|
1414
|
+
|
1415
|
+
def delete(self, shift=None):
|
1416
|
+
shifts = {
|
1417
|
+
"up": DeleteShiftDirection.xlShiftUp,
|
1418
|
+
"left": DeleteShiftDirection.xlShiftToLeft,
|
1419
|
+
None: None,
|
1420
|
+
}
|
1421
|
+
self.xl.Delete(Shift=shifts[shift])
|
1422
|
+
|
1423
|
+
def copy(self, destination=None):
|
1424
|
+
self.xl.Copy(Destination=destination.api if destination else None)
|
1425
|
+
|
1426
|
+
def paste(self, paste=None, operation=None, skip_blanks=False, transpose=False):
|
1427
|
+
pastes = {
|
1428
|
+
"all": -4104,
|
1429
|
+
None: -4104,
|
1430
|
+
"all_except_borders": 7,
|
1431
|
+
"all_merging_conditional_formats": 14,
|
1432
|
+
"all_using_source_theme": 13,
|
1433
|
+
"column_widths": 8,
|
1434
|
+
"comments": -4144,
|
1435
|
+
"formats": -4122,
|
1436
|
+
"formulas": -4123,
|
1437
|
+
"formulas_and_number_formats": 11,
|
1438
|
+
"validation": 6,
|
1439
|
+
"values": -4163,
|
1440
|
+
"values_and_number_formats": 12,
|
1441
|
+
}
|
1442
|
+
|
1443
|
+
operations = {
|
1444
|
+
"add": 2,
|
1445
|
+
"divide": 5,
|
1446
|
+
"multiply": 4,
|
1447
|
+
None: -4142,
|
1448
|
+
"subtract": 3,
|
1449
|
+
}
|
1450
|
+
|
1451
|
+
self.xl.PasteSpecial(
|
1452
|
+
Paste=pastes[paste],
|
1453
|
+
Operation=operations[operation],
|
1454
|
+
SkipBlanks=skip_blanks,
|
1455
|
+
Transpose=transpose,
|
1456
|
+
)
|
1457
|
+
|
1458
|
+
@property
|
1459
|
+
def hyperlink(self):
|
1460
|
+
if self.xl is not None:
|
1461
|
+
try:
|
1462
|
+
return self.xl.Hyperlinks(1).Address
|
1463
|
+
except pywintypes.com_error:
|
1464
|
+
raise Exception("The cell doesn't seem to contain a hyperlink!")
|
1465
|
+
else:
|
1466
|
+
return ""
|
1467
|
+
|
1468
|
+
def add_hyperlink(self, address, text_to_display, screen_tip):
|
1469
|
+
if self.xl is not None:
|
1470
|
+
# Another one of these pywin32 bugs that only materialize under certain
|
1471
|
+
# circumstances: https://stackoverflow.com/questions/
|
1472
|
+
# 6284227/hyperlink-will-not-show-display-proper-text
|
1473
|
+
link = self.xl.Hyperlinks.Add(Anchor=self.xl, Address=address)
|
1474
|
+
link.TextToDisplay = text_to_display
|
1475
|
+
link.ScreenTip = screen_tip
|
1476
|
+
|
1477
|
+
@property
|
1478
|
+
def color(self):
|
1479
|
+
if self.xl is not None:
|
1480
|
+
if self.xl.Interior.ColorIndex == ColorIndex.xlColorIndexNone:
|
1481
|
+
return None
|
1482
|
+
else:
|
1483
|
+
return int_to_rgb(self.xl.Interior.Color)
|
1484
|
+
else:
|
1485
|
+
return None
|
1486
|
+
|
1487
|
+
@color.setter
|
1488
|
+
def color(self, color_or_rgb):
|
1489
|
+
if isinstance(color_or_rgb, str):
|
1490
|
+
color_or_rgb = hex_to_rgb(color_or_rgb)
|
1491
|
+
if self.xl is not None:
|
1492
|
+
if color_or_rgb is None:
|
1493
|
+
self.xl.Interior.ColorIndex = ColorIndex.xlColorIndexNone
|
1494
|
+
elif isinstance(color_or_rgb, int):
|
1495
|
+
self.xl.Interior.Color = color_or_rgb
|
1496
|
+
else:
|
1497
|
+
self.xl.Interior.Color = rgb_to_int(color_or_rgb)
|
1498
|
+
|
1499
|
+
@property
|
1500
|
+
def name(self):
|
1501
|
+
if self.xl is not None:
|
1502
|
+
try:
|
1503
|
+
name = Name(xl=self.xl.Name)
|
1504
|
+
except pywintypes.com_error:
|
1505
|
+
name = None
|
1506
|
+
return name
|
1507
|
+
else:
|
1508
|
+
return None
|
1509
|
+
|
1510
|
+
@property
|
1511
|
+
def has_array(self):
|
1512
|
+
if self.xl is not None:
|
1513
|
+
try:
|
1514
|
+
return self.xl.HasArray
|
1515
|
+
except pywintypes.com_error:
|
1516
|
+
return False
|
1517
|
+
else:
|
1518
|
+
return False
|
1519
|
+
|
1520
|
+
@name.setter
|
1521
|
+
def name(self, value):
|
1522
|
+
if self.xl is not None:
|
1523
|
+
self.xl.Name = value
|
1524
|
+
|
1525
|
+
def __call__(self, *args):
|
1526
|
+
if self.xl is not None:
|
1527
|
+
if len(args) == 0:
|
1528
|
+
raise ValueError("Invalid arguments")
|
1529
|
+
return Range(xl=self.xl(*args))
|
1530
|
+
else:
|
1531
|
+
raise NotImplementedError()
|
1532
|
+
|
1533
|
+
@property
|
1534
|
+
def rows(self):
|
1535
|
+
return Range(xl=self.xl.Rows)
|
1536
|
+
|
1537
|
+
@property
|
1538
|
+
def columns(self):
|
1539
|
+
return Range(xl=self.xl.Columns)
|
1540
|
+
|
1541
|
+
def select(self):
|
1542
|
+
return self.xl.Select()
|
1543
|
+
|
1544
|
+
@property
|
1545
|
+
def merge_area(self):
|
1546
|
+
return Range(xl=self.xl.MergeArea)
|
1547
|
+
|
1548
|
+
@property
|
1549
|
+
def merge_cells(self):
|
1550
|
+
return self.xl.MergeCells
|
1551
|
+
|
1552
|
+
def merge(self, across):
|
1553
|
+
self.xl.Merge(across)
|
1554
|
+
|
1555
|
+
def unmerge(self):
|
1556
|
+
self.xl.UnMerge()
|
1557
|
+
|
1558
|
+
@property
|
1559
|
+
def table(self):
|
1560
|
+
if self.xl.ListObject:
|
1561
|
+
return Table(self.xl.ListObject)
|
1562
|
+
|
1563
|
+
@property
|
1564
|
+
def characters(self):
|
1565
|
+
return Characters(parent=self, xl=self.xl.GetCharacters)
|
1566
|
+
|
1567
|
+
@property
|
1568
|
+
def wrap_text(self):
|
1569
|
+
return self.xl.WrapText
|
1570
|
+
|
1571
|
+
@wrap_text.setter
|
1572
|
+
def wrap_text(self, value):
|
1573
|
+
self.xl.WrapText = value
|
1574
|
+
|
1575
|
+
@property
|
1576
|
+
def note(self):
|
1577
|
+
return Note(xl=self.xl.Comment) if self.xl.Comment else None
|
1578
|
+
|
1579
|
+
def copy_picture(self, appearance, format):
|
1580
|
+
_appearance = {"screen": 1, "printer": 2}
|
1581
|
+
_format = {"picture": -4147, "bitmap": 2}
|
1582
|
+
self.xl.CopyPicture(Appearance=_appearance[appearance], Format=_format[format])
|
1583
|
+
|
1584
|
+
def to_png(self, path):
|
1585
|
+
max_retries = 10
|
1586
|
+
for retry in range(max_retries):
|
1587
|
+
# https://stackoverflow.com/questions/
|
1588
|
+
# 24740062/copypicture-method-of-range-class-failed-sometimes
|
1589
|
+
try:
|
1590
|
+
# appearance="printer" fails here, not sure why
|
1591
|
+
self.copy_picture(appearance="screen", format="bitmap")
|
1592
|
+
im = ImageGrab.grabclipboard()
|
1593
|
+
im.save(path)
|
1594
|
+
break
|
1595
|
+
except (pywintypes.com_error, AttributeError):
|
1596
|
+
if retry == max_retries - 1:
|
1597
|
+
raise
|
1598
|
+
|
1599
|
+
def to_pdf(self, path, quality):
|
1600
|
+
self.xl.ExportAsFixedFormat(
|
1601
|
+
Type=FixedFormatType.xlTypePDF,
|
1602
|
+
Filename=path,
|
1603
|
+
Quality=quality_types[quality],
|
1604
|
+
IncludeDocProperties=True,
|
1605
|
+
IgnorePrintAreas=False,
|
1606
|
+
OpenAfterPublish=False,
|
1607
|
+
)
|
1608
|
+
|
1609
|
+
def autofill(self, destination, type_):
|
1610
|
+
types = {
|
1611
|
+
"fill_copy": constants.AutoFillType.xlFillCopy,
|
1612
|
+
"fill_days": constants.AutoFillType.xlFillDays,
|
1613
|
+
"fill_default": constants.AutoFillType.xlFillDefault,
|
1614
|
+
"fill_formats": constants.AutoFillType.xlFillFormats,
|
1615
|
+
"fill_months": constants.AutoFillType.xlFillMonths,
|
1616
|
+
"fill_series": constants.AutoFillType.xlFillSeries,
|
1617
|
+
"fill_values": constants.AutoFillType.xlFillValues,
|
1618
|
+
"fill_weekdays": constants.AutoFillType.xlFillWeekdays,
|
1619
|
+
"fill_years": constants.AutoFillType.xlFillYears,
|
1620
|
+
"growth_trend": constants.AutoFillType.xlGrowthTrend,
|
1621
|
+
"linear_trend": constants.AutoFillType.xlLinearTrend,
|
1622
|
+
"flash_fill": constants.AutoFillType.xlFlashFill,
|
1623
|
+
}
|
1624
|
+
self.xl.AutoFill(Destination=destination.api, Type=types[type_])
|
1625
|
+
|
1626
|
+
|
1627
|
+
class Shape(base_classes.Shape):
|
1628
|
+
def __init__(self, xl):
|
1629
|
+
self.xl = xl
|
1630
|
+
|
1631
|
+
@property
|
1632
|
+
def api(self):
|
1633
|
+
return self.xl
|
1634
|
+
|
1635
|
+
@property
|
1636
|
+
def name(self):
|
1637
|
+
return self.xl.Name
|
1638
|
+
|
1639
|
+
@property
|
1640
|
+
def parent(self):
|
1641
|
+
return Sheet(xl=self.xl.Parent)
|
1642
|
+
|
1643
|
+
@property
|
1644
|
+
def type(self):
|
1645
|
+
return shape_types_i2s[self.xl.Type]
|
1646
|
+
|
1647
|
+
@property
|
1648
|
+
def left(self):
|
1649
|
+
return self.xl.Left
|
1650
|
+
|
1651
|
+
@left.setter
|
1652
|
+
def left(self, value):
|
1653
|
+
self.xl.Left = value
|
1654
|
+
|
1655
|
+
@property
|
1656
|
+
def top(self):
|
1657
|
+
return self.xl.Top
|
1658
|
+
|
1659
|
+
@top.setter
|
1660
|
+
def top(self, value):
|
1661
|
+
self.xl.Top = value
|
1662
|
+
|
1663
|
+
@property
|
1664
|
+
def width(self):
|
1665
|
+
return self.xl.Width
|
1666
|
+
|
1667
|
+
@width.setter
|
1668
|
+
def width(self, value):
|
1669
|
+
self.xl.Width = value
|
1670
|
+
|
1671
|
+
@property
|
1672
|
+
def height(self):
|
1673
|
+
return self.xl.Height
|
1674
|
+
|
1675
|
+
@height.setter
|
1676
|
+
def height(self, value):
|
1677
|
+
self.xl.Height = value
|
1678
|
+
|
1679
|
+
def delete(self):
|
1680
|
+
self.xl.Delete()
|
1681
|
+
|
1682
|
+
@name.setter
|
1683
|
+
def name(self, value):
|
1684
|
+
self.xl.Name = value
|
1685
|
+
|
1686
|
+
@property
|
1687
|
+
def index(self):
|
1688
|
+
return self.xl.Index
|
1689
|
+
|
1690
|
+
def activate(self):
|
1691
|
+
self.xl.Activate()
|
1692
|
+
|
1693
|
+
def scale_height(self, factor, relative_to_original_size, scale):
|
1694
|
+
self.xl.ScaleHeight(
|
1695
|
+
Scale=scaling[scale],
|
1696
|
+
RelativeToOriginalSize=relative_to_original_size,
|
1697
|
+
Factor=factor,
|
1698
|
+
)
|
1699
|
+
|
1700
|
+
def scale_width(self, factor, relative_to_original_size, scale):
|
1701
|
+
self.xl.ScaleWidth(
|
1702
|
+
Scale=scaling[scale],
|
1703
|
+
RelativeToOriginalSize=relative_to_original_size,
|
1704
|
+
Factor=factor,
|
1705
|
+
)
|
1706
|
+
|
1707
|
+
@property
|
1708
|
+
def text(self):
|
1709
|
+
if self.xl.TextFrame2.HasText:
|
1710
|
+
return self.xl.TextFrame2.TextRange.Text
|
1711
|
+
|
1712
|
+
@text.setter
|
1713
|
+
def text(self, value):
|
1714
|
+
self.xl.TextFrame2.TextRange.Text = value
|
1715
|
+
|
1716
|
+
@property
|
1717
|
+
def font(self):
|
1718
|
+
return Font(self, self.xl.TextFrame2.TextRange.Font)
|
1719
|
+
|
1720
|
+
@property
|
1721
|
+
def characters(self):
|
1722
|
+
return Characters(parent=self, xl=self.xl.TextFrame2.TextRange.GetCharacters)
|
1723
|
+
|
1724
|
+
|
1725
|
+
class Font(base_classes.Font):
|
1726
|
+
def __init__(self, parent, xl):
|
1727
|
+
self.parent = parent
|
1728
|
+
self.xl = xl
|
1729
|
+
|
1730
|
+
@property
|
1731
|
+
def api(self):
|
1732
|
+
return self.xl
|
1733
|
+
|
1734
|
+
@property
|
1735
|
+
def bold(self):
|
1736
|
+
if isinstance(self.parent, Range):
|
1737
|
+
return self.xl.Bold
|
1738
|
+
elif isinstance(self.parent, Shape):
|
1739
|
+
return True if self.xl.Bold == -1 else False
|
1740
|
+
elif isinstance(self.parent.parent, Range):
|
1741
|
+
return self.xl.Bold
|
1742
|
+
elif isinstance(self.parent.parent, Shape):
|
1743
|
+
return True if self.xl.Bold == -1 else False
|
1744
|
+
elif isinstance(self.parent.parent.parent, Range):
|
1745
|
+
return self.xl.Bold
|
1746
|
+
elif isinstance(self.parent.parent.parent, Shape):
|
1747
|
+
return True if self.xl.Bold == -1 else False
|
1748
|
+
|
1749
|
+
@bold.setter
|
1750
|
+
def bold(self, value):
|
1751
|
+
self.xl.Bold = value
|
1752
|
+
|
1753
|
+
@property
|
1754
|
+
def italic(self):
|
1755
|
+
if isinstance(self.parent, Range):
|
1756
|
+
return self.xl.Italic
|
1757
|
+
elif isinstance(self.parent, Shape):
|
1758
|
+
return True if self.xl.Italic == -1 else False
|
1759
|
+
elif isinstance(self.parent.parent, Range):
|
1760
|
+
return self.xl.Italic
|
1761
|
+
elif isinstance(self.parent.parent, Shape):
|
1762
|
+
return True if self.xl.Italic == -1 else False
|
1763
|
+
elif isinstance(self.parent.parent.parent, Range):
|
1764
|
+
return self.xl.Italic
|
1765
|
+
elif isinstance(self.parent.parent.parent, Shape):
|
1766
|
+
return True if self.xl.Italic == -1 else False
|
1767
|
+
|
1768
|
+
@italic.setter
|
1769
|
+
def italic(self, value):
|
1770
|
+
self.xl.Italic = value
|
1771
|
+
|
1772
|
+
@property
|
1773
|
+
def size(self):
|
1774
|
+
return self.xl.Size
|
1775
|
+
|
1776
|
+
@size.setter
|
1777
|
+
def size(self, value):
|
1778
|
+
self.xl.Size = value
|
1779
|
+
|
1780
|
+
@property
|
1781
|
+
def color(self):
|
1782
|
+
# self.parent is used for direct access, self.parent.parent via characters
|
1783
|
+
if isinstance(self.parent, Shape):
|
1784
|
+
return int_to_rgb(self.xl.Fill.ForeColor.RGB)
|
1785
|
+
elif isinstance(self.parent, Range):
|
1786
|
+
return int_to_rgb(self.xl.Color)
|
1787
|
+
elif isinstance(self.parent.parent, Shape):
|
1788
|
+
return int_to_rgb(self.xl.Fill.ForeColor.RGB)
|
1789
|
+
elif isinstance(self.parent.parent, Range):
|
1790
|
+
return int_to_rgb(self.xl.Color)
|
1791
|
+
elif isinstance(self.parent.parent.parent, Shape):
|
1792
|
+
return int_to_rgb(self.xl.Fill.ForeColor.RGB)
|
1793
|
+
elif isinstance(self.parent.parent.parent, Range):
|
1794
|
+
return int_to_rgb(self.xl.Color)
|
1795
|
+
|
1796
|
+
@color.setter
|
1797
|
+
def color(self, color_or_rgb):
|
1798
|
+
# TODO: refactor
|
1799
|
+
if self.xl is not None:
|
1800
|
+
if isinstance(self.parent, Shape):
|
1801
|
+
if isinstance(color_or_rgb, int):
|
1802
|
+
self.xl.Fill.ForeColor.RGB = color_or_rgb
|
1803
|
+
else:
|
1804
|
+
self.xl.Fill.ForeColor.RGB = rgb_to_int(color_or_rgb)
|
1805
|
+
elif isinstance(self.parent, Range):
|
1806
|
+
if isinstance(color_or_rgb, int):
|
1807
|
+
self.xl.Color = color_or_rgb
|
1808
|
+
else:
|
1809
|
+
self.xl.Color = rgb_to_int(color_or_rgb)
|
1810
|
+
|
1811
|
+
elif isinstance(self.parent.parent, Shape):
|
1812
|
+
if isinstance(color_or_rgb, int):
|
1813
|
+
self.xl.Fill.ForeColor.RGB = color_or_rgb
|
1814
|
+
else:
|
1815
|
+
self.xl.Fill.ForeColor.RGB = rgb_to_int(color_or_rgb)
|
1816
|
+
elif isinstance(self.parent.parent, Range):
|
1817
|
+
if isinstance(color_or_rgb, int):
|
1818
|
+
self.xl.Color = color_or_rgb
|
1819
|
+
else:
|
1820
|
+
self.xl.Color = rgb_to_int(color_or_rgb)
|
1821
|
+
|
1822
|
+
elif isinstance(self.parent.parent.parent, Shape):
|
1823
|
+
if isinstance(color_or_rgb, int):
|
1824
|
+
self.xl.Fill.ForeColor.RGB = color_or_rgb
|
1825
|
+
else:
|
1826
|
+
self.xl.Fill.ForeColor.RGB = rgb_to_int(color_or_rgb)
|
1827
|
+
elif isinstance(self.parent.parent.parent, Range):
|
1828
|
+
if isinstance(color_or_rgb, int):
|
1829
|
+
self.xl.Color = color_or_rgb
|
1830
|
+
else:
|
1831
|
+
self.xl.Color = rgb_to_int(color_or_rgb)
|
1832
|
+
|
1833
|
+
@property
|
1834
|
+
def name(self):
|
1835
|
+
return self.xl.Name
|
1836
|
+
|
1837
|
+
@name.setter
|
1838
|
+
def name(self, value):
|
1839
|
+
self.xl.Name = value
|
1840
|
+
|
1841
|
+
|
1842
|
+
class Characters(base_classes.Characters):
|
1843
|
+
def __init__(self, parent, xl, start=None, length=None):
|
1844
|
+
self.parent = parent
|
1845
|
+
self.xl = xl
|
1846
|
+
self.start = start if start else 1
|
1847
|
+
self.length = length if length else xl().Count
|
1848
|
+
|
1849
|
+
@property
|
1850
|
+
def api(self):
|
1851
|
+
return self.xl(self.start, self.length)
|
1852
|
+
|
1853
|
+
@property
|
1854
|
+
def text(self):
|
1855
|
+
return self.xl(self.start, self.length).Text
|
1856
|
+
|
1857
|
+
@property
|
1858
|
+
def font(self):
|
1859
|
+
return Font(self, self.xl(self.start, self.length).Font)
|
1860
|
+
|
1861
|
+
def __getitem__(self, item):
|
1862
|
+
if isinstance(item, slice):
|
1863
|
+
if (item.start and item.start < 0) or (item.stop and item.stop < 0):
|
1864
|
+
raise ValueError(
|
1865
|
+
self.__class__.__name__
|
1866
|
+
+ " object does not support slicing with negative indexes"
|
1867
|
+
)
|
1868
|
+
start = item.start + 1 if item.start else 1
|
1869
|
+
length = item.stop + 1 - start if item.stop else self.length + 1 - start
|
1870
|
+
return Characters(parent=self, xl=self.xl, start=start, length=length)
|
1871
|
+
else:
|
1872
|
+
if item >= 0:
|
1873
|
+
return Characters(parent=self, xl=self.xl, start=item + 1, length=1)
|
1874
|
+
else:
|
1875
|
+
return Characters(
|
1876
|
+
parent=self, xl=self.xl, start=len(self.text) + 1 + item, length=1
|
1877
|
+
)
|
1878
|
+
|
1879
|
+
|
1880
|
+
class Collection(base_classes.Collection):
|
1881
|
+
def __init__(self, xl):
|
1882
|
+
self.xl = xl
|
1883
|
+
|
1884
|
+
@property
|
1885
|
+
def api(self):
|
1886
|
+
return self.xl
|
1887
|
+
|
1888
|
+
def __call__(self, key):
|
1889
|
+
try:
|
1890
|
+
return self._wrap(xl=self.xl.Item(key))
|
1891
|
+
except pywintypes.com_error:
|
1892
|
+
raise KeyError(key)
|
1893
|
+
|
1894
|
+
def __len__(self):
|
1895
|
+
return self.xl.Count
|
1896
|
+
|
1897
|
+
def __iter__(self):
|
1898
|
+
for xl in self.xl:
|
1899
|
+
yield self._wrap(xl=xl)
|
1900
|
+
|
1901
|
+
def __contains__(self, key):
|
1902
|
+
try:
|
1903
|
+
self.xl.Item(key)
|
1904
|
+
return True
|
1905
|
+
except pywintypes.com_error:
|
1906
|
+
return False
|
1907
|
+
|
1908
|
+
|
1909
|
+
class PageSetup(base_classes.PageSetup):
|
1910
|
+
def __init__(self, xl):
|
1911
|
+
self.xl = xl
|
1912
|
+
|
1913
|
+
@property
|
1914
|
+
def api(self):
|
1915
|
+
return self.xl
|
1916
|
+
|
1917
|
+
@property
|
1918
|
+
def print_area(self):
|
1919
|
+
value = self.xl.PrintArea
|
1920
|
+
return None if value == "" else value
|
1921
|
+
|
1922
|
+
@print_area.setter
|
1923
|
+
def print_area(self, value):
|
1924
|
+
self.xl.PrintArea = value
|
1925
|
+
|
1926
|
+
|
1927
|
+
class Note(base_classes.Note):
|
1928
|
+
def __init__(self, xl):
|
1929
|
+
self.xl = xl
|
1930
|
+
|
1931
|
+
@property
|
1932
|
+
def api(self):
|
1933
|
+
return self.xl
|
1934
|
+
|
1935
|
+
@property
|
1936
|
+
def text(self):
|
1937
|
+
return self.xl.Text()
|
1938
|
+
|
1939
|
+
@text.setter
|
1940
|
+
def text(self, value):
|
1941
|
+
self.xl.Text(value)
|
1942
|
+
|
1943
|
+
def delete(self):
|
1944
|
+
self.xl.Delete()
|
1945
|
+
|
1946
|
+
|
1947
|
+
class Shapes(Collection):
|
1948
|
+
_wrap = Shape
|
1949
|
+
|
1950
|
+
|
1951
|
+
class Table(base_classes.Table):
|
1952
|
+
def __init__(self, xl):
|
1953
|
+
self.xl = xl
|
1954
|
+
|
1955
|
+
@property
|
1956
|
+
def api(self):
|
1957
|
+
return self.xl
|
1958
|
+
|
1959
|
+
@property
|
1960
|
+
def name(self):
|
1961
|
+
return self.xl.Name
|
1962
|
+
|
1963
|
+
@name.setter
|
1964
|
+
def name(self, value):
|
1965
|
+
self.xl.Name = value
|
1966
|
+
|
1967
|
+
@property
|
1968
|
+
def data_body_range(self):
|
1969
|
+
return Range(xl=self.xl.DataBodyRange) if self.xl.DataBodyRange else None
|
1970
|
+
|
1971
|
+
@property
|
1972
|
+
def display_name(self):
|
1973
|
+
return self.xl.DisplayName
|
1974
|
+
|
1975
|
+
@display_name.setter
|
1976
|
+
def display_name(self, value):
|
1977
|
+
self.xl.DisplayName = value
|
1978
|
+
|
1979
|
+
@property
|
1980
|
+
def header_row_range(self):
|
1981
|
+
return Range(xl=self.xl.HeaderRowRange)
|
1982
|
+
|
1983
|
+
@property
|
1984
|
+
def insert_row_range(self):
|
1985
|
+
return Range(xl=self.xl.InsertRowRange)
|
1986
|
+
|
1987
|
+
@property
|
1988
|
+
def parent(self):
|
1989
|
+
return Sheet(xl=self.xl.Parent)
|
1990
|
+
|
1991
|
+
@property
|
1992
|
+
def range(self):
|
1993
|
+
return Range(xl=self.xl.Range)
|
1994
|
+
|
1995
|
+
@property
|
1996
|
+
def show_autofilter(self):
|
1997
|
+
return self.xl.ShowAutoFilter
|
1998
|
+
|
1999
|
+
@show_autofilter.setter
|
2000
|
+
def show_autofilter(self, value):
|
2001
|
+
self.xl.ShowAutoFilter = value
|
2002
|
+
|
2003
|
+
@property
|
2004
|
+
def show_headers(self):
|
2005
|
+
return self.xl.ShowHeaders
|
2006
|
+
|
2007
|
+
@show_headers.setter
|
2008
|
+
def show_headers(self, value):
|
2009
|
+
self.xl.ShowHeaders = value
|
2010
|
+
|
2011
|
+
@property
|
2012
|
+
def show_table_style_column_stripes(self):
|
2013
|
+
return self.xl.ShowTableStyleColumnStripes
|
2014
|
+
|
2015
|
+
@show_table_style_column_stripes.setter
|
2016
|
+
def show_table_style_column_stripes(self, value):
|
2017
|
+
self.xl.ShowTableStyleColumnStripes = value
|
2018
|
+
|
2019
|
+
@property
|
2020
|
+
def show_table_style_first_column(self):
|
2021
|
+
return self.xl.ShowTableStyleFirstColumn
|
2022
|
+
|
2023
|
+
@show_table_style_first_column.setter
|
2024
|
+
def show_table_style_first_column(self, value):
|
2025
|
+
self.xl.ShowTableStyleFirstColumn = value
|
2026
|
+
|
2027
|
+
@property
|
2028
|
+
def show_table_style_last_column(self):
|
2029
|
+
return self.xl.ShowTableStyleLastColumn
|
2030
|
+
|
2031
|
+
@show_table_style_last_column.setter
|
2032
|
+
def show_table_style_last_column(self, value):
|
2033
|
+
self.xl.ShowTableStyleLastColumn = value
|
2034
|
+
|
2035
|
+
@property
|
2036
|
+
def show_table_style_row_stripes(self):
|
2037
|
+
return self.xl.ShowTableStyleRowStripes
|
2038
|
+
|
2039
|
+
@show_table_style_row_stripes.setter
|
2040
|
+
def show_table_style_row_stripes(self, value):
|
2041
|
+
self.xl.ShowTableStyleRowStripes = value
|
2042
|
+
|
2043
|
+
@property
|
2044
|
+
def show_totals(self):
|
2045
|
+
return self.xl.ShowTotals
|
2046
|
+
|
2047
|
+
@show_totals.setter
|
2048
|
+
def show_totals(self, value):
|
2049
|
+
self.xl.ShowTotals = value
|
2050
|
+
|
2051
|
+
@property
|
2052
|
+
def table_style(self):
|
2053
|
+
return self.xl.TableStyle.Name
|
2054
|
+
|
2055
|
+
@table_style.setter
|
2056
|
+
def table_style(self, value):
|
2057
|
+
self.xl.TableStyle = value
|
2058
|
+
|
2059
|
+
@property
|
2060
|
+
def totals_row_range(self):
|
2061
|
+
return Range(xl=self.xl.TotalsRowRange)
|
2062
|
+
|
2063
|
+
def resize(self, range):
|
2064
|
+
self.xl.Resize(range.api)
|
2065
|
+
|
2066
|
+
|
2067
|
+
class Tables(Collection, base_classes.Tables):
|
2068
|
+
_wrap = Table
|
2069
|
+
|
2070
|
+
def add(
|
2071
|
+
self,
|
2072
|
+
source_type=None,
|
2073
|
+
source=None,
|
2074
|
+
link_source=None,
|
2075
|
+
has_headers=None,
|
2076
|
+
destination=None,
|
2077
|
+
table_style_name=None,
|
2078
|
+
name=None,
|
2079
|
+
):
|
2080
|
+
table = Table(
|
2081
|
+
xl=self.xl.Add(
|
2082
|
+
SourceType=ListObjectSourceType.xlSrcRange,
|
2083
|
+
Source=source.api,
|
2084
|
+
LinkSource=link_source,
|
2085
|
+
XlListObjectHasHeaders=True,
|
2086
|
+
Destination=destination,
|
2087
|
+
TableStyleName=table_style_name,
|
2088
|
+
)
|
2089
|
+
)
|
2090
|
+
if name is not None:
|
2091
|
+
table.name = name
|
2092
|
+
return table
|
2093
|
+
|
2094
|
+
|
2095
|
+
class Chart(base_classes.Chart):
|
2096
|
+
def __init__(self, xl_obj=None, xl=None):
|
2097
|
+
self.xl = xl_obj.Chart if xl is None else xl
|
2098
|
+
self.xl_obj = xl_obj
|
2099
|
+
|
2100
|
+
@property
|
2101
|
+
def api(self):
|
2102
|
+
return self.xl_obj, self.xl
|
2103
|
+
|
2104
|
+
@property
|
2105
|
+
def name(self):
|
2106
|
+
if self.xl_obj is None:
|
2107
|
+
return self.xl.Name
|
2108
|
+
else:
|
2109
|
+
return self.xl_obj.Name
|
2110
|
+
|
2111
|
+
@name.setter
|
2112
|
+
def name(self, value):
|
2113
|
+
if self.xl_obj is None:
|
2114
|
+
self.xl.Name = value
|
2115
|
+
else:
|
2116
|
+
self.xl_obj.Name = value
|
2117
|
+
|
2118
|
+
@property
|
2119
|
+
def parent(self):
|
2120
|
+
if self.xl_obj is None:
|
2121
|
+
return Book(xl=self.xl.Parent)
|
2122
|
+
else:
|
2123
|
+
return Sheet(xl=self.xl_obj.Parent)
|
2124
|
+
|
2125
|
+
def set_source_data(self, rng):
|
2126
|
+
self.xl.SetSourceData(rng.xl)
|
2127
|
+
|
2128
|
+
@property
|
2129
|
+
def chart_type(self):
|
2130
|
+
return chart_types_i2s[self.xl.ChartType]
|
2131
|
+
|
2132
|
+
@chart_type.setter
|
2133
|
+
def chart_type(self, chart_type):
|
2134
|
+
self.xl.ChartType = chart_types_s2i[chart_type]
|
2135
|
+
|
2136
|
+
@property
|
2137
|
+
def left(self):
|
2138
|
+
if self.xl_obj is None:
|
2139
|
+
raise Exception("This chart is not embedded.")
|
2140
|
+
return self.xl_obj.Left
|
2141
|
+
|
2142
|
+
@left.setter
|
2143
|
+
def left(self, value):
|
2144
|
+
if self.xl_obj is None:
|
2145
|
+
raise Exception("This chart is not embedded.")
|
2146
|
+
self.xl_obj.Left = value
|
2147
|
+
|
2148
|
+
@property
|
2149
|
+
def top(self):
|
2150
|
+
if self.xl_obj is None:
|
2151
|
+
raise Exception("This chart is not embedded.")
|
2152
|
+
return self.xl_obj.Top
|
2153
|
+
|
2154
|
+
@top.setter
|
2155
|
+
def top(self, value):
|
2156
|
+
if self.xl_obj is None:
|
2157
|
+
raise Exception("This chart is not embedded.")
|
2158
|
+
self.xl_obj.Top = value
|
2159
|
+
|
2160
|
+
@property
|
2161
|
+
def width(self):
|
2162
|
+
if self.xl_obj is None:
|
2163
|
+
raise Exception("This chart is not embedded.")
|
2164
|
+
return self.xl_obj.Width
|
2165
|
+
|
2166
|
+
@width.setter
|
2167
|
+
def width(self, value):
|
2168
|
+
if self.xl_obj is None:
|
2169
|
+
raise Exception("This chart is not embedded.")
|
2170
|
+
self.xl_obj.Width = value
|
2171
|
+
|
2172
|
+
@property
|
2173
|
+
def height(self):
|
2174
|
+
if self.xl_obj is None:
|
2175
|
+
raise Exception("This chart is not embedded.")
|
2176
|
+
return self.xl_obj.Height
|
2177
|
+
|
2178
|
+
@height.setter
|
2179
|
+
def height(self, value):
|
2180
|
+
if self.xl_obj is None:
|
2181
|
+
raise Exception("This chart is not embedded.")
|
2182
|
+
self.xl_obj.Height = value
|
2183
|
+
|
2184
|
+
def delete(self):
|
2185
|
+
# todo: what about chart sheets?
|
2186
|
+
self.xl_obj.Delete()
|
2187
|
+
|
2188
|
+
def to_png(self, path):
|
2189
|
+
self.xl.Export(path)
|
2190
|
+
|
2191
|
+
def to_pdf(self, path, quality):
|
2192
|
+
self.xl_obj.Select()
|
2193
|
+
self.xl.ExportAsFixedFormat(
|
2194
|
+
Type=FixedFormatType.xlTypePDF,
|
2195
|
+
Filename=path,
|
2196
|
+
Quality=quality_types[quality],
|
2197
|
+
IncludeDocProperties=True,
|
2198
|
+
IgnorePrintAreas=False,
|
2199
|
+
OpenAfterPublish=False,
|
2200
|
+
)
|
2201
|
+
try:
|
2202
|
+
self.parent.range("A1").select()
|
2203
|
+
except: # noqa: E722
|
2204
|
+
pass
|
2205
|
+
|
2206
|
+
|
2207
|
+
class Charts(Collection, base_classes.Charts):
|
2208
|
+
def _wrap(self, xl):
|
2209
|
+
return Chart(xl_obj=xl)
|
2210
|
+
|
2211
|
+
def add(self, left, top, width, height):
|
2212
|
+
return Chart(xl_obj=self.xl.Add(left, top, width, height))
|
2213
|
+
|
2214
|
+
|
2215
|
+
class Picture(base_classes.Picture):
|
2216
|
+
def __init__(self, xl):
|
2217
|
+
self.xl = xl
|
2218
|
+
|
2219
|
+
@property
|
2220
|
+
def api(self):
|
2221
|
+
return self.xl
|
2222
|
+
|
2223
|
+
@property
|
2224
|
+
def name(self):
|
2225
|
+
return self.xl.Name
|
2226
|
+
|
2227
|
+
@name.setter
|
2228
|
+
def name(self, value):
|
2229
|
+
self.xl.Name = value
|
2230
|
+
|
2231
|
+
@property
|
2232
|
+
def parent(self):
|
2233
|
+
return Sheet(xl=self.xl.Parent)
|
2234
|
+
|
2235
|
+
@property
|
2236
|
+
def left(self):
|
2237
|
+
return self.xl.Left
|
2238
|
+
|
2239
|
+
@left.setter
|
2240
|
+
def left(self, value):
|
2241
|
+
self.xl.Left = value
|
2242
|
+
|
2243
|
+
@property
|
2244
|
+
def top(self):
|
2245
|
+
return self.xl.Top
|
2246
|
+
|
2247
|
+
@top.setter
|
2248
|
+
def top(self, value):
|
2249
|
+
self.xl.Top = value
|
2250
|
+
|
2251
|
+
@property
|
2252
|
+
def width(self):
|
2253
|
+
return self.xl.Width
|
2254
|
+
|
2255
|
+
@width.setter
|
2256
|
+
def width(self, value):
|
2257
|
+
self.xl.Width = value
|
2258
|
+
|
2259
|
+
@property
|
2260
|
+
def height(self):
|
2261
|
+
return self.xl.Height
|
2262
|
+
|
2263
|
+
@height.setter
|
2264
|
+
def height(self, value):
|
2265
|
+
self.xl.Height = value
|
2266
|
+
|
2267
|
+
def delete(self):
|
2268
|
+
self.xl.Delete()
|
2269
|
+
|
2270
|
+
@property
|
2271
|
+
def lock_aspect_ratio(self):
|
2272
|
+
return self.xl.ShapeRange.LockAspectRatio
|
2273
|
+
|
2274
|
+
@lock_aspect_ratio.setter
|
2275
|
+
def lock_aspect_ratio(self, value):
|
2276
|
+
self.xl.ShapeRange.LockAspectRatio = value
|
2277
|
+
|
2278
|
+
def update(self, filename):
|
2279
|
+
return utils.excel_update_picture(self, filename)
|
2280
|
+
|
2281
|
+
|
2282
|
+
class Pictures(Collection, base_classes.Pictures):
|
2283
|
+
_wrap = Picture
|
2284
|
+
|
2285
|
+
@property
|
2286
|
+
def parent(self):
|
2287
|
+
return Sheet(xl=self.xl.Parent)
|
2288
|
+
|
2289
|
+
def add(
|
2290
|
+
self,
|
2291
|
+
filename,
|
2292
|
+
link_to_file,
|
2293
|
+
save_with_document,
|
2294
|
+
left,
|
2295
|
+
top,
|
2296
|
+
width,
|
2297
|
+
height,
|
2298
|
+
anchor,
|
2299
|
+
):
|
2300
|
+
if anchor:
|
2301
|
+
top, left = anchor.top, anchor.left
|
2302
|
+
else:
|
2303
|
+
top = top if top else 0
|
2304
|
+
left = left if left else 0
|
2305
|
+
|
2306
|
+
return Picture(
|
2307
|
+
xl=self.xl.Parent.Shapes.AddPicture(
|
2308
|
+
Filename=filename,
|
2309
|
+
LinkToFile=link_to_file,
|
2310
|
+
SaveWithDocument=save_with_document,
|
2311
|
+
Left=left,
|
2312
|
+
Top=top,
|
2313
|
+
Width=width,
|
2314
|
+
Height=height,
|
2315
|
+
).DrawingObject
|
2316
|
+
)
|
2317
|
+
|
2318
|
+
|
2319
|
+
class Names(base_classes.Names):
|
2320
|
+
def __init__(self, xl):
|
2321
|
+
self.xl = xl
|
2322
|
+
|
2323
|
+
@property
|
2324
|
+
def api(self):
|
2325
|
+
return self.xl
|
2326
|
+
|
2327
|
+
def __call__(self, name_or_index):
|
2328
|
+
return Name(xl=self.xl(name_or_index))
|
2329
|
+
|
2330
|
+
def contains(self, name_or_index):
|
2331
|
+
try:
|
2332
|
+
self.xl(name_or_index)
|
2333
|
+
except pywintypes.com_error as e:
|
2334
|
+
if e.hresult == -2147352567:
|
2335
|
+
return False
|
2336
|
+
else:
|
2337
|
+
raise
|
2338
|
+
return True
|
2339
|
+
|
2340
|
+
def __len__(self):
|
2341
|
+
return self.xl.Count
|
2342
|
+
|
2343
|
+
def add(self, name, refers_to):
|
2344
|
+
return Name(xl=self.xl.Add(name, refers_to))
|
2345
|
+
|
2346
|
+
|
2347
|
+
class Name(base_classes.Name):
|
2348
|
+
def __init__(self, xl):
|
2349
|
+
self.xl = xl
|
2350
|
+
|
2351
|
+
@property
|
2352
|
+
def api(self):
|
2353
|
+
return self.xl
|
2354
|
+
|
2355
|
+
def delete(self):
|
2356
|
+
self.xl.Delete()
|
2357
|
+
|
2358
|
+
@property
|
2359
|
+
def name(self):
|
2360
|
+
return self.xl.Name
|
2361
|
+
|
2362
|
+
@name.setter
|
2363
|
+
def name(self, value):
|
2364
|
+
self.xl.Name = value
|
2365
|
+
|
2366
|
+
@property
|
2367
|
+
def refers_to(self):
|
2368
|
+
return self.xl.RefersTo
|
2369
|
+
|
2370
|
+
@refers_to.setter
|
2371
|
+
def refers_to(self, value):
|
2372
|
+
self.xl.RefersTo = value
|
2373
|
+
|
2374
|
+
@property
|
2375
|
+
def refers_to_range(self):
|
2376
|
+
return Range(xl=self.xl.RefersToRange)
|
2377
|
+
|
2378
|
+
|
2379
|
+
# --- constants ---
|
2380
|
+
quality_types = {"minimum": 1, "standard": 0}
|
2381
|
+
|
2382
|
+
chart_types_s2i = {
|
2383
|
+
"3d_area": -4098,
|
2384
|
+
"3d_area_stacked": 78,
|
2385
|
+
"3d_area_stacked_100": 79,
|
2386
|
+
"3d_bar_clustered": 60,
|
2387
|
+
"3d_bar_stacked": 61,
|
2388
|
+
"3d_bar_stacked_100": 62,
|
2389
|
+
"3d_column": -4100,
|
2390
|
+
"3d_column_clustered": 54,
|
2391
|
+
"3d_column_stacked": 55,
|
2392
|
+
"3d_column_stacked_100": 56,
|
2393
|
+
"3d_line": -4101,
|
2394
|
+
"3d_pie": -4102,
|
2395
|
+
"3d_pie_exploded": 70,
|
2396
|
+
"area": 1,
|
2397
|
+
"area_stacked": 76,
|
2398
|
+
"area_stacked_100": 77,
|
2399
|
+
"bar_clustered": 57,
|
2400
|
+
"bar_of_pie": 71,
|
2401
|
+
"bar_stacked": 58,
|
2402
|
+
"bar_stacked_100": 59,
|
2403
|
+
"bubble": 15,
|
2404
|
+
"bubble_3d_effect": 87,
|
2405
|
+
"column_clustered": 51,
|
2406
|
+
"column_stacked": 52,
|
2407
|
+
"column_stacked_100": 53,
|
2408
|
+
"cone_bar_clustered": 102,
|
2409
|
+
"cone_bar_stacked": 103,
|
2410
|
+
"cone_bar_stacked_100": 104,
|
2411
|
+
"cone_col": 105,
|
2412
|
+
"cone_col_clustered": 99,
|
2413
|
+
"cone_col_stacked": 100,
|
2414
|
+
"cone_col_stacked_100": 101,
|
2415
|
+
"cylinder_bar_clustered": 95,
|
2416
|
+
"cylinder_bar_stacked": 96,
|
2417
|
+
"cylinder_bar_stacked_100": 97,
|
2418
|
+
"cylinder_col": 98,
|
2419
|
+
"cylinder_col_clustered": 92,
|
2420
|
+
"cylinder_col_stacked": 93,
|
2421
|
+
"cylinder_col_stacked_100": 94,
|
2422
|
+
"doughnut": -4120,
|
2423
|
+
"doughnut_exploded": 80,
|
2424
|
+
"line": 4,
|
2425
|
+
"line_markers": 65,
|
2426
|
+
"line_markers_stacked": 66,
|
2427
|
+
"line_markers_stacked_100": 67,
|
2428
|
+
"line_stacked": 63,
|
2429
|
+
"line_stacked_100": 64,
|
2430
|
+
"pie": 5,
|
2431
|
+
"pie_exploded": 69,
|
2432
|
+
"pie_of_pie": 68,
|
2433
|
+
"pyramid_bar_clustered": 109,
|
2434
|
+
"pyramid_bar_stacked": 110,
|
2435
|
+
"pyramid_bar_stacked_100": 111,
|
2436
|
+
"pyramid_col": 112,
|
2437
|
+
"pyramid_col_clustered": 106,
|
2438
|
+
"pyramid_col_stacked": 107,
|
2439
|
+
"pyramid_col_stacked_100": 108,
|
2440
|
+
"radar": -4151,
|
2441
|
+
"radar_filled": 82,
|
2442
|
+
"radar_markers": 81,
|
2443
|
+
"stock_hlc": 88,
|
2444
|
+
"stock_ohlc": 89,
|
2445
|
+
"stock_vhlc": 90,
|
2446
|
+
"stock_vohlc": 91,
|
2447
|
+
"surface": 83,
|
2448
|
+
"surface_top_view": 85,
|
2449
|
+
"surface_top_view_wireframe": 86,
|
2450
|
+
"surface_wireframe": 84,
|
2451
|
+
"xy_scatter": -4169,
|
2452
|
+
"xy_scatter_lines": 74,
|
2453
|
+
"xy_scatter_lines_no_markers": 75,
|
2454
|
+
"xy_scatter_smooth": 72,
|
2455
|
+
"xy_scatter_smooth_no_markers": 73,
|
2456
|
+
}
|
2457
|
+
|
2458
|
+
chart_types_i2s = {v: k for k, v in chart_types_s2i.items()}
|
2459
|
+
|
2460
|
+
directions_s2i = {
|
2461
|
+
"d": -4121,
|
2462
|
+
"down": -4121,
|
2463
|
+
"l": -4159,
|
2464
|
+
"left": -4159,
|
2465
|
+
"r": -4161,
|
2466
|
+
"right": -4161,
|
2467
|
+
"u": -4162,
|
2468
|
+
"up": -4162,
|
2469
|
+
}
|
2470
|
+
|
2471
|
+
directions_i2s = {-4121: "down", -4159: "left", -4161: "right", -4162: "up"}
|
2472
|
+
|
2473
|
+
calculation_s2i = {"automatic": -4105, "manual": -4135, "semiautomatic": 2}
|
2474
|
+
|
2475
|
+
calculation_i2s = {v: k for k, v in calculation_s2i.items()}
|
2476
|
+
|
2477
|
+
shape_types_s2i = {
|
2478
|
+
"3d_model": 30,
|
2479
|
+
"auto_shape": 1,
|
2480
|
+
"callout": 2,
|
2481
|
+
"canvas": 20,
|
2482
|
+
"chart": 3,
|
2483
|
+
"comment": 4,
|
2484
|
+
"content_app": 27,
|
2485
|
+
"diagram": 21,
|
2486
|
+
"embedded_ole_object": 7,
|
2487
|
+
"form_control": 8,
|
2488
|
+
"free_form": 5,
|
2489
|
+
"graphic": 28,
|
2490
|
+
"group": 6,
|
2491
|
+
"igx_graphic": 24,
|
2492
|
+
"ink": 22,
|
2493
|
+
"ink_comment": 23,
|
2494
|
+
"line": 9,
|
2495
|
+
"linked_3d_model": 31,
|
2496
|
+
"linked_graphic": 29,
|
2497
|
+
"linked_ole_object": 10,
|
2498
|
+
"linked_picture": 11,
|
2499
|
+
"media": 16,
|
2500
|
+
"ole_control_object": 12,
|
2501
|
+
"picture": 13,
|
2502
|
+
"placeholder": 14,
|
2503
|
+
"script_anchor": 18,
|
2504
|
+
"shape_type_mixed": -2,
|
2505
|
+
"slicer": 25,
|
2506
|
+
"table": 19,
|
2507
|
+
"text_box": 17,
|
2508
|
+
"text_effect": 15,
|
2509
|
+
"web_video": 26,
|
2510
|
+
}
|
2511
|
+
|
2512
|
+
scaling = {
|
2513
|
+
"scale_from_top_left": 0,
|
2514
|
+
"scale_from_bottom_right": 2,
|
2515
|
+
"scale_from_middle": 1,
|
2516
|
+
}
|
2517
|
+
|
2518
|
+
shape_types_i2s = {v: k for k, v in shape_types_s2i.items()}
|