Skip to content

Commit

Permalink
Merge 1397efb into b8068f8
Browse files Browse the repository at this point in the history
  • Loading branch information
juarezr committed Oct 7, 2020
2 parents b8068f8 + 1397efb commit 5a0a6de
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 5 deletions.
7 changes: 7 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changes
=======

Version 1.6.9
-------------

* Added `toxml()` as convenience wrapper over `totext()`.
By :user:`juarezr`, :issue:`529`.


Version 1.6.8
-------------

Expand Down
2 changes: 0 additions & 2 deletions docs/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ XML files

.. autofunction:: petl.io.xml.fromxml

For writing to an XML file, see :func:`petl.io.text.totext`.


.. module:: petl.io.html
.. _io_html:
Expand Down
2 changes: 1 addition & 1 deletion petl/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from petl.io.text import fromtext, totext, appendtext, teetext

from petl.io.xml import fromxml
from petl.io.xml import fromxml, toxml

from petl.io.html import tohtml, teehtml

Expand Down
106 changes: 105 additions & 1 deletion petl/io/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@


# internal dependencies
from petl.util.base import Table
from petl.util.base import Table, fieldnames, iterpeek
from petl.io.sources import read_source_from_arg
from petl.io.text import totext


def fromxml(source, *args, **kwargs):
Expand Down Expand Up @@ -260,3 +261,106 @@ def _get(v):
else:
return missing
return _get


def toxml(table, filename=None,
root=None, head=None, rows=None,
prologue=None, epilogue=None, encoding='utf-8'):
"""
Write the table to a xml file. E.g.::
>>> import petl as etl
>>> table1 = [['foo', 'bar'],
... ['a', 1],
... ['b', 2],
... ['c', 3]]
>>> etl.toxml(table1, 'example4.xml')
>>> # see what we did
... print(open('example4.xml').read())
<?xml version="1.0" encoding="UTF-8"?>
<table><thead>
<tr><th>foo</th><th>bar</th></tr>
</thead><tbody>
<tr><td>a</td><td>1</td></tr>
<tr><td>b</td><td>2</td></tr>
<tr><td>c</td><td>3</td></tr>
</tbody></table>
>>> etl.toxml(table1, 'example5.xml', rows='plan/line/cell')
>>> print(open('example5.xml').read())
<?xml version="1.0" encoding="UTF-8"?>
<plan>
<line><cell>a</cell><cell>1</cell></line>
<line><cell>b</cell><cell>2</cell></line>
<line><cell>c</cell><cell>3</cell></line>
</plan>
The `toxml()` function is just a wrapper over :func:`petl.io.text.totext`.
For advanced casees use a template with `totext()`.
"""
if not root and not head and not rows:
root = 'table'
head = 'thead/tr/th'
rows = 'tbody/tr/td'

sample, table2 = iterpeek(table, 2)
props = fieldnames(sample)

top = _build_xml_header(props, root, head, rows, prologue, encoding)
template = _build_cols(props, rows, '{%s}')
bottom = _build_xml_footer(epilogue, rows, root)

totext(table2, source=filename, encoding=encoding, errors='strict',
template=template, prologue=top, epilogue=bottom)


def _build_xml_header(props, root, head, rows, prologue, encoding):
tab = _build_nesting(root, False, None) if root else ''
if head:
th1 = _build_nesting(head, False, -2)
col = _build_cols(props, head, '%s')
th2 = _build_nesting(head, True, -2)
thd = '%s\n%s%s' % (th1, col, th2)
else:
thd = ''
tbd = _build_nesting(rows, False, -2)
thb = '%s%s%s\n' % (tab, thd, tbd)
if prologue and prologue.startswith('<?xml'):
return prologue + thb
enc = encoding.upper() if encoding else 'UTF-8'
xml = '<?xml version="1.0" encoding="%s"?>' % enc
pre = prologue if prologue else ''
return '%s\n%s%s' % (xml, pre, thb)


def _build_xml_footer(epilogue, rows, root):
tbd = _build_nesting(rows, True, -2)
tab = _build_nesting(root, True, None)
return epilogue + tbd + tab if epilogue else tbd + tab


def _build_nesting(path, closing, index):
if not path:
return ''
fmt = '</%s>' if closing else '<%s>'
if '/' not in path:
return fmt % path
parts = path.split('/')
elements = parts[0:index] if index else parts
if closing:
elements.reverse()
tags = [fmt % e for e in elements]
return ''.join(tags)


def _build_cols(props, path, placeholder):
if '/' not in path:
raise ValueError("Argument must have at least 2 elements for row/col: %s" % path)
parts = path.split('/')
col = parts[-1]
fmt = '<%s>%s</%s>' % (col, placeholder, col)
tags = [fmt % e for e in props]
cols = ''.join(tags)
row = parts[-2:-1][0]
res = ' <%s>%s</%s>\n' % (row, cols, row)
return res
63 changes: 62 additions & 1 deletion petl/test/io/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from petl.test.helpers import ieq
from petl.util import nrows, look
from petl.io.xml import fromxml
from petl.io.xml import fromxml, toxml
from petl.compat import urlopen


Expand Down Expand Up @@ -305,3 +305,64 @@ def test_fromxml_entity():
pass
else:
assert True, 'Error testing XML'


def _check_toxml(table, expected, check=(), **kwargs):
with NamedTemporaryFile(delete=True, suffix='.xml') as f:
filename = f.name
toxml(table, filename, **kwargs)
try:
actual = fromxml(filename, *check)
_compare(expected, actual)
except Exception as ex:
print('XML:', open(filename).read(), file=sys.stderr)
raise ex


_HEAD1 = (('foo', 'bar'),)
_BODY1 = (('a', '1'),
('b', '2'),
('c', '3'))
_TABLE1 = _HEAD1 + _BODY1


def test_toxml1():
_check_toxml(
_TABLE1, _TABLE1,
check=('.//tr', ('th', 'td'))
)


def test_toxml2():
_check_toxml(
_TABLE1, _BODY1,
check=('.//row', 'col'),
root='matrix',
rows='row/col'
)


def test_toxml3():
_check_toxml(
_TABLE1, _BODY1,
check=('line', 'cell'),
rows='plan/line/cell'
)


def test_toxml4():
_check_toxml(
_TABLE1, _BODY1,
check=('.//line', 'cell'),
rows='dir/file/book/plan/line/cell'
)


def test_toxml5():
_check_toxml(
_TABLE1, _TABLE1,
check=('.//x', 'y'),
root='a',
head='h/k/x/y',
rows='r/v/x/y'
)
4 changes: 4 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ deps =
-rtest_requirements.txt
-roptional_requirements.txt

[testenv:{py36,py37,py38,py39}-doctest]
commands =
py36,py37,py38,py39: nosetests -v --with-doctest --doctest-options=+NORMALIZE_WHITESPACE petl -I"csv_py2\.py" -I"db\.py"

[testenv:{py37,py38,py39}-docs]
# build documentation under similar environment to readthedocs
changedir = docs
Expand Down

0 comments on commit 5a0a6de

Please sign in to comment.