Skip to content

Commit 13e928f

Browse files
author
Steve Canny
committed
doc: add _Document.get_or_add_image_part()
Along the way: * introduce docx.package module and Package class * trying out idea of suffixing part classes with Part, for clarity
1 parent 10f1718 commit 13e928f

6 files changed

Lines changed: 128 additions & 33 deletions

File tree

docx/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import os
1010

11-
from docx.opc.package import OpcPackage
1211
from docx.opc.constants import CONTENT_TYPE as CT
12+
from docx.package import Package
1313

1414

1515
thisdir = os.path.split(__file__)[0]
@@ -25,7 +25,7 @@ def Document(docx=None):
2525
"""
2626
if docx is None:
2727
docx = _default_docx_path
28-
pkg = OpcPackage.open(docx)
28+
pkg = Package.open(docx)
2929
document_part = pkg.main_document
3030
if document_part.content_type != CT.WML_DOCUMENT_MAIN:
3131
tmpl = "file '%s' is not a Word file, content type is '%s'"

docx/package.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# encoding: utf-8
2+
3+
"""
4+
WordprocessingML Package class and related objects
5+
"""
6+
7+
from __future__ import absolute_import, print_function, unicode_literals
8+
9+
from docx.opc.package import OpcPackage
10+
11+
12+
class Package(OpcPackage):
13+
"""
14+
Customizations specific to a WordprocessingML package.
15+
"""
16+
@property
17+
def image_parts(self):
18+
"""
19+
Collection of all image parts in this package.
20+
"""
21+
22+
23+
class ImageParts(object):
24+
"""
25+
Collection of |ImagePart| instances containing all the image parts in the
26+
package.
27+
"""
28+
def get_or_add_image_part(self, image_descriptor):
29+
"""
30+
Return an |ImagePart| instance containing the image identified by
31+
*image_descriptor*, newly created if a matching one is not present in
32+
the collection.
33+
"""

docx/parts/document.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
from docx.enum.shape import WD_INLINE_SHAPE
8+
from docx.opc.constants import RELATIONSHIP_TYPE as RT
89
from docx.opc.oxml import serialize_part_xml
910
from docx.opc.package import Part
1011
from docx.oxml.shared import nsmap, oxml_fromstring
@@ -23,15 +24,18 @@ def __init__(self, partname, content_type, document_elm, package):
2324
)
2425
self._element = document_elm
2526

26-
def add_image(self, image_descriptor):
27+
def get_or_add_image_part(self, image_descriptor):
2728
"""
2829
Return an ``(image_part, rId)`` 2-tuple for the image identified by
2930
*image_descriptor*. *image_part* is an |Image| instance corresponding
30-
to the image, newly created if not already present in document. *rId*
31+
to the image, newly created if no matching image part is found. *rId*
3132
is the key for the relationship between this document part and the
3233
image part, reused if already present, newly created if not.
3334
"""
34-
raise NotImplementedError
35+
image_parts = self._package.image_parts
36+
image_part = image_parts.get_or_add_image_part(image_descriptor)
37+
rId = self.relate_to(image_part, RT.IMAGE)
38+
return (image_part, rId)
3539

3640
@property
3741
def blob(self):
@@ -202,7 +206,7 @@ def add_picture(self, image_descriptor):
202206
end of the document. *image_descriptor* can be a path (a string) or a
203207
file-like object containing a binary image.
204208
"""
205-
rId, image = self.part.add_image(image_descriptor)
209+
rId, image = self.part.get_or_add_image_part(image_descriptor)
206210
shape_id = self.part.next_id
207211
r = self._body.add_p().add_r()
208212
return InlineShape.new_picture(r, image, rId, shape_id)

docx/parts/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from docx.opc.package import Part
1010

1111

