Updated all of the matcher classes in sleekxmpp.xmlstream.matcher.

Matchers are now PEP8 compliant and have documentation.
This commit is contained in:
Lance Stout 2010-09-01 14:28:43 -04:00
parent 576eefb097
commit 5c3066ba30
6 changed files with 321 additions and 102 deletions

View File

@ -5,10 +5,30 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
class MatcherBase(object): class MatcherBase(object):
def __init__(self, criteria): """
self._criteria = criteria Base class for stanza matchers. Stanza matchers are used to pick
stanzas out of the XML stream and pass them to the appropriate
def match(self, xml): stream handlers.
return False """
def __init__(self, criteria):
"""
Create a new stanza matcher.
Arguments:
criteria -- Object to compare some aspect of a stanza
against.
"""
self._criteria = criteria
def match(self, xml):
"""
Check if a stanza matches the stored criteria.
Meant to be overridden.
"""
return False

View File

@ -5,9 +5,28 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
class MatcherId(base.MatcherBase): from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, xml):
return xml['id'] == self._criteria class MatcherId(MatcherBase):
"""
The ID matcher selects stanzas that have the same stanza 'id'
interface value as the desired ID.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Compare the given stanza's 'id' attribute to the stored
id value.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza to compare against.
"""
return xml['id'] == self._criteria

View File

@ -5,13 +5,36 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
from xml.etree import cElementTree
class MatchMany(base.MatcherBase): from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, xml):
for m in self._criteria: class MatchMany(MatcherBase):
if m.match(xml):
return True """
return False The MatchMany matcher may compare a stanza against multiple
criteria. It is essentially an OR relation combining multiple
matchers.
Each of the criteria must implement a match() method.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Match a stanza against multiple criteria. The match is successful
if one of the criteria matches.
Each of the criteria must implement a match() method.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza object to compare against.
"""
for m in self._criteria:
if m.match(xml):
return True
return False

View File

