tpltable 0.3.0__tar.gz → 0.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tpltable
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: define "Excel" "Table|StyleTable" "Row|Col|StyleRow|StyleCol". Can dynamic modify excel data & styles. Support multiple kinds of indexes. (70%)
5
5
  Author-email: 2229066748@qq.com
6
6
  Maintainer: Eagle'sBaby
@@ -5,7 +5,7 @@ from setuptools import find_packages, setup
5
5
 
6
6
  setup(
7
7
  name='tpltable',
8
- version='0.3.0',
8
+ version='0.3.2',
9
9
  description='define "Excel" "Table|StyleTable" "Row|Col|StyleRow|StyleCol". Can dynamic modify excel data & styles. Support multiple kinds of indexes. (70%)',
10
10
  author_email='2229066748@qq.com',
11
11
  maintainer="Eagle'sBaby",
@@ -0,0 +1,561 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from tpltable.table import *
4
+ from tpltable.style import *
5
+
6
+
7
+ class ListkeysDict(dict):
8
+ _DEFAULT_PREFIX = "NAME_"
9
+
10
+ def __init__(self, value=None, *, keys: list = None):
11
+ if value is None:
12
+ value = {}
13
+ super().__init__(value)
14
+ self._keys = list(super().keys())
15
+
16
+ if keys is not None:
17
+ for k in keys:
18
+ if k not in self:
19
+ raise KeyError(f"Unknown key:{k} from custom keys")
20
+
21
+ for k in self:
22
+ if k not in keys:
23
+ raise KeyError(f"Mising key:{k} from custom keys")
24
+
25
+ self._keys = list(keys)
26
+
27
+ self._last_key_number = 0
28
+
29
+ def _generate_key(self):
30
+ """生成一个唯一的键名,确保不与现有键冲突。"""
31
+ self._last_key_number += 1
32
+ key = f"{self._DEFAULT_PREFIX}{self._last_key_number}"
33
+ while key in self:
34
+ key = '_' + key
35
+ return key
36
+
37
+ def _get_keys(self, key) -> list:
38
+ try:
39
+ if isinstance(key, slice):
40
+ return self._keys[key]
41
+ elif isinstance(key, int):
42
+ return [self._keys[key]]
43
+ else:
44
+ return [key]
45
+ except IndexError:
46
+ raise KeyError(f"Key:{key} is out of bound.")
47
+
48
+ # Note: Dicts: ..........................................................................
49
+ def keys(self):
50
+ return self._keys.copy()
51
+
52
+ def values(self):
53
+ return [self[k] for k in self._keys]
54
+
55
+ def items(self):
56
+ return [(k, self[k]) for k in self._keys]
57
+
58
+ def get(self, key, default=None):
59
+ if isinstance(key, int):
60
+ key = self._keys[key]
61
+ elif isinstance(key, str):
62
+ key = key
63
+ else:
64
+ raise TypeError(f"unexpected key type:{type(key)}.")
65
+ return super().get(key, default)
66
+
67
+ def update(self, _m):
68
+ for k, v in _m.items():
69
+ if k not in self:
70
+ self._keys.append(k)
71
+ super().__setitem__(k, v)
72
+
73
+ @classmethod
74
+ def fromkeys(cls, keys, value=None):
75
+ return cls({k: value for k in keys})
76
+
77
+ def setdefault(self, __key, __default = None):
78
+ if __key not in self:
79
+ self._keys.append(__key)
80
+ return super().setdefault(__key, __default)
81
+
82
+ def popitem(self):
83
+ key = self._keys.pop()
84
+ return key, super().pop(key)
85
+
86
+
87
+ # Note: Lists: ..........................................................................
88
+
89
+ def append(self, value, *, key: str = None):
90
+ """在列表末尾添加元素。"""
91
+ if key is None:
92
+ key = self._generate_key()
93
+ if key in self:
94
+ raise KeyError(f"Repeat key: {key}")
95
+ self._keys.append(key)
96
+ super().__setitem__(key, value)
97
+
98
+ def extend(self, _m):
99
+ for k, v in _m.items():
100
+ if k not in self:
101
+ self._keys.append(k)
102
+ super().__setitem__(k, v)
103
+
104
+ def insert(self, index, value, *, key: str = None):
105
+ """在指定位置插入元素。"""
106
+ if index > len(self):
107
+ raise IndexError("Index out of range.")
108
+ if key is None:
109
+ key = self._generate_key()
110
+ if isinstance(index, str):
111
+ index = self._keys.index(index)
112
+
113
+ if key in self:
114
+ raise KeyError(f"Repeat key: {key}")
115
+
116
+ # self[key] = value
117
+ super().__setitem__(key, value)
118
+ self._keys.insert(index, key)
119
+
120
+
121
+ def index(self, value, start=0, stop=EMPTY_INDEX):
122
+ if stop is EMPTY_INDEX:
123
+ stop = len(self)
124
+ for i, k in enumerate(self._keys[start:stop]):
125
+ if self[k] == value:
126
+ return i
127
+ raise ValueError(f"{value} is not in listkeys_dict.")
128
+
129
+ def remove(self, value):
130
+ i = self.index(value)
131
+ super().__delitem__(self._keys[i])
132
+ self._keys.pop(i)
133
+
134
+ def reverse(self):
135
+ self._keys.reverse()
136
+
137
+ def sort(self, key=None, reverse=False):
138
+ if key is None:
139
+ key = lambda x: x
140
+ self._keys.sort(key=lambda x: key(super().__getitem__(x)), reverse=reverse)
141
+
142
+ def count(self, value):
143
+ return sum([1 for k in self._keys if super().__getitem__(k) == value])
144
+
145
+ # Note: Both Dicts & Lists: ..........................................................................
146
+ def pop(self, key, default=EMPTY_INDEX):
147
+ keys = self._get_keys(key)
148
+
149
+ for key in keys:
150
+ if default is EMPTY_INDEX:
151
+ return super().pop(key)
152
+ else:
153
+ return super().pop(key, default)
154
+
155
+
156
+ def clear(self):
157
+ self._keys.clear()
158
+ super().clear()
159
+
160
+ def copy(self):
161
+ return ListkeysDict(self, keys=self._keys.copy())
162
+
163
+
164
+ def __getitem__(self, key):
165
+ keys = self._get_keys(key)
166
+
167
+ _ret = []
168
+ for key in keys:
169
+ _ret.append(super().__getitem__(key))
170
+
171
+ if len(_ret) == 1:
172
+ return _ret[0]
173
+ return _ret
174
+
175
+ def __setitem__(self, key, value):
176
+ keys = self._get_keys(key)
177
+
178
+ for key in keys:
179
+ if key not in self:
180
+ self._keys.append(key)
181
+ super().__setitem__(key, value)
182
+
183
+ def __delitem__(self, key):
184
+ keys = self._get_keys(key)
185
+
186
+ for key in keys:
187
+ super().__delitem__(key)
188
+ self._keys.remove(key)
189
+
190
+ def __iter__(self):
191
+ return iter(self.keys())
192
+
193
+ def __len__(self):
194
+ return len(self._keys)
195
+
196
+
197
+ def __str__(self):
198
+ """提供字典内容的字符串表示。"""
199
+ txt = "{"
200
+ for i, (k, v) in enumerate(self.items()):
201
+ if isinstance(k, str):
202
+ k = f"'{k}'"
203
+ if isinstance(v, str):
204
+ v = f"'{v}'"
205
+
206
+ txt += f"{i}|{k}: {v}, "
207
+
208
+ if txt[-1] != '{':
209
+ txt = txt[:-2]
210
+
211
+ return txt + '}'
212
+
213
+ def __repr__(self):
214
+ return str(self)
215
+
216
+ # MATH
217
+ def __add__(self, other):
218
+ if isinstance(other, ListkeysDict):
219
+ return ListkeysDict({**self, **other})
220
+ return NotImplemented
221
+
222
+ def __radd__(self, other: dict):
223
+ other.update(self)
224
+ return other
225
+
226
+ def __iadd__(self, other):
227
+ self.update(other)
228
+ return self
229
+
230
+ def __sub__(self, other):
231
+ if isinstance(other, ListkeysDict):
232
+ return ListkeysDict({k: v for k, v in self.items() if k not in other})
233
+ return NotImplemented
234
+
235
+ def __rsub__(self, other: dict):
236
+ return {k: v for k, v in other.items() if k not in self}
237
+
238
+ def __isub__(self, other):
239
+ for k in other:
240
+ self.pop(k, None)
241
+ return self
242
+
243
+ def __mul__(self, other):
244
+ raise TypeError("unsupported operand type(s) for *: 'ListkeysDict' and 'int'")
245
+
246
+ def __rmul__(self, other):
247
+ raise TypeError("unsupported operand type(s) for *: 'int' and 'ListkeysDict'")
248
+
249
+ def __imul__(self, other):
250
+ raise TypeError("unsupported operand type(s) for *: 'ListkeysDict' and 'int'")
251
+
252
+ # COMPARE
253
+ def __eq__(self, other):
254
+ if isinstance(other, dict):
255
+ _items0, _items1 = self.items(), other.items()
256
+ return _items0 == _items1
257
+
258
+ return False
259
+
260
+ def __ne__(self, other):
261
+ return not self.__eq__(other)
262
+
263
+ def __lt__(self, other):
264
+ raise TypeError("unorderable types: 'ListkeysDict' < 'ListkeysDict'")
265
+
266
+ def __le__(self, other):
267
+ raise TypeError("unorderable types: 'ListkeysDict' <= 'ListkeysDict'")
268
+
269
+ def __gt__(self, other):
270
+ raise TypeError("unorderable types: 'ListkeysDict' > 'ListkeysDict'")
271
+
272
+ def __ge__(self, other):
273
+ raise TypeError("unorderable types: 'ListkeysDict' >= 'ListkeysDict'")
274
+
275
+ # TYPE
276
+ def __bool__(self):
277
+ return bool(self._keys)
278
+
279
+ def __contains__(self, item):
280
+ return item in self._keys
281
+
282
+ def __hash__(self):
283
+ raise TypeError("unhashable type: 'ListkeysDict'")
284
+
285
+
286
+ def test_listkeys_dict():
287
+ # 创建listkeys_dict实例
288
+ d = ListkeysDict()
289
+
290
+ # 测试append方法
291
+ d.append('apple')
292
+ d.append('banana')
293
+ print("After appends:", d) # 应显示:['XXX1', 'XXX0'] = ['apple', 'banana']
294
+
295
+ # 测试getitem方法
296
+ print("Get item by int index 0:", d[0]) # 应显示:'XXX0'
297
+ print("Get item by int index 1:", d[1]) # 应显示:'XXX1'
298
+
299
+ # 测试setitem方法
300
+ d[1] = 'cherry'
301
+ print("After setting item 1 to 'cherry':", d) # 应显示:['XXX0', 'XXX_XXX1'] = ['apple', 'cherry']
302
+
303
+ # 测试delitem方法
304
+ del d[0]
305
+ print("After deleting item 0:", d) # 应显示:['XXX_XXX1'] = ['cherry']
306
+
307
+ # 测试insert方法
308
+ d.insert(0, 'date')
309
+ print("After inserting 'date' at index 0:", d) # 应显示:['XXX2', 'XXX_XXX1'] = ['date', 'cherry']
310
+
311
+ # 测试迭代
312
+ print("Iterating over keys:", list(d)) # 应显示:['XXX2', 'XXX_XXX1']
313
+
314
+ # 测试长度
315
+ print("Length of d:", len(d)) # 应显示:2
316
+
317
+ # 测试切片
318
+ print("Slice of d:", d[0:2]) # 应显示:['XXX2', 'XXX_XXX1']
319
+
320
+ # 测试生成key
321
+ print("Generated key:", d._generate_key()) # 应显示一个唯一的key
322
+
323
+ # 测试异常处理
324
+ try:
325
+ d['nonexistent']
326
+ except KeyError as e:
327
+ print("Caught an exception:", e)
328
+
329
+ try:
330
+ del d[10]
331
+ except KeyError as e:
332
+ print("Caught an exception:", e)
333
+
334
+ try:
335
+ d[10] = 'mystery'
336
+ except KeyError as e:
337
+ print("Caught an exception:", e)
338
+
339
+
340
+ class ExcelFileIOException(Exception):
341
+ pass
342
+
343
+
344
+ class Excel(ListkeysDict):
345
+ def __init__(self, fpath: str = None, *, style: bool = True):
346
+ """
347
+ *Create an Excel object from an excel file or empty
348
+ :param fpath: excel file path. If None, will create an empty Excel object.
349
+ *
350
+ :param style: bool. If True, will load the style of the cell(may be cost). Default is True.
351
+ """
352
+ self._path = fpath
353
+ if self._path:
354
+ # try:
355
+ wb = load_workbook(fpath)
356
+ tbls = {sheet.title: self.__table_from_sheet(sheet, only_data=not style) for sheet in wb.worksheets}
357
+ wb.close()
358
+ # except Exception as e:
359
+ # raise ExcelFileIOException("\n\nFatal to load excel file: \n" + reinsert_indent(str(e), '\t'))
360
+ else:
361
+ tbls = {}
362
+
363
+ super().__init__(tbls)
364
+
365
+ def __getitem__(self, item):
366
+ if isinstance(item, tuple):
367
+ _i0 = item[0]
368
+ item = item[1:] if len(item) >= 2 else None
369
+ else:
370
+ _i0 = item
371
+ item = None
372
+
373
+ if item is None:
374
+ return super().__getitem__(_i0)
375
+ return super().__getitem__(_i0)[item]
376
+
377
+ def __setitem__(self, key, value):
378
+ if isinstance(key, tuple):
379
+ _i0 = key[0]
380
+ item = key[1:] if len(key) >= 2 else None
381
+ else:
382
+ _i0 = key
383
+ key = None
384
+
385
+ if key is None:
386
+ assert isinstance(value, Table), f"Unexpected Table type:{value}"
387
+
388
+ if item is None:
389
+ super().__setitem__(_i0, value)
390
+ return
391
+ super().__getitem__(_i0)[item] = value
392
+
393
+ def __delitem__(self, key):
394
+ if isinstance(key, tuple):
395
+ _i0 = key[0]
396
+ key = key[1:] if len(key) >= 2 else None
397
+ else:
398
+ _i0 = key
399
+ key = None
400
+
401
+ super().__delitem__(key)
402
+
403
+ def __table_from_sheet(self, sheet: Worksheet, only_data: bool = False):
404
+ _construct_2d_list = []
405
+ _merges = [_merge.coord for _merge in sheet.merged_cells.ranges]
406
+
407
+ if only_data:
408
+ _styles = None
409
+ for row in sheet.iter_rows():
410
+ _construct_2d_list.append([cell.value if cell.value else NaN for cell in row])
411
+ else:
412
+ _styles = []
413
+ for row in sheet.iter_rows():
414
+ _construct_2d_list.append([cell.value if cell.value else NaN for cell in row])
415
+ _styles.append([Style(sheet, cell) for cell in row])
416
+
417
+ return Table(_construct_2d_list, _styles, _merges, copy=False)
418
+
419
+ @property
420
+ def path(self):
421
+ return self._path
422
+
423
+ @property
424
+ def tables(self):
425
+ return list(self.values())
426
+
427
+ @staticmethod
428
+ def __simple_str(string, max: int = 20):
429
+ string = str(string)
430
+ _len = len(string)
431
+ assert max > 4, "Max length should be greater than 4."
432
+ if _len > max:
433
+ _half = max // 2 - 1
434
+ return f"{string[:9]}...{string[_len - 9:]}"
435
+ return string
436
+
437
+ def __repr__(self):
438
+ return f"Excel({self.__simple_str(self._path)}|{len(self)} tables)"
439
+
440
+ def __str__(self):
441
+ _txt = f"Excel({self.__simple_str(self._path)}|{len(self)} tables)\t" + '{\n'
442
+ _MAX_SIZE = 16 - 2
443
+
444
+ # Max length key
445
+ keys = [k for k in self.keys()]
446
+ max_len_key = max([len(key) for key in keys])
447
+ _MAX_SIZE = min(_MAX_SIZE, max_len_key) + 2
448
+ max_len_key = max(max_len_key, _MAX_SIZE)
449
+
450
+ for name, tbl in self.items():
451
+ name = self.__simple_str(name, _MAX_SIZE)
452
+ _left_space = ' ' * (max_len_key - len(name))
453
+ _txt += f"\t'{name}'{_left_space}: {tbl.__repr__()},\n"
454
+ _txt += '}'
455
+ return _txt
456
+
457
+ def detail(self) -> str:
458
+ _txt = f"Excel({self.__simple_str(self._path)}|{len(self)} tables)\t" + '{\n'
459
+ _MAX_SIZE = 16 - 2
460
+
461
+ for name, tbl in self.items():
462
+ _sub_txt = color_string(f"\tTable '{name}'") + " >\n" + reinsert_indent(str(tbl), '\t\t')
463
+ _txt += _sub_txt + '\n\n'
464
+
465
+ if len(self) > 0:
466
+ _txt = _txt[:-1]
467
+ _txt += '}'
468
+
469
+ return _txt
470
+
471
+ def save(self, fpath: str = None, *, style: bool = True):
472
+ """
473
+ *Save the Excel object to an excel file.
474
+ :param fpath:
475
+ *
476
+ :param style: bool. If True, will save the style of the cell. Default is True.
477
+ NOTE: if use, will slower 20X and more
478
+ :return:
479
+ """
480
+ wb = Workbook()
481
+ for name, tbl in self.items():
482
+ ws = wb.create_sheet(name)
483
+ for row in tbl:
484
+ _row = []
485
+ for value in row:
486
+ # if nan, set to None
487
+ if isnan(value):
488
+ _row.append(None)
489
+ else:
490
+ _row.append(value)
491
+ ws.append(_row)
492
+
493
+ # Merge
494
+ for merge in tbl.merges:
495
+ ws.merge_cells(f"{merge[0].letter}:{merge[1].letter}")
496
+
497
+ # Style
498
+ if style and tbl.styles is not None:
499
+ style_tbl = tbl.styles
500
+ for i, row in enumerate(ws.iter_rows()):
501
+ for j, cell in enumerate(row):
502
+ style_tbl[i][j].apply(ws, cell)
503
+
504
+ # remove the default sheet
505
+ wb.remove(wb.active)
506
+
507
+ if not fpath:
508
+ fpath = self._path
509
+ if not fpath:
510
+ raise ExcelFileIOException("\n\nNo file path defined: " + str(self))
511
+
512
+ try:
513
+ wb.save(fpath)
514
+ except Exception as e:
515
+ raise ExcelFileIOException("\n\nFatal to save excel file: \n" + reinsert_indent(str(e), '\t'))
516
+ wb.close()
517
+
518
+
519
+ if __name__ == '__main__':
520
+ # test_listkeys_dict()
521
+ # exit()
522
+ # lk = ListkeysDict({'a': "Hello, ", 'b': "World"})
523
+ # print(lk['a'])
524
+ # print(lk)
525
+ # lk.append('你好!')
526
+ # print(lk)
527
+ # lk.insert(0, 'awa')
528
+ # print(lk)
529
+ # del lk[:2]
530
+ # print(lk)
531
+ #
532
+ # exit()
533
+ from ffre import FileFinder
534
+
535
+ fdir = r"C:\Users\22290\Desktop\20240504整理\tpltable 数据"
536
+ ff = FileFinder(fdir)
537
+ fpaths = list(ff.find(".xlsx"))
538
+
539
+ if not fpaths:
540
+ print("No excel file found.")
541
+ exit(0)
542
+
543
+ fp = fpaths[1]
544
+ _t = TimeRecorder()
545
+ for i in range(1):
546
+ excel = Excel(fp, style=False)
547
+ print("Load excel without style cost:", _t.dms(1), "ms")
548
+ _t.tick()
549
+ for i in range(1):
550
+ excel = Excel(fp, style=True)
551
+ print("Load excel with style cost:", _t.dms(1), "ms")
552
+
553
+ excel[0].styles['A1'][TYPE_COL_WIDTH] = 200
554
+
555
+ print(excel)
556
+ _t.tick()
557
+ excel.save("test.xlsx", style=False)
558
+ print("Save excel without style cost:", _t.dms(1), "ms")
559
+ _t.tick()
560
+ excel.save("test_style.xlsx", style=True)
561
+ print("Save excel with style cost:", _t.dms(1), "ms")
@@ -1,13 +1,8 @@
1
- from ffre import FileFinder, FCollect
2
1
  from tpltable.excel import Excel, Table
3
2
  from tpltable.style import *
4
- DIR_PATH = r"C:\Users\22290\Desktop\20240504整理\tpltable 数据"
5
3
 
6
- xlsx_paths:FCollect = FileFinder(DIR_PATH).find("xlsx", pattern=".+线$", exclude="^~")
4
+ e = Excel()
7
5
 
8
-
9
- # e = Excel()
10
- #
11
6
  # t = e.append(Table(
12
7
  # [[0],
13
8
  # [4, 5, 6, 7],
@@ -22,15 +17,25 @@ xlsx_paths:FCollect = FileFinder(DIR_PATH).find("xlsx", pattern=".+线$", exclud
22
17
  # t.styles['A1'].set(TYPE_COL_WIDTH, 40) # 设置列宽
23
18
  # t.styles[:, 1].set(TYPE_BCOLOR, COLOR_LIGHTGRAY) # 设置浅灰色背景色
24
19
  # t.merge("A1:D1") # 合并单元格
20
+ #
21
+ # print(S
22
+ # e.detail()
23
+ # )
25
24
 
26
- e = Excel('test.xlsx')
27
- t = e[0]
25
+ t = e.append(Table())
28
26
 
29
- del t['A']
27
+ t.append(
28
+ [1, 2, 3, 4, 10],
29
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
30
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
31
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
32
+ )
30
33
 
31
- del t['B1', 'C2']
34
+ t.merge('A1:F2')
35
+ t.merge('E2:H3')
36
+ print(t)
32
37
 
33
- e.save()
38
+ e.save('test.xlsx')
34
39
 
35
40
 
36
41
 
@@ -409,7 +409,31 @@ class _ExcelIndexsLike_2DArray:
409
409
  return tuple()
410
410
 
411
411
  def __str__(self):
412
- return self.__NAN_PAT.sub(self.__NAN_SUB_FN, str(self._df))
412
+ _values = self._df.values.tolist()
413
+ _values2 = []
414
+
415
+ # make sure each one is string
416
+ for i, _l in enumerate(_values):
417
+ _values[i] = [str(v) if not isnan(v) else "NaN" for v in _l]
418
+ _values2.append([f"({v})" if not isnan(v) else "NaN" for v in _l])
419
+
420
+ _cnd = np.array(_values)
421
+ _bracket_cdf = np.array(_values2)
422
+
423
+ # 将处于merge范围内的元素加上(), 除了第一个元素
424
+ _unhandled_mask = np.ones(self._df.shape, dtype=bool)
425
+ for lt, rb in self._merges:
426
+ _new_mask = np.zeros(self._df.shape, dtype=bool)
427
+ _new_mask[lt.ir:rb.ir + 1, lt.ic:rb.ic + 1] = _unhandled_mask[lt.ir:rb.ir + 1, lt.ic:rb.ic + 1]
428
+ _cnd[_new_mask] = _bracket_cdf[_new_mask]
429
+ # 把第一个元素还原
430
+ if _unhandled_mask[lt.ir, lt.ic]:
431
+ _cnd[lt.ir, lt.ic] = ':' + _cnd[lt.ir, lt.ic][1:]
432
+ _unhandled_mask[lt.ir:rb.ir + 1, lt.ic:rb.ic + 1] = False
433
+
434
+
435
+ return str(pd.DataFrame(_cnd, index=self._df.index, columns=self._df.columns, copy=False))
436
+
413
437
 
414
438
  def __repr__(self):
415
439
  c = Coord(self._df.shape[1] - 1, self._df.shape[0] - 1)
@@ -734,7 +758,12 @@ class _ExcelIndexsLike_2DArray:
734
758
 
735
759
  def _reindex(self):
736
760
  _2d_list = self._df.values.tolist()
737
- self._df = self.__class__(_2d_list, *self._other_raws, copy=False)._df
761
+ self._reconstruct(_2d_list, *self._other_raws, copy=False)
762
+
763
+ def _reconstruct(self, *args, **kwargs):
764
+ new = self.__class__(*args, **kwargs)
765
+ self._df = new._df
766
+ return new
738
767
 
739
768
  def _enlarge(self, ir, ic, fill=np.nan):
740
769
  """
@@ -763,7 +792,7 @@ class _ExcelIndexsLike_2DArray:
763
792
  if ic >= sc:
764
793
  for _l in _2d_list:
765
794
  _l.extend([fill] * (ic - sc))
766
- self._df = self.__class__(_2d_list, *self._other_raws, copy=False)._df
795
+ self._reconstruct(_2d_list, *self._other_raws, copy=False)
767
796
 
768
797
  def __getitem__(self, key):
769
798
  """
@@ -819,7 +848,7 @@ class _ExcelIndexsLike_2DArray:
819
848
 
820
849
  if _i0 is None: # None, None
821
850
  if isinstance(value, (int, float, str, bool)):
822
- self._df = value
851
+ self._df.iloc[:, :] = value # NOTE: 使用:,:来避免修改形状
823
852
  return
824
853
  elif isinstance(value, list):
825
854
  value = np.array(value)
@@ -832,7 +861,7 @@ class _ExcelIndexsLike_2DArray:
832
861
  if value.shape == self._df.shape:
833
862
  self._df.iloc[:, :] = value
834
863
  else:
835
- self._df = self.__class__(value, *self._other_raws, copy=False)._df
864
+ self._reconstruct(value, *self._other_raws, copy=False)
836
865
  return
837
866
  else:
838
867
  raise ValueError(f"Unsupported value type: {type(value)}")
@@ -865,7 +894,7 @@ class _ExcelIndexsLike_2DArray:
865
894
  else:
866
895
  raise ValueError(f"Unsupported value type: {type(value)}")
867
896
  _2d_list = _2d_list[:_start] + _ + _2d_list[_start:]
868
- self._df = self.__class__(_2d_list, *self._other_raws, copy=False)._df
897
+ self._reconstruct(_2d_list, *self._other_raws, copy=False)
869
898
  return
870
899
  else:
871
900
  raise ValueError(f"Unsupported value type: {type(value)}")
@@ -880,7 +909,7 @@ class _ExcelIndexsLike_2DArray:
880
909
  """
881
910
  _i0, _i1 = self._dim2_parse_index_key(key)
882
911
  if _i0 is None:
883
- self._df = pd.DataFrame()
912
+ self._reconstruct(None, *self._other_raws)
884
913
  return
885
914
  elif _i1 is None:
886
915
  self._df.drop(self._df.index[_i0], inplace=True)
@@ -891,7 +920,7 @@ class _ExcelIndexsLike_2DArray:
891
920
  _is_i1_slice = isinstance(_i1, slice)
892
921
  if _is_i0_slice and _is_i1_slice:
893
922
  if _i0.start is None and _i0.stop is None and _i1.start is None and _i1.stop is None:
894
- self._df = pd.DataFrame()
923
+ self._reconstruct()
895
924
  elif _i0.start is None and _i0.stop is None:
896
925
  self._df.drop(self._df.columns[_i1], axis=1, inplace=True)
897
926
  self._reindex()
@@ -911,6 +940,7 @@ class _ExcelIndexsLike_2DArray:
911
940
  else:
912
941
  self._df.iloc[_i0, _i1] = np.nan
913
942
 
943
+
914
944
  def append(self, *values, axis=0):
915
945
  """
916
946
  Append Rows to the end of the table
@@ -925,14 +955,14 @@ class _ExcelIndexsLike_2DArray:
925
955
  else:
926
956
  for row in _construct:
927
957
  row.extend(values)
928
- self._df = self.__class__(_construct, *self._other_raws, copy=False)._df
958
+ self._reconstruct(_construct, *self._other_raws, copy=False)
929
959
 
930
960
  def clear(self):
931
961
  """
932
962
  Clear the table
933
963
  :return: None
934
964
  """
935
- self._df = pd.DataFrame()
965
+ self._reconstruct(None, *self._other_raws)
936
966
 
937
967
  def copy(self):
938
968
  """
@@ -967,7 +997,7 @@ class _ExcelIndexsLike_2DArray:
967
997
 
968
998
  if _r == 0 or _c == 0:
969
999
  _construct = [[fill.copy() if _has_fill_copy else fill for _ in range(_new_c)] for _ in range(_new_r)]
970
- self._df = self.__class__(_construct, *self._other_raws, copy=False)._df
1000
+ self._reconstruct(_construct, *self._other_raws, copy=False)
971
1001
  return
972
1002
 
973
1003
  # 先考虑扩张
@@ -978,9 +1008,9 @@ class _ExcelIndexsLike_2DArray:
978
1008
 
979
1009
  # 再考虑缩小 NOTE: 缩小不必考虑label的问题
980
1010
  if _r > _new_r:
981
- self._df = self._df.iloc[:_new_r]
1011
+ self._reconstruct(self._df.iloc[:_new_r], *self._other_raws, copy=False)
982
1012
  if _c > _new_c:
983
- self._df = self._df.iloc[:, :_new_c]
1013
+ self._reconstruct(self._df.iloc[:, :_new_c], *self._other_raws, copy=False)
984
1014
 
985
1015
 
986
1016
  def __iter__(self):
@@ -1071,7 +1101,7 @@ class Table(_ExcelIndexsLike_2DArray):
1071
1101
  def merges(self):
1072
1102
  return self._merges
1073
1103
 
1074
- def merge(self, key:U[int, str, slice, Ellipsis, None], key1:U[int, str, slice, Ellipsis, None]=EMPTY_INDEX):
1104
+ def merge(self, key:U[int, str, slice, None], key1:U[int, str, slice, None]=EMPTY_INDEX):
1075
1105
  if key1 is not EMPTY_INDEX:
1076
1106
  key = key, key1
1077
1107
 
@@ -1098,17 +1128,28 @@ class Table(_ExcelIndexsLike_2DArray):
1098
1128
  lt, rb = Coord(_i1, _i0), Coord(_i1 - 1, _i0 - 1)
1099
1129
 
1100
1130
  # check whether the merge is out of the table
1101
- if lt.ir > ishape[1] or lt.ic > ishape[0]:
1131
+ if lt.ir > ishape[0] or lt.ic > ishape[1]:
1102
1132
  return
1103
1133
 
1104
- if rb.ir > ishape[1]:
1105
- rb.ir = ishape[1]
1134
+ if rb.ir > ishape[0]:
1135
+ rb.ir = ishape[0]
1106
1136
 
1107
- if rb.ic > ishape[0]:
1108
- rb.ic = ishape[0]
1137
+ if rb.ic > ishape[1]:
1138
+ rb.ic = ishape[1]
1109
1139
 
1110
1140
  self._merges.append((lt, rb))
1111
1141
 
1142
+ def _reconstruct(self, *args, **kwargs):
1143
+ """
1144
+ 用于缩小或扩大表格时自动同步调整styles
1145
+ :param args:
1146
+ :param kwargs:
1147
+ :return:
1148
+ """
1149
+ new = super()._reconstruct(*args, **kwargs)
1150
+ self._styles = new._styles
1151
+ return new
1152
+
1112
1153
  def __delitem__(self, key):
1113
1154
  """
1114
1155
  Delete the value of the key
@@ -1117,11 +1158,12 @@ class Table(_ExcelIndexsLike_2DArray):
1117
1158
  """
1118
1159
  _i0, _i1 = self._dim2_parse_index_key(key)
1119
1160
  if _i0 is None:
1120
- self._df = pd.DataFrame()
1161
+ self._reconstruct(None, *self._other_raws, copy=False)
1121
1162
  return
1122
1163
  elif _i1 is None:
1123
1164
  self._df.drop(self._df.index[_i0], inplace=True)
1124
- self._styles.drop(self._styles.index[_i0], inplace=True)
1165
+ self._styles._df.drop(self._styles._df.index[_i0], inplace=True)
1166
+ self._remerge('dr', _i0)
1125
1167
  self._reindex()
1126
1168
  return
1127
1169
  else:
@@ -1129,26 +1171,84 @@ class Table(_ExcelIndexsLike_2DArray):
1129
1171
  _is_i1_slice = isinstance(_i1, slice)
1130
1172
  if _is_i0_slice and _is_i1_slice:
1131
1173
  if _i0.start is None and _i0.stop is None and _i1.start is None and _i1.stop is None:
1132
- self._df = pd.DataFrame()
1174
+ self._reconstruct(None, *self._other_raws, copy=False)
1133
1175
  elif _i0.start is None and _i0.stop is None:
1134
1176
  self._df.drop(self._df.columns[_i1], axis=1, inplace=True)
1177
+ self._styles._df.drop(self._styles._df.columns[_i1], axis=1, inplace=True)
1178
+ self._remerge('dc', _i1)
1135
1179
  self._reindex()
1136
1180
  elif _i1.start is None and _i1.stop is None:
1137
1181
  self._df.drop(self._df.index[_i0], inplace=True)
1182
+ self._styles._df.drop(self._styles._df.index[_i0], inplace=True)
1183
+ self._remerge('dr', _i0)
1138
1184
  self._reindex()
1139
1185
  else: # 删除一片区域为nan
1140
1186
  self._df.iloc[_i0, _i1] = np.nan
1141
1187
  elif _is_i0_slice and _i0.start is None and _i0.stop is None:
1142
1188
  self._df.drop(self._df.columns[_i1], axis=1, inplace=True)
1189
+ self._styles._df.drop(self._styles._df.columns[_i1], axis=1, inplace=True)
1190
+ self._remerge('dc', _i1)
1143
1191
  self._reindex()
1144
1192
  return
1145
1193
  elif _is_i1_slice and _i1.start is None and _i1.stop is None:
1146
1194
  self._df.drop(self._df.index[_i0], inplace=True)
1195
+ self._styles._df.drop(self._styles._df.index[_i0], inplace=True)
1196
+ self._remerge('dr', _i0)
1147
1197
  self._reindex()
1148
1198
  return
1149
1199
  else:
1150
1200
  self._df.iloc[_i0, _i1] = np.nan
1151
1201
 
1202
+ def _remerge(self, op_type, value):
1203
+ """
1204
+ 重新调整合并区域
1205
+ :param op_type: 'a'|'d'+'r'|'c' 表示增加行、删除行、增加列、删除列
1206
+ :param value: 对应的索引值,可以是int或slice
1207
+ :return: None
1208
+ """
1209
+ assert len(op_type) == 2, f"Invalid op_type: {op_type}"
1210
+ op, tar = op_type
1211
+ assert op in 'ad' and tar in 'rc', f"Invalid op_type: {op_type}"
1212
+
1213
+ # 将int转为slice
1214
+ if isinstance(value, int):
1215
+ value = slice(value, value + 1)
1216
+ elif not isinstance(value, slice):
1217
+ raise ValueError(f"Unsupported re_merge value type: {type(value)}")
1218
+ _removes = []
1219
+ if tar == 'r':
1220
+ for i, (lt, rb) in enumerate(self._merges.copy()):
1221
+ if op == 'a':
1222
+ if lt.ir >= value.start:
1223
+ lt.ir += value.stop - value.start
1224
+ if rb.ir >= value.start:
1225
+ rb.ir += value.stop - value.start
1226
+ else:
1227
+ if lt.ir >= value.start:
1228
+ lt.ir -= value.stop - value.start
1229
+ if rb.ir >= value.start:
1230
+ rb.ir -= value.stop - value.start
1231
+ if lt.ir == rb.ir:
1232
+ _removes.append(i)
1233
+ # else: NOTE: 无需赋值, 只是浅拷贝
1234
+ # self._merges[i] = (lt, rb)
1235
+ else:
1236
+ for i, (lt, rb) in enumerate(self._merges.copy()):
1237
+ if op == 'a':
1238
+ if lt.ic >= value.start:
1239
+ lt.ic += value.stop - value.start
1240
+ if rb.ic >= value.start:
1241
+ rb.ic += value.stop - value.start
1242
+ else:
1243
+ if lt.ic >= value.start:
1244
+ lt.ic -= value.stop - value.start
1245
+ if rb.ic >= value.start:
1246
+ rb.ic -= value.stop - value.start
1247
+ if lt.ic == rb.ic:
1248
+ _removes.append(i)
1249
+ # else: NOTE: 无需赋值, 只是浅拷贝
1250
+ # self._merges[i] = (lt, rb)
1251
+
1152
1252
 
1153
1253
  def __1d_test(_print=True):
1154
1254
  d = [1, 2, 3, 4, 5]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tpltable
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: define "Excel" "Table|StyleTable" "Row|Col|StyleRow|StyleCol". Can dynamic modify excel data & styles. Support multiple kinds of indexes. (70%)
5
5
  Author-email: 2229066748@qq.com
6
6
  Maintainer: Eagle'sBaby
@@ -1,208 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
- from tpltable.table import *
4
- from tpltable.style import *
5
-
6
- class ExcelFileIOException(Exception):
7
- pass
8
-
9
- class Excel(dict):
10
- def __init__(self, fpath:str=None, *, style:bool=True):
11
- """
12
- *Create an Excel object from an excel file or empty
13
- :param fpath: excel file path. If None, will create an empty Excel object.
14
- *
15
- :param style: bool. If True, will load the style of the cell(may be cost). Default is True.
16
- """
17
- self._path = fpath
18
- if self._path:
19
- # try:
20
- wb = load_workbook(fpath)
21
- tbls = {sheet.title: self.__table_from_sheet(sheet, only_data=not style) for sheet in wb.worksheets}
22
- wb.close()
23
- # except Exception as e:
24
- # raise ExcelFileIOException("\n\nFatal to load excel file: \n" + reinsert_indent(str(e), '\t'))
25
- else:
26
- tbls = {}
27
-
28
- super().__init__(tbls)
29
-
30
- # 添加数字索引
31
- self.numbers = {i: tbl for i, (name, tbl) in enumerate(tbls.items())}
32
-
33
- def __getitem__(self, item):
34
- if isinstance(item, int):
35
- return self.numbers[item]
36
- return super().__getitem__(item)
37
-
38
- def __setitem__(self, key, value):
39
- if isinstance(key, int):
40
- key = list(self.keys())[key]
41
- super().__setitem__(key, value)
42
-
43
- def __delitem__(self, key):
44
- if isinstance(key, int):
45
- key = list(self.keys())[key]
46
- del self.numbers[key]
47
- super().__delitem__(key)
48
-
49
-
50
- def __table_from_sheet(self, sheet:Worksheet, only_data:bool=False):
51
- _construct_2d_list = []
52
- _merges = [_merge.coord for _merge in sheet.merged_cells.ranges]
53
-
54
- if only_data:
55
- _styles = None
56
- for row in sheet.iter_rows():
57
- _construct_2d_list.append([cell.value if cell.value else NaN for cell in row])
58
- else:
59
- _styles = []
60
- for row in sheet.iter_rows():
61
- _construct_2d_list.append([cell.value if cell.value else NaN for cell in row])
62
- _styles.append([Style(sheet, cell) for cell in row])
63
-
64
-
65
- return Table(_construct_2d_list, _styles, _merges, copy=False)
66
-
67
- @property
68
- def path(self):
69
- return self._path
70
-
71
- @property
72
- def tables(self):
73
- return list(self.values())
74
-
75
- @staticmethod
76
- def __simple_str(string, max:int=20):
77
- _len = len(string)
78
- assert max > 4, "Max length should be greater than 4."
79
- if _len > max:
80
- _half = max // 2 - 1
81
- return f"{string[:9]}...{string[_len-9:]}"
82
- return string
83
-
84
- def __repr__(self):
85
- return f"Excel({self.__simple_str(self._path)}|{len(self)} tables)"
86
-
87
- def __str__(self):
88
- _txt = f"Excel({self.__simple_str(self._path)}|{len(self)} tables)\t" + '{\n'
89
- _MAX_SIZE = 16 - 2
90
-
91
- # Max length key
92
- keys = [k for k in self.keys()]
93
- max_len_key = max([len(key) for key in keys])
94
- _MAX_SIZE = min(_MAX_SIZE, max_len_key) + 2
95
- max_len_key = max(max_len_key, _MAX_SIZE)
96
-
97
- for name, tbl in self.items():
98
- name = self.__simple_str(name, _MAX_SIZE)
99
- _left_space = ' ' * (max_len_key - len(name))
100
- _txt += f"\t'{name}'{_left_space}: {tbl.__repr__()},\n"
101
- _txt += '}'
102
- return _txt
103
-
104
- def append(self, tbl:Table, name:str=None):
105
- if name is None:
106
- name = f"sheet{len(self)}"
107
- while name in self:
108
- name = '_' + name
109
- self.numbers[len(self)] = tbl
110
- self[name] = tbl
111
- return tbl
112
-
113
-
114
- def detail(self) -> str:
115
- _txt = f"Excel({self.__simple_str(self._path)}|{len(self)} tables)\t" + '{\n'
116
- _MAX_SIZE = 16 - 2
117
-
118
- for name, tbl in self.items():
119
- _sub_txt = color_string(f"\tTable '{name}'") + " >\n" + reinsert_indent(str(tbl), '\t\t')
120
- _txt += _sub_txt + '\n\n'
121
-
122
- if len(self) > 0:
123
- _txt = _txt[:-1]
124
- _txt += '}'
125
-
126
- return _txt
127
-
128
- def save(self, fpath:str=None, *, style:bool=True):
129
- """
130
- *Save the Excel object to an excel file.
131
- :param fpath:
132
- *
133
- :param style: bool. If True, will save the style of the cell. Default is True.
134
- NOTE: if use, will slower 20X and more
135
- :return:
136
- """
137
- wb = Workbook()
138
- for name, tbl in self.items():
139
- ws = wb.create_sheet(name)
140
- for row in tbl:
141
- _row = []
142
- for value in row:
143
- # if nan, set to None
144
- if isnan(value):
145
- _row.append(None)
146
- else:
147
- _row.append(value)
148
- ws.append(_row)
149
-
150
- # Merge
151
- for merge in tbl.merges:
152
- ws.merge_cells(f"{merge[0].letter}:{merge[1].letter}")
153
-
154
- # Style
155
- if style and tbl.styles is not None:
156
- style_tbl = tbl.styles
157
- for i, row in enumerate(ws.iter_rows()):
158
- for j, cell in enumerate(row):
159
- style_tbl[i][j].apply(ws, cell)
160
-
161
- # remove the default sheet
162
- wb.remove(wb.active)
163
-
164
- if not fpath:
165
- fpath = self._path
166
- if not fpath:
167
- raise ExcelFileIOException("\n\nNo file path defined: " + str(self))
168
-
169
- try:
170
- wb.save(fpath)
171
- except Exception as e:
172
- raise ExcelFileIOException("\n\nFatal to save excel file: \n" + reinsert_indent(str(e), '\t'))
173
- wb.close()
174
-
175
-
176
- if __name__ == '__main__':
177
- from ffre import FileFinder
178
- fdir = r"C:\Users\22290\Desktop\20240504整理\tpltable 数据"
179
- ff = FileFinder(fdir)
180
- fpaths = list(ff.find(".xlsx"))
181
-
182
- if not fpaths:
183
- print("No excel file found.")
184
- exit(0)
185
-
186
- fp = fpaths[1]
187
- _t = TimeRecorder()
188
- for i in range(1):
189
- excel = Excel(fp, style=False)
190
- print("Load excel without style cost:", _t.dms(1), "ms")
191
- _t.tick()
192
- for i in range(1):
193
- excel = Excel(fp, style=True)
194
- print("Load excel with style cost:", _t.dms(1), "ms")
195
-
196
- excel[0].styles['A1'][TYPE_COL_WIDTH] = 200
197
-
198
- print(excel)
199
- _t.tick()
200
- excel.save("test.xlsx", style=False)
201
- print("Save excel without style cost:", _t.dms(1), "ms")
202
- _t.tick()
203
- excel.save("test_style.xlsx", style=True)
204
- print("Save excel with style cost:", _t.dms(1), "ms")
205
-
206
-
207
-
208
-
File without changes
File without changes
File without changes
File without changes
File without changes