Skip to content

Commit eadd737

Browse files
author
Steve Canny
committed
api: rewrite docx.Document()
* retrofit add api-open-document.feature to drive integration of document open call * extract _default_docx_path() to make testing seam
1 parent 35db7e3 commit eadd737

File tree

9 files changed

+175
-67
lines changed

9 files changed

+175
-67
lines changed

docx/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# encoding: utf-8
22

3-
from docx.api import Document # noqa
3+
from docx.api import Document # noqa
4+
from docx.api import DocumentNew # noqa
45

56
__version__ = '0.8.1'
67

docx/api.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,27 @@
1818
from docx.shared import lazyproperty
1919

2020

21-
_thisdir = os.path.split(__file__)[0]
22-
_default_docx_path = os.path.join(_thisdir, 'templates', 'default.docx')
21+
def DocumentNew(docx=None):
22+
"""
23+
Return a |Document| object loaded from *docx*, where *docx* can be
24+
either a path to a ``.docx`` file (a string) or a file-like object. If
25+
*docx* is missing or ``None``, the built-in default document "template"
26+
is loaded.
27+
"""
28+
docx = _default_docx_path() if docx is None else docx
29+
document_part = Package.open(docx).main_document_part
30+
if document_part.content_type != CT.WML_DOCUMENT_MAIN:
31+
tmpl = "file '%s' is not a Word file, content type is '%s'"
32+
raise ValueError(tmpl % (docx, document_part.content_type))
33+
return document_part.document
34+
35+
36+
def _default_docx_path():
37+
"""
38+
Return the path to the built-in default .docx package.
39+
"""
40+
_thisdir = os.path.split(__file__)[0]
41+
return os.path.join(_thisdir, 'templates', 'default.docx')
2342

2443

2544
class Document(object):
@@ -30,10 +49,9 @@ class Document(object):
3049
is loaded.
3150
"""
3251
def __init__(self, docx=None):
33-
super(Document, self).__init__()
34-
document_part, package = self._open(docx)
35-
self._document_part = document_part
36-
self._package = package
52+
document = DocumentNew(docx)
53+
self._document_part = document._part
54+
self._package = document._part.package
3755

3856
def add_heading(self, text='', level=1):
3957
"""
@@ -172,19 +190,3 @@ def tables(self):
172190
such as ``<w:ins>`` or ``<w:del>`` do not appear in this list.
173191
"""
174192
return self._document_part.tables
175-
176-
@staticmethod
177-
def _open(docx):
178-
"""
179-
Return a (document_part, package) 2-tuple loaded from *docx*, where
180-
*docx* can be either a path to a ``.docx`` file (a string) or a
181-
file-like object. If *docx* is ``None``, the built-in default
182-
document "template" is loaded.
183-
"""
184-
docx = _default_docx_path if docx is None else docx
185-
package = Package.open(docx)
186-
document_part = package.main_document
187-
if document_part.content_type != CT.WML_DOCUMENT_MAIN:
188-
tmpl = "file '%s' is not a Word file, content type is '%s'"
189-
raise ValueError(tmpl % (docx, document_part.content_type))
190-
return document_part, package

docx/document.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# encoding: utf-8
2+
3+
"""
4+
|Document| and closely related objects
5+
"""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
10+
11+
from .shared import ElementProxy
12+
13+
14+
class Document(ElementProxy):
15+
"""
16+
WordprocessingML (WML) document.
17+
"""
18+
19+
__slots__ = ('_part',)
20+
21+
def __init__(self, element, part):
22+
super(Document, self).__init__(element)
23+
self._part = part

docx/opc/package.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def load_rel(self, reltype, target, rId, is_external=False):
9898
return self.rels.add_relationship(reltype, target, rId, is_external)
9999

100100
@property
101-
def main_document(self):
101+
def main_document_part(self):
102102
"""
103103
Return a reference to the main document part for this package.
104104
Examples include a document part for a WordprocessingML package, a

docx/parts/document.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections import Sequence
1212

1313
from ..blkcntnr import BlockItemContainer
14+
from ..document import Document
1415
from ..enum.section import WD_SECTION
1516
from ..opc.constants import RELATIONSHIP_TYPE as RT
1617
from ..opc.part import XmlPart
@@ -53,6 +54,13 @@ def body(self):
5354
"""
5455
return _Body(self._element.body, self)
5556

57+
@property
58+
def document(self):
59+
"""
60+
A |Document| object providing access to the content of this document.
61+
"""
62+
return Document(self._element, self)
63+
5664
def get_or_add_image_part(self, image_descriptor):
5765
"""
5866
Return an ``(image_part, rId)`` 2-tuple for the image identified by

