Skip to content

Commit

Permalink
Make hessian client picklable, drop python 2.6 support
Browse files Browse the repository at this point in the history
  • Loading branch information
fdintino committed Jul 19, 2018
1 parent a18ac30 commit 5d6ba4b
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 43 deletions.
86 changes: 53 additions & 33 deletions pyhessian/client.py
@@ -1,15 +1,13 @@
from six.moves.http_client import HTTPConnection, HTTPSConnection
from six.moves.urllib.parse import urlparse
from warnings import warn
import base64
import sys

import six

from pyhessian.encoder import encode_object
from pyhessian.parser import Parser
from pyhessian.protocol import Call, Fault
from pyhessian.utils import BufferedReader
from pyhessian.utils import BufferedReader, cached_property
from pyhessian import __version__


Expand All @@ -28,39 +26,31 @@ def __repr__(self):
return "<ProtocolError for %s: %s %s>" % (self._url, self._status, self._reason,)


def identity_func(val):
return val


class HessianProxy(object):

def __init__(self, service_uri, credentials=None, key_file=None,
cert_file=None, timeout=10, buffer_size=65535,
error_factory=lambda x: x, overload=False, version=1):
error_factory=identity_func, overload=False, version=1):
self.version = version
self.timeout = timeout
self._buffer_size = buffer_size
self._error_factory = error_factory
self._overload = overload
self.key_file = key_file
self.cert_file = cert_file
self._uri = urlparse(service_uri)

self._headers = list()
self._headers.append(('User-Agent', 'python-hessian/' + __version__,))
self._headers.append(('Content-Type', 'application/x-hessian',))

if sys.version_info < (2, 6):
warn('HessianProxy timeout not enforceable before Python 2.6', RuntimeWarning, stacklevel=2)
kwargs = {}
else:
kwargs = {'timeout': timeout}

if six.PY2:
kwargs['strict'] = True
if self._uri.scheme not in ('http', 'https'):
raise ValueError("HessianProxy only supports http:// and https:// URIs")

self._uri = urlparse(service_uri)
if self._uri.scheme == 'http':
self._client = HTTPConnection(self._uri.hostname,
self._uri.port or 80,
**kwargs)
elif self._uri.scheme == 'https':
self._client = HTTPSConnection(self._uri.hostname,
self._uri.port or 443,
key_file=key_file,
cert_file=cert_file,
**kwargs)
else:
raise NotImplementedError("HessianProxy only supports http:// and https:// URIs")
self._headers = [
('User-Agent', 'python-hessian/%s' % __version__),
('Content-Type', 'application/x-hessian'),
]

# autofill credentials if they were passed via url instead of kwargs
if (self._uri.username and self._uri.password) and not credentials:
Expand All @@ -70,16 +60,46 @@ def __init__(self, service_uri, credentials=None, key_file=None,
auth = 'Basic ' + base64.b64encode(':'.join(credentials))
self._headers.append(('Authorization', auth))

self._buffer_size = buffer_size
self._error_factory = error_factory
self._overload = overload
self._parser = Parser()
@cached_property
def _client(self):
kwargs = {'timeout': self.timeout}

if six.PY2:
kwargs['strict'] = True

if self._uri.scheme == 'https':
connection_cls = HTTPSConnection
default_port = 443
kwargs.update({
'key_file': self.key_file,
'cert_file': self.cert_file,
})
else:
connection_cls = HTTPConnection
default_port = 80

return connection_cls(
self._uri.hostname, self._uri.port or default_port, **kwargs)

@cached_property
def _parser(self):
return Parser()

def __getstate__(self):
state = self.__dict__.copy()
state.pop('_parser', None)
state.pop('_client', None)
return state

def __setstate__(self, state):
self.__dict__.update(state)

class __RemoteMethod(object):
# dark magic for autoloading methods
def __init__(self, caller, method):
self.__caller = caller
self.__method = method

def __call__(self, *args):
return self.__caller(self.__method, args)

Expand Down
11 changes: 7 additions & 4 deletions pyhessian/protocol.py
Expand Up @@ -77,8 +77,8 @@ def __ne__(self, other):
class Reply(object):

def __init__(self, value=None, headers=None, version=None):
self.value = value # unmanaged property
self._headers = headers or dict()
self.value = value # unmanaged property
self._headers = headers or {}
self.version = version

def _get_headers(self):
Expand Down Expand Up @@ -126,7 +126,7 @@ def __init__(self, value):
self.value = value

def __add__(self, value):
if self.value == None:
if self.value is None:
return Binary(value)
else:
return Binary(self.value + value.value)
Expand Down Expand Up @@ -233,4 +233,7 @@ def cls_factory(name, fields=None, bases=None, attrs=None):
def object_factory(name, fields=None, bases=None, attrs=None, **kwargs):
if fields is None and kwargs:
fields = kwargs.keys()
return cls_factory(name, fields, bases, attrs)(**kwargs)
cls = cls_factory(name, fields, bases, attrs)
obj = cls.__new__(cls)
obj.__setstate__(kwargs)
return obj
22 changes: 21 additions & 1 deletion pyhessian/utils.py
Expand Up @@ -6,7 +6,7 @@
from functools import reduce as _reduce


__all__ = ['BufferedReader', 'toposort', 'toposort_flatten']
__all__ = ['BufferedReader', 'toposort', 'toposort_flatten', 'cached_property']


class BufferedReader(object):
Expand Down Expand Up @@ -131,3 +131,23 @@ def toposort_flatten(data, sort=True):
for d in toposort(data):
result.extend((sorted if sort else list)(d))
return result


class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
Optional ``name`` argument allows you to make cached properties of other
methods. (e.g. url = cached_property(get_absolute_url, name='url') )
"""
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, '__doc__')
self.name = name or func.__name__

def __get__(self, instance, type=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
7 changes: 2 additions & 5 deletions tox.ini
@@ -1,8 +1,5 @@
[tox]
envlist = py{26,27,34}
envlist = py27,py34,py36

[testenv]
commands = python runtests.py
deps =
six>=1.8.0
py26: unittest2
commands = python runtests.py {posargs}

0 comments on commit 5d6ba4b

Please sign in to comment.