Skip to content

Commit 20349c1

Browse files
renejsummichael-koeller
authored andcommitted
Added customprops
1 parent 36cac78 commit 20349c1

11 files changed

Lines changed: 503 additions & 0 deletions

File tree

docx/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT
1111
from docx.opc.part import PartFactory
1212
from docx.opc.parts.coreprops import CorePropertiesPart
13+
from docx.opc.parts.customprops import CustomPropertiesPart
1314

1415
from docx.parts.document import DocumentPart
1516
from docx.parts.hdrftr import FooterPart, HeaderPart
@@ -27,6 +28,7 @@ def part_class_selector(content_type, reltype):
2728

2829
PartFactory.part_class_selector = part_class_selector
2930
PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart
31+
PartFactory.part_type_for[CT.OPC_CUSTOM_PROPERTIES] = CustomPropertiesPart
3032
PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = DocumentPart
3133
PartFactory.part_type_for[CT.WML_FOOTER] = FooterPart
3234
PartFactory.part_type_for[CT.WML_HEADER] = HeaderPart

docx/document.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ def core_properties(self):
101101
"""
102102
return self._part.core_properties
103103

104+
@property
105+
def custom_properties(self):
106+
"""
107+
A |CustomProperties| object providing read/write access to the custom
108+
properties of this document.
109+
"""
110+
return self._part.custom_properties
111+
104112
@property
105113
def inline_shapes(self):
106114
"""

