From 73bc297bf0a22474b02cd43874ae23c2d864d34c Mon Sep 17 00:00:00 2001 From: Rene Nejsum Date: Tue, 21 Jun 2016 22:15:21 +0200 Subject: [PATCH 1/3] Added customprops --- docx/__init__.py | 2 + docx/document.py | 8 ++ docx/opc/constants.py | 3 + docx/opc/customprops.py | 47 +++++++++ docx/opc/package.py | 22 +++++ docx/opc/parts/customprops.py | 74 ++++++++++++++ docx/oxml/__init__.py | 3 + docx/oxml/customprops.py | 155 +++++++++++++++++++++++++++++ docx/oxml/ns.py | 1 + docx/parts/document.py | 8 ++ tests/opc/test_customprops.py | 180 ++++++++++++++++++++++++++++++++++ 11 files changed, 503 insertions(+) create mode 100644 docx/opc/customprops.py create mode 100644 docx/opc/parts/customprops.py create mode 100644 docx/oxml/customprops.py create mode 100644 tests/opc/test_customprops.py diff --git a/docx/__init__.py b/docx/__init__.py index 7083abe5..07017b77 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -10,6 +10,7 @@ from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT from docx.opc.part import PartFactory from docx.opc.parts.coreprops import CorePropertiesPart +from docx.opc.parts.customprops import CustomPropertiesPart from docx.parts.document import DocumentPart from docx.parts.image import ImagePart @@ -26,6 +27,7 @@ def part_class_selector(content_type, reltype): PartFactory.part_class_selector = part_class_selector PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart +PartFactory.part_type_for[CT.OPC_CUSTOM_PROPERTIES] = CustomPropertiesPart PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = DocumentPart PartFactory.part_type_for[CT.WML_NUMBERING] = NumberingPart PartFactory.part_type_for[CT.WML_SETTINGS] = SettingsPart diff --git a/docx/document.py b/docx/document.py index ba94a799..21546a7a 100644 --- a/docx/document.py +++ b/docx/document.py @@ -108,6 +108,14 @@ def core_properties(self): """ return self._part.core_properties + @property + def custom_properties(self): + """ + A |CustomProperties| object providing read/write access to the custom + properties of this document. + """ + return self._part.custom_properties + @property def inline_shapes(self): """ diff --git a/docx/opc/constants.py b/docx/opc/constants.py index b90aa394..1bcf16f6 100644 --- a/docx/opc/constants.py +++ b/docx/opc/constants.py @@ -77,6 +77,9 @@ class CONTENT_TYPE(object): OPC_CORE_PROPERTIES = ( 'application/vnd.openxmlformats-package.core-properties+xml' ) + OPC_CUSTOM_PROPERTIES = ( + 'application/vnd.openxmlformats-officedocument.custom-properties+xml' + ) OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( 'application/vnd.openxmlformats-package.digital-signature-certificat' 'e' diff --git a/docx/opc/customprops.py b/docx/opc/customprops.py new file mode 100644 index 00000000..944cb3bb --- /dev/null +++ b/docx/opc/customprops.py @@ -0,0 +1,47 @@ +# encoding: utf-8 + +""" +The :mod:`pptx.packaging` module coheres around the concerns of reading and +writing presentations to and from a .pptx file. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from lxml import etree + +NS_VT = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" + +class CustomProperties(object): + """ + Corresponds to part named ``/docProps/custom.xml``, containing the custom + document properties for this document package. + """ + def __init__(self, element): + self._element = element + + def __getitem__( self, item ): + # print(etree.tostring(self._element, pretty_print=True)) + prop = self.lookup(item) + if prop is not None : + return prop[0].text + + def __setitem__( self, key, value ): + prop = self.lookup(key) + if prop is None : + prop = etree.SubElement( self._element, "property" ) + elm = etree.SubElement(prop, '{%s}lpwstr' % NS_VT, nsmap = {'vt':NS_VT} ) + prop.set("name", key) + prop.set("fmtid", "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}") + prop.set("pid", "%s" % str(len(self._element) + 1)) + else: + elm = prop[0] + elm.text = value + # etree.tostring(prop, pretty_print=True) + + def lookup(self, item): + for child in self._element : + if child.get("name") == item : + return child + return None \ No newline at end of file diff --git a/docx/opc/package.py b/docx/opc/package.py index b0ea37ea..f99d1727 100644 --- a/docx/opc/package.py +++ b/docx/opc/package.py @@ -11,6 +11,7 @@ from .packuri import PACKAGE_URI from .part import PartFactory from .parts.coreprops import CorePropertiesPart +from .parts.customprops import CustomPropertiesPart from .pkgreader import PackageReader from .pkgwriter import PackageWriter from .rel import Relationships @@ -43,6 +44,14 @@ def core_properties(self): """ return self._core_properties_part.core_properties + @property + def custom_properties(self): + """ + |CustomProperties| object providing read/write access to the Dublin + Core properties for this document. + """ + return self._custom_properties_part.custom_properties + def iter_rels(self): """ Generate exactly one reference to each relationship in the package by @@ -172,6 +181,19 @@ def _core_properties_part(self): self.relate_to(core_properties_part, RT.CORE_PROPERTIES) return core_properties_part + @property + def _custom_properties_part(self): + """ + |CustomPropertiesPart| object related to this package. Creates + a default custom properties part if one is not present (not common). + """ + try: + return self.part_related_by(RT.CUSTOM_PROPERTIES) + except KeyError: + custom_properties_part = CustomPropertiesPart.default(self) + self.relate_to(custom_properties_part, RT.CUSTOM_PROPERTIES) + return custom_properties_part + class Unmarshaller(object): """ diff --git a/docx/opc/parts/customprops.py b/docx/opc/parts/customprops.py new file mode 100644 index 00000000..e6ec3616 --- /dev/null +++ b/docx/opc/parts/customprops.py @@ -0,0 +1,74 @@ +# encoding: utf-8 + +""" +Custom properties part, corresponds to ``/docProps/custom.xml`` part in package. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from lxml import etree + +from datetime import datetime + +from ..constants import CONTENT_TYPE as CT +from ..customprops import CustomProperties +from ...oxml.customprops import CT_CustomProperties +from ..packuri import PackURI +from ..part import XmlPart + +# configure XML parser +parser_lookup = etree.ElementDefaultClassLookup(element=CT_CustomProperties) +ct_parser = etree.XMLParser(remove_blank_text=True) +ct_parser.set_element_class_lookup(parser_lookup) + + +def ct_parse_xml(xml): + """ + Return root lxml element obtained by parsing XML character string in + *xml*, which can be either a Python 2.x string or unicode. The custom + parser is used, so custom element classes are produced for elements in + *xml* that have them. + """ + root_element = etree.fromstring(xml, ct_parser) + return root_element + + + +class CustomPropertiesPart(XmlPart): + """ + Corresponds to part named ``/docProps/custom.xml``, containing the custom + document properties for this document package. + """ + @classmethod + def default(cls, package): + """ + Return a new |CustomPropertiesPart| object initialized with default + values for its base properties. + """ + custom_properties_part = cls._new(package) + custom_properties = custom_properties_part.custom_properties + return custom_properties_part + + @property + def custom_properties(self): + """ + A |CustomProperties| object providing read/write access to the custom + properties contained in this custom properties part. + """ + return CustomProperties(self.element) + + @classmethod + def load(cls, partname, content_type, blob, package): + element = ct_parse_xml(blob) + return cls(partname, content_type, element, package) + + @classmethod + def _new(cls, package): + partname = PackURI('/docProps/custom.xml') + content_type = CT.OPC_CUSTOM_PROPERTIES + customProperties = CT_CustomProperties.new() + return CustomPropertiesPart( + partname, content_type, customProperties, package + ) diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 2731302e..6d82f964 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -70,6 +70,9 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): from .coreprops import CT_CoreProperties register_element_cls('cp:coreProperties', CT_CoreProperties) +from .customprops import CT_CustomProperties +#register_element_cls('Properties', CT_CustomProperties) + from .document import CT_Body, CT_Document register_element_cls('w:body', CT_Body) register_element_cls('w:document', CT_Document) diff --git a/docx/oxml/customprops.py b/docx/oxml/customprops.py new file mode 100644 index 00000000..2c7ef343 --- /dev/null +++ b/docx/oxml/customprops.py @@ -0,0 +1,155 @@ +# encoding: utf-8 + +""" +lxml custom element classes for core properties-related XML elements. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import re + +from datetime import datetime, timedelta +from lxml import etree +from .ns import nsdecls, qn +from .xmlchemy import BaseOxmlElement, ZeroOrOne + +class CT_CustomProperties(BaseOxmlElement): + """ + ```` element, the root element of the Custom Properties + part stored as ``/docProps/custom.xml``. String elements are + limited in length to 255 unicode characters. + """ + + _customProperties_tmpl = ( + '\n' % nsdecls('vt') + ) + + @classmethod + def new(cls): + """ + Return a new ```` element + """ + xml = cls._customProperties_tmpl + customProperties = ct_parse_xml(xml) + return customProperties + + def _datetime_of_element(self, property_name): + element = getattr(self, property_name) + if element is None: + return None + datetime_str = element.text + try: + return self._parse_W3CDTF_to_datetime(datetime_str) + except ValueError: + # invalid datetime strings are ignored + return None + + def _get_or_add(self, prop_name): + """ + Return element returned by 'get_or_add_' method for *prop_name*. + """ + get_or_add_method_name = 'get_or_add_%s' % prop_name + get_or_add_method = getattr(self, get_or_add_method_name) + element = get_or_add_method() + return element + + @classmethod + def _offset_dt(cls, dt, offset_str): + """ + Return a |datetime| instance that is offset from datetime *dt* by + the timezone offset specified in *offset_str*, a string like + ``'-07:00'``. + """ + match = cls._offset_pattern.match(offset_str) + if match is None: + raise ValueError( + "'%s' is not a valid offset string" % offset_str + ) + sign, hours_str, minutes_str = match.groups() + sign_factor = -1 if sign == '+' else 1 + hours = int(hours_str) * sign_factor + minutes = int(minutes_str) * sign_factor + td = timedelta(hours=hours, minutes=minutes) + return dt + td + + _offset_pattern = re.compile('([+-])(\d\d):(\d\d)') + + @classmethod + def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): + # valid W3CDTF date cases: + # yyyy e.g. '2003' + # yyyy-mm e.g. '2003-12' + # yyyy-mm-dd e.g. '2003-12-31' + # UTC timezone e.g. '2003-12-31T10:14:55Z' + # numeric timezone e.g. '2003-12-31T10:14:55-08:00' + templates = ( + '%Y-%m-%dT%H:%M:%S', + '%Y-%m-%d', + '%Y-%m', + '%Y', + ) + # strptime isn't smart enough to parse literal timezone offsets like + # '-07:30', so we have to do it ourselves + parseable_part = w3cdtf_str[:19] + offset_str = w3cdtf_str[19:] + dt = None + for tmpl in templates: + try: + dt = datetime.strptime(parseable_part, tmpl) + except ValueError: + continue + if dt is None: + tmpl = "could not parse W3CDTF datetime string '%s'" + raise ValueError(tmpl % w3cdtf_str) + if len(offset_str) == 6: + return cls._offset_dt(dt, offset_str) + return dt + + def _set_element_datetime(self, prop_name, value): + """ + Set date/time value of child element having *prop_name* to *value*. + """ + if not isinstance(value, datetime): + tmpl = ( + "property requires object, got %s" + ) + raise ValueError(tmpl % type(value)) + element = self._get_or_add(prop_name) + dt_str = value.strftime('%Y-%m-%dT%H:%M:%SZ') + element.text = dt_str + if prop_name in ('created', 'modified'): + # These two require an explicit 'xsi:type="dcterms:W3CDTF"' + # attribute. The first and last line are a hack required to add + # the xsi namespace to the root element rather than each child + # element in which it is referenced + self.set(qn('xsi:foo'), 'bar') + element.set(qn('xsi:type'), 'dcterms:W3CDTF') + del self.attrib[qn('xsi:foo')] + + def _set_element_text(self, prop_name, value): + """ + Set string value of *name* property to *value*. + """ + value = str(value) + if len(value) > 255: + tmpl = ( + "exceeded 255 char limit for property, got:\n\n'%s'" + ) + raise ValueError(tmpl % value) + element = self._get_or_add(prop_name) + element.text = value + + def _text_of_element(self, property_name): + """ + Return the text in the element matching *property_name*, or an empty + string if the element is not present or contains no text. + """ + element = getattr(self, property_name) + if element is None: + return '' + if element.text is None: + return '' + return element.text + diff --git a/docx/oxml/ns.py b/docx/oxml/ns.py index e6f6a4ac..27f3bf1a 100644 --- a/docx/oxml/ns.py +++ b/docx/oxml/ns.py @@ -12,6 +12,7 @@ 'c': ('http://schemas.openxmlformats.org/drawingml/2006/chart'), 'cp': ('http://schemas.openxmlformats.org/package/2006/metadata/core-pr' 'operties'), + 'vt' : ("http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"), 'dc': ('http://purl.org/dc/elements/1.1/'), 'dcmitype': ('http://purl.org/dc/dcmitype/'), 'dcterms': ('http://purl.org/dc/terms/'), diff --git a/docx/parts/document.py b/docx/parts/document.py index 01266b3b..66154b04 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -36,6 +36,14 @@ def core_properties(self): """ return self.package.core_properties + @property + def custom_properties(self): + """ + A |CustomProperties| object providing read/write access to the custom + properties of this document. + """ + return self.package.custom_properties + @property def document(self): """ diff --git a/tests/opc/test_customprops.py b/tests/opc/test_customprops.py new file mode 100644 index 00000000..2be6409f --- /dev/null +++ b/tests/opc/test_customprops.py @@ -0,0 +1,180 @@ +# encoding: utf-8 + +""" +Unit test suite for the docx.opc.customprops module +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest + +from datetime import datetime + +from docx.opc.coreprops import CoreProperties +from docx.oxml import parse_xml + + +class DescribeCustomProperties(object): + + def it_knows_the_string_property_values(self, text_prop_get_fixture): + core_properties, prop_name, expected_value = text_prop_get_fixture + actual_value = getattr(core_properties, prop_name) + assert actual_value == expected_value + + def it_can_change_the_string_property_values(self, text_prop_set_fixture): + core_properties, prop_name, value, expected_xml = text_prop_set_fixture + setattr(core_properties, prop_name, value) + assert core_properties._element.xml == expected_xml + + def it_knows_the_date_property_values(self, date_prop_get_fixture): + core_properties, prop_name, expected_datetime = date_prop_get_fixture + actual_datetime = getattr(core_properties, prop_name) + assert actual_datetime == expected_datetime + + def it_can_change_the_date_property_values(self, date_prop_set_fixture): + core_properties, prop_name, value, expected_xml = ( + date_prop_set_fixture + ) + setattr(core_properties, prop_name, value) + assert core_properties._element.xml == expected_xml + + def it_knows_the_revision_number(self, revision_get_fixture): + core_properties, expected_revision = revision_get_fixture + assert core_properties.revision == expected_revision + + def it_can_change_the_revision_number(self, revision_set_fixture): + core_properties, revision, expected_xml = revision_set_fixture + core_properties.revision = revision + assert core_properties._element.xml == expected_xml + + # fixtures ------------------------------------------------------- + + @pytest.fixture(params=[ + ('created', datetime(2012, 11, 17, 16, 37, 40)), + ('last_printed', datetime(2014, 6, 4, 4, 28)), + ('modified', None), + ]) + def date_prop_get_fixture(self, request, core_properties): + prop_name, expected_datetime = request.param + return core_properties, prop_name, expected_datetime + + @pytest.fixture(params=[ + ('created', 'dcterms:created', datetime(2001, 2, 3, 4, 5), + '2001-02-03T04:05:00Z', ' xsi:type="dcterms:W3CDTF"'), + ('last_printed', 'cp:lastPrinted', datetime(2014, 6, 4, 4), + '2014-06-04T04:00:00Z', ''), + ('modified', 'dcterms:modified', datetime(2005, 4, 3, 2, 1), + '2005-04-03T02:01:00Z', ' xsi:type="dcterms:W3CDTF"'), + ]) + def date_prop_set_fixture(self, request): + prop_name, tagname, value, str_val, attrs = request.param + coreProperties = self.coreProperties(None, None) + core_properties = CoreProperties(parse_xml(coreProperties)) + expected_xml = self.coreProperties(tagname, str_val, attrs) + return core_properties, prop_name, value, expected_xml + + @pytest.fixture(params=[ + ('42', 42), (None, 0), ('foobar', 0), ('-17', 0), ('32.7', 0) + ]) + def revision_get_fixture(self, request): + str_val, expected_revision = request.param + tagname = '' if str_val is None else 'cp:revision' + coreProperties = self.coreProperties(tagname, str_val) + core_properties = CoreProperties(parse_xml(coreProperties)) + return core_properties, expected_revision + + @pytest.fixture(params=[ + (42, '42'), + ]) + def revision_set_fixture(self, request): + value, str_val = request.param + coreProperties = self.coreProperties(None, None) + core_properties = CoreProperties(parse_xml(coreProperties)) + expected_xml = self.coreProperties('cp:revision', str_val) + return core_properties, value, expected_xml + + @pytest.fixture(params=[ + ('author', 'python-docx'), + ('category', ''), + ('comments', ''), + ('content_status', 'DRAFT'), + ('identifier', 'GXS 10.2.1ab'), + ('keywords', 'foo bar baz'), + ('language', 'US-EN'), + ('last_modified_by', 'Steve Canny'), + ('subject', 'Spam'), + ('title', 'Word Document'), + ('version', '1.2.88'), + ]) + def text_prop_get_fixture(self, request, core_properties): + prop_name, expected_value = request.param + return core_properties, prop_name, expected_value + + @pytest.fixture(params=[ + ('author', 'dc:creator', 'scanny'), + ('category', 'cp:category', 'silly stories'), + ('comments', 'dc:description', 'Bar foo to you'), + ('content_status', 'cp:contentStatus', 'FINAL'), + ('identifier', 'dc:identifier', 'GT 5.2.xab'), + ('keywords', 'cp:keywords', 'dog cat moo'), + ('language', 'dc:language', 'GB-EN'), + ('last_modified_by', 'cp:lastModifiedBy', 'Billy Bob'), + ('subject', 'dc:subject', 'Eggs'), + ('title', 'dc:title', 'Dissertation'), + ('version', 'cp:version', '81.2.8'), + ]) + def text_prop_set_fixture(self, request): + prop_name, tagname, value = request.param + coreProperties = self.coreProperties(None, None) + core_properties = CoreProperties(parse_xml(coreProperties)) + expected_xml = self.coreProperties(tagname, value) + return core_properties, prop_name, value, expected_xml + + # fixture components --------------------------------------------- + + def coreProperties(self, tagname, str_val, attrs=''): + tmpl = ( + '%s\n' + ) + if not tagname: + child_element = '' + elif not str_val: + child_element = '\n <%s%s/>\n' % (tagname, attrs) + else: + child_element = ( + '\n <%s%s>%s\n' % (tagname, attrs, str_val, tagname) + ) + return tmpl % child_element + + @pytest.fixture + def core_properties(self): + element = parse_xml( + b'' + b'\n\n' + b' DRAFT\n' + b' python-docx\n' + b' 2012-11-17T11:07:' + b'40-05:30\n' + b' \n' + b' GXS 10.2.1ab\n' + b' US-EN\n' + b' 2014-06-04T04:28:00Z\n' + b' foo bar baz\n' + b' Steve Canny\n' + b' 4\n' + b' Spam\n' + b' Word Document\n' + b' 1.2.88\n' + b'\n' + ) + return CoreProperties(element) From 6666509eaee1244e603dd266f6f57891df27fcb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ko=CC=88ller?= Date: Fri, 23 Nov 2018 11:40:05 +0100 Subject: [PATCH 2/3] add tests, fix type handling --- docx/opc/customprops.py | 35 +++- docx/oxml/__init__.py | 10 +- docx/oxml/customprops.py | 5 +- features/doc-customprops.feature | 28 +++ features/steps/customprops.py | 89 ++++++++ .../steps/test_files/doc-customprops.docx | Bin 0 -> 7922 bytes .../steps/test_files/doc-no-customprops.docx | Bin 0 -> 11394 bytes tests/opc/parts/test_customprops.py | 51 +++++ tests/opc/test_customprops.py | 194 +++++------------- 9 files changed, 262 insertions(+), 150 deletions(-) create mode 100644 features/doc-customprops.feature create mode 100644 features/steps/customprops.py create mode 100644 features/steps/test_files/doc-customprops.docx create mode 100644 features/steps/test_files/doc-no-customprops.docx create mode 100644 tests/opc/parts/test_customprops.py diff --git a/docx/opc/customprops.py b/docx/opc/customprops.py index 944cb3bb..dcac276e 100644 --- a/docx/opc/customprops.py +++ b/docx/opc/customprops.py @@ -9,6 +9,7 @@ absolute_import, division, print_function, unicode_literals ) +import numbers from lxml import etree NS_VT = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" @@ -22,23 +23,47 @@ def __init__(self, element): self._element = element def __getitem__( self, item ): - # print(etree.tostring(self._element, pretty_print=True)) prop = self.lookup(item) if prop is not None : - return prop[0].text + # print(etree.tostring(prop, pretty_print=True)) + elm = prop[0] + if elm.tag == '{%s}i4' % NS_VT: + try: + return int(elm.text) + except: + return elm.text + elif elm.tag == '{%s}bool' % NS_VT: + return True if elm.text == '1' else False + return elm.text def __setitem__( self, key, value ): prop = self.lookup(key) if prop is None : + elmType = 'lpwstr' + if isinstance(value, bool): + elmType = 'bool' + value = str(1 if value else 0) + elif isinstance(value, numbers.Number): + elmType = 'i4' + value = str(int(value)) prop = etree.SubElement( self._element, "property" ) - elm = etree.SubElement(prop, '{%s}lpwstr' % NS_VT, nsmap = {'vt':NS_VT} ) + elm = etree.SubElement(prop, '{%s}%s' %(NS_VT, elmType), nsmap = {'vt':NS_VT} ) + elm.text = value prop.set("name", key) prop.set("fmtid", "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}") prop.set("pid", "%s" % str(len(self._element) + 1)) else: elm = prop[0] - elm.text = value - # etree.tostring(prop, pretty_print=True) + if elm.tag == '{%s}i4' % NS_VT: + elm.text = str(int(value)) + elif elm.tag == '{%s}bool' % NS_VT: + elm.text = str(1 if value else 0) + else: + elm.text = '%s' % str(value) + print(etree.tostring(prop, pretty_print=True)) + + def __len__( self ): + return len(self._element) def lookup(self, item): for child in self._element : diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 6d82f964..6121a1e1 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -39,6 +39,14 @@ def register_element_cls(tag, cls): namespace = element_class_lookup.get_namespace(nsmap[nspfx]) namespace[tagroot] = cls +def register_element_cls_ns(tag, ns, cls): + """ + Register *cls* to be constructed when the oxml parser encounters an + element with matching *tag*. *tag* is a string of the form + ``nspfx:tagroot``, e.g. ``'w:document'``. + """ + namespace = element_class_lookup.get_namespace(ns) + namespace[tag] = cls def OxmlElement(nsptag_str, attrs=None, nsdecls=None): """ @@ -71,7 +79,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('cp:coreProperties', CT_CoreProperties) from .customprops import CT_CustomProperties -#register_element_cls('Properties', CT_CustomProperties) +register_element_cls_ns('Properties', 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties', CT_CustomProperties) from .document import CT_Body, CT_Document register_element_cls('w:body', CT_Body) diff --git a/docx/oxml/customprops.py b/docx/oxml/customprops.py index 2c7ef343..3be69e4d 100644 --- a/docx/oxml/customprops.py +++ b/docx/oxml/customprops.py @@ -14,10 +14,11 @@ from lxml import etree from .ns import nsdecls, qn from .xmlchemy import BaseOxmlElement, ZeroOrOne +from . import parse_xml class CT_CustomProperties(BaseOxmlElement): """ - ```` element, the root element of the Custom Properties + ```` element, the root element of the Custom Properties part stored as ``/docProps/custom.xml``. String elements are limited in length to 255 unicode characters. """ @@ -32,7 +33,7 @@ def new(cls): Return a new ```` element """ xml = cls._customProperties_tmpl - customProperties = ct_parse_xml(xml) + customProperties = parse_xml(xml) return customProperties def _datetime_of_element(self, property_name): diff --git a/features/doc-customprops.feature b/features/doc-customprops.feature new file mode 100644 index 00000000..2dcec28a --- /dev/null +++ b/features/doc-customprops.feature @@ -0,0 +1,28 @@ +Feature: Read and write custom document properties + In order to find documents and make them manageable by digital means + As a developer using python-docx + I need to access and modify the Dublin Core metadata for a document + + + Scenario: read the custom properties of a document + Given a document having known custom properties + Then I can access the custom properties object + And the custom property values match the known values + + + Scenario: change the custom properties of a document + Given a document having known custom properties + When I assign new values to the custom properties + Then the custom property values match the new values + + + Scenario: a default custom properties part is added if doc doesn't have one + Given a document having no custom properties part + When I access the custom properties object + Then a custom properties part with no values is added + + + Scenario: set custom properties on a document that doesn't have one + Given a document having no custom properties part + When I assign new values to the custom properties + Then the custom property values match the new values diff --git a/features/steps/customprops.py b/features/steps/customprops.py new file mode 100644 index 00000000..d3c06c9d --- /dev/null +++ b/features/steps/customprops.py @@ -0,0 +1,89 @@ +# encoding: utf-8 + +""" +Gherkin step implementations for custom properties-related features. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from datetime import datetime, timedelta + +from behave import given, then, when + +from docx import Document +from docx.opc.customprops import CustomProperties + +from helpers import test_docx + + +# given =================================================== + +@given('a document having known custom properties') +def given_a_document_having_known_custom_properties(context): + context.document = Document(test_docx('doc-customprops')) + + +@given('a document having no custom properties part') +def given_a_document_having_no_custom_properties_part(context): + context.document = Document(test_docx('doc-no-customprops')) + + +# when ==================================================== + +@when('I access the custom properties object') +def when_I_access_the_custom_properties_object(context): + context.document.custom_properties + + +@when("I assign new values to the custom properties") +def when_I_assign_new_values_to_the_custom_properties(context): + context.propvals = ( + ('CustomPropBool', False), + ('CustomPropInt', 1), + ('CustomPropString', 'Lorem ipsum'), + ) + custom_properties = context.document.custom_properties + for name, value in context.propvals: + custom_properties[name] = value + + +# then ==================================================== + +@then('a custom properties part with no values is added') +def then_a_custom_properties_part_with_no_values_is_added(context): + custom_properties = context.document.custom_properties + assert len(custom_properties) == 0 + + +@then('I can access the custom properties object') +def then_I_can_access_the_custom_properties_object(context): + document = context.document + custom_properties = document.custom_properties + assert isinstance(custom_properties, CustomProperties) + + +@then('the custom property values match the known values') +def then_the_custom_property_values_match_the_known_values(context): + known_propvals = ( + ('CustomPropBool', True), + ('CustomPropInt', 13), + ('CustomPropString', 'Test String'), + ) + custom_properties = context.document.custom_properties + for name, expected_value in known_propvals: + value = custom_properties[name] + assert value == expected_value, ( + "got '%s' for custom property '%s'" % (value, name) + ) + + +@then('the custom property values match the new values') +def then_the_custom_property_values_match_the_new_values(context): + custom_properties = context.document.custom_properties + for name, expected_value in context.propvals: + value = custom_properties[name] + assert value == expected_value, ( + "got '%s' for custom property '%s'" % (value, name) + ) diff --git a/features/steps/test_files/doc-customprops.docx b/features/steps/test_files/doc-customprops.docx new file mode 100644 index 0000000000000000000000000000000000000000..a3dc7a027350607fc79dee5ef7aa29355665a7fc GIT binary patch literal 7922 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

>9!)DBQj43=}|5!4c+&rZJ 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\n' + '\n' + ' \n' + ' %s\n' + ' \n' + '' + ) + return tmpl %(prop_name, str_type, str_value, str_type) + + @pytest.fixture + def custom_properties_blank(self): + element = parse_xml( + '' + '\n' ) - if not tagname: - child_element = '' - elif not str_val: - child_element = '\n <%s%s/>\n' % (tagname, attrs) - else: - child_element = ( - '\n <%s%s>%s\n' % (tagname, attrs, str_val, tagname) - ) - return tmpl % child_element + return CustomProperties(element) @pytest.fixture - def core_properties(self): + def custom_properties_default(self): element = parse_xml( - b'' - b'\n\n' - b' DRAFT\n' - b' python-docx\n' - b' 2012-11-17T11:07:' - b'40-05:30\n' - b' \n' - b' GXS 10.2.1ab\n' - b' US-EN\n' - b' 2014-06-04T04:28:00Z\n' - b' foo bar baz\n' - b' Steve Canny\n' - b' 4\n' - b' Spam\n' - b' Word Document\n' - b' 1.2.88\n' - b'\n' + b'\n' + b'\n' + b' 1\n' + b' 13\n' + b' Test String\n' + b'\n' ) - return CoreProperties(element) + return CustomProperties(element) From f48abe819e1bfb3aaf3ec840ea3a9d19a8c7e8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ko=CC=88ller?= Date: Fri, 7 Dec 2018 11:04:24 +0100 Subject: [PATCH 3/3] remove debug console print on custom properties --- docx/opc/customprops.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docx/opc/customprops.py b/docx/opc/customprops.py index dcac276e..95e3fb7b 100644 --- a/docx/opc/customprops.py +++ b/docx/opc/customprops.py @@ -60,7 +60,6 @@ def __setitem__( self, key, value ): elm.text = str(1 if value else 0) else: elm.text = '%s' % str(value) - print(etree.tostring(prop, pretty_print=True)) def __len__( self ): return len(self._element)