Skip to content

Commit 1769f04

Browse files
author
Steve Canny
committed
test: transplant tests.unitutil.cxml
1 parent 767e408 commit 1769f04

File tree

4 files changed

+269
-1
lines changed

4 files changed

+269
-1
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ behave>=1.2.3
22
flake8>=2.0
33
lxml>=3.1.0
44
mock>=1.0.1
5+
pyparsing>=2.0.1
56
pytest>=2.5

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def text_of(relpath):
3838

3939
INSTALL_REQUIRES = ['lxml>=2.3.2']
4040
TEST_SUITE = 'tests'
41-
TESTS_REQUIRE = ['behave', 'mock', 'pytest']
41+
TESTS_REQUIRE = ['behave', 'mock', 'pyparsing', 'pytest']
4242

4343
CLASSIFIERS = [
4444
'Development Status :: 3 - Alpha',

tests/unitutil/cxml.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'), a compact
5+
XML specification language I made up that's useful for producing XML element
6+
trees suitable for unit testing.
7+
"""
8+
9+
from __future__ import print_function
10+
11+
from pyparsing import (
12+
alphas, alphanums, Combine, dblQuotedString, delimitedList, Forward,
13+
Group, Literal, Optional, removeQuotes, stringEnd, Suppress, Word
14+
)
15+
16+
from docx.oxml import parse_xml
17+
from docx.oxml.ns import nsmap
18+
19+
20+
# ====================================================================
21+
# api functions
22+
# ====================================================================
23+
24+
def element(cxel_str):
25+
"""
26+
Return an oxml element parsed from the XML generated from *cxel_str*.
27+
"""
28+
_xml = xml(cxel_str)
29+
return parse_xml(_xml)
30+
31+
32+
def xml(cxel_str):
33+
"""
34+
Return the XML generated from *cxel_str*.
35+
"""
36+
root_token = root_node.parseString(cxel_str)
37+
xml = root_token.element.xml
38+
return xml
39+
40+
41+
# ====================================================================
42+
# internals
43+
# ====================================================================
44+
45+
46+
def nsdecls(*nspfxs):
47+
"""
48+
Return a string containing a namespace declaration for each of *nspfxs*,
49+
in the order they are specified.
50+
"""
51+
nsdecls = ''
52+
for nspfx in nspfxs:
53+
nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx])
54+
return nsdecls
55+
56+
57+
class Element(object):
58+
"""
59+
Represents an XML element, having a namespace, tagname, attributes, and
60+
may contain either text or children (but not both) or may be empty.
61+
"""
62+
def __init__(self, tagname, attrs, text):
63+
self._tagname = tagname
64+
self._attrs = attrs
65+
self._text = text
66+
self._children = []
67+
self._is_root = False
68+
69+
def __repr__(self):
70+
"""
71+
Provide a more meaningful repr value for an Element object, one that
72+
displays the tagname as a simple empty element, e.g. ``<w:pPr/>``.
73+
"""
74+
return "<%s/>" % self._tagname
75+
76+
def connect_children(self, child_node_list):
77+
"""
78+
Make each of the elements appearing in *child_node_list* a child of
79+
this element.
80+
"""
81+
for node in child_node_list:
82+
child = node.element
83+
self._children.append(child)
84+
85+
@classmethod
86+
def from_token(cls, token):
87+
"""
88+
Return an ``Element`` object constructed from a parser element token.
89+
"""
90+
tagname = token.tagname
91+
attrs = [(name, value) for name, value in token.attr_list]
92+
text = token.text
93+
return cls(tagname, attrs, text)
94+
95+
@property
96+
def is_root(self):
97+
"""
98+
|True| if this element is the root of the tree and should include the
99+
namespace prefixes. |False| otherwise.
100+
"""
101+
return self._is_root
102+
103+
@is_root.setter
104+
def is_root(self, value):
105+
self._is_root = bool(value)
106+
107+
@property
108+
def nspfx(self):
109+
"""
110+
The namespace prefix of this element, the empty string (``''``) if
111+
the tag is in the default namespace.
112+
"""
113+
tagname = self._tagname
114+
idx = tagname.find(':')
115+
if idx == -1:
116+
return ''
117+
return tagname[:idx]
118+
119+
@property
120+
def nspfxs(self):
121+
"""
122+
A sequence containing each of the namespace prefixes appearing in
123+
this tree. Each prefix appears once and only once, and in document
124+
order.
125+
"""
126+
def merge(seq, seq_2):
127+
for item in seq_2:
128+
if item in seq:
129+
continue
130+
seq.append(item)
131+
132+
nspfxs = [self.nspfx]
133+
for child in self._children:
134+
merge(nspfxs, child.nspfxs)
135+
return nspfxs
136+
137+
@property
138+
def xml(self):
139+
"""
140+
The XML corresponding to the tree rooted at this element,
141+
pretty-printed using 2-spaces indentation at each level and with
142+
a trailing '\n'.
143+
"""
144+
return self._xml(indent=0)
145+
146+
def _xml(self, indent):
147+
"""
148+
Return a string containing the XML of this element and all its
149+
children with a starting indent of *indent* spaces.
150+
"""
151+
self._indent_str = ' ' * indent
152+
xml = self._start_tag
153+
for child in self._children:
154+
xml += child._xml(indent+2)
155+
xml += self._end_tag
156+
return xml
157+
158+
@property
159+
def _start_tag(self):
160+
"""
161+
The text of the opening tag of this element, including attributes. If
162+
this is the root element, a namespace declaration for each of the
163+
namespace prefixes that occur in this tree is added in front of any
164+
attributes. If this element contains text, that text follows the
165+
start tag. If not, and this element has no children, an empty tag is
166+
returned. Otherwise, an opening tag is returned, followed by
167+
a newline. The tag is indented by this element's indent value in all
168+
cases.
169+
"""
170+
_nsdecls = nsdecls(*self.nspfxs) if self.is_root else ''
171+
tag = '%s<%s%s' % (self._indent_str, self._tagname, _nsdecls)
172+
for attr in self._attrs:
173+
name, value = attr
174+
tag += ' %s="%s"' % (name, value)
175+
if self._text:
176+
tag += '>%s' % self._text
177+
elif self._children:
178+
tag += '>\n'
179+
else:
180+
tag += '/>\n'
181+
return tag
182+
183+
@property
184+
def _end_tag(self):
185+
"""
186+
The text of the closing tag of this element, if there is one. If the
187+
element contains text, no leading indentation is included.
188+
"""
189+
if self._text:
190+
tag = '</%s>\n' % self._tagname
191+
elif self._children:
192+
tag = '%s</%s>\n' % (self._indent_str, self._tagname)
193+
else:
194+
tag = ''
195+
return tag
196+
197+
198+
# ====================================================================
199+
# parser
200+
# ====================================================================
201+
202+
# parse actions ----------------------------------
203+
204+
def connect_node_children(s, loc, tokens):
205+
node = tokens[0]
206+
node.element.connect_children(node.child_node_list)
207+
208+
209+
def connect_root_node_children(root_node):
210+
root_node.element.connect_children(root_node.child_node_list)
211+
root_node.element.is_root = True
212+
213+
214+
def grammar():
215+
# terminals ----------------------------------
216+
colon = Literal(':')
217+
equal = Suppress('=')
218+
slash = Suppress('/')
219+
open_paren = Suppress('(')
220+
close_paren = Suppress(')')
221+
open_brace = Suppress('{')
222+
close_brace = Suppress('}')
223+
224+
# np:tagName ---------------------------------
225+
nspfx = Word(alphas)
226+
local_name = Word(alphas)
227+
tagname = Combine(nspfx + colon + local_name)
228+
229+
# np:attr_name=attr_val ----------------------
230+
attr_name = Word(alphas + ':')
231+
attr_val = Word(alphanums + '-.')
232+
attr_def = Group(attr_name + equal + attr_val)
233+
attr_list = open_brace + delimitedList(attr_def) + close_brace
234+
235+
text = dblQuotedString.setParseAction(removeQuotes)
236+
237+
# w:jc{val=right} ----------------------------
238+
element = (
239+
tagname('tagname')
240+
+ Group(Optional(attr_list))('attr_list')
241+
+ Optional(text, default='')('text')
242+
).setParseAction(Element.from_token)
243+
244+
child_node_list = Forward()
245+
246+
node = Group(
247+
element('element')
248+
+ Group(Optional(slash + child_node_list))('child_node_list')
249+
).setParseAction(connect_node_children)
250+
251+
child_node_list << (
252+
open_paren + delimitedList(node) + close_paren
253+
| node
254+
)
255+
256+
root_node = (
257+
element('element')
258+
+ Group(Optional(slash + child_node_list))('child_node_list')
259+
+ stringEnd
260+
).setParseAction(connect_root_node_children)
261+
262+
return root_node
263+
264+
root_node = grammar()

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ deps =
1515
behave
1616
lxml
1717
mock
18+
pyparsing
1819
pytest
1920

2021
commands =
@@ -25,10 +26,12 @@ commands =
2526
deps =
2627
behave
2728
lxml
29+
pyparsing
2830
pytest
2931

3032
[testenv:py34]
3133
deps =
3234
behave
3335
lxml
36+
pyparsing
3437
pytest

0 commit comments

Comments
 (0)