tdrpa.tdworker 1.1.9.3__py38-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.
Files changed (97) hide show
  1. tdrpa/_tdxlwings/__init__.py +193 -0
  2. tdrpa/_tdxlwings/__pycache__/__init__.cpython-311.pyc +0 -0
  3. tdrpa/_tdxlwings/__pycache__/__init__.cpython-38.pyc +0 -0
  4. tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-311.pyc +0 -0
  5. tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-38.pyc +0 -0
  6. tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-311.pyc +0 -0
  7. tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-38.pyc +0 -0
  8. tdrpa/_tdxlwings/__pycache__/apps.cpython-311.pyc +0 -0
  9. tdrpa/_tdxlwings/__pycache__/apps.cpython-38.pyc +0 -0
  10. tdrpa/_tdxlwings/__pycache__/base_classes.cpython-311.pyc +0 -0
  11. tdrpa/_tdxlwings/__pycache__/base_classes.cpython-38.pyc +0 -0
  12. tdrpa/_tdxlwings/__pycache__/com_server.cpython-311.pyc +0 -0
  13. tdrpa/_tdxlwings/__pycache__/com_server.cpython-38.pyc +0 -0
  14. tdrpa/_tdxlwings/__pycache__/constants.cpython-311.pyc +0 -0
  15. tdrpa/_tdxlwings/__pycache__/constants.cpython-38.pyc +0 -0
  16. tdrpa/_tdxlwings/__pycache__/expansion.cpython-311.pyc +0 -0
  17. tdrpa/_tdxlwings/__pycache__/expansion.cpython-38.pyc +0 -0
  18. tdrpa/_tdxlwings/__pycache__/main.cpython-311.pyc +0 -0
  19. tdrpa/_tdxlwings/__pycache__/main.cpython-38.pyc +0 -0
  20. tdrpa/_tdxlwings/__pycache__/udfs.cpython-311.pyc +0 -0
  21. tdrpa/_tdxlwings/__pycache__/udfs.cpython-38.pyc +0 -0
  22. tdrpa/_tdxlwings/__pycache__/utils.cpython-311.pyc +0 -0
  23. tdrpa/_tdxlwings/__pycache__/utils.cpython-38.pyc +0 -0
  24. tdrpa/_tdxlwings/_win32patch.py +90 -0
  25. tdrpa/_tdxlwings/_xlmac.py +2240 -0
  26. tdrpa/_tdxlwings/_xlwindows.py +2518 -0
  27. tdrpa/_tdxlwings/addin/Dictionary.cls +474 -0
  28. tdrpa/_tdxlwings/addin/IWebAuthenticator.cls +71 -0
  29. tdrpa/_tdxlwings/addin/WebClient.cls +772 -0
  30. tdrpa/_tdxlwings/addin/WebHelpers.bas +3203 -0
  31. tdrpa/_tdxlwings/addin/WebRequest.cls +875 -0
  32. tdrpa/_tdxlwings/addin/WebResponse.cls +453 -0
  33. tdrpa/_tdxlwings/addin/xlwings.xlam +0 -0
  34. tdrpa/_tdxlwings/apps.py +35 -0
  35. tdrpa/_tdxlwings/base_classes.py +1092 -0
  36. tdrpa/_tdxlwings/cli.py +1306 -0
  37. tdrpa/_tdxlwings/com_server.py +385 -0
  38. tdrpa/_tdxlwings/constants.py +3080 -0
  39. tdrpa/_tdxlwings/conversion/__init__.py +103 -0
  40. tdrpa/_tdxlwings/conversion/framework.py +147 -0
  41. tdrpa/_tdxlwings/conversion/numpy_conv.py +34 -0
  42. tdrpa/_tdxlwings/conversion/pandas_conv.py +184 -0
  43. tdrpa/_tdxlwings/conversion/standard.py +321 -0
  44. tdrpa/_tdxlwings/expansion.py +83 -0
  45. tdrpa/_tdxlwings/ext/__init__.py +3 -0
  46. tdrpa/_tdxlwings/ext/sql.py +73 -0
  47. tdrpa/_tdxlwings/html/xlwings-alert.html +71 -0
  48. tdrpa/_tdxlwings/js/xlwings.js +577 -0
  49. tdrpa/_tdxlwings/js/xlwings.ts +729 -0
  50. tdrpa/_tdxlwings/mac_dict.py +6399 -0
  51. tdrpa/_tdxlwings/main.py +5205 -0
  52. tdrpa/_tdxlwings/mistune/__init__.py +63 -0
  53. tdrpa/_tdxlwings/mistune/block_parser.py +366 -0
  54. tdrpa/_tdxlwings/mistune/inline_parser.py +216 -0
  55. tdrpa/_tdxlwings/mistune/markdown.py +84 -0
  56. tdrpa/_tdxlwings/mistune/renderers.py +220 -0
  57. tdrpa/_tdxlwings/mistune/scanner.py +121 -0
  58. tdrpa/_tdxlwings/mistune/util.py +41 -0
  59. tdrpa/_tdxlwings/pro/__init__.py +40 -0
  60. tdrpa/_tdxlwings/pro/_xlcalamine.py +536 -0
  61. tdrpa/_tdxlwings/pro/_xlofficejs.py +146 -0
  62. tdrpa/_tdxlwings/pro/_xlremote.py +1293 -0
  63. tdrpa/_tdxlwings/pro/custom_functions_code.js +150 -0
  64. tdrpa/_tdxlwings/pro/embedded_code.py +60 -0
  65. tdrpa/_tdxlwings/pro/udfs_officejs.py +549 -0
  66. tdrpa/_tdxlwings/pro/utils.py +199 -0
  67. tdrpa/_tdxlwings/quickstart.xlsm +0 -0
  68. tdrpa/_tdxlwings/quickstart_addin.xlam +0 -0
  69. tdrpa/_tdxlwings/quickstart_addin_ribbon.xlam +0 -0
  70. tdrpa/_tdxlwings/quickstart_fastapi/main.py +47 -0
  71. tdrpa/_tdxlwings/quickstart_fastapi/requirements.txt +3 -0
  72. tdrpa/_tdxlwings/quickstart_standalone.xlsm +0 -0
  73. tdrpa/_tdxlwings/reports.py +12 -0
  74. tdrpa/_tdxlwings/rest/__init__.py +1 -0
  75. tdrpa/_tdxlwings/rest/api.py +368 -0
  76. tdrpa/_tdxlwings/rest/serializers.py +103 -0
  77. tdrpa/_tdxlwings/server.py +14 -0
  78. tdrpa/_tdxlwings/udfs.py +775 -0
  79. tdrpa/_tdxlwings/utils.py +777 -0
  80. tdrpa/_tdxlwings/xlwings-0.31.6.applescript +30 -0
  81. tdrpa/_tdxlwings/xlwings.bas +2061 -0
  82. tdrpa/_tdxlwings/xlwings_custom_addin.bas +2042 -0
  83. tdrpa/_tdxlwings/xlwingslib.cp38-win_amd64.pyd +0 -0
  84. tdrpa/tdworker/__init__.pyi +8 -0
  85. tdrpa/tdworker/_excel.pyi +703 -0
  86. tdrpa/tdworker/_img.pyi +173 -0
  87. tdrpa/tdworker/_os.pyi +46 -0
  88. tdrpa/tdworker/_w.pyi +129 -0
  89. tdrpa/tdworker/_web.pyi +248 -0
  90. tdrpa/tdworker/_winE.pyi +246 -0
  91. tdrpa/tdworker/_winK.pyi +74 -0
  92. tdrpa/tdworker/_winM.pyi +117 -0
  93. tdrpa/tdworker.cp38-win_amd64.pyd +0 -0
  94. tdrpa.tdworker-1.1.9.3.dist-info/METADATA +25 -0
  95. tdrpa.tdworker-1.1.9.3.dist-info/RECORD +97 -0
  96. tdrpa.tdworker-1.1.9.3.dist-info/WHEEL +5 -0
  97. tdrpa.tdworker-1.1.9.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1293 @@
