From a53b2066c170f81a76071a159f532a0fcbbd8967 Mon Sep 17 00:00:00 2001 From: Daniel Nguyen Date: Fri, 9 Oct 2026 12:08:59 +0800 Subject: [PATCH] refactor(pptx): use python-pptx shape types instead of getattr/hasattr _get_shape_position read top/left with getattr(shape, attr). It now types the shape as BaseShape and reads shape.top or shape.left directly. The AttributeError catch stays, with a comment: python-pptx wraps an element without a registered class, such as a p:contentPart inside a group, in a plain BaseShape whose getters raise it. The text check used hasattr(shape, "text"). Only autoshape.Shape and its placeholder subclasses define text, so it is now an isinstance check. No output change. Signed-off-by: Daniel Nguyen --- docling/backend/mspowerpoint_backend.py | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docling/backend/mspowerpoint_backend.py b/docling/backend/mspowerpoint_backend.py index 49a041c1b1..ef888e5953 100644 --- a/docling/backend/mspowerpoint_backend.py +++ b/docling/backend/mspowerpoint_backend.py @@ -10,7 +10,7 @@ from io import BytesIO from pathlib import Path from tempfile import mkdtemp -from typing import Any, Callable, Final, Iterable, Iterator, Optional, Union +from typing import Any, Callable, Final, Iterable, Iterator, Literal, Optional, Union from docling_core.types.doc import ( BoundingBox, @@ -64,6 +64,8 @@ from pptx.exc import InvalidXmlError from pptx.oxml.ns import qn from pptx.oxml.text import CT_TextLineBreak + from pptx.shapes.autoshape import Shape + from pptx.shapes.base import BaseShape from pptx.shapes.picture import Picture _PPTX_AVAILABLE = True @@ -773,23 +775,26 @@ def _get_effective_list_marker(self, shape, paragraph) -> dict: "level": lvl, } - def _get_shape_position(self, shape: object, attr: str) -> Optional[int]: + def _get_shape_position( + self, shape: BaseShape, attr: Literal["top", "left"] + ) -> Optional[int]: """Return a shape position attribute as an integer for ordering. Args: - shape: A python-pptx shape object. Position attributes such as - ``top`` and ``left`` are accessed via ``getattr``; property - getters that raise are silently treated as absent. - attr: Name of the position attribute to read, typically ``"top"`` - or ``"left"``. + shape: A python-pptx shape object. Property getters that raise are + silently treated as absent. + attr: The position attribute to read, ``"top"`` or ``"left"``. Returns: The attribute value cast to ``int``, or ``None`` if the attribute - is missing, ``None``-valued, or its getter raises - ``AttributeError``, ``ValueError``, or ``TypeError``. + is ``None``-valued or its getter raises ``AttributeError``, + ``ValueError``, or ``TypeError``. """ + # python-pptx wraps an element without a registered class, such as a + # p:contentPart inside a group, in a plain BaseShape whose getters + # raise AttributeError. try: - value = getattr(shape, attr) + value = shape.top if attr == "top" else shape.left except (AttributeError, ValueError, TypeError): return None @@ -798,7 +803,9 @@ def _get_shape_position(self, shape: object, attr: str) -> Optional[int]: return int(value) - def _iter_shapes_by_position(self, shapes: Iterable[object]) -> Iterator[object]: + def _iter_shapes_by_position( + self, shapes: Iterable[BaseShape] + ) -> Iterator[BaseShape]: """Iterate shapes in visual top-to-bottom, left-to-right order. PowerPoint stores shapes in creation/z-order, which can differ from the @@ -827,7 +834,7 @@ def _iter_shapes_by_position(self, shapes: Iterable[object]) -> Iterator[object] class _ShapeInfo(NamedTuple): index: int - shape: object + shape: BaseShape top: int left: int @@ -1652,7 +1659,7 @@ def handle_shapes(shape, parent_slide, slide_ind, doc, slide_size): shape, parent_slide, slide_ind, doc, slide_size ) # If shape doesn't have any text, move on to the next shape - if not hasattr(shape, "text"): + if not isinstance(shape, Shape): return if shape.text is None: return