flexablelib 2.0.1__tar.gz → 2.0.3__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.
- {flexablelib-2.0.1 → flexablelib-2.0.3}/CHANGELOG.md +12 -0
- {flexablelib-2.0.1/flexablelib.egg-info → flexablelib-2.0.3}/PKG-INFO +1 -1
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/merges.py +26 -9
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/readers.py +37 -6
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/table.py +3 -1
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/writers.py +22 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3/flexablelib.egg-info}/PKG-INFO +1 -1
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib.egg-info/SOURCES.txt +1 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/package.json +1 -1
- flexablelib-2.0.3/tests/test_excel.py +110 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_merges.py +59 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/LICENSE +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/MANIFEST.in +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/README.md +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/__init__.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/_speedups.c +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/_width_tables.h +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/cells.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/colors.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/columns.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/fast.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/options.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/style_compositions.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/table_strings.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/text_width.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib/utils.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib.egg-info/dependency_links.txt +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib.egg-info/requires.txt +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/flexablelib.egg-info/top_level.txt +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/setup.cfg +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/setup.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_cells.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_columns.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_features.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_issues.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_readme_examples.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_styles.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_text_width.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_two_pass_measurement.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tests/test_upstream_issues.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tools/benchmark.py +0 -0
- {flexablelib-2.0.1 → flexablelib-2.0.3}/tools/generate_width_tables.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.0.3](https://github.com/Opus-Perpetuus/flexablelib/compare/v2.0.2...v2.0.3) (2026-10-28)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* ``to_excel`` / ``from_excel`` preserve merged cell ranges (openpyxl)
|
|
10
|
+
|
|
11
|
+
## [2.0.2](https://github.com/Opus-Perpetuus/flexablelib/compare/v2.0.1...v2.0.2) (2026-10-28)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* redraw vertical-merge vertices as edges/tees instead of leftover corners
|
|
16
|
+
|
|
5
17
|
## [2.0.1](https://github.com/Opus-Perpetuus/flexablelib/compare/v2.0.0...v2.0.1) (2026-10-28)
|
|
6
18
|
|
|
7
19
|
|
|
@@ -204,7 +204,12 @@ def _region_bounds(region, layout, index_offset):
|
|
|
204
204
|
return None
|
|
205
205
|
|
|
206
206
|
first, last = drawn[0], drawn[-1]
|
|
207
|
-
return (
|
|
207
|
+
return (
|
|
208
|
+
(layout.spans[first][0], layout.spans[last][1]),
|
|
209
|
+
layout.spans[first],
|
|
210
|
+
first,
|
|
211
|
+
last,
|
|
212
|
+
)
|
|
208
213
|
|
|
209
214
|
|
|
210
215
|
def apply(lines, row_line_map, regions, column_widths, composition,
|
|
@@ -235,7 +240,7 @@ def apply(lines, row_line_map, regions, column_widths, composition,
|
|
|
235
240
|
bounds = _region_bounds(region, layout, index_offset)
|
|
236
241
|
if bounds is None:
|
|
237
242
|
continue
|
|
238
|
-
(start, end), (cell_start, cell_end), last_span = bounds
|
|
243
|
+
(start, end), (cell_start, cell_end), first_span, last_span = bounds
|
|
239
244
|
span_width = end - start - margin * 2
|
|
240
245
|
if span_width < 1:
|
|
241
246
|
continue
|
|
@@ -277,13 +282,16 @@ def apply(lines, row_line_map, regions, column_widths, composition,
|
|
|
277
282
|
# Paint over the separator lines that fall inside the block, so the
|
|
278
283
|
# merged region reads as one cell top to bottom.
|
|
279
284
|
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
285
|
+
# Blanking the horizontal run removes the arms that fed each vertex.
|
|
286
|
+
# Rebuild those vertices from the style's own characters:
|
|
287
|
+
# - right edge, more columns: body left ('├' where '┼' was)
|
|
288
|
+
# - right edge, table border: vertical right ('│' where '┤' was)
|
|
289
|
+
# - left edge, prior columns: body right ('┤' where '┼' was)
|
|
290
|
+
# - left edge, table border: vertical left ('│' where '├' was)
|
|
284
291
|
body_rule = composition.table_body_line
|
|
285
|
-
|
|
292
|
+
v_lines = composition.vertical_table_body_lines
|
|
286
293
|
joins_more_columns = last_span + 1 < len(layout.spans)
|
|
294
|
+
joins_prev_columns = first_span > 0
|
|
287
295
|
|
|
288
296
|
covered = set(target_lines)
|
|
289
297
|
for line_index in range(min(target_lines), max(target_lines)):
|
|
@@ -293,8 +301,17 @@ def apply(lines, row_line_map, regions, column_widths, composition,
|
|
|
293
301
|
if visible_width(line) < end:
|
|
294
302
|
continue
|
|
295
303
|
rewritten = _repaint(line, start, end, blank)
|
|
296
|
-
if
|
|
297
|
-
|
|
304
|
+
if body_rule is not None or v_lines is not None:
|
|
305
|
+
if visible_width(rewritten) > end:
|
|
306
|
+
if joins_more_columns and body_rule is not None and body_rule.left:
|
|
307
|
+
rewritten = _repaint(rewritten, end, end + 1, body_rule.left)
|
|
308
|
+
elif not joins_more_columns and v_lines is not None and v_lines.right:
|
|
309
|
+
rewritten = _repaint(rewritten, end, end + 1, v_lines.right)
|
|
310
|
+
if start > 0:
|
|
311
|
+
if joins_prev_columns and body_rule is not None and body_rule.right:
|
|
312
|
+
rewritten = _repaint(rewritten, start - 1, start, body_rule.right)
|
|
313
|
+
elif not joins_prev_columns and v_lines is not None and v_lines.left:
|
|
314
|
+
rewritten = _repaint(rewritten, start - 1, start, v_lines.left)
|
|
298
315
|
lines[line_index] = rewritten
|
|
299
316
|
|
|
300
317
|
return lines
|
|
@@ -344,7 +344,8 @@ def from_excel(table_class, path, sheet=None, has_header: bool = True,
|
|
|
344
344
|
'Install it with: pip install flexablelib[excel]'
|
|
345
345
|
)
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
# read_only hides merged cell ranges; we need them to rebuild merges.
|
|
348
|
+
workbook = load_workbook(filename=path, read_only=False, data_only=True)
|
|
348
349
|
try:
|
|
349
350
|
if sheet is None:
|
|
350
351
|
worksheet = workbook.active
|
|
@@ -357,14 +358,44 @@ def from_excel(table_class, path, sheet=None, has_header: bool = True,
|
|
|
357
358
|
['' if cell is None else cell for cell in row]
|
|
358
359
|
for row in worksheet.iter_rows(values_only=True)
|
|
359
360
|
]
|
|
361
|
+
merge_ranges = [
|
|
362
|
+
(merged.min_row, merged.min_col, merged.max_row, merged.max_col)
|
|
363
|
+
for merged in worksheet.merged_cells.ranges
|
|
364
|
+
]
|
|
360
365
|
finally:
|
|
361
366
|
workbook.close()
|
|
362
367
|
|
|
363
368
|
if not rows:
|
|
364
369
|
return table_class(**options)
|
|
365
370
|
if has_header:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
+
table = _new_table(table_class, rows[0], rows[1:], parse_numbers,
|
|
372
|
+
**options)
|
|
373
|
+
header_rows = 1
|
|
374
|
+
else:
|
|
375
|
+
width = max(len(row) for row in rows)
|
|
376
|
+
headers = [f'column {i + 1}' for i in range(width)]
|
|
377
|
+
table = _new_table(table_class, headers, rows, parse_numbers, **options)
|
|
378
|
+
header_rows = 0
|
|
379
|
+
|
|
380
|
+
# Excel rows/cols are 1-based. Body rows start after the header row when
|
|
381
|
+
# has_header is set. Skip merges that only cover the header.
|
|
382
|
+
for min_row, min_col, max_row, max_col in merge_ranges:
|
|
383
|
+
# Header-only or header-spanning merges are not table body merges.
|
|
384
|
+
if min_row <= header_rows:
|
|
385
|
+
continue
|
|
386
|
+
first_row = min_row - header_rows - 1
|
|
387
|
+
last_row = max_row - header_rows - 1
|
|
388
|
+
first_column = min_col - 1
|
|
389
|
+
last_column = max_col - 1
|
|
390
|
+
if first_row == last_row and first_column == last_column:
|
|
391
|
+
continue
|
|
392
|
+
if last_row < 0 or last_column < 0:
|
|
393
|
+
continue
|
|
394
|
+
if last_row >= table.row_count or last_column >= table.column_count:
|
|
395
|
+
continue
|
|
396
|
+
try:
|
|
397
|
+
table.merge_cells(first_row, first_column, last_row, last_column)
|
|
398
|
+
except ValueError:
|
|
399
|
+
# Overlaps or single cells from odd sheets: skip rather than fail.
|
|
400
|
+
continue
|
|
401
|
+
return table
|
|
@@ -4127,6 +4127,7 @@ class Table(object):
|
|
|
4127
4127
|
"""
|
|
4128
4128
|
Build a table from a worksheet in an .xlsx file.
|
|
4129
4129
|
|
|
4130
|
+
Excel merged ranges are restored as ``merge_cells`` regions.
|
|
4130
4131
|
Requires openpyxl: pip install flexablelib[excel]
|
|
4131
4132
|
"""
|
|
4132
4133
|
from .readers import from_excel
|
|
@@ -4195,7 +4196,8 @@ class Table(object):
|
|
|
4195
4196
|
Write an .xlsx workbook.
|
|
4196
4197
|
|
|
4197
4198
|
Numbers are written as numbers so the spreadsheet can compute with
|
|
4198
|
-
them. Requires openpyxl:
|
|
4199
|
+
them. ``merged_regions`` become real Excel merges. Requires openpyxl:
|
|
4200
|
+
pip install flexablelib[excel]
|
|
4199
4201
|
"""
|
|
4200
4202
|
from .writers import to_excel
|
|
4201
4203
|
return to_excel(self, path, sheet_name, include_index, autofit,
|
|
@@ -557,4 +557,26 @@ def to_excel(table, path, sheet_name: str = 'Sheet1',
|
|
|
557
557
|
if freeze_header:
|
|
558
558
|
worksheet.freeze_panes = 'A2'
|
|
559
559
|
|
|
560
|
+
# Mirror flexablelib merged regions as Excel merged cells. Table coordinates
|
|
561
|
+
# ignore the optional index column and the header row lives on Excel row 1.
|
|
562
|
+
col_offset = 2 if include_index else 1
|
|
563
|
+
for region in getattr(table, 'merged_regions', []):
|
|
564
|
+
start_row = region.first_row + 2
|
|
565
|
+
end_row = region.last_row + 2
|
|
566
|
+
start_col = region.first_column + col_offset
|
|
567
|
+
end_col = region.last_column + col_offset
|
|
568
|
+
top_left = worksheet.cell(row=start_row, column=start_col)
|
|
569
|
+
if region.value is not None:
|
|
570
|
+
top_left.value = _plain(region.value)
|
|
571
|
+
top_left.alignment = Alignment(
|
|
572
|
+
horizontal={'l': 'left', 'r': 'right', 'c': 'center'}.get(
|
|
573
|
+
region.align, 'center'),
|
|
574
|
+
vertical='center',
|
|
575
|
+
wrap_text=True,
|
|
576
|
+
)
|
|
577
|
+
worksheet.merge_cells(
|
|
578
|
+
start_row=start_row, start_column=start_col,
|
|
579
|
+
end_row=end_row, end_column=end_col,
|
|
580
|
+
)
|
|
581
|
+
|
|
560
582
|
workbook.save(path)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Excel import / export.
|
|
3
|
+
|
|
4
|
+
Round-trips plain tables and merged regions through openpyxl workbooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from flexablelib import Table
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
pytest.importorskip('openpyxl')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_table():
|
|
18
|
+
table = Table()
|
|
19
|
+
table.add_column('Region', ['Norte', 'Norte', 'Sur', 'Sur'])
|
|
20
|
+
table.add_column('Month', ['Jan', 'Feb', 'Jan', 'Feb'])
|
|
21
|
+
table.add_column('Sales', [120.5, 98.25, 210.0, 175.75])
|
|
22
|
+
return table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_to_excel_and_from_excel_round_trip_values(tmp_path):
|
|
26
|
+
path = tmp_path / 'plain.xlsx'
|
|
27
|
+
table = build_table()
|
|
28
|
+
table.to_excel(path)
|
|
29
|
+
|
|
30
|
+
loaded = Table.from_excel(path)
|
|
31
|
+
|
|
32
|
+
assert loaded.headers == ['Region', 'Month', 'Sales']
|
|
33
|
+
assert loaded.rows[0][0] == 'Norte'
|
|
34
|
+
assert loaded.rows[0][2] == 120.5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_to_excel_writes_merged_cells(tmp_path):
|
|
38
|
+
from openpyxl import load_workbook
|
|
39
|
+
|
|
40
|
+
path = tmp_path / 'merged.xlsx'
|
|
41
|
+
table = build_table()
|
|
42
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
43
|
+
table.merge_cells(0, 1, last_column=2, value='Q1')
|
|
44
|
+
table.to_excel(path)
|
|
45
|
+
|
|
46
|
+
workbook = load_workbook(path)
|
|
47
|
+
ranges = {str(r) for r in workbook.active.merged_cells.ranges}
|
|
48
|
+
workbook.close()
|
|
49
|
+
|
|
50
|
+
assert 'A2:A3' in ranges
|
|
51
|
+
assert 'B2:C2' in ranges
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_from_excel_restores_merged_regions(tmp_path):
|
|
55
|
+
path = tmp_path / 'round.xlsx'
|
|
56
|
+
table = build_table()
|
|
57
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
58
|
+
table.merge_cells(2, 0, last_column=1, value='Sur block')
|
|
59
|
+
table.to_excel(path)
|
|
60
|
+
|
|
61
|
+
loaded = Table.from_excel(path)
|
|
62
|
+
|
|
63
|
+
assert len(loaded.merged_regions) == 2
|
|
64
|
+
regions = {(r.first_row, r.first_column, r.last_row, r.last_column)
|
|
65
|
+
for r in loaded.merged_regions}
|
|
66
|
+
assert (0, 0, 1, 0) in regions
|
|
67
|
+
assert (2, 0, 2, 1) in regions
|
|
68
|
+
assert 'Norte' in str(loaded)
|
|
69
|
+
assert 'Sur block' in str(loaded)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_to_excel_shifts_merges_when_index_included(tmp_path):
|
|
73
|
+
from openpyxl import load_workbook
|
|
74
|
+
|
|
75
|
+
path = tmp_path / 'indexed.xlsx'
|
|
76
|
+
table = build_table()
|
|
77
|
+
table.show_index = True
|
|
78
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
79
|
+
table.to_excel(path, include_index=True)
|
|
80
|
+
|
|
81
|
+
workbook = load_workbook(path)
|
|
82
|
+
ranges = {str(r) for r in workbook.active.merged_cells.ranges}
|
|
83
|
+
workbook.close()
|
|
84
|
+
|
|
85
|
+
# Index column is A; body Region is B → B2:B3
|
|
86
|
+
assert 'B2:B3' in ranges
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_from_excel_sheet_selection(tmp_path):
|
|
90
|
+
from openpyxl import Workbook
|
|
91
|
+
|
|
92
|
+
path = tmp_path / 'multi.xlsx'
|
|
93
|
+
workbook = Workbook()
|
|
94
|
+
first = workbook.active
|
|
95
|
+
first.title = 'One'
|
|
96
|
+
first.append(['A'])
|
|
97
|
+
first.append([1])
|
|
98
|
+
second = workbook.create_sheet('Two')
|
|
99
|
+
second.append(['X', 'Y'])
|
|
100
|
+
second.append(['x1', 'y1'])
|
|
101
|
+
second.append(['x2', 'y2'])
|
|
102
|
+
second.merge_cells('A2:A3')
|
|
103
|
+
workbook.save(path)
|
|
104
|
+
workbook.close()
|
|
105
|
+
|
|
106
|
+
loaded = Table.from_excel(path, sheet='Two')
|
|
107
|
+
assert loaded.headers == ['X', 'Y']
|
|
108
|
+
assert len(loaded.merged_regions) == 1
|
|
109
|
+
region = loaded.merged_regions[0]
|
|
110
|
+
assert (region.first_row, region.first_column, region.last_row, region.last_column) == (0, 0, 1, 0)
|
|
@@ -51,6 +51,65 @@ def test_a_vertical_merge_removes_the_rules_between_its_rows():
|
|
|
51
51
|
assert rendered.count('Norte') == 1
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
|
|
55
|
+
def test_vertical_merge_turns_left_border_into_an_edge():
|
|
56
|
+
"""Vertices on a body rule inside a vertical merge must become edges.
|
|
57
|
+
|
|
58
|
+
The horizontal arms into the merged cell are gone, so a left-border
|
|
59
|
+
'├' becomes the vertical edge '│', and the right junction keeps the
|
|
60
|
+
three-way '├' that opens the remaining columns.
|
|
61
|
+
"""
|
|
62
|
+
table = build_table()
|
|
63
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
64
|
+
|
|
65
|
+
sep = next(
|
|
66
|
+
line for line in str(table).splitlines()
|
|
67
|
+
if '├───────┼────────┤' in line or line.startswith('│ ├')
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert sep.startswith('│')
|
|
71
|
+
assert '├───────┼────────┤' in sep
|
|
72
|
+
assert not sep.startswith('├')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_vertical_merge_in_middle_column_uses_left_and_right_tees():
|
|
76
|
+
table = build_table()
|
|
77
|
+
table.merge_cells(0, 1, last_row=1, value='M')
|
|
78
|
+
|
|
79
|
+
sep = next(line for line in str(table).splitlines() if ' ' in line and '┤' in line and line.count('├') >= 1)
|
|
80
|
+
|
|
81
|
+
assert '├────────┤ ├────────┤' in sep
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_vertical_merge_on_last_column_turns_right_border_into_an_edge():
|
|
85
|
+
table = build_table()
|
|
86
|
+
table.merge_cells(0, 2, last_row=1, value='S')
|
|
87
|
+
|
|
88
|
+
sep = next(line for line in str(table).splitlines() if line.endswith('│') and '┤ │' in line)
|
|
89
|
+
|
|
90
|
+
assert sep.endswith('│')
|
|
91
|
+
assert '├────────┼───────┤ │' in sep
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_vertical_merge_vertices_follow_grid_ascii_style():
|
|
95
|
+
table = build_table('grid')
|
|
96
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
97
|
+
|
|
98
|
+
sep = next(line for line in str(table).splitlines() if '+-------+' in line and line.startswith('|'))
|
|
99
|
+
|
|
100
|
+
assert sep.startswith('| +')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_vertical_merge_vertices_follow_bold_borderline_style():
|
|
104
|
+
table = build_table('bold_borderline')
|
|
105
|
+
table.merge_cells(0, 0, last_row=1, value='Norte')
|
|
106
|
+
|
|
107
|
+
sep = next(line for line in str(table).splitlines() if '╟───────┼────────╢' in line)
|
|
108
|
+
|
|
109
|
+
assert sep.startswith('║')
|
|
110
|
+
assert '║ ╟───────┼────────╢' in sep
|
|
111
|
+
|
|
112
|
+
|
|
54
113
|
def test_a_block_merge_covers_both_directions():
|
|
55
114
|
table = build_table()
|
|
56
115
|
table.merge_cells(0, 0, 1, 1, value='Block')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|