Skip to content

Commit aeaf753

Browse files
add tests, fix type handling
1 parent 20349c1 commit aeaf753

9 files changed

Lines changed: 262 additions & 150 deletions

File tree

docx/opc/customprops.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
absolute_import, division, print_function, unicode_literals
1010
)
1111

12+
import numbers
1213
from lxml import etree
1314

1415
NS_VT = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
@@ -22,23 +23,47 @@ def __init__(self, element):
2223
self._element = element
2324

2425
def __getitem__( self, item ):
25-
# print(etree.tostring(self._element, pretty_print=True))
2626
prop = self.lookup(item)
2727
if prop is not None :
28-
return prop[0].text
28+
# print(etree.tostring(prop, pretty_print=True))
29+
elm = prop[0]
30+
if elm.tag == '{%s}i4' % NS_VT:
31+
try:
32+
return int(elm.text)
33+
except:
34+
return elm.text
35+
elif elm.tag == '{%s}bool' % NS_VT:
36+
return True if elm.text == '1' else False
37+
return elm.text
2938

3039
def __setitem__( self, key, value ):
3140
prop = self.lookup(key)
3241
if prop is None :
42+
elmType = 'lpwstr'
43+
if isinstance(value, bool):
44+
elmType = 'bool'
45+
value = str(1 if value else 0)
46+
elif isinstance(value, numbers.Number):
47+
elmType = 'i4'
48+
value = str(int(value))
3349
prop = etree.SubElement( self._element, "property" )
34-
elm = etree.SubElement(prop, '{%s}lpwstr' % NS_VT, nsmap = {'vt':NS_VT} )
50+
elm = etree.SubElement(prop, '{%s}%s' %(NS_VT, elmType), nsmap = {'vt':NS_VT} )
51+
elm.text = value
3552
prop.set("name", key)
3653
prop.set("fmtid", "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}")
3754
prop.set("pid", "%s" % str(len(self._element) + 1))
3855
else:
3956
elm = prop[0]
40-
elm.text = value
41-
# etree.tostring(prop, pretty_print=True)
57+
if elm.tag == '{%s}i4' % NS_VT:
58+
elm.text = str(int(value))
59+
elif elm.tag == '{%s}bool' % NS_VT:
60+
elm.text = str(1 if value else 0)
61+
else:
62+
elm.text = '%s' % str(value)
63+
print(etree.tostring(prop, pretty_print=True))
64+
65+
def __len__( self ):
66+
return len(self._element)
4267

4368
def lookup(self, item):
4469
for child in self._element :

docx/oxml/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ def register_element_cls(tag, cls):
3939
namespace = element_class_lookup.get_namespace(nsmap[nspfx])
4040
namespace[tagroot] = cls
4141

42+
def register_element_cls_ns(tag, ns, cls):
43+
"""
44+
Register *cls* to be constructed when the oxml parser encounters an
45+
element with matching *tag*. *tag* is a string of the form
46+
``nspfx:tagroot``, e.g. ``'w:document'``.
47+
"""
48+
namespace = element_class_lookup.get_namespace(ns)
49+
namespace[tag] = cls
4250

4351
def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
4452
"""
@@ -73,7 +81,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
7381
register_element_cls('cp:coreProperties', CT_CoreProperties)
7482

7583
from .customprops import CT_CustomProperties # noqa
76-
#register_element_cls('Properties', CT_CustomProperties)
84+
register_element_cls_ns('Properties', 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties', CT_CustomProperties)
7785

7886
from .document import CT_Body, CT_Document # noqa
7987
register_element_cls('w:body', CT_Body)

docx/oxml/customprops.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
from lxml import etree
1515
from .ns import nsdecls, qn
1616
from .xmlchemy import BaseOxmlElement, ZeroOrOne
17+
from . import parse_xml
1718

1819
class CT_CustomProperties(BaseOxmlElement):
1920
"""
20-
``<cp:customProperties>`` element, the root element of the Custom Properties
21+
``<Properties>`` element, the root element of the Custom Properties
2122
part stored as ``/docProps/custom.xml``. String elements are
2223
limited in length to 255 unicode characters.
2324
"""
@@ -32,7 +33,7 @@ def new(cls):
3233
Return a new ``<property>`` element
3334
"""
3435
xml = cls._customProperties_tmpl
35-
customProperties = ct_parse_xml(xml)
36+
customProperties = parse_xml(xml)
3637
return customProperties
3738

3839
def _datetime_of_element(self, property_name):

features/doc-customprops.feature

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Feature: Read and write custom document properties
2+
In order to find documents and make them manageable by digital means
3+
As a developer using python-docx
4+
I need to access and modify the Dublin Core metadata for a document
5+
6+
7+
Scenario: read the custom properties of a document
8+
Given a document having known custom properties
9+
Then I can access the custom properties object
10+
And the custom property values match the known values
11+
12+
13+
Scenario: change the custom properties of a document
14+
Given a document having known custom properties
15+
When I assign new values to the custom properties
16+
Then the custom property values match the new values
17+
18+
19+
Scenario: a default custom properties part is added if doc doesn't have one
20+
Given a document having no custom properties part
21+
When I access the custom properties object
22+
Then a custom properties part with no values is added
23+
24+
25+
Scenario: set custom properties on a document that doesn't have one
26+
Given a document having no custom properties part
27+
When I assign new values to the custom properties
28+
Then the custom property values match the new values

