Skip to content

feat(tables): add rows.add/remove + columns.add/remove (Tables 2.0 Phase 1)#37

Merged
MHoroszowski merged 1 commit into
masterfrom
feature/tables-phase1
May 8, 2026
Merged

feat(tables): add rows.add/remove + columns.add/remove (Tables 2.0 Phase 1)#37
MHoroszowski merged 1 commit into
masterfrom
feature/tables-phase1

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Tables 2.0 Phase 1 — Table.rows.add/remove + Table.columns.add/remove

Refs #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

table = shape.table

# append a row at the bottom (backwards compatible)
new_row = table.rows.add()

# insert a row at a specific position
new_row = table.rows.add(at=2)

# explicit height
new_row = table.rows.add(height=Inches(0.8))

# remove a row
table.rows.remove(2)

# same patterns for columns
new_col = table.columns.add()
new_col = table.columns.add(at=1, width=Inches(1.5))
table.columns.remove(0)

API surface

  • Table.rows.add(at=None, height=None) -> _Row

    • at=None (default) appends; 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).
    • Populates the new row with empty <a:tc> cells matching the column count.
  • Table.rows.remove(index) — drops the row at index. Raises ValueError if the row contains a cell with rowSpan > 1 (origin) or vMerge=True (target) — see merge-safety policy below.

  • Table.columns.add(at=None, width=None) -> _Column — mirror of rows.add. Inserts an empty <a:tc> at the corresponding position in every existing row, preserving cell alignment.

  • Table.columns.remove(index) — drops the gridCol at index and 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 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 — 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 ergonomics
  • Cell.gridSpan / rowSpan / hMerge / vMerge read-only accessors
  • Per-row height / per-column width getter/setter improvements
  • Table.row_count / column_count ergonomics

These 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

  • 55 new unit tests in tests/test_tables_crud.py:
    • 18 oxml-layer tests (insert/remove/cross-merge detection)
    • 32 collection-API tests
    • 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.
  • UAT script 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

$ 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

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant