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/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/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 new file mode 100644 index 000000000..ed5e69cee --- /dev/null +++ b/docx/header.py @@ -0,0 +1,58 @@ +# encoding: utf-8 + +""" +Page headers and footers. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .shared import ElementProxy, lazyproperty + + +class _BaseHeaderFooter(ElementProxy): + """ + Base class for header and footer objects. + """ + + __slots__ = ('_sectPr', '_type') + + def __init__(self, element, parent, type): + super(_BaseHeaderFooter, self).__init__(element, parent) + 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): + """ + 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. + """ + + +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 cf76b67ed..5d172d7b2 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -8,9 +8,23 @@ 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 .simpletypes import ( + ST_RelationshipId, ST_SignedTwipsMeasure, ST_TwipsMeasure +) +from .xmlchemy import ( + 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): @@ -41,22 +55,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 +112,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/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/docx/section.py b/docx/section.py index 16221243b..340ce03b5 100644 --- a/docx/section.py +++ b/docx/section.py @@ -8,37 +8,42 @@ from collections import Sequence +from .enum.header import WD_HEADER_FOOTER +from .header import Header +from .shared import ElementProxy, lazyproperty + 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 @@ -80,6 +85,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, self, WD_HEADER_FOOTER.PRIMARY) + @property def header_distance(self): """ diff --git a/features/hdr-header-props.feature b/features/hdr-header-props.feature new file mode 100644 index 000000000..8b1f4836b --- /dev/null +++ b/features/hdr-header-props.feature @@ -0,0 +1,20 @@ +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 + + + 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 | + + @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/sct-access-header.feature b/features/sct-access-header.feature new file mode 100644 index 000000000..3b5a33893 --- /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 + + + Scenario: Access default header of section + Given a section + Then section.header is a Header object diff --git a/features/steps/header.py b/features/steps/header.py new file mode 100644 index 000000000..21e6ba756 --- /dev/null +++ b/features/steps/header.py @@ -0,0 +1,49 @@ +# 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('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 + assert header.is_linked_to_previous is expected_value 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 = { 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 000000000..81ba27123 Binary files /dev/null and b/features/steps/test_files/hdr-header-props.docx differ 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_header.py b/tests/test_header.py new file mode 100644 index 000000000..0ee5429ce --- /dev/null +++ b/tests/test_header.py @@ -0,0 +1,71 @@ +# 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 _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): + + 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), + ('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 + + # 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_ + ) diff --git a/tests/test_section.py b/tests/test_section.py index a497aa727..db9812eba 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -8,11 +8,14 @@ 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 from docx.shared import Inches from .unitutil.cxml import element, xml +from .unitutil.mock import class_mock, instance_mock class DescribeSections(object): @@ -39,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 --------------------------------------------- @@ -109,8 +112,22 @@ 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, section, WD_HEADER_FOOTER.PRIMARY + ) + assert header is header_ + # fixtures ------------------------------------------------------- + @pytest.fixture + def header_fixture(self, Header_, header_): + sectPr = element('w:sectPr') + section = Section(sectPr, None) + 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), @@ -124,7 +141,7 @@ def it_can_change_its_page_margins(self, margins_set_fixture): ]) 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=[ @@ -148,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 @@ -160,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=[ @@ -170,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 @@ -181,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=[ @@ -190,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 @@ -201,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=[ @@ -210,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 @@ -225,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=[ @@ -244,6 +261,18 @@ 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 + + # 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) 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