12-
class Image(Part):
12+
class ImagePart(Part):
1313
"""
1414
An image part. Corresponds to the target part of a relationship with type
1515
RELATIONSHIP_TYPE.IMAGE.

tests/parts/test_document.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
from mock import Mock
1212

1313
from docx.enum.shape import WD_INLINE_SHAPE
14+
from docx.opc.constants import RELATIONSHIP_TYPE as RT
1415
from docx.oxml.parts.document import CT_Body, CT_Document
1516
from docx.oxml.shared import nsmap
1617
from docx.oxml.text import CT_R
18+
from docx.package import ImageParts, Package
1719
from docx.parts.document import _Body, _Document, InlineShape, InlineShapes
18-
from docx.parts.image import Image
20+
from docx.parts.image import ImagePart
1921
from docx.table import Table
2022
from docx.text import Paragraph
2123

@@ -29,7 +31,7 @@
2931
from ..oxml.unitdata.text import a_p, a_sectPr, an_r
3032
from ..unitutil import (
3133
function_mock, class_mock, initializer_mock, instance_mock, loose_mock,
32-
property_mock
34+
method_mock, property_mock
3335
)
3436

3537

@@ -53,6 +55,18 @@ def it_can_be_constructed_by_opc_part_factory(
5355
)
5456
assert isinstance(doc, _Document)
5557

58+
def it_can_add_an_image_part_to_the_document(
59+
self, get_or_add_image_fixture):
60+
(document, image_descriptor_, image_parts_, relate_to_, image_part_,
61+
rId_) = get_or_add_image_fixture
62+
image_part, rId = document.get_or_add_image_part(image_descriptor_)
63+
image_parts_.get_or_add_image_part.assert_called_once_with(
64+
image_descriptor_
65+
)
66+
relate_to_.assert_called_once_with(image_part_, RT.IMAGE)
67+
assert image_part is image_part_
68+
assert rId == rId_
69+
5670
def it_has_a_body(self, document_body_fixture):
5771
document, _Body_, body_elm = document_body_fixture
5872
_body = document.body
@@ -101,6 +115,30 @@ def document_body_fixture(self, request, _Body_):
101115
def _Body_(self, request):
102116
return class_mock(request, 'docx.parts.document._Body')
103117

118+
@pytest.fixture
119+
def get_or_add_image_fixture(
120+
self, request, package_, image_descriptor_, image_parts_,
121+
relate_to_, image_part_, rId_):
122+
document = _Document(None, None, None, package_)
123+
return (
124+
document, image_descriptor_, image_parts_, relate_to_,
125+
image_part_, rId_
126+
)
127+
128+
@pytest.fixture
129+
def image_descriptor_(self, request):
130+
return instance_mock(request, str)
131+
132+
@pytest.fixture
133+
def image_part_(self, request):
134+
return instance_mock(request, ImagePart)
135+
136+
@pytest.fixture
137+
def image_parts_(self, request, image_part_):
138+
image_parts_ = instance_mock(request, ImageParts)
139+
image_parts_.get_or_add_image_part.return_value = image_part_
140+
return image_parts_
141+
104142
@pytest.fixture
105143
def init(self, request):
106144
return initializer_mock(request, _Document)
@@ -123,6 +161,22 @@ def inline_shapes_fixture(self, request, InlineShapes_):
123161
def oxml_fromstring_(self, request):
124162
return function_mock(request, 'docx.parts.document.oxml_fromstring')
125163

164+
@pytest.fixture
165+
def package_(self, request, image_parts_):
166+
package_ = instance_mock(request, Package)
167+
package_.image_parts = image_parts_
168+
return package_
169+
170+
@pytest.fixture
171+
def relate_to_(self, request, rId_):
172+
relate_to_ = method_mock(request, _Document, 'relate_to')
173+
relate_to_.return_value = rId_
174+
return relate_to_
175+
176+
@pytest.fixture
177+
def rId_(self, request):
178+
return instance_mock(request, str)
179+
126180
@pytest.fixture
127181
def serialize_part_xml_(self, request):
128182
return function_mock(
@@ -374,10 +428,15 @@ def it_raises_on_indexed_access_out_of_range(
374428

375429
def it_can_add_an_inline_picture_to_the_document(
376430
self, add_picture_fixture):
431+
# fixture ----------------------
377432
(inline_shapes, image_descriptor_, document_, InlineShape_, r_,
378433
image_, rId_, shape_id_, new_picture_shape_) = add_picture_fixture
434+
# exercise ---------------------
379435
picture_shape = inline_shapes.add_picture(image_descriptor_)
380-
document_.add_image.assert_called_once_with(image_descriptor_)
436+
# verify -----------------------
437+
document_.get_or_add_image_part.assert_called_once_with(
438+
image_descriptor_
439+
)
381440
InlineShape_.new_picture.assert_called_once_with(
382441
r_, image_, rId_, shape_id_
383442
)
@@ -393,12 +452,12 @@ def it_knows_the_part_it_belongs_to(self, inline_shapes_with_parent_):
393452
@pytest.fixture
394453
def add_picture_fixture(
395454
self, request, body_, document_, image_descriptor_, InlineShape_,
396-
r_, image_, rId_, shape_id_, new_picture_shape_):
455+
r_, image_part_, rId_, shape_id_, new_picture_shape_):
397456
inline_shapes = InlineShapes(body_, None)
398457
property_mock(request, InlineShapes, 'part', return_value=document_)
399458
return (
400459
inline_shapes, image_descriptor_, document_, InlineShape_, r_,
401-
image_, rId_, shape_id_, new_picture_shape_
460+
image_part_, rId_, shape_id_, new_picture_shape_
402461
)
403462

404463
@pytest.fixture
@@ -408,15 +467,15 @@ def body_(self, request, r_):
408467
return body_
409468

410469
@pytest.fixture
411-
def document_(self, request, rId_, image_, shape_id_):
470+
def document_(self, request, rId_, image_part_, shape_id_):
412471
document_ = instance_mock(request, _Document, name='document_')
413-
document_.add_image.return_value = rId_, image_
472+
document_.get_or_add_image_part.return_value = rId_, image_part_
414473
document_.next_id = shape_id_
415474
return document_
416475

417476
@pytest.fixture
418-
def image_(self, request):
419-
return instance_mock(request, Image)
477+
def image_part_(self, request):
478+
return instance_mock(request, ImagePart)
420479

421480
@pytest.fixture
422481
def image_descriptor_(self, request):

tests/test_api.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from docx.api import Document, _Document
1010
from docx.opc.constants import CONTENT_TYPE as CT
11-
from docx.opc.package import OpcPackage
11+
from docx.package import Package
1212
from docx.parts.document import _Document as parts_Document, InlineShapes
1313

1414
from .unitutil import class_mock, instance_mock, var_mock
@@ -17,18 +17,17 @@
1717
class DescribeDocument(object):
1818

1919
def it_opens_a_docx_file(self, open_fixture):
20-
docx_, OpcPackage_, _Document_, package, document_part = open_fixture
20+
docx_, Package_, _Document_, package, document_part = open_fixture
2121
_document = Document(docx_)
22-
OpcPackage_.open.assert_called_once_with(docx_)
22+
Package_.open.assert_called_once_with(docx_)
2323
_Document_.assert_called_once_with(package, document_part)
2424
assert _document is _Document_.return_value
2525

26-
def it_uses_default_if_no_file_provided(self, OpcPackage_, default_docx_):
26+
def it_uses_default_if_no_file_provided(self, Package_, default_docx_):
2727
Document()
28-
OpcPackage_.open.assert_called_once_with(default_docx_)
28+
Package_.open.assert_called_once_with(default_docx_)
2929

30-
def it_should_raise_if_not_a_Word_file(
31-
self, OpcPackage_, package_, docx_):
30+
def it_should_raise_if_not_a_Word_file(self, Package_, package_, docx_):
3231
package_.main_document.content_type = 'foobar'
3332
with pytest.raises(ValueError):
3433
Document(docx_)
@@ -53,21 +52,21 @@ def document_part_(self, request):
5352
def docx_(self, request):
5453
return instance_mock(request, str)
5554

56-
@pytest.fixture
57-
def OpcPackage_(self, request, package_):
58-
OpcPackage_ = class_mock(request, 'docx.api.OpcPackage')
59-
OpcPackage_.open.return_value = package_
60-
return OpcPackage_
61-
6255
@pytest.fixture
6356
def open_fixture(
64-
self, request, docx_, OpcPackage_, _Document_, package_,
57+
self, request, docx_, Package_, _Document_, package_,
6558
document_part_):
66-
return docx_, OpcPackage_, _Document_, package_, document_part_
59+
return docx_, Package_, _Document_, package_, document_part_
60+
61+
@pytest.fixture
62+
def Package_(self, request, package_):
63+
Package_ = class_mock(request, 'docx.api.Package')
64+
Package_.open.return_value = package_
65+
return Package_
6766

6867
@pytest.fixture
6968
def package_(self, request, document_part_):
70-
package_ = instance_mock(request, OpcPackage)
69+
package_ = instance_mock(request, Package)
7170
package_.main_document = document_part_
7271
return package_
7372

@@ -118,7 +117,7 @@ def document_part_(self, request):
118117

119118
@pytest.fixture
120119
def package_(self, request):
121-
return instance_mock(request, OpcPackage)
120+
return instance_mock(request, Package)
122121

123122
@pytest.fixture
124123
def save_fixture(self, request, package_):

0 commit comments

Comments
 (0)