1
+ """
2
+ Required Notice: Copyright (C) Zoomer Analytics GmbH.
3
+
4
+ xlwings PRO is dual-licensed under one of the following licenses:
5
+
6
+ * PolyForm Noncommercial License 1.0.0 (for noncommercial use):
7
+ https://polyformproject.org/licenses/noncommercial/1.0.0
8
+ * xlwings PRO License (for commercial use):
9
+ https://github.com/xlwings/xlwings/blob/main/LICENSE_PRO.txt
10
+
11
+ Commercial licenses can be purchased at https://www.xlwings.org
12
+ """
13
+
14
+ import base64
15
+ import datetime as dt
16
+ import numbers
17
+ import re
18
+ from functools import lru_cache
19
+
20
+ try:
21
+ import numpy as np
22
+ except ImportError:
23
+ np = None
24
+ try:
25
+ import pandas as pd
26
+ except ImportError:
27
+ pd = None
28
+
29
+ from .. import NoSuchObjectError, XlwingsError, __version__, base_classes, utils
30
+
31
+ # Time types (doesn't contain dt.date)
32
+ time_types = (dt.datetime,)
33
+ if np:
34
+ time_types = time_types + (np.datetime64,)
35
+
36
+ datetime_pattern = r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa: E501
37
+ datetime_regex = re.compile(datetime_pattern)
38
+
39
+
40
+ def _clean_value_data_element(
41
+ value, datetime_builder, empty_as, number_builder, err_to_str
42
+ ):
43
+ if value == "":
44
+ return empty_as
45
+ if isinstance(value, str):
46
+ # TODO: Send arrays back and forth with indices of the location of dt values
47
+ if datetime_regex.match(value):
48
+ value = dt.datetime.fromisoformat(
49
+ value[:-1]
50
+ ) # cut off "Z" (Python doesn't accept it and Excel doesn't support tz)
51
+ elif not err_to_str and value in [
52
+ "#DIV/0!",
53
+ "#N/A",
54
+ "#NAME?",
55
+ "#NULL!",
56
+ "#NUM!",
57
+ "#REF!",
58
+ "#VALUE!",
59
+ "#DATA!",
60
+ ]:
61
+ value = None
62
+ else:
63
+ value = value
64
+ if isinstance(value, dt.datetime) and datetime_builder is not dt.datetime:
65
+ value = datetime_builder(
66
+ month=value.month,
67
+ day=value.day,
68
+ year=value.year,
69
+ hour=value.hour,
70
+ minute=value.minute,
71
+ second=value.second,
72
+ microsecond=value.microsecond,
73
+ tzinfo=None,
74
+ )
75
+ elif number_builder is not None and isinstance(value, float):
76
+ value = number_builder(value)
77
+ return value
78
+
79
+
80
+ class Engine:
81
+ def __init__(self):
82
+ self.apps = Apps()
83
+
84
+ @staticmethod
85
+ def clean_value_data(data, datetime_builder, empty_as, number_builder, err_to_str):
86
+ return [
87
+ [
88
+ _clean_value_data_element(
89
+ c, datetime_builder, empty_as, number_builder, err_to_str
90
+ )
91
+ for c in row
92
+ ]
93
+ for row in data
94
+ ]
95
+
96
+ @staticmethod
97
+ def prepare_xl_data_element(x, options):
98
+ if x is None:
99
+ return ""
100
+ elif pd and pd.isna(x):
101
+ return ""
102
+ elif np and isinstance(x, (np.floating, float)) and np.isnan(x):
103
+ return ""
104
+ elif np and isinstance(x, np.number):
105
+ return float(x)
106
+ elif np and isinstance(x, np.datetime64):
107
+ return utils.np_datetime_to_datetime(x).replace(tzinfo=None).isoformat()
108
+ elif pd and isinstance(x, pd.Timestamp):
109
+ return x.to_pydatetime().replace(tzinfo=None).isoformat()
110
+ elif pd and isinstance(x, type(pd.NaT)):
111
+ return None
112
+ elif isinstance(x, time_types):
113
+ x = x.replace(tzinfo=None).isoformat()
114
+ elif isinstance(x, dt.date):
115
+ # JS applies tz conversion with "2021-01-01" when calling
116
+ # toLocaleDateString() while it leaves "2021-01-01T00:00:00" unchanged
117
+ x = dt.datetime(x.year, x.month, x.day).isoformat()
118
+ return x
119
+
120
+ @property
121
+ def name(self):
122
+ return "remote"
123
+
124
+ @property
125
+ def type(self):
126
+ return "remote"
127
+
128
+
129
+ class Apps(base_classes.Apps):
130
+ def __init__(self):
131
+ self._apps = [App(self)]
132
+
133
+ def __iter__(self):
134
+ return iter(self._apps)
135
+
136
+ def __len__(self):
137
+ return len(self._apps)
138
+
139
+ def __getitem__(self, index):
140
+ return self._apps[index]
141
+
142
+ def add(self, **kwargs):
143
+ self._apps.insert(0, App(self, **kwargs))
144
+ return self._apps[0]
145
+
146
+
147
+ class App(base_classes.App):
148
+ _next_pid = -1
149
+
150
+ def __init__(self, apps, add_book=True, **kwargs):
151
+ self.apps = apps
152
+ self._pid = App._next_pid
153
+ App._next_pid -= 1
154
+ self._books = Books(self)
155
+ if add_book:
156
+ self._books.add()
157
+
158
+ def kill(self):
159
+ self.apps._apps.remove(self)
160
+ self.apps = None
161
+
162
+ @property
163
+ def engine(self):
164
+ return engine
165
+
166
+ @property
167
+ def books(self):
168
+ return self._books
169
+
170
+ @property
171
+ def pid(self):
172
+ return self._pid
173
+
174
+ @property
175
+ def selection(self):
176
+ book = self.books.active
177
+ return Range(sheet=book.sheets.active, arg1=book.api["book"]["selection"])
178
+
179
+ @property
180
+ def visible(self):
181
+ return True
182
+
183
+ @visible.setter
184
+ def visible(self, value):
185
+ pass
186
+
187
+ def activate(self, steal_focus=None):
188
+ pass
189
+
190
+ def alert(self, prompt, title, buttons, mode, callback):
191
+ self.books.active.append_json_action(
192
+ func="alert",
193
+ args=[
194
+ "" if prompt is None else prompt,
195
+ "" if title is None else title,
196
+ "" if buttons is None else buttons,
197
+ "" if mode is None else mode,
198
+ "" if callback is None else callback,
199
+ ],
200
+ )
201
+
202
+ def run(self, macro, args):
203
+ self.books.active.append_json_action(
204
+ func="runMacro",
205
+ args=[macro] + [args] if not isinstance(args, list) else [macro] + args,
206
+ )
207
+
208
+
209
+ class Books(base_classes.Books):
210
+ def __init__(self, app):
211
+ self.app = app
212
+ self.books = []
213
+ self._active = None
214
+
215
+ @property
216
+ def active(self):
217
+ return self._active
218
+
219
+ def open(self, json):
220
+ book = Book(api=json, books=self)
221
+ self.books.append(book)
222
+ self._active = book
223
+ return book
224
+
225
+ def add(self):
226
+ book = Book(
227
+ api={
228
+ "version": __version__,
229
+ "book": {"name": f"Book{len(self) + 1}", "active_sheet_index": 0},
230
+ "sheets": [
231
+ {
232
+ "name": "Sheet1",
233
+ "values": [[]],
234
+ },
235
+ ],
236
+ },
237
+ books=self,
238
+ )
239
+ self.books.append(book)
240
+ self._active = book
241
+ return book
242
+
243
+ def _try_find_book_by_name(self, name):
244
+ for book in self.books:
245
+ if book.name == name or book.fullname == name:
246
+ return book
247
+ return None
248
+
249
+ def __len__(self):
250
+ return len(self.books)
251
+
252
+ def __iter__(self):
253
+ for book in self.books:
254
+ yield book
255
+
256
+ def __call__(self, name_or_index):
257
+ if isinstance(name_or_index, numbers.Number):
258
+ return self.books[name_or_index - 1]
259
+ else:
260
+ book = self._try_find_book_by_name(name_or_index)
261
+ if book is None:
262
+ raise KeyError(name_or_index)
263
+ return book
264
+
265
+
266
+ class Book(base_classes.Book):
267
+ def __init__(self, api, books):
268
+ self.books = books
269
+ self._api = api
270
+ self._json = {"actions": []}
271
+ if api["version"] != __version__:
272
+ raise XlwingsError(
273
+ f'Your xlwings version is different on the client ({api["version"]}) '
274
+ f"and server ({__version__})."
275
+ )
276
+
277
+ def append_json_action(self, **kwargs):
278
+ args = kwargs.get("args")
279
+ self._json["actions"].append(
280
+ {
281
+ "func": kwargs.get("func"),
282
+ "args": [args] if not isinstance(args, list) else args,
283
+ "values": kwargs.get("values"),
284
+ "sheet_position": kwargs.get("sheet_position"),
285
+ "start_row": kwargs.get("start_row"),
286
+ "start_column": kwargs.get("start_column"),
287
+ "row_count": kwargs.get("row_count"),
288
+ "column_count": kwargs.get("column_count"),
289
+ }
290
+ )
291
+
292
+ @property
293
+ def api(self):
294
+ return self._api
295
+
296
+ def json(self):
297
+ return self._json
298
+
299
+ @property
300
+ def name(self):
301
+ return self.api["book"]["name"]
302
+
303
+ @property
304
+ def fullname(self):
305
+ return self.name
306
+
307
+ @property
308
+ def names(self):
309
+ return Names(parent=self, api=self.api["names"])
310
+
311
+ @property
312
+ def sheets(self):
313
+ return Sheets(api=self.api["sheets"], book=self)
314
+
315
+ @property
316
+ def app(self):
317
+ return self.books.app
318
+
319
+ def close(self):
320
+ assert self.api is not None, "Seems this book was already closed."
321
+ self.books.books.remove(self)
322
+ self.books = None
323
+ self._api = None
324
+
325
+ def activate(self):
326
+ pass
327
+
328
+
329
+ class Sheets(base_classes.Sheets):
330
+ def __init__(self, api, book):
331
+ self._api = api
332
+ self.book = book
333
+
334
+ @property
335
+ def active(self):
336
+ ix = self.book.api["book"]["active_sheet_index"]
337
+ return Sheet(api=self.api[ix], sheets=self, index=ix + 1)
338
+
339
+ @property
340
+ def api(self):
341
+ return self._api
342
+
343
+ def __call__(self, name_or_index):
344
+ if isinstance(name_or_index, int):
345
+ return Sheet(
346
+ api=self.api[name_or_index - 1], sheets=self, index=name_or_index
347
+ )
348
+ else:
349
+ for ix, sheet in enumerate(self.api):
350
+ if sheet["name"] == name_or_index:
351
+ return Sheet(api=sheet, sheets=self, index=ix + 1)
352
+ raise ValueError(f"Sheet '{name_or_index}' doesn't exist!")
353
+
354
+ def add(self, before=None, after=None, name=None):
355
+ # Default naming logic is different from Desktop apps!
356
+ sheet_number = 1
357
+ while True:
358
+ if f"Sheet{sheet_number}" in [sheet.name for sheet in self]:
359
+ sheet_number += 1
360
+ else:
361
+ break
362
+ api = {
363
+ "name": f"Sheet{sheet_number}",
364
+ "values": [[]],
365
+ "pictures": [],
366
+ "tables": [],
367
+ }
368
+ if before:
369
+ if before.index == 1:
370
+ ix = 1
371
+ else:
372
+ ix = before.index - 1
373
+ elif after:
374
+ ix = after.index + 1
375
+ else:
376
+ # Default position is different from Desktop apps!
377
+ ix = len(self) + 1
378
+ self.api.insert(ix - 1, api)
379
+ self.book.append_json_action(func="addSheet", args=[ix - 1, name])
380
+ self.book.api["book"]["active_sheet_index"] = ix - 1
381
+
382
+ return Sheet(api=api, sheets=self, index=ix)
383
+
384
+ def __len__(self):
385
+ return len(self.api)
386
+
387
+ def __iter__(self):
388
+ for ix, sheet in enumerate(self.api):
389
+ yield Sheet(api=sheet, sheets=self, index=ix + 1)
390
+
391
+
392
+ class Sheet(base_classes.Sheet):
393
+ def __init__(self, api, sheets, index):
394
+ self._api = api
395
+ self._index = index
396
+ self.sheets = sheets
397
+
398
+ def append_json_action(self, **kwargs):
399
+ self.book.append_json_action(
400
+ **{
401
+ **kwargs,
402
+ **{
403
+ "sheet_position": self.index - 1,
404
+ },
405
+ }
406
+ )
407
+
408
+ @property
409
+ def api(self):
410
+ return self._api
411
+
412
+ @property
413
+ def name(self):
414
+ return self.api["name"]
415
+
416
+ @name.setter
417
+ def name(self, value):
418
+ self.append_json_action(
419
+ func="setSheetName",
420
+ args=value,
421
+ )
422
+ self.api["name"] = value
423
+
424
+ @property
425
+ def index(self):
426
+ return self._index
427
+
428
+ @property
429
+ def book(self):
430
+ return self.sheets.book
431
+
432
+ def range(self, arg1, arg2=None):
433
+ return Range(sheet=self, arg1=arg1, arg2=arg2)
434
+
435
+ @property
436
+ def cells(self):
437
+ return Range(
438
+ sheet=self,
439
+ arg1=(1, 1),
440
+ arg2=(1_048_576, 16_384),
441
+ )
442
+
443
+ @property
444
+ def names(self):
445
+ api = [
446
+ name
447
+ for name in self.book.api["names"]
448
+ if name["scope_sheet_index"] is not None
449
+ and name["scope_sheet_index"] + 1 == self.index
450
+ and not name["book_scope"]
451
+ ]
452
+ return Names(parent=self, api=api)
453
+
454
+ def activate(self):
455
+ ix = self.index - 1
456
+ self.book.api["book"]["active_sheet_index"] = ix
457
+ self.append_json_action(func="activateSheet", args=ix)
458
+
459
+ @property
460
+ def pictures(self):
461
+ return Pictures(self)
462
+
463
+ @property
464
+ def tables(self):
465
+ return Tables(parent=self)
466
+
467
+ def delete(self):
468
+ del self.book.api["sheets"][self.index - 1]
469
+ self.append_json_action(func="sheetDelete")
470
+
471
+ def clear(self):
472
+ self.append_json_action(func="sheetClear")
473
+
474
+ def clear_contents(self):
475
+ self.append_json_action(func="sheetClearContents")
476
+
477
+ def clear_formats(self):
478
+ self.append_json_action(func="sheetClearFormats")
479
+
480
+
481
+ @lru_cache(None)
482
+ def get_range_api(api_values, arg1, arg2=None):
483
+ # Keeping this outside of the Range class allows us to cache it across multiple
484
+ # instances of the same range
485
+ if arg2:
486
+ values = [
487
+ row[arg1[1] - 1 : arg2[1]] for row in api_values[arg1[0] - 1 : arg2[0]]
488
+ ]
489
+ if not values:
490
+ # Completely outside the used range
491
+ return [(None,) * (arg2[1] + 1 - arg1[1])] * (arg2[0] + 1 - arg1[0])
492
+ else:
493
+ # Partly outside the used range
494
+ nrows = arg2[0] + 1 - arg1[0]
495
+ ncols = arg2[1] + 1 - arg1[1]
496
+ nrows_actual = len(values)
497
+ ncols_actual = len(values[0])
498
+ delta_rows = nrows - nrows_actual
499
+ delta_cols = ncols - ncols_actual
500
+ if delta_rows != 0:
501
+ values.extend([(None,) * ncols_actual] * delta_rows)
502
+ if delta_cols != 0:
503
+ v = []
504
+ for row in values:
505
+ v.append(row + (None,) * delta_cols)
506
+ values = v
507
+ return values
508
+ else:
509
+ try:
510
+ values = [(api_values[arg1[0] - 1][arg1[1] - 1],)]
511
+ return values
512
+ except IndexError:
513
+ # Outside the used range
514
+ return [(None,)]
515
+
516
+
517
+ class Range(base_classes.Range):
518
+ def __init__(self, sheet, arg1, arg2=None):
519
+ self.sheet = sheet
520
+
521
+ # Range
522
+ if isinstance(arg1, Range) and isinstance(arg2, Range):
523
+ cell1 = arg1.coords[1], arg1.coords[2]
524
+ cell2 = arg2.coords[1], arg2.coords[2]
525
+ arg1 = min(cell1[0], cell2[0]), min(cell1[1], cell2[1])
526
+ arg2 = max(cell1[0], cell2[0]), max(cell1[1], cell2[1])
527
+ # A1 notation
528
+ if isinstance(arg1, str):
529
+ # A1 notation
530
+ tuple1, tuple2 = utils.a1_to_tuples(arg1)
531
+ if not tuple1:
532
+ # Named range
533
+ for api_name in sheet.book.api["names"]:
534
+ if (
535
+ api_name["name"].split("!")[-1] == arg1
536
+ and api_name["sheet_index"] == sheet.index - 1
537
+ ):
538
+ tuple1, tuple2 = utils.a1_to_tuples(api_name["address"])
539
+ break
540
+ if not tuple1:
541
+ # Tables
542
+ for api_table in sheet.api["tables"]:
543
+ if api_table["name"] == arg1:
544
+ tuple1, tuple2 = utils.a1_to_tuples(
545
+ api_table["data_body_range_address"]
546
+ )
547
+ break
548
+ if not tuple1:
549
+ raise NoSuchObjectError(
550
+ f"The address/named range '{arg1}' doesn't exist."
551
+ )
552
+ arg1, arg2 = tuple1, tuple2
553
+
554
+ # Coordinates
555
+ if len(arg1) == 4:
556
+ row, col, nrows, ncols = arg1
557
+ arg1 = (row, col)
558
+ if nrows > 1 or ncols > 1:
559
+ arg2 = (row + nrows - 1, col + ncols - 1)
560
+
561
+ self.arg1 = arg1 # 1-based tuple
562
+ self.arg2 = arg2 # 1-based tuple
563
+ self.sheet = sheet
564
+
565
+ def append_json_action(self, **kwargs):
566
+ nrows, ncols = self.shape
567
+ self.sheet.book.append_json_action(
568
+ **{
569
+ **kwargs,
570
+ **{
571
+ "sheet_position": self.sheet.index - 1,
572
+ "start_row": self.row - 1,
573
+ "start_column": self.column - 1,
574
+ "row_count": nrows,
575
+ "column_count": ncols,
576
+ },
577
+ }
578
+ )
579
+
580
+ @property
581
+ def api(self):
582
+ return get_range_api(
583
+ tuple(tuple(row) for row in self.sheet.api["values"]), self.arg1, self.arg2
584
+ )
585
+
586
+ @property
587
+ def coords(self):
588
+ return self.sheet.name, self.row, self.column, len(self.api), len(self.api[0])
589
+
590
+ @property
591
+ def row(self):
592
+ return self.arg1[0]
593
+
594
+ @property
595
+ def column(self):
596
+ return self.arg1[1]
597
+
598
+ @property
599
+ def shape(self):
600
+ if self.arg2:
601
+ return self.arg2[0] - self.arg1[0] + 1, self.arg2[1] - self.arg1[1] + 1
602
+ else:
603
+ return 1, 1
604
+
605
+ @property
606
+ def raw_value(self):
607
+ return self.api
608
+
609
+ @raw_value.setter
610
+ def raw_value(self, value):
611
+ if not isinstance(value, list):
612
+ # Covers also this case: myrange['A1:B2'].value = 'xyz'
613
+ nrows, ncols = self.shape
614
+ values = [[value] * ncols] * nrows
615
+ else:
616
+ values = value
617
+ self.append_json_action(
618
+ func="setValues",
619
+ values=values,
620
+ )
621
+
622
+ def clear_contents(self):
623
+ self.append_json_action(
624
+ func="rangeClearContents",
625
+ )
626
+
627
+ def clear(self):
628
+ self.append_json_action(
629
+ func="rangeClear",
630
+ )
631
+
632
+ def clear_formats(self):
633
+ self.append_json_action(
634
+ func="rangeClearFormats",
635
+ )
636
+
637
+ @property
638
+ def address(self):
639
+ nrows, ncols = self.shape
640
+ address = f"${utils.col_name(self.column)}${self.row}"
641
+ if nrows == 1 and ncols == 1:
642
+ return address
643
+ else:
644
+ return (
645
+ f"{address}"
646
+ f":${utils.col_name(self.column + ncols - 1)}${self.row + nrows - 1}"
647
+ )
648
+
649
+ @property
650
+ def has_array(self):
651
+ # Not supported, but since this is only used for legacy CSE arrays, probably
652
+ # not much of an issue. Here as there's currently a dependency in expansion.py.
653
+ return None
654
+
655
+ def end(self, direction):
656
+ if direction == "down":
657
+ i = 1
658
+ while True:
659
+ try:
660
+ if self.sheet.api["values"][self.row - 1 + i][self.column - 1]:
661
+ i += 1
662
+ else:
663
+ break
664
+ except IndexError:
665
+ break # outside used range
666
+ nrows = i - 1
667
+ return self.sheet.range((self.row + nrows, self.column))
668
+ if direction == "up":
669
+ i = -1
670
+ while True:
671
+ row_ix = self.row - 1 + i
672
+ if row_ix >= 0 and self.sheet.api["values"][row_ix][self.column - 1]:
673
+ i -= 1
674
+ else:
675
+ break
676
+ nrows = i + 1
677
+ return self.sheet.range((self.row + nrows, self.column))
678
+ if direction == "right":
679
+ i = 1
680
+ while True:
681
+ try:
682
+ if self.sheet.api["values"][self.row - 1][self.column - 1 + i]:
683
+ i += 1
684
+ else:
685
+ break
686
+ except IndexError:
687
+ break # outside used range
688
+ ncols = i - 1
689
+ return self.sheet.range((self.row, self.column + ncols))
690
+ if direction == "left":
691
+ i = -1
692
+ while True:
693
+ col_ix = self.column - 1 + i
694
+ if col_ix >= 0 and self.sheet.api["values"][self.row - 1][col_ix]:
695
+ i -= 1
696
+ else:
697
+ break
698
+ ncols = i + 1
699
+ return self.sheet.range((self.row, self.column + ncols))
700
+
701
+ def autofit(self, axis=None):
702
+ if axis == "rows" or axis == "r":
703
+ self.append_json_action(func="setAutofit", args="rows")
704
+ elif axis == "columns" or axis == "c":
705
+ self.append_json_action(func="setAutofit", args="columns")
706
+ elif axis is None:
707
+ self.append_json_action(func="setAutofit", args="rows")
708
+ self.append_json_action(func="setAutofit", args="columns")
709
+
710
+ @property
711
+ def color(self):
712
+ raise NotImplementedError()
713
+
714
+ @color.setter
715
+ def color(self, value):
716
+ if not isinstance(value, str):
717
+ raise ValueError('Color must be supplied in hex format e.g., "#FFA500".')
718
+ self.append_json_action(func="setRangeColor", args=value)
719
+
720
+ def add_hyperlink(self, address, text_to_display=None, screen_tip=None):
721
+ self.append_json_action(
722
+ func="addHyperlink", args=[address, text_to_display, screen_tip]
723
+ )
724
+
725
+ @property
726
+ def number_format(self):
727
+ raise NotImplementedError()
728
+
729
+ @number_format.setter
730
+ def number_format(self, value):
731
+ self.append_json_action(func="setNumberFormat", args=value)
732
+
733
+ @property
734
+ def name(self):
735
+ for name in self.sheet.book.api["names"]:
736
+ if name["sheet_index"] == self.sheet.index - 1 and name[
737
+ "address"
738
+ ] == self.address.replace("$", ""):
739
+ return Name(
740
+ parent=self.sheet.book if name["book_scope"] else self.sheet,
741
+ api=name,
742
+ )
743
+
744
+ @name.setter
745
+ def name(self, value):
746
+ self.append_json_action(
747
+ func="setRangeName",
748
+ args=value,
749
+ )
750
+
751
+ def copy(self, destination=None):
752
+ # TODO: introduce the new copy_from from Office Scripts
753
+ if destination is None:
754
+ raise XlwingsError("range.copy() requires a destination argument.")
755
+ self.append_json_action(
756
+ func="copyRange",
757
+ args=[destination.sheet.index - 1, destination.address],
758
+ )
759
+
760
+ def delete(self, shift=None):
761
+ if shift not in ("up", "left"):
762
+ # Non-remote version allows shift=None
763
+ raise XlwingsError(
764
+ "range.delete() requires either 'up' or 'left' as shift arguments."
765
+ )
766
+ self.append_json_action(func="rangeDelete", args=shift)
767
+
768
+ def insert(self, shift=None, copy_origin=None):
769
+ if shift not in ("down", "right"):
770
+ raise XlwingsError(
771
+ "range.insert() requires either 'down' or 'right' as shift arguments."
772
+ )
773
+ if copy_origin not in (
774
+ "format_from_left_or_above",
775
+ "format_from_right_or_below",
776
+ ):
777
+ raise XlwingsError(
778
+ "range.insert() requires either 'format_from_left_or_above' or "
779
+ "'format_from_right_or_below' as copy_origin arguments."
780
+ )
781
+ # copy_origin is only supported by VBA clients
782
+ self.append_json_action(func="rangeInsert", args=[shift, copy_origin])
783
+
784
+ def select(self):
785
+ self.append_json_action(
786
+ func="rangeSelect",
787
+ )
788
+
789
+ def __len__(self):
790
+ nrows, ncols = self.shape
791
+ return nrows * ncols
792
+
793
+ def __call__(self, arg1, arg2=None):
794
+ if arg2 is None:
795
+ col = (arg1 - 1) % self.shape[1]
796
+ row = int((arg1 - 1 - col) / self.shape[1])
797
+ return self(row + 1, col + 1)
798
+ else:
799
+ return Range(
800
+ sheet=self.sheet,
801
+ arg1=(self.row + arg1 - 1, self.column + arg2 - 1),
802
+ )
803
+
804
+
805
+ class Collection(base_classes.Collection):
806
+ def __init__(self, parent):
807
+ self._parent = parent
808
+ self._api = parent.api[self._attr]
809
+
810
+ @property
811
+ def api(self):
812
+ return self._api
813
+
814
+ @property
815
+ def parent(self):
816
+ return self._parent
817
+
818
+ def __call__(self, key):
819
+ if isinstance(key, numbers.Number):
820
+ if key > len(self):
821
+ raise KeyError(key)
822
+ else:
823
+ return self._wrap(self.parent, key)
824
+ else:
825
+ for ix, i in enumerate(self.api):
826
+ if i["name"] == key:
827
+ return self._wrap(self.parent, ix + 1)
828
+ raise KeyError(key)
829
+
830
+ def __len__(self):
831
+ return len(self.api)
832
+
833
+ def __iter__(self):
834
+ for ix, api in enumerate(self.api):
835
+ yield self._wrap(self._parent, ix + 1)
836
+
837
+ def __contains__(self, key):
838
+ if isinstance(key, numbers.Number):
839
+ return 1 <= key <= len(self)
840
+ else:
841
+ for i in self.api:
842
+ if i["name"] == key:
843
+ return True
844
+ return False
845
+
846
+
847
+ class Picture(base_classes.Picture):
848
+ def __init__(self, parent, key):
849
+ self._parent = parent
850
+ self._api = self.parent.api["pictures"][key - 1]
851
+ self.key = key
852
+
853
+ def append_json_action(self, **kwargs):
854
+ self.parent.book.append_json_action(
855
+ **{
856
+ **kwargs,
857
+ **{
858
+ "sheet_position": self.parent.index - 1,
859
+ },
860
+ }
861
+ )
862
+
863
+ @property
864
+ def api(self):
865
+ return self._api
866
+
867
+ @property
868
+ def parent(self):
869
+ return self._parent
870
+
871
+ @property
872
+ def name(self):
873
+ return self.api["name"]
874
+
875
+ @name.setter
876
+ def name(self, value):
877
+ self.api["name"] = value
878
+ self.append_json_action(func="setPictureName", args=[self.index - 1, value])
879
+
880
+ @property
881
+ def width(self):
882
+ return self.api["width"]
883
+
884
+ @width.setter
885
+ def width(self, value):
886
+ self.append_json_action(func="setPictureWidth", args=[self.index - 1, value])
887
+
888
+ @property
889
+ def height(self):
890
+ return self.api["height"]
891
+
892
+ @height.setter
893
+ def height(self, value):
894
+ self.append_json_action(func="setPictureHeight", args=[self.index - 1, value])
895
+
896
+ @property
897
+ def index(self):
898
+ # TODO: make available in public API
899
+ if isinstance(self.key, numbers.Number):
900
+ return self.key
901
+ else:
902
+ for ix, obj in self.api:
903
+ if obj["name"] == self.key:
904
+ return ix + 1
905
+ raise KeyError(self.key)
906
+
907
+ def delete(self):
908
+ self.parent._api["pictures"].pop(self.index - 1)
909
+ self.append_json_action(func="deletePicture", args=self.index - 1)
910
+
911
+ def update(self, filename):
912
+ with open(filename, "rb") as image_file:
913
+ encoded_image_string = base64.b64encode(image_file.read()).decode("utf-8")
914
+ self.append_json_action(
915
+ func="updatePicture",
916
+ args=[
917
+ encoded_image_string,
918
+ self.index - 1,
919
+ self.name,
920
+ self.width,
921
+ self.height,
922
+ ],
923
+ )
924
+ return self
925
+
926
+
927
+ class Pictures(Collection, base_classes.Pictures):
928
+ _attr = "pictures"
929
+ _wrap = Picture
930
+
931
+ def append_json_action(self, **kwargs):
932
+ self.parent.book.append_json_action(
933
+ **{
934
+ **kwargs,
935
+ **{
936
+ "sheet_position": self.parent.index - 1,
937
+ },
938
+ }
939
+ )
940
+
941
+ def add(
942
+ self,
943
+ filename,
944
+ link_to_file=None,
945
+ save_with_document=None,
946
+ left=None,
947
+ top=None,
948
+ width=None,
949
+ height=None,
950
+ anchor=None,
951
+ ):
952
+ if self.parent.book.api["client"] == "Google Apps Script" and (left or top):
953
+ raise ValueError(
954
+ "'left' and 'top' are not supported with Google Sheets. "
955
+ "Use 'anchor' instead."
956
+ )
957
+ if anchor is None:
958
+ column_index = 0
959
+ row_index = 0
960
+ else:
961
+ column_index = anchor.column - 1
962
+ row_index = anchor.row - 1
963
+ # Google Sheets allows a max size of 1 million pixels. For matplotlib, you
964
+ # can control the pixels like so: fig = plt.figure(figsize=(6, 4), dpi=200)
965
+ # This sample has (6 * 200) * (4 * 200) = 960,000 px
966
+ # Note that savefig(bbox_inches="tight") crops the image and therefore will
967
+ # reduce the number of pixels in a non-deterministic way. Existing figure
968
+ # size can be checked via fig.get_size_inches(). pandas accepts figsize also:
969
+ # ax = df.plot(figsize=(3,3))
970
+ # fig = ax.get_figure()
971
+ with open(filename, "rb") as image_file:
972
+ encoded_image_string = base64.b64encode(image_file.read()).decode("utf-8")
973
+ # TODO: width and height are currently ignored but can be set via obj properties
974
+ self.append_json_action(
975
+ func="addPicture",
976
+ args=[
977
+ encoded_image_string,
978
+ column_index,
979
+ row_index,
980
+ left if left else 0,
981
+ top if top else 0,
982
+ ],
983
+ )
984
+ self.parent._api["pictures"].append(
985
+ {"name": "Image", "width": None, "height": None}
986
+ )
987
+ return Picture(self.parent, len(self.parent.api["pictures"]))
988
+
989
+
990
+ class Name(base_classes.Name):
991
+ def __init__(self, parent, api):
992
+ self.parent = parent
993
+ self.api = api
994
+
995
+ @property
996
+ def name(self):
997
+ if self.api["book_scope"]:
998
+ return self.api["name"]
999
+ else:
1000
+ sheet_name = self.api["scope_sheet_name"]
1001
+ if "!" not in self.api["name"]:
1002
+ # VBA/Google Sheets already do this
1003
+ sheet_name = f"'{sheet_name}'" if " " in sheet_name else sheet_name
1004
+ return f"{sheet_name}!{self.api['name']}"
1005
+ else:
1006
+ return self.api["name"]
1007
+
1008
+ @property
1009
+ def refers_to(self):
1010
+ book = self.parent if isinstance(self.parent, Book) else self.parent.book
1011
+ sheet = book.sheets(self.api["sheet_index"] + 1)
1012
+ sheet_name = f"'{sheet.name}'" if " " in sheet.name else sheet.name
1013
+ return f"={sheet_name}!{sheet.range(self.api['address']).address}"
1014
+
1015
+ @property
1016
+ def refers_to_range(self):
1017
+ book = self.parent if isinstance(self.parent, Book) else self.parent.book
1018
+ sheet = book.sheets(self.api["sheet_index"] + 1)
1019
+ return sheet.range(self.api["address"])
1020
+
1021
+ def delete(self):
1022
+ # TODO: delete in api
1023
+ self.parent.append_json_action(
1024
+ func="nameDelete",
1025
+ args=[
1026
+ self.name, # this includes the sheet name for sheet scope
1027
+ self.refers_to,
1028
+ self.api["name"], # no sheet name
1029
+ self.api["sheet_index"],
1030
+ self.api["book_scope"],
1031
+ self.api["scope_sheet_index"],
1032
+ ],
1033
+ )
1034
+
1035
+
1036
+ class Names(base_classes.Names):
1037
+ def __init__(self, parent, api):
1038
+ self.parent = parent
1039
+ self.api = api
1040
+
1041
+ def add(self, name, refers_to):
1042
+ # TODO: raise backend error in case of duplicates
1043
+ if isinstance(self.parent, Book):
1044
+ is_parent_book = True
1045
+ else:
1046
+ is_parent_book = False
1047
+ self.parent.append_json_action(func="namesAdd", args=[name, refers_to])
1048
+
1049
+ def _get_sheet_index(parent):
1050
+ if is_parent_book:
1051
+ sheets = parent.sheets
1052
+ else:
1053
+ sheets = parent.book.sheets
1054
+ for sheet in sheets:
1055
+ if sheet.name == refers_to.split("!")[0].replace("=", "").replace(
1056
+ "'", ""
1057
+ ):
1058
+ return sheet.index - 1
1059
+
1060
+ return Name(
1061
+ self.parent,
1062
+ {
1063
+ "name": name,
1064
+ "sheet_index": _get_sheet_index(self.parent),
1065
+ "address": refers_to.split("!")[1].replace("$", ""),
1066
+ "book_scope": True if is_parent_book else False,
1067
+ },
1068
+ )
1069
+
1070
+ def __call__(self, name_or_index):
1071
+ if isinstance(name_or_index, numbers.Number):
1072
+ name_or_index -= 1
1073
+ if name_or_index > len(self):
1074
+ raise KeyError(name_or_index)
1075
+ else:
1076
+ return Name(self.parent, api=self.api[name_or_index])
1077
+ else:
1078
+ for ix, i in enumerate(self.api):
1079
+ name = Name(self.parent, api=self.api[ix])
1080
+ if name.name == name_or_index:
1081
+ # Sheet scope names have the sheet name prepended
1082
+ return name
1083
+ raise KeyError(name_or_index)
1084
+
1085
+ def contains(self, name_or_index):
1086
+ if isinstance(name_or_index, numbers.Number):
1087
+ return 1 <= name_or_index <= len(self)
1088
+ else:
1089
+ for i in self.api:
1090
+ if i["name"] == name_or_index:
1091
+ return True
1092
+ return False
1093
+
1094
+ def __len__(self):
1095
+ return len(self.api)
1096
+
1097
+
1098
+ engine = Engine()
1099
+
1100
+
1101
+ class Table(base_classes.Table):
1102
+ @property
1103
+ def show_autofilter(self):
1104
+ return self.api["show_autofilter"]
1105
+
1106
+ @show_autofilter.setter
1107
+ def show_autofilter(self, value):
1108
+ self.append_json_action(
1109
+ func="showAutofilterTable", args=[self.index - 1, value]
1110
+ )
1111
+
1112
+ def __init__(self, parent, key):
1113
+ self._parent = parent
1114
+ self._api = self.parent.api["tables"][key - 1]
1115
+ self.key = key
1116
+
1117
+ def append_json_action(self, **kwargs):
1118
+ self.parent.book.append_json_action(
1119
+ **{
1120
+ **kwargs,
1121
+ **{
1122
+ "sheet_position": self.parent.index - 1,
1123
+ },
1124
+ }
1125
+ )
1126
+
1127
+ @property
1128
+ def api(self):
1129
+ return self._api
1130
+
1131
+ @property
1132
+ def parent(self):
1133
+ return self._parent
1134
+
1135
+ @property
1136
+ def name(self):
1137
+ return self.api["name"]
1138
+
1139
+ @name.setter
1140
+ def name(self, value):
1141
+ self.api["name"] = value
1142
+ self.append_json_action(func="setTableName", args=[self.index - 1, value])
1143
+
1144
+ @property
1145
+ def range(self):
1146
+ if self.api["range_address"]:
1147
+ return self.parent.range(self.api["range_address"])
1148
+ else:
1149
+ return None
1150
+
1151
+ @property
1152
+ def header_row_range(self):
1153
+ if self.api["header_row_range_address"]:
1154
+ return self.parent.range(self.api["header_row_range_address"])
1155
+ else:
1156
+ return None
1157
+
1158
+ @property
1159
+ def data_body_range(self):
1160
+ if self.api["data_body_range_address"]:
1161
+ return self.parent.range(self.api["data_body_range_address"])
1162
+ else:
1163
+ return None
1164
+
1165
+ @property
1166
+ def totals_row_range(self):
1167
+ if self.api["total_row_range_address"]:
1168
+ return self.parent.range(self.api["total_row_range_address"])
1169
+ else:
1170
+ return None
1171
+
1172
+ @property
1173
+ def show_headers(self):
1174
+ return self.api["show_headers"]
1175
+
1176
+ @show_headers.setter
1177
+ def show_headers(self, value):
1178
+ self.append_json_action(func="showHeadersTable", args=[self.index - 1, value])
1179
+
1180
+ @property
1181
+ def show_totals(self):
1182
+ return self.api["show_totals"]
1183
+
1184
+ @show_totals.setter
1185
+ def show_totals(self, value):
1186
+ self.append_json_action(func="showTotalsTable", args=[self.index - 1, value])
1187
+
1188
+ @property
1189
+ def table_style(self):
1190
+ return self.api["table_style"]
1191
+
1192
+ @table_style.setter
1193
+ def table_style(self, value):
1194
+ self.append_json_action(func="setTableStyle", args=[self.index - 1, value])
1195
+
1196
+ @property
1197
+ def index(self):
1198
+ # TODO: make available in public API
1199
+ if isinstance(self.key, numbers.Number):
1200
+ return self.key
1201
+ else:
1202
+ for ix, obj in self.api:
1203
+ if obj["name"] == self.key:
1204
+ return ix + 1
1205
+ raise KeyError(self.key)
1206
+
1207
+ def resize(self, range):
1208
+ self.append_json_action(
1209
+ func="resizeTable", args=[self.index - 1, range.address]
1210
+ )
1211
+
1212
+
1213
+ class Tables(Collection, base_classes.Tables):
1214
+ _attr = "tables"
1215
+ _wrap = Table
1216
+
1217
+ def append_json_action(self, **kwargs):
1218
+ self.parent.book.append_json_action(
1219
+ **{
1220
+ **kwargs,
1221
+ **{
1222
+ "sheet_position": self.parent.index - 1,
1223
+ },
1224
+ }
1225
+ )
1226
+
1227
+ def add(
1228
+ self,
1229
+ source_type=None,
1230
+ source=None,
1231
+ link_source=None,
1232
+ has_headers=None,
1233
+ destination=None,
1234
+ table_style_name=None,
1235
+ name=None,
1236
+ ):
1237
+ self.append_json_action(
1238
+ func="addTable",
1239
+ args=[source.address, has_headers, table_style_name, name],
1240
+ )
1241
+ self.parent._api["tables"].append(
1242
+ {
1243
+ "name": "",
1244
+ "range_address": None,
1245
+ "header_row_range_address": None,
1246
+ "data_body_range_address": None,
1247
+ "total_row_range_address": None,
1248
+ "show_headers": None,
1249
+ "show_totals": None,
1250
+ "table_style": "",
1251
+ }
1252
+ )
1253
+ return Table(self.parent, len(self.parent.api["tables"]))
1254
+
1255
+
1256
+ # if __name__ == "__main__":
1257
+ # # python -m xlwings.pro._xlremote
1258
+ # import inspect
1259
+ #
1260
+ # def print_unimplemented_attributes(class_name, base_class, derived_class=None):
1261
+ # if class_name == "Apps":
1262
+ # return
1263
+ # base_attributes = set(
1264
+ # attr
1265
+ # for attr in vars(base_class)
1266
+ # if not (attr.startswith("_") or attr == "api")
1267
+ # )
1268
+ # if derived_class:
1269
+ # derived_attributes = set(
1270
+ # attr for attr in vars(derived_class) if not attr.startswith("_")
1271
+ # )
1272
+ # else:
1273
+ # derived_attributes = set()
1274
+ # unimplemented_attributes = base_attributes - derived_attributes
1275
+ #
1276
+ # if unimplemented_attributes:
1277
+ # print("")
1278
+ # print(f" xlwings.{class_name}")
1279
+ # print("")
1280
+ # for attribute in unimplemented_attributes:
1281
+ # if not attribute.startswith("__") and attribute not in (
1282
+ # "api",
1283
+ # "xl",
1284
+ # "hwnd",
1285
+ # ):
1286
+ # if callable(getattr(base_class, attribute)):
1287
+ # print(f" - {attribute}()")
1288
+ # else:
1289
+ # print(f" - {attribute}")
1290
+ #
1291
+ # for name, obj in inspect.getmembers(base_classes):
1292
+ # if inspect.isclass(obj):
1293
+ # print_unimplemented_attributes(name, obj, globals().get(name))