Skip to content

feat(tables): table style API — apply_style, style_id, style_name (Tables 2.0 Phase 2)#40

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

feat(tables): table style API — apply_style, style_id, style_name (Tables 2.0 Phase 2)#40
MHoroszowski merged 1 commit into
masterfrom
feature/tables-phase2

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Phase 2 of issue #12 (Tables 2.0): table style API

Closes the next-largest gap on issue #12 and the most-cited scanny#27 ask (37 comments) — a public way to apply or read a table's built-in PowerPoint style after the table is created.

Phase 1 (PR #37) shipped row/column add/remove. Before this PR, the default style GUID was hardcoded inside _tbl_tmpl() as a string template ({5C22544A-...}, "Medium Style 2 - Accent 1") with no element class on the oxml side, no public getter/setter on Table, and no name registry — so the existing boolean toggles (first_row, last_row, horz_banding, vert_banding) silently bound to a default the user couldn't observe or change.

Public surface added

from pptx.enum.table import PP_TABLE_STYLE, lookup_table_style, style_name_for, register_table_style

table.style_id                                # -> "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" (default)
table.style_name                              # -> "Medium Style 2 - Accent 1"
table.apply_style("Medium Style 2 - Accent 3")  # by friendly name
table.apply_style("{2D5ABB26-0587-4C30-8999-92F81FD0307C}")  # by raw GUID (passthrough)
table.apply_style("light style 2 - accent 4")   # case-insensitive
table.style_id = None                         # clear the style

What it adds

oxml layer

  • New CT_TableStyleId(BaseOxmlElement) element class with simple text-content value property.
  • tableStyleId declared as ZeroOrOne first-child of CT_TableProperties with successors=("a:extLst",) — preserves ECMA-376 §21.1.3.15 sequence (tableStyleChoice then extLst) when a PowerPoint-authored deck already carries <a:extLst>.
  • CT_TableProperties.style_id r/w property — reads/writes the GUID, removes the child element on style_id = None.

Public API on Table

  • Table.style_id — r/w GUID property; None when absent.
  • Table.style_name — reverse lookup against the registry; None for unregistered GUIDs (lossless fallback — the GUID still round-trips).
  • Table.apply_style(name_or_guid) — accepts either a friendly registry name (case-insensitive) or a raw brace-wrapped GUID.

Built-in style registry (pptx.enum.table)

  • PP_TABLE_STYLE — 38 verified built-in name→GUID pairs covering No Style (No Grid / Table Grid), Themed Style 1/2 Accent 1, all six Medium Style 2 Accent 1-6, Medium Style 1 Accents 1/2/3/6, Medium Style 3 + Accents 4/5, Light Style 1/2/3 with Accents 1-6 (where present), Dark Style 1/2 + Accent 1/Accent 2.
  • GUIDs were harvested from real PowerPoint-saved tableStyles.xml fragments across 8 unrelated GitHub repos to ensure correctness; AI-hallucinated reference files were rejected.
  • lookup_table_style(name) / style_name_for(guid) helpers, plus register_table_style(name, guid) for runtime extension.

Out of scope (deliberately deferred)

  • Custom user-defined table styles (writing into tableStyles.xml).
  • Per-cell style overrides beyond what cell APIs already provide.
  • Theme-color resolution (PowerPoint's job at render).
  • The inline <a:tableStyle> definition variant — decks that carry it will still round-trip via raw lxml, but Table.style_id/style_name will report None. Phase-3 candidate.

Tests

  • 45 → 46 pytest cases in tests/test_tables_phase2.py (the 46th is a regression test for the extLst-sibling ordering bug surfaced by a pre-commit Forge audit and fixed before commit). Full suite: 3358 passed.
  • 7 behave scenarios in features/tbl-styles.feature: default style, apply by name, apply by GUID, case-insensitive resolve, unknown-name ValueError, clear-via-style_id = None, save/reload round-trip via stream. Full behave: 1021 scenarios passed, 0 failed.
  • Ruff clean: ruff check src testsAll checks passed!; ruff format --check211 files already formatted.

Reporting contract (CLAUDE.md §7)

$ python3 -m pytest tests/ -q | tail -3
........................................................................ [ 98%]
..............................................                           [100%]
3358 passed in 4.56s

$ python3 -m ruff check src tests | tail -3
All checks passed!

$ python3 -m behave features/ --no-color 2>&1 | tail -3
1021 scenarios passed, 0 failed, 0 skipped
3069 steps passed, 0 failed, 0 skipped
Took 0min 1.607s

UAT

  • uat_tables_phase2.py (untracked per CLAUDE.md §6) at repo root.
  • Generates uat_tables_phase2_out.pptx with five tables exercising default / apply-by-name / apply-by-GUID / case-insensitive / cleared.
  • All five round-trip assertions pass programmatically.
  • Visual UAT in PowerPoint signed off by maintainer.

Refs #12

…bles 2.0 Phase 2)

Issue: #12 (Phase 2)

The fork's Phase 1 (PR #37) shipped row/column add/remove. The next-largest
gap on issue #12 — and the most-cited scanny#27 ask (37 comments) — is a
public way to apply or read a table's built-in PowerPoint style after the
table is created. Previously the GUID was hardcoded inside `_tbl_tmpl()` as a
string template (`{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`, "Medium Style 2 -
Accent 1") with no element class on the oxml side, no public getter/setter
on `Table`, and no name registry — so the existing boolean toggles
(`first_row`, `last_row`, `horz_banding`, `vert_banding`) silently bound to
a default the user couldn't observe or change.

This change adds the public surface end-to-end:

oxml layer
- New `CT_TableStyleId(BaseOxmlElement)` element class with simple text-content
  GUID `value` property.
- `tableStyleId` declared as `ZeroOrOne` first-child of `CT_TableProperties`,
  matching the ECMA-376 §21.1.3.15 sequence rule (verified by reading
  serialized XML after a setter call).
- `CT_TableProperties.style_id` r/w property — reads/writes the GUID,
  removes the child element on `style_id = None`.
- Element registered in `pptx.oxml.__init__` next to other table tags.

Public API on `Table`
- `Table.style_id` — r/w GUID property; `None` when absent; setting `None`
  removes the element.
- `Table.style_name` — reverse lookup against the built-in registry;
  returns the friendly name or `None` for unregistered GUIDs (lossless
  fallback — the GUID still round-trips even when the name isn't known).
- `Table.apply_style(name_or_guid)` — accepts either a friendly name from
  the built-in registry (case-insensitive) or a raw brace-wrapped GUID.
  GUIDs pass through verbatim so any style not in the registry is still
  reachable. Unknown friendly names raise `ValueError`.

Built-in style registry (`pptx.enum.table`)
- `PP_TABLE_STYLE` — read-only mapping of 38 verified built-in style names to
  GUIDs. Coverage includes: No Style (No Grid / Table Grid), Themed Style 1/2
  Accent 1, all six "Medium Style 2 - Accent 1..6", Medium Style 1
  Accents 1-3+6, Medium Style 3 + Accents 4-5, Light Style 1/2/3 with
  Accents 1-6 (where present), Dark Style 1/2 + Accent 1/Accent 2.
- GUIDs were harvested from real PowerPoint-saved `tableStyles.xml`
  fragments across 8 unrelated GitHub repos to ensure correctness;
  hallucinated GUIDs from AI-generated reference files were rejected.
- `lookup_table_style(name)` and `style_name_for(guid)` helpers, plus
  `register_table_style(name, guid)` for runtime extension (custom corp
  themes, additional Office built-ins discovered later).

Tests
- 45 new pytest cases in `tests/test_tables_phase2.py` covering each new
  oxml class, every public Table API method, the registry surface, the
  `_looks_like_guid` shape detector, save/reload round-trip preservation,
  and Phase-1-toggle regression checks. Full pytest: 3357 passed
  (3017 master + 340 carried from earlier phases + 45 new from Phase 2 +
  any earlier-phase additions; -3 baseline updated by previous merges;
  see PR #37/#38/#39 history).
- 7 new behave scenarios in `features/tbl-styles.feature` exercising
  default style, apply by name, apply by GUID, case-insensitive resolve,
  unknown-name `ValueError`, clear-via-`style_id = None`, and
  save/reload round-trip via stream. Full behave: 1021 scenarios passed
  (baseline 1014 + 7 new), 0 failed.
- Ruff: `ruff check` → All checks passed; `ruff format --check` → no diff.

Out of scope (deliberately deferred)
- Custom user-defined table styles (writing into `tableStyles.xml`).
- Per-cell style overrides beyond what cell APIs already provide.
- Theme-color resolution for accent references (PowerPoint's job at render).
- Style preview / thumbnail generation.
- Validation that the GUID actually exists in `tableStyles.xml`. PowerPoint
  resolves built-in styles internally; invalid GUIDs fall back to default
  rendering.

UAT
- `uat_tables_phase2.py` (untracked per CLAUDE.md §6) at repo root;
  generates `uat_tables_phase2_out.pptx` with five tables exercising
  default / apply-by-name / apply-by-GUID / case-insensitive / cleared.
  All five round-trip assertions pass programmatically; visual UAT in
  PowerPoint or Keynote pending maintainer signoff.

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