Skip to content

refactor: split converter/elements.py into a package with scoped conversion scale (v0.5.2) - #245

Merged
ShotaroKataoka merged 5 commits into
mainfrom
feature/v0.5.2-elements-split
Jul 31, 2026
Merged

refactor: split converter/elements.py into a package with scoped conversion scale (v0.5.2)#245
ShotaroKataoka merged 5 commits into
mainfrom
feature/v0.5.2-elements-split

Conversation

@ShotaroKataoka

Copy link
Copy Markdown
Contributor

Summary

Resolves the converter/elements.py known debt (principles.md): the 1519-line / 18-definition monolith is now a 4-module package with an enforced dependency DAG, and the converter's px scale is no longer process-global mutable state.

Internal refactoring only — no changes to public import paths, signatures, or JSON output (guarded by characterization tests; no CHANGELOG entry per versioning policy).

1. Characterization first (safety net)

  • 41 tests pin the exact output of all 7 public extractors + _dispatch_shape, image/media file naming, the image counter, and pipeline JSON for standard 16:9 and non-standard 4:3 decks
  • Fixtures are generated programmatically with python-pptx — no binary fixtures added
  • Expected values were captured from the pre-split implementation and did not change through the refactoring

2. Scoped conversion scale (prerequisite for the split)

set_emu_per_px() patched a fixed string list of sys.modules entries to overwrite import-time copies of EMU_PER_PX. Splitting elements.py would have silently broken non-standard slide widths (new submodules missing from the patch list), and the process-global mutation already leaked across conversions (demonstrated: after converting a 4:3 deck, later direct extractor calls ran on a 4762.5 basis).

Replaced with ContextVar + conversion_scale() context manager + get_emu_per_px():

  • always restores on exit (normal / exception / nested)
  • thread-isolated (two concurrent conversions with different widths tested)
  • AST guards forbid reintroducing EMU_PER_PX imports or sys.modules access in converter internals
  • EMU_PER_PX = 6350 stays on the facade for import compatibility

3. Package split (mechanical move, bodies unchanged)

converter/elements/
├─ __init__.py   # compatibility facade (7 public extractors + _dispatch_shape)
├─ shapes.py     # name maps + line / freeform / auto-shape
├─ textbox.py    # textbox (delegates preset geometry to shapes)
├─ media.py      # video / picture / SVG / image fills / referenced images
└─ dispatch.py   # raw passthrough / type routing / group recursion

Dependency DAG (textbox→shapes, dispatch→{shapes,textbox,media}, facade re-export only) is enforced by an AST guard; submodules never import the facade.

4. Warning closure

  • lxml FutureWarning fixed at the root: if grp_sp_pris not None (identical semantics)
  • pytest promotes FutureWarning to error so it cannot regress
  • defusedxml's own cElementTree DeprecationWarning ignored with a message/category/module-scoped filter only — the defuse_stdlib() security patch is untouched

Testing

  • make all: 616 passed, 4 skipped, zero warnings (no existing test assertions modified)
  • make smoke: OK (24 tools, 4 templates, persona served over real stdio)
  • pytest tests/test_converter_elements.py tests/test_converter_robustness.py -W error::FutureWarning: 104 passed
  • ASH scan: no findings from this change (17 pre-existing npm-audit findings in node_modules; bandit/semgrep PASSED)

Not in scope

Extractor redesign (Strategy registry / ExtractionContext), extract_textbox_element internal decomposition, and any schema/output changes — per the v0.5.x roadmap, one theme per release.

…re split

Characterization tests captured from the pre-split implementation:
- direct output contract of all 7 public extractors + _dispatch_shape
- image/media naming and img_counter behavior
- pipeline JSON for standard 16:9 and non-standard 4:3 decks
- import-path and signature compatibility surface

SPEC: 20260731-2310_v0.5.2-elements-split
Progress: Phase 1 (characterization) green on old implementation
Next: Phase 2 scoped scale accessor
…textVar

Replace the process-global set_emu_per_px() — which patched a fixed
string list of sys.modules entries — with a ContextVar-backed
conversion_scale() context manager + get_emu_per_px() getter.

- pipeline wraps extraction in conversion_scale(slide_width); the scale
  is always restored on exit (normal / exception / nested)
- converter internals fetch the scale once per invocation instead of
  copying EMU_PER_PX at import time (the copy is what forced the module
  patch list, which a package split would silently break)
- EMU_PER_PX stays as a compat constant on the facade; internals no
  longer read it
- tests: scoped set/restore, thread isolation, pipeline no-leak, and
  AST guards forbidding EMU_PER_PX imports / sys.modules patching

SPEC: 20260731-2310_v0.5.2-elements-split
Progress: Phase 2 (scale isolation) complete, 593 passed
Next: Phase 3 elements package split
…a package

Mechanical move of the 1519-line monolith into 4 modules + facade
(bodies unchanged; only imports rewired to the parent package):

- shapes.py   name maps + line / freeform / auto-shape extraction
- textbox.py  textbox extraction (delegates preset geometry to shapes)
- media.py    video / picture / SVG / image-fill / referenced images
- dispatch.py raw passthrough, type routing, group recursion
- __init__.py compatibility facade (7 public extractors + _dispatch_shape)

Dependency DAG (textbox->shapes, dispatch->{shapes,textbox,media}) is
enforced by an AST guard; submodule import smoke tests included.
Characterization expectations unchanged — 615 passed.

SPEC: 20260731-2310_v0.5.2-elements-split
Progress: Phase 3 (package split) complete
Next: Phase 4 warning closure
- Fix lxml FutureWarning: truth-test of grpSpPr element -> 'is not None'
  (identical semantics — when grpSpPr has no children, find() returns
  None either way)
- pytest: promote FutureWarning to error so it cannot regress silently
- pytest: ignore only defusedxml's own cElementTree DeprecationWarning
  (upstream shim on Python 3.12; message/category/module-scoped filter,
  the defuse_stdlib security patch itself is untouched)

SPEC: 20260731-2310_v0.5.2-elements-split
Progress: Phase 4 (warning closure) complete
Next: final validation + PR
…lved

SPEC: 20260731-2310_v0.5.2-elements-split
@ShotaroKataoka ShotaroKataoka added the blog:skip ブログ対象外 label Jul 31, 2026
@ShotaroKataoka
ShotaroKataoka merged commit 30f96b8 into main Jul 31, 2026
14 of 15 checks passed
@ShotaroKataoka
ShotaroKataoka deleted the feature/v0.5.2-elements-split branch July 31, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blog:skip ブログ対象外

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant