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
"""
from __future__ import unicode_literals
import re
import socket
from copy import deepcopy
from functools import lru_cache
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.
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
'\\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'}
JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
#: The reverse mapping of escape sequences to their original forms.
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
@ -60,6 +45,8 @@ JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
'\\5c': '\\'}
# TODO: Find the best cache size for a standard usage.
@lru_cache(maxsize=1024)
def _parse_jid(data):
"""
Parse string data into the node, domain, and resource
@ -91,7 +78,9 @@ def _validate_node(node):
:returns: The local portion of a JID, as validated by nodeprep.
"""
if node is not None:
if node is None:
return ''
try:
node = nodeprep(node)
except StringprepError:
@ -170,7 +159,9 @@ def _validate_resource(resource):
:returns: The local portion of a JID, as validated by resourceprep.
"""
if resource is not None:
if resource is None:
return ''
try:
resource = resourceprep(resource)
except StringprepError:
@ -183,31 +174,6 @@ def _validate_resource(resource):
return resource
def _escape_node(node):
"""Escape the local portion of a JID."""
result = []
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):
"""Unescape a local portion of a JID.
@ -230,9 +196,7 @@ def _unescape_node(node):
seq = seq[1:]
else:
unescaped.append(char)
unescaped = ''.join(unescaped)
return unescaped
return ''.join(unescaped)
def _format_jid(local=None, domain=None, resource=None):
@ -266,47 +230,47 @@ class InvalidJID(ValueError):
"""
# pylint: disable=R0903
class UnescapedJID(object):
class UnescapedJID:
"""
.. versionadded:: 1.1.10
"""
def __init__(self, local, domain, resource):
self._jid = (local, domain, resource)
__slots__ = ('_node', '_domain', '_resource')
# pylint: disable=R0911
def __getattr__(self, name):
def __init__(self, node, domain, resource):
self._node = node
self._domain = domain
self._resource = resource
def __getattribute__(self, name):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
return self._jid[2] or ''
elif name in ('user', 'username', 'local', 'node'):
return self._jid[0] or ''
elif name in ('server', 'domain', 'host'):
return self._jid[1] or ''
elif name in ('full', 'jid'):
return _format_jid(*self._jid)
elif name == 'bare':
return _format_jid(self._jid[0], self._jid[1])
elif name == '_jid':
return getattr(super(JID, self), '_jid')
else:
return None
return self._resource
if name in ('user', 'username', 'local', 'node'):
return self._node
if name in ('server', 'domain', 'host'):
return self._domain
if name in ('full', 'jid'):
return _format_jid(self._node, self._domain, self._resource)
if name == 'bare':
return _format_jid(self._node, self._domain)
return object.__getattribute__(self, name)
def __str__(self):
"""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):
"""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.
@ -318,13 +282,13 @@ class JID(object):
The JID is a full JID otherwise.
**JID Properties:**
:jid: Alias for ``full``.
:full: The string value of the full JID.
:jid: Alias for ``full``.
:bare: The string value of the bare JID.
:user: The username portion of the JID.
:username: Alias for ``user``.
:local: Alias for ``user``.
:node: Alias for ``user``.
:node: The node portion of the JID.
:user: Alias for ``node``.
:local: Alias for ``node``.
:username: Alias for ``node``.
:domain: The domain name portion of the JID.
:server: Alias for ``domain``.
:host: Alias for ``domain``.
@ -332,49 +296,23 @@ class JID(object):
:param string jid:
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:
"""
# pylint: disable=W0212
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)
__slots__ = ('_node', '_domain', '_resource')
def __init__(self, jid=None):
if not jid:
parsed_jid = (None, None, None)
self._node = ''
self._domain = ''
self._resource = ''
elif not isinstance(jid, JID):
parsed_jid = _parse_jid(jid)
self._node, self._domain, self._resource = _parse_jid(jid)
else:
parsed_jid = jid._jid
local, domain, resource = parsed_jid
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)
self._node = jid._node
self._domain = jid._domain
self._resource = jid._resource
def unescape(self):
"""Return an unescaped JID object.
@ -387,151 +325,125 @@ class JID(object):
.. versionadded:: 1.1.10
"""
return UnescapedJID(_unescape_node(self._jid[0]),
self._jid[1],
self._jid[2])
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 ''
return UnescapedJID(_unescape_node(self._node),
self._domain,
self._resource)
@property
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
def username(self):
return self._jid[0] or ''
@property
def bare(self):
return _format_jid(self._jid[0], self._jid[1])
@property
def server(self):
return self._jid[1] or ''
return self._node
@property
def domain(self):
return self._jid[1] or ''
return self._domain
@property
def server(self):
return self._domain
@property
def host(self):
return self._jid[1] or ''
@property
def full(self):
return _format_jid(*self._jid)
@property
def jid(self):
return _format_jid(*self._jid)
return self._domain
@property
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
def resource(self, value):
self._jid = JID(self, resource=value)._jid
@property
def full(self):
return _format_jid(self._node, self._domain, self._resource)
@user.setter
def user(self, value):
self._jid = JID(self, local=value)._jid
@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
@property
def jid(self):
return _format_jid(self._node, self._domain, self._resource)
@node.setter
def node(self, value):
self._jid = JID(self, local=value)._jid
self._node = _validate_node(value)
@server.setter
def server(self, value):
self._jid = JID(self, domain=value)._jid
@user.setter
def user(self, value):
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
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
def host(self, value):
self._jid = JID(self, domain=value)._jid
@full.setter
def full(self, value):
self._jid = JID(value)._jid
@jid.setter
def jid(self, value):
self._jid = JID(value)._jid
self._domain = _validate_domain(value)
@bare.setter
def bare(self, value):
parsed = JID(value)._jid
self._jid = (parsed[0], parsed[1], self._jid[2])
node, domain, resource = _parse_jid(value)
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):
"""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):
"""Use the full JID as the representation."""
return self.__str__()
return _format_jid(self._node, self._domain, self._resource)
# pylint: disable=W0212
def __eq__(self, other):
"""Two JIDs are equal if they have the same full JID value."""
if isinstance(other, UnescapedJID):
return False
if not isinstance(other, JID):
other = JID(other)
return self._jid == other._jid
# pylint: disable=W0212
return (self._node == other._node and
self._domain == other._domain and
self._resource == other._resource)
def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal."""
return not self == other
def __hash__(self):
"""Hash a JID based on the string version of its full JID."""
return hash(self.__str__())
def __copy__(self):
"""Generate a duplicate JID."""
return JID(self)
def __deepcopy__(self, memo):
"""Generate a duplicate JID."""
return JID(deepcopy(str(self), memo))
return hash(_format_jid(self._node, self._domain, self._resource))