feat(tables): add rows.add/remove + columns.add/remove (Tables 2.0 Phase 1)#37
Merged
Conversation
…ase 1) Phase 1 of issue #12 (Tables 2.0 epic). Lands the second-most-asked table feature in the python-pptx ecosystem — adding and removing rows and columns on existing tables (upstream issue scanny#86, 17 comments; closes the upstream PR scanny#399 design directly). Defers table-style binding (`apply_style`), merge ergonomics, and gridSpan accessor exposure to follow-up phases. New public API -------------- - `Table.rows.add(at=None, height=None) -> _Row` * `at=None` (default) appends at the bottom; backwards compatible. * `at=N` inserts at zero-based position N; `at=len(rows)` appends. * `height=None` inherits the first row's height (or 0.4" for an empty table). Pass an explicit `Length` to override. * Populates the new row with empty `<a:tc>` cells matching the current column count. - `Table.rows.remove(index)` * Drops the row at `index`. Raises `ValueError` when the row has a cell with `rowSpan > 1` (origin) or `vMerge=True` (target) — removing such a row would orphan the rest of a vertical merge. Split the affected merge first. - `Table.columns.add(at=None, width=None) -> _Column` * Mirrors `rows.add` semantics. Inserts an empty `<a:tc>` at the corresponding position in every existing row, preserving cell alignment. `width=None` inherits the leftmost column's width (or 1" for an empty table). - `Table.columns.remove(index)` * Drops the gridCol at `index` and the corresponding `<a:tc>` from every row. Raises `ValueError` on a column carrying `gridSpan > 1` or `hMerge=True`. - `_RowCollection.__iter__` and `_ColumnCollection.__iter__` are promoted to first-class iterators (existed implicitly before via `__getitem__`; explicit makes intent clear and matches Pythonic collection conventions). Internal additions ------------------ - `pptx/oxml/table.py`: * `CT_TableGrid.{insert_gridCol_at, remove_gridCol_at}` * `CT_Table.{insert_tr_at, remove_tr_at, column_has_cross_column_merge}` * `CT_TableRow.{insert_tc_at, remove_tc_at, has_cross_row_merge}` Merge-safety policy (Phase 1) ----------------------------- This phase deliberately raises `ValueError` rather than silently mutating the merge graph when row/column CRUD would split a merge: | Operation | Raises when | |--------------------------------------|--------------------------------------| | `rows.remove(idx)` | row contains `rowSpan>1` or `vMerge` | | `rows.add(at=idx)` | row at `idx` has any `vMerge=True` | | `columns.remove(idx)` | col has `gridSpan>1` or `hMerge` | | `columns.add(at=idx)` | col `idx` has any `hMerge=True` | Auto-splitting merges through CRUD is a Phase 2 enhancement. Test coverage ------------- - 55 new unit tests in `tests/test_tables_crud.py`: * 18 oxml-layer tests (insert/remove/cross-merge detection) * 32 collection-API tests (rows.add/remove + columns.add/remove) * 5 round-trip integration tests (open → mutate → save → reopen) - 7 new behave scenarios in `features/tbl-crud.feature` covering append-row, indexed-row-insert, append-column, remove-row, remove-column, and the two merge-safety raise cases. - New `uat_tables_crud.py` (untracked per repo §6) builds a 5-slide deck where each slide shows a different stage of CRUD operations, printing per-stage cell content so the maintainer can flip through in PowerPoint and see the mutations land. Verification ------------ ``` $ python3 -m pytest tests/ -q | tail -3 3277 passed in 4.54s $ ruff check src tests | tail -3 All checks passed! $ python3 -m behave features/ --no-color | tail -3 1006 scenarios passed, 0 failed, 0 skipped 3023 steps passed, 0 failed, 0 skipped ``` Refs #12
This was referenced May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tables 2.0 Phase 1 —
Table.rows.add/remove+Table.columns.add/removeRefs #12. Phase 1 of the Tables 2.0 epic. Lands the second-most-asked table feature in the python-pptx ecosystem — adding and removing rows and columns on existing tables (upstream issue scanny#86, 17 comments; closes the upstream PR scanny#399 design directly).
What this adds
API surface
Table.rows.add(at=None, height=None) -> _Rowat=None(default) appends; backwards compatible.at=Ninserts at zero-based position N.at=len(rows)appends.height=Noneinherits the first row's height (or 0.4″ for an empty table).<a:tc>cells matching the column count.Table.rows.remove(index)— drops the row atindex. RaisesValueErrorif the row contains a cell withrowSpan > 1(origin) orvMerge=True(target) — see merge-safety policy below.Table.columns.add(at=None, width=None) -> _Column— mirror ofrows.add. Inserts an empty<a:tc>at the corresponding position in every existing row, preserving cell alignment.Table.columns.remove(index)— drops the gridCol atindexand the corresponding<a:tc>from every row. Raises on cross-column merge._RowCollection.__iter__and_ColumnCollection.__iter__are promoted to first-class iterators (existed implicitly via__getitem__; explicit makes intent clear).Merge-safety policy (Phase 1)
This phase deliberately raises
ValueErrorrather than silently mutating the merge graph when row/column CRUD would split a merge:rows.remove(idx)rowSpan>1orvMergerows.add(at=idx)idxhas anyvMerge=Truecolumns.remove(idx)gridSpan>1orhMergecolumns.add(at=idx)idxhas anyhMerge=TrueAuto-splitting merges through CRUD is a Phase 2 enhancement — see issue #12 for the full list of follow-ups.
Out of scope (Phase 2/3 follow-ups per #12)
Table.apply_style(table_style_id_or_name)— built-in table style binding ([Epic] Tables 2.0 — row/col CRUD, table styles, advanced merge #12 sub-feature 1, the 37-comment ask)Table.merge_cells(row_range, col_range)— robust idempotent merge ergonomicsCell.gridSpan/rowSpan/hMerge/vMergeread-only accessorsTable.row_count/column_countergonomicsThese are tracked under #12 and would each be their own PR.
Internal additions
pptx/oxml/table.py:CT_TableGrid.{insert_gridCol_at, remove_gridCol_at}CT_Table.{insert_tr_at, remove_tr_at, column_has_cross_column_merge}CT_TableRow.{insert_tc_at, remove_tc_at, has_cross_row_merge}Test coverage
tests/test_tables_crud.py:features/tbl-crud.featurecovering append-row, indexed-row-insert, append-column, remove-row, remove-column, and the two merge-safety raise cases.uat_tables_crud.py(untracked per repo §6) builds a 5-slide deck where each slide shows a different stage of CRUD operations. UAT signoff: ✓ (note: stage-5 explicit-Inches labels are smaller than the autosized rows/cols above them — that's UAT-script labeling, not a feature issue; verified the values are correct).Verification