From 4652fdaaa61f256aab9fe6d84827f31be1f5ff28 Mon Sep 17 00:00:00 2001 From: Tem Revil Date: Sat, 11 Jul 2026 04:26:01 +0300 Subject: [PATCH] fix: skip non-image parts when matching by sha1 in ImageParts Package._gather_image_parts appends every part reached through an image relationship to the ImageParts collection, casting it to ImagePart. When an image uses a type that does not load as an ImagePart (for example an SVG, or an image with broken or non-standard EXIF data), the target is a plain Part with no sha1 attribute. The next call to add_picture runs ImageParts._get_by_sha1, which reads .sha1 on every collected part, so it raised "'Part' object has no attribute 'sha1'" as soon as such a part was present. Skip parts that have no sha1 attribute while matching, mirroring the same fix in python-pptx. Add a unit test covering a non-image Part placed before the matching ImagePart in the collection. --- src/docx/package.py | 4 ++++ tests/test_package.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/docx/package.py b/src/docx/package.py index 7ea47e6e1..9132c4339 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -88,6 +88,10 @@ def _get_by_sha1(self, sha1: str) -> ImagePart | None: """Return the image part in this collection having a SHA1 hash matching `sha1`, or |None| if not found.""" for image_part in self._image_parts: + # ---skip parts loaded for an unsupported image type (e.g. SVG), which + # ---come through the image relationships as a plain Part and have no sha1 + if not hasattr(image_part, "sha1"): + continue if image_part.sha1 == sha1: return image_part return None diff --git a/tests/test_package.py b/tests/test_package.py index ac9839828..6eef18bf7 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -8,6 +8,7 @@ from docx.image.image import Image from docx.opc.packuri import PackURI +from docx.opc.part import Part from docx.package import ImageParts, Package from docx.parts.image import ImagePart @@ -102,6 +103,18 @@ def but_it_adds_a_new_image_part_when_match_fails( _add_image_part_.assert_called_once_with(image_parts, image_) assert image_part is image_part_ + def it_skips_parts_without_a_sha1_when_matching(self, request: FixtureRequest): + # ---an unsupported image type (e.g. SVG) is reached through the image + # ---relationships as a plain Part with no sha1, and must be skipped + # ---rather than raising AttributeError--- + non_image_part = instance_mock(request, Part) + image_part = instance_mock(request, ImagePart, sha1="f005ba11") + image_parts = ImageParts() + image_parts.append(non_image_part) + image_parts.append(image_part) + + assert image_parts._get_by_sha1("f005ba11") is image_part + @pytest.mark.parametrize( ("existing_partname_numbers", "expected_partname_number"), [