Rework slixmpp.jid’s JID classes to make them more efficient.

This commit is contained in:
Emmanuel Gil Peyrot 2015-06-12 00:46:47 +01:00 committed by Emmanuel Gil Peyrot
parent 7bce1ecc8a
commit 4afbb0322b

View File

@ -11,12 +11,11 @@
:license: MIT, see LICENSE for more details :license: MIT, see LICENSE for more details
""" """
from __future__ import unicode_literals
import re import re
import socket import socket
from copy import deepcopy from copy import deepcopy
from functools import lru_cache
from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
@ -30,22 +29,8 @@ JID_PATTERN = re.compile(
) )
#: The set of escape sequences for the characters not allowed by nodeprep. #: The set of escape sequences for the characters not allowed by nodeprep.
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f', JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
'\\3a', '\\3c', '\\3e', '\\40', '\\5c']) '\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
#: A mapping of unallowed characters to their escape sequences. An escape
#: sequence for '\' is also included since it must also be escaped in
#: certain situations.
JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20',
'"': '\\22',
'&': '\\26',
"'": '\\27',
'/': '\\2f',
':': '\\3a',
'<': '\\3c',
'>': '\\3e',
'@': '\\40',
'\\': '\\5c'}
#: The reverse mapping of escape sequences to their original forms. #: The reverse mapping of escape sequences to their original forms.
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ', JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
@ -60,6 +45,8 @@ JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
'\\5c': '\\'} '\\5c': '\\'}
# TODO: Find the best cache size for a standard usage.
@lru_cache(maxsize=1024)
def _parse_jid(data): def _parse_jid(data):
""" """
Parse string data into the node, domain, and resource Parse string data into the node, domain, and resource
@ -91,17 +78,19 @@ def _validate_node(node):
:returns: The local portion of a JID, as validated by nodeprep. :returns: The local portion of a JID, as validated by nodeprep.
""" """
if node is not None: if node is None:
try: return ''
node = nodeprep(node)
except StringprepError:
raise InvalidJID('Nodeprep failed')
if not node: try:
raise InvalidJID('Localpart must not be 0 bytes') node = nodeprep(node)
if len(node) > 1023: except StringprepError:
raise InvalidJID('Localpart must be less than 1024 bytes') raise InvalidJID('Nodeprep failed')
return node
if not node:
raise InvalidJID('Localpart must not be 0 bytes')
if len(node) > 1023:
raise InvalidJID('Localpart must be less than 1024 bytes')
return node
def _validate_domain(domain): def _validate_domain(domain):
@ -170,42 +159,19 @@ def _validate_resource(resource):
:returns: The local portion of a JID, as validated by resourceprep. :returns: The local portion of a JID, as validated by resourceprep.
""" """
if resource is not None: if resource is None:
try: return ''
resource = resourceprep(resource)
except StringprepError:
raise InvalidJID('Resourceprep failed')
if not resource: try:
raise InvalidJID('Resource must not be 0 bytes') resource = resourceprep(resource)
if len(resource) > 1023: except StringprepError:
raise InvalidJID('Resource must be less than 1024 bytes') raise InvalidJID('Resourceprep failed')
return resource
if not resource:
def _escape_node(node): raise InvalidJID('Resource must not be 0 bytes')
"""Escape the local portion of a JID.""" if len(resource) > 1023:
result = [] raise InvalidJID('Resource must be less than 1024 bytes')
return resource
for i, char in enumerate(node):
if char == '\\':
if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
result.append('\\5c')
continue
result.append(char)
for i, char in enumerate(result):
if char != '\\':
result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
escaped = ''.join(result)
if escaped.startswith('\\20') or escaped.endswith('\\20'):
raise InvalidJID('Escaped local part starts or ends with "\\20"')
_validate_node(escaped)
return escaped
def _unescape_node(node): def _unescape_node(node):
@ -230,9 +196,7 @@ def _unescape_node(node):
seq = seq[1:] seq = seq[1:]
else: else:
unescaped.append(char) unescaped.append(char)
unescaped = ''.join(unescaped) return ''.join(unescaped)
return unescaped
def _format_jid(local=None, domain=None, resource=None): def _format_jid(local=None, domain=None, resource=None):
@ -266,47 +230,47 @@ class InvalidJID(ValueError):
""" """
# pylint: disable=R0903 # pylint: disable=R0903
class UnescapedJID(object): class UnescapedJID:
""" """
.. versionadded:: 1.1.10 .. versionadded:: 1.1.10
""" """
def __init__(self, local, domain, resource): __slots__ = ('_node', '_domain', '_resource')
self._jid = (local, domain, resource)
# pylint: disable=R0911 def __init__(self, node, domain, resource):
def __getattr__(self, name): self._node = node
self._domain = domain
self._resource = resource
def __getattribute__(self, name):
"""Retrieve the given JID component. """Retrieve the given JID component.
:param name: one of: user, server, domain, resource, :param name: one of: user, server, domain, resource,
full, or bare. full, or bare.
""" """
if name == 'resource': if name == 'resource':
return self._jid[2] or '' return self._resource
elif name in ('user', 'username', 'local', 'node'): if name in ('user', 'username', 'local', 'node'):
return self._jid[0] or '' return self._node
elif name in ('server', 'domain', 'host'): if name in ('server', 'domain', 'host'):
return self._jid[1] or '' return self._domain
elif name in ('full', 'jid'): if name in ('full', 'jid'):
return _format_jid(*self._jid) return _format_jid(self._node, self._domain, self._resource)
elif name == 'bare': if name == 'bare':
return _format_jid(self._jid[0], self._jid[1]) return _format_jid(self._node, self._domain)
elif name == '_jid': return object.__getattribute__(self, name)
return getattr(super(JID, self), '_jid')
else:
return None
def __str__(self): def __str__(self):
"""Use the full JID as the string value.""" """Use the full JID as the string value."""
return _format_jid(*self._jid) return _format_jid(self._node, self._domain, self._resource)
def __repr__(self): def __repr__(self):
"""Use the full JID as the representation.""" """Use the full JID as the representation."""
return self.__str__() return _format_jid(self._node, self._domain, self._resource)
class JID(object): class JID:
""" """
A representation of a Jabber ID, or JID. A representation of a Jabber ID, or JID.
@ -318,13 +282,13 @@ class JID(object):
The JID is a full JID otherwise. The JID is a full JID otherwise.
**JID Properties:** **JID Properties:**
:jid: Alias for ``full``.
:full: The string value of the full JID. :full: The string value of the full JID.
:jid: Alias for ``full``.
:bare: The string value of the bare JID. :bare: The string value of the bare JID.
:user: The username portion of the JID. :node: The node portion of the JID.
:username: Alias for ``user``. :user: Alias for ``node``.
:local: Alias for ``user``. :local: Alias for ``node``.
:node: Alias for ``user``. :username: Alias for ``node``.
:domain: The domain name portion of the JID. :domain: The domain name portion of the JID.
:server: Alias for ``domain``. :server: Alias for ``domain``.
:host: Alias for ``domain``. :host: Alias for ``domain``.
@ -332,49 +296,23 @@ class JID(object):
:param string jid: :param string jid:
A string of the form ``'[user@]domain[/resource]'``. A string of the form ``'[user@]domain[/resource]'``.
:param string local:
Optional. Specify the local, or username, portion
of the JID. If provided, it will override the local
value provided by the `jid` parameter. The given
local value will also be escaped if necessary.
:param string domain:
Optional. Specify the domain of the JID. If
provided, it will override the domain given by
the `jid` parameter.
:param string resource:
Optional. Specify the resource value of the JID.
If provided, it will override the domain given
by the `jid` parameter.
:raises InvalidJID: :raises InvalidJID:
""" """
# pylint: disable=W0212 __slots__ = ('_node', '_domain', '_resource')
def __init__(self, jid=None, **kwargs):
in_local = kwargs.get('local', None)
in_domain = kwargs.get('domain', None)
in_resource = kwargs.get('resource', None)
parts = None
if in_local or in_domain or in_resource:
parts = (in_local, in_domain, in_resource)
def __init__(self, jid=None):
if not jid: if not jid:
parsed_jid = (None, None, None) self._node = ''
self._domain = ''
self._resource = ''
elif not isinstance(jid, JID): elif not isinstance(jid, JID):
parsed_jid = _parse_jid(jid) self._node, self._domain, self._resource = _parse_jid(jid)
else: else:
parsed_jid = jid._jid self._node = jid._node
self._domain = jid._domain
local, domain, resource = parsed_jid self._resource = jid._resource
if 'local' in kwargs:
local = _escape_node(in_local)
if 'domain' in kwargs:
domain = _validate_domain(in_domain)
if 'resource' in kwargs:
resource = _validate_resource(in_resource)
self._jid = (local, domain, resource)
def unescape(self): def unescape(self):
"""Return an unescaped JID object. """Return an unescaped JID object.
@ -387,151 +325,125 @@ class JID(object):
.. versionadded:: 1.1.10 .. versionadded:: 1.1.10
""" """
return UnescapedJID(_unescape_node(self._jid[0]), return UnescapedJID(_unescape_node(self._node),
self._jid[1], self._domain,
self._jid[2]) self._resource)
def regenerate(self):
"""No-op
.. deprecated:: 1.1.10
"""
pass
def reset(self, data):
"""Start fresh from a new JID string.
:param string data: A string of the form ``'[user@]domain[/resource]'``.
.. deprecated:: 1.1.10
"""
self._jid = JID(data)._jid
@property
def resource(self):
return self._jid[2] or ''
@property
def user(self):
return self._jid[0] or ''
@property
def local(self):
return self._jid[0] or ''
@property @property
def node(self): def node(self):
return self._jid[0] or '' return self._node
@property
def user(self):
return self._node
@property
def local(self):
return self._node
@property @property
def username(self): def username(self):
return self._jid[0] or '' return self._node
@property
def bare(self):
return _format_jid(self._jid[0], self._jid[1])
@property
def server(self):
return self._jid[1] or ''
@property @property
def domain(self): def domain(self):
return self._jid[1] or '' return self._domain
@property
def server(self):
return self._domain
@property @property
def host(self): def host(self):
return self._jid[1] or '' return self._domain
@property
def full(self):
return _format_jid(*self._jid)
@property
def jid(self):
return _format_jid(*self._jid)
@property @property
def bare(self): def bare(self):
return _format_jid(self._jid[0], self._jid[1]) return _format_jid(self._node, self._domain)
@property
def resource(self):
return self._resource
@resource.setter @property
def resource(self, value): def full(self):
self._jid = JID(self, resource=value)._jid return _format_jid(self._node, self._domain, self._resource)
@user.setter @property
def user(self, value): def jid(self):
self._jid = JID(self, local=value)._jid return _format_jid(self._node, self._domain, self._resource)
@username.setter
def username(self, value):
self._jid = JID(self, local=value)._jid
@local.setter
def local(self, value):
self._jid = JID(self, local=value)._jid
@node.setter @node.setter
def node(self, value): def node(self, value):
self._jid = JID(self, local=value)._jid self._node = _validate_node(value)
@server.setter @user.setter
def server(self, value): def user(self, value):
self._jid = JID(self, domain=value)._jid self._node = _validate_node(value)
@local.setter
def local(self, value):
self._node = _validate_node(value)
@username.setter
def username(self, value):
self._node = _validate_node(value)
@domain.setter @domain.setter
def domain(self, value): def domain(self, value):
self._jid = JID(self, domain=value)._jid self._domain = _validate_domain(value)
@server.setter
def server(self, value):
self._domain = _validate_domain(value)
@host.setter @host.setter
def host(self, value): def host(self, value):
self._jid = JID(self, domain=value)._jid self._domain = _validate_domain(value)
@full.setter
def full(self, value):
self._jid = JID(value)._jid
@jid.setter
def jid(self, value):
self._jid = JID(value)._jid
@bare.setter @bare.setter
def bare(self, value): def bare(self, value):
parsed = JID(value)._jid node, domain, resource = _parse_jid(value)
self._jid = (parsed[0], parsed[1], self._jid[2]) assert not resource
self._node = node
self._domain = domain
@resource.setter
def resource(self, value):
self._resource = _validate_resource(value)
@full.setter
def full(self, value):
self._node, self._domain, self._resource = _parse_jid(value)
@jid.setter
def jid(self, value):
self._node, self._domain, self._resource = _parse_jid(value)
def __str__(self): def __str__(self):
"""Use the full JID as the string value.""" """Use the full JID as the string value."""
return _format_jid(*self._jid) return _format_jid(self._node, self._domain, self._resource)
def __repr__(self): def __repr__(self):
"""Use the full JID as the representation.""" """Use the full JID as the representation."""
return self.__str__() return _format_jid(self._node, self._domain, self._resource)
# pylint: disable=W0212 # pylint: disable=W0212
def __eq__(self, other): def __eq__(self, other):
"""Two JIDs are equal if they have the same full JID value.""" """Two JIDs are equal if they have the same full JID value."""
if isinstance(other, UnescapedJID): if isinstance(other, UnescapedJID):
return False return False
if not isinstance(other, JID):
other = JID(other)
other = JID(other) return (self._node == other._node and
return self._jid == other._jid self._domain == other._domain and
self._resource == other._resource)
# pylint: disable=W0212
def __ne__(self, other): def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal.""" """Two JIDs are considered unequal if they are not equal."""
return not self == other return not self == other
def __hash__(self): def __hash__(self):
"""Hash a JID based on the string version of its full JID.""" """Hash a JID based on the string version of its full JID."""
return hash(self.__str__()) return hash(_format_jid(self._node, self._domain, self._resource))
def __copy__(self):
"""Generate a duplicate JID."""
return JID(self)
def __deepcopy__(self, memo):
"""Generate a duplicate JID."""
return JID(deepcopy(str(self), memo))