python-calamine-reducto 0.7.14__tar.gz → 0.7.15__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.
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/Cargo.lock +1 -1
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/Cargo.toml +1 -1
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/PKG-INFO +1 -1
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/workbook.rs +82 -12
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_band.py +86 -3
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.github/workflows/CI.yml +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.github/workflows/release.yml +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.gitignore +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.pre-commit-config.yaml +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.python-version +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/CLAUDE.md +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/LICENSE +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/README.md +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/build.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/pyproject.toml +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/python/python_calamine/__init__.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/python/python_calamine/_python_calamine.pyi +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/python/python_calamine/py.typed +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/renovate.json +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/lib.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/cell.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/errors.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/grid.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/mod.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/sheet.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/src/types/table.rs +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/__init__.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/conftest.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/create_empty_merge_fixture.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/create_richtext_fixture.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/create_test_fixture.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.ods +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.xls +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/base.ods +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/base.xls +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/base.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/base.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/dropdown_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_file.ods +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_file.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_file.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_merge_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/formula_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/grid_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/issue139.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/multi_image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/password.ods +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/password.xls +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/password.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/password.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/richtext_test.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/table-multiple.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/table-multiple.xlsx +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/temperature-table.xlsb +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_base.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_grid.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_images.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_pandas.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_sheet_type.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/test_tables.py +0 -0
- {python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/uv.lock +0 -0
|
@@ -700,6 +700,7 @@ impl CalamineWorkbook {
|
|
|
700
700
|
trigger_cells=2_000_000,
|
|
701
701
|
trigger_density=0.001,
|
|
702
702
|
coverage=0.999,
|
|
703
|
+
propagate_merges=false,
|
|
703
704
|
))]
|
|
704
705
|
fn py_for_each_band(
|
|
705
706
|
&mut self,
|
|
@@ -711,6 +712,7 @@ impl CalamineWorkbook {
|
|
|
711
712
|
trigger_cells: usize,
|
|
712
713
|
trigger_density: f64,
|
|
713
714
|
coverage: f64,
|
|
715
|
+
propagate_merges: bool,
|
|
714
716
|
) -> PyResult<(usize, usize, u64)> {
|
|
715
717
|
self.for_each_band(
|
|
716
718
|
py,
|
|
@@ -721,6 +723,7 @@ impl CalamineWorkbook {
|
|
|
721
723
|
trigger_cells,
|
|
722
724
|
trigger_density,
|
|
723
725
|
coverage,
|
|
726
|
+
propagate_merges,
|
|
724
727
|
)
|
|
725
728
|
}
|
|
726
729
|
|
|
@@ -792,6 +795,36 @@ impl CalamineWorkbook {
|
|
|
792
795
|
Ok((fp.frozen_rows, fp.frozen_cols))
|
|
793
796
|
}
|
|
794
797
|
|
|
798
|
+
/// Merged cell ranges as absolute 0-based `(start_row, start_col, end_row,
|
|
799
|
+
/// end_col)`. Cheap (bounded by the merge count, not the cell count) and does
|
|
800
|
+
/// not materialize a grid — lets a `for_each_band(propagate_merges=True)`
|
|
801
|
+
/// caller know each merge's anchor so it can carry the anchor's formatted
|
|
802
|
+
/// value across bands. Empty for non-xlsx workbooks without merges.
|
|
803
|
+
#[pyo3(name = "get_sheet_merged_ranges")]
|
|
804
|
+
fn py_get_sheet_merged_ranges(
|
|
805
|
+
&mut self,
|
|
806
|
+
py: Python<'_>,
|
|
807
|
+
name: &str,
|
|
808
|
+
) -> PyResult<Vec<(usize, usize, usize, usize)>> {
|
|
809
|
+
py.detach(|| -> PyResult<Vec<(usize, usize, usize, usize)>> {
|
|
810
|
+
let ranges = match self.sheets.worksheet_merge_cells(name)? {
|
|
811
|
+
Some(ranges) => ranges,
|
|
812
|
+
None => return Ok(Vec::new()),
|
|
813
|
+
};
|
|
814
|
+
Ok(ranges
|
|
815
|
+
.iter()
|
|
816
|
+
.map(|dim| {
|
|
817
|
+
(
|
|
818
|
+
dim.start.0 as usize,
|
|
819
|
+
dim.start.1 as usize,
|
|
820
|
+
dim.end.0 as usize,
|
|
821
|
+
dim.end.1 as usize,
|
|
822
|
+
)
|
|
823
|
+
})
|
|
824
|
+
.collect())
|
|
825
|
+
})
|
|
826
|
+
}
|
|
827
|
+
|
|
795
828
|
/// Compute filled bounds for a sheet entirely in Rust.
|
|
796
829
|
#[pyo3(name = "get_sheet_bounds")]
|
|
797
830
|
fn py_get_sheet_bounds(
|
|
@@ -1107,6 +1140,16 @@ impl CalamineWorkbook {
|
|
|
1107
1140
|
///
|
|
1108
1141
|
/// The callback must not call back into this workbook (its cell reader is
|
|
1109
1142
|
/// borrowed for the duration); fetch formulas/layout/etc. beforehand.
|
|
1143
|
+
///
|
|
1144
|
+
/// With `propagate_merges`, a band's merge map keeps every merge that
|
|
1145
|
+
/// intersects it (including children whose anchor is in an earlier band) and
|
|
1146
|
+
/// stores the anchor at its **absolute** coordinate, so each band's
|
|
1147
|
+
/// `is_merged`/`merge_parent`/`coord` are byte-identical to the same row
|
|
1148
|
+
/// range of the dense grid. The merged *value* still propagates only within a
|
|
1149
|
+
/// band here (the anchor's formatted value is not known at stream time); the
|
|
1150
|
+
/// caller carries it across bands. Without the flag, the legacy behavior
|
|
1151
|
+
/// applies: band-local anchors, dropping cross-band merges (matching
|
|
1152
|
+
/// `CellGrid::slice_copy`).
|
|
1110
1153
|
fn for_each_band(
|
|
1111
1154
|
&mut self,
|
|
1112
1155
|
py: Python<'_>,
|
|
@@ -1117,6 +1160,7 @@ impl CalamineWorkbook {
|
|
|
1117
1160
|
trigger_cells: usize,
|
|
1118
1161
|
trigger_density: f64,
|
|
1119
1162
|
coverage: f64,
|
|
1163
|
+
propagate_merges: bool,
|
|
1120
1164
|
) -> PyResult<(usize, usize, u64)> {
|
|
1121
1165
|
if band_rows == 0 {
|
|
1122
1166
|
return Err(PyValueError::new_err("band_rows must be >= 1"));
|
|
@@ -1151,7 +1195,8 @@ impl CalamineWorkbook {
|
|
|
1151
1195
|
}
|
|
1152
1196
|
}
|
|
1153
1197
|
|
|
1154
|
-
let mut acc =
|
|
1198
|
+
let mut acc =
|
|
1199
|
+
BandAccumulator::new(rows, cols, band_rows, include_styles, merges, propagate_merges);
|
|
1155
1200
|
let mut cb_err: Option<PyErr> = None;
|
|
1156
1201
|
self.sheets.for_each_cell(name, |(r, c), dref, style| {
|
|
1157
1202
|
if cb_err.is_some() {
|
|
@@ -1353,6 +1398,9 @@ struct BandAccumulator {
|
|
|
1353
1398
|
include_styles: bool,
|
|
1354
1399
|
/// Merged ranges clipped to the trimmed bounds, absolute 0-based (sr, sc, er, ec).
|
|
1355
1400
|
merges: Vec<(usize, usize, usize, usize)>,
|
|
1401
|
+
/// Keep cross-band merges with absolute anchors (dense-slice parity) instead
|
|
1402
|
+
/// of the legacy band-local + drop-cross-band behavior.
|
|
1403
|
+
propagate_merges: bool,
|
|
1356
1404
|
/// Absolute first row of the in-progress band.
|
|
1357
1405
|
top: usize,
|
|
1358
1406
|
raw: Vec<RawValue>,
|
|
@@ -1367,6 +1415,7 @@ impl BandAccumulator {
|
|
|
1367
1415
|
band_rows: usize,
|
|
1368
1416
|
include_styles: bool,
|
|
1369
1417
|
merges: Vec<(usize, usize, usize, usize)>,
|
|
1418
|
+
propagate_merges: bool,
|
|
1370
1419
|
) -> Self {
|
|
1371
1420
|
let h = band_rows.min(rows);
|
|
1372
1421
|
BandAccumulator {
|
|
@@ -1375,6 +1424,7 @@ impl BandAccumulator {
|
|
|
1375
1424
|
cols,
|
|
1376
1425
|
include_styles,
|
|
1377
1426
|
merges,
|
|
1427
|
+
propagate_merges,
|
|
1378
1428
|
top: 0,
|
|
1379
1429
|
raw: vec![RawValue::Empty; h * cols],
|
|
1380
1430
|
styles: if include_styles {
|
|
@@ -1463,20 +1513,40 @@ impl BandAccumulator {
|
|
|
1463
1513
|
Ok(())
|
|
1464
1514
|
}
|
|
1465
1515
|
|
|
1466
|
-
///
|
|
1467
|
-
///
|
|
1468
|
-
///
|
|
1516
|
+
/// Merge map for the in-progress band. Keys are band-local; values are the
|
|
1517
|
+
/// merge anchor's coordinate.
|
|
1518
|
+
///
|
|
1519
|
+
/// With `propagate_merges`, every merge that intersects the band is kept and
|
|
1520
|
+
/// the anchor is stored at its **absolute** coordinate — even when the anchor
|
|
1521
|
+
/// lies in an earlier band — so `is_merged`/`merge_parent` match a dense slice
|
|
1522
|
+
/// of the same rows (whose anchors are likewise absolute and may point above
|
|
1523
|
+
/// the slice). Otherwise the legacy behavior applies: the anchor is remapped
|
|
1524
|
+
/// band-local and merges anchored above the band are dropped, matching
|
|
1525
|
+
/// `CellGrid::slice_copy`.
|
|
1469
1526
|
fn band_merge_map(&self, h: usize) -> HashMap<(usize, usize), (i32, i32)> {
|
|
1470
1527
|
let mut merge_map = HashMap::new();
|
|
1528
|
+
let band_bottom = self.top + h - 1;
|
|
1471
1529
|
for &(sr, sc, er, ec) in &self.merges {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1530
|
+
if self.propagate_merges {
|
|
1531
|
+
if er < self.top || sr > band_bottom || sc >= self.cols {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
let anchor = (sr as i32, sc as i32);
|
|
1535
|
+
for r in sr.max(self.top)..=er.min(band_bottom) {
|
|
1536
|
+
for c in sc..=ec.min(self.cols - 1) {
|
|
1537
|
+
merge_map.insert((r - self.top, c), anchor);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
} else {
|
|
1541
|
+
let parent_r = sr as i32 - self.top as i32;
|
|
1542
|
+
let parent_c = sc as i32;
|
|
1543
|
+
if parent_r < 0 || parent_r as usize >= h || parent_c as usize >= self.cols {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
for r in sr.max(self.top)..=er.min(band_bottom) {
|
|
1547
|
+
for c in sc..=ec {
|
|
1548
|
+
merge_map.insert((r - self.top, c), (parent_r, parent_c));
|
|
1549
|
+
}
|
|
1480
1550
|
}
|
|
1481
1551
|
}
|
|
1482
1552
|
}
|
|
@@ -32,10 +32,14 @@ def _xml_escape(v: str) -> str:
|
|
|
32
32
|
return v.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _make_xlsx(
|
|
35
|
+
def _make_xlsx(
|
|
36
|
+
rows_in_order: list[tuple[int, list[tuple[int, object]]]],
|
|
37
|
+
merges: list[str] | None = None,
|
|
38
|
+
) -> str:
|
|
36
39
|
"""Write a minimal single-sheet xlsx (stdlib only — no openpyxl) and return
|
|
37
40
|
its path. `rows_in_order` is [(row_1based, [(col_1based, value), ...]), ...]
|
|
38
|
-
emitted in the given order, so callers can produce out-of-order rows.
|
|
41
|
+
emitted in the given order, so callers can produce out-of-order rows.
|
|
42
|
+
`merges` is an optional list of A1-style ranges (e.g. "A2:A5")."""
|
|
39
43
|
cells_xml = []
|
|
40
44
|
for r, cols in rows_in_order:
|
|
41
45
|
cs = []
|
|
@@ -48,10 +52,14 @@ def _make_xlsx(rows_in_order: list[tuple[int, list[tuple[int, object]]]]) -> str
|
|
|
48
52
|
else:
|
|
49
53
|
cs.append(f'<c r="{ref}"><v>{value}</v></c>')
|
|
50
54
|
cells_xml.append(f'<row r="{r}">{"".join(cs)}</row>')
|
|
55
|
+
merge_xml = ""
|
|
56
|
+
if merges:
|
|
57
|
+
refs = "".join(f'<mergeCell ref="{m}"/>' for m in merges)
|
|
58
|
+
merge_xml = f'<mergeCells count="{len(merges)}">{refs}</mergeCells>'
|
|
51
59
|
sheet = (
|
|
52
60
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
53
61
|
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
|
54
|
-
f'<sheetData>{"".join(cells_xml)}</sheetData
|
|
62
|
+
f'<sheetData>{"".join(cells_xml)}</sheetData>{merge_xml}</worksheet>'
|
|
55
63
|
)
|
|
56
64
|
content_types = (
|
|
57
65
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
@@ -175,6 +183,81 @@ def test_band_merges_match_dense_within_band() -> None:
|
|
|
175
183
|
assert list(band[r, c]["merge_parent"]) == list(dense[r, c]["merge_parent"])
|
|
176
184
|
|
|
177
185
|
|
|
186
|
+
def _make_cross_band_merge_xlsx() -> str:
|
|
187
|
+
"""6×4 sheet with two merges that both straddle a band boundary for any band
|
|
188
|
+
height < 4: a single-column vertical merge A2:A5 (anchor in row 2) and a
|
|
189
|
+
multi-column block merge C2:D4 (anchor in row 2, exercising the column loop)."""
|
|
190
|
+
return _make_xlsx(
|
|
191
|
+
[
|
|
192
|
+
(1, [(1, "h1"), (2, "h2"), (3, "h3"), (4, "h4")]),
|
|
193
|
+
(2, [(1, "Vert"), (3, "Block")]),
|
|
194
|
+
(3, [(2, "x3")]),
|
|
195
|
+
(4, [(2, "x4")]),
|
|
196
|
+
(5, [(2, "x5")]),
|
|
197
|
+
(6, [(1, "tail"), (2, "x6")]),
|
|
198
|
+
],
|
|
199
|
+
merges=["A2:A5", "C2:D4"],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.mark.parametrize("band_rows", [1, 2, 3, 4, 6])
|
|
204
|
+
def test_band_merges_match_dense_cross_band(band_rows: int) -> None:
|
|
205
|
+
"""With propagate_merges, a merge spanning a band boundary reproduces the
|
|
206
|
+
dense is_merged/merge_parent/coord in every band — including children whose
|
|
207
|
+
anchor lives in an earlier band (the anchor stays at its absolute coord, as
|
|
208
|
+
in a dense slice of the same rows)."""
|
|
209
|
+
wb = CalamineWorkbook.from_path(_make_cross_band_merge_xlsx())
|
|
210
|
+
name = wb.sheet_names[0]
|
|
211
|
+
|
|
212
|
+
dense, _, _, _ = wb.get_sheet_grid_streaming(name)
|
|
213
|
+
rows, cols = dense.shape
|
|
214
|
+
|
|
215
|
+
_, bands = _collect_bands(wb, name, band_rows, propagate_merges=True)
|
|
216
|
+
assert [top for top, _ in bands] == list(range(0, rows, band_rows))
|
|
217
|
+
|
|
218
|
+
for top, band in bands:
|
|
219
|
+
bh, _ = band.shape
|
|
220
|
+
for br in range(bh):
|
|
221
|
+
for c in range(cols):
|
|
222
|
+
assert band[br, c]["is_merged"] == dense[top + br, c]["is_merged"]
|
|
223
|
+
assert list(band[br, c]["merge_parent"]) == list(
|
|
224
|
+
dense[top + br, c]["merge_parent"]
|
|
225
|
+
)
|
|
226
|
+
assert list(band[br, c]["coord"]) == [top + br + 1, c + 1]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_band_merges_legacy_drops_cross_band() -> None:
|
|
230
|
+
"""Without propagate_merges (default), a child whose anchor is in an earlier
|
|
231
|
+
band is dropped from the merge map — legacy CellGrid::slice_copy parity."""
|
|
232
|
+
wb = CalamineWorkbook.from_path(_make_cross_band_merge_xlsx())
|
|
233
|
+
name = wb.sheet_names[0]
|
|
234
|
+
|
|
235
|
+
_, bands = _collect_bands(wb, name, 2)
|
|
236
|
+
# band1 = rows 2–3 (0-based); A3/A4 are merge children whose anchor (row 1)
|
|
237
|
+
# is in band0, so the legacy path drops them.
|
|
238
|
+
top1, band1 = bands[1]
|
|
239
|
+
assert top1 == 2
|
|
240
|
+
assert not band1[0, 0]["is_merged"]
|
|
241
|
+
assert not band1[1, 0]["is_merged"]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_merged_ranges_reports_absolute_bounds() -> None:
|
|
245
|
+
"""get_sheet_merged_ranges returns each merge as absolute 0-based
|
|
246
|
+
(start_row, start_col, end_row, end_col), so a propagate_merges caller can
|
|
247
|
+
find every anchor without materializing the grid."""
|
|
248
|
+
wb = CalamineWorkbook.from_path(_make_cross_band_merge_xlsx())
|
|
249
|
+
name = wb.sheet_names[0]
|
|
250
|
+
ranges = set(wb.get_sheet_merged_ranges(name))
|
|
251
|
+
# A2:A5 -> rows 1-4, col 0; C2:D4 -> rows 1-3, cols 2-3.
|
|
252
|
+
assert ranges == {(1, 0, 4, 0), (1, 2, 3, 3)}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_merged_ranges_empty_when_no_merges() -> None:
|
|
256
|
+
wb = CalamineWorkbook.from_path(_make_xlsx([(1, [(1, "a")])]))
|
|
257
|
+
name = wb.sheet_names[0]
|
|
258
|
+
assert wb.get_sheet_merged_ranges(name) == []
|
|
259
|
+
|
|
260
|
+
|
|
178
261
|
def test_filled_count_matches_dense_grid() -> None:
|
|
179
262
|
"""get_sheet_filled_count reports the non-empty cell count + filled bounds
|
|
180
263
|
without materializing a grid, so callers can route on it up front."""
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/.github/workflows/release.yml
RENAMED
|
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
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/python/python_calamine/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/python/python_calamine/py.typed
RENAMED
|
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
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/create_richtext_fixture.py
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/create_test_fixture.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/any_sheets.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/dropdown_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_file.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_file.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/empty_merge_test.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/formula_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/image_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/multi_image_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/richtext_test.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/table-multiple.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/table-multiple.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.14 → python_calamine_reducto-0.7.15}/tests/data/temperature-table.xlsb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|