epc-0.0.5/ 0000755 0001750 0001750 00000000000 12161373575 012127 5 ustar dogsleg dogsleg epc-0.0.5/setup.py 0000644 0001750 0001750 00000001764 12161372713 013642 0 ustar dogsleg dogsleg from distutils.core import setup
import epc
setup(
name='epc',
version=epc.__version__,
packages=['epc', 'epc.tests'],
author=epc.__author__,
author_email='aka.tkf@gmail.com',
url='https://github.com/tkf/python-epc',
license=epc.__license__,
description='EPC (RPC stack for Emacs Lisp) implementation in Python',
long_description=epc.__doc__,
keywords='Emacs, RPC',
classifiers=[
"Development Status :: 3 - Alpha",
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
"Programming Language :: Emacs-Lisp",
# see: http://pypi.python.org/pypi?%3Aaction=list_classifiers
],
install_requires=[
'sexpdata >= 0.0.3',
],
)
epc-0.0.5/PKG-INFO 0000644 0001750 0001750 00000011125 12161373575 013224 0 ustar dogsleg dogsleg Metadata-Version: 1.0
Name: epc
Version: 0.0.5
Summary: EPC (RPC stack for Emacs Lisp) implementation in Python
Home-page: https://github.com/tkf/python-epc
Author: Takafumi Arakaki
Author-email: aka.tkf@gmail.com
License: GNU General Public License v3 (GPLv3)
Description:
EPC (RPC stack for Emacs Lisp) for Python
=========================================
Links:
* `Documentation `_ (at Read the Docs)
* `Repository `_ (at GitHub)
* `Issue tracker `_ (at GitHub)
* `PyPI `_
* `Travis CI `_ |build-status|
Other resources:
* `kiwanami/emacs-epc `_
(Client and server implementation in Emacs Lisp and Perl.)
* `tkf/emacs-jedi `_
(Python completion for Emacs using EPC server.)
.. |build-status|
image:: https://secure.travis-ci.org/tkf/python-epc.png
?branch=master
:target: http://travis-ci.org/tkf/python-epc
:alt: Build Status
What is this?
-------------
EPC is an RPC stack for Emacs Lisp and Python-EPC is its server side
and client side implementation in Python. Using Python-EPC, you can
easily call Emacs Lisp functions from Python and Python functions from
Emacs. For example, you can use Python GUI module to build widgets
for Emacs (see `examples/gtk/server.py`_ for example).
Python-EPC is tested against Python 2.6, 2.7 and 3.2.
Install
-------
To install Python-EPC and its dependency sexpdata_, run the following
command.::
pip install epc
.. _sexpdata: https://github.com/tkf/sexpdata
Usage
-----
Save the following code as ``my-server.py``.
(You can find functionally the same code in `examples/echo/server.py`_)::
from epc.server import EPCServer
server = EPCServer(('localhost', 0))
@server.register_function
def echo(*a):
return a
server.print_port()
server.serve_forever()
And then run the following code from Emacs.
This is a stripped version of `examples/echo/client.el`_ included in
Python-EPC repository_.::
(require 'epc)
(defvar my-epc (epc:start-epc "python" '("my-server.py")))
(deferred:$
(epc:call-deferred my-epc 'echo '(10))
(deferred:nextc it
(lambda (x) (message "Return : %S" x))))
(message "Return : %S" (epc:call-sync my-epc 'echo '(10 40)))
.. _examples/echo/server.py:
https://github.com/tkf/python-epc/blob/master/examples/echo/server.py
.. _examples/echo/client.el:
https://github.com/tkf/python-epc/blob/master/examples/echo/client.el
If you have carton_ installed, you can run the above sample by
simply typing the following commands::
make elpa # install EPC in a separated environment
make run-sample # run examples/echo/client.el
.. _carton: https://github.com/rejeep/carton
For example of bidirectional communication and integration with GTK,
see `examples/gtk/server.py`_. You can run this example by::
make elpa
make run-gtk-sample # run examples/gtk/client.el
.. _examples/gtk/server.py:
https://github.com/tkf/python-epc/blob/master/examples/gtk/server.py
License
-------
Python-EPC is licensed under GPL v3.
See COPYING for details.
Keywords: Emacs,RPC
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Emacs-Lisp
epc-0.0.5/epc/ 0000755 0001750 0001750 00000000000 12161373575 012676 5 ustar dogsleg dogsleg epc-0.0.5/epc/server.py 0000644 0001750 0001750 00000016700 12121243564 014550 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
import logging
from .py3compat import SocketServer
from .utils import autolog, deprecated
from .core import EPCCore
from .handler import EPCHandler, ThreadingEPCHandler
@deprecated
def setuplogfile(logger=None, filename='python-epc.log'):
if logger is None:
from .core import _logger as logger
ch = logging.FileHandler(filename=filename, mode='w')
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)
class EPCClientManager:
# This class will be mixed with `SocketServer.TCPServer`,
# which is an old style class.
def __init__(self):
self.clients = []
"""
A list of :class:`EPCHandler` object for connected clients.
"""
def add_client(self, handler):
self.clients.append(handler)
self.handle_client_connect(handler)
def remove_client(self, handler):
self.clients.remove(handler)
self.handle_client_disconnect(handler)
def handle_client_connect(self, handler):
"""
Handler which is called with a newly connected `client`.
:type handler: :class:`EPCHandler`
:arg handler: Object for handling request from the client.
Default implementation does nothing.
"""
def handle_client_disconnect(self, handler):
"""
Handler which is called with a disconnected `client`.
:type handler: :class:`EPCHandler`
:arg handler: Object for handling request from the client.
Default implementation does nothing.
"""
class EPCServer(SocketServer.TCPServer, EPCClientManager,
EPCCore):
"""
A server class to publish functions and call functions via EPC protocol.
To publish Python functions, all you need is
:meth:`register_function`,
:meth:`print_port` and
:meth:`serve_forever() `.
>>> server = EPCServer(('localhost', 0))
>>> def echo(*a):
... return a
>>> server.register_function(echo) #doctest: +ELLIPSIS
>>> server.print_port() #doctest: +SKIP
9999
>>> server.serve_forever() #doctest: +SKIP
To call client's method, use :attr:`clients `
attribute to get client handler and use its :meth:`EPCHandler.call` and
:meth:`EPCHandler.methods` methods to communicate with connected client.
>>> handler = server.clients[0] #doctest: +SKIP
>>> def callback(reply):
... print(reply)
>>> handler.call('method_name', ['arg-1', 'arg-2', 'arg-3'],
... callback) #doctest: +SKIP
See :class:`SocketServer.TCPServer` and :class:`SocketServer.BaseServer`
for other usable methods.
"""
def __init__(self, server_address,
RequestHandlerClass=EPCHandler,
bind_and_activate=True,
debugger=None, log_traceback=False):
# `BaseServer` (super class of `SocketServer`) will set
# `RequestHandlerClass` to the attribute `self.RequestHandlerClass`.
# This class is initialize in `BaseServer.finish_request` by
# `self.RequestHandlerClass(request, client_address, self)`.
SocketServer.TCPServer.__init__(
self, server_address, RequestHandlerClass, bind_and_activate)
EPCClientManager.__init__(self)
EPCCore.__init__(self, debugger, log_traceback)
self.logger.debug('-' * 75)
self.logger.debug(
"EPCServer is initialized: server_address = %r",
self.server_address)
@autolog('debug')
def handle_error(self, request, client_address):
self.logger.error('handle_error: trying to get traceback.format_exc')
try:
import traceback
self.logger.error('handle_error: \n%s', traceback.format_exc())
except:
self.logger.error('handle_error: OOPS')
def print_port(self, stream=sys.stdout):
"""
Print port this EPC server runs on.
As Emacs client reads port number from STDOUT, you need to
call this just before calling :meth:`serve_forever`.
:type stream: text stream
:arg stream: A stream object to write port on.
Default is :data:`sys.stdout`.
"""
stream.write(str(self.server_address[1]))
stream.write("\n")
stream.flush()
class ThreadingEPCServer(SocketServer.ThreadingMixIn, EPCServer):
"""
Class :class:`EPCServer` mixed with :class:`SocketServer.ThreadingMixIn`.
Use this class when combining EPCServer with other Python module
which has event loop, such as GUI modules. For example, see
`examples/gtk/server.py`_ for how to use this class with GTK
.. _examples/gtk/server.py:
https://github.com/tkf/python-epc/blob/master/examples/gtk/server.py
"""
def __init__(self, *args, **kwds):
kwds.update(RequestHandlerClass=ThreadingEPCHandler)
EPCServer.__init__(self, *args, **kwds)
def main(args=None):
"""
Quick CLI to serve Python functions in a module.
Example usage::
python -m epc.server --allow-dotted-names os
Note that only the functions which gets and returns simple
built-in types (str, int, float, list, tuple, dict) works.
"""
import argparse
from textwrap import dedent
parser = argparse.ArgumentParser(
formatter_class=type('EPCHelpFormatter',
(argparse.ArgumentDefaultsHelpFormatter,
argparse.RawDescriptionHelpFormatter),
{}),
description=dedent(main.__doc__))
parser.add_argument(
'module', help='Serve python functions in this module.')
parser.add_argument(
'--address', default='localhost',
help='server address')
parser.add_argument(
'--port', default=0, type=int,
help='server port. 0 means to pick up random port.')
parser.add_argument(
'--allow-dotted-names', default=False, action='store_true')
parser.add_argument(
'--pdb', dest='debugger', const='pdb', action='store_const',
help='start pdb when error occurs.')
parser.add_argument(
'--ipdb', dest='debugger', const='ipdb', action='store_const',
help='start ipdb when error occurs.')
parser.add_argument(
'--log-traceback', action='store_true', default=False)
ns = parser.parse_args(args)
server = EPCServer((ns.address, ns.port),
debugger=ns.debugger,
log_traceback=ns.log_traceback)
server.register_instance(
__import__(ns.module),
allow_dotted_names=ns.allow_dotted_names)
server.print_port()
server.serve_forever()
if __name__ == '__main__':
main()
epc-0.0.5/epc/utils.py 0000644 0001750 0001750 00000010427 12120634031 014372 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import itertools
import functools
import threading
import warnings
from .py3compat import Queue
def func_call_as_str(name, *args, **kwds):
"""
Return arguments and keyword arguments as formatted string
>>> func_call_as_str('f', 1, 2, a=1)
'f(1, 2, a=1)'
"""
return '{0}({1})'.format(
name,
', '.join(itertools.chain(
map('{0!r}'.format, args),
map('{0[0]!s}={0[1]!r}'.format, sorted(kwds.items())))))
def autolog(level):
if isinstance(level, str):
level = getattr(logging, level.upper())
def wrapper(method):
@functools.wraps(method)
def new_method(self, *args, **kwds):
funcname = ".".join([self.__class__.__name__, method.__name__])
self.logger.log(level, "(AutoLog) Called: %s",
func_call_as_str(funcname, *args, **kwds))
ret = method(self, *args, **kwds)
self.logger.log(level, "(AutoLog) Returns: %s(...) = %r",
funcname, ret)
return ret
return new_method
return wrapper
def deprecated(func):
"""
Decorator for marking function as deprecated
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
'{0} is deprecated.'.format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
def newname(template):
global _counter
_counter = _counter + 1
return template.format(_counter)
_counter = 0
def newthread(template="EPCThread-{0}", **kwds):
"""
Instantiate :class:`threading.Thread` with an appropriate name.
"""
if not isinstance(template, str):
template = '{0}.{1}-{{0}}'.format(template.__module__,
template.__class__.__name__)
return threading.Thread(
name=newname(template), **kwds)
class ThreadedIterator(object):
def __init__(self, iterable):
self._original_iterable = iterable
self.queue = Queue.Queue()
self.thread = newthread(self, target=self._target)
self.thread.daemon = True
self._sentinel = object()
self.thread.start()
def _target(self):
for result in self._original_iterable:
self.queue.put(result)
self.stop()
def stop(self):
self.queue.put(self._sentinel)
def __iter__(self):
return self
def __next__(self):
got = self.queue.get()
if got is self._sentinel:
raise StopIteration
return got
next = __next__ # for PY2
def callwith(context_manager):
"""
A decorator to wrap execution of function with a context manager.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwds):
with context_manager:
return func(*args, **kwds)
return wrapper
return decorator
def _define_thread_safe_methods(methodnames, lockname):
def define(cls, name):
def wrapper(self, *args, **kwds):
with getattr(self, lockname):
return method(self, *args, **kwds)
method = getattr(cls, name)
setattr(cls, name, wrapper)
def decorator(cls):
for name in methodnames:
define(cls, name)
return cls
return decorator
@_define_thread_safe_methods(
['__getitem__', '__setitem__', '__delitem__', 'pop'], '_lock')
class LockingDict(dict):
def __init__(self, *args, **kwds):
super(LockingDict, self).__init__(*args, **kwds)
self._lock = threading.Lock()
epc-0.0.5/epc/tests/ 0000755 0001750 0001750 00000000000 12161373575 014040 5 ustar dogsleg dogsleg epc-0.0.5/epc/tests/test_server.py 0000644 0001750 0001750 00000030122 12121243564 016743 0 ustar dogsleg dogsleg # -*- coding: utf-8 -*-
# Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import socket
from sexpdata import Symbol, loads
from ..server import ThreadingEPCServer
from ..utils import newthread
from ..handler import encode_string, encode_object, BlockingCallback, \
ReturnError, EPCError, ReturnErrorCallerUnknown, EPCErrorCallerUnknown, \
CallerUnknown
from ..py3compat import utf8, Queue, nested
from .utils import mockedattr, logging_to_stdout, CaptureStdIO, BaseTestCase, \
streamio
class TestEPCServerMisc(BaseTestCase):
"""
Test that can be done without client.
"""
def setUp(self):
# See: http://stackoverflow.com/questions/7720953
ThreadingEPCServer.allow_reuse_address = True
self.server = ThreadingEPCServer(('localhost', 0))
self.server_thread = newthread(self, target=self.server.serve_forever)
self.server_thread.start()
def tearDown(self):
self.server.shutdown()
self.server.server_close()
def test_print_port(self):
stream = streamio()
self.server.print_port(stream)
self.assertEqual(stream.getvalue(),
'{0}\n'.format(self.server.server_address[1]))
class BaseEPCServerTestCase(BaseTestCase):
def setUp(self):
# See: http://stackoverflow.com/questions/7720953
ThreadingEPCServer.allow_reuse_address = True
self.server = ThreadingEPCServer(('localhost', 0))
self.server_thread = newthread(self, target=self.server.serve_forever)
self.server_thread.start()
def echo(*a):
"""Return argument unchanged."""
return a
def bad_method(*_):
"""This is a bad method. Don't call!"""
raise self.error_to_throw
self.error_to_throw = ValueError("This is a bad method!")
self.server.register_function(echo)
self.server.register_function(bad_method)
self.client = socket.create_connection(self.server.server_address)
self.client.settimeout(self.timeout)
def tearDown(self):
self.client.shutdown(socket.SHUT_RDWR)
self.client.close()
self.server.shutdown()
self.server.server_close()
def receive_message(self):
result = self.client.recv(1024)
self.assertEqual(int(result[:6], 16), len(result[6:]))
return loads(result[6:].decode()) # skip the length part
def client_send(self, string):
self.client.send(encode_string(string))
def check_echo(self):
self.client_send('(call 1 echo (55))')
result = self.client.recv(1024)
self.assertEqual(encode_string('(return 1 (55))'), result)
class TestEPCServerRequestHandling(BaseEPCServerTestCase):
"""
Test that EPCServer handles request from client properly.
"""
def test_echo(self):
self.check_echo()
def test_error_in_method(self):
with logging_to_stdout(self.server.logger):
self.client_send('(call 2 bad_method nil)')
result = self.client.recv(1024)
expected = encode_object([
Symbol('return-error'), 2, repr(self.error_to_throw)])
self.assertEqual(result, expected)
def test_no_such_method(self):
with logging_to_stdout(self.server.logger):
self.client_send('(call 3 no_such_method nil)')
reply = self.receive_message()
self.assertEqual(reply[0], Symbol('epc-error'))
self.assertEqual(reply[1], 3)
assert 'No such method' in reply[2]
def test_methods(self):
self.client_send('(methods 4)')
reply = self.receive_message()
self.assertEqual(reply[0], Symbol('return'))
self.assertEqual(reply[1], 4)
method = dict((m[0].value(), m[1:]) for m in reply[2])
self.assertEqual(set(method), set(['echo', 'bad_method']))
actual_docs = dict(
(n, doc) for (n, (_, doc)) in method.items())
desired_docs = dict(
(n, f.__doc__) for (n, f) in self.server.funcs.items())
self.assertEqual(actual_docs, desired_docs)
def test_unicode_message(self):
s = "日本語能力!!ソハンカク"
self.client_send(utf8('(call 1 echo ("{0}"))'.format(s)))
result = self.client.recv(1024)
self.assertEqual(encode_string(utf8('(return 1 ("{0}"))'.format(s))),
result)
def test_invalid_sexp(self):
with logging_to_stdout(self.server.logger):
self.client_send('(((invalid sexp!')
reply = self.receive_message()
self.assertEqual(reply[0].value(), Symbol('epc-error').value())
self.assertEqual(reply[1], []) # uid
assert 'Not enough closing brackets.' in reply[2]
def check_caller_unkown(self, message, eclass, eargs):
self.check_echo() # to establish connection to client
called_with = Queue.Queue()
with nested(mockedattr(self.server.clients[0],
'handle_error', called_with.put),
logging_to_stdout(self.server.logger)):
self.client_send(message)
error = called_with.get(True, 1)
self.assertIsInstance(error, eclass)
self.assertEqual(error.args, eargs)
def test_return_caller_unkown(self):
self.check_caller_unkown(
'(return 0 ("some" "value"))', # uid=0 is always unkown
CallerUnknown, (['some', 'value'],))
def test_return_error_caller_unkown(self):
self.check_caller_unkown(
'(return-error nil "message")',
ReturnErrorCallerUnknown, ('message',))
def test_epc_error_caller_unkown(self):
self.check_caller_unkown(
'(epc-error nil "message")',
EPCErrorCallerUnknown, ('message',))
def check_invalid_call(self, make_call):
# These are not necessary for the actual test, but rather
# to make sure that the server stays in the context of
# `logging_to_stdout` until the error is handled. See
# `called_with.get` below.
def handle_error(err):
self.assertTrue(orig_handle_error(err))
called_with.put(err)
return True
self.check_echo() # to fetch handler
handler = self.server.clients[0]
orig_handle_error = handler.handle_error
called_with = Queue.Queue()
# Here comes the actual test:
uid = 1
with nested(logging_to_stdout(self.server.logger),
mockedattr(handler, 'handle_error', handle_error)):
self.client_send(make_call(uid))
reply = self.receive_message()
called_with.get(timeout=1) # wait until the error got handled
self.assertEqual(reply[0], Symbol('epc-error'))
self.assertEqual(reply[1], uid)
def test_invalid_call_not_enough_arguments(self):
self.check_invalid_call('(call {0} echo)'.format)
def test_invalid_call_too_many_arguments(self):
self.check_invalid_call(
'(call {0} echo "value" "extra" "value")'.format)
def test_invalid_methods_too_many_arguments(self):
self.check_invalid_call('(methods {0} "extra value")'.format)
def test_log_traceback(self):
stdio = CaptureStdIO()
with nested(stdio, mockedattr(self.server, 'log_traceback', True)):
self.test_error_in_method()
log = stdio.read_stdout()
self.assertIn('ValueError: This is a bad method!', log)
self.assertIn('raise self.error_to_throw', log)
class TestEPCServerCallClient(BaseEPCServerTestCase):
def setUp(self):
super(TestEPCServerCallClient, self).setUp()
self.check_echo() # to start connection, client must send something
self.handler = self.server.clients[0]
self.callback_called_with = Queue.Queue()
self.callback = self.callback_called_with.put
self.errback_called_with = Queue.Queue()
self.errback = self.errback_called_with.put
def check_call_client_dummy_method(self):
(call, uid, meth, args) = self.receive_message()
self.assertIsInstance(uid, int)
self.assertEqual([call, uid, meth, args],
[Symbol('call'), uid, Symbol('dummy'), [55]])
return uid
def test_call_client_dummy_method(self):
self.handler.call('dummy', [55], self.callback, self.errback)
uid = self.check_call_client_dummy_method()
self.client_send('(return {0} 123)'.format(uid))
reply = self.callback_called_with.get(True, 1)
self.assertEqual(reply, 123)
def test_call_client_methods_info(self):
self.handler.methods(self.callback)
(methods, uid) = self.receive_message()
self.assertEqual(methods.value(), 'methods')
self.client_send('(return {0} ((dummy () "")))'.format(uid))
reply = self.callback_called_with.get(True, 1)
self.assertEqual(reply, [[Symbol('dummy'), [], ""]])
def client_send_error(self, ename, uid, message):
self.client_send('({0} {1} "{2}")'.format(ename, uid, message))
def check_call_client_error(self, ename, eclass, message=utf8("message")):
self.handler.call('dummy', [55], self.callback, self.errback)
uid = self.check_call_client_dummy_method()
self.client_send_error(ename, uid, message)
reply = self.errback_called_with.get(True, 1)
self.assertIsInstance(reply, eclass)
self.assertEqual(reply.args, (message,))
def test_call_client_return_error(self):
self.check_call_client_error('return-error', ReturnError)
def test_call_client_epc_error(self):
self.check_call_client_error('epc-error', EPCError)
def check_dont_send_error_back(self, ename, eclass,
message=utf8("message")):
self.handler.call('dummy', [55]) # no callbacks!
uid = self.check_call_client_dummy_method()
with logging_to_stdout(self.server.logger):
self.client_send_error(ename, uid, message)
try:
result = self.client.recv(1024)
self.assertEqual(result, '') # nothing goes to client
except socket.timeout:
pass
def test_dont_send_return_error_back(self):
self.check_dont_send_error_back('return-error', ReturnError)
def test_dont_send_epc_error_back(self):
self.check_dont_send_error_back('epc-error', EPCError)
def check_invalid_reply(self, make_reply, should_raise=EPCError):
bc = BlockingCallback()
self.handler.call('dummy', [55], **bc.cbs)
uid = self.check_call_client_dummy_method()
with logging_to_stdout(self.server.logger):
self.client_send(make_reply(uid))
self.assertRaises(should_raise, bc.result, timeout=self.timeout)
def test_invalid_return_not_enough_arguments(self):
self.check_invalid_reply('(return {0})'.format)
def test_invalid_return_too_many_arguments(self):
self.check_invalid_reply(
'(return {0} "value" "extra" "value")'.format)
def test_invalid_return_error_not_enough_arguments(self):
self.check_invalid_reply('(return-error {0})'.format, ReturnError)
def test_invalid_return_error_too_many_arguments(self):
self.check_invalid_reply(
'(return-error {0} "value" "extra" "value")'.format,
ReturnError)
def test_invalid_epc_error_not_enough_arguments(self):
self.check_invalid_reply('(epc-error {0})'.format)
def test_invalid_epc_error_too_many_arguments(self):
self.check_invalid_reply(
'(epc-error {0} "value" "extra" "value")'.format)
epc-0.0.5/epc/tests/utils.py 0000644 0001750 0001750 00000006155 12121243564 015547 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import os
import sys
import functools
import io
try:
import unittest
unittest.TestCase.assertIs
except AttributeError:
import unittest2 as unittest
from contextlib import contextmanager
from ..py3compat import Queue, PY3
from ..utils import newthread
@contextmanager
def mockedattr(object, name, replace):
"""
Mock `object.name` attribute using `replace`.
"""
original = getattr(object, name)
try:
setattr(object, name, replace)
yield
finally:
setattr(object, name, original)
def logging_to_stdout(logger):
# it assumes that 0-th hander is the only one stream handler...
return mockedattr(logger.handlers[0], 'stream', sys.stdout)
def streamio():
"""
Return `io.StringIO` for Python 3, otherwise `io.BytesIO`.
"""
if PY3:
return io.StringIO()
else:
return io.BytesIO()
class CaptureStdIO(object):
def __enter__(self):
self._orig_stdin = sys.stdin
self._orig_stdout = sys.stdout
self._orig_stderr = sys.stderr
self.stdin = sys.stdin = streamio()
self.stdout = sys.stdout = streamio()
self.stderr = sys.stderr = streamio()
return self
def __exit__(self, exc_type, exc_value, traceback):
sys.stdin = self._orig_stdin
sys.stdout = self._orig_stdout
sys.stderr = self._orig_stderr
def read_stdout(self):
self.stdout.seek(0)
return self.stdout.read()
def read_stderr(self):
self.stderr.seek(0)
return self.stderr.read()
class BaseTestCase(unittest.TestCase):
TRAVIS = os.getenv('TRAVIS')
if TRAVIS:
timeout = 10
else:
timeout = 1
def skip(reason):
from nose import SkipTest
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwds):
raise SkipTest("Skipping {0} because: {1}"
.format(func.__name__, reason))
return wrapper
return decorator
def post_mortem_in_thread(traceback):
"""
`pdb.post_mortem` that can be used in a daemon thread.
Put the following in the `except`-block::
import sys
from epc.tests.utils import post_mortem_in_thread
exc_info = sys.exc_info()
post_mortem_in_thread(exc_info[2])
"""
import pdb
blocker = Queue.Queue()
thread = newthread(target=blocker.get)
thread.daemon = False
thread.start()
pdb.post_mortem(traceback)
blocker.put(None)
epc-0.0.5/epc/tests/test_dispatcher.py 0000644 0001750 0001750 00000004164 12120634031 017562 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from ..core import EPCDispatcher
from .utils import BaseTestCase
class Dummy(object):
pass
class TestEPCDispatcher(BaseTestCase):
def setUp(self):
self.dispatcher = EPCDispatcher()
def test_register_module(self):
import os
self.dispatcher.register_instance(os)
self.assertIs(self.dispatcher.get_method('chmod'), os.chmod)
def test_register_module_with_dotted_names(self):
import os
self.dispatcher.register_instance(os, allow_dotted_names=True)
self.assertIs(self.dispatcher.get_method('path.join'), os.path.join)
def test_error_on_private_method_access(self):
obj = Dummy()
obj._private_method = lambda: None
obj.sub = Dummy()
obj.sub._private_attribute = Dummy()
obj.sub._private_attribute.some_method = lambda: None
self.dispatcher.register_instance(obj, allow_dotted_names=True)
self.assertRaises(AttributeError, self.dispatcher.get_method,
'_private_method')
self.assertRaises(AttributeError, self.dispatcher.get_method,
'obj.sub._private_attribute.some_method')
def test_instance_get_method(self):
always_me = lambda: None
obj = Dummy()
obj._get_method = lambda _: always_me
self.dispatcher.register_instance(obj)
self.assertIs(self.dispatcher.get_method('x'), always_me)
self.assertIs(self.dispatcher.get_method('y'), always_me)
epc-0.0.5/epc/tests/test_utils.py 0000644 0001750 0001750 00000003533 12120634031 016573 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from ..utils import ThreadedIterator, LockingDict
from .utils import BaseTestCase
class TestThreadedIterator(BaseTestCase):
def check_identity(self, iterable):
lst = list(iterable)
self.assertEqual(list(ThreadedIterator(lst)), lst)
def test_empty(self):
self.check_identity([])
def test_range_1(self):
self.check_identity(range(1))
def test_range_7(self):
self.check_identity(range(7))
class TestLockingDict(BaseTestCase):
def setUp(self):
self.ld = LockingDict()
def check_set_items(self, items):
for (k, v) in items:
self.ld[k] = v
self.assertEqual(dict(**self.ld), dict(items))
def test_simple_set_items(self):
self.check_set_items(dict(a=1, b=2, c=3).items())
def test_simple_del_items(self):
self.test_simple_set_items()
ld = self.ld
del ld['a']
del ld['b']
self.assertEqual(dict(**self.ld), dict(c=3))
def test_simple_pop_items(self):
self.test_simple_set_items()
ld = self.ld
self.assertEqual(ld.pop('a'), 1)
self.assertEqual(ld.pop('b'), 2)
self.assertEqual(dict(**self.ld), dict(c=3))
epc-0.0.5/epc/tests/__init__.py 0000644 0001750 0001750 00000001241 12120634031 016125 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
epc-0.0.5/epc/tests/test_py2py.py 0000644 0001750 0001750 00000015340 12121243564 016525 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import os
import nose
from ..client import EPCClient
from ..server import ThreadingEPCServer
from ..handler import ReturnError
from ..utils import newthread, callwith
from ..py3compat import Queue
from .utils import BaseTestCase, logging_to_stdout
def next_fib(x, fib):
if x < 2:
return x
return fib(x - 1) + fib(x - 2)
def fib(x):
return next_fib(x, fib)
class ThreadingPy2Py(object):
"""
A class to setup connected EPC server and client in one process.
This class is useful to use as a mix-in for test cases.
"""
def setup_connection(self, **kwds):
self.server = ThreadingEPCServer(('localhost', 0), **kwds)
self.server.daemon_threads = True
self.server_thread = newthread(self, target=self.server.serve_forever)
self.server_thread.start()
self.client_queue = q = Queue.Queue()
self.server.handle_client_connect = q.put
self.client = EPCClient(self.server.server_address, **kwds)
def teardown_connection(self):
self.client.close()
self.server.shutdown()
self.server.server_close()
def wait_until_client_is_connected(self):
if not self.client_ready:
self.client_queue.get(timeout=1)
self.client_ready = True
client_ready = False
class TestEPCPy2Py(ThreadingPy2Py, BaseTestCase):
def setUp(self):
ThreadingEPCServer.allow_reuse_address = True
self.setup_connection()
@self.client.register_function
@self.server.register_function
def echo(*a):
"""Return argument unchanged."""
return a
@self.client.register_function
@self.server.register_function
def bad_method(*_):
"""This is a bad method. Don't call!"""
raise ValueError("This is a bad method!")
@self.server.register_function
def ping_server(x):
return self.server.clients[0].call_sync('pong_client', [x])
@self.client.register_function
def pong_client(x):
return self.client.call_sync('echo', [x])
@self.client.register_function
def ping_client(x):
return self.client.call_sync('pong_server', [x])
@self.server.register_function
def pong_server(x):
return self.server.clients[0].call_sync('echo', [x])
@self.server.register_function
def fib_server(x):
c = self.server.clients[0].call_sync
return next_fib(x, lambda x: c('fib_client', [x]))
@self.client.register_function
def fib_client(x):
c = self.client.call_sync
return next_fib(x, lambda x: c('fib_server', [x]))
def tearDown(self):
self.teardown_connection()
def assert_call_return(self, call, method, args, reply, **kwds):
timeout = kwds.get('timeout', self.timeout)
self.assertEqual(call(method, args, timeout=timeout), reply)
def assert_client_return(self, method, args, reply, **kwds):
self.assert_call_return(self.client.call_sync,
method, args, reply, **kwds)
def assert_server_return(self, method, args, reply, **kwds):
self.wait_until_client_is_connected()
self.assert_call_return(self.server.clients[0].call_sync,
method, args, reply, **kwds)
def check_bad_method(self, call_sync):
cm = logging_to_stdout(self.server.logger)
call_sync = callwith(cm)(call_sync)
self.assertRaises(ReturnError, call_sync, 'bad_method', [55])
def test_client_calls_server_echo(self):
self.assert_client_return('echo', [55], [55])
def test_client_calls_server_bad_method(self):
self.check_bad_method(self.client.call_sync)
def test_server_calls_client_echo(self):
self.assert_server_return('echo', [55], [55])
def test_server_calls_client_bad_method(self):
self.wait_until_client_is_connected()
self.check_bad_method(self.server.clients[0].call_sync)
max_message_limit = int('f' * 6, 16) + 1 # 16MB
large_data_limit = max_message_limit \
/ float(os.getenv('PYEPC_TEST_LARGE_DATA_DISCOUNT', '128'))
large_data_limit = int(large_data_limit)
"""
Environment variable PYEPC_TEST_LARGE_DATA_DISCOUNT controls
how large "large data" must be. Default is ``2 ** 7`` times
smaller than the maximum message length (16 MB). Setting
it to 1 must *not* fail. However, it takes long time to finish
the test (typically 100 sec when I tried). Setting this value
to less than one (e.g., 0.9) *must* fail the tests.
"""
def check_large_data(self, assert_return):
margin = 100 # for parenthesis, "call", uid, etc.
data = "x" * (self.large_data_limit - margin)
timeout = self.timeout * 100
assert_return('echo', [data], [data], timeout=timeout)
def test_client_sends_large_data(self):
self.check_large_data(self.assert_client_return)
def test_server_sends_large_data(self):
self.check_large_data(self.assert_server_return)
def test_client_ping_pong(self):
self.assert_client_return('ping_server', [55], [55])
def test_server_ping_pong(self):
self.assert_server_return('ping_client', [55], [55])
def test_client_close_should_not_fail_even_if_not_used(self):
pass
fibonacci = list(map(fib, range(12)))
fibonacci_min = 2
"""
The Fibonacci test must succeeds at least until this index.
"""
def check_fib(self, assert_return, method):
try:
for (i, f) in enumerate(self.fibonacci):
assert_return(method, [i], f)
except Queue.Empty:
if i > self.fibonacci_min:
raise nose.SkipTest(
"Test for {0} fails at {1} (> {2}), but it's OK."
.format(method, i, self.fibonacci_min))
else:
raise # not OK
def test_client_fib(self):
self.check_fib(self.assert_client_return, 'fib_server')
def test_server_fib(self):
self.check_fib(self.assert_server_return, 'fib_client')
epc-0.0.5/epc/tests/test_client.py 0000644 0001750 0001750 00000011650 12120634031 016710 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import io
from sexpdata import Symbol
from ..client import EPCClient
from ..handler import encode_message, unpack_message, BlockingCallback, \
ReturnError, EPCError
from ..py3compat import Queue
from .utils import BaseTestCase
class FakeFile(object):
pass
class FakeSocket(object):
def __init__(self):
self._queue = Queue.Queue()
self._buffer = io.BytesIO()
self.sent_message = Queue.Queue()
self._alive = True
def makefile(self, mode, *_):
ff = FakeFile()
ff.closed = False
ff.close = lambda: None
ff.flush = lambda: None
if 'r' in mode:
ff.read = self.recv
return ff
elif 'w' in mode:
ff.write = self.sendall
return ff
def append(self, byte):
self._queue.put(byte)
def _pull(self):
byte = self._queue.get()
pos = self._buffer.tell()
self._buffer.write(byte)
self._buffer.seek(pos)
def recv(self, bufsize):
while True:
if not self._alive:
return ''
got = self._buffer.read(bufsize)
if got:
return got
# Do not go to the next loop until some byte is appended
# to the queue:
self._pull()
def sendall(self, string):
self.sent_message.put(string)
def close(self):
self._alive = False
self.append(''.encode('ascii'))
class TestClient(BaseTestCase):
def setUp(self):
self.fsock = FakeSocket()
self.next_reply = []
self.client = EPCClient(self.fsock)
@self.client.register_function
def echo(*a):
"""Return argument unchanged."""
return a
def tearDown(self):
self.client.socket.close() # connection is closed by server
def set_next_reply(self, *args):
self.next_reply.append(encode_message(*args))
def request(self, name, *args):
bc = BlockingCallback()
getattr(self.client, name)(*args, **bc.cbs)
self.fsock.append(self.next_reply.pop(0)) # reply comes after call!
return bc.result(timeout=self.timeout)
def sent_message(self):
raw = self.fsock.sent_message.get(timeout=self.timeout)
(name, uid, rest) = unpack_message(raw[6:])
if name == 'call':
rest[0] = rest[0].value()
return [name, uid] + rest
def check_sent_message(self, name, uid, args):
sent = self.sent_message()
self.assertEqual(sent, [name, uid] + list(args))
def check_return(self, desired_return, name, *args):
uid = 1
self.set_next_reply('return', uid, desired_return)
got = self.request(name, *args)
self.assertEqual(got, desired_return)
self.check_sent_message(name, uid, args)
def test_call_return(self):
self.check_return('some value', 'call', 'dummy', [1, 2, 3])
def test_methods_return(self):
self.check_return([[Symbol('dummy'), [], "document"]], 'methods')
def check_return_error(self, reply_name, name, *args):
uid = 1
reply = 'error value'
eclass = ReturnError if reply_name == 'return-error' else EPCError
error = eclass(reply)
self.set_next_reply(reply_name, uid, reply)
try:
self.request(name, *args)
assert False, 'self.client.{0}({1}) should raise an error' \
.format(name, args)
except Exception as got:
self.assertIsInstance(got, type(error))
self.assertEqual(got.args, error.args)
self.check_sent_message(name, uid, args)
def test_call_return_error(self):
self.check_return_error('return-error', 'call', 'dummy', [1, 2, 3])
def test_call_epc_error(self):
self.check_return_error('epc-error', 'call', 'dummy', [1, 2, 3])
def test_methods_return_error(self):
self.check_return_error('return-error', 'methods')
def test_methods_epc_error(self):
self.check_return_error('epc-error', 'methods')
def test_echo(self):
uid = 1
self.fsock.append(encode_message('call', uid, Symbol('echo'), [55]))
self.check_sent_message('return', uid, [[55]])
class TestClientClosedByClient(TestClient):
def tearDown(self):
self.client.close()
epc-0.0.5/epc/core.py 0000644 0001750 0001750 00000007540 12121243564 014174 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
from .py3compat import SimpleXMLRPCServer
def _get_logger():
"""
Generate a logger with a stream handler.
"""
logger = logging.getLogger('epc')
hndlr = logging.StreamHandler()
hndlr.setLevel(logging.INFO)
hndlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
logger.addHandler(hndlr)
return logger
_logger = _get_logger()
class EPCDispatcher:
# This class will be mixed with `SocketServer.TCPServer`,
# which is an old style class.
# see also: SimpleXMLRPCServer.SimpleXMLRPCDispatcher
def __init__(self):
self.funcs = {}
self.instance = None
def register_instance(self, instance, allow_dotted_names=False):
"""
Register an instance to respond to EPC requests.
:type instance: object
:arg instance:
An object with methods to provide to peer. If this
instance has `_get_method` method, EPC method name
resolution can be done by this method.
:type allow_dotted_names: bool
:arg allow_dotted_names:
If it is true, method names containing dots are supported.
They are resolved using `getattr` for each part of the
name as long as it does not start with '_'.
Unlike :meth:`register_function`, only one instance can
be registered.
"""
self.instance = instance
self.allow_dotted_names = allow_dotted_names
def register_function(self, function, name=None):
"""
Register function to be called from EPC client.
:type function: callable
:arg function: Function to publish.
:type name: str
:arg name: Name by which function is published.
This method returns the given `function` as-is, so that you
can use it as a decorator.
"""
if name is None:
name = function.__name__
self.funcs[name] = function
return function
def get_method(self, name):
"""
Get registered method callend `name`.
"""
try:
return self.funcs[name]
except KeyError:
try:
return self.instance._get_method(name)
except AttributeError:
return SimpleXMLRPCServer.resolve_dotted_attribute(
self.instance, name, self.allow_dotted_names)
class EPCCore(EPCDispatcher):
"""
Core methods shared by `EPCServer` and `EPCClient`.
"""
logger = _logger
def __init__(self, debugger, log_traceback):
EPCDispatcher.__init__(self)
self.set_debugger(debugger)
self.log_traceback = log_traceback
def set_debugger(self, debugger):
"""
Set debugger to run when an error occurs in published method.
You can also set debugger by passing `debugger` argument to
the class constructor.
:type debugger: {'pdb', 'ipdb', None}
:arg debugger: type of debugger.
"""
if debugger == 'pdb':
import pdb
self.debugger = pdb
elif debugger == 'ipdb':
import ipdb
self.debugger = ipdb
else:
self.debugger = debugger
epc-0.0.5/epc/handler.py 0000644 0001750 0001750 00000032022 12121243564 014652 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
import itertools
import threading
from sexpdata import loads, dumps, Symbol, String
from .py3compat import SocketServer, Queue
from .utils import autolog, LockingDict, newthread, callwith
class BaseRemoteError(Exception):
"""
All exceptions from remote method are derived from this class.
"""
class CallerUnknown(BaseRemoteError):
"""
Error raised in remote method, but caller of the method is unknown.
"""
class EPCError(BaseRemoteError):
"""
Error returned by `epc-error` protocol.
"""
class ReturnError(BaseRemoteError):
"""
Error returned by `return-error` protocol.
"""
class EPCErrorCallerUnknown(CallerUnknown, EPCError):
"""
Same as :class:`EPCError`, but caller is unknown.
"""
class ReturnErrorCallerUnknown(CallerUnknown, ReturnError):
"""
Same as :class:`ReturnError`, but caller is unknown.
"""
class EPCClosed(Exception):
"""
Trying to send to a closed socket.
"""
def encode_string(string):
data = string.encode('utf-8')
datalen = '{0:06x}'.format(len(data) + 1).encode()
return _JOIN_BYTES([datalen, data, _NEWLINE_BYTE])
_JOIN_BYTES = ''.encode().join
_NEWLINE_BYTE = '\n'.encode()
def encode_object(obj, **kwds):
return encode_string(dumps(obj, **kwds))
def encode_message(name, *args, **kwds):
return encode_object([Symbol(name)] + list(args), **kwds)
def unpack_message(bytes):
data = loads(bytes.decode('utf-8'))
return (data[0].value(), data[1], data[2:])
def itermessage(read):
while True:
head = read(6)
if not head:
return
length = int(head, 16)
data = read(length)
if len(data) < length:
raise ValueError('need {0}-length data; got {1}'
.format(length, len(data)))
yield data
class BlockingCallback(object):
def __init__(self):
self.queue = q = Queue.Queue()
self.callback = lambda x: q.put(('return', x))
self.errback = lambda x: q.put(('error', x))
self.cbs = {'callback': self.callback, 'errback': self.errback}
def result(self, timeout):
(rtype, reply) = self.queue.get(timeout=timeout)
if rtype == 'return':
return reply
else:
raise reply
class EPCCallManager:
Dict = LockingDict # FIXME: make it configurable from server class.
"""
Dictionary class used to store callbacks.
"""
def __init__(self):
self.callbacks = self.Dict()
counter = itertools.count(1)
self.get_uid = callwith(threading.Lock())(lambda: next(counter))
# Wrapping by threading.Lock is useless for non-threading
# handler. Probably it is better to make it optional.
def call(self, handler, name, args=[], callback=None, errback=None):
uid = self.get_uid()
self.callbacks[uid] = (callback, errback)
handler._send('call', uid, Symbol(name), args)
def methods(self, handler, callback=None, errback=None):
uid = self.get_uid()
self.callbacks[uid] = (callback, errback)
handler._send('methods', uid)
def handle_return(self, uid, reply):
try:
(callback, _) = self.callbacks.pop(uid)
except (KeyError, TypeError):
raise CallerUnknown(reply)
if callback is not None:
callback(reply)
def _handle_error_reply(self, uid, reply, eclass, notfound):
try:
(_, errback) = self.callbacks.pop(uid)
except (KeyError, TypeError):
raise notfound(reply)
error = eclass(reply)
if errback is None:
raise error
else:
errback(error)
def handle_return_error(self, uid, reply):
self._handle_error_reply(uid, reply, ReturnError,
ReturnErrorCallerUnknown)
def handle_epc_error(self, uid, reply):
self._handle_error_reply(uid, reply, EPCError,
EPCErrorCallerUnknown)
class EPCHandler(SocketServer.StreamRequestHandler):
# These attribute are defined in `SocketServer.BaseRequestHandler`
# self.server : an instance of `EPCServer`
# self.request :
# self.client_address
# These attribute are defined in `SocketServer.StreamRequestHandler`
# self.connection : = self.request
# self.rfile : stream from client
# self.wfile : stream to client
@property
def logger(self):
return self.server.logger
@autolog('debug')
def setup(self):
SocketServer.StreamRequestHandler.setup(self)
self.callmanager = EPCCallManager()
self.server.add_client(self)
@autolog('debug')
def finish(self):
try:
SocketServer.StreamRequestHandler.finish(self)
finally:
self.server.remove_client(self)
def _rfile_read_safely(self, size):
try:
return self.rfile.read(size)
except (AttributeError, ValueError):
if self.rfile.closed:
# Calling read on closed socket raises
# AttributeError in 2.x and ValueError in 3.x.
# http://bugs.python.org/issue9177
raise StopIteration
else:
raise # if not, just re-raise it.
def _recv(self):
self.logger.debug('receiving...')
for data in itermessage(self._rfile_read_safely):
self.logger.debug(
'received: length = %r; data = %r', len(data), data)
yield data
self.logger.debug('receiving...')
@autolog('debug')
def _send(self, *args):
string = encode_message(*args)
try:
self.wfile.write(string)
except (AttributeError, ValueError):
# See also: :meth:`_rfile_read_safely`
raise EPCClosed
@autolog('debug')
def handle(self):
for sexp in self._recv():
self._handle(sexp)
@autolog('debug')
def _handle(self, sexp):
uid = undefined = [] # default: nil
try:
(name, uid, args) = unpack_message(sexp)
pyname = name.replace('-', '_')
getattr(self, '_validate_{0}'.format(pyname))(uid, args)
handler = getattr(self, '_handle_{0}'.format(pyname))
reply = handler(uid, *args)
if reply is not None:
self._send(*reply)
except Exception as err:
if self.handle_error(err):
return
if self.server.debugger or self.server.log_traceback:
exc_info = sys.exc_info()
self.logger.error('Unexpected error', exc_info=exc_info)
if self.server.debugger:
self.server.debugger.post_mortem(exc_info[2])
name = 'epc-error' if uid is undefined else 'return-error'
self._send(name, uid, repr(err))
@autolog('debug')
def _handle_call(self, uid, meth, args):
# See: `epc:handler-called-method`
name = meth.value()
try:
func = self.server.get_method(name)
except AttributeError:
return ['epc-error', uid,
"EPC-ERROR: No such method : {0}".format(name)]
return ['return', uid, func(*args)]
def _handle_methods(self, uid):
return ['return', uid, [
(Symbol(name), [], String(func.__doc__ or ""))
# FIXNE: implement arg-specs
for (name, func)
in self.server.funcs.items()]]
def _handle_return(self, uid, reply):
self.callmanager.handle_return(uid, reply)
def _handle_return_error(self, uid, reply=None, *_):
self.callmanager.handle_return_error(uid, reply)
def _handle_epc_error(self, uid, reply=None, *_):
self.callmanager.handle_epc_error(uid, reply)
_epc_error_template = \
"(%s %d ...): Got %s arguments in the reply: %r"
def _validate_call(self, uid, args, num_expect=2, name='call'):
len_args = len(args)
if len_args == num_expect:
return
elif len_args < num_expect:
message = 'Not enough arguments {0!r}'.format(args)
else:
message = 'Too many arguments {0!r}'.format(args)
self._send("epc-error", uid, message)
raise EPCError('({0} {1} ...): {2}'.format(name, uid, message))
def _validate_methods(self, uid, args):
self._validate_call(uid, args, 0, 'methods')
def _validate_return(self, uid, args):
len_args = len(args)
error = lambda x: self._epc_error_template % ('return', uid, x, args)
if len_args == 0:
message = error('not enough')
elif len_args > 1:
message = error('too many')
else:
return
self.logger.error(message)
self._handle_epc_error(uid, message)
raise EPCError(message)
def _validate_return_error(self, uid, args):
self._log_extra_argument_error('return-error', uid, args)
def _validate_epc_error(self, uid, args):
self._log_extra_argument_error('epc-error', uid, args)
def _log_extra_argument_error(self, name, uid, args):
if len(args) > 1:
self.logger.error(self._epc_error_template,
'return-error', uid, 'too many', args)
def handle_error(self, err):
"""
Handle error which is not handled by errback.
:type err: Exception
:arg err: An error not handled by other mechanisms.
:rtype: boolean
Return True from this function means that error is properly
handled, so the error is not sent to client. Do not confuse
this with :meth:`SocketServer.BaseServer.handle_error`. This
method is for handling error for each client, not for entire
server. Default implementation logs the error and returns
True if the error is coming from remote [#]_ or returns False
otherwise. Therefore, only the error occurs in this handler
class is sent to remote.
.. [#] More specifically, it returns True if `err` is an
instance of :class:`BaseRemoteError` or :class:`EPCClosed`.
"""
self.logger.error(repr(err))
if isinstance(err, (BaseRemoteError, EPCClosed)):
# BaseRemoteError: do not send error back
# EPCClosed: no exception from thread
return True
def call(self, name, *args, **kwds):
"""
Call method connected to this handler.
:type name: str
:arg name: Method name to call.
:type args: list
:arg args: Arguments for remote method to call.
:type callback: callable
:arg callback: A function to be called with returned value of
the remote method.
:type errback: callable
:arg errback: A function to be called with an error occurred
in the remote method. It is either an instance
of :class:`ReturnError` or :class:`EPCError`.
"""
self.callmanager.call(self, name, *args, **kwds)
def methods(self, *args, **kwds):
"""
Request info of callable remote methods.
Arguments for :meth:`call` except for `name` can be applied to
this function too.
"""
self.callmanager.methods(self, *args, **kwds)
@staticmethod
def _blocking_request(call, timeout, *args):
bc = BlockingCallback()
call(*args, **bc.cbs)
return bc.result(timeout=timeout)
def call_sync(self, name, args, timeout=None):
"""
Blocking version of :meth:`call`.
:type name: str
:arg name: Remote function name to call.
:type args: list
:arg args: Arguments passed to the remote function.
:type timeout: int or None
:arg timeout: Timeout in second. None means no timeout.
If the called remote function raise an exception, this method
raise an exception. If you give `timeout`, this method may
raise an `Empty` exception.
"""
return self._blocking_request(self.call, timeout, name, args)
def methods_sync(self, timeout=None):
"""
Blocking version of :meth:`methods`. See also :meth:`call_sync`.
"""
return self._blocking_request(self.methods, timeout)
class ThreadingEPCHandler(EPCHandler):
def _handle(self, sexp):
newthread(self, target=EPCHandler._handle, args=(self, sexp)).start()
epc-0.0.5/epc/__init__.py 0000644 0001750 0001750 00000007506 12161373543 015012 0 ustar dogsleg dogsleg # [[[cog import cog; cog.outl('"""\n%s\n"""' % file('../README.rst').read())]]]
"""
EPC (RPC stack for Emacs Lisp) for Python
=========================================
Links:
* `Documentation `_ (at Read the Docs)
* `Repository `_ (at GitHub)
* `Issue tracker `_ (at GitHub)
* `PyPI `_
* `Travis CI `_ |build-status|
Other resources:
* `kiwanami/emacs-epc `_
(Client and server implementation in Emacs Lisp and Perl.)
* `tkf/emacs-jedi `_
(Python completion for Emacs using EPC server.)
.. |build-status|
image:: https://secure.travis-ci.org/tkf/python-epc.png
?branch=master
:target: http://travis-ci.org/tkf/python-epc
:alt: Build Status
What is this?
-------------
EPC is an RPC stack for Emacs Lisp and Python-EPC is its server side
and client side implementation in Python. Using Python-EPC, you can
easily call Emacs Lisp functions from Python and Python functions from
Emacs. For example, you can use Python GUI module to build widgets
for Emacs (see `examples/gtk/server.py`_ for example).
Python-EPC is tested against Python 2.6, 2.7 and 3.2.
Install
-------
To install Python-EPC and its dependency sexpdata_, run the following
command.::
pip install epc
.. _sexpdata: https://github.com/tkf/sexpdata
Usage
-----
Save the following code as ``my-server.py``.
(You can find functionally the same code in `examples/echo/server.py`_)::
from epc.server import EPCServer
server = EPCServer(('localhost', 0))
@server.register_function
def echo(*a):
return a
server.print_port()
server.serve_forever()
And then run the following code from Emacs.
This is a stripped version of `examples/echo/client.el`_ included in
Python-EPC repository_.::
(require 'epc)
(defvar my-epc (epc:start-epc "python" '("my-server.py")))
(deferred:$
(epc:call-deferred my-epc 'echo '(10))
(deferred:nextc it
(lambda (x) (message "Return : %S" x))))
(message "Return : %S" (epc:call-sync my-epc 'echo '(10 40)))
.. _examples/echo/server.py:
https://github.com/tkf/python-epc/blob/master/examples/echo/server.py
.. _examples/echo/client.el:
https://github.com/tkf/python-epc/blob/master/examples/echo/client.el
If you have carton_ installed, you can run the above sample by
simply typing the following commands::
make elpa # install EPC in a separated environment
make run-sample # run examples/echo/client.el
.. _carton: https://github.com/rejeep/carton
For example of bidirectional communication and integration with GTK,
see `examples/gtk/server.py`_. You can run this example by::
make elpa
make run-gtk-sample # run examples/gtk/client.el
.. _examples/gtk/server.py:
https://github.com/tkf/python-epc/blob/master/examples/gtk/server.py
License
-------
Python-EPC is licensed under GPL v3.
See COPYING for details.
"""
# [[[end]]]
# Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
__version__ = '0.0.5'
__author__ = 'Takafumi Arakaki'
__license__ = 'GNU General Public License v3 (GPLv3)'
epc-0.0.5/epc/client.py 0000644 0001750 0001750 00000010315 12121243564 014514 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from .py3compat import Queue
from .utils import ThreadedIterator, newthread
from .core import EPCCore
from .handler import ThreadingEPCHandler
class EPCClientHandler(ThreadingEPCHandler):
# In BaseRequestHandler, everything happen in `.__init__()`.
# Let's defer it to `.start()`.
def __init__(self, *args):
self._args = args
self._ready = Queue.Queue()
def start(self):
ThreadingEPCHandler.__init__(self, *self._args)
def setup(self):
ThreadingEPCHandler.setup(self)
self._ready.put(True)
def wait_until_ready(self):
self._ready.get()
def _recv(self):
self._recv_iter = ThreadedIterator(ThreadingEPCHandler._recv(self))
return self._recv_iter
class EPCClient(EPCCore):
"""
EPC client class to call remote functions and serve Python functions.
>>> client = EPCClient()
>>> client.connect(('localhost', 9999)) #doctest: +SKIP
>>> client.call_sync('echo', [111, 222, 333]) #doctest: +SKIP
[111, 222, 333]
To serve Python functions, you can use :meth:`register_function`.
>>> client.register_function(str.upper)
:meth:`register_function` can be used as a decorator.
>>> @client.register_function
... def add(x, y):
... return x + y
Also, you can initialize client and connect to the server by one line.
>>> client = EPCClient(('localhost', 9999)) #doctest: +SKIP
.. method:: call
Alias of :meth:`epc.server.EPCHandler.call`.
.. method:: call_sync
Alias of :meth:`epc.server.EPCHandler.call_sync`.
.. method:: methods
Alias of :meth:`epc.server.EPCHandler.methods`.
.. method:: methods_sync
Alias of :meth:`epc.server.EPCHandler.methods_sync`.
"""
thread_daemon = True
def __init__(self, socket_or_address=None,
debugger=None, log_traceback=False):
if socket_or_address is not None:
self.connect(socket_or_address)
EPCCore.__init__(self, debugger, log_traceback)
def connect(self, socket_or_address):
"""
Connect to server and start serving registered functions.
:type socket_or_address: tuple or socket object
:arg socket_or_address: A ``(host, port)`` pair to be passed
to `socket.create_connection`, or
a socket object.
"""
if isinstance(socket_or_address, tuple):
import socket
self.socket = socket.create_connection(socket_or_address)
else:
self.socket = socket_or_address
# This is what BaseServer.finish_request does:
address = None # it is not used, so leave it empty
self.handler = EPCClientHandler(self.socket, address, self)
self.call = self.handler.call
self.call_sync = self.handler.call_sync
self.methods = self.handler.methods
self.methods_sync = self.handler.methods_sync
self.handler_thread = newthread(self, target=self.handler.start)
self.handler_thread.daemon = self.thread_daemon
self.handler_thread.start()
self.handler.wait_until_ready()
def close(self):
"""Close connection."""
try:
self.handler._recv_iter.stop()
except AttributeError:
# Do not fail to close even if the client is never used.
pass
def _ignore(*_):
""""Do nothing method for `EPCHandler`."""
add_client = _ignore
remove_client = _ignore
epc-0.0.5/epc/py3compat.py 0000644 0001750 0001750 00000002651 12120634031 015151 0 ustar dogsleg dogsleg # Copyright (C) 2012- Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
PY3 = (sys.version_info[0] >= 3)
try:
import SocketServer
except:
import socketserver as SocketServer
try:
import SimpleXMLRPCServer
except:
import xmlrpc.server as SimpleXMLRPCServer
try:
import Queue
except:
import queue as Queue
try:
from contextlib import nested
except ImportError:
from contextlib import contextmanager
@contextmanager
def nested(*managers):
if managers:
with managers[0] as ctx:
with nested(*managers[1:]) as rest:
yield (ctx,) + rest
else:
yield ()
if PY3:
utf8 = lambda s: s
else:
utf8 = lambda s: s.decode('utf-8')
utf8.__doc__ = """
Decode a raw string into unicode object. Do nothing in Python 3.
"""