Skip to content

Commit f316ca8

Browse files
committed
Skip caching unhashable image descriptors
Only build and use a cache key when the image_descriptor is hashable. Previously id() was used for non-hashable descriptors (e.g. file-like objects), which could risk aliasing after GC and lead to incorrect deduplication. Now the code attempts to construct a cache key with the descriptor and falls back to skipping caching for unhashable descriptors; cache entries are only read/written when a valid cache_key exists. Filename normalization and per-insertion shape_id behavior are unchanged.
1 parent ef56632 commit f316ca8

1 file changed

Lines changed: 12 additions & 7 deletions

File tree

docxtpl/inline_image.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,16 @@ def _insert_image(self):
112112
# cached because each insertion needs a unique shape_id - header/footer
113113
# and footnote parts are not renumbered by fix_docpr_ids().
114114
cache = self.tpl._image_cache
115-
# Use id() for non-hashable descriptors (file-like objects) to avoid
116-
# TypeError on dict lookup.
117-
desc_key = image_descriptor if isinstance(image_descriptor, str) else id(image_descriptor)
118-
cache_key = (id(part), desc_key, self.width, self.height)
119-
120-
if cache_key in cache:
115+
# For hashable descriptors (strings, paths), cache by value.
116+
# For unhashable descriptors (file-like objects), skip caching
117+
# entirely — using id() would risk aliasing after GC.
118+
try:
119+
cache_key = (id(part), image_descriptor, self.width, self.height)
120+
hash(cache_key) is not None # trigger TypeError if unhashable
121+
except TypeError:
122+
cache_key = None
123+
124+
if cache_key is not None and cache_key in cache:
121125
rId, cx, cy, filename = cache[cache_key]
122126
else:
123127
# Get or add the image part with O(1) descriptor-based dedup,
@@ -129,7 +133,8 @@ def _insert_image(self):
129133
# image.filename is None for file-like descriptors (BytesIO);
130134
# normalize to empty string to match python-docx's behavior.
131135
filename = xml_escape(image.filename or "", {'"': """})
132-
cache[cache_key] = (rId, int(cx), int(cy), filename)
136+
if cache_key is not None:
137+
cache[cache_key] = (rId, int(cx), int(cy), filename)
133138

134139
# Always assign a fresh shape_id per insertion so that drawing IDs
135140
# are unique in every part (including headers/footers/footnotes

0 commit comments

Comments
 (0)