feat: shape-tree ergonomics — iter_leaf_shapes, find_by_xpath, mapping access, selection-pane order (Modernization Phase 4)#45
Merged
MHoroszowski merged 1 commit intomasterfrom May 8, 2026
Conversation
…g access, selection-pane order (Modernization Phase 4) Issue: #29 (Phase 4) Phases 1 (#39), 2 (#43), and 3 (#44) shipped PathLike, Font.color non-mutation, UTC datetimes, `Shapes.by_name`, the pyright CI gate, and pyproject cleanup. Phase 4 closes the four shape-tree ergonomic sub-features in issue #29's API ergonomics group — all small, additive, all closing upstream tickets. Public surface added (additive — no existing API removed) - `_BaseShapes.iter_leaf_shapes()` — generator that recursively descends into `GroupShape` children, yielding only non-group leaf shapes. Useful when "give me every actual shape" is the right traversal, rather than the existing `for shape in slide.shapes` which counts groups as one. A consumer wanting groups can still iterate normally. Closes scanny#435. - `_BaseShapes.__getitem__(key)` overloaded to accept `int | str`. Integer keys keep existing semantics (index in document order, IndexError on out-of-range). String keys delegate to `by_name(key)` — the Phase 2 lookup helper. Plus added `__contains__(key)` (string checks name presence; int checks index range; other types return False) and `keys()` (list of every shape's name in document order). Same Mapping-like surface added to `SlidePlaceholders`: string lookup by `.name`, integer keeps the existing `ph_idx` dictionary key semantics. Closes scanny#800. Decision: not subclassing `collections.abc.Mapping`. The Mapping contract requires `__iter__` to yield keys; existing `Shapes.__iter__` yields shape values. Switching would silently break every existing call site. Duck-typed Mapping-like behavior gets the user-facing benefit without the breaking change. - `BaseShape.find_by_xpath(xpath, namespaces=None)` — power-user XPath escape hatch over a shape's element subtree. With `namespaces=None`, uses the project's standard `_nsmap` so common prefixes (`a:`, `p:`, `r:`, `xsi:`, `adec:`, `p14:`, etc.) work without explicit declaration. Custom `namespaces` dict bypasses the project wrapper via `lxml.etree._Element.xpath` directly so the user's prefix map is honored. Returns whatever lxml returns — typically a list of matching elements, or [] on no match. - `_BaseShapes.in_selection_pane_order()` — read-only tuple of shapes in PowerPoint Selection Pane order (top-most first = last in XML). Returns a snapshot, not a live view; doesn't promise to update if the collection is mutated after the call. Closes scanny#532. Tests - 28 new pytest cases in `tests/test_modernization_phase4.py` covering: iter_leaf_shapes (top-level identity, GroupShape exclusion, leaf count); Mapping-like access on Shapes (string-key, KeyError, int-key unchanged, `__contains__` on str/int/other types, `keys()` order); same on SlidePlaceholders (str by name, int by `ph_idx`); find_by_xpath with default + custom nsmap; in_selection_pane_order tuple type + reversal contract + length preservation + non-mutation. Plus Phase-2/3 regression checks. Full pytest: `3484 passed`. - 7 new behave scenarios in `features/modernization-phase4.feature` covering the four helpers end-to-end. Full behave: `1048 scenarios passed, 0 failed` (baseline 1041 + 7 new). - Ruff: `ruff check src tests` → All checks passed; `ruff format --check` → no diff. - Pyright public-API gate (Phase 3): `0 errors, 0 warnings, 0 informations`. Issue #29 ledger after this PR - Phase 1 (#39) — PathLike, PERCENT_40 typo, Slide.background - Phase 2 (#43) — Font.color non-mutation, UTC datetimes, Shapes.by_name - Phase 3 (#44) — pyright CI gate, Python 3.13, pyproject cleanup - Phase 4 (this PR) — iter_leaf_shapes, find_by_xpath, mapping-like access on Shapes/SlidePlaceholders, in_selection_pane_order - Remaining: `uv` migration and ruff selection strengthening (`B`/`RUF`) — defer to standalone PRs per Phase 3 PR body. Refs #29
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.
Phase 4 of issue #29 (Modernization & Ergonomics): shape-tree ergonomics
Phases 1 (#39), 2 (#43), and 3 (#44) shipped PathLike, Font.color non-mutation, UTC datetimes,
Shapes.by_name, the pyright CI gate, and pyproject cleanup. This PR closes the four shape-tree ergonomic sub-features in issue #29's API ergonomics group — all small, additive, all closing upstream tickets.Public surface added
What it adds
_BaseShapes.iter_leaf_shapes()(closes scanny#435)GroupShapechildren, yielding only non-group leaf shapes.for shape in slide.shapescounts each group as 1 entry. A consumer wanting groups can still iterate normally._BaseShapes.__getitem__(key)overloaded to acceptint | str(closes scanny#800)IndexErroron out-of-range).by_name(key)from Phase 2.boolkeys are rejected withTypeError— they're a subclass ofintso would otherwise silently resolve to index 0/1, almost certainly an unintended call. Forge audit catch.__contains__(key)(string checks name; int checks index range; bool/other types returnFalse) andkeys()(list of every shape's name in document order).SlidePlaceholders— string lookup by.name, integer keeps the existingph_idxdictionary-key semantics.Decision: not subclassing
collections.abc.Mapping. The Mapping contract requires__iter__to yield keys; existingShapes.__iter__yields shape values. Switching would silently break every existing call site. Duck-typed Mapping-like behavior gets the user-facing benefit without the breaking change.BaseShape.find_by_xpath(xpath, namespaces=None)— power-user XPath escape hatchnamespaces=None(default): uses the project's standard_nsmapso common prefixes (a:,p:,r:,xsi:,adec:,p14:, etc.) work without explicit declaration.namespacesdict: bypasses the project wrapper via a module-level_LXML_XPATH = lxml.etree._Element.xpathconstant so the user's prefix map is honored verbatim. Forge audit suggested lifting the lookup to module-level rather than importing inside the method — done.[]on no match._BaseShapes.in_selection_pane_order()(closes scanny#532)tupleof shapes in PowerPoint Selection Pane order (top-most first = last in XML).Tests
tests/test_modernization_phase4.pycovering all four helpers, Mapping-like dispatch on str/int/bool/other key types, GroupShape exclusion contract, custom-nsmap path, tuple type + reversal contract, and Phase-2/3 regression checks. Full suite:3485 passed.features/modernization-phase4.featureexercising the four helpers end-to-end. Full behave:1048 scenarios passed, 0 failed.Reporting contract (CLAUDE.md §7)
UAT
uat_modernization_phase4.py(untracked per CLAUDE.md §6) at repo root.in_selection_pane_order()output (top-most first).Issue #29 ledger after this merge
Remaining on issue #29:
uvmigration and ruff selection strengthening (B/RUF) — both deferred to standalone PRs per Phase 3 PR body.Refs #29