Page MenuHomePhabricator

element.py
No OneTemporary

element.py

"""Element handling as described in RFC 4825.
This module implements
* location of an element in xml document
* location of insertion point for a new element in xml document
This allows to implement GET/PUT/DELETE for elements in XCAP server.
Syntax for element selectors is a subset of XPATH, but an XPATH implementation
was not used. One reason is that XPATH only implements locating an element but not
an insertion point for an element selector which does not point to an existing
element (but will point to the inserted element after PUT).
For element selectors of type *[@att="value"] insertion point depends on
the content of a new element. For RFC compliant behavior, fix such requests
by replacing '*' with the root tag of the new element.
"""
import sys
from StringIO import StringIO
from application import log
from xcap import uri
def make_parser():
parser = sax.make_parser()
parser.setFeature(sax.handler.feature_namespaces, 1)
parser.setFeature(sax.handler.feature_namespace_prefixes, 1)
return parser
try:
# we need feature_namespace_prefixes that is implemented in _xmlplus's expat
# parser, but not in the stock xml package
from _xmlplus import sax
except ImportError:
# let's hope for a miracle
from xml import sax
try:
make_parser()
except sax._exceptions.SAXNotSupportedException:
# no miracle today, complain about the original error
log.fatal("Package _xmlplus was not found on your system. Please install pyxml library")
# comment the following line out if you don't need element operations
sys.exit(1)
class ThrowEventsAway(sax.ContentHandler):
pass
def check_xml_fragment(element_str):
"""Run SAX parser on element_str to check its well-formedness.
Ignore unbound namespaces prefixes.
>>> check_xml_fragment("<tag></tag>")
>>> check_xml_fragment('''<entry uri="sip:xxx@yyyyy.net">
... <gm:group>Test</gm:group>
... </entry>''')
>>> check_xml_fragment("<tag></bag>")
Traceback (most recent call last):
...
SAXParseException: <unknown>:1:7: mismatched tag
>>> check_xml_fragment("<ta g></ta g>")
Traceback (most recent call last):
...
SAXParseException: <unknown>:1:5: not well-formed (invalid token)
>>> check_xml_fragment("<tag1/><tag2/>")
Traceback (most recent call last):
...
SAXParseException: <unknown>:1:7: junk after document element
>>> check_xml_fragment("<tag\\></tag\\>")
Traceback (most recent call last):
...
SAXParseException: <unknown>:1:4: not well-formed (invalid token)
"""
parser = sax.make_parser()
# ignore namespaces and prefixes
parser.setFeature(sax.handler.feature_namespaces, 0)
parser.setFeature(sax.handler.feature_namespace_prefixes, 0)
parser.setContentHandler(ThrowEventsAway())
parser.parse(StringIO(element_str))
class Step:
# to be matched against uri.Step
def __init__(self, name, position = 0):
self.name = name
# this integer holds index of a child element currently in processing
self.position = position
def __repr__(self):
return '%s[pos=%s]' % (self.name, self.position)
class ContentHandlerBase(sax.ContentHandler):
def __init__(self, selector):
sax.ContentHandler.__init__(self)
self.selector = selector
self.state = None
self.locator = None
def setDocumentLocator(self, locator):
self.locator = locator
def pos(self):
return self.locator._ref._parser.CurrentByteIndex
def set_state(self, new_state):
#print new_state, 'at %s' % str(self.pos())
self.state = new_state
def set_end_pos(self, end_pos, end_tag = None, end_pos_2 = None):
self.end_pos = end_pos
self.end_tag = end_tag
self.end_pos_2 = end_pos_2
def fix_end_pos(self, document):
if self.end_tag is not None and self.end_tag in document[self.end_pos:self.end_pos_2]:
if self.end_pos_2 is None:
self.end_pos = 1 + document.index('>', self.end_pos)
else:
self.end_pos = 1 + document.index('>', self.end_pos, self.end_pos_2)
def __repr__(self):
return '<%s selector=%r state=%r>' % (self.__class__.__name__, self.selector, self.state)
class ElementLocator(ContentHandlerBase):
"""Locates element in a document by element selector expression (subset
of XPATH defined in RFC 4825)
There's an intentional difference from XPATH (at least as implemented
in lxml): tail following the closing tag is not included in the end result
(this doesn't make sense for XCAP and incompatible with some of the
requirements in RFC).
"""
def startDocument(self):
if self.locator is None:
raise RuntimeError("The parser doesn't support locators")
self.path = []
self.state = 'LOOKING'
self.curstep = 0
self.skiplevel = 0
self.set_end_pos(None, None, None)
def startElementNS(self, name, qname, attrs):
#print '-' * (len(self.path) + self.skiplevel), '<', name, '/' + '/'.join(map(str, self.path))
if self.state=='DONE' and self.end_pos_2 is None:
self.end_pos_2 = self.pos()
if self.skiplevel>0:
self.skiplevel += 1
return
if self.curstep>=len(self.selector):
self.skiplevel = 1
return
if self.path:
parent = self.path[-1]
else:
parent = None
curstep = self.selector[self.curstep]
#print `name`, `curstep.name`
if curstep.name == '*' or curstep.name == name:
if parent:
parent.position += 1
else:
self.skiplevel = 1
return
if parent is None:
if curstep.position not in [None, 1]:
self.skiplevel = 1
return
else:
if curstep.position is not None and curstep.position != parent.position:
self.skiplevel = 1
return
if curstep.att_name is not None and attrs.get(curstep.att_name)!=curstep.att_value:
self.skiplevel = 1
return
#print '%r matched' % curstep
self.curstep += 1
self.path.append(Step(qname))
if len(self.path)==len(self.selector):
if self.state=='LOOKING':
self.set_state('FOUND')
self.start_pos = self.pos()
elif self.state=='DONE':
self.set_state('MANY')
def endElementNS(self, name, qname):
#print '-' * (len(self.path) + self.skiplevel-1), '>', name, '/' + '/'.join(map(str, self.path))
if self.state=='DONE' and self.end_pos_2 is None:
self.end_pos_2 = self.pos()
if self.skiplevel>0:
self.skiplevel -= 1
return
if len(self.path)==len(self.selector) and self.state=='FOUND':
self.set_state('DONE')
# QQQ why qname passed to endElementNS is None?
qname = self.path[-1].name
self.set_end_pos(self.pos(), '</' + qname + '>')
# where does pos() point to? two cases:
# 1. <name>....*HERE*</name>
# 2. <name/>*HERE*...
# If it's the first case we need to adjust pos() by len('</name>')
# To determine the case, let's mark the position of the next startElement/endElement
# and see if there '</name>' substring right after end_pos limited by end_pos_2
# 1. <name>....*end_pos*</name>...*end_pos_2*<...
# 2. <name/>*end_pos*...*end_pos_2*<...
element = self.path.pop()
self.curstep -= 1
class InsertPointLocator(ContentHandlerBase):
"""Locate the insertion point -- where in the document a new element should be inserted.
It operates under assumption that the request didn't yield any matches
with ElementLocator (its state was 'LOOKING' after parsing).
Note, that this class doesn't know what will be inserted and therefore
may do not do what you want with requests like 'labels/*[att="new-att"]'.
"""
def startDocument(self):
if self.locator is None:
raise RuntimeError("The parser doesn't support locators")
self.path = []
self.state = 'LOOKING'
self.curstep = 0
self.skiplevel = 0
self.set_end_pos(None, None, None)
def startElementNS(self, name, qname, attrs):
#print '<' * (1+len(self.path) + self.skiplevel), name, '/' + '/'.join(map(str, self.path)),
#print self.curstep, self.skiplevel
if self.state=='DONE' and self.end_pos_2 is None:
self.end_pos_2 = self.pos()
if self.skiplevel>0:
self.skiplevel += 1
return
if self.curstep>=len(self.selector):
self.skiplevel = 1
return
if self.path:
parent = self.path[-1]
else:
parent = None
curstep = self.selector[self.curstep]
if curstep.name == '*' or curstep.name == name:
if parent:
parent.position += 1
else:
self.skiplevel = 1
return
is_last_step = len(self.path)+1 == len(self.selector)
if not is_last_step:
if curstep.position is not None and curstep.position != parent.position:
self.skiplevel = 1
return
if curstep.att_name is not None and \
attrs.get(curstep.att_name)!=curstep.att_value:
self.skiplevel = 1
return
else:
if curstep.position == 1 and parent.position == 1:
self.set_state('DONE')
self.set_end_pos(self.pos(), end_pos_2=self.pos())
self.curstep += 1
self.path.append(Step(qname))
def endElementNS(self, name, qname):
#print '>' * (1+len(self.path)+self.skiplevel-1), name, '/' + '/'.join(map(str, self.path)),
#print self.curstep, self.skiplevel
if self.state=='DONE' and self.end_pos_2 is None:
self.end_pos_2 = self.pos()
if self.skiplevel>0:
self.skiplevel -= 1
return
qname = self.path[-1].name
curstep = self.selector[-1]
if len(self.path)==len(self.selector):
parent = self.path[-2]
if curstep.position is None:
if self.state=='DONE':
self.set_state('MANY')
else:
self.set_state('CLOSED')
self.set_end_pos(self.pos(), '</' + qname + '>')
elif curstep.position-1 == parent.position:
if self.state=='DONE':
self.set_state('MANY')
else:
self.set_state('DONE')
self.set_end_pos(self.pos(), '</' + qname + '>')
elif len(self.path)+1==len(self.selector):
if self.state == 'CLOSED':
self.set_state('DONE')
if curstep.name=='*' and curstep.position is None:
self.set_end_pos(self.pos(), end_pos_2 = self.pos())
elif self.state == 'LOOKING':
self.set_state('DONE')
self.set_end_pos(self.pos(), end_pos_2 = self.pos())
element = self.path.pop()
self.curstep -= 1
class LocatorError(ValueError):
def __init__(self, msg, handler=None):
ValueError.__init__(self, msg)
self.handler = handler
@staticmethod
def generate_error(locator, element_selector):
if locator.state == 'LOOKING':
return None
elif locator.state == 'MANY':
raise SelectorError(element_selector._original_string, locator)
else:
raise LocatorError('Internal error in %s' % locator.__class__.__name__, locator)
class SelectorError(LocatorError):
http_error = 404
def __init__(self, selector, handler=None):
msg = 'The requested node selector %s matches more than one element' % selector
LocatorError.__init__(self, msg, handler)
def find(document, element_selector):
"""Return an element as (first index, last index+1)
If it couldn't be found, return None.
If there're several matches, raise SelectorError.
"""
parser = make_parser()
el = ElementLocator(element_selector)
parser.setContentHandler(el)
parser.parse(StringIO(document))
if el.state == 'DONE':
el.fix_end_pos(document)
return (el.start_pos, el.end_pos)
else:
return LocatorError.generate_error(el, element_selector)
def get(document, element_selector):
"""Return an element as a string.
If it couldn't be found, return None.
If there're several matches, raise SelectorError.
"""
location = find(document, element_selector)
if location is not None:
start, end = location
return document[start:end]
def delete(document, element_selector):
"""Return document with element deleted.
If it couldn't be found, return None.
If there're several matches, raise SelectorError.
"""
location = find(document, element_selector)
if location is not None:
start, end = location
return document[:start] + document[end:]
def put(document, element_selector, element_str):
"""Return a 2-items tuple: (new_document, created).
new_document is a copy of document with element_str inside.
created is True if insertion was performed as opposed to replacement.
If element_selector matches an existing element, it is replaced with element_str.
If not, it is inserted at appropriate place.
If it's impossible to insert at this location, return None.
If element_selector matches more than one element or more than one possible
place to insert and there're no rule to resolve the ambiguity then SelectorError
is raised.
"""
location = find(document, element_selector)
if location is None:
ipl = InsertPointLocator(element_selector)
parser = make_parser()
parser.setContentHandler(ipl)
parser.parse(StringIO(document))
if ipl.state == 'DONE':
ipl.fix_end_pos(document)
start, end = ipl.end_pos, ipl.end_pos
created = True
else:
return LocatorError.generate_error(ipl, element_selector)
else:
start, end = location
created = False
return (document[:start] + element_str + document[end:], created)
# Q: why create a new parser for every parsing?
# A: when sax.make_parser() was called once, I've occasionaly encountered an exception like this:
#
# File "/usr/lib/python2.5/site-packages/xcap/appusage/__init__.py", line 178, in _cb_get_element
# result = XCAPElement.get(response.data, uri.node_selector.element_selector)
# File "/usr/lib/python2.5/site-packages/xcap/element.py", line 323, in get
# location = cls.find(document, element_selector)
# File "/usr/lib/python2.5/site-packages/xcap/element.py", line 308, in find
# cls.parser.setContentHandler(el)
# File "/usr/lib/python2.5/site-packages/_xmlplus/sax/expatreader.py", line 128, in setContentHandler
# self._reset_cont_handler()
# File "/usr/lib/python2.5/site-packages/_xmlplus/sax/expatreader.py", line 234, in _reset_cont_handler
# self._cont_handler.processingInstruction
# exceptions.AttributeError: 'NoneType' object has no attribute 'ProcessingInstructionHandler'
#
# I have no idea what does that mean, but probably something to do with parser's state becoming invalid
# under some circumstances.
class _test:
source1 = """<?xml version="1.0" encoding="iso-8859-1"?>
<labels>
<label added="2003-06-20">
<quote>
<emph>Midwinter Spring</emph> is its own season&#8230;
</quote>
<name>Thomas Eliot</name>
<address>
<street>3 Prufrock Lane</street>
<city>Stamford</city>
<state>CT</state>
</address>
</label>
<comment>hello</comment>
<first>
<second>hi!</second>
</first>
<label added="2003-06-10">
<name>Ezra Pound</name>
<address>
<street>45 Usura Place</street>
<city>Hailey</city>
<state>ID</state>
</address>
</label>
<label added="yesterday"/>
<label added="&quot;quoted&quot;"/>
<comment>world</comment>
</labels>
"""
source2 = """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/>
</root>"""
rls_services_xml = """<?xml version="1.0" encoding="UTF-8"?>
<rls-services xmlns="urn:ietf:params:xml:ns:rls-services"
xmlns:rl="urn:ietf:params:xml:ns:resource-lists"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<service uri="sip:mybuddies@example.com">
<resource-list>http://xcap.example.com/resource-lists/users/sip:joe@example.com/index/~~/resource-lists/list%5b@name=%22l1%22%5d</resource-list>
<packages>
<package>presence</package>
</packages>
</service>
<service uri="sip:marketing@example.com">
<list name="marketing">
<rl:entry uri="sip:joe@example.com"/>
<rl:entry uri="sip:sudhir@example.com"/>
</list>
<packages>
<package>presence</package>
</packages>
</service>
</rls-services>"""
@staticmethod
def trim(s0):
"remove tail from the result"
s = s0
while s and s[-1]!='>':
s = s[:-1]
if s:
return s
else:
return s0
@classmethod
def lxml_xpath_get(cls, xpath_expr, source=source1, namespace=None, namespaces={}):
"First, use xpath from lxml, which should produce the same results for existing nodes"
assert '/'.startswith(xpath_expr[:1]), xpath_expr
doc = etree.parse(StringIO(source))
try:
# where to put namespace?
r = doc.xpath(xpath_expr, namespaces=namespaces)
except etree.XPathEvalError:
return uri.NodeParsingError
except Exception, ex:
traceback.print_exc()
return ex
if len(r)==1:
return cls.trim(etree.tostring(r[0]))
elif len(r)>1:
return SelectorError
@staticmethod
def xcap_get(xpath_expr, source=source1, namespace=None, namespaces={}):
"Second, use xpath_get_element"
try:
selector = uri.parse_node_selector(xpath_expr, namespace, namespaces)[0]
return get(source, selector)
except (uri.NodeParsingError, SelectorError), ex :
return ex.__class__
except Exception, ex:
traceback.print_exc()
return ex
@staticmethod
def xcap_put(xpath_expr, element, source=source1, namespace=None, namespaces={}):
try:
selector = uri.parse_node_selector(xpath_expr, namespace, namespaces)[0]
return put(source, selector, element)[0]
except (uri.NodeParsingError, SelectorError), ex :
return ex.__class__
except Exception, ex:
traceback.print_exc()
return ex
@classmethod
def test_get(cls):
emph1 = '<emph>Midwinter Spring</emph>'
thomas = '<name>Thomas Eliot</name>'
ezra = '<name>Ezra Pound</name>'
hi = '<second>hi!</second>'
yesterday = '<label added="yesterday"/>'
for xpath_get in [cls.lxml_xpath_get, cls.xcap_get]:
#print '\n' + xpath_get.__doc__
def check(expected, argument, **kwargs):
result = xpath_get(argument, **kwargs)
if expected != result:
print 'EXPR: %s(%r)\nEXPECT: %r\nRESULT: %r\n' % \
(xpath_get.__name__, argument, expected, result)
#else:
#print '%s(%r)..ok!' % (xpath_get.__name__, argument)
# simple expr
check(hi, '/labels/first/second')
# error, ambiguity - there're two labels
check(SelectorError, '/labels/label')
# no ambiguity, only one label has <quote>
check(emph1, '/labels/label/quote/emph')
check(None, '/labels/labelx')
# there're minor differences between lxml/xpath and this module:
if xpath_get == cls.lxml_xpath_get:
check(uri.NodeParsingError, '/labels\label')
check(None, '/')
expected1 = '<el1/>'
expected2 = None
else:
check(None, '/labels\label')
check(uri.NodeParsingError, '/')
expected1 = '<el1></el1>'
expected2 = '<rl:entry uri="sip:joe@example.com"/>'
check(expected1, '/el1/el1', source='<?xml version="1.0"?><el1><el1></el1></el1>')
# lxml doesn't allow to have default namespace in the XPATH
check(expected2,
'/rls-services/service[2]/list/rl:entry[1]',
source=cls.rls_services_xml,
namespace="urn:ietf:params:xml:ns:rls-services",
namespaces={'rl': 'urn:ietf:params:xml:ns:resource-lists'})
check(emph1, '/labels/*[1]/quote/emph')
check(emph1, '/labels/label[1]/quote/emph')
check(thomas, '/labels/label[1]/name')
check(ezra, '/labels/label[@added="2003-06-10"]/name')
check(ezra, '/labels/label[2][@added="2003-06-10"]/name')
check(ezra, '/labels/label[2]/name')
check(ezra, '/labels/*[4]/name')
check(ezra, '/labels/*[4][@added="2003-06-10"]/name')
check(uri.NodeParsingError, '')
labels = cls.source1.split('\n', 1)[1].rstrip('\n')
check(labels, '/labels')
check(None, '/labels[0]')
check(labels, '/labels[1]')
check(None, '/labels[2]')
check(labels, '/*')
check(None, '/*[0]')
check(None, '/*[2]')
check(labels, '/*[1]')
check(yesterday, '/labels/label[@added="yesterday"]')
check(yesterday, '/labels/label[3]')
check(yesterday, '/labels/label[3][@added="yesterday"]')
check(yesterday, '/labels/*[@added="yesterday"]')
check(yesterday, '/labels/*[5]')
check(yesterday, '/labels/*[5][@added="yesterday"]')
check(SelectorError, '/labels/*')
check(None, '/labels/first/second/*')
check('<labels/>', '/labels', source='<labels/>')
check('<el3 att="first"/>', '/root/el3', source="""<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/>
<el3 att="first"/></root>""")
check('<el1/>', '/el1/el1', source='<?xml version="1.0"?><el1><el1/></el1>')
check('<el1 att="second"/>', '/root/el1[2]', source=cls.source2)
# what is the problem with this?
# check('<label added="&quot;quoted&quot;"/>',
# '''/labels/label[@added='"quoted"']''',
# source=cls.source1)
@classmethod
def xcap_get2(cls, expr, source=source2):
"get node using SourceElement.get_element, but check the result with lxml_xpath_get"
retrieved = cls.xcap_get(expr, source=source)
retrieved2 = cls.lxml_xpath_get(expr, source=source)
if retrieved2 != retrieved:
print 'xcap_get and lxml_xcap_get results differ! %r' % expr
print 'xcap_get: %s' % retrieved
print 'lxml_xpath_get: %s' % retrieved2
return retrieved
# if true, ignore the value to put and put '*' instead, so it can be easily spotted by human
simplify_check = False
@classmethod
def check(cls, insert_pos, what, expected, source=source2, **kwargs):
if cls.simplify_check:
if isinstance(expected, basestring):
expected = expected.replace(what, '*')
what = '*'
result = cls.xcap_put(insert_pos, what, source=source, **kwargs)
if callable(expected):
result_check = expected
else:
result_check = lambda s: s == expected
if not result_check(result):
print 'insert_pos: %s' % insert_pos
print 'result: %s' % result
print 'expected: %s' % expected
return
if not cls.simplify_check:
retrieved = cls.xcap_get2(insert_pos, result)
if retrieved != what.strip():
print 'GET(PUT(x))!=x'
print 'PUT: %r' % what
print 'GOT: %r' % retrieved
@classmethod
def test_put0(cls):
pos = '/rls-services/service[2]/list/rl:entry[1]'
what = '<rl:entry uri="sip:first@example.com"/>'
namespace="urn:ietf:params:xml:ns:rls-services"
namespaces={'rl': 'urn:ietf:params:xml:ns:resource-lists'}
result = cls.xcap_put(pos,
what,
source=cls.rls_services_xml,
namespace=namespace,
namespaces=namespaces)
retrieved = cls.xcap_get(pos, result, namespace, namespaces)
if retrieved != what.strip():
print 'GET(PUT(x))!=x'
print 'PUT: %r' % what
print 'GOT: %r' % retrieved
@classmethod
def test_put1(cls):
"now something xpath alone cannot do: locate position for insertion of a new node"
check = cls.check
check('/labels/label[@added="2008-08-21"]',
'<label added="2008-08-21"/>',
lambda x: x and '<label added="2008-08-21"/>' in x, source=cls.source1)
for selector in ['/root/el1[@att="third"]',
'/root/el1[3][@att="third"]',
'/root/*[3][@att="third"]']:
check(selector, '<el1 att="third"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/><el1 att="third"/>
<!-- comment -->
<el2 att="first"/>
</root>""")
check('/root/el3', '<el3 att="first"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/>
<el3 att="first"/></root>""")
for selector in ['/root/el2[@att="2"]',
'/root/el2[2][@att="2"]']:
check(selector, '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/><el2 att="2"/>
</root>""")
check('/root/*[2][@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/><el2 att="2"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/>
</root>""")
check('/root/el2[1][@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="2"/><el2 att="first"/>
</root>""")
# Not RFC-compliant, because xcap_put_element doesn't look inside
# new element. To make it RFC-compliant, fix the selector, replacing
# '*' with 'el2'
check('/root/*[@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="first"/>
<el2 att="2"/></root>""")
# spaces can be inserted along with element (but not comments)
check('/root/el2[1][@att="2"]', ' <el2 att="2"/> ', '''<?xml version="1.0"?>
<root>
<el1 att="first"/>
<el1 att="second"/>
<!-- comment -->
<el2 att="2"/> <el2 att="first"/>
</root>''')
source3 = """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="first"></el2>
</root>"""
@classmethod
def test_put2(cls):
"the same as before, with <tag/> replaced by <tag></tag>"
def check(insert_pos, what, expected, source=cls.source3):
return cls.check(insert_pos, what, expected, source)
for selector in ['/root/el1[@att="third"]',
'/root/el1[3][@att="third"]',
'/root/*[3][@att="third"]']:
check(selector, '<el1 att="third"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1><el1 att="third"/>
<!-- comment -->
<el2 att="first"></el2>
</root>""")
check('/root/el3', '<el3 att="first"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="first"></el2>
<el3 att="first"/></root>""")
for selector in ['/root/el2[@att="2"]',
'/root/el2[2][@att="2"]']:
check(selector, '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="first"></el2><el2 att="2"/>
</root>""")
check('/root/*[2][@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1><el2 att="2"/>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="first"></el2>
</root>""")
check('/root/el2[1][@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="2"/><el2 att="first"></el2>
</root>""")
# NOTE: depending on what `star' is, 'el1', 'el2' or something else it should
# be put in a different position (right after the latest <el1>, right after <el2>,
# or before </root> correspondingly)
# Such element selectors therefore, should be fixed: replace star with the actual
# element name (take it from the body of XCAP request).
# Beware though, that if you do that, you still must guarantee that [@att="2"]
# doesn't match any of the existing elements, no matter what their names are.
# This suggests 2-pass procedure for PUT:
# First try to match element using the original element selector.
# If it did match, run replacement procedure.
# If it didn't match, fix the element selector and try to locate insertion point.
# This will guarantee, that when if client uses non-fixed request next time,
# it will match exactly once
check('/root/*[@att="2"]', '<el2 att="2"/>', """<?xml version="1.0"?>
<root>
<el1 att="first"></el1>
<el1 att="second"></el1>
<!-- comment -->
<el2 att="first"></el2>
<el2 att="2"/></root>""")
if __name__ == "__main__":
import doctest
doctest.testmod()
from lxml import etree
import traceback
_test.test_get()
_test.test_put0()
_test.test_put1()
_test.test_put2()

File Metadata

Mime Type
text/plain
Expires
Sat, Nov 23, 10:08 AM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3409085
Default Alt Text
element.py (30 KB)

Event Timeline