features/steps/customprops.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Gherkin step implementations for custom properties-related features.
5+
"""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
10+
11+
from datetime import datetime, timedelta
12+
13+
from behave import given, then, when
14+
15+
from docx import Document
16+
from docx.opc.customprops import CustomProperties
17+
18+
from helpers import test_docx
19+
20+
21+
# given ===================================================
22+
23+
@given('a document having known custom properties')
24+
def given_a_document_having_known_custom_properties(context):
25+
context.document = Document(test_docx('doc-customprops'))
26+
27+
28+
@given('a document having no custom properties part')
29+
def given_a_document_having_no_custom_properties_part(context):
30+
context.document = Document(test_docx('doc-no-customprops'))
31+
32+
33+
# when ====================================================
34+
35+
@when('I access the custom properties object')
36+
def when_I_access_the_custom_properties_object(context):
37+
context.document.custom_properties
38+
39+
40+
@when("I assign new values to the custom properties")
41+
def when_I_assign_new_values_to_the_custom_properties(context):
42+
context.propvals = (
43+
('CustomPropBool', False),
44+
('CustomPropInt', 1),
45+
('CustomPropString', 'Lorem ipsum'),
46+
)
47+
custom_properties = context.document.custom_properties
48+
for name, value in context.propvals:
49+
custom_properties[name] = value
50+
51+
52+
# then ====================================================
53+
54+
@then('a custom properties part with no values is added')
55+
def then_a_custom_properties_part_with_no_values_is_added(context):
56+
custom_properties = context.document.custom_properties
57+
assert len(custom_properties) == 0
58+
59+
60+
@then('I can access the custom properties object')
61+
def then_I_can_access_the_custom_properties_object(context):
62+
document = context.document
63+
custom_properties = document.custom_properties
64+
assert isinstance(custom_properties, CustomProperties)
65+
66+
67+
@then('the custom property values match the known values')
68+
def then_the_custom_property_values_match_the_known_values(context):
69+
known_propvals = (
70+
('CustomPropBool', True),
71+
('CustomPropInt', 13),
72+
('CustomPropString', 'Test String'),
73+
)
74+
custom_properties = context.document.custom_properties
75+
for name, expected_value in known_propvals:
76+
value = custom_properties[name]
77+
assert value == expected_value, (
78+
"got '%s' for custom property '%s'" % (value, name)
79+
)
80+
81+
82+
@then('the custom property values match the new values')
83+
def then_the_custom_property_values_match_the_new_values(context):
84+
custom_properties = context.document.custom_properties
85+
for name, expected_value in context.propvals:
86+
value = custom_properties[name]
87+
assert value == expected_value, (
88+
"got '%s' for custom property '%s'" % (value, name)
89+
)
7.74 KB
Binary file not shown.
11.1 KB
Binary file not shown.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Unit test suite for the docx.opc.parts.customprops module
5+
"""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
10+
11+
from datetime import datetime, timedelta
12+
13+
import pytest
14+
15+
from docx.opc.customprops import CustomProperties
16+
from docx.opc.parts.customprops import CustomPropertiesPart
17+
from docx.oxml.customprops import CT_CustomProperties
18+
19+
from ...unitutil.mock import class_mock, instance_mock
20+
21+
22+
class DescribeCustomPropertiesPart(object):
23+
24+
def it_provides_access_to_its_custom_props_object(self, customprops_fixture):
25+
custom_properties_part, CustomProperties_ = customprops_fixture
26+
custom_properties = custom_properties_part.custom_properties
27+
CustomProperties_.assert_called_once_with(custom_properties_part.element)
28+
assert isinstance(custom_properties, CustomProperties)
29+
30+
def it_can_create_a_default_custom_properties_part(self):
31+
custom_properties_part = CustomPropertiesPart.default(None)
32+
assert isinstance(custom_properties_part, CustomPropertiesPart)
33+
custom_properties = custom_properties_part.custom_properties
34+
assert len(custom_properties) == 0
35+
36+
# fixtures ---------------------------------------------
37+
38+
@pytest.fixture
39+
def customprops_fixture(self, element_, CustomProperties_):
40+
custom_properties_part = CustomPropertiesPart(None, None, element_, None)
41+
return custom_properties_part, CustomProperties_
42+
43+
# fixture components -----------------------------------
44+
45+
@pytest.fixture
46+
def CustomProperties_(self, request):
47+
return class_mock(request, 'docx.opc.parts.customprops.CustomProperties')
48+
49+
@pytest.fixture
50+
def element_(self, request):
51+
return instance_mock(request, CT_CustomProperties)

0 commit comments

Comments
 (0)