epc-0.0.5/0000755000175000017500000000000012161373575012127 5ustar dogslegdogslegepc-0.0.5/setup.py0000644000175000017500000000176412161372713013642 0ustar dogslegdogslegfrom 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-INFO0000644000175000017500000001112512161373575013224 0ustar dogslegdogslegMetadata-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/0000755000175000017500000000000012161373575012676 5ustar dogslegdogslegepc-0.0.5/epc/server.py0000644000175000017500000001670012121243564014550 0ustar dogslegdogsleg# 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.py0000644000175000017500000001042712120634031014372 0ustar dogslegdogsleg# 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/0000755000175000017500000000000012161373575014040 5ustar dogslegdogslegepc-0.0.5/epc/tests/test_server.py0000644000175000017500000003012212121243564016743 0ustar dogslegdogsleg# -*- 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.py0000644000175000017500000000615512121243564015547 0ustar dogslegdogsleg# 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.py0000644000175000017500000000416412120634031017562 0ustar dogslegdogsleg# 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.py0000644000175000017500000000353312120634031016573 0ustar dogslegdogsleg# 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__.py0000644000175000017500000000124112120634031016125 0ustar dogslegdogsleg# 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.py0000644000175000017500000001534012121243564016525 0ustar dogslegdogsleg# 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.py0000644000175000017500000001165012120634031016710 0ustar dogslegdogsleg# 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.py0000644000175000017500000000754012121243564014174 0ustar dogslegdogsleg# 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.py0000644000175000017500000003202212121243564014652 0ustar dogslegdogsleg# 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__.py0000644000175000017500000000750612161373543015012 0ustar dogslegdogsleg# [[[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.py0000644000175000017500000001031512121243564014514 0ustar dogslegdogsleg# 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.py0000644000175000017500000000265112120634031015151 0ustar dogslegdogsleg# 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. """