From eadd73759eee9fa72226e28d922ac451ffb74837 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 14:15:30 -0800 Subject: [PATCH 001/367] 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 --- docx/__init__.py | 3 +- docx/api.py | 46 +++++---- docx/document.py | 23 +++++ docx/opc/package.py | 2 +- docx/parts/document.py | 8 ++ features/api-open-document.feature | 16 +++ features/steps/api.py | 32 +++++- features/steps/test_files/doc-default.docx | Bin 0 -> 21366 bytes tests/test_api.py | 112 +++++++++++++-------- 9 files changed, 175 insertions(+), 67 deletions(-) create mode 100644 docx/document.py create mode 100644 features/api-open-document.feature create mode 100644 features/steps/test_files/doc-default.docx diff --git a/docx/__init__.py b/docx/__init__.py index 5e0fb6edb..acf37b458 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -1,6 +1,7 @@ # encoding: utf-8 -from docx.api import Document # noqa +from docx.api import Document # noqa +from docx.api import DocumentNew # noqa __version__ = '0.8.1' diff --git a/docx/api.py b/docx/api.py index 6fdc132de..d3b7c495f 100644 --- a/docx/api.py +++ b/docx/api.py @@ -18,8 +18,27 @@ from docx.shared import lazyproperty -_thisdir = os.path.split(__file__)[0] -_default_docx_path = os.path.join(_thisdir, 'templates', 'default.docx') +def DocumentNew(docx=None): + """ + Return a |Document| object loaded from *docx*, where *docx* can be + either a path to a ``.docx`` file (a string) or a file-like object. If + *docx* is missing or ``None``, the built-in default document "template" + is loaded. + """ + docx = _default_docx_path() if docx is None else docx + document_part = Package.open(docx).main_document_part + if document_part.content_type != CT.WML_DOCUMENT_MAIN: + tmpl = "file '%s' is not a Word file, content type is '%s'" + raise ValueError(tmpl % (docx, document_part.content_type)) + return document_part.document + + +def _default_docx_path(): + """ + Return the path to the built-in default .docx package. + """ + _thisdir = os.path.split(__file__)[0] + return os.path.join(_thisdir, 'templates', 'default.docx') class Document(object): @@ -30,10 +49,9 @@ class Document(object): is loaded. """ def __init__(self, docx=None): - super(Document, self).__init__() - document_part, package = self._open(docx) - self._document_part = document_part - self._package = package + document = DocumentNew(docx) + self._document_part = document._part + self._package = document._part.package def add_heading(self, text='', level=1): """ @@ -172,19 +190,3 @@ def tables(self): such as ```` or ```` do not appear in this list. """ return self._document_part.tables - - @staticmethod - def _open(docx): - """ - Return a (document_part, package) 2-tuple loaded from *docx*, where - *docx* can be either a path to a ``.docx`` file (a string) or a - file-like object. If *docx* is ``None``, the built-in default - document "template" is loaded. - """ - docx = _default_docx_path if docx is None else docx - package = Package.open(docx) - document_part = package.main_document - if document_part.content_type != CT.WML_DOCUMENT_MAIN: - tmpl = "file '%s' is not a Word file, content type is '%s'" - raise ValueError(tmpl % (docx, document_part.content_type)) - return document_part, package diff --git a/docx/document.py b/docx/document.py new file mode 100644 index 000000000..854e7e351 --- /dev/null +++ b/docx/document.py @@ -0,0 +1,23 @@ +# encoding: utf-8 + +""" +|Document| and closely related objects +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .shared import ElementProxy + + +class Document(ElementProxy): + """ + WordprocessingML (WML) document. + """ + + __slots__ = ('_part',) + + def __init__(self, element, part): + super(Document, self).__init__(element) + self._part = part diff --git a/docx/opc/package.py b/docx/opc/package.py index 3595f46a6..b0ea37ea5 100644 --- a/docx/opc/package.py +++ b/docx/opc/package.py @@ -98,7 +98,7 @@ def load_rel(self, reltype, target, rId, is_external=False): return self.rels.add_relationship(reltype, target, rId, is_external) @property - def main_document(self): + def main_document_part(self): """ Return a reference to the main document part for this package. Examples include a document part for a WordprocessingML package, a diff --git a/docx/parts/document.py b/docx/parts/document.py index 9055ecdb5..1e24423c0 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -11,6 +11,7 @@ from collections import Sequence from ..blkcntnr import BlockItemContainer +from ..document import Document from ..enum.section import WD_SECTION from ..opc.constants import RELATIONSHIP_TYPE as RT from ..opc.part import XmlPart @@ -53,6 +54,13 @@ def body(self): """ return _Body(self._element.body, self) + @property + def document(self): + """ + A |Document| object providing access to the content of this document. + """ + return Document(self._element, self) + def get_or_add_image_part(self, image_descriptor): """ Return an ``(image_part, rId)`` 2-tuple for the image identified by diff --git a/features/api-open-document.feature b/features/api-open-document.feature new file mode 100644 index 000000000..9f9f67c70 --- /dev/null +++ b/features/api-open-document.feature @@ -0,0 +1,16 @@ +Feature: Open a document + In order work on a document + As a developer using python-docx + I need a way to open a document + + + Scenario: Open a specified document + Given I have python-docx installed + When I call docx.Document() with the path of a .docx file + Then document is a Document object + + + Scenario: Open the default document + Given I have python-docx installed + When I call docx.Document() with no arguments + Then document is a Document object diff --git a/features/steps/api.py b/features/steps/api.py index 62c5ec5d9..9144bd46e 100644 --- a/features/steps/api.py +++ b/features/steps/api.py @@ -4,15 +4,25 @@ Step implementations for basic API features """ -from behave import then, when +from behave import given, then, when +import docx + +from docx import DocumentNew from docx.shared import Inches from docx.table import Table -from helpers import test_file +from helpers import test_docx, test_file + + +# given ==================================================== +@given('I have python-docx installed') +def given_I_have_python_docx_installed(context): + pass -# when ==================================================== + +# when ===================================================== @when('I add a 2 x 2 table specifying only row and column count') 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): context.picture = document.add_picture(test_file('monty-truth.png')) +@when('I call docx.Document() with no arguments') +def when_I_call_docx_Document_with_no_arguments(context): + context.document = DocumentNew() + + +@when('I call docx.Document() with the path of a .docx file') +def when_I_call_docx_Document_with_the_path_of_a_docx_file(context): + context.document = DocumentNew(test_docx('doc-default')) + + # then ===================================================== +@then('document is a Document object') +def then_document_is_a_Document_object(context): + document = context.document + assert isinstance(document, docx.document.Document) + + @then('the document contains a 2 x 2 table') def then_document_contains_2x2_table(context): document = context.document diff --git a/features/steps/test_files/doc-default.docx b/features/steps/test_files/doc-default.docx new file mode 100644 index 0000000000000000000000000000000000000000..442db336e496e5611ae15932908908ef3ebef338 GIT binary patch literal 21366 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 Date: Sat, 14 Feb 2015 16:16:48 -0800 Subject: [PATCH 002/367] doc: add Document._body --- docx/document.py | 23 +++++++++++++++++++++- tests/test_document.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/test_document.py diff --git a/docx/document.py b/docx/document.py index 854e7e351..b4c360508 100644 --- a/docx/document.py +++ b/docx/document.py @@ -8,6 +8,7 @@ absolute_import, division, print_function, unicode_literals ) +from .blkcntnr import BlockItemContainer from .shared import ElementProxy @@ -16,8 +17,28 @@ class Document(ElementProxy): WordprocessingML (WML) document. """ - __slots__ = ('_part',) + __slots__ = ('_part', '__body') def __init__(self, element, part): super(Document, self).__init__(element) self._part = part + self.__body = None + + @property + def _body(self): + """ + The |_Body| instance containing the content for this document. + """ + if self.__body is None: + self.__body = _Body(self._element.body, self) + return self.__body + + +class _Body(BlockItemContainer): + """ + Proxy for ```` element in this document, having primarily a + container role. + """ + def __init__(self, body_elm, parent): + super(_Body, self).__init__(body_elm, parent) + self._body = body_elm diff --git a/tests/test_document.py b/tests/test_document.py new file mode 100644 index 000000000..11855bb71 --- /dev/null +++ b/tests/test_document.py @@ -0,0 +1,44 @@ +# encoding: utf-8 + +""" +Test suite for the docx.document module +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest + +from docx.document import _Body, Document + +from .unitutil.cxml import element +from .unitutil.mock import class_mock, instance_mock + + +class DescribeDocument(object): + + def it_provides_access_to_the_document_body(self, body_fixture): + document, body_elm, _Body_, body_ = body_fixture + body = document._body + _Body_.assert_called_once_with(body_elm, document) + assert body is body_ + + # fixtures ------------------------------------------------------- + + @pytest.fixture + def body_fixture(self, _Body_, body_): + document_elm = element('w:document/w:body') + body_elm = document_elm[0] + document = Document(document_elm, None) + return document, body_elm, _Body_, body_ + + # fixture components --------------------------------------------- + + @pytest.fixture + def _Body_(self, request, body_): + return class_mock(request, 'docx.document._Body', return_value=body_) + + @pytest.fixture + def body_(self, request): + return instance_mock(request, _Body) From be56d1db7e193f9352522cd5fde884a87e0fe480 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 16:36:57 -0800 Subject: [PATCH 003/367] acpt: migrate doc-add-paragraph.feature --- ...raph.feature => doc-add-paragraph.feature} | 8 ++--- features/steps/api.py | 24 --------------- features/steps/document.py | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 28 deletions(-) rename features/{api-add-paragraph.feature => doc-add-paragraph.feature} (82%) diff --git a/features/api-add-paragraph.feature b/features/doc-add-paragraph.feature similarity index 82% rename from features/api-add-paragraph.feature rename to features/doc-add-paragraph.feature index ead200014..1e5428f92 100644 --- a/features/api-add-paragraph.feature +++ b/features/doc-add-paragraph.feature @@ -1,17 +1,17 @@ Feature: Add a paragraph with optional text and style In order to populate the text of a document - As a programmer using the basic python-docx API - I want to add a styled paragraph of text in a single step + As a developer using python-docx + I need a way to add a styled paragraph of text in a single step Scenario: Add an empty paragraph - Given a document + Given a blank document When I add a paragraph without specifying text or style Then the last paragraph is the empty paragraph I added Scenario: Add a paragraph specifying its text - Given a document + Given a blank document When I add a paragraph specifying its text Then the last paragraph contains the text I specified diff --git a/features/steps/api.py b/features/steps/api.py index 9144bd46e..d047ce6ab 100644 --- a/features/steps/api.py +++ b/features/steps/api.py @@ -56,30 +56,6 @@ def when_add_page_break_to_document(context): document.add_page_break() -@when('I add a paragraph specifying its style as a {kind}') -def when_I_add_a_paragraph_specifying_its_style_as_a(context, kind): - document = context.document - style = context.style = document.styles['Heading 1'] - style_spec = { - 'style object': style, - 'style name': 'Heading 1', - }[kind] - document.add_paragraph(style=style_spec) - - -@when('I add a paragraph specifying its text') -def when_add_paragraph_specifying_text(context): - document = context.document - context.paragraph_text = 'foobar' - document.add_paragraph(context.paragraph_text) - - -@when('I add a paragraph without specifying text or style') -def when_add_paragraph_without_specifying_text_or_style(context): - document = context.document - document.add_paragraph() - - @when('I add a picture specifying 1.75" width and 2.5" height') def when_add_picture_specifying_width_and_height(context): document = context.document diff --git a/features/steps/document.py b/features/steps/document.py index b99b27d12..c5760f9da 100644 --- a/features/steps/document.py +++ b/features/steps/document.py @@ -18,6 +18,11 @@ # given =================================================== +@given('a blank document') +def given_a_blank_document(context): + context.document = Document() + + @given('a document having three sections') def given_a_document_having_three_sections(context): context.document = Document(test_docx('doc-access-sections')) @@ -38,6 +43,30 @@ def given_a_single_section_document_having_portrait_layout(context): # when ==================================================== +@when('I add a paragraph specifying its style as a {kind}') +def when_I_add_a_paragraph_specifying_its_style_as_a(context, kind): + document = context.document + style = context.style = document.styles['Heading 1'] + style_spec = { + 'style object': style, + 'style name': 'Heading 1', + }[kind] + document.add_paragraph(style=style_spec) + + +@when('I add a paragraph specifying its text') +def when_add_paragraph_specifying_text(context): + document = context.document + context.paragraph_text = 'foobar' + document.add_paragraph(context.paragraph_text) + + +@when('I add a paragraph without specifying text or style') +def when_add_paragraph_without_specifying_text_or_style(context): + document = context.document + document.add_paragraph() + + @when('I add an even-page section to the document') def when_I_add_an_even_page_section_to_the_document(context): context.section = context.document.add_section(WD_SECTION.EVEN_PAGE) From fb5e7d798c41ba782e553b7c06d254782cc46b44 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 16:42:43 -0800 Subject: [PATCH 004/367] doc: add Document.part --- docx/document.py | 7 +++++++ tests/test_document.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/docx/document.py b/docx/document.py index b4c360508..fd7df6e5f 100644 --- a/docx/document.py +++ b/docx/document.py @@ -24,6 +24,13 @@ def __init__(self, element, part): self._part = part self.__body = None + @property + def part(self): + """ + The |DocumentPart| object of this document. + """ + return self._part + @property def _body(self): """ diff --git a/tests/test_document.py b/tests/test_document.py index 11855bb71..3ee3e15ab 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -11,6 +11,7 @@ import pytest from docx.document import _Body, Document +from docx.parts.document import DocumentPart from .unitutil.cxml import element from .unitutil.mock import class_mock, instance_mock @@ -18,6 +19,10 @@ class DescribeDocument(object): + def it_provides_access_to_the_document_part(self, part_fixture): + document, part_ = part_fixture + assert document.part is part_ + def it_provides_access_to_the_document_body(self, body_fixture): document, body_elm, _Body_, body_ = body_fixture body = document._body @@ -33,6 +38,11 @@ def body_fixture(self, _Body_, body_): document = Document(document_elm, None) return document, body_elm, _Body_, body_ + @pytest.fixture + def part_fixture(self, document_part_): + document = Document(None, document_part_) + return document, document_part_ + # fixture components --------------------------------------------- @pytest.fixture @@ -42,3 +52,7 @@ def _Body_(self, request, body_): @pytest.fixture def body_(self, request): return instance_mock(request, _Body) + + @pytest.fixture + def document_part_(self, request): + return instance_mock(request, DocumentPart) From 4f983127cb002058ae5b6ae36ee047b6ec58a138 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 16:54:42 -0800 Subject: [PATCH 005/367] doc: add Document.add_paragraph() --- docx/document.py | 11 +++++++++++ tests/test_document.py | 28 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docx/document.py b/docx/document.py index fd7df6e5f..9a6a26669 100644 --- a/docx/document.py +++ b/docx/document.py @@ -24,6 +24,17 @@ def __init__(self, element, part): self._part = part self.__body = None + def add_paragraph(self, text='', style=None): + """ + Return a paragraph newly added to the end of the document, populated + with *text* and having paragraph style *style*. *text* can contain + tab (``\\t``) characters, which are converted to the appropriate XML + form for a tab. *text* can also include newline (``\\n``) or carriage + return (``\\r``) characters, each of which is converted to a line + break. + """ + return self._body.add_paragraph(text, style) + @property def part(self): """ diff --git a/tests/test_document.py b/tests/test_document.py index 3ee3e15ab..55af6cf40 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -12,13 +12,20 @@ from docx.document import _Body, Document from docx.parts.document import DocumentPart +from docx.text.paragraph import Paragraph from .unitutil.cxml import element -from .unitutil.mock import class_mock, instance_mock +from .unitutil.mock import class_mock, instance_mock, property_mock class DescribeDocument(object): + def it_can_add_a_paragraph(self, add_paragraph_fixture): + document, text, style, paragraph_ = add_paragraph_fixture + paragraph = document.add_paragraph(text, style) + document._body.add_paragraph.assert_called_once_with(text, style) + assert paragraph is paragraph_ + def it_provides_access_to_the_document_part(self, part_fixture): document, part_ = part_fixture assert document.part is part_ @@ -31,6 +38,17 @@ def it_provides_access_to_the_document_body(self, body_fixture): # fixtures ------------------------------------------------------- + @pytest.fixture(params=[ + ('', None), + ('', 'Heading 1'), + ('foo\rbar', 'Body Text'), + ]) + def add_paragraph_fixture(self, request, body_prop_, paragraph_): + text, style = request.param + document = Document(None, None) + body_prop_.return_value.add_paragraph.return_value = paragraph_ + return document, text, style, paragraph_ + @pytest.fixture def body_fixture(self, _Body_, body_): document_elm = element('w:document/w:body') @@ -53,6 +71,14 @@ def _Body_(self, request, body_): def body_(self, request): return instance_mock(request, _Body) + @pytest.fixture + def body_prop_(self, request): + return property_mock(request, Document, '_body') + @pytest.fixture def document_part_(self, request): return instance_mock(request, DocumentPart) + + @pytest.fixture + def paragraph_(self, request): + return instance_mock(request, Paragraph) From 11394a0d37438f2335f6e8cf6b5fe0a77c0dab0f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 17:06:06 -0800 Subject: [PATCH 006/367] api: remove old Document.add_paragraph() --- docx/api.py | 12 ++---------- tests/test_api.py | 18 ------------------ 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/docx/api.py b/docx/api.py index d3b7c495f..a0899cd1a 100644 --- a/docx/api.py +++ b/docx/api.py @@ -49,7 +49,7 @@ class Document(object): is loaded. """ def __init__(self, docx=None): - document = DocumentNew(docx) + self._document = document = DocumentNew(docx) self._document_part = document._part self._package = document._part.package @@ -78,15 +78,7 @@ def add_page_break(self): return p def add_paragraph(self, text='', style=None): - """ - Return a paragraph newly added to the end of the document, populated - with *text* and having paragraph style *style*. *text* can contain - tab (``\\t``) characters, which are converted to the appropriate XML - form for a tab. *text* can also include newline (``\\n``) or carriage - return (``\\r``) characters, each of which is converted to a line - break. - """ - return self._document_part.add_paragraph(text, style) + return self._document.add_paragraph(text, style) def add_picture(self, image_path_or_stream, width=None, height=None): """ diff --git a/tests/test_api.py b/tests/test_api.py index ee127236c..c3c17bfe5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -104,14 +104,6 @@ def it_should_raise_on_heading_level_out_of_range(self, document): with pytest.raises(ValueError): document.add_heading(level=10) - def it_can_add_a_paragraph(self, add_paragraph_fixture): - document, text, style, paragraph_ = add_paragraph_fixture - paragraph = document.add_paragraph(text, style) - document._document_part.add_paragraph.assert_called_once_with( - text, style - ) - assert paragraph is paragraph_ - def it_can_add_a_page_break(self, add_page_break_fixture): document, document_part_, paragraph_, run_ = add_page_break_fixture paragraph = document.add_page_break() @@ -197,16 +189,6 @@ def it_creates_numbering_part_on_first_access_if_not_present( # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('', None), - ('', 'Heading 1'), - ('foo\rbar', 'Body Text'), - ]) - def add_paragraph_fixture(self, request, document, document_part_, - paragraph_): - text, style = request.param - return document, text, style, paragraph_ - @pytest.fixture(params=[ (0, 'Title'), (1, 'Heading 1'), From f615c58a9ac64e4bfe4d741b884a2d96571a199d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 14 Feb 2015 17:16:43 -0800 Subject: [PATCH 007/367] acpt: migrate doc-add-heading.feature --- features/api-add-heading.feature | 30 ------------------------------ features/doc-add-heading.feature | 25 +++++++++++++++++++++++++ features/steps/api.py | 31 ------------------------------- features/steps/document.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 61 deletions(-) delete mode 100644 features/api-add-heading.feature create mode 100644 features/doc-add-heading.feature diff --git a/features/api-add-heading.feature b/features/api-add-heading.feature deleted file mode 100644 index 795797f28..000000000 --- a/features/api-add-heading.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Add a section heading with text - In order add a section heading to a document - As a programmer using the basic python-docx API - I need a method to add a heading with its text in a single step - - - Scenario: Add a heading specifying only its text - Given a document - When I add a heading specifying only its text - Then the style of the last paragraph is 'Heading 1' - And the last paragraph contains the heading text - - - Scenario Outline: Add a heading specifying level - Given a document - When I add a heading specifying level= - Then the style of the last paragraph is '' - - Examples: Heading level styles - | heading level | paragraph style | - | 0 | Title | - | 1 | Heading 1 | - | 2 | Heading 2 | - | 3 | Heading 3 | - | 4 | Heading 4 | - | 5 | Heading 5 | - | 6 | Heading 6 | - | 7 | Heading 7 | - | 8 | Heading 8 | - | 9 | Heading 9 | diff --git a/features/doc-add-heading.feature b/features/doc-add-heading.feature new file mode 100644 index 000000000..33b975d55 --- /dev/null +++ b/features/doc-add-heading.feature @@ -0,0 +1,25 @@ +Feature: Add a heading paragraph + In order add a heading to a document + As a developer using python-docx + I need a way to add a heading with its text and level in a single step + + + Scenario: Add a heading specifying only its text + Given a blank document + When I add a heading specifying only its text + Then the style of the last paragraph is 'Heading 1' + And the last paragraph contains the heading text + + + Scenario Outline: Add a heading specifying level + Given a blank document + When I add a heading specifying level= + Then the style of the last paragraph is '