@ -5,10 +5,34 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
from xml.etree import cElementTree
class StanzaPath(base.MatcherBase): from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, stanza):
return stanza.match(self._criteria) class StanzaPath(MatcherBase):
"""
The StanzaPath matcher selects stanzas that match a given "stanza path",
which is similar to a normal XPath except that it uses the interfaces and
plugins of the stanza instead of the actual, underlying XML.
In most cases, the stanza path and XPath should be identical, but be
aware that differences may occur.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, stanza):
"""
Compare a stanza against a "stanza path". A stanza path is similar to
an XPath expression, but uses the stanza's interfaces and plugins
instead of the underlying XML. For most cases, the stanza path and
XPath should be identical, but be aware that differences may occur.
Overrides MatcherBase.match.
Arguments:
stanza -- The stanza object to compare against.
"""
return stanza.match(self._criteria)

View File

@ -5,63 +5,151 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
from xml.etree import cElementTree
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
ignore_ns = False from sleekxmpp.xmlstream.stanzabase import ET
from sleekxmpp.xmlstream.matcher.base import MatcherBase
class MatchXMLMask(base.MatcherBase):
def __init__(self, criteria): # Flag indicating if the builtin XPath matcher should be used, which
base.MatcherBase.__init__(self, criteria) # uses namespaces, or a custom matcher that ignores namespaces.
if type(criteria) == type(''): # Changing this will affect ALL XMLMask matchers.
self._criteria = cElementTree.fromstring(self._criteria) IGNORE_NS = False
self.default_ns = 'jabber:client'
def setDefaultNS(self, ns):
self.default_ns = ns
def match(self, xml):
if hasattr(xml, 'xml'): class MatchXMLMask(MatcherBase):
xml = xml.xml
return self.maskcmp(xml, self._criteria, True) """
The XMLMask matcher selects stanzas whose XML matches a given
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'): XML pattern, or mask. For example, message stanzas with body elements
"""maskcmp(xmlobj, maskobj): could be matched using the mask:
Compare etree xml object to etree xml object mask"""
use_ns = not ignore_ns <message xmlns="jabber:client"><body /></message>
#TODO require namespaces
if source == None: #if element not found (happens during recursive check below) Use of XMLMask is discouraged, and XPath or StanzaPath should be used
return False instead.
if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
try: The use of namespaces in the mask comparison is controlled by
maskobj = cElementTree.fromstring(maskobj) IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
except ExpatError: for ALL XMLMask matchers.
logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare Methods:
return False match -- Overrides MatcherBase.match.
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ): setDefaultNS -- Set the default namespace for the mask.
return False """
if maskobj.text and source.text != maskobj.text:
return False def __init__(self, criteria):
for attr_name in maskobj.attrib: #compare attributes """
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]: Create a new XMLMask matcher.
return False
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements Arguments:
for subelement in maskobj: #recursively compare subelements criteria -- Either an XML object or XML string to use as a mask.
if use_ns: """
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns): MatcherBase.__init__(self, criteria)
return False if isinstance(criteria, str):
else: self._criteria = ET.fromstring(self._criteria)
if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns): self.default_ns = 'jabber:client'
return False
return True def setDefaultNS(self, ns):
"""
def getChildIgnoreNS(self, xml, tag): Set the default namespace to use during comparisons.
tag = tag.split('}')[-1]
try: Arguments:
idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag) ns -- The new namespace to use as the default.
except ValueError: """
return None self.default_ns = ns
return xml.getchildren()[idx]
def match(self, xml):
"""
Compare a stanza object or XML object against the stored XML mask.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza object or XML object to compare against.
"""
if hasattr(xml, 'xml'):
xml = xml.xml
return self._mask_cmp(xml, self._criteria, True)
def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
"""
Compare an XML object against an XML mask.
Arguments:
source -- The XML object to compare against the mask.
mask -- The XML object serving as the mask.
use_ns -- Indicates if namespaces should be respected during
the comparison.
default_ns -- The default namespace to apply to elements that
do not have a specified namespace.
Defaults to "__no_ns__".
"""
use_ns = not IGNORE_NS
if source is None:
# If the element was not found. May happend during recursive calls.
return False
# Convert the mask to an XML object if it is a string.
if not hasattr(mask, 'attrib'):
try:
mask = ET.fromstring(mask)
except ExpatError:
logging.log(logging.WARNING,
"Expat error: %s\nIn parsing: %s" % ('', mask))
if not use_ns:
# Compare the element without using namespaces.
source_tag = source.tag.split('}', 1)[-1]
mask_tag = mask.tag.split('}', 1)[-1]
if source_tag != mask_tag:
return False
else:
# Compare the element using namespaces
mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
if source.tag not in [mask.tag, mask_ns_tag]:
return False
# If the mask includes text, compare it.
if mask.text and source.text != mask.text:
return False
# Compare attributes. The stanza must include the attributes
# defined by the mask, but may include others.
for name, value in mask.attrib.items():
if source.attrib.get(name, "__None__") != value:
return False
# Recursively check subelements.
for subelement in mask:
if use_ns:
if not self._mask_cmp(source.find(subelement.tag),
subelement, use_ns):
return False
else:
if not self._mask_cmp(self._get_child(source, subelement.tag),
subelement, use_ns):
return False
# Everything matches.
return True
def _get_child(self, xml, tag):
"""
Return a child element given its tag, ignoring namespace values.
Returns None if the child was not found.
Arguments:
xml -- The XML object to search for the given child tag.
tag -- The name of the subelement to find.
"""
tag = tag.split('}')[-1]
try:
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
index = children.index(tag)
except ValueError:
return None
return xml.getchildren()[index]

View File

@ -5,30 +5,75 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
from xml.etree import cElementTree
ignore_ns = False from sleekxmpp.xmlstream.stanzabase import ET
from sleekxmpp.xmlstream.matcher.base import MatcherBase
class MatchXPath(base.MatcherBase):
def match(self, xml): # Flag indicating if the builtin XPath matcher should be used, which
if hasattr(xml, 'xml'): # uses namespaces, or a custom matcher that ignores namespaces.
xml = xml.xml # Changing this will affect ALL XPath matchers.
x = cElementTree.Element('x') IGNORE_NS = False
x.append(xml)
if not ignore_ns:
if x.find(self._criteria) is not None: class MatchXPath(MatcherBase):
return True
return False """
else: The XPath matcher selects stanzas whose XML contents matches a given
criteria = [c.split('}')[-1] for c in self._criteria.split('/')] XPath expression.
xml = x
for tag in criteria: Note that using this matcher may not produce expected behavior when using
children = [c.tag.split('}')[-1] for c in xml.getchildren()] attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
try: does not support the use of attribute selectors. If you need to support
idx = children.index(tag) Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
except ValueError:
return False If the value of IGNORE_NS is set to true, then XPath expressions will
xml = xml.getchildren()[idx] be matched without using namespaces.
return True
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Compare a stanza's XML contents to an XPath expression.
If the value of IGNORE_NS is set to true, then XPath expressions
will be matched without using namespaces.
Note that in Python 2.6 and 3.1 the ElementTree find method does
not support attribute selectors in the XPath expression.
Arguments:
xml -- The stanza object to compare against.
"""
if hasattr(xml, 'xml'):
xml = xml.xml
x = ET.Element('x')
x.append(xml)
if not IGNORE_NS:
# Use builtin, namespace respecting, XPath matcher.
if x.find(self._criteria) is not None:
return True
return False
else:
# Remove namespaces from the XPath expression.
criteria = []
for ns_block in self._criteria.split('{'):
criteria.extend(ns_block.split('}')[-1].split('/'))
# Walk the XPath expression.
xml = x
for tag in criteria:
if not tag:
# Skip empty tag name artifacts from the cleanup phase.
continue
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
try:
index = children.index(tag)
except ValueError:
return False
xml = xml.getchildren()[index]
return True