docx/opc/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class CONTENT_TYPE(object):
7777
OPC_CORE_PROPERTIES = (
7878
'application/vnd.openxmlformats-package.core-properties+xml'
7979
)
80+
OPC_CUSTOM_PROPERTIES = (
81+
'application/vnd.openxmlformats-officedocument.custom-properties+xml'
82+
)
8083
OPC_DIGITAL_SIGNATURE_CERTIFICATE = (
8184
'application/vnd.openxmlformats-package.digital-signature-certificat'
8285
'e'

docx/opc/customprops.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# encoding: utf-8
2+
3+
"""
4+
The :mod:`pptx.packaging` module coheres around the concerns of reading and
5+
writing presentations to and from a .pptx file.
6+
"""
7+
8+
from __future__ import (
9+
absolute_import, division, print_function, unicode_literals
10+
)
11+
12+
from lxml import etree
13+
14+
NS_VT = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
15+
16+
class CustomProperties(object):
17+
"""
18+
Corresponds to part named ``/docProps/custom.xml``, containing the custom
19+
document properties for this document package.
20+
"""
21+
def __init__(self, element):
22+
self._element = element
23+
24+
def __getitem__( self, item ):
25+
# print(etree.tostring(self._element, pretty_print=True))
26+
prop = self.lookup(item)
27+
if prop is not None :
28+
return prop[0].text
29+
30+
def __setitem__( self, key, value ):
31+
prop = self.lookup(key)
32+
if prop is None :
33+
prop = etree.SubElement( self._element, "property" )
34+
elm = etree.SubElement(prop, '{%s}lpwstr' % NS_VT, nsmap = {'vt':NS_VT} )
35+
prop.set("name", key)
36+
prop.set("fmtid", "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}")
37+
prop.set("pid", "%s" % str(len(self._element) + 1))
38+
else:
39+
elm = prop[0]
40+
elm.text = value
41+
# etree.tostring(prop, pretty_print=True)
42+
43+
def lookup(self, item):
44+
for child in self._element :
45+
if child.get("name") == item :
46+
return child
47+
return None

docx/opc/package.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from docx.opc.packuri import PACKAGE_URI, PackURI
99
from docx.opc.part import PartFactory
1010
from docx.opc.parts.coreprops import CorePropertiesPart
11+
from docx.opc.parts.customprops import CustomPropertiesPart
1112
from docx.opc.pkgreader import PackageReader
1213
from docx.opc.pkgwriter import PackageWriter
1314
from docx.opc.rel import Relationships
@@ -41,6 +42,14 @@ def core_properties(self):
4142
"""
4243
return self._core_properties_part.core_properties
4344

45+
@property
46+
def custom_properties(self):
47+
"""
48+
|CustomProperties| object providing read/write access to the Dublin
49+
Core properties for this document.
50+
"""
51+
return self._custom_properties_part.custom_properties
52+
4453
def iter_rels(self):
4554
"""
4655
Generate exactly one reference to each relationship in the package by
@@ -184,6 +193,19 @@ def _core_properties_part(self):
184193
self.relate_to(core_properties_part, RT.CORE_PROPERTIES)
185194
return core_properties_part
186195

196+
@property
197+
def _custom_properties_part(self):
198+
"""
199+
|CustomPropertiesPart| object related to this package. Creates
200+
a default custom properties part if one is not present (not common).
201+
"""
202+
try:
203+
return self.part_related_by(RT.CUSTOM_PROPERTIES)
204+
except KeyError:
205+
custom_properties_part = CustomPropertiesPart.default(self)
206+
self.relate_to(custom_properties_part, RT.CUSTOM_PROPERTIES)
207+
return custom_properties_part
208+
187209

188210
class Unmarshaller(object):
189211
"""Hosts static methods for unmarshalling a package from a |PackageReader|."""

docx/opc/parts/customprops.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Custom properties part, corresponds to ``/docProps/custom.xml`` part in package.
5+
"""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
10+
11+
from lxml import etree
12+
13+
from datetime import datetime
14+
15+
from ..constants import CONTENT_TYPE as CT
16+
from ..customprops import CustomProperties
17+
from ...oxml.customprops import CT_CustomProperties
18+
from ..packuri import PackURI
19+
from ..part import XmlPart
20+
21+
# configure XML parser
22+
parser_lookup = etree.ElementDefaultClassLookup(element=CT_CustomProperties)
23+
ct_parser = etree.XMLParser(remove_blank_text=True)
24+
ct_parser.set_element_class_lookup(parser_lookup)
25+
26+
27+
def ct_parse_xml(xml):
28+
"""
29+
Return root lxml element obtained by parsing XML character string in
30+
*xml*, which can be either a Python 2.x string or unicode. The custom
31+
parser is used, so custom element classes are produced for elements in
32+
*xml* that have them.
33+
"""
34+
root_element = etree.fromstring(xml, ct_parser)
35+
return root_element
36+
37+
38+
39+
class CustomPropertiesPart(XmlPart):
40+
"""
41+
Corresponds to part named ``/docProps/custom.xml``, containing the custom
42+
document properties for this document package.
43+
"""
44+
@classmethod
45+
def default(cls, package):
46+
"""
47+
Return a new |CustomPropertiesPart| object initialized with default
48+
values for its base properties.
49+
"""
50+
custom_properties_part = cls._new(package)
51+
custom_properties = custom_properties_part.custom_properties
52+
return custom_properties_part
53+
54+
@property
55+
def custom_properties(self):
56+
"""
57+
A |CustomProperties| object providing read/write access to the custom
58+
properties contained in this custom properties part.
59+
"""
60+
return CustomProperties(self.element)
61+
62+
@classmethod
63+
def load(cls, partname, content_type, blob, package):
64+
element = ct_parse_xml(blob)
65+
return cls(partname, content_type, element, package)
66+
67+
@classmethod
68+
def _new(cls, package):
69+
partname = PackURI('/docProps/custom.xml')
70+
content_type = CT.OPC_CUSTOM_PROPERTIES
71+
customProperties = CT_CustomProperties.new()
72+
return CustomPropertiesPart(
73+
partname, content_type, customProperties, package
74+
)

docx/oxml/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
7272
from .coreprops import CT_CoreProperties # noqa
7373
register_element_cls('cp:coreProperties', CT_CoreProperties)
7474

75+
from .customprops import CT_CustomProperties # noqa
76+
#register_element_cls('Properties', CT_CustomProperties)
77+
7578
from .document import CT_Body, CT_Document # noqa
7679
register_element_cls('w:body', CT_Body)
7780
register_element_cls('w:document', CT_Document)

docx/oxml/customprops.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# encoding: utf-8
2+
3+
"""
4+
lxml custom element classes for core properties-related XML elements.
5+
"""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
10+
11+
import re
12+
13+
from datetime import datetime, timedelta
14+
from lxml import etree
15+
from .ns import nsdecls, qn
16+
from .xmlchemy import BaseOxmlElement, ZeroOrOne
17+
18+
class CT_CustomProperties(BaseOxmlElement):
19+
"""
20+
``<cp:customProperties>`` element, the root element of the Custom Properties
21+
part stored as ``/docProps/custom.xml``. String elements are
22+
limited in length to 255 unicode characters.
23+
"""
24+
25+
_customProperties_tmpl = (
26+
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" %s/>\n' % nsdecls('vt')
27+
)
28+
29+
@classmethod
30+
def new(cls):
31+
"""
32+
Return a new ``<property>`` element
33+
"""
34+
xml = cls._customProperties_tmpl
35+
customProperties = ct_parse_xml(xml)
36+
return customProperties
37+
38+
def _datetime_of_element(self, property_name):
39+
element = getattr(self, property_name)
40+
if element is None:
41+
return None
42+
datetime_str = element.text
43+
try:
44+
return self._parse_W3CDTF_to_datetime(datetime_str)
45+
except ValueError:
46+
# invalid datetime strings are ignored
47+
return None
48+
49+
def _get_or_add(self, prop_name):
50+
"""
51+
Return element returned by 'get_or_add_' method for *prop_name*.
52+
"""
53+
get_or_add_method_name = 'get_or_add_%s' % prop_name
54+
get_or_add_method = getattr(self, get_or_add_method_name)
55+
element = get_or_add_method()
56+
return element
57+
58+
@classmethod
59+
def _offset_dt(cls, dt, offset_str):
60+
"""
61+
Return a |datetime| instance that is offset from datetime *dt* by
62+
the timezone offset specified in *offset_str*, a string like
63+
``'-07:00'``.
64+
"""
65+
match = cls._offset_pattern.match(offset_str)
66+
if match is None:
67+
raise ValueError(
68+
"'%s' is not a valid offset string" % offset_str
69+
)
70+
sign, hours_str, minutes_str = match.groups()
71+
sign_factor = -1 if sign == '+' else 1
72+
hours = int(hours_str) * sign_factor
73+
minutes = int(minutes_str) * sign_factor
74+
td = timedelta(hours=hours, minutes=minutes)
75+
return dt + td
76+
77+
_offset_pattern = re.compile('([+-])(\d\d):(\d\d)')
78+
79+
@classmethod
80+
def _parse_W3CDTF_to_datetime(cls, w3cdtf_str):
81+
# valid W3CDTF date cases:
82+
# yyyy e.g. '2003'
83+
# yyyy-mm e.g. '2003-12'
84+
# yyyy-mm-dd e.g. '2003-12-31'
85+
# UTC timezone e.g. '2003-12-31T10:14:55Z'
86+
# numeric timezone e.g. '2003-12-31T10:14:55-08:00'
87+
templates = (
88+
'%Y-%m-%dT%H:%M:%S',
89+
'%Y-%m-%d',
90+
'%Y-%m',
91+
'%Y',
92+
)
93+
# strptime isn't smart enough to parse literal timezone offsets like
94+
# '-07:30', so we have to do it ourselves
95+
parseable_part = w3cdtf_str[:19]
96+
offset_str = w3cdtf_str[19:]
97+
dt = None
98+
for tmpl in templates:
99+
try:
100+
dt = datetime.strptime(parseable_part, tmpl)
101+
except ValueError:
102+
continue
103+
if dt is None:
104+
tmpl = "could not parse W3CDTF datetime string '%s'"
105+
raise ValueError(tmpl % w3cdtf_str)
106+
if len(offset_str) == 6:
107+
return cls._offset_dt(dt, offset_str)
108+
return dt
109+
110+
def _set_element_datetime(self, prop_name, value):
111+
"""
112+
Set date/time value of child element having *prop_name* to *value*.
113+
"""
114+
if not isinstance(value, datetime):
115+
tmpl = (
116+
"property requires <type 'datetime.datetime'> object, got %s"
117+
)
118+
raise ValueError(tmpl % type(value))
119+
element = self._get_or_add(prop_name)
120+
dt_str = value.strftime('%Y-%m-%dT%H:%M:%SZ')
121+
element.text = dt_str
122+
if prop_name in ('created', 'modified'):
123+
# These two require an explicit 'xsi:type="dcterms:W3CDTF"'
124+
# attribute. The first and last line are a hack required to add
125+
# the xsi namespace to the root element rather than each child
126+
# element in which it is referenced
127+
self.set(qn('xsi:foo'), 'bar')
128+
element.set(qn('xsi:type'), 'dcterms:W3CDTF')
129+
del self.attrib[qn('xsi:foo')]
130+
131+
def _set_element_text(self, prop_name, value):
132+
"""
133+
Set string value of *name* property to *value*.
134+
"""
135+
value = str(value)
136+
if len(value) > 255:
137+
tmpl = (
138+
"exceeded 255 char limit for property, got:\n\n'%s'"
139+
)
140+
raise ValueError(tmpl % value)
141+
element = self._get_or_add(prop_name)
142+
element.text = value
143+
144+
def _text_of_element(self, property_name):
145+
"""
146+
Return the text in the element matching *property_name*, or an empty
147+
string if the element is not present or contains no text.
148+
"""
149+
element = getattr(self, property_name)
150+
if element is None:
151+
return ''
152+
if element.text is None:
153+
return ''
154+
return element.text
155+

docx/oxml/ns.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
2020
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
2121
"sl": "http://schemas.openxmlformats.org/schemaLibrary/2006/main",
22+
'vt': "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
2223
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
2324
'w14': "http://schemas.microsoft.com/office/word/2010/wordml",
2425
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",

docx/parts/document.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ def core_properties(self):
4444
"""
4545
return self.package.core_properties
4646

47+
@property
48+
def custom_properties(self):
49+
"""
50+
A |CustomProperties| object providing read/write access to the custom
51+
properties of this document.
52+
"""
53+
return self.package.custom_properties
54+
4755
@property
4856
def document(self):
4957
"""

0 commit comments

Comments
 (0)