features/api-open-document.feature

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Feature: Open a document
2+
In order work on a document
3+
As a developer using python-docx
4+
I need a way to open a document
5+
6+
7+
Scenario: Open a specified document
8+
Given I have python-docx installed
9+
When I call docx.Document() with the path of a .docx file
10+
Then document is a Document object
11+
12+
13+
Scenario: Open the default document
14+
Given I have python-docx installed
15+
When I call docx.Document() with no arguments
16+
Then document is a Document object

features/steps/api.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@
44
Step implementations for basic API features
55
"""
66

7-
from behave import then, when
7+
from behave import given, then, when
88

9+
import docx
10+
11+
from docx import DocumentNew
912
from docx.shared import Inches
1013
from docx.table import Table
1114

12-
from helpers import test_file
15+
from helpers import test_docx, test_file
16+
17+
18+
# given ====================================================
1319

20+
@given('I have python-docx installed')
21+
def given_I_have_python_docx_installed(context):
22+
pass
1423

15-
# when ====================================================
24+
25+
# when =====================================================
1626

1727
@when('I add a 2 x 2 table specifying only row and column count')
1828
def when_add_2x2_table_specifying_only_row_and_col_count(context):
@@ -101,8 +111,24 @@ def when_add_picture_specifying_only_image_file(context):
101111
context.picture = document.add_picture(test_file('monty-truth.png'))
102112

103113

114+
@when('I call docx.Document() with no arguments')
115+
def when_I_call_docx_Document_with_no_arguments(context):
116+
context.document = DocumentNew()
117+
118+
119+
@when('I call docx.Document() with the path of a .docx file')
120+
def when_I_call_docx_Document_with_the_path_of_a_docx_file(context):
121+
context.document = DocumentNew(test_docx('doc-default'))
122+
123+
104124
# then =====================================================
105125

126+
@then('document is a Document object')
127+
def then_document_is_a_Document_object(context):
128+
document = context.document
129+
assert isinstance(document, docx.document.Document)
130+
131+
106132
@then('the document contains a 2 x 2 table')
107133
def then_document_contains_2x2_table(context):
108134
document = context.document
20.9 KB
Binary file not shown.

tests/test_api.py

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import pytest
1212

13-
from docx.api import Document
13+
import docx
14+
15+
from docx.api import Document, DocumentNew
1416
from docx.enum.text import WD_BREAK
1517
from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT
1618
from docx.opc.coreprops import CoreProperties
@@ -25,34 +27,70 @@
2527
from docx.text.run import Run
2628

2729
from .unitutil.mock import (
28-
instance_mock, class_mock, method_mock, property_mock, var_mock
30+
function_mock, instance_mock, class_mock, method_mock, property_mock
2931
)
3032

3133

3234
class DescribeDocument(object):
3335

34-
def it_opens_a_docx_on_construction(self, init_fixture):
35-
docx_, open_ = init_fixture
36-
document = Document(docx_)
37-
open_.assert_called_once_with(docx_)
38-
assert isinstance(document, Document)
39-
40-
def it_can_open_a_docx_file(self, open_fixture):
41-
docx_, Package_, package_, document_part_ = open_fixture
42-
document_part, package = Document._open(docx_)
43-
Package_.open.assert_called_once_with(docx_)
44-
assert document_part is document_part
45-
assert package is package_
46-
47-
def it_opens_default_template_if_no_file_provided(
48-
self, Package_, default_docx_):
49-
Document._open(None)
50-
Package_.open.assert_called_once_with(default_docx_)
51-
52-
def it_should_raise_if_not_a_Word_file(self, Package_, package_, docx_):
53-
package_.main_document.content_type = 'foobar'
36+
def it_opens_a_docx_file(self, open_fixture):
37+
docx, Package_, document_ = open_fixture
38+
document = DocumentNew(docx)
39+
Package_.open.assert_called_once_with(docx)
40+
assert document is document_
41+
42+
def it_opens_the_default_docx_if_none_specified(self, default_fixture):
43+
docx, Package_, document_ = default_fixture
44+
document = DocumentNew()
45+
Package_.open.assert_called_once_with(docx)
46+
assert document is document_
47+
48+
def it_raises_on_not_a_Word_file(self, raise_fixture):
49+
not_a_docx = raise_fixture
5450
with pytest.raises(ValueError):
55-
Document._open(docx_)
51+
Document(not_a_docx)
52+
53+
# fixtures -------------------------------------------------------
54+
55+
@pytest.fixture
56+
def default_fixture(self, _default_docx_path_, Package_, document_):
57+
docx = 'barfoo.docx'
58+
_default_docx_path_.return_value = docx
59+
document_part = Package_.open.return_value.main_document_part
60+
document_part.document = document_
61+
document_part.content_type = CT.WML_DOCUMENT_MAIN
62+
return docx, Package_, document_
63+
64+
@pytest.fixture
65+
def open_fixture(self, Package_, document_):
66+
docx = 'foobar.docx'
67+
document_part = Package_.open.return_value.main_document_part
68+
document_part.document = document_
69+
document_part.content_type = CT.WML_DOCUMENT_MAIN
70+
return docx, Package_, document_
71+
72+
@pytest.fixture
73+
def raise_fixture(self, Package_):
74+
not_a_docx = 'foobar.xlsx'
75+
Package_.open.return_value.main_document_part.content_type = 'BOGUS'
76+
return not_a_docx
77+
78+
# fixture components ---------------------------------------------
79+
80+
@pytest.fixture
81+
def _default_docx_path_(self, request):
82+
return function_mock(request, 'docx.api._default_docx_path')
83+
84+
@pytest.fixture
85+
def document_(self, request):
86+
return instance_mock(request, docx.document.Document)
87+
88+
@pytest.fixture
89+
def Package_(self, request):
90+
return class_mock(request, 'docx.api.Package')
91+
92+
93+
class DescribeDocumentOld(object):
5694

5795
def it_can_add_a_heading(self, add_heading_fixture):
5896
document, text, level, style, paragraph_ = add_heading_fixture
@@ -219,10 +257,6 @@ def num_part_get_fixture(self, document, document_part_, numbering_part_):
219257
document_part_.part_related_by.return_value = numbering_part_
220258
return document, document_part_, numbering_part_
221259

222-
@pytest.fixture
223-
def open_fixture(self, docx_, Package_, package_, document_part_):
224-
return docx_, Package_, package_, document_part_
225-
226260
@pytest.fixture
227261
def paragraphs_fixture(self, document, paragraphs_):
228262
return document, paragraphs_
@@ -254,10 +288,6 @@ def add_paragraph_(self, request, paragraph_):
254288
def core_properties_(self, request):
255289
return instance_mock(request, CoreProperties)
256290

257-
@pytest.fixture
258-
def default_docx_(self, request):
259-
return var_mock(request, 'docx.api._default_docx_path')
260-
261291
@pytest.fixture
262292
def Document_inline_shapes_(self, request, inline_shapes_):
263293
return property_mock(
@@ -268,6 +298,10 @@ def Document_inline_shapes_(self, request, inline_shapes_):
268298
def document(self, open_):
269299
return Document()
270300

301+
@pytest.fixture
302+
def document_obj_(self, request):
303+
return instance_mock(request, docx.document.Document)
304+
271305
@pytest.fixture
272306
def document_part_(
273307
self, request, paragraph_, paragraphs_, section_, table_,
@@ -282,10 +316,6 @@ def document_part_(
282316
document_part_.tables = tables_
283317
return document_part_
284318

285-
@pytest.fixture
286-
def docx_(self, request):
287-
return instance_mock(request, str)
288-
289319
@pytest.fixture
290320
def inline_shapes_(self, request):
291321
return instance_mock(request, InlineShapes)
@@ -307,10 +337,12 @@ def numbering_part_(self, request):
307337
return instance_mock(request, NumberingPart)
308338

309339
@pytest.fixture
310-
def open_(self, request, document_part_, package_):
311-
return method_mock(
312-
request, Document, '_open',
313-
return_value=(document_part_, package_)
340+
def open_(self, request, document_obj_, document_part_, package_):
341+
document_part_.package = package_
342+
document_obj_._part = document_part_
343+
return function_mock(
344+
request, 'docx.api.DocumentNew',
345+
return_value=document_obj_
314346
)
315347

316348
@pytest.fixture
@@ -322,7 +354,7 @@ def Package_(self, request, package_):
322354
@pytest.fixture
323355
def package_(self, request, document_part_):
324356
package_ = instance_mock(request, Package)
325-
package_.main_document = document_part_
357+
package_.main_document_part = document_part_
326358
return package_
327359

328360
@pytest.fixture

0 commit comments

Comments
 (0)