Add initial support for xml:lang for streams and stanza plugins.

Remaining items are suitable default actions for language supporting
interfaces.
This commit is contained in:
Lance Stout 2012-06-05 16:54:26 -07:00
parent ee702f4071
commit 181aea737d
10 changed files with 243 additions and 89 deletions

View file

@ -31,6 +31,7 @@ from sleekxmpp.xmlstream import XMLStream, JID
from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.stanzabase import XML_NS
from sleekxmpp.features import *
from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin
@ -180,6 +181,8 @@ class BaseXMPP(XMLStream):
:param xml: The incoming stream's root element.
"""
self.stream_id = xml.get('id', '')
self.stream_version = xml.get('version', '')
self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
def process(self, *args, **kwargs):
"""Initialize plugins and begin processing the XML stream.
@ -272,7 +275,9 @@ class BaseXMPP(XMLStream):
def Message(self, *args, **kwargs):
"""Create a Message stanza associated with this stream."""
return Message(self, *args, **kwargs)
msg = Message(self, *args, **kwargs)
msg['lang'] = self.default_lang
return msg
def Iq(self, *args, **kwargs):
"""Create an Iq stanza associated with this stream."""
@ -280,7 +285,9 @@ class BaseXMPP(XMLStream):
def Presence(self, *args, **kwargs):
"""Create a Presence stanza associated with this stream."""
return Presence(self, *args, **kwargs)
pres = Presence(self, *args, **kwargs)
pres['lang'] = self.default_lang
return pres
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
"""Create a new Iq stanza with a given Id and from JID.

View file

@ -60,8 +60,8 @@ class ClientXMPP(BaseXMPP):
:param escape_quotes: **Deprecated.**
"""
def __init__(self, jid, password, ssl=False, plugin_config={},
plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
escape_quotes=True, sasl_mech=None, lang='en'):
BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
@ -69,15 +69,18 @@ class ClientXMPP(BaseXMPP):
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.default_port = 5222
self.default_lang = lang
self.credentials = {}
self.password = password
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
"xmlns='%s'" % self.default_ns)
"xmlns='%s'" % self.default_ns,
"xml:lang='%s'" % self.default_lang,
"version='1.0'")
self.stream_footer = "</stream:stream>"
self.features = set()

View file

@ -42,6 +42,8 @@ class Addresses(ElementBase):
self.delAddresses(set_type)
for addr in addresses:
addr = dict(addr)
if 'lang' in addr:
del addr['lang']
# Remap 'type' to 'atype' to match the add method
if set_type is not None:
addr['type'] = set_type

View file

@ -79,5 +79,7 @@ class XEP_0092(BasePlugin):
result = iq.send()
if result and result['type'] != 'error':
return result['software_version'].values
values = result['software_version'].values
del values['lang']
return values
return False

View file

@ -102,6 +102,7 @@ class Roster(ElementBase):
# Remove extra JID reference to keep everything
# backward compatible
del items[item['jid']]['jid']
del items[item['jid']]['lang']
return items
def del_items(self):

View file

