Skip to content

Commit c376fb9

Browse files
author
Steve Canny
committed
shp: add InlineShapes.__len__()
Along the way: * transplant NamespacePrefixedTag from python-pptx into temporary location * rewrite OxmlElement to only provide namespaces required for that element
1 parent 9c05418 commit c376fb9

File tree

5 files changed

+134
-7
lines changed

5 files changed

+134
-7
lines changed

docx/oxml/shared.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99

1010
nsmap = {
11-
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
11+
'w': ('http://schemas.openxmlformats.org/wordprocessingml/2006/main'),
12+
'wp': ('http://schemas.openxmlformats.org/drawingml/2006/wordprocessingD'
13+
'rawing'),
1214
}
1315

1416
# configure XML parser
@@ -25,12 +27,74 @@
2527
# return oxml_parser.makeelement(qn(tag), nsmap=nsmap)
2628

2729

30+
class NamespacePrefixedTag(str):
31+
"""
32+
Value object that knows the semantics of an XML tag having a namespace
33+
prefix.
34+
"""
35+
def __new__(cls, nstag, *args):
36+
return super(NamespacePrefixedTag, cls).__new__(cls, nstag)
37+
38+
def __init__(self, nstag):
39+
self._pfx, self._local_part = nstag.split(':')
40+
self._ns_uri = nsmap[self._pfx]
41+
42+
@property
43+
def clark_name(self):
44+
return '{%s}%s' % (self._ns_uri, self._local_part)
45+
46+
@property
47+
def local_part(self):
48+
"""
49+
Return the local part of the tag as a string. E.g. 'foobar' is
50+
returned for tag 'f:foobar'.
51+
"""
52+
return self._local_part
53+
54+
@property
55+
def nsmap(self):
56+
"""
57+
Return a dict having a single member, mapping the namespace prefix of
58+
this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This
59+
is handy for passing to xpath calls and other uses.
60+
"""
61+
return {self._pfx: self._ns_uri}
62+
63+
@property
64+
def nspfx(self):
65+
"""
66+
Return the string namespace prefix for the tag, e.g. 'f' is returned
67+
for tag 'f:foobar'.
68+
"""
69+
return self._pfx
70+
71+
@property
72+
def nsuri(self):
73+
"""
74+
Return the namespace URI for the tag, e.g. 'http://foo/bar' would be
75+
returned for tag 'f:foobar' if the 'f' prefix maps to
76+
'http://foo/bar' in nsmap.
77+
"""
78+
return self._ns_uri
79+
80+
2881
def nsdecls(*prefixes):
2982
return ' '.join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes])
3083

3184

32-
def OxmlElement(tag, attrs=None):
33-
return oxml_parser.makeelement(qn(tag), attrib=attrs, nsmap=nsmap)
85+
def OxmlElement(nsptag_str, attrs=None, nsmap=None):
86+
"""
87+
Return a 'loose' lxml element having the tag specified by *nsptag_str*.
88+
*nsptag_str* must contain the standard namespace prefix, e.g. 'a:tbl'.
89+
The resulting element is an instance of the custom element class for this
90+
tag name if one is defined. A dictionary of attribute values may be
91+
provided as *attrs*; they are set if present.
92+
"""
93+
nsptag = NamespacePrefixedTag(nsptag_str)
94+
nsmap = nsmap if nsmap is not None else nsptag.nsmap
95+
return oxml_parser.makeelement(
96+
nsptag.clark_name, attrib=attrs, nsmap=nsmap
97+
)
3498

3599

36100
def oxml_fromstring(text):

docx/parts.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from docx.opc.oxml import serialize_part_xml
88
from docx.opc.package import Part
9-
from docx.oxml.shared import oxml_fromstring
9+
from docx.oxml.shared import nsmap, oxml_fromstring
1010
from docx.shared import lazyproperty
1111
from docx.table import Table
1212
from docx.text import Paragraph
@@ -114,3 +114,9 @@ class InlineShapes(object):
114114
def __init__(self, body_elm):
115115
super(InlineShapes, self).__init__()
116116
self._body = body_elm
117+
118+
def __len__(self):
119+
body = self._body
120+
xpath = './w:p/w:r/w:drawing/wp:inline'
121+
inline_elms = body.xpath(xpath, namespaces=nsmap)
122+
return len(inline_elms)

features/shp-inline-shape-access.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Feature: Access inline shapes in document
33
As an python-docx developer
44
I need the ability to access the inline shapes in a document
55

6-
@wip
76
Scenario: Access inline shapes collection of document
87
Given a document containing two inline shapes
98
Then I can access the inline shape collection of the document

tests/oxml/unitdata/dml.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Test data builders for text XML elements
5+
"""
6+
7+
from ...unitdata import BaseBuilder
8+
9+
10+
class CT_DrawingBuilder(BaseBuilder):
11+
__tag__ = 'w:drawing'
12+
__nspfxs__ = ('w',)
13+
__attrs__ = ()
14+
15+
16+
def a_drawing():
17+
return CT_DrawingBuilder()
18+
19+
20+
class CT_InlineBuilder(BaseBuilder):
21+
__tag__ = 'wp:inline'
22+
__nspfxs__ = ('wp',)
23+
__attrs__ = ('distT', 'distB', 'distL', 'distR')
24+
25+
26+
def an_inline():
27+
return CT_InlineBuilder()

tests/test_parts.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111
from mock import Mock
1212

1313
from docx.oxml.parts import CT_Document
14-
from docx.parts import _Body, _Document
14+
from docx.parts import _Body, _Document, InlineShapes
1515
from docx.table import Table
1616
from docx.text import Paragraph
1717

18+
from .oxml.unitdata.dml import a_drawing, an_inline
1819
from .oxml.unitdata.parts import a_body, a_document
1920
from .oxml.unitdata.table import (
2021
a_gridCol, a_tbl, a_tblGrid, a_tblPr, a_tc, a_tr
2122
)
22-
from .oxml.unitdata.text import a_p, a_sectPr
23+
from .oxml.unitdata.text import a_p, a_sectPr, an_r
2324
from .unitutil import (
2425
function_mock, class_mock, initializer_mock, instance_mock
2526
)
@@ -253,3 +254,33 @@ def _tr_bldr(self, cols):
253254
tc_bldr = self._tc_bldr()
254255
tr_bldr.with_child(tc_bldr)
255256
return tr_bldr
257+
258+
259+
class DescribeInlineShapes(object):
260+
261+
def it_knows_how_many_inline_shapes_it_contains(
262+
self, inline_shapes_fixture):
263+
inline_shapes, inline_shape_count = inline_shapes_fixture
264+
print(inline_shapes._body.xml)
265+
assert len(inline_shapes) == inline_shape_count
266+
267+
# fixtures -------------------------------------------------------
268+
269+
@pytest.fixture
270+
def inline_shapes_fixture(self):
271+
inline_shape_count = 2
272+
body = (
273+
a_body().with_nsdecls('w', 'wp').with_child(
274+
a_p().with_child(
275+
an_r().with_child(
276+
a_drawing().with_child(
277+
an_inline()))).with_child(
278+
an_r().with_child(
279+
a_drawing().with_child(
280+
an_inline())
281+
)
282+
)
283+
)
284+
).element
285+
inline_shapes = InlineShapes(body)
286+
return inline_shapes, inline_shape_count

0 commit comments

Comments
 (0)