From 5a3c563bcc05e4489189c2ee1e357f72cbf73010 Mon Sep 17 00:00:00 2001 From: eupharis Date: Wed, 20 Apr 2016 21:58:25 -0700 Subject: [PATCH 1/8] acpt: add scenario for default header access --- docx/header.py | 19 +++++++++++++++++++ features/sct-access-header.feature | 9 +++++++++ features/steps/section.py | 13 +++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 docx/header.py create mode 100644 features/sct-access-header.feature diff --git a/docx/header.py b/docx/header.py new file mode 100644 index 000000000..d93c0016d --- /dev/null +++ b/docx/header.py @@ -0,0 +1,19 @@ +# encoding: utf-8 + +""" +Page headers and footers. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .shared import ElementProxy + + +class Header(ElementProxy): + """ + The default page header of a section. + """ + + __slots__ = () diff --git a/features/sct-access-header.feature b/features/sct-access-header.feature new file mode 100644 index 000000000..a27ea599c --- /dev/null +++ b/features/sct-access-header.feature @@ -0,0 +1,9 @@ +Feature: Access section headers and footers + In order to operate on the headers or footers of a section + As a developer using python-docx + I need access to the section headers and footers + + @wip + Scenario: Access default header of section + Given a section + Then section.header is a Header object diff --git a/features/steps/section.py b/features/steps/section.py index 496e0f17f..91acaff12 100644 --- a/features/steps/section.py +++ b/features/steps/section.py @@ -10,6 +10,7 @@ from docx import Document from docx.enum.section import WD_ORIENT, WD_SECTION +from docx.header import Header from docx.section import Section from docx.shared import Inches @@ -18,6 +19,12 @@ # given ==================================================== +@given('a section') +def given_a_section(context): + document = Document(test_docx('sct-section-props')) + context.section = document.sections[0] + + @given('a section collection containing 3 sections') def given_a_section_collection_containing_3_sections(context): document = Document(test_docx('doc-access-sections')) @@ -137,6 +144,12 @@ def then_len_sections_is_3(context): ) +@then('section.header is a Header object') +def then_section_header_is_a_Header_object(context): + section = context.section + assert isinstance(section.header, Header) + + @then('the reported {margin_side} margin is {inches} inches') def then_the_reported_margin_is_inches(context, margin_side, inches): prop_name = { From 04fd31e7b54f9ced8a30a457471b9c31815b4799 Mon Sep 17 00:00:00 2001 From: eupharis Date: Mon, 9 May 2016 20:59:23 -0700 Subject: [PATCH 2/8] hdr: add Section.header --- docx/section.py | 13 +++++++++++++ features/sct-access-header.feature | 2 +- tests/test_section.py | 26 ++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docx/section.py b/docx/section.py index 16221243b..9b5d169d9 100644 --- a/docx/section.py +++ b/docx/section.py @@ -8,6 +8,9 @@ from collections import Sequence +from .header import Header +from .shared import lazyproperty + class Sections(Sequence): """ @@ -80,6 +83,16 @@ def gutter(self): def gutter(self, value): self._sectPr.gutter = value + @lazyproperty + def header(self): + """ + Return the |Header| object representing the default header for this + section. A |Header| object is always returned, whether such a header + is present or not. The header itself is added, updated, or removed + using the returned object. + """ + return Header(self._sectPr) + @property def header_distance(self): """ diff --git a/features/sct-access-header.feature b/features/sct-access-header.feature index a27ea599c..3b5a33893 100644 --- a/features/sct-access-header.feature +++ b/features/sct-access-header.feature @@ -3,7 +3,7 @@ Feature: Access section headers and footers As a developer using python-docx I need access to the section headers and footers - @wip + Scenario: Access default header of section Given a section Then section.header is a Header object diff --git a/tests/test_section.py b/tests/test_section.py index a497aa727..d51fcfe00 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -9,10 +9,12 @@ import pytest from docx.enum.section import WD_ORIENT, WD_SECTION +from docx.header import Header from docx.section import Section, Sections from docx.shared import Inches from .unitutil.cxml import element, xml +from .unitutil.mock import class_mock, instance_mock class DescribeSections(object): @@ -109,8 +111,20 @@ def it_can_change_its_page_margins(self, margins_set_fixture): setattr(section, margin_prop_name, new_value) assert section._sectPr.xml == expected_xml + def it_provides_access_to_its_header(self, header_fixture): + section, Header_, sectPr, header_ = header_fixture + header = section.header + Header_.assert_called_once_with(sectPr) + assert header is header_ + # fixtures ------------------------------------------------------- + @pytest.fixture + def header_fixture(self, Header_, header_): + sectPr = element('w:sectPr') + section = Section(sectPr) + return section, Header_, sectPr, header_ + @pytest.fixture(params=[ ('w:sectPr/w:pgMar{w:left=120}', 'left_margin', 76200), ('w:sectPr/w:pgMar{w:right=240}', 'right_margin', 152400), @@ -247,3 +261,15 @@ def start_type_set_fixture(self, request): section = Section(element(initial_cxml)) expected_xml = xml(expected_cxml) return section, new_start_type, expected_xml + + # fixture components --------------------------------------------- + + @pytest.fixture + def Header_(self, request, header_): + return class_mock( + request, 'docx.section.Header', return_value=header_ + ) + + @pytest.fixture + def header_(self, request): + return instance_mock(request, Header) From b14766c37fc62533d54691cc1caf8564d0bf1a34 Mon Sep 17 00:00:00 2001 From: eupharis Date: Wed, 11 May 2016 14:01:19 -0700 Subject: [PATCH 3/8] acpt: scenarios for Header.is_linked_to_previous --- features/hdr-header-props.feature | 15 ++++++++ features/steps/header.py | 36 ++++++++++++++++++ .../steps/test_files/hdr-header-props.docx | Bin 0 -> 12737 bytes 3 files changed, 51 insertions(+) create mode 100644 features/hdr-header-props.feature create mode 100644 features/steps/header.py create mode 100644 features/steps/test_files/hdr-header-props.docx diff --git a/features/hdr-header-props.feature b/features/hdr-header-props.feature new file mode 100644 index 000000000..498ca6d7f --- /dev/null +++ b/features/hdr-header-props.feature @@ -0,0 +1,15 @@ +Feature: Header properties + In order to interact with document headers + As a developer using python-docx + I need read/write properties on the Header object + + + @wip + Scenario Outline: Get Header.is_linked_to_previous + Given a header definition + Then header.is_linked_to_previous is + + Examples: Header.is_linked_to_previous states + | having-or-no | value | + | having a | False | + | having no | True | diff --git a/features/steps/header.py b/features/steps/header.py new file mode 100644 index 000000000..d65b17b22 --- /dev/null +++ b/features/steps/header.py @@ -0,0 +1,36 @@ +# encoding: utf-8 + +""" +Step implementations for header-related features +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from behave import given, then + +from docx import Document + +from helpers import test_docx + + +# given =================================================== + +@given('a header {having_or_no} definition') +def given_a_header_having_or_no_definition(context, having_or_no): + filename = { + 'having a': 'hdr-header-props', + 'having no': 'doc-default', + }[having_or_no] + document = Document(test_docx(filename)) + context.header = document.sections[0].header + + +# then ===================================================== + +@then(u'header.is_linked_to_previous is {value}') +def then_header_is_linked_to_previous_is_value(context, value): + expected_value = {'True': True, 'False': False}[value] + header = context.header + assert header.is_linked_to_previous is expected_value diff --git a/features/steps/test_files/hdr-header-props.docx b/features/steps/test_files/hdr-header-props.docx new file mode 100644 index 0000000000000000000000000000000000000000..fb54f28c6ab8a019738936486e2bee549c5d7d70 GIT binary patch literal 12737 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 Date: Wed, 18 May 2016 18:35:46 -0700 Subject: [PATCH 4/8] hdr: add _BaseHeaderFooter.is_linked_to_previous --- docs/api/enum/WdHeaderFooterIndex.rst | 22 ++++++++++++++ docs/api/enum/index.rst | 1 + docx/enum/header.py | 40 +++++++++++++++++++++++++ docx/header.py | 28 ++++++++++++++++-- docx/oxml/section.py | 42 +++++++++++++++++---------- docx/section.py | 3 +- features/hdr-header-props.feature | 1 - tests/test_header.py | 35 ++++++++++++++++++++++ tests/test_section.py | 5 +++- 9 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 docs/api/enum/WdHeaderFooterIndex.rst create mode 100644 docx/enum/header.py create mode 100644 tests/test_header.py diff --git a/docs/api/enum/WdHeaderFooterIndex.rst b/docs/api/enum/WdHeaderFooterIndex.rst new file mode 100644 index 000000000..41ddd3490 --- /dev/null +++ b/docs/api/enum/WdHeaderFooterIndex.rst @@ -0,0 +1,22 @@ +.. _WdHeaderFooterIndex: + +``WD_HEADER_FOOTER_INDEX`` +========================== + +alias: **WD_HEADER_FOOTER** + +Specifies the type of a header or footer. + +---- + +PRIMARY + The header or footer appearing on all pages except when an even and/or + first-page header/footer is defined. + +FIRST_PAGE + The header or footer appearing only on the first page of the specified + section. + +EVEN_PAGES + The header or footer appearing on even numbered (verso) pages in the + specified section. diff --git a/docs/api/enum/index.rst b/docs/api/enum/index.rst index 9c7371dcf..6dee7631e 100644 --- a/docs/api/enum/index.rst +++ b/docs/api/enum/index.rst @@ -13,6 +13,7 @@ can be found here: WdAlignParagraph WdBuiltinStyle WdColorIndex + WdHeaderFooterIndex WdLineSpacing WdOrientation WdSectionStart diff --git a/docx/enum/header.py b/docx/enum/header.py new file mode 100644 index 000000000..79a8695b9 --- /dev/null +++ b/docx/enum/header.py @@ -0,0 +1,40 @@ +# encoding: utf-8 + +""" +Enumerations related to headers and footers +""" + +from __future__ import ( + absolute_import, print_function, unicode_literals, division +) + +from .base import alias, XmlEnumeration, XmlMappedEnumMember + + +@alias('WD_HEADER_FOOTER') +class WD_HEADER_FOOTER_INDEX(XmlEnumeration): + """ + alias: **WD_HEADER_FOOTER** + + Specifies the type of a header or footer. + """ + + __ms_name__ = 'WdHeaderFooterIndex' + + __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff839314.aspx' + + __members__ = ( + XmlMappedEnumMember( + 'PRIMARY', 1, 'default', 'The header or footer appearing on all ' + 'pages except when an even and/or first-page header/footer is de' + 'fined.' + ), + XmlMappedEnumMember( + 'FIRST_PAGE', 2, 'first', 'The header or footer appearing only o' + 'n the first page of the specified section.' + ), + XmlMappedEnumMember( + 'EVEN_PAGES', 3, 'even', 'The header or footer appearing on even' + ' numbered (verso) pages in the specified section.' + ), + ) diff --git a/docx/header.py b/docx/header.py index d93c0016d..98d531a95 100644 --- a/docx/header.py +++ b/docx/header.py @@ -11,9 +11,31 @@ from .shared import ElementProxy -class Header(ElementProxy): +class _BaseHeaderFooter(ElementProxy): """ - The default page header of a section. + Base class for header and footer objects. """ - __slots__ = () + __slots__ = ('_sectPr', '_type') + + def __init__(self, element, parent, type): + super(_BaseHeaderFooter, self).__init__(element, parent) + self._sectPr = element + self._type = type + + @property + def is_linked_to_previous(self): + """ + Boolean representing whether this Header is inherited from + a previous section. + """ + ref = self._sectPr.get_headerReference_of_type(self._type) + if ref is None: + return True + return False + + +class Header(_BaseHeaderFooter): + """ + One of the page headers for a section. + """ diff --git a/docx/oxml/section.py b/docx/oxml/section.py index cf76b67ed..5712870a4 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -8,9 +8,12 @@ from copy import deepcopy +from ..enum.header import WD_HEADER_FOOTER from ..enum.section import WD_ORIENTATION, WD_SECTION_START from .simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure -from .xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne +from .xmlchemy import ( + BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne +) class CT_PageMar(BaseOxmlElement): @@ -41,22 +44,18 @@ class CT_SectPr(BaseOxmlElement): """ ```` element, the container element for section properties. """ - __child_sequence__ = ( - 'w:footnotePr', 'w:endnotePr', 'w:type', 'w:pgSz', 'w:pgMar', - 'w:paperSrc', 'w:pgBorders', 'w:lnNumType', 'w:pgNumType', 'w:cols', - 'w:formProt', 'w:vAlign', 'w:noEndnote', 'w:titlePg', - 'w:textDirection', 'w:bidi', 'w:rtlGutter', 'w:docGrid', - 'w:printerSettings', 'w:sectPrChange', + _tag_seq = ( + 'w:headerReference', 'w:footerReference', 'w:footnotePr', + 'w:endnotePr', 'w:type', 'w:pgSz', 'w:pgMar', 'w:paperSrc', + 'w:pgBorders', 'w:lnNumType', 'w:pgNumType', 'w:cols', 'w:formProt', + 'w:vAlign', 'w:noEndnote', 'w:titlePg', 'w:textDirection', 'w:bidi', + 'w:rtlGutter', 'w:docGrid', 'w:printerSettings', 'w:sectPrChange', ) - type = ZeroOrOne('w:type', successors=( - __child_sequence__[__child_sequence__.index('w:type')+1:] - )) - pgSz = ZeroOrOne('w:pgSz', successors=( - __child_sequence__[__child_sequence__.index('w:pgSz')+1:] - )) - pgMar = ZeroOrOne('w:pgMar', successors=( - __child_sequence__[__child_sequence__.index('w:pgMar')+1:] - )) + headerReference = ZeroOrMore('w:headerReference', successors=_tag_seq[1:]) + type = ZeroOrOne('w:type', successors=_tag_seq[5:]) + pgSz = ZeroOrOne('w:pgSz', successors=_tag_seq[6:]) + pgMar = ZeroOrOne('w:pgMar', successors=_tag_seq[7:]) + del _tag_seq @property def bottom_margin(self): @@ -102,6 +101,17 @@ def footer(self, value): pgMar = self.get_or_add_pgMar() pgMar.footer = value + def get_headerReference_of_type(self, type_member): + """ + Return the `w:headerReference` child having type attribute value + associated with *type_member*, or |None| if not present. + """ + type_str = WD_HEADER_FOOTER.to_xml(type_member) + matches = self.xpath('w:headerReference[@w:type="%s"]' % type_str) + if matches: + return matches[0] + return None + @property def gutter(self): """ diff --git a/docx/section.py b/docx/section.py index 9b5d169d9..aa40009f9 100644 --- a/docx/section.py +++ b/docx/section.py @@ -8,6 +8,7 @@ from collections import Sequence +from .enum.header import WD_HEADER_FOOTER from .header import Header from .shared import lazyproperty @@ -91,7 +92,7 @@ def header(self): is present or not. The header itself is added, updated, or removed using the returned object. """ - return Header(self._sectPr) + return Header(self._sectPr, self, WD_HEADER_FOOTER.PRIMARY) @property def header_distance(self): diff --git a/features/hdr-header-props.feature b/features/hdr-header-props.feature index 498ca6d7f..3411e25c0 100644 --- a/features/hdr-header-props.feature +++ b/features/hdr-header-props.feature @@ -4,7 +4,6 @@ Feature: Header properties I need read/write properties on the Header object - @wip Scenario Outline: Get Header.is_linked_to_previous Given a header definition Then header.is_linked_to_previous is diff --git a/tests/test_header.py b/tests/test_header.py new file mode 100644 index 000000000..d63356d5e --- /dev/null +++ b/tests/test_header.py @@ -0,0 +1,35 @@ +# encoding: utf-8 + +""" +Test suite for the docx.header module +""" + +from __future__ import ( + absolute_import, print_function, unicode_literals, division +) + +import pytest + +from docx.enum.header import WD_HEADER_FOOTER +from docx.header import Header + +from .unitutil.cxml import element + + +class Describe_BaseHeaderFooter(object): + + def it_knows_whether_it_is_linked_to_previous(self, is_linked_fixture): + header, expected_value = is_linked_fixture + assert header.is_linked_to_previous is expected_value + + # fixtures ------------------------------------------------------- + + @pytest.fixture(params=[ + ('w:sectPr', True), + ('w:sectPr/w:headerReference{w:type=default}', False), + ('w:sectPr/w:headerReference{w:type=even}', True), + ]) + def is_linked_fixture(self, request): + sectPr_cxml, expected_value = request.param + header = Header(element(sectPr_cxml), None, WD_HEADER_FOOTER.PRIMARY) + return header, expected_value diff --git a/tests/test_section.py b/tests/test_section.py index d51fcfe00..8f25d6d8f 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -8,6 +8,7 @@ import pytest +from docx.enum.header import WD_HEADER_FOOTER from docx.enum.section import WD_ORIENT, WD_SECTION from docx.header import Header from docx.section import Section, Sections @@ -114,7 +115,9 @@ def it_can_change_its_page_margins(self, margins_set_fixture): def it_provides_access_to_its_header(self, header_fixture): section, Header_, sectPr, header_ = header_fixture header = section.header - Header_.assert_called_once_with(sectPr) + Header_.assert_called_once_with( + sectPr, section, WD_HEADER_FOOTER.PRIMARY + ) assert header is header_ # fixtures ------------------------------------------------------- From f2bdc0060b6c71a897cd1c98438b8e081cbfcc01 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 7 Jun 2016 19:05:21 -0700 Subject: [PATCH 5/8] rfctr: make Section an ElementProxy subclass --- docx/document.py | 4 ++-- docx/section.py | 17 +++++++++-------- tests/test_document.py | 4 ++-- tests/test_section.py | 28 ++++++++++++++-------------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docx/document.py b/docx/document.py index ba94a7990..c52b43a33 100644 --- a/docx/document.py +++ b/docx/document.py @@ -87,7 +87,7 @@ def add_section(self, start_type=WD_SECTION.NEW_PAGE): """ new_sectPr = self._element.body.add_section_break() new_sectPr.start_type = start_type - return Section(new_sectPr) + return Section(new_sectPr, self) def add_table(self, rows, cols, style=None): """ @@ -147,7 +147,7 @@ def sections(self): A |Sections| object providing access to each section in this document. """ - return Sections(self._element) + return Sections(self._element, self) @property def settings(self): diff --git a/docx/section.py b/docx/section.py index aa40009f9..340ce03b5 100644 --- a/docx/section.py +++ b/docx/section.py @@ -10,7 +10,7 @@ from .enum.header import WD_HEADER_FOOTER from .header import Header -from .shared import lazyproperty +from .shared import ElementProxy, lazyproperty class Sections(Sequence): @@ -18,31 +18,32 @@ class Sections(Sequence): Sequence of |Section| objects corresponding to the sections in the document. Supports ``len()``, iteration, and indexed access. """ - def __init__(self, document_elm): + def __init__(self, document_elm, parent): super(Sections, self).__init__() + self._parent = parent self._document_elm = document_elm def __getitem__(self, key): if isinstance(key, slice): sectPr_lst = self._document_elm.sectPr_lst[key] - return [Section(sectPr) for sectPr in sectPr_lst] + return [Section(sectPr, self._parent) for sectPr in sectPr_lst] sectPr = self._document_elm.sectPr_lst[key] - return Section(sectPr) + return Section(sectPr, self._parent) def __iter__(self): for sectPr in self._document_elm.sectPr_lst: - yield Section(sectPr) + yield Section(sectPr, self._parent) def __len__(self): return len(self._document_elm.sectPr_lst) -class Section(object): +class Section(ElementProxy): """ Document section, providing access to section and page setup settings. """ - def __init__(self, sectPr): - super(Section, self).__init__() + def __init__(self, sectPr, parent): + super(Section, self).__init__(sectPr, parent) self._sectPr = sectPr @property diff --git a/tests/test_document.py b/tests/test_document.py index c1cb060ec..1475b1387 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -73,7 +73,7 @@ def it_can_add_a_section(self, add_section_fixture): assert document.element.xml == expected_xml sectPr = document.element.xpath('w:body/w:sectPr')[0] - Section_.assert_called_once_with(sectPr) + Section_.assert_called_once_with(sectPr, document) assert section is section_ def it_can_add_a_table(self, add_table_fixture): @@ -105,7 +105,7 @@ def it_provides_access_to_its_paragraphs(self, paragraphs_fixture): def it_provides_access_to_its_sections(self, sections_fixture): document, Sections_, sections_ = sections_fixture sections = document.sections - Sections_.assert_called_once_with(document._element) + Sections_.assert_called_once_with(document._element, document) assert sections is sections_ def it_provides_access_to_its_settings(self, settings_fixture): diff --git a/tests/test_section.py b/tests/test_section.py index 8f25d6d8f..db9812eba 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -42,17 +42,17 @@ def it_can_access_its_Section_instances_by_index(self, index_fixture): @pytest.fixture def index_fixture(self, document_elm): - sections = Sections(document_elm) + sections = Sections(document_elm, None) return sections, [0, 1] @pytest.fixture def iter_fixture(self, document_elm): - sections = Sections(document_elm) + sections = Sections(document_elm, None) return sections, 2 @pytest.fixture def len_fixture(self, document_elm): - sections = Sections(document_elm) + sections = Sections(document_elm, None) return sections, 2 # fixture components --------------------------------------------- @@ -125,7 +125,7 @@ def it_provides_access_to_its_header(self, header_fixture): @pytest.fixture def header_fixture(self, Header_, header_): sectPr = element('w:sectPr') - section = Section(sectPr) + section = Section(sectPr, None) return section, Header_, sectPr, header_ @pytest.fixture(params=[ @@ -141,7 +141,7 @@ def header_fixture(self, Header_, header_): ]) def margins_get_fixture(self, request): sectPr_cxml, margin_prop_name, expected_value = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) return section, margin_prop_name, expected_value @pytest.fixture(params=[ @@ -165,7 +165,7 @@ def margins_get_fixture(self, request): ]) def margins_set_fixture(self, request): sectPr_cxml, property_name, new_value, expected_cxml = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) expected_xml = xml(expected_cxml) return section, property_name, new_value, expected_xml @@ -177,7 +177,7 @@ def margins_set_fixture(self, request): ]) def orientation_get_fixture(self, request): sectPr_cxml, expected_orientation = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) return section, expected_orientation @pytest.fixture(params=[ @@ -187,7 +187,7 @@ def orientation_get_fixture(self, request): ]) def orientation_set_fixture(self, request): new_orientation, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), None) expected_xml = xml(expected_cxml) return section, new_orientation, expected_xml @@ -198,7 +198,7 @@ def orientation_set_fixture(self, request): ]) def page_height_get_fixture(self, request): sectPr_cxml, expected_page_height = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) return section, expected_page_height @pytest.fixture(params=[ @@ -207,7 +207,7 @@ def page_height_get_fixture(self, request): ]) def page_height_set_fixture(self, request): new_page_height, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), None) expected_xml = xml(expected_cxml) return section, new_page_height, expected_xml @@ -218,7 +218,7 @@ def page_height_set_fixture(self, request): ]) def page_width_get_fixture(self, request): sectPr_cxml, expected_page_width = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) return section, expected_page_width @pytest.fixture(params=[ @@ -227,7 +227,7 @@ def page_width_get_fixture(self, request): ]) def page_width_set_fixture(self, request): new_page_width, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), None) expected_xml = xml(expected_cxml) return section, new_page_width, expected_xml @@ -242,7 +242,7 @@ def page_width_set_fixture(self, request): ]) def start_type_get_fixture(self, request): sectPr_cxml, expected_start_type = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), None) return section, expected_start_type @pytest.fixture(params=[ @@ -261,7 +261,7 @@ def start_type_get_fixture(self, request): ]) def start_type_set_fixture(self, request): initial_cxml, new_start_type, expected_cxml = request.param - section = Section(element(initial_cxml)) + section = Section(element(initial_cxml), None) expected_xml = xml(expected_cxml) return section, new_start_type, expected_xml From 14cb350d348ee75d6e4703f2a698cbab67ee2d10 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 7 Jun 2016 19:10:15 -0700 Subject: [PATCH 6/8] bld: update cxml parser to latest A feature added to the cxml parser in python-pptx automatically generates namespace prefixes for attributes. Update the cxml Element object to use this latest code. Also, don't generate a namespace declration for the xml: namespace prefix. --- tests/unitutil/cxml.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index 6bf0ce3f3..c4be20a48 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -50,6 +50,8 @@ def nsdecls(*nspfxs): """ nsdecls = '' for nspfx in nspfxs: + if nspfx == 'xml': + continue nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx]) return nsdecls @@ -105,16 +107,25 @@ def is_root(self, value): self._is_root = bool(value) @property - def nspfx(self): + def local_nspfxs(self): """ - The namespace prefix of this element, the empty string (``''``) if - the tag is in the default namespace. + The namespace prefixes local to this element, both on the tagname and + all of its attributes. An empty string (``''``) is used to represent + the default namespace for an element tag having no prefix. """ - tagname = self._tagname - idx = tagname.find(':') - if idx == -1: - return '' - return tagname[:idx] + def nspfx(name, is_element=False): + idx = name.find(':') + if idx == -1: + return '' if is_element else None + return name[:idx] + + nspfxs = [nspfx(self._tagname, True)] + for name, val in self._attrs: + pfx = nspfx(name) + if pfx is None or pfx in nspfxs: + continue + nspfxs.append(pfx) + return nspfxs @property def nspfxs(self): @@ -129,7 +140,7 @@ def merge(seq, seq_2): continue seq.append(item) - nspfxs = [self.nspfx] + nspfxs = self.local_nspfxs for child in self._children: merge(nspfxs, child.nspfxs) return nspfxs From 3c7ec6961589e8379db1ec10953405475a3e43b8 Mon Sep 17 00:00:00 2001 From: eupharis Date: Tue, 24 May 2016 13:19:57 -0700 Subject: [PATCH 7/8] acpt: add scenario for _BaseHeader.body --- features/hdr-header-props.feature | 6 ++++++ features/steps/header.py | 15 ++++++++++++++- .../steps/test_files/hdr-header-props.docx | Bin 12737 -> 26804 bytes 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/features/hdr-header-props.feature b/features/hdr-header-props.feature index 3411e25c0..8b1f4836b 100644 --- a/features/hdr-header-props.feature +++ b/features/hdr-header-props.feature @@ -12,3 +12,9 @@ Feature: Header properties | having-or-no | value | | having a | False | | having no | True | + + @wip + Scenario: Get Header.body + Given a header having a definition + Then header.body is a BlockItemContainer object + And header.body contains the text of the header diff --git a/features/steps/header.py b/features/steps/header.py index d65b17b22..21e6ba756 100644 --- a/features/steps/header.py +++ b/features/steps/header.py @@ -29,7 +29,20 @@ def given_a_header_having_or_no_definition(context, having_or_no): # then ===================================================== -@then(u'header.is_linked_to_previous is {value}') +@then('header.body contains the text of the header') +def then_header_body_contains_the_text_of_the_header(context): + header = context.header + text = header.body.paragraphs[0].text + assert text == 'S1HP1' + + +@then('header.body is a BlockItemContainer object') +def then_header_body_is_a_BlockItemContainer_object(context): + header = context.header + assert type(header.body).__name__ == 'BlockItemContainer' + + +@then('header.is_linked_to_previous is {value}') def then_header_is_linked_to_previous_is_value(context, value): expected_value = {'True': True, 'False': False}[value] header = context.header diff --git a/features/steps/test_files/hdr-header-props.docx b/features/steps/test_files/hdr-header-props.docx index fb54f28c6ab8a019738936486e2bee549c5d7d70..81ba271236c0cb1b84e0c23b89ece46316e20bed 100644 GIT binary patch literal 26804 zcmeIbbzB$S^C-UXrlh1nBqgOgq`SLIB;J&CBZ#yjpfm_bH%cSjARwT0cc+w;AlwC? zM}7P}_ulXQ-G6?+-`C~E?r_eTIWu$SoSEG{J1EOR;BWv000{s9a^PhO{=z310QhhN z0I&fhSUoX2TW6@Pv!S|&J=Dp7$=$}9EFBJ(HWh#c+y6QK8*89VQ%-4$1*1W0Tq8b%C=!^pIq!~8G$D+a}Pslypw;IipiZN56ukr3MMMUm-y?hnS-#y4Th|7(U z?)uJ5YS<6=L6+Ktt(9x_g^IV4gt$@6T2f$w4SDI{aL3K=!x1qC+R66`70)i{S5@9U z5Py?|C>QY_o+A8yYmg`T<#V*%#rA~&Ide>g*m#yPt5ae^hiM}RSE&?4s_2i;?OLnF zHQ47L=VS?EeNcw)%p7SLw@aUwsXp7IEEUc*(Ub46<$HrnA+}%Qlh~465ny)#g*_TB(zpleJXj z&^YJAM@YFw`q)PX6%-VVOhtw4nZ44!6Ro{x3f3++0pRiy0x17u9q4GGsY3U!z!oV6ktKK( zVS-YYe{3eXxK%bwMZvhE?PJQ$@y@c5!4}8zfOgIG|)o*w0-S`ygD*rM3m1GM>_Oq2ZN<#LV2dDe}o&1^pF?SQ%PdT12 zw_*@fPG0n;Z6idRsCVe@k>sA zncA7S*ns}@%8~v(n_xh13by|5eU!&2D1Eo4^QUxf2`>I^JhTB0@@_gOFFsv@2;3VtF2$7@=XhrT!yN!+3h&xua# zSqPjahaZ$ddMzTddYDQnjrtv@mDd~F!&I{Z_$X$r`o`4!@yXbVGij~Sr4uIIaj>Zt zX@DSdm}u59cncGuCRzrc_gUH+yMu60*SmJNnZ#7dZe@xUqn#clx|55t@#t$Ut<>XB zjA>m&V{fSHE{3*fCnQudO;y=BuG!CMy5B9S(n6*r!QCM`3|E)bz+4_G#8!UISGZ)OW47e!?cuJ^K_*P9ja?!O z#)Y8eCCsU(Uz%k?TDYuUIUOmrM(+`X$fSwRJ7PNk+USjl9uUSQJE=r4JMHvIKrkil zxKRR^D859$3r9jN6qY*gLluMSm(A7VE?D>x%vTYzGO_6tWozDtr-P-6Y!b(QGWbA1 zxy)P3=d!ELSV6I|@)&%$NrX_1&)Mq1%FDbt3~yq(S_q`73@kHf%kcIeQ|t>f+-tt! zLYzQW4tc?PKfup^+$8kY-CoMw-Wbfb@jNzo@%YvHWI#-TXm;;;EyZr@0m({udVNG! zUcQW>26hzSyF`}%@s>s7=S^Jc^PCWt3{gX}b+HkeF1<%-Mz|Q71F`xkE}qPUYbvhM zOnCgX??(Eqql@BJ6T}Q}tG1521wRz<^Ari~K@9$|*~c+d9d;oVx{FIQApC`>Fm2cF zis5kHoj8lqM^|%RNmergLjA2x&_o{+6a#$yuG&rF=uw?!9~m zUI;KKq~Y|-g+7;#d1bzoMuK(D=xBN1nxu}5#O1ubfpgnAI5>fMR6;DdH|yi zgeaF?uEt-F_++GYk!tDev894nd$>Cmbqwk)VmMn6EfrqiIr*^}RWW9cFR!hCn%=^K zlC!C?K*Kuw@54D)TsXXwEvNHB=%?;ma(|IeK-|u=D{FzbqU&hq&$yVR7iOa)%Vb~L z&?Hw%Hl2778-hVL%*c+0vZkRpIXJ|z=)W5oF8AgYN5=c;&U;2amF4%#2v$aR$XbL( zZ4m-qNL0tjz8Bby2vN!IQrSj({ML#~bNIb7kQ%1hYSwgdJTn6`!7LV#g7X!|l(e*@ zZlt9R{ibIcFqWV!n9U^OSjN%pOBF2guVFG*=X>?p}`?e-_Eq;KIo)xlGUW;l<- zHS2HMh!Ib$)i3xZ+248F)@hI2GMNPFayZB;(s5eLT$($2yej(b%R5&S4MlII?PmU$ zy4x5haJQ=03cos7&a1&j>FjB<(kN&1J|4TZIj#wV$&*|`96Z7juOCZ`TE5`+0;#%I zC)sc|b$(DX`ioL&ByHMaNLj@y$LlT#P(s2{37Bh**al#^szro@>bcHG}w^>z>|jSrv?fXQ)m zRF9al6?!sJUMEl9YJu;u302IG=Fj2yAghBJr`Zi)ekhNAa5p+D_W3&uEvYw{cN*9qQKG6X=VEIM2;2lxfrn-EI-`4Oyoo?cld|BeFN@Oe#;G0 zlv*+wjgpUwy2o>&KWvir>J+V7vS(0{@0BsWDYefJ@Y zMSe;g-Yg|cBJRzY-RGv7`)B_CWF%-AzO* zKXdy3A9K3F=Cbb!<5G^xZsVK^ zyC`P$^GhIp?zcHQ>@FpTe!jCilFSjCm5bUO$#e8(>G{`cte#tT&soeR+p}rm79C9V zNto^ZYG1nqIqZ-RapynW=*P_IfpSQU1NR?zVbjxn>5Qjii+$LqN04K&>$K=PH-8lK z+E-N0e+>C<3(S}KO-!|%T*m~WvV<_53PCuyaNVwE0@KUccSXQ@Tyv`e)?*3Nol>qh zs`^hA?{8JXHA|kUj;T!x%KATw_Rr+T7LkS9EelwgmeDcagG^gV;gSM|)C_NG? zogm*uho01&ZLH>4=;l0-B1w%u6aDuhaCqg|->Yb7&?;7NZ46afP74Cb$~3dSjtNk%mIrG<>Ea(IP-Q@z#^@>>y9S5VoD=W ztz+P&CrPbfL?)e*8nDNp8xchZ*F7*4B5U<|RrdxW$p;qvR5e;TDpSH{+;u;*@o2IMMCp2!TfOojfbrmJ~l1_M3P&TpN?)l_-0;gA0@0Ld_?^aBP^EM**!0c z6V`t(ru1puV@2~-=(5M zzv!}Nm`_R5v$7@m?+&aAJL-GwIpy)HtCpX@HSVW+^H^*V#Aq0*d9r-U(44#>C@fi2 zljBX4a2ei|sl~53B^zncOu~gg^dvt5i9NX-S4Q*c=V);}S3K_e@~J)G#atwvBU^Wk z6W)`OGu%c;(e-cJ3;Sm_p6PnJ-I3fb@t@E6b*)}kyAlXw7CRZ;j{8xjxgZt5K5i|%^&>*nD7T0&2SApIy7BTyGsQ0AU zU8(8XB8Hi+E#d-r?htCl1IKg!;_UVI#NNgm*(Y@mP^XRs-H0k1E==-Gi@f?Cutzf8 zl8KH9e^}eyxnt-SLLV7GYt2MgOh*i5DgoL=2w8>-f=b|I>pf? zXcBXvN$i$gn}l9?g$kFe`%nS0Z*!1pt;x(xLC`5JVx|@(8E&%bi}8w`{3qVHhQst` z&?LBRson0}@~|__7rbdBLfpNUW&Gh|RfMLoDG%bjVG5+H{3PKxYeicotd;vD0`YKK zZ2Ns~(nb74{bRz43C&#uHrH{1LraT!1=zCtd&zAL#MQ=rsE8X)%BQPOYYhC$2zRUY7BK&ig_y6U) zJY;a1UmfH?f_AljG&ulV{o;cq;FBh0M>~5bW@if*8)I7|OKT=8d#L&4*yS96AuA~( z3BbU>0Mg(OaJhh3B_k$gsI00aDI+fdRsaBEu93aHH7pqb*w{Kds!EBHY3t~cA&!GD zDsO?WFbDw@BNHck5oKlhtM0GbUS0or9F2T`5Ie~D@Ady|{o7wCrcfsn0D!rAk!ok^ zWMTut?f?L9ZDQ}}3;+mAU^$PQvponS5`i#|Bgh~Kvs}UE-(m49*!VlFeceV~RSax% z#Vfjrg^?);dx0>6@h|1(zrf#l09$}`q@cD?M@tj3D_ww^HM6vaURQiS{coy&Y5Bhb zZLD2DnXeBL@Qmv8P)!cJ(q74K;V7;87o2KhB%ubvlwf(My|d_5IW-7xxjfVm0bx1- zfW?XA5=EPOos` zxSSo;|H6gSHM5kI`K!H`z4f&%g1o_HI=ZM{@dna|>oanckOE zl+x~JJxJ$=#30XhziI$*0yXfrbd101sl%&|;71MFFkF@XAZmvfp!C1fFZ>ex8vHPL z9*3X$i4Q*sKL!4b10vuESYLBw19D|~JrYho+A;n^hGwv(GhhUs=fQS%Ag_OO`i~Jc zvkfQwS#zZc=u^cbMZk~YXDQ1l%Qnj}OBzci%jQ4&_(|~}T7pDDDj@ZcYDf*F2Oxtq zL7E|rkoS-xNbL`r|33ELsr(rGYk9BAuB5#hg}?jzRo76!8eJ6qHaZ2mI65gh6}tS7 zR%Gb-=&b0{=rmx>?H}W2{j>fbvRDE-psxPb>c97OH3KXG4UiK{zzuBc2x|38!?u72 zNcGow@)xIYByg;7GQU|Z&y_9z%>{x4f;fT*K!!kvz>C0&AbSP>J@XJ~z&c*=Eb)W; z?=$B|i+@M}{iPMCl^=8%udVLNR!8K=sFt>le zk}$O30)ABv3jj|o0l2@~T&>kt$JP1@^WzGe3j22pn2=P+-!O;@9+#7V*j4oKe|20D z`M)A?g?!lrU?RZ+Fd+~aOaK-W27(E5*#?k;lE8nbea$a;f`Ns=!6P6d-9SbG8&qHb zurLq^EF1(L9u8Cl%ww<~fWw5xB4-srz*aFryyJkw<{OcQL?K$zjH}weOUZ8R=ywAd z51-%`Ar&`qz8oGMpPYWZk_!fa{7~6<$$payR00ev92^7=@k%ZjShp*|G2!6J zSrM>AR1l3EuBH@Teq?I(^pkPD|C(Ta|Bod5A=pp3 zCID0jXq1=`Oh5>@xS$16VSSLn5a7?{FBL!zUM_0+2y$(tRJc@z@Uv%oJs7>f)Aynr z;TKq-b#A<^FgH!wd@r!l=Ypx>^}?**@eIL9#r%4O67fk!Tf$DzIohHU{JHpvlZP$z z3E-u?tJQWBb>^I>zOoKDHIzA#J9FN^xcH~8 zJGdKPFM;<0=8AicUZN)yZxU}3xO++szvbsHTyMbJMTmMhhp>l3SaG3K!$02ek2n0|4gYw{I0|mEofamLdJ6k$RcDE&P z8Y%XOB|Vu-uxS+1?`dp}p_N-^j4^IF)bSlDve%X++@je-tB|6k6c-`NZ3~0I(p6x^ z?OVc1C}(=Z&-82+x-B?FMFY4TPe=I9Jnd%A>V*!>i}VF#3GR`JswQ*!fAy>YA~=>X z6{LFSTpK+tIfYf{Ia)$>SYjfXMKl4*6VbF3PwHv{Cq zlv^!G6IhoUjw10!*iiSunDqiH!6!y3M0o%!18$uK0_OofgSEa+(`Ge>nv*HLqrpL3 z_cCn%MfxkXP=w{oLtB&Wea*aCAU>hp<=$)8ockN>08!9fUPV@ZS--*CO61qL;oo=* zY7O)XIrY6HE(bz;AHLOm=)qXoPRB`hR>taY`1UOa7d~+vjL!K!YD52ob)^>3)gK>F z8$3O1vgQ}iXw5jLQ$#j>Tx+Y3u-KNYCs7RHPplZ9Dyf8{L0#AH2JvegI5IXLSFOPc zLvCquNLWQikfW1_Ovd;7rapqLQ?y?WBSv2C2~S4pQtp%I^uNi5ipx5Dyg%;7oFX97 zahyUjd%1Vg?7w&>q%HAch)YpQBM*Pp>uX2G%f20@2JgLHC!zbAdyQU~d!K|JuONz^ z%J+I)v@aT_2zp;E(=x0bVxGM)9TZ&TF$e{O=C^fORK7Yu2>)CpjWHwVUW;>q8>@!;u9sixXD$$bOS4H3)~6PPkk6oV^SLR9u&0dbEj3AqYCKqBZBU9A zTjb=|! z)DHGWL&CUVf9~qM;=5HYe9XrOwFh`4Ikj-e#;(DSCX;5lR_BCflU9ch+AEhV@q-ZF zR?53(*u0zWnld;JXB$z&mb8^F+lwte5;q_ia$ISX`Jkqa*Q#XD#p3IK`-Zh?_}qqA z{=)rG_ObF&2>7v?(=bIa8`FHTMNoYWCkfZZ2NKS>L-ICP^x$Fims`7$BupO(vj|Ow zJBchpf@915$`s!@^J*bC)PFQ}u^D=0UT}L*Ud`Ez-e3eLQH)plehq>@Rbp|TNHH#K ztkzslQBF`Rx*6p_-6R375v$6Hr)adN4RNr8xb?H7dE`VF?AkklX6J*S$BB4J6R92w zPpnJEm&d0FnNwCPvryfFQ_{qiE2wB!bsj?@ZSK(GD-DB_j>OP3Is|rL_JaMQ_i`q6O+U1BU z((mnNxcI_wfa6b9=;bkc#Cy<8wIb@7Z|3#L=7MusI%VmI+(Cc;%lU@Sz!JU zV?}uRNx#1vMxTU1;vOvd)E086a~21LNxdfx1gVedFDeWK#ZfuuRt95<|6!lVhBZ-4uAF>8VnuO{dO4?BsUysSHoim;=D==ss zuWy(yhq;$XY7+M|XV+#*s^ETHsC*ZzbeKS$1cfPlQ}NEE*S)Svp8!!M>5u@S`YyWO{n8vF<5RTaMVvimO8l5|OXjx9n{?R#ed<1a#6_I2b;XQ?snlYaNQ= zkWTpv+cn2r#*ZthvH8=eA&wWajC0QQQ(ksu8ra*6th0womO8&g#SDA#{6MNJfRX&n zBXjJ|?Q*=qU`=GEQ*^Y#2QNm`T`L#@@s;qIsCs((O6824;#SlE(vJQ2Z(q9lR4R-- zdK*&k;Ca@Qz*2UCotz%eeO`L#QR>MD;d%t<#@cZ7vOJ7OSukP>H+)cr)~7Fq+RoP5 ziP6(1*#GviV{*Q1(=oXFVFJv<@?)#K6V%z+($@TXt9+*Bq~jzP#^W9NOOe*q50q5n zwmcE+4eDLV{-xq(tY>w}_e0EdGPs#NGgPP;3 z7%2%Zm~pxfOJ=VgduuCP_E3*t%G}VxiXA7hKAPV;o}D?4fnQbbQN$3PkS*`F-b^ao zpL(Z<(MEC?4eQlID4c^kXQDpF!PvTGK>+m&&QSKsPv#O1k!UPduUsTGA#Li#BeVr0 z9}Cp2>dhEYQ!4AT&G(zobh;3?|N2t zo--W#o$yo?5j#vlJS zyRnN*ZW70THrJ<=+@b$=UDI}_c9Hw}I+KFe-x){;{qEp@W(4)zD3%TI5_ zH->FEmyg^|H+dde?N~yv8}D2xPg%2_aL-aE$#S;%o{Z4vP{f{&-7hh%8U(W6VmlQ+ zz;&*#PQ;O5Y<^^<=-v`ytFJzuh&Q#hY807&VG-7PV>v;gx`$`d+F*T{Xol8yz}}Lp z%$FO5zs}=+RrrVyUe;58{AW_p;4XWcWQ&7{AkRA=7$qkh(urS|M~Sy8%xLLZv-*pj zVy;Txd-PS;q3}`aCFKH}r^s^=Bx2_oUKifbd83U9j61bdgvA)=p&d-h8f2(qvx=^` z{PPXK2L7|*P~`o9C*17}Ll0oCX$%)plkged(^Xvo>o3>R)&T>Wy@)XUmSt8eSp4nEQdkLfo` zvEbdq{P)!8XJ)xV5LJ{_y_N9<$`_C*N2a%X>9}uwCC+RU%6rhRw&+a15-mzWwSFrp z9Sh=a`-0%Ltp0nMd$>YJMrr-#V%gGXt3UP!iLZ`4ClJ|3|7K`{k- zsrz2W7WK2eCl?rnX5okFZ`hM-dZQ*8=POSM)=0FW#m4U3brhc(nm%hK6G!psb6;(WczPU*=xB}!v7*bQGGKCjJ)Goi_+y(_d3R zr}ilHfvE~Y5WMO5*=A)9q9jtl8s1VVO*hM+p->}9_aLcYeSq&FL*!6ge-BCht%=xY z`s=6ti#^^|jl3S_dX_8V$=IFJJzH846DfihryWmwIePCyB6P{fWOEY7UXK?rgyi^( zY!r&GW!j0(Dwxv6jo;P^f>i5&EhMluT|j%^+K(aB9_%cU06P(bEx?!eIs}DEL3gbx zaBk23#1`Y8x2fq6XJpAU;n^&CQ5w0~&LvD&N~O&cIF8WQJ%%T~DfNEdt?U#l9%e;R z772PT#&1+Tvs0VC@S;9%Pjx-e-S*A9P}!2U3i_;~YF8cm`5ul+&;}Y7rcVo-td@7( z;bh4`6KR2HmOnyV=yY4gAYrn@O)$s}F7qA3Z%VA9wtdOYUw;?1oK#)ZW-yn|frC<9 zJecL4viev%1IJyO*%h8qYj#U)F&Njoxs6!WFTylRW)&uEzL=WcC3#@M#owyRoETdQ zH&kWZ;Am{5m&RBBIMj530k#i`-!?C_TnLuiW1t>S&YajZ?<;4u+`i$#>~2R`Xz9>| z{Oh5^Z=Q1u;aFeEcDp5_zjD=KDkd0YHmG@@@;=F)DH>|omn$1oD2r^8n?2a}g;lr= zwE3jSQr3~v9z~zk_OYxfm?Xsf&OML#xR_VtEB&xbAFXM4Um4OGLCVWdbV_-+jy5%L zkAVo_Z4YOP|3TY+fpba#_oun`o}gr(o20foSccTX;p}JM&d%rc11|7q4+5-6+gmJ_ zJr{Ojo{^}|+oW99i7fO|)i@GE=bj$0qw0h|N-@;Oq*sL0b*jvt6mkKzev7g^xqTC} zRvDz~%6s!d@JrI3p3EO)dGhCVaxX>5${$VNvUtNlciYENr$(_RglC&+!Nv`<&!YD0 zLR~Eq&x-S~=&Iyh`gM`s%z~(*BW7j~HB#Ac6e_A+LDn8-q{wNQ-mE3-vf|_|1G`47LaR<0{Ki|wh6Dxo zfU(Jz&#)c1^tasalzeUvc|mJ(>1Nq$($U`P9^y#c`gQn;J`|5=UcKAquuaE*Fi{|F z`N^T$m*!?$?~w2&oQW` zrTT~xXypT~ytRI4aJ_J@n72Mw2EDN@yKpR*dGCm0GKV{$i?NRS&55z>i2X~I z??EnYC8n1_Byva)mt`(Loo=#H%`B_&)Z0B)pBfio1m#N2PQa@3uVtCpA4mBN>&;R= z;B*dKA&B9PF~O;|i}mf1@p+&kw4?xRJgvJS{4JatCYR5n(*KLp3>*U&H-RE!6gLb~ zoc+ZRw=i^{8Yrn2DKofutYF^1JY9U|A?O>IE^fcX7OPndRPyL{zy4l|96)2GP zBbLATHe&WLVZCs@;uNBO-N8LvQBHoTCS$~0KWJy7PvGgm6-L#PHrWdaP!Y}P-kpiS z&`prDnkY9u=_g?C#!mk8Lsz_xeR<&X0o0nYQ|tZx_k^e0OA5k9h`EY0a5%2cbOBZ% zy>@hE_-gGY;>SkY2PPweBm5gZ?md zpwgf9qHQEIPT^A-DiL#1!W<@xI28Dz^4KC_;j(b=#%Vu?Geeg++YW!>6Bvlv7W5@G z!Ts)Xltk^AfTc7BTXZ9mSFl!%;a;qwC5Dls_k+l}i;kE>iJMJBM@F*SGj+bVGWM%5 zK62zWllQTEh8daCFxke^Te8Sz1{LpPk|hz;2l}RZy^*rfbP_Da=t$Y`IC(NjgewTS zWBW#|n!d@AJ%Y&R19M#>pS1gQJ@S-2Jqhmk3sW4cmCD6RbJ}{_)fX!w_3SR#=RP0Z z)y`92GU%sg=xSjEl__U&p^=-0%rU9V_}r|FK*Y1%UuPhe-)ZkppZgYr8l{-Vx58d;dCI#CKe zUJE2(!7Frxg$Zg+?A}{%&%%RX2gY-KghgZ^&j@kZ5X~>_)NV;yT{q?LQM0H;5PO3R z#MSZfwNueW)7}(JJvS@bR&S_PBu}&VDv{-&+AL@oDthzQoDO|jj%Y2W#&c4Ca;d`% za-7p17H}4JSgyU%kyPFn`R+Y;ROcltqwI**plIVp0-mnyaXBp(<+l@X>-WDzC?*|o z2p_>ms?vf0aP#lL?t6rC^_}!@VTt)x8!i=;Z|}W^2Tz27rzA^hHYzF#?YrYqJEfbU zm`MFLolvEBL9Z^po^qDv%szGx6CEM6NA1%=h9}4pD?UCvd|~H-wOt2G&Kw`k!=BY`#mI`I-W;V1t0NN3 zcFX9L+o|k|C`xbZUX~4FdqSwtv1n3>5v7`$d#L+$A!ENX;+Ye_kgNm}mPV3|X=Ka* z3GD!hIw57=zC^5Vzlz0@L~`ic_^vu~+DPwwwu+C2zN{1({n8c!7o3r#V_~ zC9?GzNFP1pd!~=)s)bKU+b1T2JhFwdM=keCP6C9OG?L6ew&um1s?PYxi3n4QjI5UN zL^1JW-j7Xz4ipHUtG`)ay2soeS`sG%f2l`o}+FQ{Ljw!3-v9iTt`Hi z8|X*Hy_{>`(3RP546b?{ckx576~{o6N>DhHaY{cYI!b{ko9Zh(tEupc_AxYf2@MTfMKa zui<2TbR_&?13QdigTG%o;Ujh!(K-kOg8=Fpe?OA@YOtgipAtul$8Nw5xx;XoMbHcx z?on8eSlTC2%2XMpeEI}Gr|qm6gAJkHTDd0q_VS7 z@Aq`2q?;~6vF=;m(-+!a|9bn|^M&)652@H26*3J$3Ma@Vm4OCNlbN~bP{lW~vZ)8%+i-lg4v%VSLeH7m#7K!4vKR5xY67B+-KQes zwp$vX3)^N4a0XOHn6$2jvW$``x3@ASY+`VRR1G2%1J7?{8|*3wpTd1paGLKPWzAl* z)9puD#{R+`wi&q3Dumt<*ULdVe>+=@t_a(8`+lx+)~1d2h%k*nQZfCQXrKH8{E`@@ zJHgnl@H>4uPAg}-yp-9(9QO z#*QK5@MJPdZ-dO0FB-q{|7$~!UTr7By=neR4O5Wyu)HJpLmpAa*yA!}(^)tqJhUs1 zYy`SF4ph*oeT{PC?zI2z=9Y)(u3R$8YC!{he&n?~3fg|&cq``d%`Bn|M?SyEV-^R2KDmztbY+A=pg(N|LkbImEALOUV;eqyM!59?Y-4R(oTPhAkAy)v<|?wV9g~~Urf7U! zcNJ!Fb|w0>yig2NX1t1QVDgQ0@+lr%u}3D~GXtukg^?L(p<4*B{6YRdl?}!ezWLWN zg@1!N%CSV(s2UBUjBvfV@{AQ3L{ZOA9G(I7{O-t|z+l?`$9$PI4=l+cCE$FfPNsL+ z>0@&Jq>B}RN~M*i?w)ma!){$20n>~Ik@|*hgCS2!FCf22ls)vh_jWvM{tJ|O`6v=1 zSb>8aI7!#YeY5IVgbeXVNCgBiEgtXa36SXCKl)-MunM{DaGt0A{&Es8cbAWBp_w|m z5r#nse;p=wp{2IoKVWovvx%{CK_y}}B2=|^!S(!rFM7PvN$MbVrn=#Vx3GELUGF|A zbX!&x8#<&`om|zpAdFHC_t#c-P0U&j0A7nY>BVRd9J~9MP+E6y@|N8KbJv2*#}8>o zsy*)AGC_P3teeNLrE+$P!~86R`V3C^o_Y)`X$9uC2bNt%jC4rt_S?I8e29wa+u51b zn-h`9>PnTH@6?#y2|J63>W&_$-^_1jqbu8g@a}UhNzB6`McTzP$7$&rAKCQ}vC~@2 zZ9Zulud$I>NDPyeR?Sx>QRQ)rC*0|YiUUHKj@ME0vhQHWFixGKu23s$LvIFY;{&!H zPW&B=_dwV0Epobje=^VONur48{E6{v3%Zff$Cf0#pOW)d43wW3%8 z++g-}X`8TQqGWK|mz}tG%DL}aZA)ek%DyF;y3#rBAie*fnng(j-v()yBOm6XvfB*2 zl;#hSzY!aUtg7lyRkn4(G=C3_Nww6hSZJFcDawq;Z$@unxX9-%^U=l=^9DK2Wplub z;U_Q-hwhByS>V8_=>lxc;r)_*qxE-q<&bCOHvGHvV4CkPDf@`ZwW>%L>BFgw38CmN z1?fn?shC{n|NQpd9PwD0lIBrce!k>z6>)1X>A=h7?tFvcxct56kS?rdljwM{*uKrd z`mOoR+dQu8g7yG+Q!&Rl@;uT^(p`a`n?Dd{;VI(${8x zQiTx}jHin{NP>xP5e~z24AOZ%9Y1OBQr^4McZ=gZb^Iz0{m*A<_#V`^#6VMB1*280 zzav#MFloHHk+Jo)y$(g!+D@|I1RcXZM)dV$kkwQjeMVGniRQenTy;};Fj^rR-#7EE zyUWo>;t46*d*#A$bz#15JOYVS;&_T-tNit%gGAPa_#52?iT2MIWS>814_(&HqTMjU zVUaitE%10QsQ8`+f}<@p6pHj#`}s&udPx6)`D#1M;D>QwKZaN~AwAIPlm(U1fJMey zX;of?Wl$0evB-=>)>x4706QP5NR;B(Jbf>eRZy%VjJHj~{8r9ZShieO^ucMG5iC(f zDN6F>`?ox8Z~M$*Z!5g2P%guXNukuQ;8H0yr{v|F*yuS-jW3>1(bg4t6!0wyhc?{# z#I~@UTn%RHIju+rp}sOIZUotVEMCr}6Jf>ZcMN`G&!6GU6VROC9M7lBvB%F72WCMG zFic)O#9xs8_-x5|I@fgFIcs&QUxK0t$qW5REs!Ba?15ztkAxj-s5`%83|1EH-Iz=b z<1saX4bE>J$cKX7GG7MHd|(A_@U{fs+%vz~Hr;z%o0!tP9rvlyu+e3~Lf>^ok;q?> zNK!66|4_CzSf{k>o;k@Xh2X75@a>^A;F{_e-=nkWq%z>kxCE2 z1pVzhf!>IVP9~mmcd2^lqS4X{~s!awl~~ z=IQR^?{s?COl{q_JlKMkC>;{Bxk58k+=++E#FVyI3(qDN=+j1XnHOY=@8dEXu04@( zQEoT%CW!3}f=69Tj|o_CD>1oE+$rLj&H@+d%7WhhVlU5^kJpIFnsJVm^0frwwr-PA ztPI`4Ir5bAu7KpjXv&WoQ-m?1=oA{2s<)KLKWSisTU?NN;z@>`*vfG#Y)~3Q%_-zZ z{lchtscx-uSFNXN4J`O7F-uyqi+Up1BQeP~KJ)LQQ~wru!{9>w+n0Q!+=x#&IF42Fhi7>G(4Honi1s4h%YO}XK1VP6&bsUJ>y1N)-bR0{jn=Q4 z?Vsfa_m1)ABlb97lSG_=uWrAul`yc3;H#N`|Mwby57gh+fB9z{%5wiC@Xx&be|Yb<{f_>7qP3sswgkVh{v++$@A&`oLGuOE zLj1z~FN*u0XL|bC`$I4h(vJZ5pLw5tXW{oS^-`>6cPT}_#k3T6a%l^&4zrBY1ozCx%2Y=Fe3T6xX zF32^VD|h%i@;`4dexd?^pWZK{@%Qb<@94h=d_M`W-v6ube}sR(%m=@8 Z|CcXUmV-yQYJ&^@h=5r_3{0;6`X6HG47vaS literal 12737 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 Date: Tue, 7 Jun 2016 18:48:14 -0700 Subject: [PATCH 8/8] hdr: add _BaseHeaderFooter.body --- docx/header.py | 19 ++++++++++++++++++- docx/oxml/__init__.py | 13 ++++++++----- docx/oxml/section.py | 15 +++++++++++++-- docx/parts/document.py | 7 +++++++ tests/test_header.py | 38 +++++++++++++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/docx/header.py b/docx/header.py index 98d531a95..ed5e69cee 100644 --- a/docx/header.py +++ b/docx/header.py @@ -8,7 +8,7 @@ absolute_import, division, print_function, unicode_literals ) -from .shared import ElementProxy +from .shared import ElementProxy, lazyproperty class _BaseHeaderFooter(ElementProxy): @@ -23,6 +23,16 @@ def __init__(self, element, parent, type): self._sectPr = element self._type = type + @lazyproperty + def body(self): + """ + BlockItemContainer instance with contents of Header + """ + headerReference = self._sectPr.get_headerReference_of_type(self._type) + if headerReference is None: + return None + return self.part.related_hdrftr_body(headerReference.rId) + @property def is_linked_to_previous(self): """ @@ -39,3 +49,10 @@ class Header(_BaseHeaderFooter): """ One of the page headers for a section. """ + + +class HeaderFooterBody(object): + """ + The rich-text body of a header or footer. Supports the same rich text + operations as a document, such as paragraphs and tables. + """ diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index a96cfc1b4..9ba4ddafd 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -86,11 +86,14 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:numbering', CT_Numbering) register_element_cls('w:startOverride', CT_DecimalNumber) -from .section import CT_PageMar, CT_PageSz, CT_SectPr, CT_SectType -register_element_cls('w:pgMar', CT_PageMar) -register_element_cls('w:pgSz', CT_PageSz) -register_element_cls('w:sectPr', CT_SectPr) -register_element_cls('w:type', CT_SectType) +from .section import ( + CT_HdrFtrRef, CT_PageMar, CT_PageSz, CT_SectPr, CT_SectType +) +register_element_cls('w:headerReference', CT_HdrFtrRef) +register_element_cls('w:pgMar', CT_PageMar) +register_element_cls('w:pgSz', CT_PageSz) +register_element_cls('w:sectPr', CT_SectPr) +register_element_cls('w:type', CT_SectType) from .shape import ( CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, diff --git a/docx/oxml/section.py b/docx/oxml/section.py index 5712870a4..5d172d7b2 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -10,12 +10,23 @@ from ..enum.header import WD_HEADER_FOOTER from ..enum.section import WD_ORIENTATION, WD_SECTION_START -from .simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure +from .simpletypes import ( + ST_RelationshipId, ST_SignedTwipsMeasure, ST_TwipsMeasure +) from .xmlchemy import ( - BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne + BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore, + ZeroOrOne ) +class CT_HdrFtrRef(BaseOxmlElement): + """ + `w:headerReference` and `w:footerReference` elements, specifying the + various headers and footers for a section. + """ + rId = RequiredAttribute('r:id', ST_RelationshipId) + + class CT_PageMar(BaseOxmlElement): """ ```` element, defining page margins. diff --git a/docx/parts/document.py b/docx/parts/document.py index 7a23e9a5e..87e9ee2ef 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -121,6 +121,13 @@ def numbering_part(self): self.relate_to(numbering_part, RT.NUMBERING) return numbering_part + def related_hdrftr_body(self, rId): + """ + Return the |HeaderFooterBody| object corresponding to the related + part identified by *rId*. + """ + raise NotImplementedError + def save(self, path_or_stream): """ Save this document to *path_or_stream*, which can be either a path to diff --git a/tests/test_header.py b/tests/test_header.py index d63356d5e..0ee5429ce 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -11,9 +11,11 @@ import pytest from docx.enum.header import WD_HEADER_FOOTER -from docx.header import Header +from docx.header import _BaseHeaderFooter, Header, HeaderFooterBody +from docx.parts.document import DocumentPart from .unitutil.cxml import element +from .unitutil.mock import call, instance_mock, property_mock class Describe_BaseHeaderFooter(object): @@ -22,8 +24,26 @@ def it_knows_whether_it_is_linked_to_previous(self, is_linked_fixture): header, expected_value = is_linked_fixture assert header.is_linked_to_previous is expected_value + def it_provides_access_to_its_body(self, body_fixture): + header, calls, expected_value = body_fixture + body = header.body + assert header.part.related_hdrftr_body.call_args_list == calls + assert body == expected_value + # fixtures ------------------------------------------------------- + @pytest.fixture(params=[ + ('w:sectPr', None), + ('w:sectPr/w:headerReference{w:type=even,r:id=rId6}', None), + ('w:sectPr/w:headerReference{w:type=default,r:id=rId8}', 'rId8'), + ]) + def body_fixture(self, request, body_, part_prop_, document_part_): + sectPr_cxml, rId = request.param + header = Header(element(sectPr_cxml), None, WD_HEADER_FOOTER.PRIMARY) + calls, expected_value = ([call(rId)], body_) if rId else ([], None) + document_part_.related_hdrftr_body.return_value = body_ + return header, calls, expected_value + @pytest.fixture(params=[ ('w:sectPr', True), ('w:sectPr/w:headerReference{w:type=default}', False), @@ -33,3 +53,19 @@ def is_linked_fixture(self, request): sectPr_cxml, expected_value = request.param header = Header(element(sectPr_cxml), None, WD_HEADER_FOOTER.PRIMARY) return header, expected_value + + # fixture components --------------------------------------------- + + @pytest.fixture + def body_(self, request): + return instance_mock(request, HeaderFooterBody) + + @pytest.fixture + def document_part_(self, request): + return instance_mock(request, DocumentPart) + + @pytest.fixture + def part_prop_(self, request, document_part_): + return property_mock( + request, _BaseHeaderFooter, 'part', return_value=document_part_ + )