@ -333,6 +333,9 @@ class SleekTest(unittest.TestCase):
# Remove unique ID prefix to make it easier to test
self.xmpp._id_prefix = ''
self.xmpp._disconnect_wait_for_threads = False
self.xmpp.default_lang = None
self.xmpp.peer_default_lang = None
# We will use this to wait for the session_start event
# for live connections.
@ -386,6 +389,7 @@ class SleekTest(unittest.TestCase):
sid='',
stream_ns="http://etherx.jabber.org/streams",
default_ns="jabber:client",
default_lang="en",
version="1.0",
xml_header=True):
"""
@ -413,6 +417,8 @@ class SleekTest(unittest.TestCase):
parts.append('from="%s"' % sfrom)
if sid:
parts.append('id="%s"' % sid)
if default_lang:
parts.append('xml:lang="%s"' % default_lang)
parts.append('version="%s"' % version)
parts.append('xmlns:stream="%s"' % stream_ns)
parts.append('xmlns="%s"' % default_ns)
@ -564,6 +570,7 @@ class SleekTest(unittest.TestCase):
sid='',
stream_ns="http://etherx.jabber.org/streams",
default_ns="jabber:client",
default_lang="en",
version="1.0",
xml_header=False,
timeout=1):
@ -585,6 +592,7 @@ class SleekTest(unittest.TestCase):
header = self.make_header(sto, sfrom, sid,
stream_ns=stream_ns,
default_ns=default_ns,
default_lang=default_lang,
version=version,
xml_header=xml_header)
sent_header = self.xmpp.socket.next_sent(timeout)

View file

@ -12,6 +12,8 @@
:license: MIT, see LICENSE for more details
"""
from __future__ import with_statement, unicode_literals
import copy
import logging
import weakref
@ -29,6 +31,9 @@ log = logging.getLogger(__name__)
XML_TYPE = type(ET.Element('xml'))
XML_NS = 'http://www.w3.org/XML/1998/namespace'
def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
"""
Associate a stanza object as a plugin for another stanza.
@ -101,20 +106,26 @@ def multifactory(stanza, plugin_attrib):
def setup(self, xml=None):
self.xml = ET.Element('')
def get_multi(self):
def get_multi(self, lang=None):
parent = self.parent()
res = filter(lambda sub: isinstance(sub, self._multistanza), parent)
if lang is None:
res = filter(lambda sub: isinstance(sub, self._multistanza), parent)
else:
res = filter(lambda sub: isinstance(sub, self._multistanza) and sub['lang'] == lang, parent)
return list(res)
def set_multi(self, val):
def set_multi(self, val, lang=None):
parent = self.parent()
del parent[self.plugin_attrib]
for sub in val:
parent.append(sub)
def del_multi(self):
def del_multi(self, lang=None):
parent = self.parent()
res = filter(lambda sub: isinstance(sub, self._multistanza), parent)
if lang is None:
res = filter(lambda sub: isinstance(sub, self._multistanza), parent)
else:
res = filter(lambda sub: isinstance(sub, self._multistanza) and sub['lang'] == lang, parent)
for stanza in list(res):
parent.iterables.remove(stanza)
parent.xml.remove(stanza.xml)
@ -122,7 +133,8 @@ def multifactory(stanza, plugin_attrib):
Multi.is_extension = True
Multi.plugin_attrib = plugin_attrib
Multi._multistanza = stanza
Multi.interfaces = (plugin_attrib,)
Multi.interfaces = set([plugin_attrib])
Multi.lang_interfaces = set([plugin_attrib])
setattr(Multi, "get_%s" % plugin_attrib, get_multi)
setattr(Multi, "set_%s" % plugin_attrib, set_multi)
setattr(Multi, "del_%s" % plugin_attrib, del_multi)
@ -289,14 +301,17 @@ class ElementBase(object):
#: subelements of the underlying XML object. Using this set, the text
#: of these subelements may be set, retrieved, or removed without
#: needing to define custom methods.
sub_interfaces = tuple()
sub_interfaces = set()
#: A subset of :attr:`interfaces` which maps the presence of
#: subelements to boolean values. Using this set allows for quickly
#: checking for the existence of empty subelements like ``<required />``.
#:
#: .. versionadded:: 1.1
bool_interfaces = tuple()
bool_interfaces = set()
#: .. versionadded:: 1.1.2
lang_interfaces = set()
#: In some cases you may wish to override the behaviour of one of the
#: parent stanza's interfaces. The ``overrides`` list specifies the
@ -363,7 +378,7 @@ class ElementBase(object):
subitem = set()
#: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``.
xml_ns = 'http://www.w3.org/XML/1998/namespace'
xml_ns = XML_NS
def __init__(self, xml=None, parent=None):
self._index = 0
@ -375,6 +390,7 @@ class ElementBase(object):
#: An ordered dictionary of plugin stanzas, mapped by their
#: :attr:`plugin_attrib` value.
self.plugins = OrderedDict()
self.loaded_plugins = set()
#: A list of child stanzas whose class is included in
#: :attr:`plugin_iterables`.
@ -385,6 +401,12 @@ class ElementBase(object):
#: ``'{namespace}elementname'``.
self.tag = self.tag_name()
if 'lang' not in self.interfaces:
if isinstance(self.interfaces, tuple):
self.interfaces += ('lang',)
else:
self.interfaces.add('lang')
#: A :class:`weakref.weakref` to the parent stanza, if there is one.
#: If not, then :attr:`parent` is ``None``.
self.parent = None
@ -406,10 +428,9 @@ class ElementBase(object):
for child in self.xml.getchildren():
if child.tag in self.plugin_tag_map:
plugin_class = self.plugin_tag_map[child.tag]
plugin = plugin_class(child, self)
self.plugins[plugin.plugin_attrib] = plugin
if plugin_class in self.plugin_iterables:
self.iterables.append(plugin)
self.init_plugin(plugin_class.plugin_attrib,
existing_xml=child,
reuse=False)
def setup(self, xml=None):
"""Initialize the stanza's XML contents.
@ -443,7 +464,7 @@ class ElementBase(object):
# We did not generate XML
return False
def enable(self, attrib):
def enable(self, attrib, lang=None):
"""Enable and initialize a stanza plugin.
Alias for :meth:`init_plugin`.
@ -451,24 +472,60 @@ class ElementBase(object):
:param string attrib: The :attr:`plugin_attrib` value of the
plugin to enable.
"""
return self.init_plugin(attrib)
return self.init_plugin(attrib, lang)
def init_plugin(self, attrib):
def _get_plugin(self, name, lang=None):
if lang is None:
lang = self.get_lang()
plugin_class = self.plugin_attrib_map[name]
if plugin_class.is_extension:
if (name, None) in self.plugins:
return self.plugins[(name, None)]
else:
return self.init_plugin(name, lang)
else:
if (name, lang) in self.plugins:
return self.plugins[(name, lang)]
else:
return self.init_plugin(name, lang)
def init_plugin(self, attrib, lang=None, existing_xml=None, reuse=True):
"""Enable and initialize a stanza plugin.
:param string attrib: The :attr:`plugin_attrib` value of the
plugin to enable.
"""
if attrib not in self.plugins:
plugin_class = self.plugin_attrib_map[attrib]
if lang is None:
lang = self.get_lang()
plugin_class = self.plugin_attrib_map[attrib]
if plugin_class.is_extension and (attrib, None) in self.plugins:
return self.plugins[(attrib, None)]
if reuse and (attrib, lang) in self.plugins:
return self.plugins[(attrib, lang)]
if existing_xml is None:
existing_xml = self.xml.find(plugin_class.tag_name())
plugin = plugin_class(parent=self, xml=existing_xml)
self.plugins[attrib] = plugin
if plugin_class in self.plugin_iterables:
self.iterables.append(plugin)
if plugin_class.plugin_multi_attrib:
self.init_plugin(plugin_class.plugin_multi_attrib)
return self
if existing_xml is not None and existing_xml.attrib.get('{%s}lang' % XML_NS, '') != lang:
existing_xml = None
plugin = plugin_class(parent=self, xml=existing_xml)
if plugin.is_extension:
self.plugins[(attrib, None)] = plugin
else:
plugin['lang'] = lang
self.plugins[(attrib, lang)] = plugin
if plugin_class in self.plugin_iterables:
self.iterables.append(plugin)
if plugin_class.plugin_multi_attrib:
self.init_plugin(plugin_class.plugin_multi_attrib)
self.loaded_plugins.add(attrib)
return plugin
def _get_stanza_values(self):
"""Return A JSON/dictionary version of the XML content
@ -493,7 +550,11 @@ class ElementBase(object):
for interface in self.interfaces:
values[interface] = self[interface]
for plugin, stanza in self.plugins.items():
values[plugin] = stanza.values
lang = stanza['lang']
if lang:
values['%s|%s' % (plugin, lang)] = stanza.values
else:
values[plugin[0]] = stanza.values
if self.iterables:
iterables = []
for stanza in self.iterables:
@ -517,6 +578,11 @@ class ElementBase(object):
p in self.plugin_iterables]
for interface, value in values.items():
full_interface = interface
interface_lang = ('%s|' % interface).split('|')
interface = interface_lang[0]
lang = interface_lang[1] or self.get_lang()
if interface == 'substanzas':
# Remove existing substanzas
for stanza in self.iterables:
@ -538,9 +604,8 @@ class ElementBase(object):
self[interface] = value
elif interface in self.plugin_attrib_map:
if interface not in iterable_interfaces:
if interface not in self.plugins:
self.init_plugin(interface)
self.plugins[interface].values = value
plugin = self._get_plugin(interface, lang)
plugin.values = value
return self
def __getitem__(self, attrib):
@ -572,6 +637,15 @@ class ElementBase(object):
:param string attrib: The name of the requested stanza interface.
"""
full_attrib = attrib
attrib_lang = ('%s|' % attrib).split('|')
attrib = attrib_lang[0]
lang = attrib_lang[1] or ''
kwargs = {}
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
if attrib == 'substanzas':
return self.iterables
elif attrib in self.interfaces:
@ -579,18 +653,17 @@ class ElementBase(object):
get_method2 = "get%s" % attrib.title()
if self.plugin_overrides:
plugin = self.plugin_overrides.get(get_method, None)
if plugin:
if plugin not in self.plugins:
self.init_plugin(plugin)
handler = getattr(self.plugins[plugin], get_method, None)
name = self.plugin_overrides.get(get_method, None)
if name:
plugin = self._get_plugin(name, lang)
handler = getattr(plugin, get_method, None)
if handler:
return handler()
return handler(**kwargs)
if hasattr(self, get_method):
return getattr(self, get_method)()
return getattr(self, get_method)(**kwargs)
elif hasattr(self, get_method2):
return getattr(self, get_method2)()
return getattr(self, get_method2)(**kwargs)
else:
if attrib in self.sub_interfaces:
return self._get_sub_text(attrib)
@ -600,11 +673,10 @@ class ElementBase(object):
else:
return self._get_attr(attrib)
elif attrib in self.plugin_attrib_map:
if attrib not in self.plugins:
self.init_plugin(attrib)
if self.plugins[attrib].is_extension:
return self.plugins[attrib][attrib]
return self.plugins[attrib]
plugin = self._get_plugin(attrib, lang)
if plugin.is_extension:
return plugin[full_attrib]
return plugin
else:
return ''
@ -640,25 +712,32 @@ class ElementBase(object):
:param string attrib: The name of the stanza interface to modify.
:param value: The new value of the stanza interface.
"""
full_attrib = attrib
attrib_lang = ('%s|' % attrib).split('|')
attrib = attrib_lang[0]
lang = attrib_lang[1] or ''
kwargs = {}
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
if attrib in self.interfaces:
if value is not None:
set_method = "set_%s" % attrib.lower()
set_method2 = "set%s" % attrib.title()
if self.plugin_overrides:
plugin = self.plugin_overrides.get(set_method, None)
if plugin:
if plugin not in self.plugins:
self.init_plugin(plugin)
handler = getattr(self.plugins[plugin],
set_method, None)
name = self.plugin_overrides.get(set_method, None)
if name:
plugin = self._get_plugin(name, lang)
handler = getattr(plugin, set_method, None)
if handler:
return handler(value)
return handler(value, **kwargs)
if hasattr(self, set_method):
getattr(self, set_method)(value,)
getattr(self, set_method)(value, **kwargs)
elif hasattr(self, set_method2):
getattr(self, set_method2)(value,)
getattr(self, set_method2)(value, **kwargs)
else:
if attrib in self.sub_interfaces:
return self._set_sub_text(attrib, text=value)
@ -672,9 +751,8 @@ class ElementBase(object):
else:
self.__delitem__(attrib)
elif attrib in self.plugin_attrib_map:
if attrib not in self.plugins:
self.init_plugin(attrib)
self.plugins[attrib][attrib] = value
plugin = self._get_plugin(attrib, lang)
plugin[full_attrib] = value
return self
def __delitem__(self, attrib):
@ -709,23 +787,31 @@ class ElementBase(object):
:param attrib: The name of the affected stanza interface.
"""
full_attrib = attrib
attrib_lang = ('%s|' % attrib).split('|')
attrib = attrib_lang[0]
lang = attrib_lang[1] or ''
kwargs = {}
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
if attrib in self.interfaces:
del_method = "del_%s" % attrib.lower()
del_method2 = "del%s" % attrib.title()
if self.plugin_overrides:
plugin = self.plugin_overrides.get(del_method, None)
if plugin:
if plugin not in self.plugins:
self.init_plugin(plugin)
handler = getattr(self.plugins[plugin], del_method, None)
name = self.plugin_overrides.get(del_method, None)
if name:
plugin = self._get_plugin(attrib, lang)
handler = getattr(plugin, del_method, None)
if handler:
return handler()
return handler(**kwargs)
if hasattr(self, del_method):
getattr(self, del_method)()
getattr(self, del_method)(**kwargs)
elif hasattr(self, del_method2):
getattr(self, del_method2)()
getattr(self, del_method2)(**kwargs)
else:
if attrib in self.sub_interfaces:
return self._del_sub(attrib)
@ -734,15 +820,17 @@ class ElementBase(object):
else:
self._del_attr(attrib)
elif attrib in self.plugin_attrib_map:
if attrib in self.plugins:
xml = self.plugins[attrib].xml
if self.plugins[attrib].is_extension:
del self.plugins[attrib][attrib]
del self.plugins[attrib]
try:
self.xml.remove(xml)
except:
pass
plugin = self._get_plugin(attrib, lang)
if plugin.is_extension:
del plugin[full_attrib]
del self.plugins[(attrib, None)]
else:
del self.plugins[(attrib, lang)]
self.loaded_plugins.remove(attrib)
try:
self.xml.remove(plugin.xml)
except:
pass
return self
def _set_attr(self, name, value):
@ -903,7 +991,7 @@ class ElementBase(object):
attributes = components[1:]
if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
tag not in self.plugins and tag not in self.plugin_attrib:
tag not in self.loaded_plugins and tag not in self.plugin_attrib:
# The requested tag is not in this stanza, so no match.
return False
@ -932,10 +1020,11 @@ class ElementBase(object):
if not matched_substanzas and len(xpath) > 1:
# Convert {namespace}tag@attribs to just tag
next_tag = xpath[1].split('@')[0].split('}')[-1]
if next_tag in self.plugins:
return self.plugins[next_tag].match(xpath[1:])
else:
return False
langs = [name[1] for name in self.plugins if name[0] == next_tag]
for lang in langs:
if self._get_plugin(next_tag, lang).match(xpath[1:]):
return True
return False
# Everything matched.
return True
@ -995,7 +1084,7 @@ class ElementBase(object):
"""
out = []
out += [x for x in self.interfaces]
out += [x for x in self.plugins]
out += [x for x in self.loaded_plugins]
if self.iterables:
out.append('substanzas')
return out
@ -1075,6 +1164,23 @@ class ElementBase(object):
"""
return "{%s}%s" % (cls.namespace, cls.name)
def get_lang(self):
result = self.xml.attrib.get('{%s}lang' % XML_NS, '')
if not result and self.parent and self.parent():
return self.parent()['lang']
return result
def set_lang(self, lang):
self.del_lang()
attr = '{%s}lang' % XML_NS
if lang:
self.xml.attrib[attr] = lang
def del_lang(self):
attr = '{%s}lang' % XML_NS
if attr in self.xml.attrib:
del self.xml.attrib[attr]
@property
def attrib(self):
"""Return the stanza object itself.

View file

@ -13,14 +13,19 @@
:license: MIT, see LICENSE for more details
"""
from __future__ import unicode_literals
import sys
if sys.version_info < (3, 0):
import types
XML_NS = 'http://www.w3.org/XML/1998/namespace'
def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
outbuffer='', top_level=False):
outbuffer='', top_level=False, open_only=False):
"""Serialize an XML object to a Unicode string.
If namespaces are provided using ``xmlns`` or ``stanza_ns``, then
@ -88,6 +93,13 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
output.append(' %s:%s="%s"' % (mapped_ns,
attrib,
value))
elif attrib_ns == XML_NS:
output.append(' xml:%s="%s"' % (attrib, value))
if open_only:
# Only output the opening tag, regardless of content.
output.append(">")
return ''.join(output)
if len(xml) or xml.text:
# If there are additional child elements to serialize.

View file

@ -223,6 +223,9 @@ class XMLStream(object):
#: stream wrapper itself.
self.default_ns = ''
self.default_lang = None
self.peer_default_lang = None
#: The namespace of the enveloping stream element.
self.stream_ns = ''
@ -1431,6 +1434,10 @@ class XMLStream(object):
if depth == 0:
# We have received the start of the root element.
root = xml
log.debug('RECV: %s', tostring(root, xmlns=self.default_ns,
stream=self,
top_level=True,
open_only=True))
# Perform any stream initialization actions, such
# as handshakes.
self.stream_end_event.clear()
@ -1478,6 +1485,8 @@ class XMLStream(object):
stanza_type = stanza_class
break
stanza = stanza_type(self, xml)
if stanza['lang'] is None and self.peer_default_lang:
stanza['lang'] = self.peer_default_lang
return stanza
def __spawn_event(self, xml):

View file

@ -64,14 +64,18 @@ class TestElementBase(SleekTest):
stanza.append(substanza)
values = stanza.getStanzaValues()
expected = {'bar': 'a',
expected = {'lang': '',
'bar': 'a',
'baz': '',
'foo2': {'bar': '',
'foo2': {'lang': '',
'bar': '',
'baz': 'b'},
'substanzas': [{'__childtag__': '{foo}foo2',
'lang': '',
'bar': '',
'baz': 'b'},
{'__childtag__': '{foo}subfoo',
'lang': '',
'bar': 'c',
'baz': ''}]}
self.failUnless(values == expected,
@ -555,12 +559,12 @@ class TestElementBase(SleekTest):
stanza = TestStanza()
self.failUnless(set(stanza.keys()) == set(('bar', 'baz')),
self.failUnless(set(stanza.keys()) == set(('lang', 'bar', 'baz')),
"Returned set of interface keys does not match expected.")
stanza.enable('qux')
self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')),
self.failUnless(set(stanza.keys()) == set(('lang', 'bar', 'baz', 'qux')),
"Incorrect set of interface and plugin keys.")
def testGet(self):