From 8b2194c92ef9b4d90300541bff03d390d71c9708 Mon Sep 17 00:00:00 2001 From: Thomas Mustier Date: Thu, 12 Feb 2026 16:32:51 +0000 Subject: [PATCH 1/2] refactor(chart-engine): use shared chart metadata access helpers --- clean_slides/chart_engine/builder.py | 13 ++++++++----- clean_slides/chart_engine/template_ops.py | 11 +++++++---- clean_slides/pptx_access.py | 7 +++++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/clean_slides/chart_engine/builder.py b/clean_slides/chart_engine/builder.py index 4519368..46ab28e 100644 --- a/clean_slides/chart_engine/builder.py +++ b/clean_slides/chart_engine/builder.py @@ -13,7 +13,7 @@ from pptx.oxml.ns import qn from pptx.util import Emu, Inches, Pt -from ..pptx_access import chart_series_names, shape_has_text_frame, slide_charts +from ..pptx_access import chart_series_names, chart_xml_space, shape_has_text_frame, slide_charts from . import annotations as _annotations from . import payloads as _payloads from .colors import apply_color @@ -162,7 +162,7 @@ def _chart_box(value: tuple[object, object, object, object]) -> ChartBox | None: def apply_chart_template_dlbls( - target_chart: Any, + target_chart: object, template_path: Path, slide_index: int = 0, chart_index: int = 0, @@ -178,10 +178,13 @@ def apply_chart_template_dlbls( return template_chart = template_charts[chart_index] + template_chart_space = chart_xml_space(template_chart) + target_chart_space = chart_xml_space(target_chart) + if template_chart_space is None or target_chart_space is None: + return - template_chart_obj = cast(Any, template_chart) - template_series = get_chart_series(template_chart_obj._chartSpace) - target_series = get_chart_series(target_chart._chartSpace) + template_series = get_chart_series(template_chart_space) + target_series = get_chart_series(target_chart_space) if not template_series or not target_series: return if series_index < 0 or series_index >= len(template_series): diff --git a/clean_slides/chart_engine/template_ops.py b/clean_slides/chart_engine/template_ops.py index 61054e8..65ad4b8 100644 --- a/clean_slides/chart_engine/template_ops.py +++ b/clean_slides/chart_engine/template_ops.py @@ -8,11 +8,11 @@ import zipfile from collections.abc import Sequence from pathlib import Path -from typing import Any, NamedTuple, cast +from typing import NamedTuple from pptx import Presentation -from ..pptx_access import slide_charts +from ..pptx_access import chart_part_name, slide_charts def read_pptx_part(path: Path, part_name: str) -> bytes: @@ -69,8 +69,11 @@ def replace_chart_with_template( ) template_chart = template_charts[template_chart_index] - template_chart_obj = cast(Any, template_chart) - template_chart_part = str(template_chart_obj.part.partname).lstrip("/") + template_chart_part_name = chart_part_name(template_chart) + if template_chart_part_name is None: + raise ValueError("Template chart is missing partname") + + template_chart_part = template_chart_part_name.lstrip("/") template_chart_rels_part = f"ppt/charts/_rels/{Path(template_chart_part).name}.rels" template_chart_xml = read_pptx_part(template_path, template_chart_part) diff --git a/clean_slides/pptx_access.py b/clean_slides/pptx_access.py index 4bf96a5..57da677 100644 --- a/clean_slides/pptx_access.py +++ b/clean_slides/pptx_access.py @@ -168,6 +168,13 @@ def chart_xml_element(chart: object) -> object | None: return getattr(chart, "_element", None) +def chart_part_name(chart: object) -> str | None: + """Return chart partname (e.g. '/ppt/charts/chart1.xml') when available.""" + chart_part = getattr(chart, "part", None) + partname = getattr(chart_part, "partname", None) + return str(partname) if partname is not None else None + + class _AddChartCallable(Protocol): def __call__( self, From feb9e1fedf440ebb8b340ece3c561ee2290df0d1 Mon Sep 17 00:00:00 2001 From: Thomas Mustier Date: Thu, 12 Feb 2026 16:32:57 +0000 Subject: [PATCH 2/2] test(pptx): cover chart part-name helper --- tests/test_pptx_access.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_pptx_access.py b/tests/test_pptx_access.py index fb41b37..a5aa63d 100644 --- a/tests/test_pptx_access.py +++ b/tests/test_pptx_access.py @@ -4,6 +4,7 @@ from clean_slides.pptx_access import ( chart_first_plot, + chart_part_name, chart_plots, chart_series, chart_series_names, @@ -61,10 +62,16 @@ class _Chart: series: list[object] _chartSpace: object | None = None _element: object | None = None + part: object | None = None plots: list[object] = field(default_factory=_object_list) has_legend: bool = True +@dataclass +class _ChartPart: + partname: str + + @dataclass class _TextFrame: text: str @@ -229,7 +236,12 @@ def test_shape_and_text_frame_helpers_cover_text_placeholder_connector() -> None text_frame = _TextFrame(text=" Hello ", paragraphs=[object()], _element={"tag": "txBody"}) shape = _Shape( has_chart=True, - chart=_Chart(chart_type=57, series=[_Series(name="Revenue")], _chartSpace={"tag": "cs"}), + chart=_Chart( + chart_type=57, + series=[_Series(name="Revenue")], + _chartSpace={"tag": "cs"}, + part=_ChartPart(partname="/ppt/charts/chart7.xml"), + ), has_text_frame=True, text_frame=text_frame, text="inline", @@ -259,6 +271,7 @@ def test_shape_and_text_frame_helpers_cover_text_placeholder_connector() -> None assert shape_xml_element(shape) == {"tag": "shape"} assert chart_xml_space(chart) == {"tag": "cs"} + assert chart_part_name(chart) == "/ppt/charts/chart7.xml" paragraph = _Paragraph(_element={"tag": "p"}) assert paragraph_xml_element(paragraph) == {"tag": "p"}