From commits-noreply at bitbucket.org Thu Oct 1 12:08:05 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 1 Oct 2009 10:08:05 +0000 (UTC) Subject: [execnet-commit] execnet commit bf9200d244db: remove superflous old tags Message-ID: <20091001100805.D5DF0710A9@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254391159 -7200 # Node ID bf9200d244dbf8a12de6a499cae2b65413f7915d # Parent 2e79990d3390d1e19769cd1749fa474b17d9e71c remove superflous old tags --- a/.hgtags +++ b/.hgtags @@ -1,7 +0,0 @@ -6b41b610bcde267f4e8bbcaebbfc5f5dd48eb6bb 1.0.0 -6b41b610bcde267f4e8bbcaebbfc5f5dd48eb6bb 1.0.0b8 -6b41b610bcde267f4e8bbcaebbfc5f5dd48eb6bb 1.0.0b9 -8a4d9daf73aab2b1ea8f77d06bcf11b939eca70f 1.0.0b3 -9d205ffed94fa0318548a7e727fd67d7dda6f7fa 1.0.0b6 -a598d3e3e0196c73665377921ff22f0c4f74b2a5 1.0.1 -a598d3e3e0196c73665377921ff22f0c4f74b2a5 1.0.2 From commits-noreply at bitbucket.org Thu Oct 1 18:27:14 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 1 Oct 2009 16:27:14 +0000 (UTC) Subject: [execnet-commit] execnet commit 50df8d9326a2: [mq]: doc Message-ID: <20091001162714.8B01D71099@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254392265 -7200 # Node ID 50df8d9326a2a6d9514fac0a22535c6e579d48d7 # Parent bf9200d244dbf8a12de6a499cae2b65413f7915d [mq]: doc From commits-noreply at bitbucket.org Thu Oct 1 18:27:16 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 1 Oct 2009 16:27:16 +0000 (UTC) Subject: [execnet-commit] execnet commit 9c03758bf580: * improved documentation with graphics, new site layout Message-ID: <20091001162716.99E5C710AD@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254414087 -7200 # Node ID 9c03758bf58043d68850d41075d8b0d321e93a58 # Parent 50df8d9326a2a6d9514fac0a22535c6e579d48d7 * improved documentation with graphics, new site layout * remove remaining usages of py lib * create setup.py and other missing bits --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -236,24 +236,24 @@ class SocketGateway(Gateway): super(SocketGateway, self).__init__(io=io) def new_remote(cls, gateway, hostport=None): - """ return a new (connected) socket gateway, instatiated - indirectly through the given 'gateway'. + """ return a new (connected) socket gateway, + instantiated through the given 'gateway'. """ if hostport is None: host, port = ('', 0) # XXX works on all platforms? else: host, port = hostport - import py - mydir = py.path.local(__file__).dirpath() - socketserverbootstrap = py.code.Source( - mydir.join('script', 'socketserver.py').read('r'), """ + + mydir = os.path.dirname(__file__) + socketserver = os.path.join(mydir, 'script', 'socketserver.py') + socketserverbootstrap = "\n".join([ + open(socketserver, 'r').read(), """if 1: import socket sock = bind_and_listen((%r, %r)) port = sock.getsockname() channel.send(port) startserver(sock) - """ % (host, port) - ) + """ % (host, port)]) # execute the above socketserverbootstrap on the other side channel = gateway.remote_exec(socketserverbootstrap) (realhost, realport) = channel.receive() Binary file doc/_static/execnet.png has changed Binary file doc/_static/jython.png has changed --- /dev/null +++ b/doc/_templates/layout.html @@ -0,0 +1,14 @@ +{% extends "!layout.html" %} + +{% block rootrellink %} +
  • home | 
  • +
  • issues | 
  • +
  • pypi | 
  • +
  • Documentation»
  • +{% endblock %} + +{% block header %} +
    +execnet +
    +{% endblock %} --- a/testing/test_rsync.py +++ b/testing/test_rsync.py @@ -1,5 +1,5 @@ import py -from execnet.rsync import RSync +from execnet import RSync import execnet def pytest_funcarg__gw1(request): --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -12,3 +12,4 @@ from execnet.gateway import PopenGateway from execnet.gateway import HostNotFound from execnet.xspec import makegateway, XSpec from execnet.multi import MultiGateway,MultiChannel +from execnet.rsync import RSync Binary file doc/_static/basic1.png has changed Binary file doc/_static/python-powered-h-70x91.png has changed --- a/doc/index.txt +++ b/doc/index.txt @@ -1,49 +1,57 @@ -============================================================================== -execnet: Elastic Python Deployment. -============================================================================== +.. image:: _static/pythonring.png + :align: right -the execnet package allows to: +Welcome to execnet! +========================= -* ad-hoc instantiate local or remote Python Interpreters +The execnet package allows to: + +* instantiate local/remote Python Interpreters * send code for execution to one or many Interpreters -* send and receive data between Interpreters through channels +* send and receive data between codeInterpreters through channels -execnet uses a **zero-install self-bootstrapping** technique such that -no manual installation steps are required on remote places. A -basic connection method and a Python executable suffices. The following -interpreters can be connected with each other: +execnet performs **zero-install bootstrapping** into other interpreters; +package installation is only required at the initiating side. execnet enables +interoperation between CPython 2.4-3.1, Jython 2.5 and PyPy 1.1 and works +well on Windows, Linux and OSX systems. -* |cpython| CPython versions, 2.4, 2.5, 2.6, 2.7, 3.0 and 3.1 -* |jython| Jython 2.5.1 -* |pypy| PyPy 1.1 and trunk versions +execnet was written and is maintained by Holger Krekel with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +Getting Started +==================== -.. |jython| image:: _static/jython.png - :target: http://jython.org - :align: middle +To install a public `pypi release`_ via `easy_install`_:: -.. |cpython| image:: _static/python-powered-h-70x91.png - :target: http://python.org - :align: middle + easy_install -U execnet -.. |pypy| image:: _static/py-web1.png - :width: 75 - :height: 55 - :target: http://pypy.org - :align: middle +To work from the development tree checkout the `execnet mercurial repository`_. -Contents: +Next checkout the basic api and examples: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 basics examples + changelog -Indices and tables -================== +Contact channels +=========================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +You are welcome to: +* join and post to the `execnet-dev`_ mailing list or `open an issue`_ +* join the #pylib IRC channel on Freenode +* subscribe to the `execnet-commit`_ mailing list + +.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit +.. _`open an issue`: http://bitbucket.org/hpk42/execnet/issues/ + +.. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall +.. _`execnet mercurial repository`: http://bitbucket.org/hpk42/execnet/ +.. _`pypi release`: http://pypi.python.org/pypi/execnet + + + + --- a/execnet/rsync.py +++ b/execnet/rsync.py @@ -3,10 +3,20 @@ 1:N rsync implemenation on top of execne (c) 2006-2009, Armin Rigo, Holger Krekel, Maciej Fijalkowski """ -import py, os, stat +import os, stat -md5 = py.builtin._tryimport('hashlib', 'md5').md5 -Queue = py.builtin._tryimport('queue', 'Queue').Queue +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +try: + from queue import Queue +except ImportError: + from Queue import Queue + +remote_file = os.path.join(os.path.dirname(__file__), 'rsync_remote.py') +REMOTE_SOURCE = open(remote_file, 'r').read() + "\nf()" class RSync(object): """ This class allows to send a directory structure (recursively) @@ -20,7 +30,7 @@ class RSync(object): def __init__(self, sourcedir, callback=None, verbose=True): self._sourcedir = str(sourcedir) self._verbose = verbose - assert callback is None or py.builtin.callable(callback) + assert callback is None or hasattr(callback, '__call__') self._callback = callback self._channels = {} self._receivequeue = Queue() @@ -134,10 +144,9 @@ class RSync(object): def add_target(self, gateway, destdir, finishedcallback=None, **options): - """ Adds a remote target specified via a 'gateway' + """ Adds a remote target specified via a gateway and a remote destination directory. """ - assert finishedcallback is None or py.builtin.callable(finishedcallback) for name in options: assert name in ('delete',) def itemcallback(req): @@ -196,6 +205,3 @@ class RSync(object): else: raise ValueError("cannot sync %r" % (path,)) -REMOTE_SOURCE = py.path.local(__file__).dirpath().\ - join('rsync_remote.py').open().read() + "\nf()" - --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +""" +the execnet package allows to: + +* instantiate local/remote Python Interpreters +* send code for execution to one or many Interpreters +* send and receive data between codeInterpreters through channels + +execnet performs **zero-install bootstrapping** into other interpreters; +package installation is only required at the initiating side. execnet enables +interoperation between CPython 2.4-3.1, Jython 2.5 and PyPy 1.1 and works +well on Windows, Linux and OSX systems. + +execnet was written and is maintained by Holger Krekel with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +""" + +import os, sys +from distutils.core import setup + +from execnet import __version__ + +def main(): + setup( + name='execnet', + description='execnet: elastic Python deployment', + long_description = __doc__, + version= __version__, + url='http://codespeak.net/execnet', + license='GPL V2 or later', + platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], + author='holger krekel and others', + author_email='holger at merlinux.eu', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Libraries', + 'Topic :: System :: Distributed Computing', + 'Topic :: System :: Networking', + 'Programming Language :: Python'], + packages=['execnet', 'execnet.script'], + ) + +if __name__ == '__main__': + main() --- a/doc/basics.txt +++ b/doc/basics.txt @@ -4,7 +4,11 @@ execnet API in a nutshell execnet allows to create **gateways** which establish a self-bootstrapping connection to another -local or remote Python interpreter. +local or remote Python interpreter. Through this +connection you can execute code on the other side +and maintain a data exchange via channels. + +.. image:: _static/basic1.png Gateways: connecting to another Python Interpreter =================================================== @@ -49,7 +53,7 @@ Here is an self-contained example for re .. _`channel-api`: .. _`exchange data`: -Exchanging data with Channels +Channels: exchanging data with remote code ======================================================= .. currentmodule:: execnet.gateway_base @@ -106,3 +110,20 @@ Here are some examples for valid specifi process that listens on 192.168.1.4:8888; current dir will be the 'pyexecnet-cache' sub directory which is used a default for all remote processes. + +rsync: synchronise filesystem with remote +=============================================================== + +``execnet`` implements a simple efficient rsyncing protocol. +Here is a basic example:: + + rsync = execnet.RSync('/tmp/source') + gw = execnet.PopenGateway() + rsync.add_target(gw, '/tmp/dest') + rsync.send() + +More info on the RSync class: + +.. currentmodule:: execnet +.. autoclass:: RSync + :members: add_target,send --- /dev/null +++ b/doc/_templates/indexsidebar.html @@ -0,0 +1,21 @@ +

    Download

    +{% if version.endswith('(hg)') %} +

    This documentation is for version {{ version }}, which is + not released yet.

    +

    You can use it from the + Mercurial repo or look for + released versions in the Python + Package Index.

    +{% else %} +

    Current version: {{ version }}

    +

    Get execnet from the Python Package +Index, or install it with:

    +
    easy_install -U execnet
    +{% endif %} + +

    Questions? Suggestions?

    + +

    Join + execnet-dev mailing list

    +

    open an issue.

    +

    come to #pylib on FreeNode

    --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ -* use sphinx for documentation, improve docs and docstrings +1.0.0alpha1 +---------------------------- + +* improve documentation, new website + +* use sphinx for documentation, added boilerplate files and setup.py * fixes for standalone usage, adding boilerplate files -* imported py/execnet and related bits +* imported py/execnet and made it work standalone Binary file doc/_static/pythonring.png has changed --- /dev/null +++ b/doc/changelog.txt @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +execnet CHANGELOG +******************** + +.. include:: ../CHANGELOG --- a/execnet/script/socketserver.py +++ b/execnet/script/socketserver.py @@ -20,8 +20,6 @@ if debug: # and not os.isatty(sys.stdin f = open('/tmp/execnet-socket-pyout.log', 'w') old = sys.stdout, sys.stderr sys.stdout = sys.stderr = f - #import py - #compile = py.code.compile def print_(*args): print(" ".join(str(arg) for arg in args)) Binary file doc/_static/py-web1.png has changed --- a/doc/conf.py +++ b/doc/conf.py @@ -74,7 +74,7 @@ exclude_trees = ['_build'] # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. @@ -93,6 +93,12 @@ pygments_style = 'sphinx' # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'sphinxdoc' +#html_index = 'index.html' +#html_sidebars = {'index': 'indexsidebar.html', +# 'basics': 'indexsidebar.html', +#} +#html_additional_pages = {'index': 'index.html'} + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. @@ -130,15 +136,8 @@ html_static_path = ['_static'] # typographically correct entities. #html_use_smartypants = True -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - # If false, no module index is generated. -html_use_modindex = True +html_use_modindex = False # If false, no index is generated. #html_use_index = True @@ -147,7 +146,7 @@ html_use_modindex = True #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +html_show_sourcelink = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the --- a/.hgignore +++ b/.hgignore @@ -1,1 +1,4 @@ doc/_build +build/ +execnet.egg-info/ +dist/ From commits-noreply at bitbucket.org Fri Oct 2 12:59:53 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 2 Oct 2009 10:59:53 +0000 (UTC) Subject: [execnet-commit] execnet commit 70834ed6a5b0: * typos and fixes Message-ID: <20091002105953.F1A7A71099@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254415920 -7200 # Node ID 70834ed6a5b02d8a2b947a4b3142002dc3093650 # Parent 9c03758bf58043d68850d41075d8b0d321e93a58 * typos and fixes * add google analytics key --- a/doc/index.txt +++ b/doc/index.txt @@ -8,14 +8,14 @@ The execnet package allows to: * instantiate local/remote Python Interpreters * send code for execution to one or many Interpreters -* send and receive data between codeInterpreters through channels +* send and receive data through channels execnet performs **zero-install bootstrapping** into other interpreters; package installation is only required at the initiating side. execnet enables interoperation between CPython 2.4-3.1, Jython 2.5 and PyPy 1.1 and works well on Windows, Linux and OSX systems. -execnet was written and is maintained by Holger Krekel with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +execnet was written and is maintained by `Holger Krekel`_ with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. Getting Started ==================== @@ -44,6 +44,7 @@ You are welcome to: * join the #pylib IRC channel on Freenode * subscribe to the `execnet-commit`_ mailing list +.. _`Holger Krekel`: http://tetamap.wordpress.com .. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev .. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit .. _`open an issue`: http://bitbucket.org/hpk42/execnet/issues/ --- /dev/null +++ b/doc/docindex.txt @@ -0,0 +1,10 @@ + +Documentation Index +============================ + +.. toctree:: + :maxdepth: 2 + + basics + examples + changelog --- a/doc/examples.txt +++ b/doc/examples.txt @@ -2,6 +2,33 @@ execnet examples ============================================================================== +Work with Java objects from CPython +---------------------------------------- + +Use your CPython interpreter to connect to a Jython_ interpreter +and work with Java types:: + + import execnet + gw = execnet.PopenGateway("jython") + channel = gw.remote_exec(""" + from java.util import Vector + v = Vector() + v.add('aaa') + v.add('bbb') + for val in v: + channel.send(val) + """) + + for item in channel: + print (item) + +will print on the CPython side:: + + aaa + bbb + +.. _Jython: http://www.jython.org + Compare cwd() of Popen Gateways ---------------------------------------- --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -2,9 +2,11 @@ {% block rootrellink %}
  • home | 
  • +
  • api | 
  • +
  • examples | 
  • issues | 
  • pypi | 
  • -
  • Documentation»
  • +
  • docindex»
  • {% endblock %} {% block header %} @@ -12,3 +14,16 @@ execnet {% endblock %} + +{% block footer %} +{{ super() }} + + +{% endblock %} From commits-noreply at bitbucket.org Fri Oct 2 17:00:18 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 2 Oct 2009 15:00:18 +0000 (UTC) Subject: [execnet-commit] execnet commit fa4978439179: alpha2: some fixes triggered by making pylib and py.test work Message-ID: <20091002150018.7521E710A3@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254494678 -7200 # Node ID fa4978439179100c8a65b2e59cd21d9dcceecd78 # Parent 70834ed6a5b02d8a2b947a4b3142002dc3093650 alpha2: some fixes triggered by making pylib and py.test work --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ -1.0.0alpha1 +1.0.0alpha2 ---------------------------- * improve documentation, new website --- a/execnet/multi.py +++ b/execnet/multi.py @@ -3,10 +3,9 @@ Support for working with multiple channe (c) 2008-2009, Holger Krekel and others """ -try: - import queue -except ImportError: - import Queue as queue + +import sys +from execnet.gateway_base import queue, reraise NO_ENDMARKER_WANTED = object() @@ -65,4 +64,4 @@ class MultiChannel: if first is None: first = sys.exc_info() if first: - py.builtin._reraise(first[0], first[1], first[2]) + reraise(*first) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -41,15 +41,14 @@ except ImportError: import Queue as queue if sys.version_info > (3, 0): - exec("""def do_exec(co, loc): - exec(co, loc)""") + exec("def do_exec(co, loc): exec(co, loc)\n" + "def reraise(cls, val, tb): raise val\n") unicode = str else: - exec("""def do_exec(co, loc): - exec co in loc""") + exec("def do_exec(co, loc): exec co in loc\n" + "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str - def str(*args): raise EnvironmentError( "use unicode or bytes, not cross-python ambigous 'str'") --- a/execnet/threadpool.py +++ b/execnet/threadpool.py @@ -17,7 +17,6 @@ if sys.version_info >= (3,0): else: exec ("def reraise(cls, val, tb): raise cls, val, tb") - ERRORMARKER = object() class Reply(object): --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -5,7 +5,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0alpha" +__version__ = "1.0.0alpha2" __author__ = "holger krekel and others" from execnet.gateway import PopenGateway, SocketGateway, SshGateway --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,6 @@ doc/_build build/ execnet.egg-info/ dist/ + +syntax:glob +.py From commits-noreply at bitbucket.org Fri Oct 16 22:58:40 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 16 Oct 2009 20:58:40 +0000 (UTC) Subject: [execnet-commit] execnet commit ba43d965f3e4: ignore pyc files Message-ID: <20091016205840.52EA17EF10@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1254566104 -7200 # Node ID ba43d965f3e4b6b88223295d1e63bbc7e203c8eb # Parent fa4978439179100c8a65b2e59cd21d9dcceecd78 ignore pyc files --- a/.hgignore +++ b/.hgignore @@ -4,4 +4,4 @@ execnet.egg-info/ dist/ syntax:glob -.py +*.pyc From commits-noreply at bitbucket.org Fri Oct 16 22:58:41 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 16 Oct 2009 20:58:41 +0000 (UTC) Subject: [execnet-commit] execnet commit 97dcb89f2ea2: fix workerpool usage Message-ID: <20091016205841.E662A7EF4B@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1255357563 -7200 # Node ID 97dcb89f2ea2421ded744b1e78247bef53b99245 # Parent ba43d965f3e4b6b88223295d1e63bbc7e203c8eb fix workerpool usage --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -5,6 +5,7 @@ import os, sys, time import py import execnet from execnet import gateway_base, gateway +from execnet.threadpool import WorkerPool queue = py.builtin._tryimport('queue', 'Queue') TESTTIMEOUT = 10.0 # seconds @@ -400,7 +401,7 @@ def test_join_blocked_execution_gateway( gateway.join(joinexec=True) return 17 - pool = py._thread.WorkerPool() + pool = WorkerPool() reply = pool.dispatch(doit) x = reply.get(timeout=1.0) assert x == 17 From commits-noreply at bitbucket.org Fri Oct 16 22:58:41 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 16 Oct 2009 20:58:41 +0000 (UTC) Subject: [execnet-commit] execnet commit ab11b55dc82b: adding ironpython example Message-ID: <20091016205841.F0EF27EF4D@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1255724987 -7200 # Node ID ab11b55dc82b47e420c663dfa361af754c753ce8 # Parent 97dcb89f2ea2421ded744b1e78247bef53b99245 adding ironpython example --- a/doc/index.txt +++ b/doc/index.txt @@ -12,11 +12,16 @@ The execnet package allows to: execnet performs **zero-install bootstrapping** into other interpreters; package installation is only required at the initiating side. execnet enables -interoperation between CPython 2.4-3.1, Jython 2.5 and PyPy 1.1 and works -well on Windows, Linux and OSX systems. +interoperation between CPython 2.4-3.1, Jython 2.5.1, PyPy 1.1 and IronPython +and works well on Windows, Linux and OSX systems. execnet was written and is maintained by `Holger Krekel`_ with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +.. note:: + Support of Jython and IronPython are experimental at this point. + Feedback and help with testing welcome. + + Getting Started ==================== --- a/doc/examples.txt +++ b/doc/examples.txt @@ -29,6 +29,35 @@ will print on the CPython side:: .. _Jython: http://www.jython.org +Work with C# objects from CPython +---------------------------------------- + +(Experimental) use your CPython interpreter to connect to a IronPython_ interpreter +which can work with C# classes. Here is an example for instantiating +a CLR Array instance and sending back its representation:: + + import execnet + gw = execnet.PopenGateway("ipy") + + channel = gw.remote_exec(""" + import clr + clr.AddReference("System") + from System import Array + array = Array[float]([1,2]) + channel.send(str(array)) + """) + print (channel.receive()) + +using Mono 2.0 and IronPython-1.1 this will print on the CPython side:: + + System.Double[](1.0, 2.0) + +.. note:: + Using IronPython needs more testing, likely newer versions + will work better. please feedback if you have information. + +.. _IronPython: http://www.IronPython.org + Compare cwd() of Popen Gateways ---------------------------------------- From commits-noreply at bitbucket.org Thu Oct 29 20:58:32 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 29 Oct 2009 19:58:32 +0000 (UTC) Subject: [execnet-commit] execnet commit 69505782d0fd: heading towards a 1.0b1 Message-ID: <20091029195832.BDD117EFFC@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1256846251 -3600 # Node ID 69505782d0fdcdd674deaf0a035df5055e75caf2 # Parent ab11b55dc82b47e420c663dfa361af754c753ce8 heading towards a 1.0b1 - fixing python2.5 compat - fixing windows / rsyncing config - adding some examples --- /dev/null +++ b/doc/example/svn-sync-repo.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python + +""" + +small utility for hot-syncing a svn repository through ssh. +uses execnet. + +""" + +import py +import sys, os + +def usage(): + arg0 = sys.argv[0] + print """%s [user@]remote-host:/repo/location localrepo [identity keyfile]""" % (arg0,) + + +def main(args): + remote = args[0] + localrepo = py.path.local(args[1]) + if not localrepo.check(dir=1): + raise SystemExit("localrepo %s does not exist" %(localrepo,)) + if len(args) == 3: + keyfile = py.path.local(args[2]) + else: + keyfile = None + remote_host, path = remote.split(':', 1) + print "ssh-connecting to", remote_host + gw = getgateway(remote_host, keyfile) + + local_rev = get_svn_youngest(localrepo) + + # local protocol + # 1. client sends rev/repo -> server + # 2. server checks for newer revisions and sends dumps + # 3. client receives dumps, updates local repo + # 4. client goes back to step 1 + c = gw.remote_exec(""" + import py + import os + remote_rev, repopath = channel.receive() + while 1: + rev = py.process.cmdexec('svnlook youngest "%s"' % repopath) + rev = int(rev) + if rev > remote_rev: + revrange = (remote_rev+1, rev) + dumpchannel = channel.gateway.newchannel() + channel.send(revrange) + channel.send(dumpchannel) + + f = os.popen( + "svnadmin dump -q --incremental -r %s:%s %s" + % (revrange[0], revrange[1], repopath), 'r') + try: + maxcount = dumpchannel.receive() + count = maxcount + while 1: + s = f.read(8192) + if not s: + raise EOFError + dumpchannel.send(s) + count = count - 1 + if count <= 0: + ack = dumpchannel.receive() + count = maxcount + + except EOFError: + dumpchannel.close() + remote_rev = rev + else: + # using svn-hook instead would be nice here + py.std.time.sleep(30) + """) + + c.send((local_rev, path)) + print "checking revisions from %d in %s" %(local_rev, remote) + while 1: + revstart, revend = c.receive() + dumpchannel = c.receive() + print "receiving revisions", revstart, "-", revend, "replaying..." + svn_load(localrepo, dumpchannel) + print "current revision", revend + +def svn_load(repo, dumpchannel, maxcount=100): + # every maxcount we will send an ACK to the other + # side in order to synchronise and avoid our side + # growing buffers (py.execnet does not control + # RAM usage or receive queue sizes) + dumpchannel.send(maxcount) + f = os.popen("svnadmin load -q %s" %(repo, ), "w") + count = maxcount + for x in dumpchannel: + sys.stdout.write(".") + sys.stdout.flush() + f.write(x) + count = count - 1 + if count <= 0: + dumpchannel.send(maxcount) + count = maxcount + print >>sys.stdout + f.close() + +def get_svn_youngest(repo): + rev = py.process.cmdexec('svnlook youngest "%s"' % repo) + return int(rev) + +def getgateway(host, keyfile=None): + return execnet.SshGateway(host, identity=keyfile) + +if __name__ == '__main__': + if len(sys.argv) < 3: + usage() + raise SystemExit(1) + + main(sys.argv[1:]) + --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -510,7 +510,7 @@ class TestSshPopenGateway: def test_sshaddress(self, gw, specssh): assert gw.remoteaddress == specssh.ssh - def test_host_not_found(self): + def test_host_not_found(self, gw): py.test.raises(execnet.HostNotFound, "execnet.SshGateway('nowhere.codespeak.net')") --- /dev/null +++ b/doc/example/popen_read_multiple.py @@ -0,0 +1,37 @@ +""" +example + +reading results from possibly blocking code running in sub processes. +""" +import py + +NUM_PROCESSES = 5 + +channels = [] +for i in range(NUM_PROCESSES): + gw = execnet.PopenGateway() # or use SSH or socket gateways + channel = gw.remote_exec(""" + import time + secs = channel.receive() + time.sleep(secs) + channel.send("waited %d secs" % secs) + """) + channels.append(channel) + print "*** instantiated subprocess", gw + +mc = execnet.MultiChannel(channels) +queue = mc.make_receive_queue() + +print "***", "verifying that timeout on receiving results from blocked subprocesses works" +try: + queue.get(timeout=1.0) +except Exception: + pass + +print "*** sending subprocesses some data to have them unblock" +mc.send_each(1) + +print "*** receiving results asynchronously" +for i in range(NUM_PROCESSES): + channel, result = queue.get(timeout=2.0) + print "result", channel.gateway, result --- /dev/null +++ b/doc/example/sysinfo.py @@ -0,0 +1,139 @@ +""" +sysinfo.py [host1] [host2] [options] + +obtain system info from remote machine. +""" + +import py +import sys + + +parser = py.std.optparse.OptionParser(usage=__doc__) +parser.add_option("-f", "--sshconfig", action="store", dest="ssh_config", default=None, + help="use given ssh config file, and add info all contained hosts for getting info") +parser.add_option("-i", "--ignore", action="store", dest="ignores", default=None, + help="ignore hosts (useful if the list of hostnames come from a file list)") + +def parsehosts(path): + path = py.path.local(path) + l = [] + rex = py.std.re.compile(r'Host\s*(\S+)') + for line in path.readlines(): + m = rex.match(line) + if m is not None: + sshname, = m.groups() + l.append(sshname) + return l + +class RemoteInfo: + def __init__(self, gateway): + self.gw = gateway + self._cache = {} + + def exreceive(self, execstring): + if execstring not in self._cache: + channel = self.gw.remote_exec(execstring) + self._cache[execstring] = channel.receive() + return self._cache[execstring] + + def getmodattr(self, modpath): + module = modpath.split(".")[0] + return self.exreceive(""" + import %s + channel.send(%s) + """ %(module, modpath)) + + def islinux(self): + return self.getmodattr('sys.platform').find("linux") != -1 + + def getfqdn(self): + return self.exreceive(""" + import socket + channel.send(socket.getfqdn()) + """) + + def getmemswap(self): + if self.islinux(): + return self.exreceive(""" + import commands, re + out = commands.getoutput("free") + mem = re.search(r"Mem:\s+(\S*)", out).group(1) + swap = re.search(r"Swap:\s+(\S*)", out).group(1) + channel.send((mem, swap)) + """) + + def getcpuinfo(self): + if self.islinux(): + return self.exreceive(""" + # a hyperthreaded cpu core only counts as 1, although it + # is present as 2 in /proc/cpuinfo. Counting it as 2 is + # misleading because it is *by far* not as efficient as + # two independent cores. + cpus = {} + cpuinfo = {} + f = open("/proc/cpuinfo") + lines = f.readlines() + f.close() + for line in lines + ['']: + if line.strip(): + key, value = line.split(":", 1) + cpuinfo[key.strip()] = value.strip() + else: + corekey = (cpuinfo.get("physical id"), + cpuinfo.get("core id")) + cpus[corekey] = 1 + numcpus = len(cpus) + model = cpuinfo.get("model name") + channel.send((numcpus, model)) + """) + +def debug(*args): + print >>sys.stderr, " ".join(map(str, args)) +def error(*args): + debug("ERROR", args[0] + ":", *args[1:]) + +def getinfo(sshname, ssh_config=None, loginfo=sys.stdout): + debug("connecting to", sshname) + try: + gw = execnet.SshGateway(sshname, ssh_config=ssh_config) + except IOError: + error("could not get sshagteway", sshname) + else: + ri = RemoteInfo(gw) + #print "%s info:" % sshname + prefix = sshname.upper() + " " + print >>loginfo, prefix, "fqdn:", ri.getfqdn() + for attr in ( + "sys.platform", + "sys.version_info", + ): + loginfo.write("%s %s: " %(prefix, attr,)) + loginfo.flush() + value = ri.getmodattr(attr) + loginfo.write(str(value)) + loginfo.write("\n") + loginfo.flush() + memswap = ri.getmemswap() + if memswap: + mem,swap = memswap + print >>loginfo, prefix, "Memory:", mem, "Swap:", swap + cpuinfo = ri.getcpuinfo() + if cpuinfo: + numcpu, model = cpuinfo + print >>loginfo, prefix, "number of cpus:", numcpu + print >>loginfo, prefix, "cpu model", model + return ri + +if __name__ == '__main__': + options, args = parser.parse_args() + hosts = list(args) + ssh_config = options.ssh_config + if ssh_config: + hosts.extend(parsehosts(ssh_config)) + ignores = options.ignores or () + if ignores: + ignores = ignores.split(",") + for host in hosts: + if host not in ignores: + getinfo(host, ssh_config=ssh_config) + --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -5,7 +5,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0alpha2" +__version__ = "1.0.0b1" __author__ = "holger krekel and others" from execnet.gateway import PopenGateway, SocketGateway, SshGateway --- /dev/null +++ b/doc/example/redirect_remote_output.py @@ -0,0 +1,31 @@ +""" +redirect output from remote to a local function +showcasing features of the channel object: + +- sending a channel over a channel +- adapting a channel to a file object +- setting a callback for receiving channel data + +""" + +import py + +gw = execnet.PopenGateway() + +outchan = gw.remote_exec(""" + import sys + outchan = channel.gateway.newchannel() + sys.stdout = outchan.makefile("w") + channel.send(outchan) +""").receive() + +# note: callbacks execute in receiver thread! +def write(data): + print "received:", repr(data) +outchan.setcallback(write) + +gw.remote_exec(""" + print 'hello world' + print 'remote execution ends' +""").waitclose() + --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,10 +1,12 @@ import execnet import py +rsyncdirs = ['../execnet', '.'] + pytest_plugins = ['pytester'] # configuration information for tests def pytest_addoption(parser): - group = parser.addgroup("pylib", "py lib testing options") + group = parser.getgroup("pylib", "py lib testing options") group.addoption('--gx', action="append", dest="gspecs", default=None, help=("add a global test environment, XSpec-syntax. ")) --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -12,6 +12,11 @@ def _find_version(suffix=""): name = "python" + suffix executable = py.path.local.sysfind(name) if executable is None: + if sys.platform == "win32" and suffix == "3": + for name in ('python31', 'python30'): + executable = py.path.local(r"c:\\%s\python.exe" % (name,)) + if executable.check(): + return executable py.test.skip("can't find a %r executable" % (name,)) return executable --- a/execnet/serializer.py +++ b/execnet/serializer.py @@ -79,7 +79,7 @@ class Serializer(object): def save_bytes(self, bytes_): self.stream.write(BYTES) self._write_byte_sequence(bytes_) - dispatch[bytes] = save_bytes + dispatch[type("".encode('ascii'))] = save_bytes if _INPY3: def save_string(self, s): From commits-noreply at bitbucket.org Mon Nov 2 15:59:17 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 2 Nov 2009 14:59:17 +0000 (UTC) Subject: [execnet-commit] execnet commit 3573cc5b82a7: small fixes/adjustments for running tests on jython-2.5.1 Message-ID: <20091102145917.5A08C7EFAB@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257173606 -3600 # Node ID 3573cc5b82a7f502b6f42c028334c0f2d47bcfad # Parent 69505782d0fdcdd674deaf0a035df5055e75caf2 small fixes/adjustments for running tests on jython-2.5.1 --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -119,7 +119,7 @@ simple_tests = { 'float':'3.25', 'list': '[1, 2, 3]', 'tuple': '(1, 2, 3)', - 'dict': '{6: 2, (1, 2, 3): 32}', + 'dict': '{(1, 2, 3): 32}', } def test_simple(tp_name, repr, dump, load): --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -71,9 +71,9 @@ class SocketIO: def __init__(self, sock): self.sock = sock try: + sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) - sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY - except socket.error: + except (AttributeError, socket.error): e = sys.exc_info()[1] sys.stderr.write("WARNING: cannot set socketoption") self.readable = self.writeable = True --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -9,6 +9,7 @@ from execnet.threadpool import WorkerPoo queue = py.builtin._tryimport('queue', 'Queue') TESTTIMEOUT = 10.0 # seconds +skiponjython = py.test.mark.skipif("sys.platform.startswith('java')") class TestBasicRemoteExecution: def test_correct_setup(self, gw): @@ -216,6 +217,7 @@ class TestChannelBasicBehaviour: assert l == [0, 100, 200, 300, 400] return subchannel + @skiponjython def test_channel_callback_remote_freed(self, gw): channel = self.check_channel_callback_stays_active(gw, earlyfree=False) # freed automatically at the end of producer() @@ -341,6 +343,7 @@ class TestChannelFile: channel = gw.newchannel() py.test.raises(ValueError, 'channel.makefile("rw")') + @skiponjython def test_confusion_from_os_write_stdout(self, gw): channel = gw.remote_exec(""" import os @@ -355,6 +358,7 @@ class TestChannelFile: res = channel.receive() assert res == 42 + @skiponjython def test_confusion_from_os_write_stderr(self, gw): channel = gw.remote_exec(""" import os @@ -394,16 +398,16 @@ def test_join_blocked_execution_gateway( gateway = execnet.PopenGateway() channel = gateway.remote_exec(""" import time - time.sleep(5.0) + time.sleep(10.0) """) def doit(): gateway.exit() - gateway.join(joinexec=True) + gateway.join() return 17 pool = WorkerPool() reply = pool.dispatch(doit) - x = reply.get(timeout=1.0) + x = reply.get(timeout=5.0) assert x == 17 class TestPopenGateway: From commits-noreply at bitbucket.org Mon Nov 2 15:59:19 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 2 Nov 2009 14:59:19 +0000 (UTC) Subject: [execnet-commit] execnet commit 309d91b1db59: fix python2.4 serializer compatibility (does not have struct.Struct) Message-ID: <20091102145919.24BE67EFFF@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257173924 -3600 # Node ID 309d91b1db599824a012ebf42118326eee442c41 # Parent 3573cc5b82a7f502b6f42c028334c0f2d47bcfad fix python2.4 serializer compatibility (does not have struct.Struct) --- a/execnet/serializer.py +++ b/execnet/serializer.py @@ -35,8 +35,8 @@ else: FOUR_BYTE_INT_MAX = 2147483647 -_int4_format = struct.Struct("!i") -_float_format = struct.Struct("!d") +FLOAT_FORMAT = "!d" +FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) # Protocol constants VERSION_NUMBER = 1 @@ -114,14 +114,14 @@ class Serializer(object): def save_float(self, flt): self.stream.write(FLOAT) - self.stream.write(_float_format.pack(flt)) + self.stream.write(struct.pack(FLOAT_FORMAT, flt)) dispatch[float] = save_float def _write_int4(self, i, error="int must be less than %i" % (FOUR_BYTE_INT_MAX,)): if i > FOUR_BYTE_INT_MAX: raise SerializationError(error) - self.stream.write(_int4_format.pack(i)) + self.stream.write(struct.pack("!i", i)) def save_list(self, L): self.stream.write(NEWLIST) @@ -206,12 +206,12 @@ class Unserializer(object): opcodes[INT] = load_int def load_float(self): - binary = self.stream.read(_float_format.size) - self.stack.append(_float_format.unpack(binary)[0]) + binary = self.stream.read(FLOAT_FORMAT_SIZE) + self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) opcodes[FLOAT] = load_float def _read_int4(self): - return _int4_format.unpack(self.stream.read(4))[0] + return struct.unpack("!i", self.stream.read(4))[0] def _read_byte_string(self): length = self._read_int4() From commits-noreply at bitbucket.org Tue Nov 3 12:48:09 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 11:48:09 +0000 (UTC) Subject: [execnet-commit] execnet commit d8ed57971198: adding neccessary serialization support for long, None, bools Message-ID: <20091103114809.1A5BE7EF46@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257248495 -3600 # Node ID d8ed5797119873303ca129658d7fc5c0d03e529c # Parent 6ea0aa1903dbe6f1a520b690d36ca2795deadad4 adding neccessary serialization support for long, None, bools --- a/testing/test_rsync.py +++ b/testing/test_rsync.py @@ -34,7 +34,9 @@ class TestRSync: rsync = RSync(dirs.source) rsync.add_target(gw1, dest) rsync.add_target(gw2, dest2) + print "now sending", rsync rsync.send() + print "did send", rsync assert dest.join('subdir').check(dir=1) assert dest.join('subdir', 'file1').check(file=1) assert dest.join('subdir', 'file1').read() == s --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -1,7 +1,7 @@ """ -base execnet gateway bootstrapping code. +base execnet gateway code send to the other side for bootstrapping. -NOTE: below code is to be compatible to Python 2.3-3.1, Jython and IronPython +NOTE: aims to be compatible to Python 2.3-3.1, Jython and IronPython (C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others """ @@ -125,11 +125,6 @@ sys.stdin = tempfile.TemporaryFile('r') pass self.writeable = None -# ___________________________________________________________________________ -# -# Messages -# ___________________________________________________________________________ - class Message: """ encapsulates Messages and their wire protocol. """ _types = {} @@ -712,6 +707,8 @@ FLOAT_FORMAT_SIZE = struct.calcsize(FLOA # Protocol constants VERSION_NUMBER = 1 VERSION = b(chr(VERSION_NUMBER)) +NONE = b('n') +NONE = b('n') PY2STRING = b('s') PY3STRING = b('t') UNICODE = b('u') @@ -722,6 +719,8 @@ SETITEM = b('m') NEWDICT = b('d') INT = b('i') FLOAT = b('f') +TRUE = b('1') +FALSE = b('0') STOP = b('S') class Serializer(object): @@ -744,6 +743,17 @@ class Serializer(object): dispatch = {} + def save_none(self, non): + self.stream.write(NONE) + dispatch[type(None)] = save_none + + def save_bool(self, boolean): + if boolean: + self.stream.write(TRUE) + else: + self.stream.write(FALSE) + dispatch[bool] = save_bool + def save_bytes(self, bytes_): self.stream.write(BYTES) self._write_byte_sequence(bytes_) @@ -779,6 +789,8 @@ class Serializer(object): self.stream.write(INT) self._write_int4(i) dispatch[int] = save_int + if not ISPY3: + dispatch[long] = save_int def save_float(self, flt): self.stream.write(FLOAT) @@ -868,6 +880,17 @@ class Unserializer(object): opcodes = {} + def load_none(self): + self.stack.append(None) + opcodes[NONE] = load_none + + def load_true(self): + self.stack.append(True) + opcodes[TRUE] = load_true + def load_false(self): + self.stack.append(False) + opcodes[FALSE] = load_false + def load_int(self): i = self._read_int4() self.stack.append(i) From commits-noreply at bitbucket.org Tue Nov 3 12:48:10 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 11:48:10 +0000 (UTC) Subject: [execnet-commit] execnet commit b0289aa788c4: refine docs, avoid redundancy, move some bits to a new implnotes.txt file Message-ID: <20091103114810.A54A37EF4F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257177655 -3600 # Node ID b0289aa788c451cda903489e26da5030aa708167 # Parent 309d91b1db599824a012ebf42118326eee442c41 refine docs, avoid redundancy, move some bits to a new implnotes.txt file --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -1,35 +1,7 @@ """ -base execnet gateway code, a quick overview. +base execnet gateway code send to the other side for bootstrapping. -the code of this module is sent to the "other side" -as a means of bootstrapping a Gateway object -capable of receiving and executing code, -and routing data through channels. - -Gateways operate on InputOutput objects offering -a write and a read(n) method. - -Once bootstrapped a higher level protocol -based on Messages is used. Messages are serialized -to and from InputOutput objects. The details of this protocol -are locally defined in this module. There is no need -for standardizing or versioning the protocol. - -After bootstrapping the BaseGateway opens a receiver thread which -accepts encoded messages and triggers actions to interpret them. -Sending of channel data items happens directly through -write operations to InputOutput objects so there is no -separate thread. - -Code execution messages are put into an execqueue from -which they will be taken for execution. gateway.serve() -will take and execute such items, one by one. This means -that by incoming default execution is single-threaded. - -The receiver thread terminates if the remote side sends -a gateway termination message or if the IO-connection drops. -It puts an end symbol into the execqueue so -that serve() can cleanly finish as well. +NOTE: compatible to Python2.3-3.1, Jython and IronPython! (C) 2004-2009 Holger Krekel, Armin Rigo and others """ @@ -611,8 +583,6 @@ class BaseGateway(object): pass def __init__(self, io, _startcount=2): - """ initialize core gateway, using the given inputoutput object. - """ self._io = io self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = threading.RLock() @@ -634,7 +604,6 @@ class BaseGateway(object): sys.stderr.write("exception during tracing\n") def _thread_receiver(self): - """ thread to read and handle Messages half-sync-half-async. """ self._trace("starting to receive") try: while 1: @@ -694,7 +663,6 @@ class BaseGateway(object): # _____________________________________________________________________ # def newchannel(self): - """ return new channel object. """ return self._channelfactory.new() def join(self, joinexec=True): @@ -732,11 +700,9 @@ class SlaveGateway(BaseGateway): self.join() def executetask(self, item): - """ execute channel/source items. """ channel, source = item try: - loc = { 'channel' : channel, '__name__': '__channelexec__'} - #open("task.py", 'w').write(source) + loc = {'channel' : channel, '__name__': '__channelexec__'} self._trace("execution starts: %s" % repr(source)[:50]) try: co = compile(source+'\n', '', 'exec') --- /dev/null +++ b/doc/implnotes.txt @@ -0,0 +1,33 @@ + +gateway_base.py:: + +the code of this module is sent to the "other side" +as a means of bootstrapping a Gateway object +capable of receiving and executing code, +and routing data through channels. + +Gateways operate on InputOutput objects offering +a write and a read(n) method. + +Once bootstrapped a higher level protocol +based on Messages is used. Messages are serialized +to and from InputOutput objects. The details of this protocol +are locally defined in this module. There is no need +for standardizing or versioning the protocol. + +After bootstrapping the BaseGateway opens a receiver thread which +accepts encoded messages and triggers actions to interpret them. +Sending of channel data items happens directly through +write operations to InputOutput objects so there is no +separate thread. + +Code execution messages are put into an execqueue from +which they will be taken for execution. gateway.serve() +will take and execute such items, one by one. This means +that by incoming default execution is single-threaded. + +The receiver thread terminates if the remote side sends +a gateway termination message or if the IO-connection drops. +It puts an end symbol into the execqueue so +that serve() can cleanly finish as well. + From commits-noreply at bitbucket.org Tue Nov 3 12:48:12 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 11:48:12 +0000 (UTC) Subject: [execnet-commit] execnet commit 6ea0aa1903db: integrate serializer into gateway bootstrap, simplify exception hierarchies Message-ID: <20091103114812.5E5D17EF50@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257248494 -3600 # Node ID 6ea0aa1903dbe6f1a520b690d36ca2795deadad4 # Parent b0289aa788c451cda903489e26da5030aa708167 integrate serializer into gateway bootstrap, simplify exception hierarchies --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -5,7 +5,7 @@ import tempfile import subprocess import py import execnet -from execnet import serializer +from execnet import gateway_base as serializer def _find_version(suffix=""): @@ -46,7 +46,7 @@ class PythonWrapper(object): def dump(self, obj_rep): script_file = TEMPDIR.join("dump.py") script_file.write(""" -from execnet import serializer +from execnet import gateway_base as serializer import sys if sys.version_info > (3, 0): # Need binary output sys.stdout = sys.stdout.detach() @@ -57,7 +57,7 @@ saver.save(%s)""" % (obj_rep,)) def load(self, data, option_args=""): script_file = TEMPDIR.join("load.py") script_file.write(r""" -from execnet import serializer +from execnet import gateway_base as serializer import sys if sys.version_info > (3, 0): sys.stdin = sys.stdin.detach() @@ -158,7 +158,7 @@ def test_string(py2, py3): assert tp == "str" assert s == "'xyz'" tp, s = py3.load(p) - assert tp == "bytes" + assert tp == "bytes" # depends on unserialization defaults assert s == "b'xyz'" tp, s = py3.load(p, "True") assert tp == "str" @@ -181,5 +181,5 @@ def test_unicode(py2, py3): assert tp == "str" assert s == "'hi'" tp, s = py2.load(p) - assert tp == "unicode" + assert tp == "unicode" # depends on unserialization defaults assert s == "u'hi'" --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -1,9 +1,9 @@ """ -base execnet gateway code send to the other side for bootstrapping. +base execnet gateway bootstrapping code. -NOTE: compatible to Python2.3-3.1, Jython and IronPython! +NOTE: below code is to be compatible to Python 2.3-3.1, Jython and IronPython -(C) 2004-2009 Holger Krekel, Armin Rigo and others +(C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others """ import sys, os, weakref import threading, traceback, socket, struct @@ -12,7 +12,8 @@ try: except ImportError: import Queue as queue -if sys.version_info > (3, 0): +ISPY3 = sys.version_info > (3, 0) +if ISPY3: exec("def do_exec(co, loc): exec(co, loc)\n" "def reraise(cls, val, tb): raise val\n") unicode = str @@ -21,10 +22,6 @@ else: "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str -def str(*args): - raise EnvironmentError( - "use unicode or bytes, not cross-python ambigous 'str'") - default_encoding = "UTF-8" sysex = (KeyboardInterrupt, SystemExit) @@ -132,56 +129,22 @@ sys.stdin = tempfile.TemporaryFile('r') # # Messages # ___________________________________________________________________________ -# the header format -HDR_FORMAT = "!hhii" -HDR_SIZE = struct.calcsize(HDR_FORMAT) - -is3k = sys.version_info >= (3,0) class Message: """ encapsulates Messages and their wire protocol. """ _types = {} + def __init__(self, channelid=0, data=''): self.channelid = channelid self.data = data def writeto(self, io): - # XXX marshal.dumps doesn't work for exchanging data across Python - # version :-((( XXX check this statement wrt python2.4 through 3.1 - data = self.data - if isinstance(data, bytes): - dataformat = 1 + int(is3k) - else: - if isinstance(data, unicode): - dataformat = 3 - else: - data = repr(self.data) # argh - dataformat = 4 - data = data.encode(default_encoding) - header = struct.pack(HDR_FORMAT, self.msgtype, dataformat, - self.channelid, len(data)) - io.write(header + data) + ser = Serializer(io) + ser.save((self.msgtype, self.channelid, self.data)) def readfrom(cls, io): - header = io.read(HDR_SIZE) - (msgtype, dataformat, - senderid, stringlen) = struct.unpack(HDR_FORMAT, header) - data = io.read(stringlen) - if dataformat == 1: - if is3k: - # remote was python2-str, we are 3k-text - data = data.decode(default_encoding) - elif dataformat == 2: - # remote was python3-bytes - pass - else: - data = data.decode(default_encoding) - if dataformat == 3: - pass - elif dataformat == 4: - data = eval(data, {}) # reversed argh - else: - raise ValueError("bad data format") + unser = Unserializer(io) + msgtype, senderid, data = unser.load() return cls._types[msgtype](senderid, data) readfrom = classmethod(readfrom) @@ -722,3 +685,258 @@ class SlaveGateway(BaseGateway): else: channel.close() +# +# Cross-Python pickling code, tested from test_serializer.py +# + +class SerializeError(Exception): + pass + +class SerializationError(SerializeError): + """Error while serializing an object.""" + +class UnserializationError(SerializeError): + """Error while unserializing an object.""" + +if ISPY3: + def b(s): + return s.encode("ascii") +else: + b = str + +FOUR_BYTE_INT_MAX = 2147483647 + +FLOAT_FORMAT = "!d" +FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) + +# Protocol constants +VERSION_NUMBER = 1 +VERSION = b(chr(VERSION_NUMBER)) +PY2STRING = b('s') +PY3STRING = b('t') +UNICODE = b('u') +BYTES = b('b') +NEWLIST = b('l') +BUILDTUPLE = b('T') +SETITEM = b('m') +NEWDICT = b('d') +INT = b('i') +FLOAT = b('f') +STOP = b('S') + +class Serializer(object): + + def __init__(self, stream): + self.stream = stream + + def save(self, obj): + self.stream.write(VERSION) + self._save(obj) + self.stream.write(STOP) + + def _save(self, obj): + tp = type(obj) + try: + dispatch = self.dispatch[tp] + except KeyError: + raise SerializationError("can't serialize %s" % (tp,)) + dispatch(self, obj) + + dispatch = {} + + def save_bytes(self, bytes_): + self.stream.write(BYTES) + self._write_byte_sequence(bytes_) + dispatch[type("".encode('ascii'))] = save_bytes + + if ISPY3: + def save_string(self, s): + self.stream.write(PY3STRING) + self._write_unicode_string(s) + else: + def save_string(self, s): + self.stream.write(PY2STRING) + self._write_byte_sequence(s) + + def save_unicode(self, s): + self.stream.write(UNICODE) + self._write_unicode_string(s) + dispatch[unicode] = save_unicode + dispatch[str] = save_string + + def _write_unicode_string(self, s): + try: + as_bytes = s.encode("utf-8") + except UnicodeEncodeError: + raise SerializationError("strings must be utf-8 encodable") + self._write_byte_sequence(as_bytes) + + def _write_byte_sequence(self, bytes_): + self._write_int4(len(bytes_), "string is too long") + self.stream.write(bytes_) + + def save_int(self, i): + self.stream.write(INT) + self._write_int4(i) + dispatch[int] = save_int + + def save_float(self, flt): + self.stream.write(FLOAT) + self.stream.write(struct.pack(FLOAT_FORMAT, flt)) + dispatch[float] = save_float + + def _write_int4(self, i, error="int must be less than %i" % + (FOUR_BYTE_INT_MAX,)): + if i > FOUR_BYTE_INT_MAX: + raise SerializationError(error) + self.stream.write(struct.pack("!i", i)) + + def save_list(self, L): + self.stream.write(NEWLIST) + self._write_int4(len(L), "list is too long") + for i, item in enumerate(L): + self._write_setitem(i, item) + dispatch[list] = save_list + + def _write_setitem(self, key, value): + self._save(key) + self._save(value) + self.stream.write(SETITEM) + + def save_dict(self, d): + self.stream.write(NEWDICT) + for key, value in d.items(): + self._write_setitem(key, value) + dispatch[dict] = save_dict + + def save_tuple(self, tup): + for item in tup: + self._save(item) + self.stream.write(BUILDTUPLE) + self._write_int4(len(tup), "tuple is too long") + dispatch[tuple] = save_tuple + +class _UnserializationOptions(object): + pass + +class _Py2UnserializationOptions(_UnserializationOptions): + + def __init__(self, py3_strings_as_str=False): + self.py3_strings_as_str = py3_strings_as_str + +class _Py3UnserializationOptions(_UnserializationOptions): + + def __init__(self, py2_strings_as_str=False): + self.py2_strings_as_str = py2_strings_as_str + +if ISPY3: + UnserializationOptions = _Py3UnserializationOptions +else: + UnserializationOptions = _Py2UnserializationOptions + +class _Stop(Exception): + pass + +class Unserializer(object): + + def __init__(self, stream, options=UnserializationOptions()): + self.stream = stream + self.options = options + + def load(self): + self.stack = [] + version = ord(self.stream.read(1)) + if version != VERSION_NUMBER: + raise UnserializationError( + "version mismatch: %i != %i" % (version, VERSION_NUMBER)) + try: + while True: + opcode = self.stream.read(1) + if not opcode: + raise EOFError + try: + loader = self.opcodes[opcode] + except KeyError: + raise UnserializationError("unkown opcode %s" % (opcode,)) + loader(self) + except _Stop: + if len(self.stack) != 1: + raise UnserializationError("internal unserialization error") + return self.stack[0] + else: + raise UnserializationError("didn't get STOP") + + opcodes = {} + + def load_int(self): + i = self._read_int4() + self.stack.append(i) + opcodes[INT] = load_int + + def load_float(self): + binary = self.stream.read(FLOAT_FORMAT_SIZE) + self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) + opcodes[FLOAT] = load_float + + def _read_int4(self): + return struct.unpack("!i", self.stream.read(4))[0] + + def _read_byte_string(self): + length = self._read_int4() + as_bytes = self.stream.read(length) + return as_bytes + + def load_py3string(self): + as_bytes = self._read_byte_string() + if not ISPY3 and self.options.py3_strings_as_str: + # XXX Should we try to decode into latin-1? + self.stack.append(as_bytes) + else: + self.stack.append(as_bytes.decode("utf-8")) + opcodes[PY3STRING] = load_py3string + + def load_py2string(self): + as_bytes = self._read_byte_string() + if ISPY3 and self.options.py2_strings_as_str: + s = as_bytes.decode("latin-1") + else: + s = as_bytes + self.stack.append(s) + opcodes[PY2STRING] = load_py2string + + def load_bytes(self): + s = self._read_byte_string() + self.stack.append(s) + opcodes[BYTES] = load_bytes + + def load_unicode(self): + self.stack.append(self._read_byte_string().decode("utf-8")) + opcodes[UNICODE] = load_unicode + + def load_newlist(self): + length = self._read_int4() + self.stack.append([None] * length) + opcodes[NEWLIST] = load_newlist + + def load_setitem(self): + if len(self.stack) < 3: + raise UnserializationError("not enough items for setitem") + value = self.stack.pop() + key = self.stack.pop() + self.stack[-1][key] = value + opcodes[SETITEM] = load_setitem + + def load_newdict(self): + self.stack.append({}) + opcodes[NEWDICT] = load_newdict + + def load_buildtuple(self): + length = self._read_int4() + tup = tuple(self.stack[-length:]) + del self.stack[-length:] + self.stack.append(tup) + opcodes[BUILDTUPLE] = load_buildtuple + + def load_stop(self): + raise _Stop + opcodes[STOP] = load_stop --- a/execnet/serializer.py +++ /dev/null @@ -1,274 +0,0 @@ -""" -Simple marshal format (based on pickle) designed to work across Python versions. - -(c) 2006-2009 Benjamin Peterson, Ronny Pfannschmidt and others -""" - -import sys -import struct - -_INPY3 = _REALLY_PY3 = sys.version_info > (3, 0) - -class SerializeError(Exception): - pass - -class SerializationError(SerializeError): - """Error while serializing an object.""" - -class UnserializableType(SerializationError): - """Can't serialize a type.""" - -class UnserializationError(SerializeError): - """Error while unserializing an object.""" - -class VersionMismatch(UnserializationError): - """Data from a previous or later format.""" - -class Corruption(UnserializationError): - """The pickle format appears to have been corrupted.""" - -if _INPY3: - def b(s): - return s.encode("ascii") -else: - b = str - -FOUR_BYTE_INT_MAX = 2147483647 - -FLOAT_FORMAT = "!d" -FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) - -# Protocol constants -VERSION_NUMBER = 1 -VERSION = b(chr(VERSION_NUMBER)) -PY2STRING = b('s') -PY3STRING = b('t') -UNICODE = b('u') -BYTES = b('b') -NEWLIST = b('l') -BUILDTUPLE = b('T') -SETITEM = b('m') -NEWDICT = b('d') -INT = b('i') -FLOAT = b('f') -STOP = b('S') - -class CrossVersionOptions(object): - pass - -class Serializer(object): - - def __init__(self, stream): - self.stream = stream - - def save(self, obj): - self.stream.write(VERSION) - self._save(obj) - self.stream.write(STOP) - - def _save(self, obj): - tp = type(obj) - try: - dispatch = self.dispatch[tp] - except KeyError: - raise UnserializableType("can't serialize %s" % (tp,)) - dispatch(self, obj) - - dispatch = {} - - def save_bytes(self, bytes_): - self.stream.write(BYTES) - self._write_byte_sequence(bytes_) - dispatch[type("".encode('ascii'))] = save_bytes - - if _INPY3: - def save_string(self, s): - self.stream.write(PY3STRING) - self._write_unicode_string(s) - else: - def save_string(self, s): - self.stream.write(PY2STRING) - self._write_byte_sequence(s) - - def save_unicode(self, s): - self.stream.write(UNICODE) - self._write_unicode_string(s) - dispatch[unicode] = save_unicode - dispatch[str] = save_string - - def _write_unicode_string(self, s): - try: - as_bytes = s.encode("utf-8") - except UnicodeEncodeError: - raise SerializationError("strings must be utf-8 encodable") - self._write_byte_sequence(as_bytes) - - def _write_byte_sequence(self, bytes_): - self._write_int4(len(bytes_), "string is too long") - self.stream.write(bytes_) - - def save_int(self, i): - self.stream.write(INT) - self._write_int4(i) - dispatch[int] = save_int - - def save_float(self, flt): - self.stream.write(FLOAT) - self.stream.write(struct.pack(FLOAT_FORMAT, flt)) - dispatch[float] = save_float - - def _write_int4(self, i, error="int must be less than %i" % - (FOUR_BYTE_INT_MAX,)): - if i > FOUR_BYTE_INT_MAX: - raise SerializationError(error) - self.stream.write(struct.pack("!i", i)) - - def save_list(self, L): - self.stream.write(NEWLIST) - self._write_int4(len(L), "list is too long") - for i, item in enumerate(L): - self._write_setitem(i, item) - dispatch[list] = save_list - - def _write_setitem(self, key, value): - self._save(key) - self._save(value) - self.stream.write(SETITEM) - - def save_dict(self, d): - self.stream.write(NEWDICT) - for key, value in d.items(): - self._write_setitem(key, value) - dispatch[dict] = save_dict - - def save_tuple(self, tup): - for item in tup: - self._save(item) - self.stream.write(BUILDTUPLE) - self._write_int4(len(tup), "tuple is too long") - dispatch[tuple] = save_tuple - - -class _UnserializationOptions(object): - pass - -class _Py2UnserializationOptions(_UnserializationOptions): - - def __init__(self, py3_strings_as_str=False): - self.py3_strings_as_str = py3_strings_as_str - -class _Py3UnserializationOptions(_UnserializationOptions): - - def __init__(self, py2_strings_as_str=False): - self.py2_strings_as_str = py2_strings_as_str - -if _INPY3: - UnserializationOptions = _Py3UnserializationOptions -else: - UnserializationOptions = _Py2UnserializationOptions - -class _Stop(Exception): - pass - -class Unserializer(object): - - def __init__(self, stream, options=UnserializationOptions()): - self.stream = stream - self.options = options - - def load(self): - self.stack = [] - version = ord(self.stream.read(1)) - if version != VERSION_NUMBER: - raise VersionMismatch("%i != %i" % (version, VERSION_NUMBER)) - try: - while True: - opcode = self.stream.read(1) - if not opcode: - raise EOFError - try: - loader = self.opcodes[opcode] - except KeyError: - raise Corruption("unkown opcode %s" % (opcode,)) - loader(self) - except _Stop: - if len(self.stack) != 1: - raise UnserializationError("internal unserialization error") - return self.stack[0] - else: - raise Corruption("didn't get STOP") - - opcodes = {} - - def load_int(self): - i = self._read_int4() - self.stack.append(i) - opcodes[INT] = load_int - - def load_float(self): - binary = self.stream.read(FLOAT_FORMAT_SIZE) - self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) - opcodes[FLOAT] = load_float - - def _read_int4(self): - return struct.unpack("!i", self.stream.read(4))[0] - - def _read_byte_string(self): - length = self._read_int4() - as_bytes = self.stream.read(length) - return as_bytes - - def load_py3string(self): - as_bytes = self._read_byte_string() - if not _INPY3 and self.options.py3_strings_as_str: - # XXX Should we try to decode into latin-1? - self.stack.append(as_bytes) - else: - self.stack.append(as_bytes.decode("utf-8")) - opcodes[PY3STRING] = load_py3string - - def load_py2string(self): - as_bytes = self._read_byte_string() - if _INPY3 and self.options.py2_strings_as_str: - s = as_bytes.decode("latin-1") - else: - s = as_bytes - self.stack.append(s) - opcodes[PY2STRING] = load_py2string - - def load_bytes(self): - s = self._read_byte_string() - self.stack.append(s) - opcodes[BYTES] = load_bytes - - def load_unicode(self): - self.stack.append(self._read_byte_string().decode("utf-8")) - opcodes[UNICODE] = load_unicode - - def load_newlist(self): - length = self._read_int4() - self.stack.append([None] * length) - opcodes[NEWLIST] = load_newlist - - def load_setitem(self): - if len(self.stack) < 3: - raise Corruption("not enough items for setitem") - value = self.stack.pop() - key = self.stack.pop() - self.stack[-1][key] = value - opcodes[SETITEM] = load_setitem - - def load_newdict(self): - self.stack.append({}) - opcodes[NEWDICT] = load_newdict - - def load_buildtuple(self): - length = self._read_int4() - tup = tuple(self.stack[-length:]) - del self.stack[-length:] - self.stack.append(tup) - opcodes[BUILDTUPLE] = load_buildtuple - - def load_stop(self): - raise _Stop - opcodes[STOP] = load_stop From commits-noreply at bitbucket.org Tue Nov 3 12:59:51 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 11:59:51 +0000 (UTC) Subject: [execnet-commit] execnet commit 9ca8d04cb915: adding tests, fixing typos and setting option to automatically turn py2 strings into a py3 'str' Message-ID: <20091103115951.D6F207EF46@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257249262 -3600 # Node ID 9ca8d04cb915afc9e42278b2fe28284cc44d05f1 # Parent d8ed5797119873303ca129658d7fc5c0d03e529c adding tests, fixing typos and setting option to automatically turn py2 strings into a py3 'str' --- a/testing/test_rsync.py +++ b/testing/test_rsync.py @@ -34,9 +34,7 @@ class TestRSync: rsync = RSync(dirs.source) rsync.add_target(gw1, dest) rsync.add_target(gw2, dest2) - print "now sending", rsync rsync.send() - print "did send", rsync assert dest.join('subdir').check(dir=1) assert dest.join('subdir', 'file1').check(file=1) assert dest.join('subdir', 'file1').read() == s --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -838,7 +838,7 @@ class _Py2UnserializationOptions(_Unseri class _Py3UnserializationOptions(_UnserializationOptions): - def __init__(self, py2_strings_as_str=False): + def __init__(self, py2_strings_as_str=True): self.py2_strings_as_str = py2_strings_as_str if ISPY3: --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -158,14 +158,7 @@ def test_string(py2, py3): assert tp == "str" assert s == "'xyz'" tp, s = py3.load(p) - assert tp == "bytes" # depends on unserialization defaults - assert s == "b'xyz'" - tp, s = py3.load(p, "True") - assert tp == "str" - assert s == "'xyz'" - p = py3.dump("'xyz'") - tp, s = py2.load(p, True) - assert tp == "str" + assert tp == "str" # depends on unserialization defaults assert s == "'xyz'" def test_unicode(py2, py3): @@ -183,3 +176,29 @@ def test_unicode(py2, py3): tp, s = py2.load(p) assert tp == "unicode" # depends on unserialization defaults assert s == "u'hi'" + +def test_long(py2, py3): + p = py2.dump("123L") + tp, s = py2.load(p) + assert s == "123" + tp, s = py3.load(p) + assert s == "123" + +def test_bool(py2, py3): + p = py2.dump("True") + tp, s = py2.load(p) + assert tp == "bool" + assert s == "True" + tp, s = py3.load(p) + assert s == "True" + assert tp == "bool" + p = py2.dump("False") + tp, s = py2.load(p) + assert s == "False" + +def test_none(py2, py3): + p = py2.dump("None") + tp, s = py2.load(p) + assert s == "None" + tp, s = py3.load(p) + assert s == "None" From commits-noreply at bitbucket.org Tue Nov 3 18:04:56 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 17:04:56 +0000 (UTC) Subject: [execnet-commit] execnet commit 6371d6423d5d: small refinements: simplifying send-code, fixing RInfo for python3, extend .hgignore Message-ID: <20091103170456.E4F3D7EF06@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257264604 -3600 # Node ID 6371d6423d5d28faba17462c154b3282db0c3d99 # Parent 9ca8d04cb915afc9e42278b2fe28284cc44d05f1 small refinements: simplifying send-code, fixing RInfo for python3, extend .hgignore --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -95,7 +95,7 @@ class Gateway(gateway_base.BaseGateway): """ return some sys/env information from remote. """ if update or not hasattr(self, '_cache_rinfo'): ch = self.remote_exec(rinfo_source) - self._cache_rinfo = RInfo(**ch.receive()) + self._cache_rinfo = RInfo(ch.receive()) return self._cache_rinfo def remote_exec(self, source): @@ -156,7 +156,7 @@ class Gateway(gateway_base.BaseGateway): class RInfo: - def __init__(self, **kwargs): + def __init__(self, kwargs): self.__dict__.update(kwargs) def __repr__(self): info = ", ".join(["%s=%s" % item --- a/.hgignore +++ b/.hgignore @@ -5,3 +5,4 @@ dist/ syntax:glob *.pyc +*$py.class --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -595,19 +595,13 @@ class BaseGateway(object): self._trace('leaving %r' % threading.currentThread()) def _send(self, msg): - if msg is None: - self._io.close_write() - else: - try: - msg.writeto(self._io) - except: - excinfo = self.exc_info() - self._trace(geterrortext(excinfo)) - else: - self._trace('sent -> %r' % msg) + assert isinstance(msg, Message) + msg.writeto(self._io) + self._trace('sent -> %r' % msg) def _stopsend(self): - self._send(None) + self._io.close_write() + self._trace('closing IO') def _stopexec(self): pass From commits-noreply at bitbucket.org Tue Nov 3 18:05:00 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 17:05:00 +0000 (UTC) Subject: [execnet-commit] execnet commit 5302d8c04c02: simplify serialization Message-ID: <20091103170500.B6D3E7EF49@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257267874 -3600 # Node ID 5302d8c04c02a193d1e2463aa0cfd615ebfe32de # Parent 6371d6423d5d28faba17462c154b3282db0c3d99 simplify serialization - automatically generate serialization opcodes encoding - remove VERSION opcode because it's not needed for execnet --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -698,130 +698,6 @@ FOUR_BYTE_INT_MAX = 2147483647 FLOAT_FORMAT = "!d" FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) -# Protocol constants -VERSION_NUMBER = 1 -VERSION = b(chr(VERSION_NUMBER)) -NONE = b('n') -NONE = b('n') -PY2STRING = b('s') -PY3STRING = b('t') -UNICODE = b('u') -BYTES = b('b') -NEWLIST = b('l') -BUILDTUPLE = b('T') -SETITEM = b('m') -NEWDICT = b('d') -INT = b('i') -FLOAT = b('f') -TRUE = b('1') -FALSE = b('0') -STOP = b('S') - -class Serializer(object): - - def __init__(self, stream): - self.stream = stream - - def save(self, obj): - self.stream.write(VERSION) - self._save(obj) - self.stream.write(STOP) - - def _save(self, obj): - tp = type(obj) - try: - dispatch = self.dispatch[tp] - except KeyError: - raise SerializationError("can't serialize %s" % (tp,)) - dispatch(self, obj) - - dispatch = {} - - def save_none(self, non): - self.stream.write(NONE) - dispatch[type(None)] = save_none - - def save_bool(self, boolean): - if boolean: - self.stream.write(TRUE) - else: - self.stream.write(FALSE) - dispatch[bool] = save_bool - - def save_bytes(self, bytes_): - self.stream.write(BYTES) - self._write_byte_sequence(bytes_) - dispatch[type("".encode('ascii'))] = save_bytes - - if ISPY3: - def save_string(self, s): - self.stream.write(PY3STRING) - self._write_unicode_string(s) - else: - def save_string(self, s): - self.stream.write(PY2STRING) - self._write_byte_sequence(s) - - def save_unicode(self, s): - self.stream.write(UNICODE) - self._write_unicode_string(s) - dispatch[unicode] = save_unicode - dispatch[str] = save_string - - def _write_unicode_string(self, s): - try: - as_bytes = s.encode("utf-8") - except UnicodeEncodeError: - raise SerializationError("strings must be utf-8 encodable") - self._write_byte_sequence(as_bytes) - - def _write_byte_sequence(self, bytes_): - self._write_int4(len(bytes_), "string is too long") - self.stream.write(bytes_) - - def save_int(self, i): - self.stream.write(INT) - self._write_int4(i) - dispatch[int] = save_int - if not ISPY3: - dispatch[long] = save_int - - def save_float(self, flt): - self.stream.write(FLOAT) - self.stream.write(struct.pack(FLOAT_FORMAT, flt)) - dispatch[float] = save_float - - def _write_int4(self, i, error="int must be less than %i" % - (FOUR_BYTE_INT_MAX,)): - if i > FOUR_BYTE_INT_MAX: - raise SerializationError(error) - self.stream.write(struct.pack("!i", i)) - - def save_list(self, L): - self.stream.write(NEWLIST) - self._write_int4(len(L), "list is too long") - for i, item in enumerate(L): - self._write_setitem(i, item) - dispatch[list] = save_list - - def _write_setitem(self, key, value): - self._save(key) - self._save(value) - self.stream.write(SETITEM) - - def save_dict(self, d): - self.stream.write(NEWDICT) - for key, value in d.items(): - self._write_setitem(key, value) - dispatch[dict] = save_dict - - def save_tuple(self, tup): - for item in tup: - self._save(item) - self.stream.write(BUILDTUPLE) - self._write_int4(len(tup), "tuple is too long") - dispatch[tuple] = save_tuple - class _UnserializationOptions(object): pass @@ -844,6 +720,7 @@ class _Stop(Exception): pass class Unserializer(object): + num2func = {} # is filled after this class definition def __init__(self, stream, options=UnserializationOptions()): self.stream = stream @@ -851,17 +728,13 @@ class Unserializer(object): def load(self): self.stack = [] - version = ord(self.stream.read(1)) - if version != VERSION_NUMBER: - raise UnserializationError( - "version mismatch: %i != %i" % (version, VERSION_NUMBER)) try: while True: opcode = self.stream.read(1) if not opcode: raise EOFError try: - loader = self.opcodes[opcode] + loader = self.num2func[opcode] except KeyError: raise UnserializationError("unkown opcode %s" % (opcode,)) loader(self) @@ -872,28 +745,22 @@ class Unserializer(object): else: raise UnserializationError("didn't get STOP") - opcodes = {} - def load_none(self): self.stack.append(None) - opcodes[NONE] = load_none def load_true(self): self.stack.append(True) - opcodes[TRUE] = load_true + def load_false(self): self.stack.append(False) - opcodes[FALSE] = load_false def load_int(self): i = self._read_int4() self.stack.append(i) - opcodes[INT] = load_int def load_float(self): binary = self.stream.read(FLOAT_FORMAT_SIZE) self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) - opcodes[FLOAT] = load_float def _read_int4(self): return struct.unpack("!i", self.stream.read(4))[0] @@ -910,7 +777,6 @@ class Unserializer(object): self.stack.append(as_bytes) else: self.stack.append(as_bytes.decode("utf-8")) - opcodes[PY3STRING] = load_py3string def load_py2string(self): as_bytes = self._read_byte_string() @@ -919,21 +785,17 @@ class Unserializer(object): else: s = as_bytes self.stack.append(s) - opcodes[PY2STRING] = load_py2string def load_bytes(self): s = self._read_byte_string() self.stack.append(s) - opcodes[BYTES] = load_bytes def load_unicode(self): self.stack.append(self._read_byte_string().decode("utf-8")) - opcodes[UNICODE] = load_unicode def load_newlist(self): length = self._read_int4() self.stack.append([None] * length) - opcodes[NEWLIST] = load_newlist def load_setitem(self): if len(self.stack) < 3: @@ -941,19 +803,140 @@ class Unserializer(object): value = self.stack.pop() key = self.stack.pop() self.stack[-1][key] = value - opcodes[SETITEM] = load_setitem def load_newdict(self): self.stack.append({}) - opcodes[NEWDICT] = load_newdict def load_buildtuple(self): length = self._read_int4() tup = tuple(self.stack[-length:]) del self.stack[-length:] self.stack.append(tup) - opcodes[BUILDTUPLE] = load_buildtuple def load_stop(self): raise _Stop - opcodes[STOP] = load_stop + +# automatically build opcodes and byte-encoding + +class opcode: + """ container for name -> num mappings. """ + +def _buildopcodes(): + l = [] + for name, func in Unserializer.__dict__.items(): + if name.startswith("load_"): + opname = name[5:].upper() + l.append((opname, func)) + l.sort() + for i,(opname, func) in enumerate(l): + assert i < 26, "xxx" + i = b(chr(64+i)) + Unserializer.num2func[i] = func + setattr(opcode, opname, i) + +_buildopcodes() + +class Serializer(object): + dispatch = {} + + def __init__(self, stream): + self.stream = stream + + def save(self, obj): + self._save(obj) + self.stream.write(opcode.STOP) + + def _save(self, obj): + tp = type(obj) + try: + dispatch = self.dispatch[tp] + except KeyError: + raise SerializationError("can't serialize %s" % (tp,)) + dispatch(self, obj) + + + def save_none(self, non): + self.stream.write(opcode.NONE) + dispatch[type(None)] = save_none + + def save_bool(self, boolean): + if boolean: + self.stream.write(opcode.TRUE) + else: + self.stream.write(opcode.FALSE) + dispatch[bool] = save_bool + + def save_bytes(self, bytes_): + self.stream.write(opcode.BYTES) + self._write_byte_sequence(bytes_) + dispatch[type("".encode('ascii'))] = save_bytes + + if ISPY3: + def save_string(self, s): + self.stream.write(opcode.PY3STRING) + self._write_unicode_string(s) + else: + def save_string(self, s): + self.stream.write(opcode.PY2STRING) + self._write_byte_sequence(s) + + def save_unicode(self, s): + self.stream.write(opcode.UNICODE) + self._write_unicode_string(s) + dispatch[unicode] = save_unicode + dispatch[str] = save_string + + def _write_unicode_string(self, s): + try: + as_bytes = s.encode("utf-8") + except UnicodeEncodeError: + raise SerializationError("strings must be utf-8 encodable") + self._write_byte_sequence(as_bytes) + + def _write_byte_sequence(self, bytes_): + self._write_int4(len(bytes_), "string is too long") + self.stream.write(bytes_) + + def save_int(self, i): + self.stream.write(opcode.INT) + self._write_int4(i) + dispatch[int] = save_int + if not ISPY3: + dispatch[long] = save_int + + def save_float(self, flt): + self.stream.write(opcode.FLOAT) + self.stream.write(struct.pack(FLOAT_FORMAT, flt)) + dispatch[float] = save_float + + def _write_int4(self, i, error="int must be less than %i" % + (FOUR_BYTE_INT_MAX,)): + if i > FOUR_BYTE_INT_MAX: + raise SerializationError(error) + self.stream.write(struct.pack("!i", i)) + + def save_list(self, L): + self.stream.write(opcode.NEWLIST) + self._write_int4(len(L), "list is too long") + for i, item in enumerate(L): + self._write_setitem(i, item) + dispatch[list] = save_list + + def _write_setitem(self, key, value): + self._save(key) + self._save(value) + self.stream.write(opcode.SETITEM) + + def save_dict(self, d): + self.stream.write(opcode.NEWDICT) + for key, value in d.items(): + self._write_setitem(key, value) + dispatch[dict] = save_dict + + def save_tuple(self, tup): + for item in tup: + self._save(item) + self.stream.write(opcode.BUILDTUPLE) + self._write_int4(len(tup), "tuple is too long") + dispatch[tuple] = save_tuple + From commits-noreply at bitbucket.org Tue Nov 3 21:21:27 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:21:27 +0000 (UTC) Subject: [execnet-commit] execnet commit dd5b365f3af7: adding examples and fixing code for __channelexec__ and a new "command" example. Message-ID: <20091103202127.425BC7EF36@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257279619 -3600 # Node ID dd5b365f3af7f7165c50e6b9c576d23d5725e463 # Parent 4322e331bdc52010664b8b5fe14d7b0413f16a9b adding examples and fixing code for __channelexec__ and a new "command" example. --- a/doc/examples.txt +++ b/doc/examples.txt @@ -2,6 +2,47 @@ execnet examples ============================================================================== +execnet is under active development. If you have questions +or ideas or otherwise want to contribute please feel welcome +to join the `execnet-dev`_ mailing list. + +.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev + +Connect to Python2/Numpy from Python3 +---------------------------------------- + +Here we run a Python3 interpreter to connect to a Python2.6 interpreter +that has numpy installed. We send items to be added to an array and +receive back the remote "repr" of the array:: + + import execnet + gw = execnet.PopenGateway("python2.6") + channel = gw.remote_exec(""" + import numpy + array = numpy.array([1,2,3]) + while 1: + x = channel.receive() + if x is None: + break + array = numpy.append(array, x) + channel.send(repr(array)) + """) + for x in range(10): + channel.send(x) + channel.send(None) + print (channel.receive()) + +will print on the CPython3.1 side:: + + array([1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +A more refined real-life example of python3/python2 interaction +is the anyvc_ project which uses version-control bindings in +a Python2 subprocess in order to offer Python3-based library +functionality. + +.. _anyvc: http://bitbucket.org/RonnyPfannschmidt/anyvc/overview/ + Work with Java objects from CPython ---------------------------------------- @@ -68,7 +109,66 @@ A PopenGateway has the same working dire >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") >>> res = ch.receive() >>> assert res == os.getcwd() - >>> gw.exit() + +.. _channelexec: + +Sending modules with remote_exec +-------------------------------------------------------------- + +You can pass a module object to ``remote_exec`` in which case +its source code will be sent. No dependencies will be transferred +so the module must be self-contained or only use modules that are +installed on the "other" side. Module code can detect if it is +running in a remote_exec situation by checking for the special +``__name__`` attribute:: + + # content of a module remote1.py + + if __name__ == '__channelexec__': + channel.send('initialization complete') + +You can now send the module like this:: + + >>> import execnet, remote1 + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remote1) + >>> print (ch.receive()) + +which will print the 'initialization complete' string. + +.. _command: + +A simple command pattern +-------------------------------------------------------------- + +If you want the remote side to serve a number +of synchronous function calls, here is a base +implementation:: + + # contents of: remotecmd.py + def simple(arg): + return arg + 1 + + if __name__ == '__channelexec__': + for item in channel: + funcname = item[0] + func = globals()[funcname] + args = item[1:] + result = func(*args) + channel.send(result) + +Then on the local side you can do:: + + >>> import execnet, remotecmd + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remotecmd) + >>> ch.send(('simple', 10)) # execute func-call remotely + >>> ch.receive() + 11 + +It's straight forward to build a proxy-object +that would hide the details and perform remote +function calls with basic input and output values. Synchronously receive results from two sub processes ----------------------------------------------------- @@ -145,19 +245,3 @@ socketserver:: print socketgw._rinfo() # print some info about the remote environment - -Sending a module / checking if run through remote_exec --------------------------------------------------------------- - -You can pass a module object to ``remote_exec`` in which case -its source code will be sent. No dependencies will be transferred -so the module must be self-contained or only use modules that are -installed on the "other" side. Module code can detect if it is -running in a remote_exec situation by checking for the special -``__name__`` attribute like this:: - - if __name__ == '__channelexec__': - # ... call module functions ... - - - --- a/doc/example/svn-sync-repo.py +++ b/doc/example/svn-sync-repo.py @@ -7,7 +7,7 @@ uses execnet. """ -import py +import execnet import sys, os def usage(): @@ -84,7 +84,7 @@ def main(args): def svn_load(repo, dumpchannel, maxcount=100): # every maxcount we will send an ACK to the other # side in order to synchronise and avoid our side - # growing buffers (py.execnet does not control + # growing buffers (execnet does not control # RAM usage or receive queue sizes) dumpchannel.send(maxcount) f = os.popen("svnadmin load -q %s" %(repo, ), "w") --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -8,6 +8,7 @@ import textwrap import execnet from execnet.gateway_base import Message, Popen2IO, SocketIO from execnet import gateway_base +ModuleType = type(os) debug = False @@ -104,7 +105,10 @@ class Gateway(gateway_base.BaseGateway): and has the sister 'channel' object in its global namespace. """ - source = textwrap.dedent(str(source)) + if isinstance(source, ModuleType): + source = inspect.getsource(source) + else: + source = textwrap.dedent(str(source)) channel = self.newchannel() self._send(Message.CHANNEL_OPEN(channel.id, source)) return channel --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -23,6 +23,15 @@ class TestBasicRemoteExecution: name = channel.receive() assert name == "__channelexec__" + def test_remote_exec_module(self, tmpdir, gw): + p = tmpdir.join("remotetest.py") + p.write("channel.send(1)") + mod = type(os)("remotetest") + mod.__file__ = str(p) + channel = gw.remote_exec(mod) + name = channel.receive() + assert name == 1 + def test_correct_setup_no_py(self, gw): channel = gw.remote_exec(""" import sys --- /dev/null +++ b/doc/example/remotecmd.py @@ -0,0 +1,14 @@ + + +# contents of: remotecmd.py +def simple(arg): + return arg + 1 + +if __name__ == '__channelexec__': + for item in channel: + funcname = item[0] + func = globals()[funcname] + args = item[1:] + result = func(*args) + channel.send(result) + --- /dev/null +++ b/doc/example/py3topy2.py @@ -0,0 +1,17 @@ + +import execnet +gw = execnet.PopenGateway("python2.6") +channel = gw.remote_exec(""" + import numpy + array = numpy.array([1,2,3]) + while 1: + x = channel.receive() + if x is None: + break + array = numpy.append(array, x) + channel.send(repr(array)) +""") +for x in range(10): + channel.send(x) +channel.send(None) +print (channel.receive()) --- a/doc/basics.txt +++ b/doc/basics.txt @@ -48,6 +48,12 @@ Here is an self-contained example for re >>> remote_pid != os.getpid() True +If you'd like to avoid inlining source strings take a look +at the channelexec_ and command_ example. + + +.. _channelexec: examples.html#channelexec +.. _command: examples.html#command .. _`Channel`: .. _`channel-api`: From commits-noreply at bitbucket.org Tue Nov 3 21:21:29 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:21:29 +0000 (UTC) Subject: [execnet-commit] execnet commit dea537cf22d4: automatically find "save_typename" methods instead of redundant registration Message-ID: <20091103202129.131EF7EF37@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257269569 -3600 # Node ID dea537cf22d47f35d430069e02b313d9162667e9 # Parent 5302d8c04c02a193d1e2463aa0cfd615ebfe32de automatically find "save_typename" methods instead of redundant registration --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -837,10 +837,10 @@ def _buildopcodes(): _buildopcodes() class Serializer(object): - dispatch = {} def __init__(self, stream): self.stream = stream + self.dispatch = {} def save(self, obj): self._save(obj) @@ -851,40 +851,38 @@ class Serializer(object): try: dispatch = self.dispatch[tp] except KeyError: - raise SerializationError("can't serialize %s" % (tp,)) - dispatch(self, obj) + methodname = 'save_' + tp.__name__.lower() + meth = getattr(self, methodname, None) + if meth is None: + raise SerializationError("can't serialize %s" % (tp,)) + dispatch = self.dispatch[tp] = meth + dispatch(obj) - - def save_none(self, non): + def save_nonetype(self, non): self.stream.write(opcode.NONE) - dispatch[type(None)] = save_none def save_bool(self, boolean): if boolean: self.stream.write(opcode.TRUE) else: self.stream.write(opcode.FALSE) - dispatch[bool] = save_bool def save_bytes(self, bytes_): self.stream.write(opcode.BYTES) self._write_byte_sequence(bytes_) - dispatch[type("".encode('ascii'))] = save_bytes if ISPY3: - def save_string(self, s): + def save_str(self, s): self.stream.write(opcode.PY3STRING) self._write_unicode_string(s) else: - def save_string(self, s): + def save_str(self, s): self.stream.write(opcode.PY2STRING) self._write_byte_sequence(s) def save_unicode(self, s): self.stream.write(opcode.UNICODE) self._write_unicode_string(s) - dispatch[unicode] = save_unicode - dispatch[str] = save_string def _write_unicode_string(self, s): try: @@ -900,14 +898,11 @@ class Serializer(object): def save_int(self, i): self.stream.write(opcode.INT) self._write_int4(i) - dispatch[int] = save_int - if not ISPY3: - dispatch[long] = save_int + save_long = save_int # only used from python2 def save_float(self, flt): self.stream.write(opcode.FLOAT) self.stream.write(struct.pack(FLOAT_FORMAT, flt)) - dispatch[float] = save_float def _write_int4(self, i, error="int must be less than %i" % (FOUR_BYTE_INT_MAX,)): @@ -920,7 +915,6 @@ class Serializer(object): self._write_int4(len(L), "list is too long") for i, item in enumerate(L): self._write_setitem(i, item) - dispatch[list] = save_list def _write_setitem(self, key, value): self._save(key) @@ -931,12 +925,9 @@ class Serializer(object): self.stream.write(opcode.NEWDICT) for key, value in d.items(): self._write_setitem(key, value) - dispatch[dict] = save_dict def save_tuple(self, tup): for item in tup: self._save(item) self.stream.write(opcode.BUILDTUPLE) self._write_int4(len(tup), "tuple is too long") - dispatch[tuple] = save_tuple - From commits-noreply at bitbucket.org Tue Nov 3 21:21:31 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:21:31 +0000 (UTC) Subject: [execnet-commit] execnet commit 53e5b0719e1a: hide a spurious finalization issue Message-ID: <20091103202131.481AC7EF3A@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257279609 -3600 # Node ID 53e5b0719e1a886799d197988383964e1ad5b9ad # Parent 4983a8c1c244abf3c741ec47426a8e9e5623b478 hide a spurious finalization issue --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -292,7 +292,10 @@ class Channel(object): Msg = Message.CHANNEL_LAST_MESSAGE else: Msg = Message.CHANNEL_CLOSE - self.gateway._send(Msg(self.id)) + try: + self.gateway._send(Msg(self.id)) + except ValueError: # XXX IO operation on closed file, why? + pass def _getremoteerror(self): try: From commits-noreply at bitbucket.org Tue Nov 3 21:21:31 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:21:31 +0000 (UTC) Subject: [execnet-commit] execnet commit 4983a8c1c244: use per-gateway serializers and unserializers Message-ID: <20091103202131.04C7F7EF39@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257270498 -3600 # Node ID 4983a8c1c244abf3c741ec47426a8e9e5623b478 # Parent dea537cf22d47f35d430069e02b313d9162667e9 use per-gateway serializers and unserializers --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -70,18 +70,20 @@ def test_io_message(anypython, tmpdir): temp_out = BytesIO() temp_in = BytesIO() io = Popen2IO(temp_out, temp_in) + serializer = Serializer(io) + unserializer = Unserializer(io) for i, msg_cls in Message._types.items(): print ("checking %s %s" %(i, msg_cls)) for data in "hello", "hello".encode('ascii'): msg1 = msg_cls(i, data) - msg1.writeto(io) + msg1.writeto(serializer) x = io.outfile.getvalue() io.outfile.truncate(0) io.outfile.seek(0) io.infile.seek(0) io.infile.write(x) io.infile.seek(0) - msg2 = Message.readfrom(io) + msg2 = Message.readfrom(unserializer) assert msg1.channelid == msg2.channelid, (msg1, msg2) assert msg1.data == msg2.data print ("all passed") @@ -160,10 +162,12 @@ class TestMessage: def test_wire_protocol(self): for cls in Message._types.values(): one = py.io.BytesIO() + serializer = gateway_base.Serializer(one) data = '23'.encode('ascii') - cls(42, data).writeto(one) + cls(42, data).writeto(serializer) two = py.io.BytesIO(one.getvalue()) - msg = Message.readfrom(two) + unserializer = gateway_base.Unserializer(two) + msg = Message.readfrom(unserializer) assert isinstance(msg, cls) assert msg.channelid == 42 assert msg.data == data --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -133,13 +133,11 @@ class Message: self.channelid = channelid self.data = data - def writeto(self, io): - ser = Serializer(io) - ser.save((self.msgtype, self.channelid, self.data)) + def writeto(self, serializer): + serializer.save((self.msgtype, self.channelid, self.data)) - def readfrom(cls, io): - unser = Unserializer(io) - msgtype, senderid, data = unser.load() + def readfrom(cls, unserializer): + msgtype, senderid, data = unserializer.load() return cls._types[msgtype](senderid, data) readfrom = classmethod(readfrom) @@ -544,6 +542,7 @@ class BaseGateway(object): self._io = io self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = threading.RLock() + self._serializer = Serializer(io) def _initreceive(self): self._receiverthread = threading.Thread(name="receiver", @@ -563,10 +562,11 @@ class BaseGateway(object): def _thread_receiver(self): self._trace("starting to receive") + unserializer = Unserializer(self._io) try: while 1: try: - msg = Message.readfrom(self._io) + msg = Message.readfrom(unserializer) self._trace("received <- %r" % msg) _receivelock = self._receivelock _receivelock.acquire() @@ -596,7 +596,7 @@ class BaseGateway(object): def _send(self, msg): assert isinstance(msg, Message) - msg.writeto(self._io) + msg.writeto(self._serializer) self._trace('sent -> %r' % msg) def _stopsend(self): From commits-noreply at bitbucket.org Tue Nov 3 21:21:31 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:21:31 +0000 (UTC) Subject: [execnet-commit] execnet commit 4322e331bdc5: add and use apipkg.py for lazy loading. Message-ID: <20091103202131.6C3B77EF3B@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257279612 -3600 # Node ID 4322e331bdc52010664b8b5fe14d7b0413f16a9b # Parent 53e5b0719e1a886799d197988383964e1ad5b9ad add and use apipkg.py for lazy loading. --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,12 +4,18 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ +__version__ = "1.0.0b1" -__version__ = "1.0.0b1" -__author__ = "holger krekel and others" +import execnet.apipkg -from execnet.gateway import PopenGateway, SocketGateway, SshGateway -from execnet.gateway import HostNotFound -from execnet.xspec import makegateway, XSpec -from execnet.multi import MultiGateway,MultiChannel -from execnet.rsync import RSync +execnet.apipkg.initpkg(__name__, { + 'PopenGateway': '.gateway:PopenGateway', + 'SocketGateway': '.gateway:SocketGateway', + 'SshGateway': '.gateway:SshGateway', + 'HostNotFound': '.gateway:HostNotFound', + 'makegateway': '.xspec:makegateway', + 'XSpec': '.xspec:XSpec', + 'MultiGateway': '.multi:MultiGateway', + 'MultiChannel': '.multi:MultiChannel', + 'RSync': '.rsync:RSync', +}) --- /dev/null +++ b/execnet/apipkg.py @@ -0,0 +1,69 @@ +""" +apipkg: control the exported namespace of a python package. + +see http://pypi.python.org/pypi/apipkg + +(c) holger krekel, 2009 - MIT license +""" +import sys +from types import ModuleType + +__version__ = "1.0b2" + +def initpkg(pkgname, exportdefs): + """ initialize given package from the export definitions. """ + mod = ApiModule(pkgname, exportdefs, implprefix=pkgname) + oldmod = sys.modules[pkgname] + mod.__file__ = getattr(oldmod, '__file__', None) + mod.__version__ = getattr(oldmod, '__version__', None) + mod.__path__ = getattr(oldmod, '__path__', None) + sys.modules[pkgname] = mod + +def importobj(modpath, attrname): + module = __import__(modpath, None, None, ['__doc__']) + return getattr(module, attrname) + +class ApiModule(ModuleType): + def __init__(self, name, importspec, implprefix=None): + self.__name__ = name + self.__all__ = list(importspec) + self.__map__ = {} + self.__implprefix__ = implprefix or name + for name, importspec in importspec.items(): + if isinstance(importspec, dict): + subname = '%s.%s'%(self.__name__, name) + apimod = ApiModule(subname, importspec, implprefix) + sys.modules[subname] = apimod + setattr(self, name, apimod) + else: + modpath, attrname = importspec.split(':') + if modpath[0] == '.': + modpath = implprefix + modpath + if name == '__doc__': + self.__doc__ = importobj(modpath, attrname) + else: + self.__map__[name] = (modpath, attrname) + + def __repr__(self): + return '' % (self.__name__,) + + def __getattr__(self, name): + try: + modpath, attrname = self.__map__[name] + except KeyError: + raise AttributeError(name) + else: + result = importobj(modpath, attrname) + setattr(self, name, result) + del self.__map__[name] + return result + + def __dict__(self): + # force all the content of the module to be loaded when __dict__ is read + dictdescr = ModuleType.__dict__['__dict__'] + dict = dictdescr.__get__(self) + if dict is not None: + for name in self.__all__: + hasattr(self, name) # force attribute load, ignore errors + return dict + __dict__ = property(__dict__) From commits-noreply at bitbucket.org Tue Nov 3 21:45:13 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:45:13 +0000 (UTC) Subject: [execnet-commit] execnet commit 39e4b3a1397a: packaging a 1.0.0b1 release Message-ID: <20091103204513.68FC87EEFD@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257281076 -3600 # Node ID 39e4b3a1397adad17ecfcd9e65a08521d90b2795 # Parent dd5b365f3af7f7165c50e6b9c576d23d5725e463 packaging a 1.0.0b1 release --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): author='holger krekel and others', author_email='holger at merlinux.eu', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX', --- /dev/null +++ b/MANIFEST @@ -0,0 +1,21 @@ +LICENSE +CHANGELOG +README.txt +setup.py +execnet/__init__.py +execnet/apipkg.py +execnet/gateway.py +execnet/gateway_base.py +execnet/multi.py +execnet/rsync.py +execnet/rsync_remote.py +execnet/serializer.py +execnet/threadpool.py +execnet/xspec.py +execnet/script/__init__.py +execnet/script/loop_socketserver.py +execnet/script/quitserver.py +execnet/script/shell.py +execnet/script/socketserver.py +execnet/script/socketserverservice.py +execnet/script/xx.py --- a/LICENSE +++ b/LICENSE @@ -3,15 +3,15 @@ License (GPL), version 2 or later. See http://www.fsf.org/licensing/licenses/ for more information. -This package contains code and contributions from +This package contains some code and contributions from Armin Rigo, Benjamin Peterson, Carl Friedrich Bolz, Maciej Fijalkowski, Ronny Pfannschmidt and others -which is useable under the MIT license. +which are useable under the MIT license. If you have questions and/or want to use parts of the code under a different license than the GPL -please don't hesitate to contact me. +please contact me. -holger krekel, October 2009, holger at merlinux eu +holger krekel, November 2009, holger at merlinux eu --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ + +1.0.0b1 +---------------------------- + +* added new examples for NumPy, Jython, IronPython +* improved documentation +* include apipkg.py for lazy-importing +* integrated new serializer code from Benjamin Peterson +* improved support for Jython-2.5.1 1.0.0alpha2 ---------------------------- From commits-noreply at bitbucket.org Tue Nov 3 21:45:15 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 3 Nov 2009 20:45:15 +0000 (UTC) Subject: [execnet-commit] execnet commit 0a17bf52bab1: Added tag 1.0.0b1 for changeset 39e4b3a1397a Message-ID: <20091103204515.4CE067EF34@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257281087 -3600 # Node ID 0a17bf52bab1572b21fdb79031fa4a1a43c055c5 # Parent 39e4b3a1397adad17ecfcd9e65a08521d90b2795 Added tag 1.0.0b1 for changeset 39e4b3a1397a --- a/.hgtags +++ b/.hgtags @@ -0,0 +1,1 @@ +39e4b3a1397adad17ecfcd9e65a08521d90b2795 1.0.0b1 From commits-noreply at bitbucket.org Wed Nov 4 16:03:32 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 4 Nov 2009 15:03:32 +0000 (UTC) Subject: [execnet-commit] execnet commit 0868fc4f0c55: tweaks to the home page Message-ID: <20091104150332.3E5F47EEFE@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257282806 -3600 # Node ID 0868fc4f0c550dfc33cfaaefaeb89850443dd6da # Parent 0a17bf52bab1572b21fdb79031fa4a1a43c055c5 tweaks to the home page --- a/doc/index.txt +++ b/doc/index.txt @@ -15,7 +15,7 @@ package installation is only required at interoperation between CPython 2.4-3.1, Jython 2.5.1, PyPy 1.1 and IronPython and works well on Windows, Linux and OSX systems. -execnet was written and is maintained by `Holger Krekel`_ with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +execnet was written and is maintained by `Holger Krekel`_ (blog_) with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. .. note:: Support of Jython and IronPython are experimental at this point. @@ -25,7 +25,7 @@ execnet was written and is maintained by Getting Started ==================== -To install a public `pypi release`_ via `easy_install`_:: +Install a public `pypi release`_ via `easy_install`_ or ``pip``:: easy_install -U execnet @@ -49,7 +49,8 @@ You are welcome to: * join the #pylib IRC channel on Freenode * subscribe to the `execnet-commit`_ mailing list -.. _`Holger Krekel`: http://tetamap.wordpress.com +.. _`Holger Krekel`: http://twitter.com/hpk42 +.. _`blog`: http://tetamap.wordpress.com .. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev .. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit .. _`open an issue`: http://bitbucket.org/hpk42/execnet/issues/ From commits-noreply at bitbucket.org Wed Nov 4 16:03:34 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 4 Nov 2009 15:03:34 +0000 (UTC) Subject: [execnet-commit] execnet commit 22303634ba47: make tracing dependent on existence of EXECNET_DEBUG instead of module-global Message-ID: <20091104150334.6AF877EEFC@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257346995 -3600 # Node ID 22303634ba47b233b76f2f4bbf500dea288d0359 # Parent 0868fc4f0c550dfc33cfaaefaeb89850443dd6da make tracing dependent on existence of EXECNET_DEBUG instead of module-global --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -39,8 +39,8 @@ class Gateway(gateway_base.BaseGateway): _cleanup = GatewayCleanup() def __init__(self, io): + super(Gateway, self).__init__(io=io, _startcount=1) self._remote_bootstrap_gateway(io) - super(Gateway, self).__init__(io=io, _startcount=1) self._initreceive() self._cleanup.register(self) @@ -51,15 +51,14 @@ class Gateway(gateway_base.BaseGateway): else: addr = '' try: - r = (self._receiverthread.isAlive() and "receiving" or + r = (self._receiverthread.isAlive() and "receiver-alive" or "not receiving") - s = "sending" # XXX i = len(self._channelfactory.channels()) except AttributeError: - r = s = "uninitialized" + r = "uninitialized" i = "no" - return "<%s%s %s/%s (%s active channels)>" %( - self.__class__.__name__, addr, r, s, i) + return "<%s%s %s (%s active channels)>" %( + self.__class__.__name__, addr, r, i) def exit(self): """ Try to stop all exec and IO activity. """ @@ -261,7 +260,7 @@ class SocketGateway(Gateway): # execute the above socketserverbootstrap on the other side channel = gateway.remote_exec(socketserverbootstrap) (realhost, realport) = channel.receive() - #gateway._trace("new_remote received" + #self._trace("new_remote received" # "port=%r, hostname = %r" %(realport, hostname)) return SocketGateway(host, realport) new_remote = classmethod(new_remote) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -545,7 +545,13 @@ class TestThreads: gw.remote_init_threads(3) py.test.raises(IOError, gw.remote_init_threads, 3) +def test_debug(monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "1") + source = py.std.inspect.getsource(gateway_base) + d = {} + gateway_base.do_exec(source, d) + assert 'debugfile' in d def test_nodebug(): from execnet import gateway_base - assert not gateway_base.debug + assert not hasattr(gateway_base, 'debugfile') --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -22,10 +22,21 @@ else: "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str -default_encoding = "UTF-8" sysex = (KeyboardInterrupt, SystemExit) -debug = 0 # open('/tmp/execnet-debug-%d' % os.getpid() , 'w') +if os.environ.get('EXECNET_DEBUG'): + debugfile = open('/tmp/execnet-debug-%d' % os.getpid() , 'w') + def trace(msg): + try: + debugfile.write(unicode(msg) + "\n") + debugfile.flush() + except sysex: + raise + except: + sys.stderr.write("exception during tracing\n") +else: + def trace(msg): + pass # ___________________________________________________________________________ @@ -546,6 +557,7 @@ class BaseGateway(object): self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = threading.RLock() self._serializer = Serializer(io) + self._trace = trace # globals may be NONE at process-termination def _initreceive(self): self._receiverthread = threading.Thread(name="receiver", @@ -553,16 +565,6 @@ class BaseGateway(object): self._receiverthread.setDaemon(1) self._receiverthread.start() - def _trace(self, msg): - if debug: - try: - debug.write(unicode(msg) + "\n") - debug.flush() - except sysex: - raise - except: - sys.stderr.write("exception during tracing\n") - def _thread_receiver(self): self._trace("starting to receive") unserializer = Unserializer(self._io) @@ -650,7 +652,7 @@ class SlaveGateway(BaseGateway): except self._StopExecLoop: break finally: - self._trace("serve") + self._trace("serving execution finished") if joining: self.join() --- a/doc/basics.txt +++ b/doc/basics.txt @@ -133,3 +133,12 @@ More info on the RSync class: .. currentmodule:: execnet .. autoclass:: RSync :members: add_target,send + + +Debugging execnet +=============================================================== + +If you set the enviornment variable ``EXECNET_DEBUG`` to any value +trace-files will be written to ``execnet-debug-PID`` files in +the system temporary directory. ``NUM`` will be the process id. + --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +1.0.0b2 +-------------------------------- +* setting the environment variable EXECNET_DEBUG will generate per + process trace-files for debugging 1.0.0b1 ---------------------------- From commits-noreply at bitbucket.org Wed Nov 4 16:27:50 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 4 Nov 2009 15:27:50 +0000 (UTC) Subject: [execnet-commit] execnet commit 413beb635298: fix empty tuple-handling (thanks ronny) Message-ID: <20091104152750.AA4507EEF5@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257348460 -3600 # Node ID 413beb635298066aae894a07dfb1cadee7ccdb3a # Parent 22303634ba47b233b76f2f4bbf500dea288d0359 fix empty tuple-handling (thanks ronny) --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -202,3 +202,9 @@ def test_none(py2, py3): assert s == "None" tp, s = py3.load(p) assert s == "None" + +def test_tuple_nested_with_empty_in_between(py2): + p = py2.dump("(1, (), 3)") + tp, s = py2.load(p) + assert tp == 'tuple' + assert s == "(1, (), 3)" --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -814,8 +814,11 @@ class Unserializer(object): def load_buildtuple(self): length = self._read_int4() - tup = tuple(self.stack[-length:]) - del self.stack[-length:] + if length: + tup = tuple(self.stack[-length:]) + del self.stack[-length:] + else: + tup = () self.stack.append(tup) def load_stop(self): --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ 1.0.0b2 -------------------------------- +* fix a seralization bug with nested tuples containing empty tuples + (thanks to ronny for discovering it) + * setting the environment variable EXECNET_DEBUG will generate per process trace-files for debugging From commits-noreply at bitbucket.org Wed Nov 4 17:34:09 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 4 Nov 2009 16:34:09 +0000 (UTC) Subject: [execnet-commit] execnet commit b66d332b2ad8: reduce boilerplate around options, add a test, Message-ID: <20091104163409.777497EEE8@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257352382 -3600 # Node ID b66d332b2ad8afbab89288d4d1989b7c86fae1c5 # Parent 413beb635298066aae894a07dfb1cadee7ccdb3a reduce boilerplate around options, add a test, try harder to import correct execnet, bump version. --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -28,16 +28,11 @@ def setup_module(mod): else: mod._py3_wrapper = PythonWrapper(_find_version("3")) mod._py2_wrapper = PythonWrapper(py.path.local(sys.executable)) - mod._old_pypath = os.environ.get("PYTHONPATH") - p = os.path.dirname(os.path.dirname(execnet.__file__)) - os.environ["PYTHONPATH"] = p def teardown_module(mod): TEMPDIR.remove(True) - if _old_pypath is not None: - os.environ["PYTHONPATH"] = _old_pypath - +pyimportdir = str(py.path.local(execnet.__file__).dirpath().dirpath()) class PythonWrapper(object): def __init__(self, executable): @@ -46,26 +41,28 @@ class PythonWrapper(object): def dump(self, obj_rep): script_file = TEMPDIR.join("dump.py") script_file.write(""" +import sys +sys.path.insert(0, %r) from execnet import gateway_base as serializer -import sys if sys.version_info > (3, 0): # Need binary output sys.stdout = sys.stdout.detach() saver = serializer.Serializer(sys.stdout) -saver.save(%s)""" % (obj_rep,)) +saver.save(%s)""" % (pyimportdir, obj_rep,)) return self.executable.sysexec(script_file) - def load(self, data, option_args=""): + def load(self, data, option_args="__class__"): script_file = TEMPDIR.join("load.py") script_file.write(r""" +import sys +sys.path.insert(0, %r) from execnet import gateway_base as serializer -import sys if sys.version_info > (3, 0): sys.stdin = sys.stdin.detach() -options = serializer.UnserializationOptions(%s) -loader = serializer.Unserializer(sys.stdin, options) +loader = serializer.Unserializer(sys.stdin) +loader.%s obj = loader.load() sys.stdout.write(type(obj).__name__ + "\n") -sys.stdout.write(repr(obj))""" % (option_args,)) +sys.stdout.write(repr(obj))""" % (pyimportdir, option_args,)) popen = subprocess.Popen([str(self.executable), str(script_file)], stdin=subprocess.PIPE, stderr=subprocess.PIPE, @@ -152,14 +149,17 @@ def test_bytes(py2, py3): assert tp == "bytes" assert v == "b'hi'" -def test_string(py2, py3): +def test_str(py2, py3): p = py2.dump("'xyz'") tp, s = py2.load(p) assert tp == "str" assert s == "'xyz'" - tp, s = py3.load(p) - assert tp == "str" # depends on unserialization defaults + tp, s = py3.load(p, "py2str_as_py3str=True") + assert tp == "str" assert s == "'xyz'" + tp, s = py3.load(p, "py2str_as_py3str=False") + assert s == "b'xyz'" + assert tp == "bytes" def test_unicode(py2, py3): p = py2.dump("u'hi'") --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -703,33 +703,16 @@ FOUR_BYTE_INT_MAX = 2147483647 FLOAT_FORMAT = "!d" FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) -class _UnserializationOptions(object): - pass - -class _Py2UnserializationOptions(_UnserializationOptions): - - def __init__(self, py3_strings_as_str=False): - self.py3_strings_as_str = py3_strings_as_str - -class _Py3UnserializationOptions(_UnserializationOptions): - - def __init__(self, py2_strings_as_str=True): - self.py2_strings_as_str = py2_strings_as_str - -if ISPY3: - UnserializationOptions = _Py3UnserializationOptions -else: - UnserializationOptions = _Py2UnserializationOptions - class _Stop(Exception): pass class Unserializer(object): num2func = {} # is filled after this class definition + py2str_as_py3str = False # True + py3str_as_py2str = False # false means py2 will get unicode - def __init__(self, stream, options=UnserializationOptions()): + def __init__(self, stream): self.stream = stream - self.options = options def load(self): self.stack = [] @@ -777,7 +760,7 @@ class Unserializer(object): def load_py3string(self): as_bytes = self._read_byte_string() - if not ISPY3 and self.options.py3_strings_as_str: + if not ISPY3 and self.py3str_as_py2str: # XXX Should we try to decode into latin-1? self.stack.append(as_bytes) else: @@ -785,7 +768,7 @@ class Unserializer(object): def load_py2string(self): as_bytes = self._read_byte_string() - if ISPY3 and self.options.py2_strings_as_str: + if ISPY3 and self.py2str_as_py3str: s = as_bytes.decode("latin-1") else: s = as_bytes --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0b1" +__version__ = "1.0.0b2" import execnet.apipkg From commits-noreply at bitbucket.org Wed Nov 4 20:16:20 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 4 Nov 2009 19:16:20 +0000 (UTC) Subject: [execnet-commit] execnet commit f92bb37b9b25: only write to the stream if serialization succeeded in toto. Message-ID: <20091104191620.5A7127EEEB@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257362151 -3600 # Node ID f92bb37b9b25c5aa6002627ba9e3d2da4ff5703e # Parent b66d332b2ad8afbab89288d4d1989b7c86fae1c5 only write to the stream if serialization succeeded in toto. --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -157,6 +157,22 @@ def test_stdouterrin_setnull(): assert not out assert not err +class PseudoChannel: + def __init__(self): + self._sent = [] + self._closed = [] + def send(self, obj): + self._sent.append(obj) + def close(self, errortext=None): + self._closed.append(errortext) + +def test_exectask(): + io = py.io.BytesIO() + gw = gateway_base.SlaveGateway(io) + ch = PseudoChannel() + gw.executetask((ch, "raise ValueError()")) + assert "ValueError" in str(ch._closed[0]) + class TestMessage: def test_wire_protocol(self): --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -828,14 +828,23 @@ def _buildopcodes(): _buildopcodes() class Serializer(object): + WRITE_ON_SUCCESS=True # more robust against serialize failures def __init__(self, stream): - self.stream = stream self.dispatch = {} + self._stream = stream def save(self, obj): + if self.WRITE_ON_SUCCESS: + self.streamlist = [] + self._write = self.streamlist.append + else: + self._write = self.stream.write self._save(obj) - self.stream.write(opcode.STOP) + self._write(opcode.STOP) + if self.WRITE_ON_SUCCESS: + for x in self.streamlist: + self._stream.write(x) def _save(self, obj): tp = type(obj) @@ -850,29 +859,29 @@ class Serializer(object): dispatch(obj) def save_nonetype(self, non): - self.stream.write(opcode.NONE) + self._write(opcode.NONE) def save_bool(self, boolean): if boolean: - self.stream.write(opcode.TRUE) + self._write(opcode.TRUE) else: - self.stream.write(opcode.FALSE) + self._write(opcode.FALSE) def save_bytes(self, bytes_): - self.stream.write(opcode.BYTES) + self._write(opcode.BYTES) self._write_byte_sequence(bytes_) if ISPY3: def save_str(self, s): - self.stream.write(opcode.PY3STRING) + self._write(opcode.PY3STRING) self._write_unicode_string(s) else: def save_str(self, s): - self.stream.write(opcode.PY2STRING) + self._write(opcode.PY2STRING) self._write_byte_sequence(s) def save_unicode(self, s): - self.stream.write(opcode.UNICODE) + self._write(opcode.UNICODE) self._write_unicode_string(s) def _write_unicode_string(self, s): @@ -884,25 +893,25 @@ class Serializer(object): def _write_byte_sequence(self, bytes_): self._write_int4(len(bytes_), "string is too long") - self.stream.write(bytes_) + self._write(bytes_) def save_int(self, i): - self.stream.write(opcode.INT) + self._write(opcode.INT) self._write_int4(i) save_long = save_int # only used from python2 def save_float(self, flt): - self.stream.write(opcode.FLOAT) - self.stream.write(struct.pack(FLOAT_FORMAT, flt)) + self._write(opcode.FLOAT) + self._write(struct.pack(FLOAT_FORMAT, flt)) def _write_int4(self, i, error="int must be less than %i" % (FOUR_BYTE_INT_MAX,)): if i > FOUR_BYTE_INT_MAX: raise SerializationError(error) - self.stream.write(struct.pack("!i", i)) + self._write(struct.pack("!i", i)) def save_list(self, L): - self.stream.write(opcode.NEWLIST) + self._write(opcode.NEWLIST) self._write_int4(len(L), "list is too long") for i, item in enumerate(L): self._write_setitem(i, item) @@ -910,15 +919,15 @@ class Serializer(object): def _write_setitem(self, key, value): self._save(key) self._save(value) - self.stream.write(opcode.SETITEM) + self._write(opcode.SETITEM) def save_dict(self, d): - self.stream.write(opcode.NEWDICT) + self._write(opcode.NEWDICT) for key, value in d.items(): self._write_setitem(key, value) def save_tuple(self, tup): for item in tup: self._save(item) - self.stream.write(opcode.BUILDTUPLE) + self._write(opcode.BUILDTUPLE) self._write_int4(len(tup), "tuple is too long") --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -11,6 +11,11 @@ queue = py.builtin._tryimport('queue', ' TESTTIMEOUT = 10.0 # seconds skiponjython = py.test.mark.skipif("sys.platform.startswith('java')") +def test_serialize_error(gw): + ch = gw.remote_exec("channel.send(ValueError(42))") + excinfo = py.test.raises(ch.RemoteError, "ch.receive()") + assert "can't serialize" in str(excinfo.value) + class TestBasicRemoteExecution: def test_correct_setup(self, gw): assert gw._receiverthread.isAlive() --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ 1.0.0b2 -------------------------------- + +* make internal protocols more robust against serialization failures + * fix a seralization bug with nested tuples containing empty tuples (thanks to ronny for discovering it) From commits-noreply at bitbucket.org Sun Nov 8 20:38:49 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 8 Nov 2009 19:38:49 +0000 (UTC) Subject: [execnet-commit] execnet commit d157364437d2: fixing some docs, removing issue tracker. Message-ID: <20091108193849.7F70A7EED0@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257530358 -3600 # Node ID d157364437d2b0816ebbbbe1f6152d32edea0bf0 # Parent f92bb37b9b25c5aa6002627ba9e3d2da4ff5703e fixing some docs, removing issue tracker. --- a/doc/index.txt +++ b/doc/index.txt @@ -40,22 +40,23 @@ Next checkout the basic api and examples examples changelog -Contact channels +Issues / Contact channels =========================== -You are welcome to: +If you have a questions, issues or suggestions you are welcome to join +and post to the `execnet-dev`_ mailing list. Alternatively you +you get see to help on the #pylib IRC channel on Freenode. -* join and post to the `execnet-dev`_ mailing list or `open an issue`_ -* join the #pylib IRC channel on Freenode -* subscribe to the `execnet-commit`_ mailing list +There also is the `execnet-commit`_ mailing list +which relays commit mails to the `bitbucket repository`_. .. _`Holger Krekel`: http://twitter.com/hpk42 .. _`blog`: http://tetamap.wordpress.com .. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev .. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit -.. _`open an issue`: http://bitbucket.org/hpk42/execnet/issues/ .. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall +.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet/ .. _`execnet mercurial repository`: http://bitbucket.org/hpk42/execnet/ .. _`pypi release`: http://pypi.python.org/pypi/execnet --- a/doc/basics.txt +++ b/doc/basics.txt @@ -2,11 +2,9 @@ execnet API in a nutshell ============================================================================== -execnet allows to create **gateways** which establish -a self-bootstrapping connection to another -local or remote Python interpreter. Through this -connection you can execute code on the other side -and maintain a data exchange via channels. +execnet ad-hoc instantiates **gateways** to Python +interpreter processes with which you can **remote execute +code** and exchange basic python objects through **channels**. .. image:: _static/basic1.png @@ -71,7 +69,7 @@ two asynchronously running programs. .. automethod:: Channel.send(item) .. automethod:: Channel.receive() - .. automethod:: Channel.setcallback(callback, endmarker) + .. automethod:: Channel.setcallback(callback, endmarker=_NOENDMARKER) .. automethod:: Channel.makefile(mode, proxyclose=False) .. automethod:: Channel.close(error) .. automethod:: Channel.waitclose(timeout) --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -2,10 +2,10 @@ {% block rootrellink %}
  • home | 
  • -
  • api | 
  • +
  • basics | 
  • examples | 
  • -
  • issues | 
  • -
  • pypi | 
  • +
  • mailing-list | 
  • +
  • pypi-home | 
  • docindex»
  • {% endblock %} From commits-noreply at bitbucket.org Sun Nov 8 20:38:51 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 8 Nov 2009 19:38:51 +0000 (UTC) Subject: [execnet-commit] execnet commit 14fb410db7ef: * disallow closing a channel from within remote_exec code Message-ID: <20091108193851.2E29A7EEE1@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257703819 -3600 # Node ID 14fb410db7efb8259f982e430463c31f34071865 # Parent d157364437d2b0816ebbbbe1f6152d32edea0bf0 * disallow closing a channel from within remote_exec code * add more tracing * bump version --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -61,13 +61,15 @@ class Gateway(gateway_base.BaseGateway): self.__class__.__name__, addr, r, i) def exit(self): - """ Try to stop all exec and IO activity. """ + """ exit gateway (stop local execution, stop sending)""" + self._trace("exiting gateway") try: self._cleanup.unregister(self) except KeyError: return # we assume it's already happened self._stopexec() self._stopsend() + #self.join(timeout=timeout) # receiverthread receive close() messages def _remote_bootstrap_gateway(self, io, extra=''): """ return Gateway with a asynchronously remotely @@ -186,6 +188,7 @@ class PopenCmdGateway(Gateway): def exit(self): super(PopenCmdGateway, self).exit() + self._trace("polling Popen subprocess") self._popen.poll() popen_bootstrapline = "import sys ; exec(eval(sys.stdin.readline()))" --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -234,6 +234,7 @@ NO_ENDMARKER_WANTED = object() class Channel(object): """Communication channel between two Python Interpreter execution points.""" RemoteError = RemoteError + _executing = False def __init__(self, gateway, id): assert isinstance(id, int) @@ -335,7 +336,14 @@ class Channel(object): raise ValueError("mode %r not availabe" %(mode,)) def close(self, error=None): - """ close down this channel with an optional error message. """ + """ close down this channel with an optional error message. + Note that closing of a channel tied to remote_exec happens + automatically at the end of execution and cannot be done explicitely. + """ + if self._executing: + raise IOError("cannot explicitly close channel within remote_exec") + if self._closed: + trace("%r channel already closed, close() called." % (self, )) if not self._closed: # state transition "opened/sendonly" --> "closed" # threads warning: the channel might be closed under our feet, @@ -345,6 +353,7 @@ class Channel(object): put(Message.CHANNEL_CLOSE_ERROR(self.id, error)) else: put(Message.CHANNEL_CLOSE(self.id)) + trace("%r: sent channel close message" %(self,)) if isinstance(error, RemoteError): self._remoteerrors.append(error) self._closed = True # --> "closed" @@ -584,7 +593,8 @@ class BaseGateway(object): except EOFError: break except: - self._trace(geterrortext(self.exc_info())) + self._trace("RECEIVERTHREAD: %s " %( + geterrortext(self.exc_info()), )) break finally: # XXX we need to signal fatal error states to @@ -606,7 +616,7 @@ class BaseGateway(object): def _stopsend(self): self._io.close_write() - self._trace('closing IO') + self._trace('closed IO write pipe') def _stopexec(self): pass @@ -622,14 +632,15 @@ class BaseGateway(object): def newchannel(self): return self._channelfactory.new() - def join(self, joinexec=True): - """ Wait for all IO (and by default all execution activity) - to stop. the joinexec parameter is obsolete. - """ + def join(self, timeout=None): + """ Wait for receiverthread to terminate. """ current = threading.currentThread() if self._receiverthread.isAlive(): self._trace("joining receiver thread") - self._receiverthread.join() + self._receiverthread.join(timeout) + else: + self._trace("gateway.join() called while receiverthread " + "already finished") class SlaveGateway(BaseGateway): def _stopexec(self): @@ -652,7 +663,7 @@ class SlaveGateway(BaseGateway): except self._StopExecLoop: break finally: - self._trace("serving execution finished") + self._trace("slavegateway.serve finished") if joining: self.join() @@ -661,19 +672,19 @@ class SlaveGateway(BaseGateway): try: loc = {'channel' : channel, '__name__': '__channelexec__'} self._trace("execution starts: %s" % repr(source)[:50]) + channel._executing = True try: co = compile(source+'\n', '', 'exec') do_exec(co, loc) finally: + channel._executing = False self._trace("execution finished") - except sysex: - pass except self._StopExecLoop: channel.close() raise except: excinfo = self.exc_info() - self._trace("got exception %s" % excinfo[1]) + self._trace("got exception: %s" % (excinfo[1],)) errortext = geterrortext(excinfo) channel.close(errortext) else: --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0b2" +__version__ = "1.0.0b3" import execnet.apipkg --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -63,6 +63,12 @@ class TestBasicRemoteExecution: channel.waitclose(TESTTIMEOUT) py.test.raises(IOError, channel.send, 0) + def test_remote_exec_no_explicit_close(self, gw): + channel = gw.remote_exec('channel.close()') + excinfo = py.test.raises(channel.RemoteError, + "channel.waitclose(TESTTIMEOUT)") + assert "explicit" in excinfo.value.formatted + def test_remote_exec_channel_anonymous(self, gw): channel = gw.remote_exec(''' obj = channel.receive() @@ -408,7 +414,7 @@ class TestChannelFile: gw._cache_rinfo = rinfo gw.remote_exec("import os ; os.chdir(%r)" % old).waitclose() -def test_join_blocked_execution_gateway(): +def test_join_blocked_slave_execution_gateway(): gateway = execnet.PopenGateway() channel = gateway.remote_exec(""" import time @@ -416,7 +422,7 @@ def test_join_blocked_execution_gateway( """) def doit(): gateway.exit() - gateway.join() + gateway.join(timeout=3.0) return 17 pool = WorkerPool() --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +1.0.0b3 +-------------------------------- + +* disallow explicit close in remote_exec situation +* perform some more detailed tracing with EXECNET_DEBUG + 1.0.0b2 -------------------------------- From commits-noreply at bitbucket.org Mon Nov 9 16:08:45 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 15:08:45 +0000 (UTC) Subject: [execnet-commit] execnet commit 38c616690338: make remote_status work with remote_init_threads Message-ID: <20091109150845.698207EF10@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257779282 -3600 # Node ID 38c6166903389d1dc29a020c53cb4e069bec79c4 # Parent 06731e9242279b7b5877aae1168303076f50a83f make remote_status work with remote_init_threads also fix atomic-send behaviour and add some more tracing --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -106,7 +106,7 @@ class Gateway(gateway_base.BaseGateway): statusdict = channel.receive() # the other side didn't actually instantiate a channel # so we just delete the internal id/channel mapping - self._channelfactory._no_longer_opened(channel.id) + self._channelfactory._local_close(channel.id) return RemoteStatus(statusdict) def remote_exec(self, source): --- a/execnet/threadpool.py +++ b/execnet/threadpool.py @@ -222,11 +222,15 @@ if __name__ == '__channelexec__': maxthreads = channel.receive() execpool = WorkerPool(maxthreads=maxthreads) gw = channel.gateway + gw._trace("instantiated thread work pool maxthreads=%s" %(maxthreads,)) while 1: + gw._trace("waiting for new exec task") task = gw._execqueue.get() if task is None: - gw._stopsend() + gw._trace("thread-dispatcher got None, exiting") execpool.shutdown() execpool.join() + gw._stopsend() raise gw._StopExecLoop + gw._trace("dispatching exec task to thread pool") execpool.dispatch(gw.executetask, task) --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -161,6 +161,7 @@ class PseudoChannel: def __init__(self): self._sent = [] self._closed = [] + self.id = 1000 def send(self, obj): self._sent.append(obj) def close(self, errortext=None): --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -687,7 +687,7 @@ class SlaveGateway(BaseGateway): channel, source = item try: loc = {'channel' : channel, '__name__': '__channelexec__'} - self._trace("execution starts: %s" % repr(source)[:50]) + self._trace("execution starts[%s]: %s" % (channel.id, repr(source)[:50])) channel._executing = True try: co = compile(source+'\n', '', 'exec') @@ -751,7 +751,8 @@ class Unserializer(object): try: loader = self.num2func[opcode] except KeyError: - raise UnserializationError("unkown opcode %s" % (opcode,)) + raise UnserializationError("unkown opcode %r - " + "wire protocol corruption?" % (opcode,)) loader(self) except _Stop: if len(self.stack) != 1: @@ -870,8 +871,9 @@ class Serializer(object): self._save(obj) self._write(opcode.STOP) if self.WRITE_ON_SUCCESS: - for x in self.streamlist: - self._stream.write(x) + # atomic write! (compatible to python3 and python2) + s = type(self.streamlist[0])().join(self.streamlist) + self._stream.write(s) def _save(self, obj): tp = type(obj) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -38,6 +38,9 @@ class TestBasicRemoteExecution: numchan = gw._channelfactory.channels() st = gw.remote_status() numchan2 = gw._channelfactory.channels() + # note that on CPython this can not really + # fail because refcounting leads to immediate + # closure of temporary channels assert numchan2 == numchan def test_gateway_status_busy(self, gw): @@ -582,6 +585,24 @@ class TestThreads: res = c1.receive() assert res == 42 + def test_status_with_threads(self): + gw = execnet.PopenGateway() + gw.remote_init_threads(3) + c1 = gw.remote_exec("channel.send(1) ; channel.receive()") + c2 = gw.remote_exec("channel.send(2) ; channel.receive()") + c1.receive() + c2.receive() + rstatus = gw.remote_status() + assert rstatus.numexecuting == 2 + 1 + assert rstatus.execqsize == 0 + c1.send(1) + c2.send(1) + c1.waitclose() + c2.waitclose() + rstatus = gw.remote_status() + assert rstatus.numexecuting == 0 + 1 + assert rstatus.execqsize == 0 + def test_threads_twice(self): gw = execnet.PopenGateway() gw.remote_init_threads(3) From commits-noreply at bitbucket.org Mon Nov 9 16:08:45 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 15:08:45 +0000 (UTC) Subject: [execnet-commit] execnet commit 06731e924227: introduce slightly experimental remote_status method Message-ID: <20091109150845.389737EF0E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257779140 -3600 # Node ID 06731e9242279b7b5877aae1168303076f50a83f # Parent 14fb410db7efb8259f982e430463c31f34071865 introduce slightly experimental remote_status method to get information about the remote execution status --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -100,6 +100,15 @@ class Gateway(gateway_base.BaseGateway): self._cache_rinfo = RInfo(ch.receive()) return self._cache_rinfo + def remote_status(self): + channel = self.newchannel() + self._send(Message.STATUS(channel.id)) + statusdict = channel.receive() + # the other side didn't actually instantiate a channel + # so we just delete the internal id/channel mapping + self._channelfactory._no_longer_opened(channel.id) + return RemoteStatus(statusdict) + def remote_exec(self, source): """ return channel object and connect it to a remote execution thread where the given 'source' executes @@ -159,7 +168,6 @@ class Gateway(gateway_base.BaseGateway): return Handle() - class RInfo: def __init__(self, kwargs): self.__dict__.update(kwargs) @@ -168,6 +176,8 @@ class RInfo: for item in self.__dict__.items()]) return "" % info +RemoteStatus = RInfo + rinfo_source = """ import sys, os channel.send(dict( --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -162,6 +162,22 @@ class Message: self.channelid, self.data) def _setupmessages(): + class STATUS(Message): + def received(self, gateway): + # we use self.channelid to send back information + # but don't instantiate a channel object + active_channels = gateway._channelfactory.channels() + numexec = 0 + for ch in active_channels: + if getattr(ch, '_executing', False): + numexec += 1 + d = {'receiving': True, + 'execqsize': gateway._execqueue.qsize(), + 'numchannels': len(active_channels), + 'numexecuting': numexec + } + gateway._send(Message.CHANNEL_DATA(self.channelid, d)) + class CHANNEL_OPEN(Message): def received(self, gateway): channel = gateway._channelfactory.new(self.channelid) @@ -191,7 +207,7 @@ def _setupmessages(): def received(self, gateway): gateway._channelfactory._local_close(self.channelid, sendonly=True) - classes = [CHANNEL_OPEN, CHANNEL_NEW, CHANNEL_DATA, + classes = [STATUS, CHANNEL_OPEN, CHANNEL_NEW, CHANNEL_DATA, CHANNEL_CLOSE, CHANNEL_CLOSE_ERROR, CHANNEL_LAST_MESSAGE] for i, cls in enumerate(classes): --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -28,6 +28,37 @@ class TestBasicRemoteExecution: name = channel.receive() assert name == "__channelexec__" + def test_gateway_status_simple(self, gw): + status = gw.remote_status() + assert status.receiving + assert not status.execqsize + assert status.numexecuting == 0 + + def test_gateway_status_no_real_channel(self, gw): + numchan = gw._channelfactory.channels() + st = gw.remote_status() + numchan2 = gw._channelfactory.channels() + assert numchan2 == numchan + + def test_gateway_status_busy(self, gw): + ch1 = gw.remote_exec("channel.send(1); channel.receive()") + ch2 = gw.remote_exec("channel.receive()") + ch1.receive() + status = gw.remote_status() + assert status.receiving + assert status.numexecuting == 1 # number of active execution threads + assert status.execqsize == 1 # one more queued + assert status.numchannels == 2 + ch1.send(None) + ch2.send(None) + ch1.waitclose() + ch2.waitclose() + status = gw.remote_status() + assert status.receiving + assert status.execqsize == 0 + assert status.numexecuting == 0 + assert status.numchannels == 0 + def test_remote_exec_module(self, tmpdir, gw): p = tmpdir.join("remotetest.py") p.write("channel.send(1)") --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 1.0.0b3 -------------------------------- +* introduce remote_status() method which on the low level gives + information about the remote side of a gateway * disallow explicit close in remote_exec situation * perform some more detailed tracing with EXECNET_DEBUG From commits-noreply at bitbucket.org Mon Nov 9 22:48:44 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 21:48:44 +0000 (UTC) Subject: [execnet-commit] execnet commit ac44b2b936df: add support for set and frozenset Message-ID: <20091109214844.304657EF0B@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User Benjamin Peterson # Date 1257803298 21600 # Node ID ac44b2b936dfa797622ae22c117030c9e26b263d # Parent 38c6166903389d1dc29a020c53cb4e069bec79c4 add support for set and frozenset --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -125,6 +125,39 @@ def test_simple(tp_name, repr, dump, loa assert tp == tp_name assert v == repr +def test_set(py2, py3): + for dump in py2.dump, py3.dump: + p = dump("set((1, 2, 3))") + tp, v = py2.load(p) + assert tp == "set" + assert v == "set([1, 2, 3])" + tp, v = py3.load(p) + assert tp == "set" + assert v == "{1, 2, 3}" + p = dump("set()") + tp, v = py2.load(p) + assert tp == "set" + assert v == "set([])" + tp, v = py3.load(p) + assert tp == "set" + assert v == "set()" + +def test_frozenset(py2, py3): + for dump in py2.dump, py3.dump: + p = dump("frozenset((1, 2, 3))") + tp, v = py2.load(p) + assert tp == "frozenset" + assert v == "frozenset([1, 2, 3])" + tp, v = py3.load(p) + assert tp == "frozenset" + assert v == "frozenset({1, 2, 3})" + p = dump("frozenset()") + tp, v = py2.load(p) + assert tp == "frozenset" + assert v == "frozenset([])" + tp, v = py3.load(p) + assert tp == "frozenset" + assert v == "frozenset()" @py.test.mark.xfail # I'm not sure if we need the complexity. --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -823,14 +823,23 @@ class Unserializer(object): def load_newdict(self): self.stack.append({}) - def load_buildtuple(self): + def _load_tuple(self): length = self._read_int4() if length: tup = tuple(self.stack[-length:]) del self.stack[-length:] else: tup = () - self.stack.append(tup) + return tup + + def load_buildtuple(self): + self.stack.append(self._load_tuple()) + + def load_set(self): + self.stack.append(set(self._load_tuple())) + + def load_frozenset(self): + self.stack.append(frozenset(self._load_tuple())) def load_stop(self): raise _Stop @@ -960,3 +969,15 @@ class Serializer(object): self._save(item) self._write(opcode.BUILDTUPLE) self._write_int4(len(tup), "tuple is too long") + + def _write_set(self, s, op): + for item in s: + self._save(item) + self._write(op) + self._write_int4(len(s), "set is too long") + + def save_set(self, s): + self._write_set(s, opcode.SET) + + def save_frozenset(self, s): + self._write_set(s, opcode.FROZENSET) From commits-noreply at bitbucket.org Mon Nov 9 23:34:00 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 22:34:00 +0000 (UTC) Subject: [execnet-commit] execnet commit b161226fbabf: add proper long support Message-ID: <20091109223400.E035E7EF06@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User Benjamin Peterson # Date 1257806191 21600 # Node ID b161226fbabfe4f9c5272c4a3dab6120de93ea1e # Parent ac44b2b936dfa797622ae22c117030c9e26b263d add proper long support --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -48,7 +48,15 @@ if sys.version_info > (3, 0): # Need bin sys.stdout = sys.stdout.detach() saver = serializer.Serializer(sys.stdout) saver.save(%s)""" % (pyimportdir, obj_rep,)) - return self.executable.sysexec(script_file) + popen = subprocess.Popen([str(self.executable), str(script_file)], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + ret = popen.wait() + if ret: + raise py.process.cmdexec.Error(ret, ret, str(self.executable), + stdout, stderr) + return popen.stdout.read() def load(self, data, option_args="__class__"): script_file = TEMPDIR.join("load.py") @@ -67,7 +75,7 @@ sys.stdout.write(repr(obj))""" % (pyimpo stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - stdout, stderr = popen.communicate(data.encode("latin-1")) + stdout, stderr = popen.communicate(data) ret = popen.returncode if ret: raise py.process.cmdexec.Error(ret, ret, str(self.executable), @@ -159,6 +167,31 @@ def test_frozenset(py2, py3): assert tp == "frozenset" assert v == "frozenset()" +def test_long(py2, py3): + really_big = "9223372036854775807324234" + p = py2.dump(really_big) + tp, v = py2.load(p) + assert tp == "long" + assert v == really_big + "L" + tp, v = py3.load(p) + assert tp == "int" + assert v == really_big + p = py3.dump(really_big) + tp, v == py3.load(p) + assert tp == "int" + assert v == really_big + tp, v = py2.load(p) + assert tp == "long" + assert v == really_big + "L" + +def test_small_long(py2, py3): + p = py2.dump("123L") + tp, s = py2.load(p) + assert s == "123L" + tp, s = py3.load(p) + assert s == "123" + + @py.test.mark.xfail # I'm not sure if we need the complexity. def test_recursive_list(py2, py3): @@ -168,11 +201,6 @@ def test_recursive_list(py2, py3): tp, rep = py2.load(l) assert tp == "list" -def test_bigint_should_fail(): - py.test.raises(serializer.SerializationError, - serializer.Serializer(py.io.BytesIO()).save, - 123456678900) - def test_bytes(py2, py3): p = py3.dump("b'hi'") tp, v = py2.load(p) @@ -210,13 +238,6 @@ def test_unicode(py2, py3): assert tp == "unicode" # depends on unserialization defaults assert s == "u'hi'" -def test_long(py2, py3): - p = py2.dump("123L") - tp, s = py2.load(p) - assert s == "123" - tp, s = py3.load(p) - assert s == "123" - def test_bool(py2, py3): p = py2.dump("True") tp, s = py2.load(p) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -6,7 +6,7 @@ NOTE: aims to be compatible to Python 2. (C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others """ import sys, os, weakref -import threading, traceback, socket, struct +import threading, traceback, socket, struct, binascii try: import queue except ImportError: @@ -17,10 +17,12 @@ if ISPY3: exec("def do_exec(co, loc): exec(co, loc)\n" "def reraise(cls, val, tb): raise val\n") unicode = str + _long_type = int else: exec("def do_exec(co, loc): exec co in loc\n" "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str + _long_type = long sysex = (KeyboardInterrupt, SystemExit) @@ -721,7 +723,7 @@ class UnserializationError(SerializeErro if ISPY3: def b(s): - return s.encode("ascii") + return s.encode("latin-1") else: b = str @@ -774,6 +776,22 @@ class Unserializer(object): i = self._read_int4() self.stack.append(i) + def load_longint(self): + l = _decode_long(self._read_byte_string()) + self.stack.append(int(l)) + + if ISPY3: + load_long = load_int + load_longlong = load_longint + else: + def load_long(self): + i = self._read_int4() + self.stack.append(long(i)) + + def load_longlong(self): + l = _decode_long(self._read_byte_string()) + self.stack.append(l) + def load_float(self): binary = self.stream.read(FLOAT_FORMAT_SIZE) self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) @@ -863,7 +881,59 @@ def _buildopcodes(): setattr(opcode, opname, i) _buildopcodes() - + +# Copied straight from pickle.py. +def _encode_long(x): + """Encode a long to a two's complement little-endian binary string.""" + if x > 0: + ashex = hex(x) + assert ashex.startswith("0x") + njunkchars = 2 + ashex.endswith('L') + nibbles = len(ashex) - njunkchars + if nibbles & 1: + # need an even # of nibbles for unhexlify + ashex = "0x0" + ashex[2:] + elif int(ashex[2], 16) >= 8: + # "looks negative", so need a byte of sign bits + ashex = "0x00" + ashex[2:] + else: + # Build the 256's-complement: (1L << nbytes) + x. The trick is + # to find the number of bytes in linear time (although that should + # really be a constant-time task). + ashex = hex(-x) + njunkchars = 2 + ashex.endswith('L') + nibbles = len(ashex) - njunkchars + if nibbles & 1: + # Extend to a full byte. + nibbles += 1 + nbits = nibbles * 4 + x += 1 << nbits + ashex = hex(x) + njunkchars = 2 + ashex.endswith('L') + newnibbles = len(ashex) - njunkchars + if newnibbles < nibbles: + ashex = "0x" + "0" * (nibbles - newnibbles) + ashex[2:] + if int(ashex[2], 16) < 8: + # "looks positive", so need a byte of sign bits + ashex = "0xff" + ashex[2:] + + if ashex.endswith('L'): + ashex = ashex[2:-1] + else: + ashex = ashex[2:] + binary = binascii.unhexlify(ashex) + return binary[::-1] + +def _decode_long(data): + """Decode a long from a two's complement little-endian binary string.""" + nbytes = len(data) + ashex = binascii.hexlify(data[::-1]) + n = _long_type(ashex, 16) # quadratic time before Python 2.3; linear now + if data[-1:] >= b('\x80'): + n -= 1 << (nbytes * 8) + return n + + class Serializer(object): WRITE_ON_SUCCESS=True # more robust against serialize failures @@ -933,10 +1003,19 @@ class Serializer(object): self._write_int4(len(bytes_), "string is too long") self._write(bytes_) + def _save_integral(self, i, short_op, long_op): + if i <= FOUR_BYTE_INT_MAX: + self._write(short_op) + self._write_int4(i) + else: + self._write(long_op) + self._write_byte_sequence(_encode_long(i)) + def save_int(self, i): - self._write(opcode.INT) - self._write_int4(i) - save_long = save_int # only used from python2 + self._save_integral(i, opcode.INT, opcode.LONGINT) + + def save_long(self, l): + self._save_integral(l, opcode.LONG, opcode.LONGLONG) def save_float(self, flt): self._write(opcode.FLOAT) From commits-noreply at bitbucket.org Mon Nov 9 23:50:28 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 22:50:28 +0000 (UTC) Subject: [execnet-commit] execnet commit 36fdf3d6180f: use popen.communicate() Message-ID: <20091109225028.495027EF0C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User Benjamin Peterson # Date 1257807180 21600 # Node ID 36fdf3d6180ffe9bdf521a27e32c0e9fd34debe3 # Parent b161226fbabfe4f9c5272c4a3dab6120de93ea1e use popen.communicate() --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -52,11 +52,12 @@ saver.save(%s)""" % (pyimportdir, obj_re stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - ret = popen.wait() + stdout, stderr = popen.communicate() + ret = popen.returncode if ret: raise py.process.cmdexec.Error(ret, ret, str(self.executable), stdout, stderr) - return popen.stdout.read() + return stdout def load(self, data, option_args="__class__"): script_file = TEMPDIR.join("load.py") From commits-noreply at bitbucket.org Tue Nov 10 00:00:37 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 23:00:37 +0000 (UTC) Subject: [execnet-commit] execnet commit 24b1ef2beadd: take the simple way out with long encoding: use the repr Message-ID: <20091109230037.CD49C7EF0E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User Benjamin Peterson # Date 1257807766 21600 # Node ID 24b1ef2beadd85ceaea922cd9a4110319524851f # Parent 36fdf3d6180ffe9bdf521a27e32c0e9fd34debe3 take the simple way out with long encoding: use the repr --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -6,7 +6,7 @@ NOTE: aims to be compatible to Python 2. (C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others """ import sys, os, weakref -import threading, traceback, socket, struct, binascii +import threading, traceback, socket, struct try: import queue except ImportError: @@ -777,8 +777,8 @@ class Unserializer(object): self.stack.append(i) def load_longint(self): - l = _decode_long(self._read_byte_string()) - self.stack.append(int(l)) + s = self._read_byte_string() + self.stack.append(int(s)) if ISPY3: load_long = load_int @@ -789,8 +789,8 @@ class Unserializer(object): self.stack.append(long(i)) def load_longlong(self): - l = _decode_long(self._read_byte_string()) - self.stack.append(l) + l = self._read_byte_string() + self.stack.append(long(l)) def load_float(self): binary = self.stream.read(FLOAT_FORMAT_SIZE) @@ -882,58 +882,6 @@ def _buildopcodes(): _buildopcodes() -# Copied straight from pickle.py. -def _encode_long(x): - """Encode a long to a two's complement little-endian binary string.""" - if x > 0: - ashex = hex(x) - assert ashex.startswith("0x") - njunkchars = 2 + ashex.endswith('L') - nibbles = len(ashex) - njunkchars - if nibbles & 1: - # need an even # of nibbles for unhexlify - ashex = "0x0" + ashex[2:] - elif int(ashex[2], 16) >= 8: - # "looks negative", so need a byte of sign bits - ashex = "0x00" + ashex[2:] - else: - # Build the 256's-complement: (1L << nbytes) + x. The trick is - # to find the number of bytes in linear time (although that should - # really be a constant-time task). - ashex = hex(-x) - njunkchars = 2 + ashex.endswith('L') - nibbles = len(ashex) - njunkchars - if nibbles & 1: - # Extend to a full byte. - nibbles += 1 - nbits = nibbles * 4 - x += 1 << nbits - ashex = hex(x) - njunkchars = 2 + ashex.endswith('L') - newnibbles = len(ashex) - njunkchars - if newnibbles < nibbles: - ashex = "0x" + "0" * (nibbles - newnibbles) + ashex[2:] - if int(ashex[2], 16) < 8: - # "looks positive", so need a byte of sign bits - ashex = "0xff" + ashex[2:] - - if ashex.endswith('L'): - ashex = ashex[2:-1] - else: - ashex = ashex[2:] - binary = binascii.unhexlify(ashex) - return binary[::-1] - -def _decode_long(data): - """Decode a long from a two's complement little-endian binary string.""" - nbytes = len(data) - ashex = binascii.hexlify(data[::-1]) - n = _long_type(ashex, 16) # quadratic time before Python 2.3; linear now - if data[-1:] >= b('\x80'): - n -= 1 << (nbytes * 8) - return n - - class Serializer(object): WRITE_ON_SUCCESS=True # more robust against serialize failures @@ -1009,7 +957,7 @@ class Serializer(object): self._write_int4(i) else: self._write(long_op) - self._write_byte_sequence(_encode_long(i)) + self._write_byte_sequence(str(i).rstrip("L").encode("ascii")) def save_int(self, i): self._save_integral(i, opcode.INT, opcode.LONGINT) From commits-noreply at bitbucket.org Tue Nov 10 00:10:26 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 9 Nov 2009 23:10:26 +0000 (UTC) Subject: [execnet-commit] execnet commit 493fc337a1f1: adding doc about new remote_status() method, updated Changelog Message-ID: <20091109231026.676257EF0C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257808172 -3600 # Node ID 493fc337a1f1cb0452bfe6070844b5b1a3a540ae # Parent 24b1ef2beadd85ceaea922cd9a4110319524851f adding doc about new remote_status() method, updated Changelog --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -101,6 +101,7 @@ class Gateway(gateway_base.BaseGateway): return self._cache_rinfo def remote_status(self): + """ return information object about remote execution status. """ channel = self.newchannel() self._send(Message.STATUS(channel.id)) statusdict = channel.receive() --- a/doc/basics.txt +++ b/doc/basics.txt @@ -75,9 +75,23 @@ two asynchronously running programs. .. automethod:: Channel.waitclose(timeout) .. autoattribute:: Channel.RemoteError + +remote_status: get low-level execution info +=================================================== + +.. currentmodule:: execnet.gateway + +All gateways offer a simple method to obtain some status +information from the remote side. + +.. automethod:: Gateway.remote_status(source) + +Calling this method tells you e.g. how many execution +tasks are queued, how many are executing and how many +channels are active. + .. _xspec: - creating gateways from a generic string format =============================================================== --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 1.0.0b3 -------------------------------- +* add support for serializing longs, sets and frozensets (thanks + Benjamin Peterson) * introduce remote_status() method which on the low level gives information about the remote side of a gateway * disallow explicit close in remote_exec situation From commits-noreply at bitbucket.org Thu Nov 19 21:55:29 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:29 +0000 (UTC) Subject: [execnet-commit] execnet commit 83d9978c473e: fix EXECNET_DEBUG to work with win32 Message-ID: <20091119205529.B3A7E7EEEB@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1257809070 -3600 # Node ID 83d9978c473ee74a34592de68311aabd771508d3 # Parent 493fc337a1f1cb0452bfe6070844b5b1a3a540ae fix EXECNET_DEBUG to work with win32 --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -27,7 +27,9 @@ else: sysex = (KeyboardInterrupt, SystemExit) if os.environ.get('EXECNET_DEBUG'): - debugfile = open('/tmp/execnet-debug-%d' % os.getpid() , 'w') + import tempfile, os.path + fn = os.path.join(tempfile.gettempdir(), 'execnet-debug-%d' % os.getpid()) + debugfile = open(fn, 'w') def trace(msg): try: debugfile.write(unicode(msg) + "\n") --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ 1.0.0b3 -------------------------------- +* fix EXECNET_DEBUG to work with win32 * add support for serializing longs, sets and frozensets (thanks Benjamin Peterson) * introduce remote_status() method which on the low level gives From commits-noreply at bitbucket.org Thu Nov 19 21:55:31 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:31 +0000 (UTC) Subject: [execnet-commit] execnet commit 44271e00ae78: re-org and refine examples, make them tested by default, refine gateway __repr__ Message-ID: <20091119205531.BC41A7EEF0@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258572019 -3600 # Node ID 44271e00ae788aebea2c2fb65b3c44ed3fc3a2c7 # Parent 83d9978c473ee74a34592de68311aabd771508d3 re-org and refine examples, make them tested by default, refine gateway __repr__ --- /dev/null +++ b/doc/example/test_channelexec.txt @@ -0,0 +1,23 @@ + +remote execute modules using their __name__ +-------------------------------------------------------------- + +You can pass a module object to ``remote_exec`` in which case +its source code will be sent. No dependencies will be transferred +so the module must be self-contained or only use modules that are +installed on the "other" side. Module code can detect if it is +running in a remote_exec situation by checking for the special +``__name__`` attribute. + +.. include:: remote1.py + :literal: + +You can now remote-execute the module like this:: + + >>> import execnet, remote1 + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remote1) + >>> print (ch.receive()) + initialization complete + +which will print the 'initialization complete' string. --- /dev/null +++ b/doc/example/conftest.py @@ -0,0 +1,13 @@ + +import py, sys + +# make execnet and example code importable +cand = py.path.local(__file__).dirpath().dirpath().dirpath() +if cand.join("execnet", "__init__.py").check(): + if str(cand) not in sys.path: + sys.path.insert(0, str(cand)) +cand = py.path.local(__file__).dirpath() +if str(cand) not in sys.path: + sys.path.insert(0, str(cand)) + +pytest_plugins = ['doctest'] --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -51,13 +51,13 @@ class Gateway(gateway_base.BaseGateway): else: addr = '' try: - r = (self._receiverthread.isAlive() and "receiver-alive" or - "not receiving") + r = (self._receiverthread.isAlive() and "receive-live" or + "not-receiving") i = len(self._channelfactory.channels()) except AttributeError: r = "uninitialized" i = "no" - return "<%s%s %s (%s active channels)>" %( + return "<%s%s %s, %s active channels>" %( self.__class__.__name__, addr, r, i) def exit(self): --- a/doc/example/popen_read_multiple.py +++ b/doc/example/popen_read_multiple.py @@ -3,7 +3,7 @@ example reading results from possibly blocking code running in sub processes. """ -import py +import execnet NUM_PROCESSES = 5 --- /dev/null +++ b/doc/example/test_syncreceive2.txt @@ -0,0 +1,14 @@ + +Synchronously receive results from two sub processes +----------------------------------------------------- + +Use MultiChannels for receiving multiple results from remote code:: + + >>> import execnet + >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") + >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") + >>> mch = execnet.MultiChannel([ch1, ch2]) + >>> l = mch.receive_each() + >>> assert len(l) == 2 + >>> assert 1 in l + >>> assert 2 in l --- /dev/null +++ b/doc/example/remote1.py @@ -0,0 +1,4 @@ +# content of a module remote1.py + +if __name__ == '__channelexec__': + channel.send('initialization complete') --- /dev/null +++ b/doc/example/test_remotecmd.txt @@ -0,0 +1,24 @@ +A simple command pattern +-------------------------------------------------------------- + +If you want the remote side to serve a number +of synchronous function you can setup a serving +loop and invent your own local protocol + +.. include:: remotecmd.py + :literal: + +Then on the local side you can do:: + + >>> import execnet, remotecmd + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remotecmd) + >>> ch.send('simple(10)') # execute func-call remotely + >>> ch.receive() + 11 + +Our remotecmd module starts up remote serving +through the ``for item in channel`` loop which +will terminate when the channel closes. It evaluates +all incoming requests in the global name space and +sends back the results. --- a/doc/example/remotecmd.py +++ b/doc/example/remotecmd.py @@ -1,4 +1,3 @@ - # contents of: remotecmd.py def simple(arg): @@ -6,9 +5,4 @@ def simple(arg): if __name__ == '__channelexec__': for item in channel: - funcname = item[0] - func = globals()[funcname] - args = item[1:] - result = func(*args) - channel.send(result) - + channel.send(eval(item)) --- /dev/null +++ b/doc/example/test_asyncreceive2.txt @@ -0,0 +1,21 @@ + +Asynchronously receive results from two sub processes +----------------------------------------------------- + +Use ``MultiChannel.make_receive_queue()`` for asynchronously receiving +multiple results from remote code. This standard Queue provides +``(channel, result)`` tuples which allows to determine where +a result comes from:: + + >>> import execnet + >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") + >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") + >>> mch = execnet.MultiChannel([ch1, ch2]) + >>> queue = mch.make_receive_queue() + >>> chan1, res1 = queue.get() # you may also specify a timeout + >>> chan2, res2 = queue.get() + >>> res1 + res2 + 3 + >>> assert chan1 in (ch1, ch2) + >>> assert chan2 in (ch1, ch2) + >>> assert chan1 != chan2 --- a/.hgignore +++ b/.hgignore @@ -6,3 +6,4 @@ dist/ syntax:glob *.pyc *$py.class +*.orig --- a/doc/examples.txt +++ b/doc/examples.txt @@ -99,111 +99,21 @@ using Mono 2.0 and IronPython-1.1 this w .. _IronPython: http://www.IronPython.org -Compare cwd() of Popen Gateways ----------------------------------------- -A PopenGateway has the same working directory as the instantiatior:: - - >>> import execnet, os - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") - >>> res = ch.receive() - >>> assert res == os.getcwd() +.. include example/test_cwd.txt .. _channelexec: -Sending modules with remote_exec --------------------------------------------------------------- - -You can pass a module object to ``remote_exec`` in which case -its source code will be sent. No dependencies will be transferred -so the module must be self-contained or only use modules that are -installed on the "other" side. Module code can detect if it is -running in a remote_exec situation by checking for the special -``__name__`` attribute:: - - # content of a module remote1.py - - if __name__ == '__channelexec__': - channel.send('initialization complete') - -You can now send the module like this:: - - >>> import execnet, remote1 - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec(remote1) - >>> print (ch.receive()) - -which will print the 'initialization complete' string. +.. include:: example/test_channelexec.txt .. _command: -A simple command pattern --------------------------------------------------------------- +.. include:: example/test_remotecmd.txt -If you want the remote side to serve a number -of synchronous function calls, here is a base -implementation:: +.. include:: example/test_syncreceive2.txt - # contents of: remotecmd.py - def simple(arg): - return arg + 1 - - if __name__ == '__channelexec__': - for item in channel: - funcname = item[0] - func = globals()[funcname] - args = item[1:] - result = func(*args) - channel.send(result) - -Then on the local side you can do:: - - >>> import execnet, remotecmd - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec(remotecmd) - >>> ch.send(('simple', 10)) # execute func-call remotely - >>> ch.receive() - 11 - -It's straight forward to build a proxy-object -that would hide the details and perform remote -function calls with basic input and output values. - -Synchronously receive results from two sub processes ------------------------------------------------------ - -Use MultiChannels for receiving multiple results from remote code:: - - >>> import execnet - >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") - >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") - >>> mch = execnet.MultiChannel([ch1, ch2]) - >>> l = mch.receive_each() - >>> assert len(l) == 2 - >>> assert 1 in l - >>> assert 2 in l +.. include:: example/test_asyncreceive2.txt -Asynchronously receive results from two sub processes ------------------------------------------------------ - -Use ``MultiChannel.make_receive_queue()`` for asynchronously receiving -multiple results from remote code. This standard Queue provides -``(channel, result)`` tuples which allows to determine where -a result comes from:: - - >>> import execnet - >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") - >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") - >>> mch = execnet.MultiChannel([ch1, ch2]) - >>> queue = mch.make_receive_queue() - >>> chan1, res1 = queue.get() # you may also specify a timeout - >>> chan2, res2 = queue.get() - >>> res1 + res2 - 3 - >>> assert chan1 in (ch1, ch2) - >>> assert chan2 in (ch1, ch2) - >>> assert chan1 != chan2 Receive file contents from remote SSH account ----------------------------------------------------- --- /dev/null +++ b/doc/example/test_cwd.txt @@ -0,0 +1,10 @@ +Compare cwd() of Popen Gateways +---------------------------------------- + +A PopenGateway has the same working directory as the instantiatior:: + + >>> import execnet, os + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") + >>> res = ch.receive() + >>> assert res == os.getcwd() From commits-noreply at bitbucket.org Thu Nov 19 21:55:33 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:33 +0000 (UTC) Subject: [execnet-commit] execnet commit a5abe9344482: introduce execnet.Group to manage multiple gateways including Message-ID: <20091119205533.D7E617EEF5@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258650088 -3600 # Node ID a5abe93444823019796d6e3f60b5c25a438f1eb5 # Parent 44271e00ae788aebea2c2fb65b3c44ed3fc3a2c7 introduce execnet.Group to manage multiple gateways including their automatic termination. Route all global gateway creation through a default Group instance. --- a/doc/examples.txt +++ b/doc/examples.txt @@ -16,7 +16,7 @@ that has numpy installed. We send items receive back the remote "repr" of the array:: import execnet - gw = execnet.PopenGateway("python2.6") + gw = execnet.makegateway("popen//python=python2.6") channel = gw.remote_exec(""" import numpy array = numpy.array([1,2,3]) @@ -50,7 +50,7 @@ Use your CPython interpreter to connect and work with Java types:: import execnet - gw = execnet.PopenGateway("jython") + gw = execnet.makegateway("popen//python=jython") channel = gw.remote_exec(""" from java.util import Vector v = Vector() @@ -78,7 +78,7 @@ which can work with C# classes. Here is a CLR Array instance and sending back its representation:: import execnet - gw = execnet.PopenGateway("ipy") + gw = execnet.makegateway("popen//python=ipy") channel = gw.remote_exec(""" import clr @@ -110,6 +110,10 @@ using Mono 2.0 and IronPython-1.1 this w .. include:: example/test_remotecmd.txt +.. _group: + +.. include:: example/test_group.txt + .. include:: example/test_syncreceive2.txt .. include:: example/test_asyncreceive2.txt @@ -123,7 +127,7 @@ contents of remote files:: import execnet # open a gateway to a fresh child process - gw = execnet.SshGateway('codespeak.net') + gw = execnet.makegateway("ssh=codespeak.net") channel = gw.remote_exec(""" for fn in channel: f = open(fn, 'rb') @@ -153,5 +157,5 @@ socketserver:: popengw = execnet.PopenGateway() socketgw = execnet.SocketGateway.new_remote(popengw, ("127.0.0.1", 0)) - print socketgw._rinfo() # print some info about the remote environment + print socketgw.remote_status() # print some info about the remote environment --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +1.0.0 +-------------------------------- + +* introduce execnet.Group for grouping gateways, scoping termination. + introduce execnet.default_group through which all "global" calls + are routed. Use makegateway() intead of the Popen/Socket/Ssh classes + in examples. + +* refine and automatically test some documentation examples + 1.0.0b3 -------------------------------- --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -3,46 +3,19 @@ gateway code for initiating popen, socke (c) 2004-2009, Holger Krekel and others """ -import sys, os, inspect, socket, atexit, weakref +import sys, os, inspect, socket, types import textwrap import execnet from execnet.gateway_base import Message, Popen2IO, SocketIO from execnet import gateway_base -ModuleType = type(os) - -debug = False - -class GatewayCleanup: - def __init__(self): - self._activegateways = weakref.WeakKeyDictionary() - atexit.register(self.cleanup_atexit) - - def register(self, gateway): - assert gateway not in self._activegateways - self._activegateways[gateway] = True - - def unregister(self, gateway): - del self._activegateways[gateway] - - def cleanup_atexit(self): - if debug: - debug.writeslines(["="*20, "cleaning up", "=" * 20]) - debug.flush() - for gw in list(self._activegateways): - gw.exit() - #gw.join() # should work as well class Gateway(gateway_base.BaseGateway): """ Gateway to a local or remote Python Intepreter. """ - # XXX put the next two global variables into an Execnet object - # which intiaties gateways and passes in appropriate values. - _cleanup = GatewayCleanup() def __init__(self, io): super(Gateway, self).__init__(io=io, _startcount=1) self._remote_bootstrap_gateway(io) self._initreceive() - self._cleanup.register(self) def __repr__(self): """ return string representing gateway type and status. """ @@ -61,12 +34,13 @@ class Gateway(gateway_base.BaseGateway): self.__class__.__name__, addr, r, i) def exit(self): - """ exit gateway (stop local execution, stop sending)""" - self._trace("exiting gateway") + """ trigger gateway exit. """ + self._trace("trigger gateway exit") try: - self._cleanup.unregister(self) + self._group._unregister(self) except KeyError: return # we assume it's already happened + self._trace("stopping exec and closing write connection") self._stopexec() self._stopsend() #self.join(timeout=timeout) # receiverthread receive close() messages @@ -116,7 +90,7 @@ class Gateway(gateway_base.BaseGateway): and has the sister 'channel' object in its global namespace. """ - if isinstance(source, ModuleType): + if isinstance(source, types.ModuleType): source = inspect.getsource(source) else: source = textwrap.dedent(str(source)) @@ -276,7 +250,7 @@ class SocketGateway(Gateway): (realhost, realport) = channel.receive() #self._trace("new_remote received" # "port=%r, hostname = %r" %(realport, hostname)) - return SocketGateway(host, realport) + return gateway._group.makegateway("socket=%s:%s" %(host, realport)) new_remote = classmethod(new_remote) class HostNotFound(Exception): --- a/doc/basics.txt +++ b/doc/basics.txt @@ -8,21 +8,57 @@ code** and exchange basic python objects .. image:: _static/basic1.png + Gateways: connecting to another Python Interpreter =================================================== .. currentmodule:: execnet -Gateway classes allow to ad-hoc instantiate local or -remote Python Interpreters and deploy code to them. +All Gateways are instantiated via a call to ``makegateway()`` +passing it a gateway specification or URL. -.. autoclass:: PopenGateway +.. autofunction:: execnet.makegateway(xspec) -.. autoclass:: SshGateway +Here is an example to instantiate a simple Python subprocess:: -.. autoclass:: SocketGateway + >>> gateway = execnet.makegateway("popen") +You can then use this gateway object to `remote execute code`_ and +`exchange data`_ bidirectionally. +examples for valid gateway specifications +------------------------------------------- + +* ``ssh=wyvern//python=python2.4//chdir=mycache`` specifies a Python2.4 + interpreter on the host ``wyvern``. The remote process will have + ``mycache`` as its current working directory. + +* ``popen//python=2.5//nice=20`` specification of a python2.5 + subprocess; running with the lowest CPU priority ("nice" level). + By default current dir will be the current dir of the instantiator. + +* ``socket=192.168.1.4:8888`` specifies of a Python Socket server + process that listens on 192.168.1.4:8888; current dir will be the + 'pyexecnet-cache' sub directory which is used a default for all remote + processes. + +.. _xspec: + +recognized Gateway URL parts +--------------------------------------- + +The following parameters are recognized as part of a gateway specification:: + +* ``popen`` for a PopenGateway. +* ``ssh=host`` for a SshGateway to the given host. +* ``socket=address:port`` for a SocketGateway at the given address. +* ``id=NAME`` set the ``id`` of the gateway, must be unique within Group_ +* ``python=executable`` for specifying Python Interpreter executables +* ``chdir=path`` change remote working dir to given relative or absolute path +* ``nice=value`` decrease remote nice level if platforms supports it + + +.. _`remote execute code`: remote_exec: execute source code remotely =================================================== @@ -34,29 +70,14 @@ in the connected interpreter: .. automethod:: Gateway.remote_exec(source) -Here is an self-contained example for reading the process identifier:: - - >>> import execnet, os - >>> gw = execnet.PopenGateway() - >>> channel = gw.remote_exec(""" - ... import os - ... channel.send(os.getpid()) - ... """) - >>> remote_pid = channel.receive() - >>> remote_pid != os.getpid() - True - -If you'd like to avoid inlining source strings take a look -at the channelexec_ and command_ example. - - -.. _channelexec: examples.html#channelexec -.. _command: examples.html#command +.. include:: example/test_pid.txt .. _`Channel`: .. _`channel-api`: .. _`exchange data`: +.. _`exchange data`: + Channels: exchanging data with remote code ======================================================= @@ -90,44 +111,21 @@ Calling this method tells you e.g. how m tasks are queued, how many are executing and how many channels are active. -.. _xspec: -creating gateways from a generic string format -=============================================================== +.. _Group: -``execnet`` supports a simple extensible format for -specifying and configuring Gateways for remote execution. +Grouping Gateways +============================ -.. autofunction:: execnet.makegateway(xspec) +All created gateway instances are part of a group. Gateways made +through the global ``execnet.makegateway()`` will become a +member of the ``default_group``:: -The following paramters are recognized by default: +A Group instance can terminate its gateways explicitely +or at process termination (each group registers with Python's +``atexit`` mechanism). See the `group examples`_ for details. -* ``popen`` for a PopenGateway. -* ``ssh=host`` for a SshGateway to the given host. -* ``socket=address:port`` for a SocketGateway at the given address. -* ``python=executable`` for specifying Python Interpreter executables -* ``chdir=path`` change remote working dir to given relative or absolute path -* ``nice=value`` decrease remote nice level if platforms supports it - -You can use a string specification to, for example, -instantiate a simple new SshGateway:: - - gateway = execnet.makegateway("ssh=myhost") - -Here are some examples for valid specifications: - -* ``ssh=wyvern//python=python2.4//chdir=mycache`` specifies a Python2.4 - interpreter on the host wyvern. The remote process will have - ``mycache`` as its current working directory. - -* ``popen//python=2.5//nice=20`` specification of a python2.5 - subprocess; running with the lowest CPU priority ("nice" level). - By default current dir will be the current dir of the instantiator. - -* ``socket=192.168.1.4:8888`` specifies of a Python Socket server - process that listens on 192.168.1.4:8888; current dir will be the - 'pyexecnet-cache' sub directory which is used a default for all remote - processes. +.. _`group examples`: examples#group rsync: synchronise filesystem with remote =============================================================== @@ -136,7 +134,7 @@ rsync: synchronise filesystem with remot Here is a basic example:: rsync = execnet.RSync('/tmp/source') - gw = execnet.PopenGateway() + gw = execnet.makegateway("popen") rsync.add_target(gw, '/tmp/dest') rsync.send() --- a/testing/conftest.py +++ b/testing/conftest.py @@ -3,7 +3,7 @@ import py rsyncdirs = ['../execnet', '.'] -pytest_plugins = ['pytester'] +pytest_plugins = ['pytester', 'doctest'] # configuration information for tests def pytest_addoption(parser): group = parser.getgroup("pylib", "py lib testing options") @@ -69,7 +69,8 @@ def pytest_funcarg__gw(request): def setup_socket_gateway(): proxygw = execnet.PopenGateway() - gw = execnet.SocketGateway.new_remote(proxygw, ("127.0.0.1", 0)) + from execnet.gateway import SocketGateway + gw = SocketGateway.new_remote(proxygw, ("127.0.0.1", 0)) gw.proxygw = proxygw return gw --- /dev/null +++ b/doc/example/test_group.txt @@ -0,0 +1,21 @@ + + +Creation and termination of of grouped Gateways +------------------------------------------------------ + +:: + + >>> import execnet + >>> group = execnet.Group() + >>> group.makegateway("popen") + + >>> group.makegateway("popen") + + >>> group + , ]> + >>> group.makegateway("popen") + + >>> group + + >>> group.terminate() + --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -23,8 +23,7 @@ class TestMultiChannelAndGateway: assert l == [12,12] def test_multichannel_send_each(self): - l = [execnet.PopenGateway() for x in range(2)] - gm = execnet.MultiGateway(l) + gm = execnet.Group(["popen"] * 2) mc = gm.remote_exec(""" import os channel.send(channel.receive() + 1) @@ -34,8 +33,7 @@ class TestMultiChannelAndGateway: assert l == [42,42] def test_multichannel_receive_queue_for_two_subprocesses(self): - l = [execnet.PopenGateway() for x in range(2)] - gm = execnet.MultiGateway(l) + gm = execnet.Group(["popen"] * 2) mc = gm.remote_exec(""" import os channel.send(os.getpid()) @@ -57,3 +55,30 @@ class TestMultiChannelAndGateway: multichannel.waitclose() assert len(l) == 2 + +from execnet.multi import Group +def test_basic_group(monkeypatch): + import atexit + atexitlist = [] + monkeypatch.setattr(atexit, 'register', atexitlist.append) + group = Group() + assert atexitlist == [group._cleanup_atexit] + exitlist = [] + class PseudoGW: + def exit(self): + exitlist.append(self) + gw = PseudoGW() + group._register(gw) + assert len(exitlist) == 0 + group._cleanup_atexit() + assert len(exitlist) == 1 + assert exitlist == [gw] + group._cleanup_atexit() + assert len(exitlist) == 1 + +def test_group_PopenGateway(): + group = Group() + gw = group.makegateway("popen") + assert list(group._activegateways) == [gw] + group._cleanup_atexit() + assert not group._activegateways --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -9,13 +9,13 @@ __version__ = "1.0.0b3" import execnet.apipkg execnet.apipkg.initpkg(__name__, { - 'PopenGateway': '.gateway:PopenGateway', - 'SocketGateway': '.gateway:SocketGateway', - 'SshGateway': '.gateway:SshGateway', + 'PopenGateway': '.multi:PopenGateway', + 'SocketGateway': '.multi:SocketGateway', + 'SshGateway': '.multi:SshGateway', + 'makegateway': '.multi:makegateway', 'HostNotFound': '.gateway:HostNotFound', - 'makegateway': '.xspec:makegateway', 'XSpec': '.xspec:XSpec', - 'MultiGateway': '.multi:MultiGateway', + 'Group': '.multi:Group', 'MultiChannel': '.multi:MultiChannel', 'RSync': '.rsync:RSync', }) --- a/execnet/multi.py +++ b/execnet/multi.py @@ -1,25 +1,106 @@ """ -Support for working with multiple channels and gateways +Managing Gateway Groups and interactions with multiple channels. (c) 2008-2009, Holger Krekel and others """ -import sys -from execnet.gateway_base import queue, reraise +import sys, weakref, atexit +import execnet +from execnet import XSpec +from execnet import gateway +from execnet.gateway_base import queue, reraise, trace NO_ENDMARKER_WANTED = object() -class MultiGateway: - def __init__(self, gateways): - self.gateways = gateways +class Group: + """ Gateway Groups. """ + def __init__(self, xspecs=()): + """ initialize group and make gateways as specified. """ + self._activegateways = weakref.WeakKeyDictionary() + for xspec in xspecs: + self.makegateway(xspec) + atexit.register(self._cleanup_atexit) + + def __repr__(self): + numgw = len(self._activegateways) + if numgw > 2: + return "" %(len(self._activegateways)) + else: + gws = ", ".join([repr(x) for x in self._activegateways]) + return "" %(gws,) + + def makegateway(self, spec): + """ create and configure a gateway to a Python interpreter + specified by a 'execution specification' string. + The format of the string generally is:: + + key1=value1//key2=value2//... + + If you leave out the ``=value`` part a True value is assumed. + """ + if not isinstance(spec, XSpec): + spec = XSpec(spec) + if spec.popen: + gw = gateway.PopenGateway(python=spec.python) + elif spec.ssh: + gw = gateway.SshGateway(spec.ssh, remotepython=spec.python, ssh_config=spec.ssh_config) + elif spec.socket: + assert not spec.python, ( + "socket: specifying python executables not supported") + hostport = spec.socket.split(":") + gw = gateway.SocketGateway(*hostport) + else: + raise ValueError("no gateway type found for %r" % (spec._spec,)) + gw.spec = spec + self._register(gw) + if spec.chdir or spec.nice: + channel = gw.remote_exec(""" + import os + path, nice = channel.receive() + if path: + if not os.path.exists(path): + os.mkdir(path) + os.chdir(path) + if nice and hasattr(os, 'nice'): + os.nice(nice) + """) + nice = spec.nice and int(spec.nice) or 0 + channel.send((spec.chdir, nice)) + channel.waitclose() + return gw + + def _register(self, gateway): + assert gateway not in self._activegateways + assert not hasattr(gateway, '_group') + self._activegateways[gateway] = True + gateway._group = self + + def _unregister(self, gateway): + del self._activegateways[gateway] + + def _cleanup_atexit(self): + trace("=== atexit cleanup %r ===" %(self,)) + self.terminate() + + def terminate(self): + """ trigger exit of all gateways. """ + gwlist = [] + while 1: + try: + gw, _ = self._activegateways.popitem() + except KeyError: + break + else: + gw.exit() + gwlist.append(gw) + #for gw in gwlist: + # gw.join(timeout=1.0) + def remote_exec(self, source): channels = [] - for gw in self.gateways: + for gw in list(self._activegateways): channels.append(gw.remote_exec(source)) return MultiChannel(channels) - def exit(self): - for gw in self.gateways: - gw.exit() class MultiChannel: def __init__(self, channels): @@ -65,3 +146,37 @@ class MultiChannel: first = sys.exc_info() if first: reraise(*first) + + +default_group = Group() + +makegateway = default_group.makegateway + +def PopenGateway(python=None): + """ instantiate a gateway to a subprocess + started with the given 'python' executable. + """ + spec = execnet.XSpec("popen") + spec.python = python + return default_group.makegateway(spec) + +def SocketGateway(host, port): + """ This Gateway provides interaction with a remote process + by connecting to a specified socket. On the remote + side you need to manually start a small script + (py/execnet/script/socketserver.py) that accepts + SocketGateway connections or use the experimental + new_remote() method on existing gateways. + """ + spec = execnet.XSpec("socket=%s:%s" %(host, port)) + return default_group.makegateway(spec) + +def SshGateway(sshaddress, remotepython=None, ssh_config=None): + """ instantiate a remote ssh process with the + given 'sshaddress' and remotepython version. + you may specify an ssh_config file. + """ + spec = execnet.XSpec("ssh=%s" % sshaddress) + spec.python = remotepython + spec.ssh_config = ssh_config + return default_group.makegateway(spec) --- a/execnet/xspec.py +++ b/execnet/xspec.py @@ -48,41 +48,3 @@ class XSpec: def _samefilesystem(self): return bool(self.popen and not self.chdir) -def makegateway(spec): - """ create and configure a gateway to a Python interpreter - specified by a 'execution specification' string. - The format of the string generally is:: - - key1=value1//key2=value2//... - - If you leave out the ``=value`` part a True value is assumed. - """ - if not isinstance(spec, XSpec): - spec = XSpec(spec) - if spec.popen: - gw = execnet.PopenGateway(python=spec.python) - elif spec.ssh: - gw = execnet.SshGateway(spec.ssh, remotepython=spec.python) - elif spec.socket: - assert not spec.python, ( - "socket: specifying python executables not supported") - hostport = spec.socket.split(":") - gw = execnet.SocketGateway(*hostport) - else: - raise ValueError("no gateway type found for %r" % (spec._spec,)) - gw.spec = spec - if spec.chdir or spec.nice: - channel = gw.remote_exec(""" - import os - path, nice = channel.receive() - if path: - if not os.path.exists(path): - os.mkdir(path) - os.chdir(path) - if nice and hasattr(os, 'nice'): - os.nice(nice) - """) - nice = spec.nice and int(spec.nice) or 0 - channel.send((spec.chdir, nice)) - channel.waitclose() - return gw --- /dev/null +++ b/doc/example/test_pid.txt @@ -0,0 +1,18 @@ + +Here is an self-contained example for reading the process identifier. + + >>> import execnet, os + >>> gw = execnet.makegateway("popen") + >>> channel = gw.remote_exec(""" + ... import os + ... channel.send(os.getpid()) + ... """) + >>> remote_pid = channel.receive() + >>> remote_pid != os.getpid() + True + +If you'd like to avoid inlining source strings take a look +at the channelexec_ and command_ example. + +.. _channelexec: examples.html#channelexec +.. _command: examples.html#command From commits-noreply at bitbucket.org Thu Nov 19 21:55:36 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:36 +0000 (UTC) Subject: [execnet-commit] execnet commit 1fbf1ba9e24f: some tests and additions for id-handling with gateways and groups Message-ID: <20091119205536.070117EEF8@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258654651 -3600 # Node ID 1fbf1ba9e24f59348c7d5ffe7866b1a83e72b865 # Parent 48d78b31ec63289d11279cf636867f6fcb73f26f some tests and additions for id-handling with gateways and groups --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -19,6 +19,10 @@ class Gateway(gateway_base.BaseGateway): def __repr__(self): """ return string representing gateway type and status. """ + if hasattr(self, 'id'): + id = self.id + else: + id = "???" if hasattr(self, 'remoteaddress'): addr = '[%s]' % (self.remoteaddress,) else: @@ -30,8 +34,8 @@ class Gateway(gateway_base.BaseGateway): except AttributeError: r = "uninitialized" i = "no" - return "<%s%s %s, %s active channels>" %( - self.__class__.__name__, addr, r, i) + return "<%s%s id=%r %s, %s active channels>" %( + self.__class__.__name__, addr, id, r, i) def exit(self): """ trigger gateway exit. """ --- a/execnet/multi.py +++ b/execnet/multi.py @@ -31,6 +31,12 @@ class Group: gws = ", ".join([repr(x) for x in self._activegateways]) return "" %(gws,) + def __getitem__(self, key): + return self._id2gateway[key] + + def __contains__(self, key): + return key in self._id2gateway + def makegateway(self, spec): """ create and configure a gateway to a Python interpreter specified by a 'execution specification' string. @@ -59,7 +65,7 @@ class Group: else: raise ValueError("no gateway type found for %r" % (spec._spec,)) gw.spec = spec - self._register(gw) + self._register(gw, id=spec.id) if spec.chdir or spec.nice: channel = gw.remote_exec(""" import os @@ -91,6 +97,7 @@ class Group: def _unregister(self, gateway): del self._activegateways[gateway] + del self._id2gateway[gateway.id] def _cleanup_atexit(self): trace("=== atexit cleanup %r ===" %(self,)) --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -8,14 +8,30 @@ Creation and termination of of grouped G >>> import execnet >>> group = execnet.Group() >>> group.makegateway("popen") - + >>> group.makegateway("popen") - + >>> group - , ]> + , ]> >>> group.makegateway("popen") - + >>> group >>> group.terminate() +Group termination will cause all member Gateways to exit. + +Working with Groups and Gateway IDs +------------------------------------------------------ + +All gateways are created as part of a group and receive +a per-group unique ``id`` after successful initialization. +This identification string can be set via an ``id=...`` part +in an gateway URL as passed to ``group.makegateway``. Example:: + + >>> import execnet + >>> group = execnet.Group() + >>> gw = group.makegateway("popen//id=sub1") + >>> assert gw.id == "sub1" + >>> group['sub1'] + --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -1,7 +1,5 @@ """ - tests for - - multi channels and multi gateways - + tests for multi channels and gateway Groups """ import execnet @@ -57,28 +55,40 @@ class TestMultiChannelAndGateway: from execnet.multi import Group -def test_basic_group(monkeypatch): - import atexit - atexitlist = [] - monkeypatch.setattr(atexit, 'register', atexitlist.append) - group = Group() - assert atexitlist == [group._cleanup_atexit] - exitlist = [] - class PseudoGW: - def exit(self): - exitlist.append(self) - gw = PseudoGW() - group._register(gw) - assert len(exitlist) == 0 - group._cleanup_atexit() - assert len(exitlist) == 1 - assert exitlist == [gw] - group._cleanup_atexit() - assert len(exitlist) == 1 +class TestGroup: + def test_basic_group(self, monkeypatch): + import atexit + atexitlist = [] + monkeypatch.setattr(atexit, 'register', atexitlist.append) + group = Group() + assert atexitlist == [group._cleanup_atexit] + exitlist = [] + class PseudoGW: + def exit(self): + exitlist.append(self) + gw = PseudoGW() + group._register(gw) + assert len(exitlist) == 0 + group._cleanup_atexit() + assert len(exitlist) == 1 + assert exitlist == [gw] + group._cleanup_atexit() + assert len(exitlist) == 1 -def test_group_PopenGateway(): - group = Group() - gw = group.makegateway("popen") - assert list(group._activegateways) == [gw] - group._cleanup_atexit() - assert not group._activegateways + def test_group_PopenGateway(self, ): + group = Group() + gw = group.makegateway("popen") + assert list(group._activegateways) == [gw] + group._cleanup_atexit() + assert not group._activegateways + + def test_gateway_id(self): + group = Group() + gw = group.makegateway("popen//id=hello") + assert group["hello"] == gw + py.test.raises(AttributeError, "del group['hello']") + py.test.raises(AttributeError, "group['hello'] = 5") + assert 'hello' in group + gw.exit() + assert 'hello' not in group + py.test.raises(KeyError, "group['hello']") From commits-noreply at bitbucket.org Thu Nov 19 21:55:35 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:35 +0000 (UTC) Subject: [execnet-commit] execnet commit 48d78b31ec63: introduce socketserver examples and gateway.id for handling "installvia" spec Message-ID: <20091119205535.DC0D47EEF6@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258654650 -3600 # Node ID 48d78b31ec63289d11279cf636867f6fcb73f26f # Parent a5abe93444823019796d6e3f60b5c25a438f1eb5 introduce socketserver examples and gateway.id for handling "installvia" spec --- a/execnet/multi.py +++ b/execnet/multi.py @@ -17,6 +17,8 @@ class Group: def __init__(self, xspecs=()): """ initialize group and make gateways as specified. """ self._activegateways = weakref.WeakKeyDictionary() + self._id2gateway = weakref.WeakValueDictionary() + self._autoidcounter = 1 for xspec in xspecs: self.makegateway(xspec) atexit.register(self._cleanup_atexit) @@ -46,9 +48,14 @@ class Group: gw = gateway.SshGateway(spec.ssh, remotepython=spec.python, ssh_config=spec.ssh_config) elif spec.socket: assert not spec.python, ( - "socket: specifying python executables not supported") - hostport = spec.socket.split(":") - gw = gateway.SocketGateway(*hostport) + "socket: specifying python executables not yet supported") + gateway_id = spec.installvia + if gateway_id: + viagw = self._id2gateway[gateway_id] + return gateway.SocketGateway.new_remote(viagw) + else: + hostport = spec.socket.split(":") + gw = gateway.SocketGateway(*hostport) else: raise ValueError("no gateway type found for %r" % (spec._spec,)) gw.spec = spec @@ -69,11 +76,18 @@ class Group: channel.waitclose() return gw - def _register(self, gateway): + def _register(self, gateway, id=None): + assert not hasattr(gateway, '_group') + if id is None: + id = self._autoidcounter + self._autoidcounter += 1 + id = str(id) + assert id not in self._id2gateway assert gateway not in self._activegateways - assert not hasattr(gateway, '_group') self._activegateways[gateway] = True + self._id2gateway[id] = gateway gateway._group = self + gateway.id = id def _unregister(self, gateway): del self._activegateways[gateway] --- /dev/null +++ b/doc/example/test_socketnewremote.txt @@ -0,0 +1,44 @@ +Instantiate a Socket Gateway +----------------------------------------------------- + +Sometimes there is no direct access method towards a +machine. If you have a trusted network (like a internal +developer LAN) you can use a simplistic `socketserver script`_ +which listens on a socket ("8888" by default) and executes +incoming strings. Download it and run it on the target +machine like this:: + + python socketserver.py localhost:8888 + +After which you can initiate a Gateway to the socket-server +running machine like this:: + + >>> import execnet + >>> gw = execnet.makegateway("socket=localhost:8888") # doctest: +SKIP + + + +.. _`socketserver script`: http://bitbucket.org/hpk42/execnet/raw/493fc337a1f1/execnet/script/socketserver.py + +Instantiate a socket server through an existing Gateway +-------------------------------------------------------- + +.. note:: + Experimental, please send a note if you are using + the following feature. + +The following example shows a special method to open up +a SocketServer gateway through another existing gateway. + + >>> import execnet + >>> group = execnet.Group() + >>> gw = group.makegateway("popen") + >>> gw.id + '1' + >>> group.makegateway("socket//installvia=1") # doctest: +SKIP + + + +This method will instantiate a Socket based Gateway +whose remote counterpart will be run via the +given ``installvia`` specified gateway. --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 1.0.0 -------------------------------- + +* refine socketserver-examples, experimentally introduce a + way to indirectly setup a SocketServer ("installvia") + * introduce execnet.Group for grouping gateways, scoping termination. introduce execnet.default_group through which all "global" calls are routed. Use makegateway() intead of the Popen/Socket/Ssh classes From commits-noreply at bitbucket.org Thu Nov 19 21:55:36 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:36 +0000 (UTC) Subject: [execnet-commit] execnet commit cb9bc8210ab5: bumping version to the envisioned 1.0.0b4, using setuptools if available Message-ID: <20091119205536.22FE67EEF9@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258654652 -3600 # Node ID cb9bc8210ab5f0722e58832c214323a97432a0ee # Parent 1fbf1ba9e24f59348c7d5ffe7866b1a83e72b865 bumping version to the envisioned 1.0.0b4, using setuptools if available --- a/setup.py +++ b/setup.py @@ -13,8 +13,10 @@ well on Windows, Linux and OSX systems. execnet was written and is maintained by Holger Krekel with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. """ -import os, sys -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup from execnet import __version__ --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0b3" +__version__ = "1.0.0b4" import execnet.apipkg From commits-noreply at bitbucket.org Thu Nov 19 21:55:36 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 20:55:36 +0000 (UTC) Subject: [execnet-commit] execnet commit 591908a9c739: make groups iterable, expose and test execnet.default_group Message-ID: <20091119205536.2FBA87EEFA@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258664083 -3600 # Node ID 591908a9c73927d24723cbfa7af8ad2ba07b8f2a # Parent cb9bc8210ab5f0722e58832c214323a97432a0ee make groups iterable, expose and test execnet.default_group --- a/execnet/multi.py +++ b/execnet/multi.py @@ -16,6 +16,7 @@ class Group: """ Gateway Groups. """ def __init__(self, xspecs=()): """ initialize group and make gateways as specified. """ + # Gateways may evolve to become GC-collectable self._activegateways = weakref.WeakKeyDictionary() self._id2gateway = weakref.WeakValueDictionary() self._autoidcounter = 1 @@ -24,12 +25,9 @@ class Group: atexit.register(self._cleanup_atexit) def __repr__(self): - numgw = len(self._activegateways) - if numgw > 2: - return "" %(len(self._activegateways)) - else: - gws = ", ".join([repr(x) for x in self._activegateways]) - return "" %(gws,) + keys = self._id2gateway.keys() + keys.sort() + return "" %(keys,) def __getitem__(self, key): return self._id2gateway[key] @@ -37,6 +35,9 @@ class Group: def __contains__(self, key): return key in self._id2gateway + def __iter__(self): + return iter(list(self._activegateways)) + def makegateway(self, spec): """ create and configure a gateway to a Python interpreter specified by a 'execution specification' string. --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -18,4 +18,5 @@ execnet.apipkg.initpkg(__name__, { 'Group': '.multi:Group', 'MultiChannel': '.multi:MultiChannel', 'RSync': '.rsync:RSync', + 'default_group': '.multi:default_group', }) --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -1,9 +1,8 @@ - - Creation and termination of of grouped Gateways ------------------------------------------------------ -:: +You can use ``execnet.Group`` to manage creation and +termination of Gateways. Example usage:: >>> import execnet >>> group = execnet.Group() @@ -12,16 +11,12 @@ Creation and termination of of grouped G >>> group.makegateway("popen") >>> group - , ]> - >>> group.makegateway("popen") - - >>> group - - >>> group.terminate() + + >>> list(group) # list all member gateways + [, ] + >>> group.terminate() # exit all member gateways -Group termination will cause all member Gateways to exit. - -Working with Groups and Gateway IDs +Acessing Gateways on a group by ID ------------------------------------------------------ All gateways are created as part of a group and receive @@ -35,3 +30,7 @@ in an gateway URL as passed to ``group.m >>> assert gw.id == "sub1" >>> group['sub1'] + +Using the default_group +------------------------------------------------------ + --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -75,10 +75,10 @@ class TestGroup: group._cleanup_atexit() assert len(exitlist) == 1 - def test_group_PopenGateway(self, ): + def test_group_PopenGateway(self): group = Group() gw = group.makegateway("popen") - assert list(group._activegateways) == [gw] + assert list(group) == [gw] group._cleanup_atexit() assert not group._activegateways @@ -92,3 +92,12 @@ class TestGroup: gw.exit() assert 'hello' not in group py.test.raises(KeyError, "group['hello']") + + def test_default_group(self): + oldlist = list(execnet.default_group) + gw = execnet.makegateway("popen") + newlist = list(execnet.default_group) + assert len(newlist) == len(oldlist) + 1 + assert gw in newlist + assert gw not in oldlist + From commits-noreply at bitbucket.org Thu Nov 19 23:58:23 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 22:58:23 +0000 (UTC) Subject: [execnet-commit] execnet commit d903ea597bb3: actually deprecate execnet.XYZGateway Message-ID: <20091119225823.BAAE87EEEB@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258665672 -3600 # Node ID d903ea597bb3594c1ea7c5036085cd9b7c2581a4 # Parent 591908a9c73927d24723cbfa7af8ad2ba07b8f2a actually deprecate execnet.XYZGateway --- a/execnet/multi.py +++ b/execnet/multi.py @@ -178,6 +178,7 @@ def PopenGateway(python=None): """ instantiate a gateway to a subprocess started with the given 'python' executable. """ + APIWARN("1.0.0b4", "use makegateway('popen')") spec = execnet.XSpec("popen") spec.python = python return default_group.makegateway(spec) @@ -190,6 +191,7 @@ def SocketGateway(host, port): SocketGateway connections or use the experimental new_remote() method on existing gateways. """ + APIWARN("1.0.0b4", "use makegateway('socket=host:port')") spec = execnet.XSpec("socket=%s:%s" %(host, port)) return default_group.makegateway(spec) @@ -198,7 +200,13 @@ def SshGateway(sshaddress, remotepython= given 'sshaddress' and remotepython version. you may specify an ssh_config file. """ + APIWARN("1.0.0b4", "use makegateway('ssh=host')") spec = execnet.XSpec("ssh=%s" % sshaddress) spec.python = remotepython spec.ssh_config = ssh_config return default_group.makegateway(spec) + +def APIWARN(version, msg, stacklevel=3): + import warnings + Warn = DeprecationWarning("(since version %s) %s" %(version, msg)) + warnings.warn(Warn, stacklevel=stacklevel) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -16,6 +16,14 @@ def test_serialize_error(gw): excinfo = py.test.raises(ch.RemoteError, "ch.receive()") assert "can't serialize" in str(excinfo.value) +def test_deprecation(recwarn): + execnet.PopenGateway() + assert recwarn.pop(DeprecationWarning) + py.test.raises(Exception, 'execnet.SocketGateway("localhost", 8888)') + assert recwarn.pop(DeprecationWarning) + py.test.raises(Exception, 'execnet.SshGateway("not-existing")') + assert recwarn.pop(DeprecationWarning) + class TestBasicRemoteExecution: def test_correct_setup(self, gw): assert gw._receiverthread.isAlive() @@ -449,7 +457,7 @@ class TestChannelFile: gw.remote_exec("import os ; os.chdir(%r)" % old).waitclose() def test_join_blocked_slave_execution_gateway(): - gateway = execnet.PopenGateway() + gateway = execnet.makegateway('popen') channel = gateway.remote_exec(""" import time time.sleep(10.0) @@ -470,7 +478,7 @@ class TestPopenGateway: def test_chdir_separation(self, tmpdir): old = tmpdir.chdir() try: - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') finally: waschangedir = old.chdir() c = gw.remote_exec("import os ; channel.send(os.getcwd())") @@ -481,7 +489,7 @@ class TestPopenGateway: num = 4 l = [] for i in range(num): - l.append(execnet.PopenGateway()) + l.append(execnet.makegateway('popen')) channels = [] for gw in l: channel = gw.remote_exec("""channel.send(42)""") @@ -510,7 +518,7 @@ class TestPopenGateway: @py.test.mark.xfail # "fix needed: dying remote process does not cause waitclose() to fail" def test_waitclose_on_remote_killed(self): - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') channel = gw.remote_exec(""" import os import time @@ -528,7 +536,7 @@ class TestPopenGateway: def test_endmarker_delivery_on_remote_killterm(): if not hasattr(py.std.os, 'kill'): py.test.skip("no os.kill()") - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') try: q = queue.Queue() channel = gw.remote_exec(source=''' @@ -547,7 +555,7 @@ def test_endmarker_delivery_on_remote_ki def test_socket_gw_host_not_found(gw): py.test.raises(execnet.HostNotFound, - 'execnet.SocketGateway("qowieuqowe", 9000)' + 'execnet.makegateway("socket=qwepoipqwe:9000")' ) class TestSshPopenGateway: @@ -559,7 +567,7 @@ class TestSshPopenGateway: monkeypatch.setattr(subprocess, 'Popen', lambda *args, **kwargs: l.append(args[0])) py.test.raises(AttributeError, - """execnet.SshGateway("xyz", ssh_config='qwe')""") + """execnet.makegateway("ssh=xyz//ssh_config=qwe")""") assert len(l) == 1 popen_args = l[0] i = popen_args.index('-F') @@ -570,11 +578,11 @@ class TestSshPopenGateway: def test_host_not_found(self, gw): py.test.raises(execnet.HostNotFound, - "execnet.SshGateway('nowhere.codespeak.net')") + "execnet.makegateway('ssh=nowhere.codespeak.net')") class TestThreads: def test_threads(self): - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') gw.remote_init_threads(3) c1 = gw.remote_exec("channel.send(channel.receive())") c2 = gw.remote_exec("channel.send(channel.receive())") @@ -586,7 +594,7 @@ class TestThreads: assert res == 42 def test_status_with_threads(self): - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') gw.remote_init_threads(3) c1 = gw.remote_exec("channel.send(1) ; channel.receive()") c2 = gw.remote_exec("channel.send(2) ; channel.receive()") @@ -604,7 +612,7 @@ class TestThreads: assert rstatus.execqsize == 0 def test_threads_twice(self): - gw = execnet.PopenGateway() + gw = execnet.makegateway('popen') gw.remote_init_threads(3) py.test.raises(IOError, gw.remote_init_threads, 3) @@ -618,3 +626,5 @@ def test_debug(monkeypatch): def test_nodebug(): from execnet import gateway_base assert not hasattr(gateway_base, 'debugfile') + + --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ -1.0.0 +1.0.0b4 -------------------------------- +* deprecate execnet.XYZGateway in favour of makegateway() * refine socketserver-examples, experimentally introduce a way to indirectly setup a SocketServer ("installvia") From commits-noreply at bitbucket.org Thu Nov 19 23:58:25 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 22:58:25 +0000 (UTC) Subject: [execnet-commit] execnet commit 0dc5d92806f6: enhance docs, add __len__ and __iter__ to groups Message-ID: <20091119225825.8D6807EEF4@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258668388 -3600 # Node ID 0dc5d92806f6cc4b8ceaae24306831f3d0505e44 # Parent d903ea597bb3594c1ea7c5036085cd9b7c2581a4 enhance docs, add __len__ and __iter__ to groups --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,17 @@ 1.0.0b4 -------------------------------- -* deprecate execnet.XYZGateway in favour of makegateway() +* deprecate execnet.XYZGateway in favour of direct makegateway() + Use makegateway() intead of the Popen/Socket/Ssh classes in examples. * refine socketserver-examples, experimentally introduce a - way to indirectly setup a SocketServer ("installvia") + way to indirectly setup a socket server ("installvia") -* introduce execnet.Group for grouping gateways, scoping termination. - introduce execnet.default_group through which all "global" calls - are routed. Use makegateway() intead of the Popen/Socket/Ssh classes - in examples. +* introduce execnet.Group for managing gateway creation + and termination. Introduce execnet.default_group through which + all "global" calls are routed. -* refine and automatically test some documentation examples +* refine and automatically test documentation examples 1.0.0b3 -------------------------------- --- a/doc/conf.py +++ b/doc/conf.py @@ -45,9 +45,10 @@ copyright = '2009, holger krekel and oth # built documents. # # The short X.Y version. -version = '1.0' +import execnet +version = execnet.__version__ # The full version, including alpha/beta/rc tags. -release = '1.0.0alpha' +release = execnet.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -94,7 +95,8 @@ pygments_style = 'sphinx' html_theme = 'sphinxdoc' #html_index = 'index.html' -#html_sidebars = {'index': 'indexsidebar.html', +html_sidebars = {'index': 'indexsidebar.html', +} # 'basics': 'indexsidebar.html', #} #html_additional_pages = {'index': 'index.html'} --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -14,6 +14,8 @@ termination of Gateways. Example usage: >>> list(group) # list all member gateways [, ] + >>> len(group) + 2 >>> group.terminate() # exit all member gateways Acessing Gateways on a group by ID @@ -31,6 +33,14 @@ in an gateway URL as passed to ``group.m >>> group['sub1'] -Using the default_group + +Gateways live in a default_group ------------------------------------------------------ +Each time you create a gateway with ``execnet.makegateway()`` +you actually use the ``execnet.default_group``:: + + >>> import execnet + >>> gw = execnet.makegateway("popen") + >>> gw in execnet.default_group + True --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -82,13 +82,15 @@ class TestGroup: group._cleanup_atexit() assert not group._activegateways - def test_gateway_id(self): + def test_gateway_and_id(self): group = Group() gw = group.makegateway("popen//id=hello") assert group["hello"] == gw py.test.raises(AttributeError, "del group['hello']") py.test.raises(AttributeError, "group['hello'] = 5") assert 'hello' in group + assert gw in group + assert len(group) == 1 gw.exit() assert 'hello' not in group py.test.raises(KeyError, "group['hello']") --- a/execnet/multi.py +++ b/execnet/multi.py @@ -33,7 +33,10 @@ class Group: return self._id2gateway[key] def __contains__(self, key): - return key in self._id2gateway + return key in self._id2gateway or key in self._activegateways + + def __len__(self): + return len(self._activegateways) def __iter__(self): return iter(list(self._activegateways)) --- a/doc/_templates/indexsidebar.html +++ b/doc/_templates/indexsidebar.html @@ -7,7 +7,8 @@ released versions in the Python Package Index.

    {% else %} -

    Current version: {{ version }}

    +

    Current: {{ version }} +[Changes]

    Get execnet from the Python Package Index, or install it with:

    easy_install -U execnet
    @@ -17,5 +18,4 @@ Index, or install it with:

    Join execnet-dev mailing list

    -

    open an issue.

    come to #pylib on FreeNode

    From commits-noreply at bitbucket.org Thu Nov 19 23:58:27 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 19 Nov 2009 22:58:27 +0000 (UTC) Subject: [execnet-commit] execnet commit e03e84d72df8: avoid closing a channel that was remotely triggered to be closed already Message-ID: <20091119225827.2425C7EEF5@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258671366 -3600 # Node ID e03e84d72df8541bf179217df0f44fbe06fae931 # Parent 0dc5d92806f6cc4b8ceaae24306831f3d0505e44 avoid closing a channel that was remotely triggered to be closed already --- a/execnet/threadpool.py +++ b/execnet/threadpool.py @@ -230,7 +230,6 @@ if __name__ == '__channelexec__': gw._trace("thread-dispatcher got None, exiting") execpool.shutdown() execpool.join() - gw._stopsend() raise gw._StopExecLoop gw._trace("dispatching exec task to thread pool") execpool.dispatch(gw.executetask, task) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -363,17 +363,20 @@ class Channel(object): if self._executing: raise IOError("cannot explicitly close channel within remote_exec") if self._closed: - trace("%r channel already closed, close() called." % (self, )) + trace("%r redundant call to close(), ignoring" %(self,)) if not self._closed: # state transition "opened/sendonly" --> "closed" # threads warning: the channel might be closed under our feet, # but it's never damaging to send too many CHANNEL_CLOSE messages - put = self.gateway._send - if error is not None: - put(Message.CHANNEL_CLOSE_ERROR(self.id, error)) - else: - put(Message.CHANNEL_CLOSE(self.id)) - trace("%r: sent channel close message" %(self,)) + # however, if the other side triggered a close already, we + # do not send back a closed message. + if not self._receiveclosed.isSet(): + put = self.gateway._send + if error is not None: + put(Message.CHANNEL_CLOSE_ERROR(self.id, error)) + else: + put(Message.CHANNEL_CLOSE(self.id)) + trace("%r: sent channel close message" %(self,)) if isinstance(error, RemoteError): self._remoteerrors.append(error) self._closed = True # --> "closed" --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -616,6 +616,20 @@ class TestThreads: gw.remote_init_threads(3) py.test.raises(IOError, gw.remote_init_threads, 3) +def test_close_initiating_remote_no_error(testdir): + import subprocess + p = testdir.makepyfile(""" + import execnet + gw = execnet.makegateway("popen") + gw.remote_init_threads(num=2) + ch = gw.remote_exec("channel.receive()") + ch.close() + """) + popen = subprocess.Popen([sys.executable, str(p)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = popen.communicate() + assert not stderr + def test_debug(monkeypatch): monkeypatch.setenv('EXECNET_DEBUG', "1") source = py.std.inspect.getsource(gateway_base) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 1.0.0b4 -------------------------------- +* cleanup gateway termination a bit + * deprecate execnet.XYZGateway in favour of direct makegateway() Use makegateway() intead of the Popen/Socket/Ssh classes in examples. From commits-noreply at bitbucket.org Tue Nov 24 10:39:56 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 09:39:56 +0000 (UTC) Subject: [execnet-commit] execnet commit a4fe6d21ca1a: preparing final execnet-1.0.0 Message-ID: <20091124093956.9489E7EF14@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258993666 -3600 # Node ID a4fe6d21ca1ae691b2f7ff1553e49261d9c0d806 # Parent e03e84d72df8541bf179217df0f44fbe06fae931 preparing final execnet-1.0.0 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def main(): author='holger krekel and others', author_email='holger at merlinux.eu', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX', --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0b4" +__version__ = "1.0.0" import execnet.apipkg --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,15 @@ -1.0.0b4 +1.0.0 -------------------------------- -* cleanup gateway termination a bit +* introduce execnet.Group for managing gateway creation + and termination. Introduce execnet.default_group through which + all "global" calls are routed. cleanup gateway termination a bit. -* deprecate execnet.XYZGateway in favour of direct makegateway() - Use makegateway() intead of the Popen/Socket/Ssh classes in examples. +* deprecate execnet.XYZGateway in favour of direct makegateway() calls. * refine socketserver-examples, experimentally introduce a way to indirectly setup a socket server ("installvia") - -* introduce execnet.Group for managing gateway creation - and termination. Introduce execnet.default_group through which - all "global" calls are routed. + through a gateway url. * refine and automatically test documentation examples From commits-noreply at bitbucket.org Tue Nov 24 10:39:58 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 09:39:58 +0000 (UTC) Subject: [execnet-commit] execnet commit 467184964d4a: fixing docs and some tests for jython Message-ID: <20091124093958.7C8837EF19@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259055576 -3600 # Node ID 467184964d4af2a9c724a5801bedf107d6464d4a # Parent 9b2015e7e393d3a95dbe4a1430a1c68a66bf86a7 fixing docs and some tests for jython --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -13,7 +13,7 @@ skiponjython = py.test.mark.skipif("sys. def test_serialize_error(gw): ch = gw.remote_exec("channel.send(ValueError(42))") - excinfo = py.test.raises(ch.RemoteError, "ch.receive()") + excinfo = py.test.raises(ch.RemoteError, ch.receive) assert "can't serialize" in str(excinfo.value) def test_deprecation(recwarn): --- /dev/null +++ b/doc/rel-1.0.0.txt @@ -0,0 +1,19 @@ +Hi all, + +execnet enables zero-install ad-hoc instantiation of local or remote +Python processes. It establishes channels for basic data communication. +Data Serialization is independent from Pickle and is tested between +Python2.4, 2.5, 2.6, 3.1 and Jython interpreters. + +execnet-1.0.0 (compared to 1.0.0b3) has bug fixes, new tested +examples and introduces execnet.Group for managing a dynamic +bunch of hosts. See the improved docs + + http://codespeak.net/execnet/ + +and below the changelog. I am set to improve and develop +execnet further and thus am very interested in feedback +and suggestions. + +cheers, +holger --- a/doc/basics.txt +++ b/doc/basics.txt @@ -74,7 +74,6 @@ in the connected interpreter: .. _`Channel`: .. _`channel-api`: -.. _`exchange data`: .. _`exchange data`: @@ -130,17 +129,20 @@ or at process termination (each group re rsync: synchronise filesystem with remote =============================================================== +.. currentmodule:: execnet + + ``execnet`` implements a simple efficient rsyncing protocol. -Here is a basic example:: +Here is a basic example for using RSync:: rsync = execnet.RSync('/tmp/source') gw = execnet.makegateway("popen") rsync.add_target(gw, '/tmp/dest') rsync.send() -More info on the RSync class: -.. currentmodule:: execnet +And here is API info about the RSync class. + .. autoclass:: RSync :members: add_target,send --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -139,10 +139,15 @@ def test_set(py2, py3): p = dump("set((1, 2, 3))") tp, v = py2.load(p) assert tp == "set" - assert v == "set([1, 2, 3])" + #assert v == "set([1, 2, 3])" # ordering prevents this assertion + assert v.startswith("set([") and v.endswith("])") + assert '1' in v and '2' in v and '3' in v + tp, v = py3.load(p) assert tp == "set" - assert v == "{1, 2, 3}" + #assert v == "{1, 2, 3}" # ordering prevents this assertion + assert v.startswith("{") and v.endswith("}") + assert '1' in v and '2' in v and '3' in v p = dump("set()") tp, v = py2.load(p) assert tp == "set" --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,9 @@ 1.0.0 * introduce execnet.Group for managing gateway creation and termination. Introduce execnet.default_group through which - all "global" calls are routed. cleanup gateway termination a bit. + all "global" calls are routed. cleanup gateway termination. + All Gateways get an id through which they can be + retrieved from a group object. * deprecate execnet.XYZGateway in favour of direct makegateway() calls. From commits-noreply at bitbucket.org Tue Nov 24 10:39:58 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 09:39:58 +0000 (UTC) Subject: [execnet-commit] execnet commit 9b2015e7e393: some python3 and deprecation related fixes Message-ID: <20091124093958.6CE8D7EF15@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1258995078 -3600 # Node ID 9b2015e7e393d3a95dbe4a1430a1c68a66bf86a7 # Parent a4fe6d21ca1ae691b2f7ff1553e49261d9c0d806 some python3 and deprecation related fixes --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -150,6 +150,7 @@ class Gateway(gateway_base.BaseGateway): class RInfo: def __init__(self, kwargs): self.__dict__.update(kwargs) + def __repr__(self): info = ", ".join(["%s=%s" % item for item in self.__dict__.items()]) --- a/execnet/multi.py +++ b/execnet/multi.py @@ -25,7 +25,7 @@ class Group: atexit.register(self._cleanup_atexit) def __repr__(self): - keys = self._id2gateway.keys() + keys = list(self._id2gateway) keys.sort() return "" %(keys,) --- a/testing/conftest.py +++ b/testing/conftest.py @@ -50,7 +50,7 @@ def pytest_funcarg__gw(request): scope = "session" if request.param == "popen": return request.cached_setup( - setup=execnet.PopenGateway, + setup=lambda: execnet.makegateway("popen"), teardown=lambda gw: gw.exit(), extrakey=request.param, scope=scope) @@ -68,9 +68,8 @@ def pytest_funcarg__gw(request): scope=scope) def setup_socket_gateway(): - proxygw = execnet.PopenGateway() - from execnet.gateway import SocketGateway - gw = SocketGateway.new_remote(proxygw, ("127.0.0.1", 0)) + proxygw = execnet.makegateway("popen") + gw = execnet.makegateway("socket//installvia=%s" % proxygw.id) gw.proxygw = proxygw return gw @@ -80,5 +79,5 @@ def teardown_socket_gateway(gw): def setup_ssh_gateway(request): sshhost = request.getfuncargvalue('specssh').ssh - gw = execnet.SshGateway(sshhost) + gw = execnet.makegateway("ssh=%s" %(sshhost,)) return gw --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -742,7 +742,7 @@ class _Stop(Exception): class Unserializer(object): num2func = {} # is filled after this class definition - py2str_as_py3str = False # True + py2str_as_py3str = True # True py3str_as_py2str = False # false means py2 will get unicode def __init__(self, stream): --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -86,8 +86,8 @@ class TestGroup: group = Group() gw = group.makegateway("popen//id=hello") assert group["hello"] == gw - py.test.raises(AttributeError, "del group['hello']") - py.test.raises(AttributeError, "group['hello'] = 5") + py.test.raises((TypeError, AttributeError), "del group['hello']") + py.test.raises((TypeError, AttributeError), "group['hello'] = 5") assert 'hello' in group assert gw in group assert len(group) == 1 From commits-noreply at bitbucket.org Tue Nov 24 14:47:28 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 13:47:28 +0000 (UTC) Subject: [execnet-commit] execnet commit 47e73b3c07c5: "" or "0.0.0.0" do not work on my VirtualBox/Win XP2 box Message-ID: <20091124134728.607637EF07@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259058730 28800 # Node ID 47e73b3c07c55bcb808bec88896dc072f9f20df2 # Parent 467184964d4af2a9c724a5801bedf107d6464d4a "" or "0.0.0.0" do not work on my VirtualBox/Win XP2 box so fallback to localhost. --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -255,7 +255,9 @@ class SocketGateway(Gateway): (realhost, realport) = channel.receive() #self._trace("new_remote received" # "port=%r, hostname = %r" %(realport, hostname)) - return gateway._group.makegateway("socket=%s:%s" %(host, realport)) + if not realhost or realhost=="0.0.0.0": + realhost = "localhost" + return gateway._group.makegateway("socket=%s:%s" %(realhost, realport)) new_remote = classmethod(new_remote) class HostNotFound(Exception): From commits-noreply at bitbucket.org Tue Nov 24 17:46:24 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 16:46:24 +0000 (UTC) Subject: [execnet-commit] execnet commit 6cb40cacafd7: Added tag 1.0.0 for changeset 7cf86a1811b1 Message-ID: <20091124164624.A8F9F7EF15@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259080915 -3600 # Node ID 6cb40cacafd7bdc87a57d9e82b3c1034e2da8c33 # Parent 7cf86a1811b11aa58112a255e6afba870c8d855e Added tag 1.0.0 for changeset 7cf86a1811b1 --- a/.hgtags +++ b/.hgtags @@ -1,1 +1,2 @@ 39e4b3a1397adad17ecfcd9e65a08521d90b2795 1.0.0b1 +7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 From commits-noreply at bitbucket.org Tue Nov 24 17:46:26 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 16:46:26 +0000 (UTC) Subject: [execnet-commit] execnet commit 5d1c438e335d: merge in accidental branch Message-ID: <20091124164626.937427EF3C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259081120 -3600 # Node ID 5d1c438e335dc369834207882ec5f3227d365c11 # Parent 47e73b3c07c55bcb808bec88896dc072f9f20df2 # Parent 6cb40cacafd7bdc87a57d9e82b3c1034e2da8c33 merge in accidental branch From commits-noreply at bitbucket.org Tue Nov 24 17:46:28 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 16:46:28 +0000 (UTC) Subject: [execnet-commit] execnet commit c75295641d3c: Added tag 1.0.0 for changeset 5d1c438e335d Message-ID: <20091124164628.1BEB47EF3E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259081137 -3600 # Node ID c75295641d3c589777ca92a70559e0e654d03d1b # Parent 5d1c438e335dc369834207882ec5f3227d365c11 Added tag 1.0.0 for changeset 5d1c438e335d --- a/.hgtags +++ b/.hgtags @@ -1,2 +1,4 @@ 39e4b3a1397adad17ecfcd9e65a08521d90b2795 1.0.0b1 7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 +7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 +5d1c438e335dc369834207882ec5f3227d365c11 1.0.0 From commits-noreply at bitbucket.org Tue Nov 24 17:46:29 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 24 Nov 2009 16:46:29 +0000 (UTC) Subject: [execnet-commit] execnet commit 7cf86a1811b1: include tests, mark 'installvia' experimental Message-ID: <20091124164629.DA9387EF3F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259076721 -3600 # Node ID 7cf86a1811b11aa58112a255e6afba870c8d855e # Parent 467184964d4af2a9c724a5801bedf107d6464d4a include tests, mark 'installvia' experimental --- a/doc/examples.txt +++ b/doc/examples.txt @@ -147,7 +147,7 @@ contents of remote files:: Instantiate a socket server in a new subprocess ----------------------------------------------------- -The following example opens a PopenGateway, i.e. a python +(experimental) The following example opens a PopenGateway, i.e. a python child process, and starts a socket server within that process and then opens a second gateway to the freshly started socketserver:: --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,13 @@ +include CHANGELOG +include README.txt +include setup.py +include LICENSE +graft doc +graft testing +prune doc/_build +exclude *.orig +exclude *.rej +recursive-exclude execnet *.pyc *$py.class +recursive-exclude testing *.pyc *$py.class +prune .svn +prune .hg From commits-noreply at bitbucket.org Wed Nov 25 12:03:58 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 25 Nov 2009 11:03:58 +0000 (UTC) Subject: [execnet-commit] execnet commit cb65283a1bc3: porting some issues from bb-py-trunk tracker Message-ID: <20091125110358.863817EEE2@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259147024 -3600 # Node ID cb65283a1bc3f795d5eeaf08c549ecda7b71639d # Parent c75295641d3c589777ca92a70559e0e654d03d1b porting some issues from bb-py-trunk tracker --- /dev/null +++ b/ISSUES.txt @@ -0,0 +1,35 @@ +report back errors when calling callbacks +--------------------------------------------------- + +tags: 1.0.1 1.1 +bb: http://bitbucket.org/hpk42/py-trunk/issue/37/ +path: execnet/gateway_base.py + +Callbacks are invoked in the receiver-thread +and any exception will kill the whole receiver-thread. +This is especially bad if it happens remotely as +it leaves the other side inaccesssible. + +Control-C behaviour / best practises +------------------------------------------- + +tags: 1.1 +bb: http://bitbucket.org/hpk42/py-trunk/issue/36 +path: execnet/gateway_base.py + +currently calling channel.receive() in the +main thread will prevent Control-C from passing. +Consider an internal timeout so that signals +get a chance to propagate. + + +rsync links from posix to windows +----------------------------------------- + +tags: 1.1 +bb: http://bitbucket.org/hpk42/py-trunk/issue/35 +path: execnet/rsync.py + +currently rsyncing bails out with a strange message when trying to rsync +links. should provide better message or even some support for doing it. + From commits-noreply at bitbucket.org Mon Nov 30 13:10:47 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 30 Nov 2009 12:10:47 +0000 (UTC) Subject: [execnet-commit] execnet commit 778696674e2d: add some more issues/thoughts Message-ID: <20091130121047.A08597EEE7@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259582636 -3600 # Node ID 778696674e2d21287300ea9e3719aea65f82a95a # Parent cb65283a1bc3f795d5eeaf08c549ecda7b71639d add some more issues/thoughts --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,7 +1,7 @@ report back errors when calling callbacks --------------------------------------------------- -tags: 1.0.1 1.1 +tags: 1.0 bb: http://bitbucket.org/hpk42/py-trunk/issue/37/ path: execnet/gateway_base.py @@ -12,7 +12,6 @@ it leaves the other side inaccesssible. Control-C behaviour / best practises ------------------------------------------- - tags: 1.1 bb: http://bitbucket.org/hpk42/py-trunk/issue/36 path: execnet/gateway_base.py @@ -25,11 +24,60 @@ get a chance to propagate. rsync links from posix to windows ----------------------------------------- - -tags: 1.1 +tags: 1.1 feature bb: http://bitbucket.org/hpk42/py-trunk/issue/35 path: execnet/rsync.py currently rsyncing bails out with a strange message when trying to rsync links. should provide better message or even some support for doing it. +PopenGateway imports local execnet package +------------------------------------------------- +tags: 1.0 feature + +For gateway processes instantiated on the same +machine the local execnet package could be +imported instead of the current serialization +of source code. This should result in faster +sub process creation and better traceback-printing. + +a process keeping state across invoactions +-------------------------------------------------- +tags: 1.1 feature newdist + +A building block for keeping permanent network +connections would be a started-on-first-access process +that keeps state. Considered: + +- PopenGateway imports local execnet package + and avoids parse/compile code strings + +- a rendevouz process that starts threads for each + incoming connection. On each such connection + a SocketGateway can be instantiated but it + will re-use the already-imported gateway base code + so again avoid parse/compile on arbitrary strings. + +a channel-IO based gateway +-------------------------------------------------- +tags: 1.1 feature newdist + +A building block for establishing a permanent hierarchy +of processes is a Gateway that operates on a Channel object. +This way a gateway can be instantiated through another +intermediating gateway which bi-directionallry forwads +Messages through a existing channel. + +fix group termination +--------------------------- +tags: 1.0 bug + +This session does not close down gateways like expected: + +>>> execnet.makegateway("popen") +>>> execnet.makegateway("popen") +>>> execnet.default_group.terminate() +>>> execnet.default_group + +>>> execnet.default_group['1'].remote_exec("channel.send(1)").receive() +1 From commits-noreply at bitbucket.org Tue Dec 1 16:02:09 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 15:02:09 +0000 (UTC) Subject: [execnet-commit] execnet commit 4b3e70882e97: make iteration ordered by gateway id Message-ID: <20091201150209.9C4CF7EF1C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259585385 -3600 # Node ID 4b3e70882e978e8a59a16bfa203a84a43edf1a8a # Parent 778696674e2d21287300ea9e3719aea65f82a95a make iteration ordered by gateway id --- a/execnet/multi.py +++ b/execnet/multi.py @@ -39,7 +39,10 @@ class Group: return len(self._activegateways) def __iter__(self): - return iter(list(self._activegateways)) + l = list(self._id2gateway.items()) + l.sort() + for id, gw in l: + yield gw def makegateway(self, spec): """ create and configure a gateway to a Python interpreter --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -82,6 +82,17 @@ class TestGroup: group._cleanup_atexit() assert not group._activegateways + def test_group_ordering(self): + group = Group() + gw = group.makegateway("popen//id=3") + gw = group.makegateway("popen//id=2") + gw = group.makegateway("popen//id=5") + gwlist = list(group) + assert len(gwlist) == 3 + idlist = [x.id for x in gwlist] + assert idlist == list('235') + group.terminate() + def test_gateway_and_id(self): group = Group() gw = group.makegateway("popen//id=hello") From commits-noreply at bitbucket.org Tue Dec 1 16:02:11 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 15:02:11 +0000 (UTC) Subject: [execnet-commit] execnet commit 398fae98d769: have popen-gateways use imports instead of source-strings, Message-ID: <20091201150211.6C4B67EF1D@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259678705 -3600 # Node ID 398fae98d7699421e05369463dddd8fd13cb02c9 # Parent 4b3e70882e978e8a59a16bfa203a84a43edf1a8a have popen-gateways use imports instead of source-strings, also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) this commit also contains some related bootstrapping cleanups. --- a/ISSUES.txt +++ b/ISSUES.txt @@ -31,16 +31,6 @@ path: execnet/rsync.py currently rsyncing bails out with a strange message when trying to rsync links. should provide better message or even some support for doing it. -PopenGateway imports local execnet package -------------------------------------------------- -tags: 1.0 feature - -For gateway processes instantiated on the same -machine the local execnet package could be -imported instead of the current serialization -of source code. This should result in faster -sub process creation and better traceback-printing. - a process keeping state across invoactions -------------------------------------------------- tags: 1.1 feature newdist --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +1.0.1 (pending) +-------------------------------- + +- have popen-gateways use imports instead of source-strings, + also improves debugging/tracebacks, as a side effect + popen-gateway startup can be substantially faster (>30%) + 1.0.0 -------------------------------- --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -8,6 +8,7 @@ import textwrap import execnet from execnet.gateway_base import Message, Popen2IO, SocketIO from execnet import gateway_base +importdir = os.path.dirname(os.path.dirname(execnet.__file__)) class Gateway(gateway_base.BaseGateway): """ Gateway to a local or remote Python Intepreter. """ @@ -49,25 +50,16 @@ class Gateway(gateway_base.BaseGateway): self._stopsend() #self.join(timeout=timeout) # receiverthread receive close() messages - def _remote_bootstrap_gateway(self, io, extra=''): - """ return Gateway with a asynchronously remotely - initialized counterpart Gateway (which may or may not succeed). - Note that the other sides gateways starts enumerating - its channels with even numbers while the sender - gateway starts with odd numbers. This allows to - uniquely identify channels across both sides. + def _remote_bootstrap_gateway(self, io): + """ send gateway bootstrap code to a remote Python interpreter + endpoint, which reads from io for a string to execute. """ - bootstrap = [extra] - bootstrap += [inspect.getsource(gateway_base)] - bootstrap += [io.server_stmt, - "io.write('1'.encode('ascii'))", - "SlaveGateway(io=io, _startcount=2).serve()", - ] - source = "\n".join(bootstrap) - self._trace("sending gateway bootstrap code") - #open("/tmp/bootstrap.py", 'w').write(source) - repr_source = repr(source) + "\n" - io.write(repr_source.encode('ascii')) + sendexec(io, + inspect.getsource(gateway_base), + self._remotesetup, + "io.write('1'.encode('ascii'))", + "serve(io)" + ) s = io.read(1) assert s == "1".encode('ascii') @@ -170,6 +162,7 @@ channel.send(dict( """ class PopenCmdGateway(Gateway): + _remotesetup = "io = init_popen_io()" def __init__(self, args): from subprocess import Popen, PIPE self._popen = p = Popen(args, stdin=PIPE, stdout=PIPE) @@ -195,19 +188,23 @@ class PopenGateway(PopenCmdGateway): args = [str(python), '-c', popen_bootstrapline] super(PopenGateway, self).__init__(args) - def _remote_bootstrap_gateway(self, io, extra=''): - # have the subprocess use the same PYTHONPATH and py lib - x = os.path.dirname(os.path.dirname(execnet.__file__)) - ppath = os.environ.get('PYTHONPATH', '') - plist = [str(x)] + ppath.split(':') - s = "\n".join([extra, - "import sys ; sys.path[:0] = %r" % (plist,), - "import os ; os.environ['PYTHONPATH'] = %r" % ppath, - inspect.getsource(stdouterrin_setnull), - "stdouterrin_setnull()", - "" - ]) - super(PopenGateway, self)._remote_bootstrap_gateway(io, s) + def _remote_bootstrap_gateway(self, io): + sendexec(io, + "import sys", + "sys.stdout.write('1'.encode('ascii'))", + "sys.stdout.flush()", + popen_bootstrapline) + sendexec(io, + "import sys ; sys.path.insert(0, %r)" % importdir, + "from execnet.gateway_base import serve, init_popen_io", + "serve(init_popen_io())", + ) + s = io.read(1) + assert s == "1".encode('ascii') + +def sendexec(io, *sources): + source = "\n".join(sources) + io.write((repr(source)+ "\n").encode('ascii')) class SocketGateway(Gateway): """ This Gateway provides interaction with a remote process @@ -216,6 +213,8 @@ class SocketGateway(Gateway): (py/execnet/script/socketserver.py) that accepts SocketGateway connections. """ + _remotesetup = "io = SocketIO(clientsock)" + def __init__(self, host, port): """ instantiate a gateway to a process accessed via a host/port specified socket. @@ -236,7 +235,7 @@ class SocketGateway(Gateway): instantiated through the given 'gateway'. """ if hostport is None: - host, port = ('', 0) # XXX works on all platforms? + host, port = ('localhost', 0) else: host, port = hostport @@ -268,7 +267,6 @@ class SshGateway(PopenCmdGateway): established via the 'ssh' command line binary. The remote side needs to have a Python interpreter executable. """ - def __init__(self, sshaddress, remotepython=None, ssh_config=None): """ instantiate a remote ssh process with the given 'sshaddress' and remotepython version. @@ -284,49 +282,10 @@ class SshGateway(PopenCmdGateway): args.extend([sshaddress, remotecmd]) super(SshGateway, self).__init__(args) - def _remote_bootstrap_gateway(self, io, s=""): - extra = "\n".join([ - inspect.getsource(stdouterrin_setnull), - "stdouterrin_setnull()"]) + def _remote_bootstrap_gateway(self, io): try: - super(SshGateway, self)._remote_bootstrap_gateway(io, extra) + super(SshGateway, self)._remote_bootstrap_gateway(io) except EOFError: ret = self._popen.wait() if ret == 255: raise HostNotFound(self.remoteaddress) - -def stdouterrin_setnull(): - """ redirect file descriptors 0 and 1 (and possibly 2) to /dev/null. - note that this function may run remotely without py lib support. - """ - # complete confusion (this is independent from the sys.stdout - # and sys.stderr redirection that gateway.remote_exec() can do) - # note that we redirect fd 2 on win too, since for some reason that - # blocks there, while it works (sending to stderr if possible else - # ignoring) on *nix - import sys, os - if not hasattr(os, 'dup'): # jython - return - try: - devnull = os.devnull - except AttributeError: - if os.name == 'nt': - devnull = 'NUL' - else: - devnull = '/dev/null' - # stdin - sys.stdin = os.fdopen(os.dup(0), 'r', 1) - fd = os.open(devnull, os.O_RDONLY) - os.dup2(fd, 0) - os.close(fd) - - # stdout - sys.stdout = os.fdopen(os.dup(1), 'w', 1) - fd = os.open(devnull, os.O_WRONLY) - os.dup2(fd, 1) - - # stderr for win32 - if os.name == 'nt': - sys.stderr = os.fdopen(os.dup(2), 'w', 1) - os.dup2(fd, 2) - os.close(fd) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -49,8 +49,6 @@ else: # ___________________________________________________________________________ class SocketIO: - server_stmt = "io = SocketIO(clientsock)" - error = (socket.error, EOFError) def __init__(self, sock): self.sock = sock @@ -92,12 +90,6 @@ class SocketIO: self.writeable = None class Popen2IO: - server_stmt = """ -import os, sys, tempfile -io = Popen2IO(sys.stdout, sys.stdin) -sys.stdout = tempfile.TemporaryFile('w') -sys.stdin = tempfile.TemporaryFile('r') -""" error = (IOError, OSError, EOFError) def __init__(self, outfile, infile): @@ -326,7 +318,7 @@ class Channel(object): Msg = Message.CHANNEL_CLOSE try: self.gateway._send(Msg(self.id)) - except ValueError: # XXX IO operation on closed file, why? + except (IOError, ValueError): # ignore problems with sending pass def _getremoteerror(self): @@ -1013,3 +1005,42 @@ class Serializer(object): def save_frozenset(self, s): self._write_set(s, opcode.FROZENSET) + +def init_popen_io(): + if not hasattr(os, 'dup'): # jython + io = Popen2IO(sys.stdout, sys.stdin) + import tempfile + sys.stdin = tempfile.TemporaryFile('r') + sys.stdout = tempfile.TemporaryFile('w') + else: + try: + devnull = os.devnull + except AttributeError: + if os.name == 'nt': + devnull = 'NUL' + else: + devnull = '/dev/null' + # stdin + stdin = os.fdopen(os.dup(0), 'r', 1) + fd = os.open(devnull, os.O_RDONLY) + os.dup2(fd, 0) + os.close(fd) + + # stdout + stdout = os.fdopen(os.dup(1), 'w', 1) + fd = os.open(devnull, os.O_WRONLY) + os.dup2(fd, 1) + + # stderr for win32 + if os.name == 'nt': + sys.stderr = os.fdopen(os.dup(2), 'w', 1) + os.dup2(fd, 2) + os.close(fd) + io = Popen2IO(stdout, stdin) + sys.stdin = os.fdopen(0, 'r', 1) + sys.stdout = os.fdopen(1, 'w', 1) + return io + +def serve(io): + trace("creating slavegateway on %r" %(io,)) + SlaveGateway(io=io, _startcount=2).serve() --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -485,6 +485,11 @@ class TestPopenGateway: x = c.receive() assert x == str(waschangedir) + def test_remoteerror_readable_traceback(self, gw): + e = py.test.raises(gateway_base.RemoteError, + 'gw.remote_exec("x y").waitclose()') + assert "gateway_base" in e.value.formatted + def test_many_popen(self): num = 4 l = [] @@ -618,15 +623,18 @@ class TestThreads: def test_close_initiating_remote_no_error(testdir): import subprocess + dir = py.path.local(execnet.__file__).dirpath().dirpath() p = testdir.makepyfile(""" + import sys + sys.path.insert(0, %r) import execnet gw = execnet.makegateway("popen") gw.remote_init_threads(num=2) ch = gw.remote_exec("channel.receive()") ch.close() - """) + """ % str(dir)) popen = subprocess.Popen([sys.executable, str(p)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout=subprocess.PIPE, stderr=subprocess.PIPE,) stdout, stderr = popen.communicate() assert not stderr --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.0" +__version__ = "1.0.1a" import execnet.apipkg --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -12,7 +12,7 @@ def test_subprocess_interaction(anypytho stdin=subprocess.PIPE, stdout=subprocess.PIPE) def send(line): popen.stdin.write(line.encode('ascii')) - if sys.version_info > (3,0): # 3k still buffers + if sys.version_info > (3,0) or sys.platform.startswith("java"): popen.stdin.flush() def receive(): return popen.stdout.readline().decode('ascii') @@ -96,7 +96,7 @@ def test_io_message(anypython, tmpdir): def test_popen_io(anypython, tmpdir): check = tmpdir.join("check.py") check.write(py.code.Source(gateway_base, """ - do_exec(Popen2IO.server_stmt, globals()) + do_exec("io = init_popen_io()", globals()) io.write("hello".encode('ascii')) s = io.read(1) assert s == "x".encode('ascii') @@ -145,9 +145,10 @@ def test_geterrortext(anypython, tmpdir) print (out) assert "all passed" in out + at py.test.mark.skipif("not hasattr(os, 'dup')") def test_stdouterrin_setnull(): cap = py.io.StdCaptureFD() - gateway.stdouterrin_setnull() + io = gateway_base.init_popen_io() import os os.write(1, "hello".encode('ascii')) if os.name == "nt": From commits-noreply at bitbucket.org Tue Dec 1 16:13:47 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 15:13:47 +0000 (UTC) Subject: [execnet-commit] execnet commit 92de97216048: fix group termination to work more reliably Message-ID: <20091201151347.645857EE7F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259680403 -3600 # Node ID 92de97216048d44408050c688f65cd1ba18888e8 # Parent 398fae98d7699421e05369463dddd8fd13cb02c9 fix group termination to work more reliably (and also fix little python3 issue) --- a/ISSUES.txt +++ b/ISSUES.txt @@ -58,16 +58,3 @@ This way a gateway can be instantiated t intermediating gateway which bi-directionallry forwads Messages through a existing channel. -fix group termination ---------------------------- -tags: 1.0 bug - -This session does not close down gateways like expected: - ->>> execnet.makegateway("popen") ->>> execnet.makegateway("popen") ->>> execnet.default_group.terminate() ->>> execnet.default_group - ->>> execnet.default_group['1'].remote_exec("channel.send(1)").receive() -1 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ 1.0.1 (pending) also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) +- fix group.terminate() termination to work (more reliably) + 1.0.0 -------------------------------- --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -191,7 +191,7 @@ class PopenGateway(PopenCmdGateway): def _remote_bootstrap_gateway(self, io): sendexec(io, "import sys", - "sys.stdout.write('1'.encode('ascii'))", + "sys.stdout.write('1')", "sys.stdout.flush()", popen_bootstrapline) sendexec(io, --- a/execnet/multi.py +++ b/execnet/multi.py @@ -103,8 +103,8 @@ class Group: gateway.id = id def _unregister(self, gateway): + del self._id2gateway[gateway.id] del self._activegateways[gateway] - del self._id2gateway[gateway.id] def _cleanup_atexit(self): trace("=== atexit cleanup %r ===" %(self,)) --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -17,6 +17,8 @@ termination of Gateways. Example usage: >>> len(group) 2 >>> group.terminate() # exit all member gateways + >>> group + Acessing Gateways on a group by ID ------------------------------------------------------ --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -82,7 +82,7 @@ class TestGroup: group._cleanup_atexit() assert not group._activegateways - def test_group_ordering(self): + def test_group_ordering_and_termination(self): group = Group() gw = group.makegateway("popen//id=3") gw = group.makegateway("popen//id=2") @@ -92,6 +92,8 @@ class TestGroup: idlist = [x.id for x in gwlist] assert idlist == list('235') group.terminate() + assert not group + assert repr(group) == "" def test_gateway_and_id(self): group = Group() From commits-noreply at bitbucket.org Tue Dec 1 18:00:17 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 17:00:17 +0000 (UTC) Subject: [execnet-commit] execnet commit a2b92aa8e4eb: skip/adapt tests properly to pass on pypy-c and jython Message-ID: <20091201170017.3D0A47EF06@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259686052 -3600 # Node ID a2b92aa8e4ebc43e05a3cec33c6037f924dd8d17 # Parent 92de97216048d44408050c688f65cd1ba18888e8 skip/adapt tests properly to pass on pypy-c and jython --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -8,7 +8,7 @@ def test_subprocess_interaction(anypytho line = gateway.popen_bootstrapline compile(line, 'xyz', 'exec') args = [str(anypython), '-c', line] - popen = subprocess.Popen(args, bufsize=0, stderr=subprocess.STDOUT, + popen = subprocess.Popen(args, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE) def send(line): popen.stdin.write(line.encode('ascii')) @@ -30,6 +30,7 @@ def test_subprocess_interaction(anypytho send("world\n") s = receive() assert s == "received: world\n" + send('\n') # terminate loop finally: popen.stdin.close() popen.stdout.close() @@ -42,6 +43,8 @@ def read_write_loop(): while 1: try: line = sys.stdin.readline() + if not line.strip(): + break sys.stdout.write("received: %s" % line) sys.stdout.flush() except (IOError, EOFError): @@ -49,7 +52,8 @@ def read_write_loop(): def pytest_generate_tests(metafunc): if 'anypython' in metafunc.funcargnames: - for name in 'python3.1', 'python2.4', 'python2.5', 'python2.6': + for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', + 'pypy-c', 'jython'): metafunc.addcall(id=name, param=name) def pytest_funcarg__anypython(request): --- a/testing/test_rsync.py +++ b/testing/test_rsync.py @@ -4,7 +4,7 @@ import execnet def pytest_funcarg__gw1(request): return request.cached_setup( - setup=execnet.PopenGateway, + setup=lambda: execnet.makegateway("popen"), teardown=lambda val: val.exit(), scope="module" ) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -9,7 +9,8 @@ from execnet.threadpool import WorkerPoo queue = py.builtin._tryimport('queue', 'Queue') TESTTIMEOUT = 10.0 # seconds -skiponjython = py.test.mark.skipif("sys.platform.startswith('java')") +needs_early_gc = py.test.mark.skipif("not hasattr(sys, 'getrefcount')") +needs_osdup = py.test.mark.skipif("not hasattr(os, 'dup')") def test_serialize_error(gw): ch = gw.remote_exec("channel.send(ValueError(42))") @@ -279,7 +280,7 @@ class TestChannelBasicBehaviour: assert l == [0, 100, 200, 300, 400] return subchannel - @skiponjython + @needs_early_gc def test_channel_callback_remote_freed(self, gw): channel = self.check_channel_callback_stays_active(gw, earlyfree=False) # freed automatically at the end of producer() @@ -405,7 +406,7 @@ class TestChannelFile: channel = gw.newchannel() py.test.raises(ValueError, 'channel.makefile("rw")') - @skiponjython + @needs_osdup def test_confusion_from_os_write_stdout(self, gw): channel = gw.remote_exec(""" import os @@ -420,7 +421,7 @@ class TestChannelFile: res = channel.receive() assert res == 42 - @skiponjython + @needs_osdup def test_confusion_from_os_write_stderr(self, gw): channel = gw.remote_exec(""" import os From commits-noreply at bitbucket.org Tue Dec 1 18:00:23 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 17:00:23 +0000 (UTC) Subject: [execnet-commit] execnet commit 9eddac8d8fc3: get rid of custom source-detection and fix "is not" to "!=" for pypy-c/jython Message-ID: <20091201170023.1BA367EF12@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259686787 -3600 # Node ID 9eddac8d8fc354c232459185a756c48c6c15de07 # Parent a2b92aa8e4ebc43e05a3cec33c6037f924dd8d17 get rid of custom source-detection and fix "is not" to "!=" for pypy-c/jython --- a/execnet/rsync.py +++ b/execnet/rsync.py @@ -15,8 +15,7 @@ try: except ImportError: from Queue import Queue -remote_file = os.path.join(os.path.dirname(__file__), 'rsync_remote.py') -REMOTE_SOURCE = open(remote_file, 'r').read() + "\nf()" +import execnet.rsync_remote class RSync(object): """ This class allows to send a directory structure (recursively) @@ -151,7 +150,7 @@ class RSync(object): assert name in ('delete',) def itemcallback(req): self._receivequeue.put((channel, req)) - channel = gateway.remote_exec(REMOTE_SOURCE) + channel = gateway.remote_exec(execnet.rsync_remote) channel.setcallback(itemcallback, endmarker = None) channel.send((str(destdir), options)) self._channels[channel] = finishedcallback --- a/execnet/rsync_remote.py +++ b/execnet/rsync_remote.py @@ -1,7 +1,7 @@ """ (c) 2006-2009, Armin Rigo, Holger Krekel, Maciej Fijalkowski """ -def f(): +def serve_rsync(channel): import os, stat, shutil try: from hashlib import md5 @@ -79,7 +79,7 @@ def f(): channel.send(("links", None)) msg = channel.receive() - while msg is not 42: + while msg != 42: # we get symlink _type, relpath, linkpoint = msg assert _type == "link" @@ -93,3 +93,5 @@ def f(): msg = channel.receive() channel.send(("done", None)) +if __name__ == '__channelexec__': + serve_rsync(channel) From commits-noreply at bitbucket.org Tue Dec 1 21:03:30 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 20:03:30 +0000 (UTC) Subject: [execnet-commit] execnet commit b6af4948c926: remove potential support and tests for stdout/stderr redirection Message-ID: <20091201200330.3E46D7EF1C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259689143 -3600 # Node ID b6af4948c926f66fecef13c777d31cad11e14cd8 # Parent 9eddac8d8fc354c232459185a756c48c6c15de07 remove potential support and tests for stdout/stderr redirection --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -106,39 +106,6 @@ class Gateway(gateway_base.BaseGateway): self._remotechannelthread = self.remote_exec(source) self._remotechannelthread.send(num) - def _remote_redirect(self, stdout=None, stderr=None): - """ return a handle representing a redirection of a remote - end's stdout to a local file object. with handle.close() - the redirection will be reverted. - """ - # XXX implement a remote_exec_in_globals(...) - # to send ThreadOut implementation over - clist = [] - for name, out in ('stdout', stdout), ('stderr', stderr): - if out: - outchannel = self.newchannel() - outchannel.setcallback(getattr(out, 'write', out)) - channel = self.remote_exec(""" - import sys - outchannel = channel.receive() - ThreadOut(sys, %r).setdefaultwriter(outchannel.send) - """ % name) - channel.send(outchannel) - clist.append(channel) - for c in clist: - c.waitclose() - class Handle: - def close(_): - for name, out in ('stdout', stdout), ('stderr', stderr): - if out: - c = self.remote_exec(""" - import sys - channel.gateway._ThreadOut(sys, %r).resetdefault() - """ % name) - c.waitclose() - return Handle() - - class RInfo: def __init__(self, kwargs): self.__dict__.update(kwargs) --- a/testing/test_serializer.py +++ b/testing/test_serializer.py @@ -197,16 +197,6 @@ def test_small_long(py2, py3): tp, s = py3.load(p) assert s == "123" - - at py.test.mark.xfail -# I'm not sure if we need the complexity. -def test_recursive_list(py2, py3): - l = [1, 2, 3] - l.append(l) - p = py2.dump(l) - tp, rep = py2.load(l) - assert tp == "list" - def test_bytes(py2, py3): p = py3.dump("b'hi'") tp, v = py2.load(p) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -313,32 +313,6 @@ class TestChannelBasicBehaviour: assert err assert str(err).find("ValueError") != -1 - @py.test.mark.xfail - def test_remote_redirect_stdout(self, gw): - out = py.io.TextIO() - handle = gw._remote_redirect(stdout=out) - c = gw.remote_exec("print 42") - c.waitclose(TESTTIMEOUT) - handle.close() - s = out.getvalue() - assert s.strip() == "42" - - @py.test.mark.xfail - def test_remote_exec_redirect_multi(self, gw): - num = 3 - l = [[] for x in range(num)] - channels = [gw.remote_exec("print %d" % i, - stdout=l[i].append) - for i in range(num)] - for x in channels: - x.waitclose(TESTTIMEOUT) - - for i in range(num): - subl = l[i] - assert subl - s = subl[0] - assert s.strip() == str(i) - class TestChannelFile: def test_channel_file_write(self, gw): channel = gw.remote_exec(""" From commits-noreply at bitbucket.org Tue Dec 1 21:03:30 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 1 Dec 2009 20:03:30 +0000 (UTC) Subject: [execnet-commit] execnet commit 2c58c2663ecd: refine gateway termination such that proper and unexpected exits Message-ID: <20091201200330.641967EF1D@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259697442 -3600 # Node ID 2c58c2663ecdc212860bebbab8af6d500408692e # Parent b6af4948c926f66fecef13c777d31cad11e14cd8 refine gateway termination such that proper and unexpected exits can be better distinguished: - channel.receive and channel.waitclose() will now raise EOFError if the reading connection unexpectedly closed - channel.send will raise an IOError if the writing connection is closed - callback endmarkers are delivered on unexpected closes --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -40,14 +40,15 @@ class Gateway(gateway_base.BaseGateway): def exit(self): """ trigger gateway exit. """ - self._trace("trigger gateway exit") + self._trace("gateway.exit() called") try: self._group._unregister(self) except KeyError: return # we assume it's already happened - self._trace("stopping exec and closing write connection") + self._trace("--> stopping exec and sending GATEWAY_TERMINATE") self._stopexec() - self._stopsend() + self._send(Message.GATEWAY_TERMINATE(0, '')) + self._io.close_write() #self.join(timeout=timeout) # receiverthread receive close() messages def _remote_bootstrap_gateway(self, io): --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -203,9 +203,15 @@ def _setupmessages(): def received(self, gateway): gateway._channelfactory._local_close(self.channelid, sendonly=True) - classes = [STATUS, CHANNEL_OPEN, CHANNEL_NEW, CHANNEL_DATA, - CHANNEL_CLOSE, CHANNEL_CLOSE_ERROR, CHANNEL_LAST_MESSAGE] + class GATEWAY_TERMINATE(Message): + def received(self, gateway): + gateway._io.close_read() + raise SystemExit(0) + classes = [ + STATUS, CHANNEL_OPEN, CHANNEL_NEW, CHANNEL_DATA, CHANNEL_CLOSE, + CHANNEL_CLOSE_ERROR, CHANNEL_LAST_MESSAGE, GATEWAY_TERMINATE + ] for i, cls in enumerate(classes): Message._types[i] = cls cls.msgtype = i @@ -318,13 +324,17 @@ class Channel(object): Msg = Message.CHANNEL_CLOSE try: self.gateway._send(Msg(self.id)) - except (IOError, ValueError): # ignore problems with sending + except IOError: # ignore problems with sending pass def _getremoteerror(self): try: return self._remoteerrors.pop(0) except IndexError: + try: + return self.gateway._error + except AttributeError: + pass return None # @@ -382,8 +392,10 @@ class Channel(object): """ wait until this channel is closed (or the remote side otherwise signalled that no more data was being sent). The channel may still hold receiveable items, but not receive - any more after waitclose() has returned. exceptions from executing + any more after waitclose() has returned. Exceptions from executing code on the other side are reraised as local channel.RemoteErrors. + EOFError is raised if the reading-connection was prematurely closed, + which often indicates a dying process. """ self._receiveclosed.wait(timeout=timeout) # wait for non-"opened" state if not self._receiveclosed.isSet(): @@ -395,7 +407,9 @@ class Channel(object): def send(self, item): """sends the given item to the other side of the channel, possibly blocking if the sender queue is full. - Note that an item needs to be marshallable. + The item must be a simple python type and will be + copied to the other side by value. IOError is + raised if the write pipe was prematurely closed. """ if self.isclosed(): raise IOError("cannot send to %r" %(self,)) @@ -606,20 +620,14 @@ class BaseGateway(object): except sysex: break except EOFError: + self._error = sys.exc_info()[1] break except: self._trace("RECEIVERTHREAD: %s " %( geterrortext(self.exc_info()), )) break finally: - # XXX we need to signal fatal error states to - # channels/callbacks, particularly ones - # where the other side just died. self._stopexec() - try: - self._stopsend() - except IOError: - self._trace('IOError on _stopsend()') self._channelfactory._finished_receiving() if threading: # might be None during shutdown/finalization self._trace('leaving %r' % threading.currentThread()) @@ -629,10 +637,6 @@ class BaseGateway(object): msg.writeto(self._serializer) self._trace('sent -> %r' % msg) - def _stopsend(self): - self._io.close_write() - self._trace('closed IO write pipe') - def _stopexec(self): pass @@ -671,7 +675,7 @@ class SlaveGateway(BaseGateway): while 1: item = self._execqueue.get() if item is None: - self._stopsend() + self._io.close_write() break try: self.executetask(item) --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -496,41 +496,43 @@ class TestPopenGateway: assert rinfo.cwd == py.std.os.getcwd() assert rinfo.version_info == py.std.sys.version_info - @py.test.mark.xfail # "fix needed: dying remote process does not cause waitclose() to fail" def test_waitclose_on_remote_killed(self): gw = execnet.makegateway('popen') channel = gw.remote_exec(""" import os import time channel.send(os.getpid()) - while 1: - channel.send("#" * 100) + time.sleep(100) """) remotepid = channel.receive() py.process.kill(remotepid) - py.test.raises(channel.RemoteError, "channel.waitclose(TESTTIMEOUT)") - py.test.raises(EOFError, channel.send, None) + py.test.raises(EOFError, "channel.waitclose(TESTTIMEOUT)") + py.test.raises(IOError, channel.send, None) py.test.raises(EOFError, channel.receive) - at py.test.mark.xfail + def test_receive_on_remote_io_closed(self): + gw = execnet.makegateway('popen') + channel = gw.remote_exec(""" + raise SystemExit() + """) + py.test.raises(EOFError, channel.receive) + + at py.test.mark.skipif("not hasattr(os, 'kill')") + at py.test.mark.xfail("sys.platform.startswith('java')") def test_endmarker_delivery_on_remote_killterm(): - if not hasattr(py.std.os, 'kill'): - py.test.skip("no os.kill()") gw = execnet.makegateway('popen') - try: - q = queue.Queue() - channel = gw.remote_exec(source=''' - import os - os.kill(os.getpid(), 15) - ''') - channel.setcallback(q.put, endmarker=999) - val = q.get(TESTTIMEOUT) - assert val == 999 - err = channel._getremoteerror() - finally: - gw.exit() - assert "killed" in str(err) - assert "15" in str(err) + q = queue.Queue() + channel = gw.remote_exec(source=''' + import os + os.kill(os.getpid(), 15) + ''') + channel.setcallback(q.put, endmarker=999) + val = q.get(TESTTIMEOUT) + assert val == 999 + # XXX jython does not get its io-read pipe closed + # when the remote side died?! + err = channel._getremoteerror() + assert isinstance(err, EOFError) def test_socket_gw_host_not_found(gw): --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,11 @@ 1.0.1 (pending) - fix group.terminate() termination to work (more reliably) +- EOFError on channel.receive/waitclose if the other + side unexpectedly went away. When a gateway exits + it now internally sends an explicit termination message + instead of abruptly closing. + 1.0.0 -------------------------------- From commits-noreply at bitbucket.org Wed Dec 2 11:02:35 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 2 Dec 2009 10:02:35 +0000 (UTC) Subject: [execnet-commit] execnet commit 43c47dc35331: introduce TimeoutError and receive(timeout) parameter Message-ID: <20091202100235.ED8D67EF11@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259747951 -3600 # Node ID 43c47dc35331d81c146c16422b3d25f7b1fa5ffc # Parent 2c58c2663ecdc212860bebbab8af6d500408692e introduce TimeoutError and receive(timeout) parameter --- a/ISSUES.txt +++ b/ISSUES.txt @@ -10,18 +10,6 @@ and any exception will kill the whole re This is especially bad if it happens remotely as it leaves the other side inaccesssible. -Control-C behaviour / best practises -------------------------------------------- -tags: 1.1 -bb: http://bitbucket.org/hpk42/py-trunk/issue/36 -path: execnet/gateway_base.py - -currently calling channel.receive() in the -main thread will prevent Control-C from passing. -Consider an internal timeout so that signals -get a chance to propagate. - - rsync links from posix to windows ----------------------------------------- tags: 1.1 feature --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -246,12 +246,16 @@ class RemoteError(EOFError): # XXX do this better sys.stderr.write("Warning: unhandled %r\n" % (self,)) +class TimeoutError(IOError): + """ Exception indicating that a timeout was reached. """ + NO_ENDMARKER_WANTED = object() class Channel(object): """Communication channel between two Python Interpreter execution points.""" RemoteError = RemoteError + TimeoutError = TimeoutError _executing = False def __init__(self, gateway, id): @@ -396,10 +400,12 @@ class Channel(object): code on the other side are reraised as local channel.RemoteErrors. EOFError is raised if the reading-connection was prematurely closed, which often indicates a dying process. + self.TimeoutError is raised after the specified number of seconds + (default is None, i.e. wait indefinitely). """ - self._receiveclosed.wait(timeout=timeout) # wait for non-"opened" state + self._receiveclosed.wait(timeout=timeout) # wait for non-"opened" state if not self._receiveclosed.isSet(): - raise IOError("Timeout") + raise self.TimeoutError("Timeout after %r seconds" % timeout) error = self._getremoteerror() if error: raise error @@ -419,19 +425,34 @@ class Channel(object): data = Message.CHANNEL_DATA(self.id, item) self.gateway._send(data) - def receive(self): - """receives an item that was sent from the other side, - possibly blocking if there is none. - Note that exceptions from the other side will be + def receive(self, timeout=-1): + """receive a data item that was sent from the other side. + timeout: -1 [default] blocked waiting, but wake up periodically + to let CTRL-C through. A positive number indicates the + number of seconds after which a channel.TimeoutError exception + will be raised if no item was received. + Note that exceptions from the remotely executing code will be reraised as channel.RemoteError exceptions containing a textual representation of the remote traceback. """ - queue = self._items - if queue is None: - raise IOError("calling receive() on channel with receiver callback") - x = queue.get() + itemqueue = self._items + if itemqueue is None: + raise IOError("cannot receive(), channel has receiver callback") + if timeout < 0: + internal_timeout = 1000 + else: + internal_timeout = timeout + + while 1: + try: + x = itemqueue.get(timeout=internal_timeout) + break + except queue.Empty: + if internal_timeout < 0: + continue + raise self.TimeoutError("no item after %r seconds" %(timeout)) if x is ENDMARKER: - queue.put(x) # for other receivers + itemqueue.put(x) # for other receivers raise self._getremoteerror() or EOFError() else: return x --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -134,6 +134,18 @@ class TestChannelBasicBehaviour: py.test.raises(EOFError, channel.receive) py.test.raises(EOFError, channel.receive) + def test_waitclose_timeouterror(self, gw): + channel = gw.remote_exec("channel.receive()") + py.test.raises(channel.TimeoutError, channel.waitclose, 0.02) + channel.send(1) + channel.waitclose(timeout=TESTTIMEOUT) + + def test_channel_receive_timeout(self, gw): + channel = gw.remote_exec('channel.send(channel.receive())') + py.test.raises(channel.TimeoutError, "channel.receive(timeout=0.2)") + channel.send(1) + x = channel.receive(timeout=0.1) + def test_channel_close_and_then_receive_error_multiple(self, gw): channel = gw.remote_exec('channel.send(42) ; raise ValueError') x = channel.receive() --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,10 @@ 1.0.1 (pending) it now internally sends an explicit termination message instead of abruptly closing. +- introduce a timeout parameter to channel.receive() + and default to periodically internally wake up + to let KeyboardInterrupts pass through. + 1.0.0 -------------------------------- From commits-noreply at bitbucket.org Thu Dec 3 03:11:57 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 02:11:57 +0000 (UTC) Subject: [execnet-commit] execnet commit 81dde6121815: refactor gateway termination and tests to allow for timely termination. Message-ID: <20091203021157.63A397EF4E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259806144 -3600 # Node ID 81dde61218151c0c0f7000b8ab19fb0289b4bdce # Parent 87be32c84857879b4016ddc78e0f5db3641f6cc9 refactor gateway termination and tests to allow for timely termination. It should now be more reliable and easy to properly terminate (a group of) gateways. Sorry, ronny - a somewhat larger commit but it'd be hard to split this up into smaller bits as termination is a rather pervasive issue. --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -5,7 +5,6 @@ import os, sys, time import py import execnet from execnet import gateway_base, gateway -from execnet.threadpool import WorkerPool queue = py.builtin._tryimport('queue', 'Queue') TESTTIMEOUT = 10.0 # seconds @@ -443,22 +442,6 @@ class TestChannelFile: gw._cache_rinfo = rinfo gw.remote_exec("import os ; os.chdir(%r)" % old).waitclose() -def test_join_blocked_slave_execution_gateway(): - gateway = execnet.makegateway('popen') - channel = gateway.remote_exec(""" - import time - time.sleep(10.0) - """) - def doit(): - gateway.exit() - gateway.join(timeout=3.0) - return 17 - - pool = WorkerPool() - reply = pool.dispatch(doit) - x = reply.get(timeout=5.0) - assert x == 17 - class TestPopenGateway: gwtype = 'popen' @@ -529,24 +512,6 @@ class TestPopenGateway: """) py.test.raises(EOFError, channel.receive) - at py.test.mark.skipif("not hasattr(os, 'kill')") - at py.test.mark.xfail("sys.platform.startswith('java')") -def test_endmarker_delivery_on_remote_killterm(): - gw = execnet.makegateway('popen') - q = queue.Queue() - channel = gw.remote_exec(source=''' - import os - os.kill(os.getpid(), 15) - ''') - channel.setcallback(q.put, endmarker=999) - val = q.get(TESTTIMEOUT) - assert val == 999 - # XXX jython does not get its io-read pipe closed - # when the remote side died?! - err = channel._getremoteerror() - assert isinstance(err, EOFError) - - def test_socket_gw_host_not_found(gw): py.test.raises(execnet.HostNotFound, 'execnet.makegateway("socket=qwepoipqwe:9000")' @@ -610,22 +575,6 @@ class TestThreads: gw.remote_init_threads(3) py.test.raises(IOError, gw.remote_init_threads, 3) -def test_close_initiating_remote_no_error(testdir): - import subprocess - dir = py.path.local(execnet.__file__).dirpath().dirpath() - p = testdir.makepyfile(""" - import sys - sys.path.insert(0, %r) - import execnet - gw = execnet.makegateway("popen") - gw.remote_init_threads(num=2) - ch = gw.remote_exec("channel.receive()") - ch.close() - """ % str(dir)) - popen = subprocess.Popen([sys.executable, str(p)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE,) - stdout, stderr = popen.communicate() - assert not stderr class TestTracing: def test_debug(self, monkeypatch): @@ -649,6 +598,7 @@ class TestTracing: py.test.fail("did not find %r in tracefile" %(slave_line,)) gw.exit() + @needs_osdup def test_popen_stderr_tracing(self, capfd, monkeypatch): monkeypatch.setenv('EXECNET_DEBUG', "2") gw = execnet.makegateway("popen") --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,9 @@ 1.0.1 (pending) also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) -- fix group.terminate() termination to work (more reliably) +- refine internal gateway exit/termination procedure + and introduce group.terminate(timeout) for timely + termination. - EOFError on channel.receive/waitclose if the other side unexpectedly went away. When a gateway exits --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -39,17 +39,24 @@ class Gateway(gateway_base.BaseGateway): self.__class__.__name__, addr, id, r, i) def exit(self): - """ trigger gateway exit. """ + """ trigger gateway exit. Defer waiting for finishing + of receiver-thread and subprocess activity to when + group.terminate() is called. + """ self._trace("gateway.exit() called") try: self._group._unregister(self) except KeyError: - return # we assume it's already happened - self._trace("--> stopping exec and sending GATEWAY_TERMINATE") - self._stopexec() - self._send(Message.GATEWAY_TERMINATE(0, '')) - self._io.close_write() - #self.join(timeout=timeout) # receiverthread receive close() messages + self._trace("gateway already unregistered with group") + return + self._trace("--> sending GATEWAY_TERMINATE") + try: + self._send(Message.GATEWAY_TERMINATE(0, '')) + self._io.close_write() + except IOError: + v = sys.exc_info()[1] + self._trace("io-error: could not send termination sequence") + self._trace(" exception: %r" % v) def _remote_bootstrap_gateway(self, io): """ send gateway bootstrap code to a remote Python interpreter @@ -137,11 +144,6 @@ class PopenCmdGateway(Gateway): io = Popen2IO(p.stdin, p.stdout) super(PopenCmdGateway, self).__init__(io=io) - def exit(self): - super(PopenCmdGateway, self).exit() - self._trace("polling Popen subprocess") - self._popen.poll() - popen_bootstrapline = "import sys ; exec(eval(sys.stdin.readline()))" class PopenGateway(PopenCmdGateway): """ This Gateway provides interaction with a newly started --- a/execnet/multi.py +++ b/execnet/multi.py @@ -4,7 +4,7 @@ Managing Gateway Groups and interactions (c) 2008-2009, Holger Krekel and others """ -import sys, weakref, atexit +import sys, weakref, atexit, time import execnet from execnet import XSpec from execnet import gateway @@ -20,6 +20,7 @@ class Group: self._activegateways = weakref.WeakKeyDictionary() self._id2gateway = weakref.WeakValueDictionary() self._autoidcounter = 1 + self._gateways_to_join = [] for xspec in xspecs: self.makegateway(xspec) atexit.register(self._cleanup_atexit) @@ -105,24 +106,33 @@ class Group: def _unregister(self, gateway): del self._id2gateway[gateway.id] del self._activegateways[gateway] + self._gateways_to_join.append(gateway) def _cleanup_atexit(self): trace("=== atexit cleanup %r ===" %(self,)) - self.terminate() + self.terminate(timeout=2.0) - def terminate(self): - """ trigger exit of all gateways. """ - gwlist = [] - while 1: - try: - gw, _ = self._activegateways.popitem() - except KeyError: - break - else: - gw.exit() - gwlist.append(gw) - #for gw in gwlist: - # gw.join(timeout=1.0) + def terminate(self, timeout=None): + """ trigger exit of member gateways and and wait for completion + of receiver-threads of previously exited member gateways and any + started subprocesses. If after timeout seconds (None for no-timeout) + waiting does not finish a TimeoutError will be raised. + """ + endtime = timeout and (time.time() + timeout) or None + for gw in self: + gw.exit() + def join_receiver_and_wait_for_subprocesses(): + for gw in self._gateways_to_join: + gw.join() + while self._gateways_to_join: + gw = self._gateways_to_join.pop(0) + if hasattr(gw, '_popen'): + gw._popen.wait() + self._gateways_to_join[:] = [] + from execnet.threadpool import WorkerPool + pool = WorkerPool(1) + reply = pool.dispatch(join_receiver_and_wait_for_subprocesses) + reply.get(timeout=timeout) def remote_exec(self, source): channels = [] --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -27,8 +27,8 @@ else: sysex = (KeyboardInterrupt, SystemExit) DEBUG = os.environ.get('EXECNET_DEBUG') +pid = os.getpid() if DEBUG == '2': - pid = os.getpid() def trace(msg): sys.stderr.write("[%s] %s\n" % (pid, msg)) sys.stderr.flush() @@ -38,12 +38,17 @@ elif DEBUG: debugfile = open(fn, 'w') def trace(msg): try: - debugfile.write(unicode(msg) + "\n") + debugfile.write(msg + "\n") debugfile.flush() except sysex: raise except: - sys.stderr.write("exception during tracing\n") + v = sys.exc_info()[1] + try: + sys.stderr.write( + "[%d] exception during tracing: %r\n" % (pid, v)) + except (IOError,ValueError): + pass # nothing we can do anymore else: def trace(msg): pass @@ -64,7 +69,6 @@ class SocketIO: except (AttributeError, socket.error): e = sys.exc_info()[1] sys.stderr.write("WARNING: cannot set socketoption") - self.readable = self.writeable = True def read(self, numbytes): "Read exactly 'bytes' bytes from the socket." @@ -81,19 +85,15 @@ class SocketIO: self.sock.sendall(data) def close_read(self): - if self.readable: - try: - self.sock.shutdown(0) - except socket.error: - pass - self.readable = None + try: + self.sock.shutdown(0) + except socket.error: + pass def close_write(self): - if self.writeable: - try: - self.sock.shutdown(1) - except socket.error: - pass - self.writeable = None + try: + self.sock.shutdown(1) + except socket.error: + pass class Popen2IO: error = (IOError, OSError, EOFError) @@ -105,7 +105,6 @@ class Popen2IO: import msvcrt msvcrt.setmode(infile.fileno(), os.O_BINARY) msvcrt.setmode(outfile.fileno(), os.O_BINARY) - self.readable = self.writeable = True def read(self, numbytes): """Read exactly 'numbytes' bytes from the pipe. """ @@ -127,16 +126,12 @@ class Popen2IO: self.outfile.flush() def close_read(self): - if self.readable: - self.infile.close() - self.readable = None + trace("popen-io.close_read()") + self.infile.close() def close_write(self): - try: - self.outfile.close() - except EnvironmentError: - pass - self.writeable = None + trace("popen-io.close_write()") + self.outfile.close() class Message: """ encapsulates Messages and their wire protocol. """ @@ -211,7 +206,8 @@ def _setupmessages(): class GATEWAY_TERMINATE(Message): def received(self, gateway): - gateway._io.close_read() + gateway._trace("putting None to execqueue") + gateway._execqueue.put(None) raise SystemExit(0) classes = [ @@ -334,7 +330,7 @@ class Channel(object): Msg = Message.CHANNEL_CLOSE try: self.gateway._send(Msg(self.id)) - except IOError: # ignore problems with sending + except (IOError, ValueError): # ignore problems with sending pass def _getremoteerror(self): @@ -645,6 +641,7 @@ class BaseGateway(object): finally: _receivelock.release() except sysex: + self._io.close_read() break except EOFError: self._error = sys.exc_info()[1] @@ -654,7 +651,6 @@ class BaseGateway(object): geterrortext(self.exc_info()), )) break finally: - self._stopexec() self._channelfactory._finished_receiving() if threading: # might be None during shutdown/finalization self._trace('leaving %r' % threading.currentThread()) @@ -664,9 +660,6 @@ class BaseGateway(object): msg.writeto(self._serializer) self._trace('sent -> %r' % msg) - def _stopexec(self): - pass - def _local_schedulexec(self, channel, sourcetask): channel.close("execution disallowed") @@ -689,9 +682,6 @@ class BaseGateway(object): "already finished") class SlaveGateway(BaseGateway): - def _stopexec(self): - self._execqueue.put(None) - def _local_schedulexec(self, channel, sourcetask): self._execqueue.put((channel, sourcetask)) @@ -702,13 +692,13 @@ class SlaveGateway(BaseGateway): while 1: item = self._execqueue.get() if item is None: - self._io.close_write() break try: self.executetask(item) except self._StopExecLoop: break finally: + self._io.close_write() self._trace("slavegateway.serve finished") if joining: self.join() --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -1,8 +1,9 @@ -Creation and termination of of grouped Gateways +Creating and working with grouped Gateways ------------------------------------------------------ -You can use ``execnet.Group`` to manage creation and -termination of Gateways. Example usage:: +Use ``execnet.Group`` to manage the lifetime of multiple +gateways and perform iteration and membership +checks:: >>> import execnet >>> group = execnet.Group() @@ -12,21 +13,25 @@ termination of Gateways. Example usage: >>> group - >>> list(group) # list all member gateways + >>> list(group) [, ] >>> len(group) 2 + >>> '1' in group and '2' in group + True + >>> group['1'] + + >>> group.terminate() # exit all member gateways >>> group -Acessing Gateways on a group by ID +Assigning Gateway IDs ------------------------------------------------------ All gateways are created as part of a group and receive a per-group unique ``id`` after successful initialization. -This identification string can be set via an ``id=...`` part -in an gateway URL as passed to ``group.makegateway``. Example:: +Pass an ``id=MYNAME`` part to ``group.makegateway``. Example:: >>> import execnet >>> group = execnet.Group() @@ -46,3 +51,22 @@ you actually use the ``execnet.default_g >>> gw = execnet.makegateway("popen") >>> gw in execnet.default_group True + +Timely termination +------------------------- + +Use ``group.terminate(timeout)`` if you want to terminate +member gateways and wait a limited time for proper termination +of all execution and IO activity:: + + >>> import execnet + >>> mygroup = execnet.Group() + >>> gw = mygroup.makegateway("popen") + >>> ch = gw.remote_exec("import time ; time.sleep(2.0)") + >>> try: + ... mygroup.terminate(timeout=1.0) + ... except IOError: + ... pass # subprocess-execution still sleeping + ... else: + ... print ("fail") + >>> --- /dev/null +++ b/testing/test_termination.py @@ -0,0 +1,111 @@ + +import sys, os +import execnet +import time +import subprocess +import py +from execnet.threadpool import WorkerPool +queue = py.builtin._tryimport('queue', 'Queue') +from testing.test_gateway import TESTTIMEOUT +execnetdir = py.path.local(execnet.__file__).dirpath().dirpath() + +def test_exit_blocked_slave_execution_gateway(anypython): + group = execnet.Group() + gateway = group.makegateway('popen//python=%s' % anypython) + channel = gateway.remote_exec(""" + import time + time.sleep(10.0) + """) + def doit(): + gateway.exit() + return 17 + + pool = WorkerPool() + reply = pool.dispatch(doit) + x = reply.get(timeout=5.0) + assert x == 17 + +def test_endmarker_delivery_on_remote_killterm(): + gw = execnet.makegateway('popen') + q = queue.Queue() + channel = gw.remote_exec(source=''' + import os, time + channel.send(os.getpid()) + time.sleep(100) + ''') + pid = channel.receive() + py.process.kill(pid) + channel.setcallback(q.put, endmarker=999) + val = q.get(TESTTIMEOUT) + assert val == 999 + err = channel._getremoteerror() + assert isinstance(err, EOFError) + +def test_termination_on_remote_channel_receive(monkeypatch): + if not py.path.local.sysfind('ps'): + py.test.skip("need 'ps' command to externally check process status") + monkeypatch.setenv('EXECNET_DEBUG', '2') + group = execnet.Group() + gw = group.makegateway("popen") + pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() + gw.remote_exec("channel.receive()") + group.terminate() + command = ["ps", "-p", str(pid)] + popen = subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + out, err = popen.communicate() + out = py.builtin._totext(out, 'utf8') + assert str(pid) not in out, out + +def test_close_initiating_remote_no_error(testdir, anypython): + p = testdir.makepyfile(""" + import sys + sys.path.insert(0, %r) + import execnet + gw = execnet.makegateway("popen") + gw.remote_init_threads(num=3) + ch1 = gw.remote_exec("channel.receive()") + ch2 = gw.remote_exec("channel.receive()") + ch3 = gw.remote_exec("channel.receive()") + execnet.default_group.terminate() + """ % str(execnetdir)) + popen = subprocess.Popen([str(anypython), str(p)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE,) + stdout, stderr = popen.communicate() + assert not stderr + +def test_double_call_to_terminate(testdir, anypython): + triggerfile = testdir.tmpdir.join("trigger") + p = testdir.makepyfile(""" + import sys + sys.path.insert(0, %r) + triggerfile = %r + import execnet + gw = execnet.makegateway("popen") + gw.remote_exec(''' + import os, time + while 1: + if os.path.exists(%%r): + break + time.sleep(0.2) + ''' %% triggerfile) + ok = 0 + try: + execnet.default_group.terminate(1.0) + except IOError: + try: + execnet.default_group.terminate(0.1) + except IOError: + f = open(triggerfile, 'w') + f.write("") + f.close() + execnet.default_group.terminate(5.0) + ok = 1 + if not ok: + sys.stderr.write("no-timeout!\\n") + """ % (str(execnetdir), str(triggerfile))) + popen = subprocess.Popen([str(anypython), str(p)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE,) + stdout, stderr = popen.communicate() + assert not stderr + --- a/testing/conftest.py +++ b/testing/conftest.py @@ -39,12 +39,24 @@ def getsocketspec(config=None): def pytest_generate_tests(metafunc): if 'gw' in metafunc.funcargnames: + assert 'anypython' not in metafunc.funcargnames, "need combine?" if hasattr(metafunc.cls, 'gwtype'): gwtypes = [metafunc.cls.gwtype] else: gwtypes = ['popen', 'socket', 'ssh'] for gwtype in gwtypes: metafunc.addcall(id=gwtype, param=gwtype) + elif 'anypython' in metafunc.funcargnames: + for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', + 'pypy-c', 'jython'): + metafunc.addcall(id=name, param=name) + +def pytest_funcarg__anypython(request): + name = request.param + executable = py.path.local.sysfind(name) + if executable is None: + py.test.skip("no %s found" % (name,)) + return executable def pytest_funcarg__gw(request): scope = "session" @@ -81,3 +93,4 @@ def setup_ssh_gateway(request): sshhost = request.getfuncargvalue('specssh').ssh gw = execnet.makegateway("ssh=%s" %(sshhost,)) return gw + --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -63,17 +63,25 @@ class TestGroup: group = Group() assert atexitlist == [group._cleanup_atexit] exitlist = [] + joinlist = [] class PseudoGW: def exit(self): exitlist.append(self) + group._unregister(self) + def join(self): + joinlist.append(self) gw = PseudoGW() group._register(gw) assert len(exitlist) == 0 + assert len(joinlist) == 0 group._cleanup_atexit() assert len(exitlist) == 1 assert exitlist == [gw] + assert len(joinlist) == 1 + assert joinlist == [gw] group._cleanup_atexit() assert len(exitlist) == 1 + assert len(joinlist) == 1 def test_group_PopenGateway(self): group = Group() --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -50,19 +50,6 @@ def read_write_loop(): except (IOError, EOFError): break -def pytest_generate_tests(metafunc): - if 'anypython' in metafunc.funcargnames: - for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', - 'pypy-c', 'jython'): - metafunc.addcall(id=name, param=name) - -def pytest_funcarg__anypython(request): - name = request.param - executable = py.path.local.sysfind(name) - if executable is None: - py.test.skip("no %s found" % (name,)) - return executable - def test_io_message(anypython, tmpdir): check = tmpdir.join("check.py") check.write(py.code.Source(gateway_base, """ --- a/doc/basics.txt +++ b/doc/basics.txt @@ -95,6 +95,26 @@ two asynchronously running programs. .. automethod:: Channel.waitclose(timeout) .. autoattribute:: Channel.RemoteError +.. _Group: + +Grouping Gateways and guaranteed termination +=============================================== + +.. _`group examples`: examples.html#group +.. currentmodule:: execnet.multi + +All created gateway instances are part of a group. Group objects +are container-like objects (see `group examples`_) and manage +the final termination procedure: + +.. automethod:: Group.terminate(timeout=None) + +This method is implicitely called for each gateway group at +process-exit, using a small timeout. This is fine +for interactive sessions or random scripts which +you rather like to error out than hang. If you start many +processes then you often want to call ``group.terminate()`` +yourself and specify a larger or not timeout. remote_status: get low-level execution info =================================================== @@ -108,23 +128,10 @@ information from the remote side. Calling this method tells you e.g. how many execution tasks are queued, how many are executing and how many -channels are active. - - -.. _Group: - -Grouping Gateways -============================ - -All created gateway instances are part of a group. Gateways made -through the global ``execnet.makegateway()`` will become a -member of the ``default_group``:: - -A Group instance can terminate its gateways explicitely -or at process termination (each group registers with Python's -``atexit`` mechanism). See the `group examples`_ for details. - -.. _`group examples`: examples#group +channels are active. Note that remote_status() +works even if the other side's execution thread +is blocked and can thus be used to query status +in a non-blocking manner. rsync: synchronise filesystem with remote =============================================================== From commits-noreply at bitbucket.org Thu Dec 3 03:11:55 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 02:11:55 +0000 (UTC) Subject: [execnet-commit] execnet commit 57c400924a5a: refine EXECNET_DEBUG tracing Message-ID: <20091203021155.904067EF28@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259753970 -3600 # Node ID 57c400924a5a4ebf5bc6b126b0e8829c279b5de5 # Parent 43c47dc35331d81c146c16422b3d25f7b1fa5ffc refine EXECNET_DEBUG tracing --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -626,13 +626,37 @@ def test_close_initiating_remote_no_erro stdout=subprocess.PIPE, stderr=subprocess.PIPE,) stdout, stderr = popen.communicate() assert not stderr - -def test_debug(monkeypatch): - monkeypatch.setenv('EXECNET_DEBUG', "1") - source = py.std.inspect.getsource(gateway_base) - d = {} - gateway_base.do_exec(source, d) - assert 'debugfile' in d + +class TestTracing: + def test_debug(self, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "1") + source = py.std.inspect.getsource(gateway_base) + d = {} + gateway_base.do_exec(source, d) + assert 'debugfile' in d + + def test_popen_filetracing(self, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "1") + gw = execnet.makegateway("popen") + pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() + tmpdir = py.path.local(py.std.tempfile.gettempdir()) + slavefile = tmpdir.join("execnet-debug-%s" % pid) + slave_line = "creating slavegateway" + for line in slavefile.readlines(): + if slave_line in line: + break + else: + py.test.fail("did not find %r in tracefile" %(slave_line,)) + gw.exit() + + def test_popen_stderr_tracing(self, capfd, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "2") + gw = execnet.makegateway("popen") + pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() + out, err = capfd.readouterr() + slave_line = "[%s] creating slavegateway" % pid + assert slave_line in err + gw.exit() def test_nodebug(): from execnet import gateway_base --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -26,7 +26,13 @@ else: sysex = (KeyboardInterrupt, SystemExit) -if os.environ.get('EXECNET_DEBUG'): +DEBUG = os.environ.get('EXECNET_DEBUG') +if DEBUG == '2': + pid = os.getpid() + def trace(msg): + sys.stderr.write("[%s] %s\n" % (pid, msg)) + sys.stderr.flush() +elif DEBUG: import tempfile, os.path fn = os.path.join(tempfile.gettempdir(), 'execnet-debug-%d' % os.getpid()) debugfile = open(fn, 'w') --- a/doc/basics.txt +++ b/doc/basics.txt @@ -150,7 +150,9 @@ And here is API info about the RSync cla Debugging execnet =============================================================== -If you set the enviornment variable ``EXECNET_DEBUG`` to any value -trace-files will be written to ``execnet-debug-PID`` files in -the system temporary directory. ``NUM`` will be the process id. +By setting the environment variable ``EXECNET_DEBUG`` you can +configure a tracing mechanism: +:EXECNET_DEBUG=1: write per-process trace-files to ``execnet-debug-PID`` +:EXECNET_DEUBG=2: perform tracing to stderr (popen-gateway slaves will send this to their instantiator) + --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,11 @@ 1.0.1 (pending) and default to periodically internally wake up to let KeyboardInterrupts pass through. +- EXECNET_DEBUG=2 will cause tracing to go to stderr, + which with popen slave gateways will relay back + tracing to the instantiator process. + + 1.0.0 -------------------------------- From commits-noreply at bitbucket.org Thu Dec 3 03:11:57 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 02:11:57 +0000 (UTC) Subject: [execnet-commit] execnet commit 87be32c84857: assume python2.3 and use queue.get(timeout) instead of hacking our own. Message-ID: <20091203021157.4D0047EF2E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259799191 -3600 # Node ID 87be32c84857879b4016ddc78e0f5db3641f6cc9 # Parent 57c400924a5a4ebf5bc6b126b0e8829c279b5de5 assume python2.3 and use queue.get(timeout) instead of hacking our own. --- a/execnet/threadpool.py +++ b/execnet/threadpool.py @@ -36,21 +36,6 @@ class Reply(object): self._excinfo = excinfo self._queue.put(ERRORMARKER) - def _get_with_timeout(self, timeout): - # taken from python2.3's Queue.get() - # we want to run on python2.2 here - delay = 0.0005 # 500 us -> initial delay of 1 ms - endtime = time.time() + timeout - while 1: - try: - return self._queue.get_nowait() - except queue.Empty: - remaining = endtime - time.time() - if remaining <= 0: #time is over and no element arrived - raise IOError("timeout waiting for task %r" %(self.task,)) - delay = min(delay * 2, remaining, .05) - time.sleep(delay) #reduce CPU usage by using a sleep - def get(self, timeout=None): """ get the result object from an asynchronous function execution. if the function execution raised an exception, @@ -59,10 +44,10 @@ class Reply(object): """ if self._queue is None: raise EOFError("reply has already been delivered") - if timeout is not None: - result = self._get_with_timeout(timeout) - else: - result = self._queue.get() + try: + result = self._queue.get(timeout=timeout) + except queue.Empty: + raise IOError("timeout waiting for %r" %(self.task, )) if result is ERRORMARKER: self._queue = None excinfo = self._excinfo --- a/testing/test_threadpool.py +++ b/testing/test_threadpool.py @@ -74,6 +74,7 @@ def test_join_timeout(): reply.get(timeout=1.0) pool.join(timeout=0.1) + at py.test.mark.skipif("not hasattr(os, 'dup')") def test_pool_clean_shutdown(): capture = py.io.StdCaptureFD() pool = WorkerPool() From commits-noreply at bitbucket.org Thu Dec 3 17:15:37 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 16:15:37 +0000 (UTC) Subject: [execnet-commit] execnet commit 26f11eb2bb99: avoid actually trying to instantiate gateways for deprecation tests Message-ID: <20091203161537.909D27EF35@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259834868 -3600 # Node ID 26f11eb2bb99bd1ba6e4b4bfe632333aebb148b1 # Parent 81dde61218151c0c0f7000b8ab19fb0289b4bdce avoid actually trying to instantiate gateways for deprecation tests --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -16,11 +16,13 @@ def test_serialize_error(gw): excinfo = py.test.raises(ch.RemoteError, ch.receive) assert "can't serialize" in str(excinfo.value) -def test_deprecation(recwarn): +def test_deprecation(recwarn, monkeypatch): execnet.PopenGateway() assert recwarn.pop(DeprecationWarning) - py.test.raises(Exception, 'execnet.SocketGateway("localhost", 8888)') + monkeypatch.setattr(py.std.socket, 'socket', lambda *args: 0/0) + py.test.raises(Exception, 'execnet.SocketGateway("localhost", 8811)') assert recwarn.pop(DeprecationWarning) + monkeypatch.setattr(py.std.subprocess, 'Popen', lambda *args,**kwargs: 0/0) py.test.raises(Exception, 'execnet.SshGateway("not-existing")') assert recwarn.pop(DeprecationWarning) From commits-noreply at bitbucket.org Thu Dec 3 17:15:39 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 16:15:39 +0000 (UTC) Subject: [execnet-commit] execnet commit f429a449f16c: fix socket gateway creation Message-ID: <20091203161539.5C1EB7EF37@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259855712 -3600 # Node ID f429a449f16cf5e3a9b43f2fd181062ecb2c0e31 # Parent a29ab6d3ac2e74296a96859faffb335913313790 fix socket gateway creation --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -226,7 +226,7 @@ class SocketGateway(Gateway): # "port=%r, hostname = %r" %(realport, hostname)) if not realhost or realhost=="0.0.0.0": realhost = "localhost" - return gateway._group.makegateway("socket=%s:%s" %(realhost, realport)) + return cls(realhost, realport) new_remote = classmethod(new_remote) class HostNotFound(Exception): --- a/execnet/multi.py +++ b/execnet/multi.py @@ -66,7 +66,7 @@ class Group: gateway_id = spec.installvia if gateway_id: viagw = self._id2gateway[gateway_id] - return gateway.SocketGateway.new_remote(viagw) + gw = gateway.SocketGateway.new_remote(viagw) else: hostport = spec.socket.split(":") gw = gateway.SocketGateway(*hostport) --- a/testing/test_xspec.py +++ b/testing/test_xspec.py @@ -129,8 +129,9 @@ class TestMakegateway: def test_ssh(self, specssh): sshhost = specssh.ssh - gw = execnet.makegateway("ssh=%s" % sshhost) + gw = execnet.makegateway("ssh=%s//id=ssh1" % sshhost) rinfo = gw._rinfo() + assert gw.id == 'ssh1' gw2 = execnet.SshGateway(sshhost) rinfo2 = gw2._rinfo() assert rinfo.executable == rinfo2.executable @@ -138,11 +139,12 @@ class TestMakegateway: assert rinfo.version_info == rinfo2.version_info def test_socket(self, specsocket): - gw = execnet.makegateway("socket=%s" % specsocket.socket) + gw = execnet.makegateway("socket=%s//id=sock1" % specsocket.socket) rinfo = gw._rinfo() assert rinfo.executable assert rinfo.cwd assert rinfo.version_info + assert gw.id == "sock1" # we cannot instantiate a second gateway #gw2 = execnet.SocketGateway(*specsocket.socket.split(":")) @@ -150,3 +152,11 @@ class TestMakegateway: #assert rinfo.executable == rinfo2.executable #assert rinfo.cwd == rinfo2.cwd #assert rinfo.version_info == rinfo2.version_info + + def test_socket_installvia(self): + group = execnet.Group() + group.makegateway("popen//id=p1") + gw = group.makegateway("socket//installvia=p1//id=s1") + assert gw.id == "s1" + assert gw.remote_status() + group.terminate() From commits-noreply at bitbucket.org Thu Dec 3 17:15:39 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 16:15:39 +0000 (UTC) Subject: [execnet-commit] execnet commit a29ab6d3ac2e: add remote_status/receiving issue Message-ID: <20091203161539.4E5277EF36@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259852891 -3600 # Node ID a29ab6d3ac2e74296a96859faffb335913313790 # Parent 26f11eb2bb99bd1ba6e4b4bfe632333aebb148b1 add remote_status/receiving issue --- a/ISSUES.txt +++ b/ISSUES.txt @@ -46,3 +46,11 @@ This way a gateway can be instantiated t intermediating gateway which bi-directionallry forwads Messages through a existing channel. +cleanup remote_status / receive status +---------------------------------------- +tags: 1.0.1 feature + +remote_status() will only return a result object +if the other side could receive so the "receiving" +bit is redundant. Rather a gateway needs a +gateway.isreceiving() method. From commits-noreply at bitbucket.org Thu Dec 3 17:15:41 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 16:15:41 +0000 (UTC) Subject: [execnet-commit] execnet commit 7681be82d2ff: refactor and re-group tests and test gateway creation Message-ID: <20091203161541.9E7187EF39@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259856463 -3600 # Node ID 7681be82d2ffb3f8de8253f9ac1278fe89be99d8 # Parent f429a449f16cf5e3a9b43f2fd181062ecb2c0e31 refactor and re-group tests and test gateway creation --- a/testing/conftest.py +++ b/testing/conftest.py @@ -40,7 +40,9 @@ def getsocketspec(config=None): def pytest_generate_tests(metafunc): if 'gw' in metafunc.funcargnames: assert 'anypython' not in metafunc.funcargnames, "need combine?" - if hasattr(metafunc.cls, 'gwtype'): + if hasattr(metafunc.function, 'gwtypes'): + gwtypes = metafunc.function.gwtypes + elif hasattr(metafunc.cls, 'gwtype'): gwtypes = [metafunc.cls.gwtype] else: gwtypes = ['popen', 'socket', 'ssh'] @@ -60,37 +62,27 @@ def pytest_funcarg__anypython(request): def pytest_funcarg__gw(request): scope = "session" - if request.param == "popen": - return request.cached_setup( - setup=lambda: execnet.makegateway("popen"), - teardown=lambda gw: gw.exit(), - extrakey=request.param, - scope=scope) - elif request.param == "socket": - return request.cached_setup( - setup=setup_socket_gateway, - teardown=teardown_socket_gateway, - extrakey=request.param, - scope=scope) - elif request.param == "ssh": - return request.cached_setup( - setup=lambda: setup_ssh_gateway(request), - teardown=lambda gw: gw.exit(), - extrakey=request.param, - scope=scope) - -def setup_socket_gateway(): - proxygw = execnet.makegateway("popen") - gw = execnet.makegateway("socket//installvia=%s" % proxygw.id) - gw.proxygw = proxygw - return gw - -def teardown_socket_gateway(gw): - gw.exit() - gw.proxygw.exit() - -def setup_ssh_gateway(request): - sshhost = request.getfuncargvalue('specssh').ssh - gw = execnet.makegateway("ssh=%s" %(sshhost,)) - return gw - + group = request.cached_setup( + setup=execnet.Group, + teardown=lambda group: group.terminate(timeout=1), + extrakey="testgroup", + scope=scope, + ) + try: + return group[request.param] + except KeyError: + if request.param == "popen": + gw = group.makegateway("popen//id=popen") + elif request.param == "socket": + pname = 'sproxy1' + if pname not in group: + proxygw = group.makegateway("popen//id=%s" % pname) + #assert group['proxygw'].remote_status().receiving + gw = group.makegateway("socket//id=socket//installvia=%s" % pname) + gw.proxygw = proxygw + assert pname in group + + elif request.param == "ssh": + sshhost = request.getfuncargvalue('specssh').ssh + gw = group.makegateway("ssh=%s//id=ssh" %(sshhost,)) + return gw --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -5,17 +5,10 @@ import os, sys, time import py import execnet from execnet import gateway_base, gateway -queue = py.builtin._tryimport('queue', 'Queue') TESTTIMEOUT = 10.0 # seconds -needs_early_gc = py.test.mark.skipif("not hasattr(sys, 'getrefcount')") needs_osdup = py.test.mark.skipif("not hasattr(os, 'dup')") -def test_serialize_error(gw): - ch = gw.remote_exec("channel.send(ValueError(42))") - excinfo = py.test.raises(ch.RemoteError, ch.receive) - assert "can't serialize" in str(excinfo.value) - def test_deprecation(recwarn, monkeypatch): execnet.PopenGateway() assert recwarn.pop(DeprecationWarning) @@ -26,9 +19,12 @@ def test_deprecation(recwarn, monkeypatc py.test.raises(Exception, 'execnet.SshGateway("not-existing")') assert recwarn.pop(DeprecationWarning) -class TestBasicRemoteExecution: +class TestBasicGateway: def test_correct_setup(self, gw): assert gw._receiverthread.isAlive() + assert gw in gw._group + assert gw.id in gw._group + assert gw.spec def test_repr_doesnt_crash(self, gw): assert isinstance(repr(gw), str) @@ -122,277 +118,6 @@ class TestBasicRemoteExecution: result = channel.receive() assert result == 42 -class TestChannelBasicBehaviour: - def test_channel_close_and_then_receive_error(self, gw): - channel = gw.remote_exec('raise ValueError') - py.test.raises(channel.RemoteError, channel.receive) - - def test_channel_finish_and_then_EOFError(self, gw): - channel = gw.remote_exec('channel.send(42)') - x = channel.receive() - assert x == 42 - py.test.raises(EOFError, channel.receive) - py.test.raises(EOFError, channel.receive) - py.test.raises(EOFError, channel.receive) - - def test_waitclose_timeouterror(self, gw): - channel = gw.remote_exec("channel.receive()") - py.test.raises(channel.TimeoutError, channel.waitclose, 0.02) - channel.send(1) - channel.waitclose(timeout=TESTTIMEOUT) - - def test_channel_receive_timeout(self, gw): - channel = gw.remote_exec('channel.send(channel.receive())') - py.test.raises(channel.TimeoutError, "channel.receive(timeout=0.2)") - channel.send(1) - x = channel.receive(timeout=0.1) - - def test_channel_close_and_then_receive_error_multiple(self, gw): - channel = gw.remote_exec('channel.send(42) ; raise ValueError') - x = channel.receive() - assert x == 42 - py.test.raises(channel.RemoteError, channel.receive) - - def test_channel__local_close(self, gw): - channel = gw._channelfactory.new() - gw._channelfactory._local_close(channel.id) - channel.waitclose(0.1) - - def test_channel__local_close_error(self, gw): - channel = gw._channelfactory.new() - gw._channelfactory._local_close(channel.id, - channel.RemoteError("error")) - py.test.raises(channel.RemoteError, channel.waitclose, 0.01) - - def test_channel_error_reporting(self, gw): - channel = gw.remote_exec('def foo():\n return foobar()\nfoo()\n') - try: - channel.receive() - except channel.RemoteError: - e = sys.exc_info()[1] - assert str(e).startswith('Traceback (most recent call last):') - assert str(e).find('NameError: global name \'foobar\' ' - 'is not defined') > -1 - else: - py.test.fail('No exception raised') - - def test_channel_syntax_error(self, gw): - # missing colon - channel = gw.remote_exec('def foo()\n return 1\nfoo()\n') - try: - channel.receive() - except channel.RemoteError: - e = sys.exc_info()[1] - assert str(e).startswith('Traceback (most recent call last):') - assert str(e).find('SyntaxError') > -1 - - def test_channel_iter(self, gw): - channel = gw.remote_exec(""" - for x in range(3): - channel.send(x) - """) - l = list(channel) - assert l == [0, 1, 2] - - def test_channel_passing_over_channel(self, gw): - channel = gw.remote_exec(''' - c = channel.gateway.newchannel() - channel.send(c) - c.send(42) - ''') - c = channel.receive() - x = c.receive() - assert x == 42 - - # check that the both sides previous channels are really gone - channel.waitclose(TESTTIMEOUT) - #assert c.id not in gw._channelfactory - newchan = gw.remote_exec(''' - assert %d not in channel.gateway._channelfactory._channels - ''' % (channel.id)) - newchan.waitclose(TESTTIMEOUT) - assert channel.id not in gw._channelfactory._channels - - def test_channel_receiver_callback(self, gw): - l = [] - #channel = gw.newchannel(receiver=l.append) - channel = gw.remote_exec(source=''' - channel.send(42) - channel.send(13) - channel.send(channel.gateway.newchannel()) - ''') - channel.setcallback(callback=l.append) - py.test.raises(IOError, channel.receive) - channel.waitclose(TESTTIMEOUT) - assert len(l) == 3 - assert l[:2] == [42,13] - assert isinstance(l[2], channel.__class__) - - def test_channel_callback_after_receive(self, gw): - l = [] - channel = gw.remote_exec(source=''' - channel.send(42) - channel.send(13) - channel.send(channel.gateway.newchannel()) - ''') - x = channel.receive() - assert x == 42 - channel.setcallback(callback=l.append) - py.test.raises(IOError, channel.receive) - channel.waitclose(TESTTIMEOUT) - assert len(l) == 2 - assert l[0] == 13 - assert isinstance(l[1], channel.__class__) - - def test_waiting_for_callbacks(self, gw): - l = [] - def callback(msg): - import time; time.sleep(0.2) - l.append(msg) - channel = gw.remote_exec(source=''' - channel.send(42) - ''') - channel.setcallback(callback) - channel.waitclose(TESTTIMEOUT) - assert l == [42] - - def test_channel_callback_stays_active(self, gw): - self.check_channel_callback_stays_active(gw, earlyfree=True) - - def check_channel_callback_stays_active(self, gw, earlyfree=True): - # with 'earlyfree==True', this tests the "sendonly" channel state. - l = [] - channel = gw.remote_exec(source=''' - try: - import thread - except ImportError: - import _thread as thread - import time - def producer(subchannel): - for i in range(5): - time.sleep(0.15) - subchannel.send(i*100) - channel2 = channel.receive() - thread.start_new_thread(producer, (channel2,)) - del channel2 - ''') - subchannel = gw.newchannel() - subchannel.setcallback(l.append) - channel.send(subchannel) - if earlyfree: - subchannel = None - counter = 100 - while len(l) < 5: - if subchannel and subchannel.isclosed(): - break - counter -= 1 - print(counter) - if not counter: - py.test.fail("timed out waiting for the answer[%d]" % len(l)) - time.sleep(0.04) # busy-wait - assert l == [0, 100, 200, 300, 400] - return subchannel - - @needs_early_gc - def test_channel_callback_remote_freed(self, gw): - channel = self.check_channel_callback_stays_active(gw, earlyfree=False) - # freed automatically at the end of producer() - channel.waitclose(TESTTIMEOUT) - - def test_channel_endmarker_callback(self, gw): - l = [] - channel = gw.remote_exec(source=''' - channel.send(42) - channel.send(13) - channel.send(channel.gateway.newchannel()) - ''') - channel.setcallback(l.append, 999) - py.test.raises(IOError, channel.receive) - channel.waitclose(TESTTIMEOUT) - assert len(l) == 4 - assert l[:2] == [42,13] - assert isinstance(l[2], channel.__class__) - assert l[3] == 999 - - def test_channel_endmarker_callback_error(self, gw): - q = queue.Queue() - channel = gw.remote_exec(source=''' - raise ValueError() - ''') - channel.setcallback(q.put, endmarker=999) - val = q.get(TESTTIMEOUT) - assert val == 999 - err = channel._getremoteerror() - assert err - assert str(err).find("ValueError") != -1 - -class TestChannelFile: - def test_channel_file_write(self, gw): - channel = gw.remote_exec(""" - f = channel.makefile() - f.write("hello world\\n") - f.close() - channel.send(42) - """) - first = channel.receive() - assert first.strip() == 'hello world' - second = channel.receive() - assert second == 42 - - def test_channel_file_write_error(self, gw): - channel = gw.remote_exec("pass") - f = channel.makefile() - channel.waitclose(TESTTIMEOUT) - py.test.raises(IOError, f.write, 'hello') - - def test_channel_file_proxyclose(self, gw): - channel = gw.remote_exec(""" - f = channel.makefile(proxyclose=True) - f.write("hello world") - f.close() - channel.send(42) - """) - first = channel.receive() - assert first.strip() == 'hello world' - py.test.raises(EOFError, channel.receive) - - def test_channel_file_read(self, gw): - channel = gw.remote_exec(""" - f = channel.makefile(mode='r') - s = f.read(2) - channel.send(s) - s = f.read(5) - channel.send(s) - """) - channel.send("xyabcde") - s1 = channel.receive() - s2 = channel.receive() - assert s1 == "xy" - assert s2 == "abcde" - - def test_channel_file_read_empty(self, gw): - channel = gw.remote_exec("pass") - f = channel.makefile(mode="r") - s = f.read(3) - assert s == "" - s = f.read(5) - assert s == "" - - def test_channel_file_readline_remote(self, gw): - channel = gw.remote_exec(""" - channel.send('123\\n45') - """) - channel.waitclose(TESTTIMEOUT) - f = channel.makefile(mode="r") - s = f.readline() - assert s == "123\n" - s = f.readline() - assert s == "45" - - def test_channel_makefile_incompatmode(self, gw): - channel = gw.newchannel() - py.test.raises(ValueError, 'channel.makefile("rw")') - @needs_osdup def test_confusion_from_os_write_stdout(self, gw): channel = gw.remote_exec(""" --- /dev/null +++ b/testing/test_channel.py @@ -0,0 +1,461 @@ +""" +mostly functional tests of gateways. +""" +import os, sys, time +import py +import execnet +from execnet import gateway_base, gateway +needs_early_gc = py.test.mark.skipif("not hasattr(sys, 'getrefcount')") +needs_osdup = py.test.mark.skipif("not hasattr(os, 'dup')") +queue = py.builtin._tryimport('queue', 'Queue') + +TESTTIMEOUT = 10.0 # seconds + +class TestChannelBasicBehaviour: + def test_serialize_error(self, gw): + ch = gw.remote_exec("channel.send(ValueError(42))") + excinfo = py.test.raises(ch.RemoteError, ch.receive) + assert "can't serialize" in str(excinfo.value) + + def test_channel_close_and_then_receive_error(self, gw): + channel = gw.remote_exec('raise ValueError') + py.test.raises(channel.RemoteError, channel.receive) + + def test_channel_finish_and_then_EOFError(self, gw): + channel = gw.remote_exec('channel.send(42)') + x = channel.receive() + assert x == 42 + py.test.raises(EOFError, channel.receive) + py.test.raises(EOFError, channel.receive) + py.test.raises(EOFError, channel.receive) + + def test_waitclose_timeouterror(self, gw): + channel = gw.remote_exec("channel.receive()") + py.test.raises(channel.TimeoutError, channel.waitclose, 0.02) + channel.send(1) + channel.waitclose(timeout=TESTTIMEOUT) + + def test_channel_receive_timeout(self, gw): + channel = gw.remote_exec('channel.send(channel.receive())') + py.test.raises(channel.TimeoutError, "channel.receive(timeout=0.2)") + channel.send(1) + x = channel.receive(timeout=0.1) + + def test_channel_close_and_then_receive_error_multiple(self, gw): + channel = gw.remote_exec('channel.send(42) ; raise ValueError') + x = channel.receive() + assert x == 42 + py.test.raises(channel.RemoteError, channel.receive) + + def test_channel__local_close(self, gw): + channel = gw._channelfactory.new() + gw._channelfactory._local_close(channel.id) + channel.waitclose(0.1) + + def test_channel__local_close_error(self, gw): + channel = gw._channelfactory.new() + gw._channelfactory._local_close(channel.id, + channel.RemoteError("error")) + py.test.raises(channel.RemoteError, channel.waitclose, 0.01) + + def test_channel_error_reporting(self, gw): + channel = gw.remote_exec('def foo():\n return foobar()\nfoo()\n') + try: + channel.receive() + except channel.RemoteError: + e = sys.exc_info()[1] + assert str(e).startswith('Traceback (most recent call last):') + assert str(e).find('NameError: global name \'foobar\' ' + 'is not defined') > -1 + else: + py.test.fail('No exception raised') + + def test_channel_syntax_error(self, gw): + # missing colon + channel = gw.remote_exec('def foo()\n return 1\nfoo()\n') + try: + channel.receive() + except channel.RemoteError: + e = sys.exc_info()[1] + assert str(e).startswith('Traceback (most recent call last):') + assert str(e).find('SyntaxError') > -1 + + def test_channel_iter(self, gw): + channel = gw.remote_exec(""" + for x in range(3): + channel.send(x) + """) + l = list(channel) + assert l == [0, 1, 2] + + def test_channel_passing_over_channel(self, gw): + channel = gw.remote_exec(''' + c = channel.gateway.newchannel() + channel.send(c) + c.send(42) + ''') + c = channel.receive() + x = c.receive() + assert x == 42 + + # check that the both sides previous channels are really gone + channel.waitclose(TESTTIMEOUT) + #assert c.id not in gw._channelfactory + newchan = gw.remote_exec(''' + assert %d not in channel.gateway._channelfactory._channels + ''' % (channel.id)) + newchan.waitclose(TESTTIMEOUT) + assert channel.id not in gw._channelfactory._channels + + def test_channel_receiver_callback(self, gw): + l = [] + #channel = gw.newchannel(receiver=l.append) + channel = gw.remote_exec(source=''' + channel.send(42) + channel.send(13) + channel.send(channel.gateway.newchannel()) + ''') + channel.setcallback(callback=l.append) + py.test.raises(IOError, channel.receive) + channel.waitclose(TESTTIMEOUT) + assert len(l) == 3 + assert l[:2] == [42,13] + assert isinstance(l[2], channel.__class__) + + def test_channel_callback_after_receive(self, gw): + l = [] + channel = gw.remote_exec(source=''' + channel.send(42) + channel.send(13) + channel.send(channel.gateway.newchannel()) + ''') + x = channel.receive() + assert x == 42 + channel.setcallback(callback=l.append) + py.test.raises(IOError, channel.receive) + channel.waitclose(TESTTIMEOUT) + assert len(l) == 2 + assert l[0] == 13 + assert isinstance(l[1], channel.__class__) + + def test_waiting_for_callbacks(self, gw): + l = [] + def callback(msg): + import time; time.sleep(0.2) + l.append(msg) + channel = gw.remote_exec(source=''' + channel.send(42) + ''') + channel.setcallback(callback) + channel.waitclose(TESTTIMEOUT) + assert l == [42] + + def test_channel_callback_stays_active(self, gw): + self.check_channel_callback_stays_active(gw, earlyfree=True) + + def check_channel_callback_stays_active(self, gw, earlyfree=True): + # with 'earlyfree==True', this tests the "sendonly" channel state. + l = [] + channel = gw.remote_exec(source=''' + try: + import thread + except ImportError: + import _thread as thread + import time + def producer(subchannel): + for i in range(5): + time.sleep(0.15) + subchannel.send(i*100) + channel2 = channel.receive() + thread.start_new_thread(producer, (channel2,)) + del channel2 + ''') + subchannel = gw.newchannel() + subchannel.setcallback(l.append) + channel.send(subchannel) + if earlyfree: + subchannel = None + counter = 100 + while len(l) < 5: + if subchannel and subchannel.isclosed(): + break + counter -= 1 + print(counter) + if not counter: + py.test.fail("timed out waiting for the answer[%d]" % len(l)) + time.sleep(0.04) # busy-wait + assert l == [0, 100, 200, 300, 400] + return subchannel + + @needs_early_gc + def test_channel_callback_remote_freed(self, gw): + channel = self.check_channel_callback_stays_active(gw, earlyfree=False) + # freed automatically at the end of producer() + channel.waitclose(TESTTIMEOUT) + + def test_channel_endmarker_callback(self, gw): + l = [] + channel = gw.remote_exec(source=''' + channel.send(42) + channel.send(13) + channel.send(channel.gateway.newchannel()) + ''') + channel.setcallback(l.append, 999) + py.test.raises(IOError, channel.receive) + channel.waitclose(TESTTIMEOUT) + assert len(l) == 4 + assert l[:2] == [42,13] + assert isinstance(l[2], channel.__class__) + assert l[3] == 999 + + def test_channel_endmarker_callback_error(self, gw): + q = queue.Queue() + channel = gw.remote_exec(source=''' + raise ValueError() + ''') + channel.setcallback(q.put, endmarker=999) + val = q.get(TESTTIMEOUT) + assert val == 999 + err = channel._getremoteerror() + assert err + assert str(err).find("ValueError") != -1 + +class TestChannelFile: + def test_channel_file_write(self, gw): + channel = gw.remote_exec(""" + f = channel.makefile() + f.write("hello world\\n") + f.close() + channel.send(42) + """) + first = channel.receive() + assert first.strip() == 'hello world' + second = channel.receive() + assert second == 42 + + def test_channel_file_write_error(self, gw): + channel = gw.remote_exec("pass") + f = channel.makefile() + channel.waitclose(TESTTIMEOUT) + py.test.raises(IOError, f.write, 'hello') + + def test_channel_file_proxyclose(self, gw): + channel = gw.remote_exec(""" + f = channel.makefile(proxyclose=True) + f.write("hello world") + f.close() + channel.send(42) + """) + first = channel.receive() + assert first.strip() == 'hello world' + py.test.raises(EOFError, channel.receive) + + def test_channel_file_read(self, gw): + channel = gw.remote_exec(""" + f = channel.makefile(mode='r') + s = f.read(2) + channel.send(s) + s = f.read(5) + channel.send(s) + """) + channel.send("xyabcde") + s1 = channel.receive() + s2 = channel.receive() + assert s1 == "xy" + assert s2 == "abcde" + + def test_channel_file_read_empty(self, gw): + channel = gw.remote_exec("pass") + f = channel.makefile(mode="r") + s = f.read(3) + assert s == "" + s = f.read(5) + assert s == "" + + def test_channel_file_readline_remote(self, gw): + channel = gw.remote_exec(""" + channel.send('123\\n45') + """) + channel.waitclose(TESTTIMEOUT) + f = channel.makefile(mode="r") + s = f.readline() + assert s == "123\n" + s = f.readline() + assert s == "45" + + def test_channel_makefile_incompatmode(self, gw): + channel = gw.newchannel() + py.test.raises(ValueError, 'channel.makefile("rw")') + + +class TestPopenGateway: + gwtype = 'popen' + + def test_chdir_separation(self, tmpdir): + old = tmpdir.chdir() + try: + gw = execnet.makegateway('popen') + finally: + waschangedir = old.chdir() + c = gw.remote_exec("import os ; channel.send(os.getcwd())") + x = c.receive() + assert x == str(waschangedir) + + def test_remoteerror_readable_traceback(self, gw): + e = py.test.raises(gateway_base.RemoteError, + 'gw.remote_exec("x y").waitclose()') + assert "gateway_base" in e.value.formatted + + def test_many_popen(self): + num = 4 + l = [] + for i in range(num): + l.append(execnet.makegateway('popen')) + channels = [] + for gw in l: + channel = gw.remote_exec("""channel.send(42)""") + channels.append(channel) +## try: +## while channels: +## channel = channels.pop() +## try: +## ret = channel.receive() +## assert ret == 42 +## finally: +## channel.gateway.exit() +## finally: +## for x in channels: +## x.gateway.exit() + while channels: + channel = channels.pop() + ret = channel.receive() + assert ret == 42 + + def test_rinfo_popen(self, gw): + rinfo = gw._rinfo() + assert rinfo.executable == py.std.sys.executable + assert rinfo.cwd == py.std.os.getcwd() + assert rinfo.version_info == py.std.sys.version_info + + def test_waitclose_on_remote_killed(self): + gw = execnet.makegateway('popen') + channel = gw.remote_exec(""" + import os + import time + channel.send(os.getpid()) + time.sleep(100) + """) + remotepid = channel.receive() + py.process.kill(remotepid) + py.test.raises(EOFError, "channel.waitclose(TESTTIMEOUT)") + py.test.raises(IOError, channel.send, None) + py.test.raises(EOFError, channel.receive) + + def test_receive_on_remote_io_closed(self): + gw = execnet.makegateway('popen') + channel = gw.remote_exec(""" + raise SystemExit() + """) + py.test.raises(EOFError, channel.receive) + +def test_socket_gw_host_not_found(gw): + py.test.raises(execnet.HostNotFound, + 'execnet.makegateway("socket=qwepoipqwe:9000")' + ) + +class TestSshPopenGateway: + gwtype = "ssh" + + def test_sshconfig_config_parsing(self, monkeypatch): + import subprocess + l = [] + monkeypatch.setattr(subprocess, 'Popen', + lambda *args, **kwargs: l.append(args[0])) + py.test.raises(AttributeError, + """execnet.makegateway("ssh=xyz//ssh_config=qwe")""") + assert len(l) == 1 + popen_args = l[0] + i = popen_args.index('-F') + assert popen_args[i+1] == "qwe" + + def test_sshaddress(self, gw, specssh): + assert gw.remoteaddress == specssh.ssh + + def test_host_not_found(self, gw): + py.test.raises(execnet.HostNotFound, + "execnet.makegateway('ssh=nowhere.codespeak.net')") + +class TestThreads: + def test_threads(self): + gw = execnet.makegateway('popen') + gw.remote_init_threads(3) + c1 = gw.remote_exec("channel.send(channel.receive())") + c2 = gw.remote_exec("channel.send(channel.receive())") + c2.send(1) + res = c2.receive() + assert res == 1 + c1.send(42) + res = c1.receive() + assert res == 42 + + def test_status_with_threads(self): + gw = execnet.makegateway('popen') + gw.remote_init_threads(3) + c1 = gw.remote_exec("channel.send(1) ; channel.receive()") + c2 = gw.remote_exec("channel.send(2) ; channel.receive()") + c1.receive() + c2.receive() + rstatus = gw.remote_status() + assert rstatus.numexecuting == 2 + 1 + assert rstatus.execqsize == 0 + c1.send(1) + c2.send(1) + c1.waitclose() + c2.waitclose() + rstatus = gw.remote_status() + assert rstatus.numexecuting == 0 + 1 + assert rstatus.execqsize == 0 + + def test_threads_twice(self): + gw = execnet.makegateway('popen') + gw.remote_init_threads(3) + py.test.raises(IOError, gw.remote_init_threads, 3) + + +class TestTracing: + def test_debug(self, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "1") + source = py.std.inspect.getsource(gateway_base) + d = {} + gateway_base.do_exec(source, d) + assert 'debugfile' in d + + def test_popen_filetracing(self, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "1") + gw = execnet.makegateway("popen") + pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() + tmpdir = py.path.local(py.std.tempfile.gettempdir()) + slavefile = tmpdir.join("execnet-debug-%s" % pid) + slave_line = "creating slavegateway" + for line in slavefile.readlines(): + if slave_line in line: + break + else: + py.test.fail("did not find %r in tracefile" %(slave_line,)) + gw.exit() + + @needs_osdup + def test_popen_stderr_tracing(self, capfd, monkeypatch): + monkeypatch.setenv('EXECNET_DEBUG', "2") + gw = execnet.makegateway("popen") + pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() + out, err = capfd.readouterr() + slave_line = "[%s] creating slavegateway" % pid + assert slave_line in err + gw.exit() + +def test_nodebug(): + from execnet import gateway_base + assert not hasattr(gateway_base, 'debugfile') + + From commits-noreply at bitbucket.org Thu Dec 3 23:31:43 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 22:31:43 +0000 (UTC) Subject: [execnet-commit] execnet commit 75e22c145464: assign gateway id's early, cleanup Message-ID: <20091203223143.271357F099@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259865531 -3600 # Node ID 75e22c14546489cfccef5af042cae853ac05abf3 # Parent 7681be82d2ffb3f8de8253f9ac1278fe89be99d8 assign gateway id's early, cleanup --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -13,8 +13,8 @@ importdir = os.path.dirname(os.path.dirn class Gateway(gateway_base.BaseGateway): """ Gateway to a local or remote Python Intepreter. """ - def __init__(self, io): - super(Gateway, self).__init__(io=io, _startcount=1) + def __init__(self, io, id): + super(Gateway, self).__init__(io=io, id=id, _startcount=1) self._remote_bootstrap_gateway(io) self._initreceive() @@ -138,25 +138,25 @@ channel.send(dict( class PopenCmdGateway(Gateway): _remotesetup = "io = init_popen_io()" - def __init__(self, args): + def __init__(self, args, id): from subprocess import Popen, PIPE self._popen = p = Popen(args, stdin=PIPE, stdout=PIPE) io = Popen2IO(p.stdin, p.stdout) - super(PopenCmdGateway, self).__init__(io=io) + super(PopenCmdGateway, self).__init__(io=io, id=id) popen_bootstrapline = "import sys ; exec(eval(sys.stdin.readline()))" class PopenGateway(PopenCmdGateway): """ This Gateway provides interaction with a newly started python subprocess. """ - def __init__(self, python=None): + def __init__(self, id, python=None): """ instantiate a gateway to a subprocess started with the given 'python' executable. """ if not python: python = sys.executable args = [str(python), '-c', popen_bootstrapline] - super(PopenGateway, self).__init__(args) + super(PopenGateway, self).__init__(args, id=id) def _remote_bootstrap_gateway(self, io): sendexec(io, @@ -185,7 +185,7 @@ class SocketGateway(Gateway): """ _remotesetup = "io = SocketIO(clientsock)" - def __init__(self, host, port): + def __init__(self, host, port, id): """ instantiate a gateway to a process accessed via a host/port specified socket. """ @@ -198,9 +198,9 @@ class SocketGateway(Gateway): except socket.gaierror: raise HostNotFound(str(sys.exc_info()[1])) io = SocketIO(sock) - super(SocketGateway, self).__init__(io=io) + super(SocketGateway, self).__init__(io=io, id=id) - def new_remote(cls, gateway, hostport=None): + def new_remote(cls, gateway, id, hostport=None): """ return a new (connected) socket gateway, instantiated through the given 'gateway'. """ @@ -226,7 +226,7 @@ class SocketGateway(Gateway): # "port=%r, hostname = %r" %(realport, hostname)) if not realhost or realhost=="0.0.0.0": realhost = "localhost" - return cls(realhost, realport) + return cls(realhost, realport, id=id) new_remote = classmethod(new_remote) class HostNotFound(Exception): @@ -237,7 +237,7 @@ class SshGateway(PopenCmdGateway): established via the 'ssh' command line binary. The remote side needs to have a Python interpreter executable. """ - def __init__(self, sshaddress, remotepython=None, ssh_config=None): + def __init__(self, sshaddress, id, remotepython=None, ssh_config=None): """ instantiate a remote ssh process with the given 'sshaddress' and remotepython version. you may specify an ssh_config file. @@ -250,7 +250,7 @@ class SshGateway(PopenCmdGateway): args.extend(['-F', str(ssh_config)]) remotecmd = '%s -c "%s"' %(remotepython, popen_bootstrapline) args.extend([sshaddress, remotecmd]) - super(SshGateway, self).__init__(args) + super(SshGateway, self).__init__(args, id=id) def _remote_bootstrap_gateway(self, io): try: --- a/execnet/multi.py +++ b/execnet/multi.py @@ -56,24 +56,26 @@ class Group: """ if not isinstance(spec, XSpec): spec = XSpec(spec) + id = self._allocate_id(spec.id) if spec.popen: - gw = gateway.PopenGateway(python=spec.python) + gw = gateway.PopenGateway(python=spec.python, id=id) elif spec.ssh: - gw = gateway.SshGateway(spec.ssh, remotepython=spec.python, ssh_config=spec.ssh_config) + gw = gateway.SshGateway(spec.ssh, remotepython=spec.python, + ssh_config=spec.ssh_config, id=id) elif spec.socket: assert not spec.python, ( "socket: specifying python executables not yet supported") gateway_id = spec.installvia if gateway_id: viagw = self._id2gateway[gateway_id] - gw = gateway.SocketGateway.new_remote(viagw) + gw = gateway.SocketGateway.new_remote(viagw, id=id) else: - hostport = spec.socket.split(":") - gw = gateway.SocketGateway(*hostport) + host, port = spec.socket.split(":") + gw = gateway.SocketGateway(host, port, id=id) else: raise ValueError("no gateway type found for %r" % (spec._spec,)) gw.spec = spec - self._register(gw, id=spec.id) + self._register(gw) if spec.chdir or spec.nice: channel = gw.remote_exec(""" import os @@ -90,18 +92,22 @@ class Group: channel.waitclose() return gw - def _register(self, gateway, id=None): + def _allocate_id(self, id=None): + if id is None: + id = str(self._autoidcounter) + self._autoidcounter += 1 + assert not callable(id) + assert id not in self._id2gateway + return id + + def _register(self, gateway): assert not hasattr(gateway, '_group') - if id is None: - id = self._autoidcounter - self._autoidcounter += 1 - id = str(id) + assert gateway.id assert id not in self._id2gateway assert gateway not in self._activegateways self._activegateways[gateway] = True - self._id2gateway[id] = gateway + self._id2gateway[gateway.id] = gateway gateway._group = self - gateway.id = id def _unregister(self, gateway): del self._id2gateway[gateway.id] --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -613,8 +613,9 @@ class BaseGateway(object): class _StopExecLoop(Exception): pass - def __init__(self, io, _startcount=2): + def __init__(self, io, id, _startcount=2): self._io = io + self.id = id self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = threading.RLock() self._serializer = Serializer(io) @@ -1064,4 +1065,4 @@ def init_popen_io(): def serve(io): trace("creating slavegateway on %r" %(io,)) - SlaveGateway(io=io, _startcount=2).serve() + SlaveGateway(io=io, id="slave", _startcount=2).serve() From commits-noreply at bitbucket.org Thu Dec 3 23:31:44 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 22:31:44 +0000 (UTC) Subject: [execnet-commit] execnet commit 00cb8b7aa529: do nicer, faster and more informative tracing. Message-ID: <20091203223144.B5E0C7F0AE@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259866049 -3600 # Node ID 00cb8b7aa5296d69f1312a77dda79835bb1959bd # Parent 75e22c14546489cfccef5af042cae853ac05abf3 do nicer, faster and more informative tracing. --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -66,7 +66,7 @@ class Gateway(gateway_base.BaseGateway): inspect.getsource(gateway_base), self._remotesetup, "io.write('1'.encode('ascii'))", - "serve(io)" + "serve(io, id='%s-slave')" % self.id, ) s = io.read(1) assert s == "1".encode('ascii') @@ -167,7 +167,7 @@ class PopenGateway(PopenCmdGateway): sendexec(io, "import sys ; sys.path.insert(0, %r)" % importdir, "from execnet.gateway_base import serve, init_popen_io", - "serve(init_popen_io())", + "serve(init_popen_io(), id='%s-slave')" % self.id, ) s = io.read(1) assert s == "1".encode('ascii') --- a/execnet/multi.py +++ b/execnet/multi.py @@ -96,7 +96,6 @@ class Group: if id is None: id = str(self._autoidcounter) self._autoidcounter += 1 - assert not callable(id) assert id not in self._id2gateway return id --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -161,7 +161,7 @@ class PseudoChannel: def test_exectask(): io = py.io.BytesIO() - gw = gateway_base.SlaveGateway(io) + gw = gateway_base.SlaveGateway(io, id="something") ch = PseudoChannel() gw.executetask((ch, "raise ValueError()")) assert "ValueError" in str(ch._closed[0]) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -29,16 +29,18 @@ sysex = (KeyboardInterrupt, SystemExit) DEBUG = os.environ.get('EXECNET_DEBUG') pid = os.getpid() if DEBUG == '2': - def trace(msg): - sys.stderr.write("[%s] %s\n" % (pid, msg)) + def trace(*msg): + line = " ".join(map(str, msg)) + sys.stderr.write("[%s] %s\n" % (pid, line)) sys.stderr.flush() elif DEBUG: import tempfile, os.path fn = os.path.join(tempfile.gettempdir(), 'execnet-debug-%d' % os.getpid()) debugfile = open(fn, 'w') - def trace(msg): + def trace(*msg): + line = " ".join(map(str, msg)) try: - debugfile.write(msg + "\n") + debugfile.write(line + "\n") debugfile.flush() except sysex: raise @@ -50,7 +52,7 @@ elif DEBUG: except (IOError,ValueError): pass # nothing we can do anymore else: - def trace(msg): + def trace(*msg): pass @@ -126,11 +128,9 @@ class Popen2IO: self.outfile.flush() def close_read(self): - trace("popen-io.close_read()") self.infile.close() def close_write(self): - trace("popen-io.close_write()") self.outfile.close() class Message: @@ -269,6 +269,9 @@ class Channel(object): self._receiveclosed = threading.Event() self._remoteerrors = [] + def _trace(self, *msg): + self.gateway._trace(self.id, *msg) + def setcallback(self, callback, endmarker=NO_ENDMARKER_WANTED): """ set a callback function for receiving items. @@ -312,7 +315,7 @@ class Channel(object): def __del__(self): if self.gateway is None: # can be None in tests return - self.gateway._trace("Channel(%d).__del__" % self.id) + self._trace("channel.__del__") # no multithreading issues here, because we have the last ref to 'self' if self._closed: # state transition "closed" --> "deleted" @@ -371,7 +374,7 @@ class Channel(object): if self._executing: raise IOError("cannot explicitly close channel within remote_exec") if self._closed: - trace("%r redundant call to close(), ignoring" %(self,)) + self.gateway._trace(self, "ignoring redundant call to close()") if not self._closed: # state transition "opened/sendonly" --> "closed" # threads warning: the channel might be closed under our feet, @@ -384,7 +387,7 @@ class Channel(object): put(Message.CHANNEL_CLOSE_ERROR(self.id, error)) else: put(Message.CHANNEL_CLOSE(self.id)) - trace("%r: sent channel close message" %(self,)) + self._trace("sent channel close message") if isinstance(error, RemoteError): self._remoteerrors.append(error) self._closed = True # --> "closed" @@ -609,6 +612,7 @@ class ChannelFileRead(ChannelFile): class BaseGateway(object): exc_info = sys.exc_info + id = "" class _StopExecLoop(Exception): pass @@ -619,7 +623,10 @@ class BaseGateway(object): self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = threading.RLock() self._serializer = Serializer(io) - self._trace = trace # globals may be NONE at process-termination + self._globaltrace = trace # globals may be NONE at process-termination + + def _trace(self, *msg): + self._globaltrace(self.id, *msg) def _initreceive(self): self._receiverthread = threading.Thread(name="receiver", @@ -634,7 +641,7 @@ class BaseGateway(object): while 1: try: msg = Message.readfrom(unserializer) - self._trace("received <- %r" % msg) + self._trace("received", msg) _receivelock = self._receivelock _receivelock.acquire() try: @@ -642,24 +649,25 @@ class BaseGateway(object): finally: _receivelock.release() except sysex: + self._trace("io.close_read()") self._io.close_read() break except EOFError: self._error = sys.exc_info()[1] break except: - self._trace("RECEIVERTHREAD: %s " %( - geterrortext(self.exc_info()), )) + self._trace("RECEIVERTHREAD", + geterrortext(self.exc_info())) break finally: self._channelfactory._finished_receiving() if threading: # might be None during shutdown/finalization - self._trace('leaving %r' % threading.currentThread()) + self._trace('leaving', threading.currentThread()) def _send(self, msg): assert isinstance(msg, Message) msg.writeto(self._serializer) - self._trace('sent -> %r' % msg) + self._trace('sent', msg) def _local_schedulexec(self, channel, sourcetask): channel.close("execution disallowed") @@ -699,6 +707,7 @@ class SlaveGateway(BaseGateway): except self._StopExecLoop: break finally: + self._trace("io.close_write()") self._io.close_write() self._trace("slavegateway.serve finished") if joining: @@ -1063,6 +1072,6 @@ def init_popen_io(): sys.stdout = os.fdopen(1, 'w', 1) return io -def serve(io): +def serve(io, id): trace("creating slavegateway on %r" %(io,)) - SlaveGateway(io=io, id="slave", _startcount=2).serve() + SlaveGateway(io=io, id=id, _startcount=2).serve() --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -65,6 +65,7 @@ class TestGroup: exitlist = [] joinlist = [] class PseudoGW: + id = "9999" def exit(self): exitlist.append(self) group._unregister(self) From commits-noreply at bitbucket.org Fri Dec 4 00:01:04 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 23:01:04 +0000 (UTC) Subject: [execnet-commit] execnet commit c4646e0593cb: restructuring example docs Message-ID: <20091203230104.EE0F97F0B2@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259880974 -3600 # Node ID c4646e0593cb7eff6403b832ce16771047e5c882 # Parent 00cb8b7aa5296d69f1312a77dda79835bb1959bd restructuring example docs --- a/doc/example/test_channelexec.txt +++ b/doc/example/test_channelexec.txt @@ -1,3 +1,4 @@ +.. _channelexec: remote execute modules using their __name__ -------------------------------------------------------------- --- a/doc/examples.txt +++ b/doc/examples.txt @@ -2,160 +2,21 @@ execnet examples ============================================================================== -execnet is under active development. If you have questions -or ideas or otherwise want to contribute please feel welcome -to join the `execnet-dev`_ mailing list. +.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit -.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev +execnet is under active development. Checkout `execnet-dev`_ +and `execnet-commit`_. -Connect to Python2/Numpy from Python3 ----------------------------------------- +.. toctree:: + :maxdepth: 1 -Here we run a Python3 interpreter to connect to a Python2.6 interpreter -that has numpy installed. We send items to be added to an array and -receive back the remote "repr" of the array:: - - import execnet - gw = execnet.makegateway("popen//python=python2.6") - channel = gw.remote_exec(""" - import numpy - array = numpy.array([1,2,3]) - while 1: - x = channel.receive() - if x is None: - break - array = numpy.append(array, x) - channel.send(repr(array)) - """) - for x in range(10): - channel.send(x) - channel.send(None) - print (channel.receive()) - -will print on the CPython3.1 side:: - - array([1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - -A more refined real-life example of python3/python2 interaction -is the anyvc_ project which uses version-control bindings in -a Python2 subprocess in order to offer Python3-based library -functionality. - -.. _anyvc: http://bitbucket.org/RonnyPfannschmidt/anyvc/overview/ - -Work with Java objects from CPython ----------------------------------------- - -Use your CPython interpreter to connect to a Jython_ interpreter -and work with Java types:: - - import execnet - gw = execnet.makegateway("popen//python=jython") - channel = gw.remote_exec(""" - from java.util import Vector - v = Vector() - v.add('aaa') - v.add('bbb') - for val in v: - channel.send(val) - """) - - for item in channel: - print (item) - -will print on the CPython side:: - - aaa - bbb - -.. _Jython: http://www.jython.org - -Work with C# objects from CPython ----------------------------------------- - -(Experimental) use your CPython interpreter to connect to a IronPython_ interpreter -which can work with C# classes. Here is an example for instantiating -a CLR Array instance and sending back its representation:: - - import execnet - gw = execnet.makegateway("popen//python=ipy") - - channel = gw.remote_exec(""" - import clr - clr.AddReference("System") - from System import Array - array = Array[float]([1,2]) - channel.send(str(array)) - """) - print (channel.receive()) - -using Mono 2.0 and IronPython-1.1 this will print on the CPython side:: - - System.Double[](1.0, 2.0) - -.. note:: - Using IronPython needs more testing, likely newer versions - will work better. please feedback if you have information. - -.. _IronPython: http://www.IronPython.org - - -.. include example/test_cwd.txt - -.. _channelexec: - -.. include:: example/test_channelexec.txt - -.. _command: - -.. include:: example/test_remotecmd.txt - -.. _group: - -.. include:: example/test_group.txt - -.. include:: example/test_syncreceive2.txt - -.. include:: example/test_asyncreceive2.txt - - -Receive file contents from remote SSH account ------------------------------------------------------ - -Here is a small program that you can use to retrieve -contents of remote files:: - - import execnet - # open a gateway to a fresh child process - gw = execnet.makegateway("ssh=codespeak.net") - channel = gw.remote_exec(""" - for fn in channel: - f = open(fn, 'rb') - channel.send(f.read()) - f.close() - """) - - for fn in somefilelist: - channel.send(fn) - content = channel.receive() - # process content - - # later you can exit / close down the gateway - gw.exit() - - -Instantiate a socket server in a new subprocess ------------------------------------------------------ - -(experimental) The following example opens a PopenGateway, i.e. a python -child process, and starts a socket server within that process -and then opens a second gateway to the freshly started -socketserver:: - - import execnet - - popengw = execnet.PopenGateway() - socketgw = execnet.SocketGateway.new_remote(popengw, ("127.0.0.1", 0)) - - print socketgw.remote_status() # print some info about the remote environment - + example/test_connect + example/test_cwd.txt + example/test_channelexec.txt + example/test_remotecmd.txt + example/test_group.txt + example/test_syncreceive2.txt + example/test_asyncreceive2.txt + example/test_ssh_fileserver.txt + example/test_installvia.txt --- a/doc/example/test_pid.txt +++ /dev/null @@ -1,18 +0,0 @@ - -Here is an self-contained example for reading the process identifier. - - >>> import execnet, os - >>> gw = execnet.makegateway("popen") - >>> channel = gw.remote_exec(""" - ... import os - ... channel.send(os.getpid()) - ... """) - >>> remote_pid = channel.receive() - >>> remote_pid != os.getpid() - True - -If you'd like to avoid inlining source strings take a look -at the channelexec_ and command_ example. - -.. _channelexec: examples.html#channelexec -.. _command: examples.html#command --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -1,3 +1,5 @@ +.. _group: + Creating and working with grouped Gateways ------------------------------------------------------ --- /dev/null +++ b/doc/example/test_connect.txt @@ -0,0 +1,93 @@ + +Connect to Python2/Numpy from Python3 +---------------------------------------- + +Here we run a Python3 interpreter to connect to a Python2.6 interpreter +that has numpy installed. We send items to be added to an array and +receive back the remote "repr" of the array:: + + import execnet + gw = execnet.makegateway("popen//python=python2.6") + channel = gw.remote_exec(""" + import numpy + array = numpy.array([1,2,3]) + while 1: + x = channel.receive() + if x is None: + break + array = numpy.append(array, x) + channel.send(repr(array)) + """) + for x in range(10): + channel.send(x) + channel.send(None) + print (channel.receive()) + +will print on the CPython3.1 side:: + + array([1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +A more refined real-life example of python3/python2 interaction +is the anyvc_ project which uses version-control bindings in +a Python2 subprocess in order to offer Python3-based library +functionality. + +.. _anyvc: http://bitbucket.org/RonnyPfannschmidt/anyvc/overview/ + +Work with Java objects from CPython +---------------------------------------- + +Use your CPython interpreter to connect to a `Jython 2.5.1`_ interpreter +and work with Java types:: + + import execnet + gw = execnet.makegateway("popen//python=jython") + channel = gw.remote_exec(""" + from java.util import Vector + v = Vector() + v.add('aaa') + v.add('bbb') + for val in v: + channel.send(val) + """) + + for item in channel: + print (item) + +will print on the CPython side:: + + aaa + bbb + +.. _`Jython 2.5.1`: http://www.jython.org + +Work with C# objects from CPython +---------------------------------------- + +(Experimental) use your CPython interpreter to connect to a IronPython_ interpreter +which can work with C# classes. Here is an example for instantiating +a CLR Array instance and sending back its representation:: + + import execnet + gw = execnet.makegateway("popen//python=ipy") + + channel = gw.remote_exec(""" + import clr + clr.AddReference("System") + from System import Array + array = Array[float]([1,2]) + channel.send(str(array)) + """) + print (channel.receive()) + +using Mono 2.0 and IronPython-1.1 this will print on the CPython side:: + + System.Double[](1.0, 2.0) + +.. note:: + Using IronPython needs more testing, likely newer versions + will work better. please feedback if you have information. + +.. _IronPython: http://www.IronPython.org + + --- a/doc/example/test_remotecmd.txt +++ b/doc/example/test_remotecmd.txt @@ -1,3 +1,5 @@ +.. _command: + A simple command pattern -------------------------------------------------------------- --- /dev/null +++ b/doc/example/test_installvia.txt @@ -0,0 +1,16 @@ + +Instantiate a socket server in a new subprocess +----------------------------------------------------- + +(experimental) The following example opens a PopenGateway, i.e. a python +child process, and starts a socket server within that process +and then opens a second gateway to the freshly started +socketserver:: + + import execnet + + popengw = execnet.PopenGateway() + socketgw = execnet.SocketGateway.new_remote(popengw, ("127.0.0.1", 0)) + + print socketgw.remote_status() # print some info about the remote environment + --- /dev/null +++ b/doc/example/test_ssh_fileserver.txt @@ -0,0 +1,25 @@ + +Receive file contents from remote SSH account +----------------------------------------------------- + +Here is a small program that you can use to retrieve +contents of remote files:: + + import execnet + # open a gateway to a fresh child process + gw = execnet.makegateway("ssh=codespeak.net") + channel = gw.remote_exec(""" + for fn in channel: + f = open(fn, 'rb') + channel.send(f.read()) + f.close() + """) + + for fn in somefilelist: + channel.send(fn) + content = channel.receive() + # process content + + # later you can exit / close down the gateway + gw.exit() + --- a/doc/basics.txt +++ b/doc/basics.txt @@ -70,8 +70,6 @@ in the connected interpreter: .. automethod:: Gateway.remote_exec(source) -.. include:: example/test_pid.txt - .. _`Channel`: .. _`channel-api`: --- a/doc/example/test_cwd.txt +++ b/doc/example/test_cwd.txt @@ -8,3 +8,25 @@ A PopenGateway has the same working dire >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") >>> res = ch.receive() >>> assert res == os.getcwd() + + +Getting the remote process ID +-------------------------------------- + +Here is an self-contained example for reading a remote process identifier. + + >>> import execnet, os + >>> gw = execnet.makegateway("popen") + >>> channel = gw.remote_exec(""" + ... import os + ... channel.send(os.getpid()) + ... """) + >>> remote_pid = channel.receive() + >>> remote_pid != os.getpid() + True + +If you'd like to avoid inlining source strings take a look +at the channelexec_ and command_ example. + +.. _channelexec: examples.html#channelexec +.. _command: examples.html#command From commits-noreply at bitbucket.org Fri Dec 4 00:01:06 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 23:01:06 +0000 (UTC) Subject: [execnet-commit] execnet commit d7477e49fdd0: new method: gateway.hasreceiver() returns True Message-ID: <20091203230106.BA4E67F0B3@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259881220 -3600 # Node ID d7477e49fdd084039a9ff597d109ebfa3f53c6e3 # Parent c4646e0593cb7eff6403b832ce16771047e5c882 new method: gateway.hasreceiver() returns True if the gateway is still receive-active. remote_status now only carries information about remote execution status. --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -78,6 +78,10 @@ class Gateway(gateway_base.BaseGateway): self._cache_rinfo = RInfo(ch.receive()) return self._cache_rinfo + def hasreceiver(self): + """ return True if gateway is able to receive data. """ + return self._receiverthread.isAlive() # approxmimation + def remote_status(self): """ return information object about remote execution status. """ channel = self.newchannel() --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -168,8 +168,7 @@ def _setupmessages(): for ch in active_channels: if getattr(ch, '_executing', False): numexec += 1 - d = {'receiving': True, - 'execqsize': gateway._execqueue.qsize(), + d = {'execqsize': gateway._execqueue.qsize(), 'numchannels': len(active_channels), 'numexecuting': numexec } --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -21,7 +21,7 @@ def test_deprecation(recwarn, monkeypatc class TestBasicGateway: def test_correct_setup(self, gw): - assert gw._receiverthread.isAlive() + assert gw.hasreceiver() assert gw in gw._group assert gw.id in gw._group assert gw.spec @@ -36,7 +36,6 @@ class TestBasicGateway: def test_gateway_status_simple(self, gw): status = gw.remote_status() - assert status.receiving assert not status.execqsize assert status.numexecuting == 0 @@ -54,7 +53,6 @@ class TestBasicGateway: ch2 = gw.remote_exec("channel.receive()") ch1.receive() status = gw.remote_status() - assert status.receiving assert status.numexecuting == 1 # number of active execution threads assert status.execqsize == 1 # one more queued assert status.numchannels == 2 @@ -63,7 +61,6 @@ class TestBasicGateway: ch1.waitclose() ch2.waitclose() status = gw.remote_status() - assert status.receiving assert status.execqsize == 0 assert status.numexecuting == 0 assert status.numchannels == 0 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 1.0.1 (pending) -------------------------------- +- new method: gateway.hasreceiver() returns True + if the gateway is still receive-active. remote_status + now only carries information about remote execution status. + - have popen-gateways use imports instead of source-strings, also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) From commits-noreply at bitbucket.org Fri Dec 4 00:08:51 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 3 Dec 2009 23:08:51 +0000 (UTC) Subject: [execnet-commit] execnet commit 9c5a39c0506e: remove redundant file, better gateway repr, remove fixed issue Message-ID: <20091203230851.CBC557F0B1@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259881560 -3600 # Node ID 9c5a39c0506e65266f279dd8ee4356bc9f9c5ce7 # Parent d7477e49fdd084039a9ff597d109ebfa3f53c6e3 remove redundant file, better gateway repr, remove fixed issue --- a/ISSUES.txt +++ b/ISSUES.txt @@ -46,11 +46,3 @@ This way a gateway can be instantiated t intermediating gateway which bi-directionallry forwads Messages through a existing channel. -cleanup remote_status / receive status ----------------------------------------- -tags: 1.0.1 feature - -remote_status() will only return a result object -if the other side could receive so the "receiving" -bit is redundant. Rather a gateway needs a -gateway.isreceiving() method. --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -20,23 +20,14 @@ class Gateway(gateway_base.BaseGateway): def __repr__(self): """ return string representing gateway type and status. """ - if hasattr(self, 'id'): - id = self.id - else: - id = "???" - if hasattr(self, 'remoteaddress'): - addr = '[%s]' % (self.remoteaddress,) - else: - addr = '' try: - r = (self._receiverthread.isAlive() and "receive-live" or - "not-receiving") + r = (self.hasreceiver() and 'receive-live' or 'not-receiving') i = len(self._channelfactory.channels()) except AttributeError: r = "uninitialized" i = "no" - return "<%s%s id=%r %s, %s active channels>" %( - self.__class__.__name__, addr, id, r, i) + return "<%s id=%r %s, %s active channels>" %( + self.__class__.__name__, self.id, r, i) def exit(self): """ trigger gateway exit. Defer waiting for finishing --- a/doc/example/test_socketnewremote.txt +++ /dev/null @@ -1,44 +0,0 @@ -Instantiate a Socket Gateway ------------------------------------------------------ - -Sometimes there is no direct access method towards a -machine. If you have a trusted network (like a internal -developer LAN) you can use a simplistic `socketserver script`_ -which listens on a socket ("8888" by default) and executes -incoming strings. Download it and run it on the target -machine like this:: - - python socketserver.py localhost:8888 - -After which you can initiate a Gateway to the socket-server -running machine like this:: - - >>> import execnet - >>> gw = execnet.makegateway("socket=localhost:8888") # doctest: +SKIP - - - -.. _`socketserver script`: http://bitbucket.org/hpk42/execnet/raw/493fc337a1f1/execnet/script/socketserver.py - -Instantiate a socket server through an existing Gateway --------------------------------------------------------- - -.. note:: - Experimental, please send a note if you are using - the following feature. - -The following example shows a special method to open up -a SocketServer gateway through another existing gateway. - - >>> import execnet - >>> group = execnet.Group() - >>> gw = group.makegateway("popen") - >>> gw.id - '1' - >>> group.makegateway("socket//installvia=1") # doctest: +SKIP - - - -This method will instantiate a Socket based Gateway -whose remote counterpart will be run via the -given ``installvia`` specified gateway. From commits-noreply at bitbucket.org Fri Dec 4 15:05:54 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 4 Dec 2009 14:05:54 +0000 (UTC) Subject: [execnet-commit] execnet commit 3023e9db5d9e: re-work initial documentation and web page Message-ID: <20091204140554.4ECFB7F0B3@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259935536 -3600 # Node ID 3023e9db5d9e030e8c1467a9e132b673c02961b0 # Parent 9c5a39c0506e65266f279dd8ee4356bc9f9c5ce7 re-work initial documentation and web page --- a/doc/index.txt +++ b/doc/index.txt @@ -1,65 +1,62 @@ + +execnet: connect your execution environments +======================================================== + .. image:: _static/pythonring.png :align: right -Welcome to execnet! -========================= +Execnet allows to ad-hoc connect to Python interpreters across version, platform and network barriers. It provides a minimal, fast and robust API for the following uses: -The execnet package allows to: +* distribute tasks to multiple CPUs +* deploy hybrid applications +* manage local and remote execution environments -* instantiate local/remote Python Interpreters -* send code for execution to one or many Interpreters -* send and receive data through channels +Features +------------------ -execnet performs **zero-install bootstrapping** into other interpreters; -package installation is only required at the initiating side. execnet enables -interoperation between CPython 2.4-3.1, Jython 2.5.1, PyPy 1.1 and IronPython -and works well on Windows, Linux and OSX systems. +* zero-install bootstrapping: no remote installation required! -execnet was written and is maintained by `Holger Krekel`_ (blog_) with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +* flexible communication: send/receive as well as + callback/queue mechanisms supported -.. note:: - Support of Jython and IronPython are experimental at this point. - Feedback and help with testing welcome. +* simple serialization of python builtin types (no pickling) +* grouped creation and robust termination of processes -Getting Started -==================== +* well tested between CPython 2.4-3.1, Jython 2.5.1 and PyPy 1.1 + interpreters. -Install a public `pypi release`_ via `easy_install`_ or ``pip``:: +* fully interoperable between Windows and Unix-ish systems. - easy_install -U execnet +Known users +------------------- -To work from the development tree checkout the `execnet mercurial repository`_. +* `py.test`_ uses it for its `distributed testing`_ mechanism. -Next checkout the basic api and examples: +* Jacob Perkins uses it for his `Distributed NTLK with execnet`_ + project. -.. toctree:: - :maxdepth: 1 +* Ronny Pfannschmidt uses it for his `anyvc`_ VCS-abstraction project - basics - examples - changelog +* sysadmins and developers are using it for ad-hoc custom scripting -Issues / Contact channels -=========================== +.. _`py.test`: http://pytest.org +.. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html +.. _`Distributed NTLK with execnet`: http://streamhacker.com/2009/11/29/distributed-nltk-execnet/ +.. _`anyvc`: http://bitbucket.org/RonnyPfannschmidt/anyvc/ -If you have a questions, issues or suggestions you are welcome to join -and post to the `execnet-dev`_ mailing list. Alternatively you -you get see to help on the #pylib IRC channel on Freenode. +Project status +-------------------------- -There also is the `execnet-commit`_ mailing list -which relays commit mails to the `bitbucket repository`_. +The current 1.0 series aims at `basic API`_ stabilization, improved tracing and robust termination. +The 1.1 series will target setting up permanent networks and offering unix-shell-like capabilities to spawn processes and applications. + +execnet was conceived and is `actively developed`_ by `Holger Krekel`_. +The package is licensed under the GPL Version 2 or later, at your choice. +Armin Rigo and Benjamin Peterson have done major contributions which +are MIT-licensed. + +.. _`basic API`: basics.html +.. _`actively developed`: http://bitbucket.org/hpk42/execnet/changesets .. _`Holger Krekel`: http://twitter.com/hpk42 -.. _`blog`: http://tetamap.wordpress.com -.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev -.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit - -.. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall -.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet/ -.. _`execnet mercurial repository`: http://bitbucket.org/hpk42/execnet/ -.. _`pypi release`: http://pypi.python.org/pypi/execnet - - - - --- a/setup.py +++ b/setup.py @@ -1,16 +1,32 @@ """ -the execnet package allows to: +execnet: connect your execution environments +======================================================== -* instantiate local/remote Python Interpreters -* send code for execution to one or many Interpreters -* send and receive data between codeInterpreters through channels +.. image:: _static/pythonring.png + :align: right -execnet performs **zero-install bootstrapping** into other interpreters; -package installation is only required at the initiating side. execnet enables -interoperation between CPython 2.4-3.1, Jython 2.5 and PyPy 1.1 and works -well on Windows, Linux and OSX systems. +Execnet allows to ad-hoc connect to Python interpreters across version, platform and network barriers. It provides a minimal, fast and robust API for the following uses: -execnet was written and is maintained by Holger Krekel with contributions from many others. The package is licensed under the GPL Version 2 or later, at your choice. Contributions and some parts of the package are licensed under the MIT license. +* distribute tasks to multiple CPUs +* deploy hybrid applications +* manage local and remote execution environments + +Features +------------------ + +* zero-install bootstrapping: no remote installation required! + +* flexible communication: send/receive as well as + callback/queue mechanisms supported + +* simple serialization of python builtin types (no pickling) + +* grouped creation and robust termination of processes + +* well tested between CPython 2.4-3.1, Jython 2.5.1 and PyPy 1.1 + interpreters. + +* fully interoperable between Windows and Unix-ish systems. """ try: @@ -23,7 +39,7 @@ from execnet import __version__ def main(): setup( name='execnet', - description='execnet: elastic Python deployment', + description='execnet: connect your execution environments', long_description = __doc__, version= __version__, url='http://codespeak.net/execnet', --- a/doc/basics.txt +++ b/doc/basics.txt @@ -3,12 +3,13 @@ execnet API in a nutshell ============================================================================== execnet ad-hoc instantiates **gateways** to Python -interpreter processes with which you can **remote execute +interpreter processes though which you can **remote execute code** and exchange basic python objects through **channels**. +All gateways are created as part of a **group** which is responsible +for managing membership and **proper termination**. .. image:: _static/basic1.png - Gateways: connecting to another Python Interpreter =================================================== @@ -19,12 +20,12 @@ passing it a gateway specification or UR .. autofunction:: execnet.makegateway(xspec) -Here is an example to instantiate a simple Python subprocess:: +Here is an example which instantiates a simple Python subprocess:: >>> gateway = execnet.makegateway("popen") -You can then use this gateway object to `remote execute code`_ and -`exchange data`_ bidirectionally. +You can use a gateway to `remote execute code`_ and +`exchange data`_ bidirectionally. examples for valid gateway specifications ------------------------------------------- @@ -70,6 +71,12 @@ in the connected interpreter: .. automethod:: Gateway.remote_exec(source) +It is allowed to pass a module object as source code +in which case it's source code will be obtained and +get sent for remote execution. ``remote_exec`` returns +a channel object whose symmetric counterpart channel +is available to the remotely executing source. + .. _`Channel`: .. _`channel-api`: @@ -86,24 +93,27 @@ two asynchronously running programs. .. class:: Channel .. automethod:: Channel.send(item) - .. automethod:: Channel.receive() + .. automethod:: Channel.receive(timeout) .. automethod:: Channel.setcallback(callback, endmarker=_NOENDMARKER) .. automethod:: Channel.makefile(mode, proxyclose=False) .. automethod:: Channel.close(error) .. automethod:: Channel.waitclose(timeout) .. autoattribute:: Channel.RemoteError + .. autoattribute:: Channel.TimeoutError .. _Group: -Grouping Gateways and guaranteed termination +Grouped Gateways and robust termination =============================================== .. _`group examples`: examples.html#group .. currentmodule:: execnet.multi -All created gateway instances are part of a group. Group objects -are container-like objects (see `group examples`_) and manage -the final termination procedure: +All created gateway instances are part of a group. If you +call ``execnet.makegateway`` it actually is forwarded to +the ``execnet.default_group``. Group objects are container +objects (see `group examples`_) and manage the final +termination procedure: .. automethod:: Group.terminate(timeout=None) --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -2,10 +2,9 @@ {% block rootrellink %}
  • home | 
  • -
  • basics | 
  • +
  • basic API | 
  • examples | 
  • -
  • mailing-list | 
  • -
  • pypi-home | 
  • +
  • contact | 
  • docindex»
  • {% endblock %} From commits-noreply at bitbucket.org Fri Dec 4 15:44:02 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 4 Dec 2009 14:44:02 +0000 (UTC) Subject: [execnet-commit] execnet commit bdfe9a4a229e: adding contact and install pages Message-ID: <20091204144402.62D587F031@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259937824 -3600 # Node ID bdfe9a4a229e7f0bf6327997ae7bc593b87e6959 # Parent 3023e9db5d9e030e8c1467a9e132b673c02961b0 adding contact and install pages --- /dev/null +++ b/doc/install.txt @@ -0,0 +1,34 @@ + +Installation +==================== + +Install a public `pypi release`_ via `easy_install`_ or ``pip``:: + + easy_install -U execnet + + or + + pip install execnet + + or + + hg clone https://hpk42 at bitbucket.org/hpk42/execnet/ + python setup.py install # or add checkout path to PYTHONPATH directly + +Next checkout the basic api and examples: + +.. toctree:: + :maxdepth: 1 + + basics + examples + changelog + +.. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall +.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet/ +.. _`execnet mercurial repository`: http://bitbucket.org/hpk42/execnet/ +.. _`pypi release`: http://pypi.python.org/pypi/execnet + + + + --- /dev/null +++ b/doc/contact.txt @@ -0,0 +1,17 @@ +Contact channels +------------------------ + +If you have interest, questions, issues or suggestions you +are welcome to: + +* join `execnet-dev`_ for general discussions +* join `execnet-commit`_ to be notified of changes +* clone the `bitbucket repository`_ and submit patches +* hang out on the irc.freenode.net #pylib channel +* follow the `tetamap blog`_ or `Holger's twitter presence`_. + +.. _`Holger's twitter presence`: http://twitter.com/hpk42 +.. _`tetamap blog`: http://tetamap.wordpress.com +.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit +.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet From commits-noreply at bitbucket.org Sat Dec 5 12:55:52 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 11:55:52 +0000 (UTC) Subject: [execnet-commit] execnet commit 318325f5bc20: fix dist-testing setup Message-ID: <20091205115552.BA8337EF12@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259938630 -3600 # Node ID 318325f5bc20e02a1e45bdfdd538743a0662d7cd # Parent bdfe9a4a229e7f0bf6327997ae7bc593b87e6959 fix dist-testing setup --- /dev/null +++ b/conftest.py @@ -0,0 +1,90 @@ +import execnet +import py + +collect_ignore = ['build', 'doc/_build'] + +rsyncdirs = ['conftest.py', 'execnet', 'testing', 'doc'] + +pytest_plugins = ['pytester', 'doctest'] +# configuration information for tests +def pytest_addoption(parser): + group = parser.getgroup("pylib", "py lib testing options") + group.addoption('--gx', + action="append", dest="gspecs", default=None, + help=("add a global test environment, XSpec-syntax. ")) + +def pytest_funcarg__specssh(request): + return getspecssh(request.config) +def pytest_funcarg__specsocket(request): + return getsocketspec(request.config) +def getgspecs(config=None): + if config is None: + config = py.test.config + return [execnet.XSpec(spec) + for spec in config.getvalueorskip("gspecs")] + +def getspecssh(config=None): + xspecs = getgspecs(config) + for spec in xspecs: + if spec.ssh: + if not py.path.local.sysfind("ssh"): + py.test.skip("command not found: ssh") + return spec + py.test.skip("need '--gx ssh=...'") + +def getsocketspec(config=None): + xspecs = getgspecs(config) + for spec in xspecs: + if spec.socket: + return spec + py.test.skip("need '--gx socket=...'") + +def pytest_generate_tests(metafunc): + if 'gw' in metafunc.funcargnames: + assert 'anypython' not in metafunc.funcargnames, "need combine?" + if hasattr(metafunc.function, 'gwtypes'): + gwtypes = metafunc.function.gwtypes + elif hasattr(metafunc.cls, 'gwtype'): + gwtypes = [metafunc.cls.gwtype] + else: + gwtypes = ['popen', 'socket', 'ssh'] + for gwtype in gwtypes: + metafunc.addcall(id=gwtype, param=gwtype) + elif 'anypython' in metafunc.funcargnames: + for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', + 'pypy-c', 'jython'): + metafunc.addcall(id=name, param=name) + +def pytest_funcarg__anypython(request): + name = request.param + executable = py.path.local.sysfind(name) + if executable is None: + py.test.skip("no %s found" % (name,)) + return executable + +def pytest_funcarg__gw(request): + scope = "session" + group = request.cached_setup( + setup=execnet.Group, + teardown=lambda group: group.terminate(timeout=1), + extrakey="testgroup", + scope=scope, + ) + try: + return group[request.param] + except KeyError: + if request.param == "popen": + gw = group.makegateway("popen//id=popen") + elif request.param == "socket": + pname = 'sproxy1' + if pname not in group: + proxygw = group.makegateway("popen//id=%s" % pname) + #assert group['proxygw'].remote_status().receiving + gw = group.makegateway("socket//id=socket//installvia=%s" % pname) + gw.proxygw = proxygw + assert pname in group + + elif request.param == "ssh": + sshhost = request.getfuncargvalue('specssh').ssh + gw = group.makegateway("ssh=%s//id=ssh" %(sshhost,)) + return gw --- a/testing/conftest.py +++ /dev/null @@ -1,88 +0,0 @@ -import execnet -import py - -rsyncdirs = ['../execnet', '.'] - -pytest_plugins = ['pytester', 'doctest'] -# configuration information for tests -def pytest_addoption(parser): - group = parser.getgroup("pylib", "py lib testing options") - group.addoption('--gx', - action="append", dest="gspecs", default=None, - help=("add a global test environment, XSpec-syntax. ")) - -def pytest_funcarg__specssh(request): - return getspecssh(request.config) -def pytest_funcarg__specsocket(request): - return getsocketspec(request.config) -def getgspecs(config=None): - if config is None: - config = py.test.config - return [execnet.XSpec(spec) - for spec in config.getvalueorskip("gspecs")] - -def getspecssh(config=None): - xspecs = getgspecs(config) - for spec in xspecs: - if spec.ssh: - if not py.path.local.sysfind("ssh"): - py.test.skip("command not found: ssh") - return spec - py.test.skip("need '--gx ssh=...'") - -def getsocketspec(config=None): - xspecs = getgspecs(config) - for spec in xspecs: - if spec.socket: - return spec - py.test.skip("need '--gx socket=...'") - -def pytest_generate_tests(metafunc): - if 'gw' in metafunc.funcargnames: - assert 'anypython' not in metafunc.funcargnames, "need combine?" - if hasattr(metafunc.function, 'gwtypes'): - gwtypes = metafunc.function.gwtypes - elif hasattr(metafunc.cls, 'gwtype'): - gwtypes = [metafunc.cls.gwtype] - else: - gwtypes = ['popen', 'socket', 'ssh'] - for gwtype in gwtypes: - metafunc.addcall(id=gwtype, param=gwtype) - elif 'anypython' in metafunc.funcargnames: - for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', - 'pypy-c', 'jython'): - metafunc.addcall(id=name, param=name) - -def pytest_funcarg__anypython(request): - name = request.param - executable = py.path.local.sysfind(name) - if executable is None: - py.test.skip("no %s found" % (name,)) - return executable - -def pytest_funcarg__gw(request): - scope = "session" - group = request.cached_setup( - setup=execnet.Group, - teardown=lambda group: group.terminate(timeout=1), - extrakey="testgroup", - scope=scope, - ) - try: - return group[request.param] - except KeyError: - if request.param == "popen": - gw = group.makegateway("popen//id=popen") - elif request.param == "socket": - pname = 'sproxy1' - if pname not in group: - proxygw = group.makegateway("popen//id=%s" % pname) - #assert group['proxygw'].remote_status().receiving - gw = group.makegateway("socket//id=socket//installvia=%s" % pname) - gw.proxygw = proxygw - assert pname in group - - elif request.param == "ssh": - sshhost = request.getfuncargvalue('specssh').ssh - gw = group.makegateway("ssh=%s//id=ssh" %(sshhost,)) - return gw --- /dev/null +++ b/doc/__init__.py @@ -0,0 +1,1 @@ +# From commits-noreply at bitbucket.org Sat Dec 5 12:55:54 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 11:55:54 +0000 (UTC) Subject: [execnet-commit] execnet commit 874bca2734d7: try to find python versions on windows as well Message-ID: <20091205115554.3D8DF7EF24@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259948127 -3600 # Node ID 874bca2734d712fb10c09a90c3e4cc438445b871 # Parent 318325f5bc20e02a1e45bdfdd538743a0662d7cd try to find python versions on windows as well --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,18 @@ import execnet import py +import sys collect_ignore = ['build', 'doc/_build'] rsyncdirs = ['conftest.py', 'execnet', 'testing', 'doc'] +winpymap = { + 'python2.6': r'C:\Python26\python.exe', + 'python2.5': r'C:\Python25\python.exe', + 'python2.4': r'C:\Python24\python.exe', + 'python3.1': r'C:\Python31\python.exe', +} + pytest_plugins = ['pytester', 'doctest'] # configuration information for tests def pytest_addoption(parser): @@ -59,6 +67,12 @@ def pytest_funcarg__anypython(request): name = request.param executable = py.path.local.sysfind(name) if executable is None: + if sys.platform == "win32": + executable = winpymap.get(name, None) + if executable: + executable = py.path.local(executable) + if executable.check(): + return executable py.test.skip("no %s found" % (name,)) return executable From commits-noreply at bitbucket.org Sat Dec 5 12:55:55 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 11:55:55 +0000 (UTC) Subject: [execnet-commit] execnet commit b9ae9afeb79d: kill processes after timeout Message-ID: <20091205115555.E51F27EF29@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260014112 -3600 # Node ID b9ae9afeb79d210e10b25558dc32c5fafff195b0 # Parent 874bca2734d712fb10c09a90c3e4cc438445b871 kill processes after timeout --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -138,6 +138,10 @@ class PopenCmdGateway(Gateway): self._popen = p = Popen(args, stdin=PIPE, stdout=PIPE) io = Popen2IO(p.stdin, p.stdout) super(PopenCmdGateway, self).__init__(io=io, id=id) + # fix for jython 2.5.1 + if p.pid is None: + p.pid = self.remote_exec( + "import os; channel.send(os.getpid())").receive() popen_bootstrapline = "import sys ; exec(eval(sys.stdin.readline()))" class PopenGateway(PopenCmdGateway): --- a/execnet/multi.py +++ b/execnet/multi.py @@ -4,11 +4,11 @@ Managing Gateway Groups and interactions (c) 2008-2009, Holger Krekel and others """ -import sys, weakref, atexit, time +import os, sys, weakref, atexit import execnet from execnet import XSpec from execnet import gateway -from execnet.gateway_base import queue, reraise, trace +from execnet.gateway_base import queue, reraise, trace, TimeoutError NO_ENDMARKER_WANTED = object() @@ -115,31 +115,43 @@ class Group: def _cleanup_atexit(self): trace("=== atexit cleanup %r ===" %(self,)) - self.terminate(timeout=2.0) + self.terminate(timeout=1.0) def terminate(self, timeout=None): - """ trigger exit of member gateways and and wait for completion - of receiver-threads of previously exited member gateways and any - started subprocesses. If after timeout seconds (None for no-timeout) - waiting does not finish a TimeoutError will be raised. + """ trigger exit of member gateways and wait for termination + of member gateways and associated subprocesses. After waiting + timeout seconds an attempt to kill local sub processes of popen- + and ssh-gateways is started. Timeout defaults to None meaning + open-ended waiting and no kill attempts. """ - endtime = timeout and (time.time() + timeout) or None for gw in self: - gw.exit() + gw.exit() def join_receiver_and_wait_for_subprocesses(): for gw in self._gateways_to_join: gw.join() while self._gateways_to_join: - gw = self._gateways_to_join.pop(0) + gw = self._gateways_to_join[0] if hasattr(gw, '_popen'): gw._popen.wait() - self._gateways_to_join[:] = [] + del self._gateways_to_join[0] from execnet.threadpool import WorkerPool pool = WorkerPool(1) reply = pool.dispatch(join_receiver_and_wait_for_subprocesses) - reply.get(timeout=timeout) + try: + reply.get(timeout=timeout) + except IOError: + trace("Gateways did not come down after timeout: %r" + %(self._gateways_to_join)) + while self._gateways_to_join: + gw = self._gateways_to_join.pop(0) + popen = getattr(gw, '_popen', None) + if popen: + killpopen(popen) def remote_exec(self, source): + """ remote_exec source on all member gateways and return + MultiChannel connecting to all sub processes. + """ channels = [] for gw in list(self._activegateways): channels.append(gw.remote_exec(source)) @@ -231,3 +243,34 @@ def APIWARN(version, msg, stacklevel=3): import warnings Warn = DeprecationWarning("(since version %s) %s" %(version, msg)) warnings.warn(Warn, stacklevel=stacklevel) + +def killpopen(popen): + try: + if hasattr(popen, 'kill'): + popen.kill() + else: + killpid(popen.pid) + except EnvironmentError: + sys.stderr.write("ERROR killing: %s\n" %(sys.exc_info()[1])) + sys.stderr.flush() + +def killpid(pid): + if hasattr(os, 'kill'): + os.kill(pid, 15) + elif sys.platform == "win32": + try: + import ctypes + except ImportError: + # T: treekill, F: Force + cmd = ("taskkill /T /F /PID %d" %(pid)).split() + ret = subprocess.call(cmd) + if ret != 0: + raise EnvironmentError("taskkill returned %r" %(ret,)) + else: + PROCESS_TERMINATE = 1 + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_TERMINATE, False, pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + else: + raise EnvironmmentError("no method to kill %s" %(pid,)) --- a/testing/test_termination.py +++ b/testing/test_termination.py @@ -74,38 +74,23 @@ def test_close_initiating_remote_no_erro stdout, stderr = popen.communicate() assert not stderr -def test_double_call_to_terminate(testdir, anypython): - triggerfile = testdir.tmpdir.join("trigger") +def test_terminate_implicit_does_trykill(testdir, anypython, capfd): p = testdir.makepyfile(""" import sys sys.path.insert(0, %r) - triggerfile = %r import execnet - gw = execnet.makegateway("popen") - gw.remote_exec(''' - import os, time - while 1: - if os.path.exists(%%r): - break - time.sleep(0.2) - ''' %% triggerfile) - ok = 0 - try: - execnet.default_group.terminate(1.0) - except IOError: - try: - execnet.default_group.terminate(0.1) - except IOError: - f = open(triggerfile, 'w') - f.write("") - f.close() - execnet.default_group.terminate(5.0) - ok = 1 - if not ok: - sys.stderr.write("no-timeout!\\n") - """ % (str(execnetdir), str(triggerfile))) - popen = subprocess.Popen([str(anypython), str(p)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE,) - stdout, stderr = popen.communicate() - assert not stderr - + group = execnet.Group() + gw = group.makegateway("popen") + ch = gw.remote_exec("import time ; channel.send(1) ; time.sleep(100)") + ch.receive() # remote execution started + sys.stdout.write("1\\n") + sys.stdout.flush() + # use process at-exit group.terminate call + """ % str(execnetdir)) + popen = subprocess.Popen([str(anypython), str(p)], stdout=subprocess.PIPE) + # sync with start-up + line = popen.stdout.readline() + reply = WorkerPool(1).dispatch(popen.communicate) + reply.get(timeout=10) + out, err = capfd.readouterr() + assert not err or "Killed" in err --- a/CHANGELOG +++ b/CHANGELOG @@ -10,8 +10,9 @@ 1.0.1 (pending) popen-gateway startup can be substantially faster (>30%) - refine internal gateway exit/termination procedure - and introduce group.terminate(timeout) for timely - termination. + and introduce group.terminate(timeout) which will + attempt to kill all subprocesses that did not terminate + within time. - EOFError on channel.receive/waitclose if the other side unexpectedly went away. When a gateway exits From commits-noreply at bitbucket.org Sat Dec 5 14:39:16 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 13:39:16 +0000 (UTC) Subject: [execnet-commit] execnet commit 80baab4140de: refine and rework documentation and layout for 1.0.1 relase Message-ID: <20091205133916.3E2037EF0F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1259582636 -3600 # Node ID 80baab4140de4e421f7c872ea962291d99fd44dd # Parent b9ae9afeb79d210e10b25558dc32c5fafff195b0 refine and rework documentation and layout for 1.0.1 relase --- a/doc/examples.txt +++ b/doc/examples.txt @@ -1,5 +1,5 @@ ============================================================================== -execnet examples +examples ============================================================================== .. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev --- a/doc/install.txt +++ b/doc/install.txt @@ -2,7 +2,7 @@ Installation ==================== -Install a public `pypi release`_ via `easy_install`_ or ``pip``:: +Install a public `pypi release`_ via `easy_install`_ or pip_:: easy_install -U execnet @@ -20,11 +20,12 @@ Next checkout the basic api and examples .. toctree:: :maxdepth: 1 + examples basics - examples changelog .. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall +.. _pip: http://pypi.python.org/pypi/pip .. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet/ .. _`execnet mercurial repository`: http://bitbucket.org/hpk42/execnet/ .. _`pypi release`: http://pypi.python.org/pypi/execnet --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -1.0.1 (pending) +1.0.1 -------------------------------- - new method: gateway.hasreceiver() returns True --- a/doc/index.txt +++ b/doc/index.txt @@ -1,20 +1,26 @@ -execnet: connect your execution environments +Welcome to rapid python deployment ======================================================== .. image:: _static/pythonring.png :align: right -Execnet allows to ad-hoc connect to Python interpreters across version, platform and network barriers. It provides a minimal, fast and robust API for the following uses: +Python_ is a mature dynamic language whose interpreters can interact with +all major computing platforms today. Execnet provides carefully tested +means to ad-hoc connect to Python interpreters across version, platform +and network barriers. It provides a minimal, fast and robust API for +the following uses: * distribute tasks to multiple CPUs -* deploy hybrid applications -* manage local and remote execution environments +* write and deploy hybrid multi-process applications +* write scripts to interact a bunch of exec environments + +.. _Python: http://www.python.org Features ------------------ -* zero-install bootstrapping: no remote installation required! +* zero-install bootstrapping: no manual remote installation! * flexible communication: send/receive as well as callback/queue mechanisms supported @@ -28,15 +34,18 @@ Features * fully interoperable between Windows and Unix-ish systems. -Known users +* many tested :doc:`examples` + +Known uses ------------------- * `py.test`_ uses it for its `distributed testing`_ mechanism. * Jacob Perkins uses it for his `Distributed NTLK with execnet`_ - project. + project to launch computation processes through ssh. * Ronny Pfannschmidt uses it for his `anyvc`_ VCS-abstraction project + to bridge the Python2/Python3 version gap. * sysadmins and developers are using it for ad-hoc custom scripting @@ -48,7 +57,7 @@ Known users Project status -------------------------- -The current 1.0 series aims at `basic API`_ stabilization, improved tracing and robust termination. +The current 1.0 series aims at :doc:`basic API ` stabilization, improved tracing and robust termination. The 1.1 series will target setting up permanent networks and offering unix-shell-like capabilities to spawn processes and applications. @@ -60,3 +69,12 @@ are MIT-licensed. .. _`basic API`: basics.html .. _`actively developed`: http://bitbucket.org/hpk42/execnet/changesets .. _`Holger Krekel`: http://twitter.com/hpk42 + +.. toctree:: + :hidden: + + support + implnotes + install + rel-1.0.0 + docindex --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -54,21 +54,24 @@ you actually use the ``execnet.default_g >>> gw in execnet.default_group True -Timely termination -------------------------- +Robust Termination of ssh/popen processes +----------------------------------------------- Use ``group.terminate(timeout)`` if you want to terminate -member gateways and wait a limited time for proper termination -of all execution and IO activity:: +member gateways and ensure that no local sub processes remain +you can specify a ``timeout`` after which an attempt at killing +the related process is made:: >>> import execnet - >>> mygroup = execnet.Group() - >>> gw = mygroup.makegateway("popen") + >>> group = execnet.Group() + >>> gw = group.makegateway("popen//id=sleeper") >>> ch = gw.remote_exec("import time ; time.sleep(2.0)") - >>> try: - ... mygroup.terminate(timeout=1.0) - ... except IOError: - ... pass # subprocess-execution still sleeping - ... else: - ... print ("fail") - >>> + >>> group + + >>> group.terminate(timeout=1.0) + >>> group + + +execnet aims to provide totally robust termination so if +you have termination issues please report them via :doc:`contact <../contact>`. +thanks! --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -1,16 +1,21 @@ {% extends "!layout.html" %} {% block rootrellink %} -
  • home | 
  • -
  • basic API | 
  • -
  • examples | 
  • -
  • contact | 
  • docindex»
  • {% endblock %} {% block header %}
    -execnet + +

    execnet: robust gateways to hybrid execution environments

    + + home |  + install |  + examples |  + basic API |  + support  +
    {% endblock %} --- a/doc/example/test_remotecmd.txt +++ b/doc/example/test_remotecmd.txt @@ -1,4 +1,3 @@ -.. _command: A simple command pattern -------------------------------------------------------------- --- /dev/null +++ b/doc/support.txt @@ -0,0 +1,19 @@ +Contact and Support channels +------------------------------ + +If you have interest, questions, issues or suggestions you +are welcome to: + +* join `execnet-dev`_ for general discussions +* join `execnet-commit`_ to be notified of changes +* clone the `bitbucket repository`_ and submit patches +* hang out on the irc.freenode.net #pylib channel +* follow the `tetamap blog`_ or `Holger's twitter presence`_. +* contact merlinux_ if you want to buy teaching or other support. + +.. _`Holger's twitter presence`: http://twitter.com/hpk42 +.. _merlinux: http://merlinux.eu +.. _`tetamap blog`: http://tetamap.wordpress.com +.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit +.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet --- a/doc/basics.txt +++ b/doc/basics.txt @@ -1,12 +1,12 @@ ============================================================================== -execnet API in a nutshell +API in a nutshell ============================================================================== -execnet ad-hoc instantiates **gateways** to Python -interpreter processes though which you can **remote execute -code** and exchange basic python objects through **channels**. -All gateways are created as part of a **group** which is responsible -for managing membership and **proper termination**. +execnet ad-hoc instantiates local and remote Python interpreters. +Each interpreter is accessible through a **Gateway** which manages +code and data communication. **Channels** allow to exchange +data between the local and the remote end. **Groups** +help to manage creation and termination of sub interpreters. .. image:: _static/basic1.png @@ -106,14 +106,13 @@ two asynchronously running programs. Grouped Gateways and robust termination =============================================== -.. _`group examples`: examples.html#group .. currentmodule:: execnet.multi All created gateway instances are part of a group. If you call ``execnet.makegateway`` it actually is forwarded to the ``execnet.default_group``. Group objects are container -objects (see `group examples`_) and manage the final -termination procedure: +objects (see :doc:`group examples >`_) +and manage the final termination procedure: .. automethod:: Group.terminate(timeout=None) --- a/doc/example/test_cwd.txt +++ b/doc/example/test_cwd.txt @@ -26,7 +26,5 @@ Here is an self-contained example for re True If you'd like to avoid inlining source strings take a look -at the channelexec_ and command_ example. - -.. _channelexec: examples.html#channelexec -.. _command: examples.html#command +at the :doc:`channelexec ` +and the :doc:`command pattern` examples. --- a/doc/contact.txt +++ /dev/null @@ -1,17 +0,0 @@ -Contact channels ------------------------- - -If you have interest, questions, issues or suggestions you -are welcome to: - -* join `execnet-dev`_ for general discussions -* join `execnet-commit`_ to be notified of changes -* clone the `bitbucket repository`_ and submit patches -* hang out on the irc.freenode.net #pylib channel -* follow the `tetamap blog`_ or `Holger's twitter presence`_. - -.. _`Holger's twitter presence`: http://twitter.com/hpk42 -.. _`tetamap blog`: http://tetamap.wordpress.com -.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev -.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit -.. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet From commits-noreply at bitbucket.org Sat Dec 5 17:10:11 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 16:10:11 +0000 (UTC) Subject: [execnet-commit] execnet commit 9ec10766044b: refine group-membership handling: index by integer, don't use weakref Message-ID: <20091205161011.39B4C7EF0F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260027855 -3600 # Node ID 9ec10766044b9b74a4e0712f0abc16829d4e4ae2 # Parent e07cbeaf8e3bca3be4ee1e966e193522a0409f42 refine group-membership handling: index by integer, don't use weakref because gateways do not work with implicit GC-related termination anyway (channels do, however). --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -35,11 +35,10 @@ class Gateway(gateway_base.BaseGateway): group.terminate() is called. """ self._trace("gateway.exit() called") - try: - self._group._unregister(self) - except KeyError: + if self not in self._group: self._trace("gateway already unregistered with group") return + self._group._unregister(self) self._trace("--> sending GATEWAY_TERMINATE") try: self._send(Message.GATEWAY_TERMINATE(0, '')) --- a/execnet/multi.py +++ b/execnet/multi.py @@ -4,7 +4,7 @@ Managing Gateway Groups and interactions (c) 2008-2009, Holger Krekel and others """ -import os, sys, weakref, atexit +import os, sys, atexit import execnet from execnet import XSpec from execnet import gateway @@ -17,33 +17,37 @@ class Group: def __init__(self, xspecs=()): """ initialize group and make gateways as specified. """ # Gateways may evolve to become GC-collectable - self._activegateways = weakref.WeakKeyDictionary() - self._id2gateway = weakref.WeakValueDictionary() - self._autoidcounter = 1 + self._gateways = [] + self._autoidcounter = 0 self._gateways_to_join = [] for xspec in xspecs: self.makegateway(xspec) atexit.register(self._cleanup_atexit) def __repr__(self): - keys = list(self._id2gateway) - keys.sort() - return "" %(keys,) + idgateways = [gw.id for gw in self] + return "" %(idgateways) def __getitem__(self, key): - return self._id2gateway[key] + if isinstance(key, int): + return self._gateways[key] + for gw in self._gateways: + if gw == key or gw.id == key: + return gw + raise KeyError(key) def __contains__(self, key): - return key in self._id2gateway or key in self._activegateways + try: + self[key] + return True + except KeyError: + return False def __len__(self): - return len(self._activegateways) + return len(self._gateways) def __iter__(self): - l = list(self._id2gateway.items()) - l.sort() - for id, gw in l: - yield gw + return iter(list(self._gateways)) def makegateway(self, spec): """ create and configure a gateway to a Python interpreter @@ -67,7 +71,7 @@ class Group: "socket: specifying python executables not yet supported") gateway_id = spec.installvia if gateway_id: - viagw = self._id2gateway[gateway_id] + viagw = self[gateway_id] gw = gateway.SocketGateway.new_remote(viagw, id=id) else: host, port = spec.socket.split(":") @@ -94,23 +98,21 @@ class Group: def _allocate_id(self, id=None): if id is None: - id = str(self._autoidcounter) + id = "gw" + str(self._autoidcounter) self._autoidcounter += 1 - assert id not in self._id2gateway + if id in self: + raise ValueError("already have member gateway with id %r" %(id,)) return id def _register(self, gateway): assert not hasattr(gateway, '_group') assert gateway.id - assert id not in self._id2gateway - assert gateway not in self._activegateways - self._activegateways[gateway] = True - self._id2gateway[gateway.id] = gateway + assert id not in self + self._gateways.append(gateway) gateway._group = self def _unregister(self, gateway): - del self._id2gateway[gateway.id] - del self._activegateways[gateway] + self._gateways.remove(gateway) self._gateways_to_join.append(gateway) def _cleanup_atexit(self): @@ -153,7 +155,7 @@ class Group: MultiChannel connecting to all sub processes. """ channels = [] - for gw in list(self._activegateways): + for gw in self: channels.append(gw.remote_exec(source)) return MultiChannel(channels) --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -102,8 +102,10 @@ class TestGroup: group = Group() gw = group.makegateway("popen") assert list(group) == [gw] + assert group[0] == gw + assert len(group) == 1 group._cleanup_atexit() - assert not group._activegateways + assert not group._gateways def test_group_ordering_and_termination(self): group = Group() @@ -113,8 +115,10 @@ class TestGroup: gwlist = list(group) assert len(gwlist) == 3 idlist = [x.id for x in gwlist] - assert idlist == list('235') + assert idlist == list('325') + print group group.terminate() + print group assert not group assert repr(group) == "" --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ 1.0.1 - new: execnet.MultiChannel provides basic iteration/contain interface +- new: execnet.Group can be indexed by integer + - have popen-gateways use imports instead of source-strings, also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) From commits-noreply at bitbucket.org Sat Dec 5 17:10:12 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 16:10:12 +0000 (UTC) Subject: [execnet-commit] execnet commit 835e81022d0e: group.makegateway() uses group.default_spec if no spec is given Message-ID: <20091205161012.DEE987EF10@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260029386 -3600 # Node ID 835e81022d0e495086b2b7be3edc007eca636f51 # Parent 9ec10766044b9b74a4e0712f0abc16829d4e4ae2 group.makegateway() uses group.default_spec if no spec is given and the execnet.default_group uses ``popen`` as a default spec. This makes a simple execnet.makegateway() return a subprocess/popen-gateway. --- a/execnet/multi.py +++ b/execnet/multi.py @@ -14,6 +14,7 @@ NO_ENDMARKER_WANTED = object() class Group: """ Gateway Groups. """ + defaultspec = XSpec("popen") def __init__(self, xspecs=()): """ initialize group and make gateways as specified. """ # Gateways may evolve to become GC-collectable @@ -49,15 +50,26 @@ class Group: def __iter__(self): return iter(list(self._gateways)) - def makegateway(self, spec): - """ create and configure a gateway to a Python interpreter - specified by a 'execution specification' string. - The format of the string generally is:: + def makegateway(self, spec=None): + """ create and configure a gateway to a Python interpreter. + + The ``spec`` string encodes the target gateway type + and configuration information. The general format is:: key1=value1//key2=value2//... If you leave out the ``=value`` part a True value is assumed. + Valid types: ``popen``, ``ssh=hostname``, ``socket=host:port``. + Valid configuration:: + id= specifies the gateway id + python= specifies which python interpreter to execute + chdir= specifies to which directory to change + nice= specifies process priority of new process + + If no spec is given, self.defaultspec is used. """ + if not spec: + spec = self.defaultspec if not isinstance(spec, XSpec): spec = XSpec(spec) id = self._allocate_id(spec.id) --- a/testing/test_xspec.py +++ b/testing/test_xspec.py @@ -60,8 +60,9 @@ class TestMakegateway: def test_no_type(self): py.test.raises(ValueError, "execnet.makegateway('hello')") - def test_popen(self): - gw = execnet.makegateway("popen") + def test_popen_default(self): + gw = execnet.makegateway("") + assert gw.spec.popen assert gw.spec.python == None rinfo = gw._rinfo() assert rinfo.executable == py.std.sys.executable --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -98,6 +98,11 @@ class TestGroup: assert len(exitlist) == 1 assert len(joinlist) == 1 + def test_group_default_spec(self): + group = Group() + group.defaultspec = "not-existing-type" + py.test.raises(ValueError, group.makegateway) + def test_group_PopenGateway(self): group = Group() gw = group.makegateway("popen") --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,9 @@ 1.0.1 - new: execnet.Group can be indexed by integer +- new: group.makegateway() uses group.default_spec if no spec is given + and the execnet.default_group uses ``popen`` as a default spec. + - have popen-gateways use imports instead of source-strings, also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) From commits-noreply at bitbucket.org Sat Dec 5 17:10:14 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 16:10:14 +0000 (UTC) Subject: [execnet-commit] execnet commit e07cbeaf8e3b: introduce basic container API to multichannels Message-ID: <20091205161014.7DC817EF11@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260025425 -3600 # Node ID e07cbeaf8e3bca3be4ee1e966e193522a0409f42 # Parent 80baab4140de4e421f7c872ea962291d99fd44dd introduce basic container API to multichannels --- a/execnet/multi.py +++ b/execnet/multi.py @@ -161,6 +161,18 @@ class MultiChannel: def __init__(self, channels): self._channels = channels + def __len__(self): + return len(self._channels) + + def __iter__(self): + return iter(self._channels) + + def __getitem__(self, key): + return self._channels[key] + + def __contains__(self, chan): + return chan in self._channels + def send_each(self, item): for ch in self._channels: ch.send(item) --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -4,8 +4,22 @@ import execnet import py +from execnet.gateway_base import Channel class TestMultiChannelAndGateway: + def test_multichannel_container_basics(self): + mch = execnet.MultiChannel([Channel(None, i) for i in range(3)]) + assert len(mch) == 3 + channels = list(mch) + assert len(channels) == 3 + # ordering + for i in range(3): + assert channels[i].id == i + assert channels[i] == mch[i] + assert channels[0] in mch + assert channels[1] in mch + assert channels[2] in mch + def test_multichannel_receive_each(self): class pseudochannel: def receive(self): --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ 1.0.1 if the gateway is still receive-active. remote_status now only carries information about remote execution status. +- new: execnet.MultiChannel provides basic iteration/contain interface + - have popen-gateways use imports instead of source-strings, also improves debugging/tracebacks, as a side effect popen-gateway startup can be substantially faster (>30%) From commits-noreply at bitbucket.org Sat Dec 5 18:33:48 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 17:33:48 +0000 (UTC) Subject: [execnet-commit] execnet commit 2233f21d1d91: revamp and add docs Message-ID: <20091205173348.1CB6C7EF10@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260034398 -3600 # Node ID 2233f21d1d91e0d52ba6c9fe4896b02285d85aa7 # Parent 835e81022d0e495086b2b7be3edc007eca636f51 revamp and add docs --- a/doc/example/test_channelexec.txt +++ /dev/null @@ -1,24 +0,0 @@ -.. _channelexec: - -remote execute modules using their __name__ --------------------------------------------------------------- - -You can pass a module object to ``remote_exec`` in which case -its source code will be sent. No dependencies will be transferred -so the module must be self-contained or only use modules that are -installed on the "other" side. Module code can detect if it is -running in a remote_exec situation by checking for the special -``__name__`` attribute. - -.. include:: remote1.py - :literal: - -You can now remote-execute the module like this:: - - >>> import execnet, remote1 - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec(remote1) - >>> print (ch.receive()) - initialization complete - -which will print the 'initialization complete' string. --- /dev/null +++ b/doc/example/hybridpython.txt @@ -0,0 +1,95 @@ +Connecting different Python interpreters +========================================== + +Connect to Python2/Numpy from Python3 +---------------------------------------- + +Here we run a Python3 interpreter to connect to a Python2.6 interpreter +that has numpy installed. We send items to be added to an array and +receive back the remote "repr" of the array:: + + import execnet + gw = execnet.makegateway("popen//python=python2.6") + channel = gw.remote_exec(""" + import numpy + array = numpy.array([1,2,3]) + while 1: + x = channel.receive() + if x is None: + break + array = numpy.append(array, x) + channel.send(repr(array)) + """) + for x in range(10): + channel.send(x) + channel.send(None) + print (channel.receive()) + +will print on the CPython3.1 side:: + + array([1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +A more refined real-life example of python3/python2 interaction +is the anyvc_ project which uses version-control bindings in +a Python2 subprocess in order to offer Python3-based library +functionality. + +.. _anyvc: http://bitbucket.org/RonnyPfannschmidt/anyvc/overview/ + +Work with Java objects from CPython +---------------------------------------- + +Use your CPython interpreter to connect to a `Jython 2.5.1`_ interpreter +and work with Java types:: + + import execnet + gw = execnet.makegateway("popen//python=jython") + channel = gw.remote_exec(""" + from java.util import Vector + v = Vector() + v.add('aaa') + v.add('bbb') + for val in v: + channel.send(val) + """) + + for item in channel: + print (item) + +will print on the CPython side:: + + aaa + bbb + +.. _`Jython 2.5.1`: http://www.jython.org + +Work with C# objects from CPython +---------------------------------------- + +(Experimental) use your CPython interpreter to connect to a IronPython_ interpreter +which can work with C# classes. Here is an example for instantiating +a CLR Array instance and sending back its representation:: + + import execnet + gw = execnet.makegateway("popen//python=ipy") + + channel = gw.remote_exec(""" + import clr + clr.AddReference("System") + from System import Array + array = Array[float]([1,2]) + channel.send(str(array)) + """) + print (channel.receive()) + +using Mono 2.0 and IronPython-1.1 this will print on the CPython side:: + + System.Double[](1.0, 2.0) + +.. note:: + Using IronPython needs more testing, likely newer versions + will work better. please feedback if you have information. + +.. _IronPython: http://www.IronPython.org + + --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 1.0.1 -------------------------------- +- revamp and better structure documentation + - new method: gateway.hasreceiver() returns True if the gateway is still receive-active. remote_status now only carries information about remote execution status. --- a/doc/index.txt +++ b/doc/index.txt @@ -1,36 +1,37 @@ -Welcome to rapid python deployment +Welcome to rapid Python deployment ======================================================== .. image:: _static/pythonring.png :align: right Python_ is a mature dynamic language whose interpreters can interact with -all major computing platforms today. Execnet provides carefully tested -means to ad-hoc connect to Python interpreters across version, platform -and network barriers. It provides a minimal, fast and robust API for -the following uses: +all major computing platforms today. -* distribute tasks to multiple CPUs +**execnet** provides carefully tested means to ad-hoc interact with Python +interpreters across version, platform and network barriers. It provides +a minimal and fast API targetting the following uses: + +* distribute tasks to local or remote CPUs * write and deploy hybrid multi-process applications -* write scripts to interact a bunch of exec environments +* write scripts to administer a bunch of exec environments .. _Python: http://www.python.org Features ------------------ -* zero-install bootstrapping: no manual remote installation! +* automatic bootstrapping: no manual remote installation. -* flexible communication: send/receive as well as +* safe and simple serialization of python builtin types (no pickle) + +* flexible communication: synchronous send/receive as well as callback/queue mechanisms supported -* simple serialization of python builtin types (no pickling) +* easy creation, handling and termination of multiple processes -* grouped creation and robust termination of processes - -* well tested between CPython 2.4-3.1, Jython 2.5.1 and PyPy 1.1 - interpreters. +* well tested interactions between CPython 2.4-3.1, Jython 2.5.1 + and PyPy 1.1 interpreters. * fully interoperable between Windows and Unix-ish systems. --- a/execnet/multi.py +++ b/execnet/multi.py @@ -14,7 +14,7 @@ NO_ENDMARKER_WANTED = object() class Group: """ Gateway Groups. """ - defaultspec = XSpec("popen") + defaultspec = "popen" def __init__(self, xspecs=()): """ initialize group and make gateways as specified. """ # Gateways may evolve to become GC-collectable --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -1,29 +1,26 @@ -.. _group: - -Creating and working with grouped Gateways +Managing multiple gateways and clusters +================================================== + +Usings Groups for managing multiple gateways ------------------------------------------------------ -Use ``execnet.Group`` to manage the lifetime of multiple -gateways and perform iteration and membership -checks:: +Use ``execnet.Group`` to manage membership and lifetime of +of multiple gateways:: >>> import execnet - >>> group = execnet.Group() - >>> group.makegateway("popen") - - >>> group.makegateway("popen") - - >>> group - - >>> list(group) - [, ] + >>> group = execnet.Group(['popen'] * 2) >>> len(group) 2 - >>> '1' in group and '2' in group + >>> group + + >>> list(group) + [, ] + >>> 'gw0' in group and 'gw1' in group True - >>> group['1'] - - + >>> group['gw0'] == group[0] + True + >>> group['gw1'] == group[1] + True >>> group.terminate() # exit all member gateways >>> group @@ -43,16 +40,18 @@ Pass an ``id=MYNAME`` part to ``group.ma -Gateways live in a default_group +execnet.makegateway uses execnet.default_group ------------------------------------------------------ Each time you create a gateway with ``execnet.makegateway()`` you actually use the ``execnet.default_group``:: >>> import execnet - >>> gw = execnet.makegateway("popen") + >>> gw = execnet.makegateway() >>> gw in execnet.default_group True + >>> gw.defaultspec # used for empty makegateway() calls + "popen" Robust Termination of ssh/popen processes ----------------------------------------------- @@ -73,5 +72,5 @@ the related process is made:: execnet aims to provide totally robust termination so if -you have termination issues please report them via :doc:`contact <../contact>`. -thanks! +you have left-over processes or other termination issues +please :doc:`report them <../support>`. thanks! --- a/doc/example/test_syncreceive2.txt +++ /dev/null @@ -1,14 +0,0 @@ - -Synchronously receive results from two sub processes ------------------------------------------------------ - -Use MultiChannels for receiving multiple results from remote code:: - - >>> import execnet - >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") - >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") - >>> mch = execnet.MultiChannel([ch1, ch2]) - >>> l = mch.receive_each() - >>> assert len(l) == 2 - >>> assert 1 in l - >>> assert 2 in l --- /dev/null +++ b/doc/example/test_multi.txt @@ -0,0 +1,75 @@ +advanced (multi) channel communication +===================================================== + +MultiChannel: container for multiple channels +------------------------------------------------------ + +Use ``execnet.MultiChannel`` to work with multiple channels:: + + >>> import execnet + >>> ch1 = execnet.makegateway().remote_exec("channel.send(1)") + >>> ch2 = execnet.makegateway().remote_exec("channel.send(2)") + >>> mch = execnet.MultiChannel([ch1, ch2]) + >>> len(mch) + 2 + >>> mch[0] is ch1 and mch[1] is ch2 + True + >>> ch1 in mch and ch2 in mch + True + >>> sum(mch.receive_each()) + 3 + +receive results from sub processes with a Queue +----------------------------------------------------- + +Use ``MultiChannel.make_receive_queue()`` to get a queue +from which to obtain results:: + + >>> ch1 = execnet.makegateway().remote_exec("channel.send(1)") + >>> ch2 = execnet.makegateway().remote_exec("channel.send(2)") + >>> mch = execnet.MultiChannel([ch1, ch2]) + >>> queue = mch.make_receive_queue() + >>> chan1, res1 = queue.get() + >>> chan2, res2 = queue.get(timeout=3) + >>> res1 + res2 + 3 + +Working asynchronously/event-based with channels +--------------------------------------------------- + +Use channel callbacks if you want to process incoming +data immediately and without blocking execution:: + + >>> import execnet + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec("channel.receive() ; channel.send(42)") + >>> l = [] + >>> ch.setcallback(l.append) + >>> ch.send(1) + >>> ch.waitclose() + >>> assert l == [42] + +Note that the callback function will be executed in the +receiver thread and should not block or run for too long. + +robustly receive results and termination notification +----------------------------------------------------- + +Use ``MultiChannel.make_receive_queue(endmarker)`` to specify +an object to be put to the queue when the remote side of a channel +is closed. The endmarker will also be put to the Queue if the gateway +is blocked in execution and is terminated/killed:: + + >>> group = execnet.Group(['popen'] * 3) # create three gateways + >>> mch = group.remote_exec("channel.send(channel.receive()+1)") + >>> queue = mch.make_receive_queue(endmarker=42) + >>> mch[0].send(1) + >>> chan1, res1 = queue.get() + >>> res1 + 2 + >>> group.terminate(timeout=1) # kill processes waiting on receive + >>> for i in range(3): + ... chan1, res1 = queue.get() + ... assert res1 == 42 + >>> group + --- /dev/null +++ b/doc/example/test_info.txt @@ -0,0 +1,104 @@ +basic local and remote communication +========================================= + +Comparing current working directories +---------------------------------------- + +A popen gateway has the same working directory as the instantiatior:: + + >>> import execnet, os + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") + >>> res = ch.receive() + >>> assert res == os.getcwd() + +"ssh" gateways default to the login home directory. + +Getting information from remote ssh account +-------------------------------------------- + +Use simple execution to obtain information from remote environments:: + + >>> import execnet, os + >>> gw = execnet.makegateway("ssh=codespeak.net") + >>> channel = gw.remote_exec(""" + ... import sys, os + ... channel.send((sys.platform, sys.version_info, os.getpid())) + ... """) + >>> platform, version_info, remote_pid = channel.receive() + >>> platform + 'linux2' + >>> version_info + (2, 4, 2, 'final', 0) + + +Avoid "inlined" source strings with remote_exec +-------------------------------------------------------------- + +You can pass a module object to ``remote_exec`` in which case +its source code will be sent. No dependencies will be transferred +so the module must be self-contained or only use modules that are +installed on the "other" side. Module code can detect if it is +running in a remote_exec situation by checking for the special +``__name__`` attribute. + +.. include:: remote1.py + :literal: + +You can now remote-execute the module like this:: + + >>> import execnet, remote1 + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remote1) + >>> print (ch.receive()) + initialization complete + +which will print the 'initialization complete' string. + +a simple command loop pattern +-------------------------------------------------------------- + +If you want the remote side to serve a number +of synchronous function calls into your module +you can setup a serving loop and write a local protocol. + +.. include:: remotecmd.py + :literal: + +Then on the local side you can do:: + + >>> import execnet, remotecmd + >>> gw = execnet.PopenGateway() + >>> ch = gw.remote_exec(remotecmd) + >>> ch.send('simple(10)') # execute func-call remotely + >>> ch.receive() + 11 + +Our remotecmd module starts up remote serving +through the ``for item in channel`` loop which +will terminate when the channel closes. It evaluates +all incoming requests in the global name space and +sends back the results. + + +Instantiate gateways through sockets +----------------------------------------------------- + +.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/80baab4140de/execnet/script/socketserver.py + +In cases where you do not have SSH-access to a machine +you need to download a small version-independent standalone +`socketserver.py`_ script to provide a remote bootstrapping-point. +You do not need to install the execnet package remotely. +Simply run the script like this:: + + python socketserver.py :8888 # bind to all IPs, port 8888 + +You can then instruct execnet on your local machine to bootstrap +itself into the remote socket endpoint: + + import execnet + gw = execnet.SocketGateway("TARGET-IP:8888") + +That's it, you can now use the gateway object just like +a popen- or ssh-based one. --- a/doc/example/test_connect.txt +++ /dev/null @@ -1,93 +0,0 @@ - -Connect to Python2/Numpy from Python3 ----------------------------------------- - -Here we run a Python3 interpreter to connect to a Python2.6 interpreter -that has numpy installed. We send items to be added to an array and -receive back the remote "repr" of the array:: - - import execnet - gw = execnet.makegateway("popen//python=python2.6") - channel = gw.remote_exec(""" - import numpy - array = numpy.array([1,2,3]) - while 1: - x = channel.receive() - if x is None: - break - array = numpy.append(array, x) - channel.send(repr(array)) - """) - for x in range(10): - channel.send(x) - channel.send(None) - print (channel.receive()) - -will print on the CPython3.1 side:: - - array([1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - -A more refined real-life example of python3/python2 interaction -is the anyvc_ project which uses version-control bindings in -a Python2 subprocess in order to offer Python3-based library -functionality. - -.. _anyvc: http://bitbucket.org/RonnyPfannschmidt/anyvc/overview/ - -Work with Java objects from CPython ----------------------------------------- - -Use your CPython interpreter to connect to a `Jython 2.5.1`_ interpreter -and work with Java types:: - - import execnet - gw = execnet.makegateway("popen//python=jython") - channel = gw.remote_exec(""" - from java.util import Vector - v = Vector() - v.add('aaa') - v.add('bbb') - for val in v: - channel.send(val) - """) - - for item in channel: - print (item) - -will print on the CPython side:: - - aaa - bbb - -.. _`Jython 2.5.1`: http://www.jython.org - -Work with C# objects from CPython ----------------------------------------- - -(Experimental) use your CPython interpreter to connect to a IronPython_ interpreter -which can work with C# classes. Here is an example for instantiating -a CLR Array instance and sending back its representation:: - - import execnet - gw = execnet.makegateway("popen//python=ipy") - - channel = gw.remote_exec(""" - import clr - clr.AddReference("System") - from System import Array - array = Array[float]([1,2]) - channel.send(str(array)) - """) - print (channel.receive()) - -using Mono 2.0 and IronPython-1.1 this will print on the CPython side:: - - System.Double[](1.0, 2.0) - -.. note:: - Using IronPython needs more testing, likely newer versions - will work better. please feedback if you have information. - -.. _IronPython: http://www.IronPython.org - - --- a/doc/examples.txt +++ b/doc/examples.txt @@ -5,18 +5,13 @@ examples .. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev .. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit -execnet is under active development. Checkout `execnet-dev`_ -and `execnet-commit`_. +Note: all examples with `>>>` prompts are automatically tested. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 - example/test_connect - example/test_cwd.txt - example/test_channelexec.txt - example/test_remotecmd.txt + example/test_info.txt example/test_group.txt - example/test_syncreceive2.txt - example/test_asyncreceive2.txt - example/test_ssh_fileserver.txt - example/test_installvia.txt + example/test_multi.txt + example/hybridpython.txt + example/test_debug.txt --- a/doc/example/test_remotecmd.txt +++ /dev/null @@ -1,25 +0,0 @@ - -A simple command pattern --------------------------------------------------------------- - -If you want the remote side to serve a number -of synchronous function you can setup a serving -loop and invent your own local protocol - -.. include:: remotecmd.py - :literal: - -Then on the local side you can do:: - - >>> import execnet, remotecmd - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec(remotecmd) - >>> ch.send('simple(10)') # execute func-call remotely - >>> ch.receive() - 11 - -Our remotecmd module starts up remote serving -through the ``for item in channel`` loop which -will terminate when the channel closes. It evaluates -all incoming requests in the global name space and -sends back the results. --- a/doc/example/test_installvia.txt +++ /dev/null @@ -1,16 +0,0 @@ - -Instantiate a socket server in a new subprocess ------------------------------------------------------ - -(experimental) The following example opens a PopenGateway, i.e. a python -child process, and starts a socket server within that process -and then opens a second gateway to the freshly started -socketserver:: - - import execnet - - popengw = execnet.PopenGateway() - socketgw = execnet.SocketGateway.new_remote(popengw, ("127.0.0.1", 0)) - - print socketgw.remote_status() # print some info about the remote environment - --- a/doc/example/test_asyncreceive2.txt +++ /dev/null @@ -1,21 +0,0 @@ - -Asynchronously receive results from two sub processes ------------------------------------------------------ - -Use ``MultiChannel.make_receive_queue()`` for asynchronously receiving -multiple results from remote code. This standard Queue provides -``(channel, result)`` tuples which allows to determine where -a result comes from:: - - >>> import execnet - >>> ch1 = execnet.PopenGateway().remote_exec("channel.send(1)") - >>> ch2 = execnet.PopenGateway().remote_exec("channel.send(2)") - >>> mch = execnet.MultiChannel([ch1, ch2]) - >>> queue = mch.make_receive_queue() - >>> chan1, res1 = queue.get() # you may also specify a timeout - >>> chan2, res2 = queue.get() - >>> res1 + res2 - 3 - >>> assert chan1 in (ch1, ch2) - >>> assert chan2 in (ch1, ch2) - >>> assert chan1 != chan2 --- a/doc/basics.txt +++ b/doc/basics.txt @@ -10,7 +10,7 @@ help to manage creation and termination .. image:: _static/basic1.png -Gateways: connecting to another Python Interpreter +Gateways: bootstrapping Python interpreters =================================================== .. currentmodule:: execnet @@ -18,13 +18,15 @@ Gateways: connecting to another Python I All Gateways are instantiated via a call to ``makegateway()`` passing it a gateway specification or URL. -.. autofunction:: execnet.makegateway(xspec) +.. _xspec: + +.. autofunction:: execnet.makegateway(spec) Here is an example which instantiates a simple Python subprocess:: >>> gateway = execnet.makegateway("popen") -You can use a gateway to `remote execute code`_ and +gateways allow to `remote execute code`_ and `exchange data`_ bidirectionally. examples for valid gateway specifications @@ -38,26 +40,8 @@ examples for valid gateway specification subprocess; running with the lowest CPU priority ("nice" level). By default current dir will be the current dir of the instantiator. -* ``socket=192.168.1.4:8888`` specifies of a Python Socket server - process that listens on 192.168.1.4:8888; current dir will be the - 'pyexecnet-cache' sub directory which is used a default for all remote - processes. - -.. _xspec: - -recognized Gateway URL parts ---------------------------------------- - -The following parameters are recognized as part of a gateway specification:: - -* ``popen`` for a PopenGateway. -* ``ssh=host`` for a SshGateway to the given host. -* ``socket=address:port`` for a SocketGateway at the given address. -* ``id=NAME`` set the ``id`` of the gateway, must be unique within Group_ -* ``python=executable`` for specifying Python Interpreter executables -* ``chdir=path`` change remote working dir to given relative or absolute path -* ``nice=value`` decrease remote nice level if platforms supports it - +* ``socket=192.168.1.4:8888`` specifies a Python Socket server + process that listens on 192.168.1.4:8888`` .. _`remote execute code`: @@ -90,8 +74,6 @@ Channels: exchanging data with remote co A channel object allows to send and receive data between two asynchronously running programs. -.. class:: Channel - .. automethod:: Channel.send(item) .. automethod:: Channel.receive(timeout) .. automethod:: Channel.setcallback(callback, endmarker=_NOENDMARKER) @@ -111,7 +93,7 @@ Grouped Gateways and robust termination All created gateway instances are part of a group. If you call ``execnet.makegateway`` it actually is forwarded to the ``execnet.default_group``. Group objects are container -objects (see :doc:`group examples >`_) +objects (see :doc:`group examples `) and manage the final termination procedure: .. automethod:: Group.terminate(timeout=None) @@ -135,10 +117,9 @@ information from the remote side. Calling this method tells you e.g. how many execution tasks are queued, how many are executing and how many -channels are active. Note that remote_status() -works even if the other side's execution thread -is blocked and can thus be used to query status -in a non-blocking manner. +channels are active. Note that ``remote_status()`` +works even if the other side is busy executing code +and can thus be used to query status in a non-blocking manner. rsync: synchronise filesystem with remote =============================================================== --- a/doc/example/test_cwd.txt +++ /dev/null @@ -1,30 +0,0 @@ -Compare cwd() of Popen Gateways ----------------------------------------- - -A PopenGateway has the same working directory as the instantiatior:: - - >>> import execnet, os - >>> gw = execnet.PopenGateway() - >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") - >>> res = ch.receive() - >>> assert res == os.getcwd() - - -Getting the remote process ID --------------------------------------- - -Here is an self-contained example for reading a remote process identifier. - - >>> import execnet, os - >>> gw = execnet.makegateway("popen") - >>> channel = gw.remote_exec(""" - ... import os - ... channel.send(os.getpid()) - ... """) - >>> remote_pid = channel.receive() - >>> remote_pid != os.getpid() - True - -If you'd like to avoid inlining source strings take a look -at the :doc:`channelexec ` -and the :doc:`command pattern` examples. From commits-noreply at bitbucket.org Sat Dec 5 19:29:01 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 18:29:01 +0000 (UTC) Subject: [execnet-commit] execnet commit 66760dc71203: finalizing bits for 1.0.1 Message-ID: <20091205182901.5609D7EF0C@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260036340 -3600 # Node ID 66760dc71203dbceeb08851cf4cf74e2687156d0 # Parent 2233f21d1d91e0d52ba6c9fe4896b02285d85aa7 finalizing bits for 1.0.1 --- a/doc/Makefile +++ b/doc/Makefile @@ -30,6 +30,9 @@ help: clean: -rm -rf $(BUILDDIR)/* +install: clean html linkcheck + rsync -avz $(BUILDDIR)/html/ code:www-execnet/trunk/ + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo --- a/doc/index.txt +++ b/doc/index.txt @@ -1,6 +1,4 @@ -Welcome to rapid Python deployment -======================================================== .. image:: _static/pythonring.png :align: right @@ -12,7 +10,7 @@ all major computing platforms today. interpreters across version, platform and network barriers. It provides a minimal and fast API targetting the following uses: -* distribute tasks to local or remote CPUs +* distribute tasks to local or remote CPUs * write and deploy hybrid multi-process applications * write scripts to administer a bunch of exec environments @@ -78,4 +76,3 @@ are MIT-licensed. implnotes install rel-1.0.0 - docindex --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -105,8 +105,11 @@ class Popen2IO: self.outfile, self.infile = outfile, infile if sys.platform == "win32": import msvcrt - msvcrt.setmode(infile.fileno(), os.O_BINARY) - msvcrt.setmode(outfile.fileno(), os.O_BINARY) + try: + msvcrt.setmode(infile.fileno(), os.O_BINARY) + msvcrt.setmode(outfile.fileno(), os.O_BINARY) + except (AttributeError, IOError): + pass def read(self, numbytes): """Read exactly 'numbytes' bytes from the pipe. """ --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -50,8 +50,8 @@ you actually use the ``execnet.default_g >>> gw = execnet.makegateway() >>> gw in execnet.default_group True - >>> gw.defaultspec # used for empty makegateway() calls - "popen" + >>> execnet.default_group.defaultspec # used for empty makegateway() calls + 'popen' Robust Termination of ssh/popen processes ----------------------------------------------- --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include CHANGELOG +include conftest.py include README.txt include setup.py include LICENSE --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -1,14 +1,14 @@ {% extends "!layout.html" %} {% block rootrellink %} -
  • docindex»
  • + {% endblock %} {% block header %}
    -

    execnet: robust gateways to hybrid execution environments

    +

    execnet: rapid multi-Python deployment

    home |  install |  --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.1a" +__version__ = "1.0.1" import execnet.apipkg --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -8,7 +8,7 @@ def test_subprocess_interaction(anypytho line = gateway.popen_bootstrapline compile(line, 'xyz', 'exec') args = [str(anypython), '-c', line] - popen = subprocess.Popen(args, bufsize=0, + popen = subprocess.Popen(args, bufsize=0, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) def send(line): popen.stdin.write(line.encode('ascii')) --- a/doc/docindex.txt +++ /dev/null @@ -1,10 +0,0 @@ - -Documentation Index -============================ - -.. toctree:: - :maxdepth: 2 - - basics - examples - changelog --- a/setup.py +++ b/setup.py @@ -1,15 +1,16 @@ """ -execnet: connect your execution environments +execnet: rapid multi-Python deployment ======================================================== -.. image:: _static/pythonring.png - :align: right +.. _execnet: http://codespeak.net/execnet -Execnet allows to ad-hoc connect to Python interpreters across version, platform and network barriers. It provides a minimal, fast and robust API for the following uses: +execnet_ provides carefully tested means to ad-hoc interact with Python +interpreters across version, platform and network barriers. It provides +a minimal and fast API targetting the following uses: -* distribute tasks to multiple CPUs -* deploy hybrid applications -* manage local and remote execution environments +* distribute tasks to local or remote CPUs +* write and deploy hybrid multi-process applications +* write scripts to administer a bunch of exec environments Features ------------------ @@ -39,7 +40,7 @@ from execnet import __version__ def main(): setup( name='execnet', - description='execnet: connect your execution environments', + description='execnet: rapid multi-Python deployment', long_description = __doc__, version= __version__, url='http://codespeak.net/execnet', From commits-noreply at bitbucket.org Sat Dec 5 19:29:08 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 5 Dec 2009 18:29:08 +0000 (UTC) Subject: [execnet-commit] execnet commit e77a2974394b: Added tag 1.0.1 for changeset 66760dc71203 Message-ID: <20091205182908.784C27EF0D@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260037737 -3600 # Node ID e77a2974394bd0c6bc18f6c2f90f436be51b90ef # Parent 66760dc71203dbceeb08851cf4cf74e2687156d0 Added tag 1.0.1 for changeset 66760dc71203 --- a/.hgtags +++ b/.hgtags @@ -2,3 +2,4 @@ 39e4b3a1397adad17ecfcd9e65a08521d90b2795 7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 5d1c438e335dc369834207882ec5f3227d365c11 1.0.0 +66760dc71203dbceeb08851cf4cf74e2687156d0 1.0.1 From commits-noreply at bitbucket.org Sun Dec 6 18:56:35 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 6 Dec 2009 17:56:35 +0000 (UTC) Subject: [execnet-commit] execnet commit de6284add5db: avoid deprecated PopenGateway, new Group.defaultspec example, some doc tweaks Message-ID: <20091206175635.422797EE74@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260118830 -3600 # Node ID de6284add5db8329f9b57fc44026b6f031771782 # Parent e77a2974394bd0c6bc18f6c2f90f436be51b90ef avoid deprecated PopenGateway, new Group.defaultspec example, some doc tweaks --- a/doc/Makefile +++ b/doc/Makefile @@ -31,7 +31,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html linkcheck - rsync -avz $(BUILDDIR)/html/ code:www-execnet/trunk/ + rsync -avz $(BUILDDIR)/html/ code:www-execnet/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html --- a/doc/install.txt +++ b/doc/install.txt @@ -13,7 +13,7 @@ Install a public `pypi release`_ via `ea or hg clone https://hpk42 at bitbucket.org/hpk42/execnet/ - python setup.py install # or add checkout path to PYTHONPATH directly + python setup.py install # or 'develop' or add checkout path to PYTHONPATH Next checkout the basic api and examples: --- a/doc/example/popen_read_multiple.py +++ b/doc/example/popen_read_multiple.py @@ -9,7 +9,7 @@ NUM_PROCESSES = 5 channels = [] for i in range(NUM_PROCESSES): - gw = execnet.PopenGateway() # or use SSH or socket gateways + gw = execnet.makegateway() # or use SSH or socket gateways channel = gw.remote_exec(""" import time secs = channel.receive() --- a/doc/example/test_group.txt +++ b/doc/example/test_group.txt @@ -74,3 +74,25 @@ the related process is made:: execnet aims to provide totally robust termination so if you have left-over processes or other termination issues please :doc:`report them <../support>`. thanks! + + +Using Groups to manage a certain type of gateway +------------------------------------------------------ + +Set ``group.defaultspec`` to determine the default gateway +specification used by ``group.makegateway()``: + + >>> import execnet + >>> group = execnet.Group() + >>> group.defaultspec = "ssh=localhost//chdir=mytmp//nice=20" + >>> gw = group.makegateway() + >>> ch = gw.remote_exec(""" + ... import os.path + ... basename = os.path.basename(os.getcwd()) + ... channel.send(basename) + ... """) + >>> ch.receive() + 'mytmp' + +This way a Group object becomes kind of a Gateway factory where +the factory-caller does not need to know the setup. --- a/doc/example/test_multi.txt +++ b/doc/example/test_multi.txt @@ -41,7 +41,7 @@ Use channel callbacks if you want to pro data immediately and without blocking execution:: >>> import execnet - >>> gw = execnet.PopenGateway() + >>> gw = execnet.makegateway() >>> ch = gw.remote_exec("channel.receive() ; channel.send(42)") >>> l = [] >>> ch.setcallback(l.append) --- a/doc/example/test_info.txt +++ b/doc/example/test_info.txt @@ -4,10 +4,10 @@ basic local and remote communication Comparing current working directories ---------------------------------------- -A popen gateway has the same working directory as the instantiatior:: +A local popen gateway has the same working directory as the instantiatior:: >>> import execnet, os - >>> gw = execnet.PopenGateway() + >>> gw = execnet.makegateway() >>> ch = gw.remote_exec("import os; channel.send(os.getcwd())") >>> res = ch.receive() >>> assert res == os.getcwd() @@ -48,7 +48,7 @@ running in a remote_exec situation by ch You can now remote-execute the module like this:: >>> import execnet, remote1 - >>> gw = execnet.PopenGateway() + >>> gw = execnet.makegateway() >>> ch = gw.remote_exec(remote1) >>> print (ch.receive()) initialization complete @@ -68,7 +68,7 @@ you can setup a serving loop and write a Then on the local side you can do:: >>> import execnet, remotecmd - >>> gw = execnet.PopenGateway() + >>> gw = execnet.makegateway() >>> ch = gw.remote_exec(remotecmd) >>> ch.send('simple(10)') # execute func-call remotely >>> ch.receive() @@ -95,7 +95,7 @@ Simply run the script like this:: python socketserver.py :8888 # bind to all IPs, port 8888 You can then instruct execnet on your local machine to bootstrap -itself into the remote socket endpoint: +itself into the remote socket endpoint:: import execnet gw = execnet.SocketGateway("TARGET-IP:8888") --- a/doc/example/redirect_remote_output.py +++ b/doc/example/redirect_remote_output.py @@ -10,7 +10,7 @@ showcasing features of the channel objec import py -gw = execnet.PopenGateway() +gw = execnet.makegateway() outchan = gw.remote_exec(""" import sys --- a/doc/basics.txt +++ b/doc/basics.txt @@ -24,7 +24,7 @@ passing it a gateway specification or UR Here is an example which instantiates a simple Python subprocess:: - >>> gateway = execnet.makegateway("popen") + >>> gateway = execnet.makegateway() gateways allow to `remote execute code`_ and `exchange data`_ bidirectionally. @@ -131,7 +131,7 @@ rsync: synchronise filesystem with remot Here is a basic example for using RSync:: rsync = execnet.RSync('/tmp/source') - gw = execnet.makegateway("popen") + gw = execnet.makegateway() rsync.add_target(gw, '/tmp/dest') rsync.send() From commits-noreply at bitbucket.org Sun Dec 6 18:56:37 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 6 Dec 2009 17:56:37 +0000 (UTC) Subject: [execnet-commit] execnet commit 800841c9b050: skip jython tests if it is not a 2.5 version Message-ID: <20091206175637.8D9A67EE74@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260121901 -3600 # Node ID 800841c9b050c49df81bddf3270d74cd5263df69 # Parent de6284add5db8329f9b57fc44026b6f031771782 skip jython tests if it is not a 2.5 version --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ import execnet import py import sys +import subprocess collect_ignore = ['build', 'doc/_build'] @@ -63,9 +64,24 @@ def pytest_generate_tests(metafunc): 'pypy-c', 'jython'): metafunc.addcall(id=name, param=name) +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + popen = subprocess.Popen([str(executable), "--version"], + stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + cache[name] = executable + return executable + def pytest_funcarg__anypython(request): name = request.param - executable = py.path.local.sysfind(name) + executable = getexecutable(name) if executable is None: if sys.platform == "win32": executable = winpymap.get(name, None) From commits-noreply at bitbucket.org Sun Dec 6 20:11:57 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 6 Dec 2009 19:11:57 +0000 (UTC) Subject: [execnet-commit] execnet commit a15041d7bb5e: work out issues/ideas a bit Message-ID: <20091206191157.4755D7EEE5@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260125857 -3600 # Node ID a15041d7bb5e008dc1a4a2976f03a79ca2085841 # Parent 800841c9b050c49df81bddf3270d74cd5263df69 work out issues/ideas a bit --- a/ISSUES.txt +++ b/ISSUES.txt @@ -19,8 +19,9 @@ path: execnet/rsync.py currently rsyncing bails out with a strange message when trying to rsync links. should provide better message or even some support for doing it. -a process keeping state across invoactions --------------------------------------------------- +socketservice process for keeping cross-invocation state +--------------------------------------------------------- + tags: 1.1 feature newdist A building block for keeping permanent network @@ -36,6 +37,16 @@ that keeps state. Considered: will re-use the already-imported gateway base code so again avoid parse/compile on arbitrary strings. +Possible code example: + + # connect to on-demand started port-8888 socket server + # similar to popen+socket//installvia=popen + gw = group.makegateway("socketservice//port=8888") + gw.remote_exec("...") # execute code in a thread in the socket server + ... + # exit the gateway-connection, but leave socket server running + gw.exit() + a channel-IO based gateway -------------------------------------------------- tags: 1.1 feature newdist @@ -46,3 +57,24 @@ This way a gateway can be instantiated t intermediating gateway which bi-directionallry forwads Messages through a existing channel. +A code example could look like this: + + group.setlocalfactory(port=8888) + gw = group.makegateway("ssh=codespeak.net") + # local ssh-process would be a child of the factory service + # which could have timeouts for killing, or allow resuming. + # for instantiating remote it could also install a multiplexer + # on the remote place such that the same ssh connection is re-used + # for multiple ssh=codespeak.net connections, makes for small latency. + +some first simple implemenation might look like this:: + + gw = group.makegateway("socketservice//port=8888") + # we can rely that the subprocess gw can import execnet + ch = gw.remote_exec(""" + import execnet.service + execnet.service.gatewaymaker(channel) + """) + ch.send(xspec) + subchannel = ch.receive() + return ChannelGateway(subchannel) From commits-noreply at bitbucket.org Mon Dec 7 13:34:59 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 7 Dec 2009 12:34:59 +0000 (UTC) Subject: [execnet-commit] execnet commit e5cbfedbd84e: internal tweaks: move deprecated code to its own module, add python2.7 to python interpreters to look for. Message-ID: <20091207123459.57DA17EE85@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260189273 -3600 # Node ID e5cbfedbd84e6dc2ee0f62f3b5ea70d865cd67ae # Parent a15041d7bb5e008dc1a4a2976f03a79ca2085841 internal tweaks: move deprecated code to its own module, add python2.7 to python interpreters to look for. --- a/execnet/multi.py +++ b/execnet/multi.py @@ -228,48 +228,6 @@ class MultiChannel: if first: reraise(*first) - -default_group = Group() - -makegateway = default_group.makegateway - -def PopenGateway(python=None): - """ instantiate a gateway to a subprocess - started with the given 'python' executable. - """ - APIWARN("1.0.0b4", "use makegateway('popen')") - spec = execnet.XSpec("popen") - spec.python = python - return default_group.makegateway(spec) - -def SocketGateway(host, port): - """ This Gateway provides interaction with a remote process - by connecting to a specified socket. On the remote - side you need to manually start a small script - (py/execnet/script/socketserver.py) that accepts - SocketGateway connections or use the experimental - new_remote() method on existing gateways. - """ - APIWARN("1.0.0b4", "use makegateway('socket=host:port')") - spec = execnet.XSpec("socket=%s:%s" %(host, port)) - return default_group.makegateway(spec) - -def SshGateway(sshaddress, remotepython=None, ssh_config=None): - """ instantiate a remote ssh process with the - given 'sshaddress' and remotepython version. - you may specify an ssh_config file. - """ - APIWARN("1.0.0b4", "use makegateway('ssh=host')") - spec = execnet.XSpec("ssh=%s" % sshaddress) - spec.python = remotepython - spec.ssh_config = ssh_config - return default_group.makegateway(spec) - -def APIWARN(version, msg, stacklevel=3): - import warnings - Warn = DeprecationWarning("(since version %s) %s" %(version, msg)) - warnings.warn(Warn, stacklevel=stacklevel) - def killpopen(popen): try: if hasattr(popen, 'kill'): @@ -300,3 +258,7 @@ def killpid(pid): ctypes.windll.kernel32.CloseHandle(handle) else: raise EnvironmmentError("no method to kill %s" %(pid,)) + +default_group = Group() +makegateway = default_group.makegateway + --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,7 @@ collect_ignore = ['build', 'doc/_build'] rsyncdirs = ['conftest.py', 'execnet', 'testing', 'doc'] winpymap = { + 'python2.7': r'C:\Python27\python.exe', 'python2.6': r'C:\Python26\python.exe', 'python2.5': r'C:\Python25\python.exe', 'python2.4': r'C:\Python24\python.exe', @@ -61,7 +62,7 @@ def pytest_generate_tests(metafunc): metafunc.addcall(id=gwtype, param=gwtype) elif 'anypython' in metafunc.funcargnames: for name in ('python3.1', 'python2.4', 'python2.5', 'python2.6', - 'pypy-c', 'jython'): + 'python2.7', 'pypy-c', 'jython'): metafunc.addcall(id=name, param=name) def getexecutable(name, cache={}): --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -9,9 +9,9 @@ __version__ = "1.0.1" import execnet.apipkg execnet.apipkg.initpkg(__name__, { - 'PopenGateway': '.multi:PopenGateway', - 'SocketGateway': '.multi:SocketGateway', - 'SshGateway': '.multi:SshGateway', + 'PopenGateway': '.deprecated:PopenGateway', + 'SocketGateway': '.deprecated:SocketGateway', + 'SshGateway': '.deprecated:SshGateway', 'makegateway': '.multi:makegateway', 'HostNotFound': '.gateway:HostNotFound', 'XSpec': '.xspec:XSpec', --- /dev/null +++ b/execnet/deprecated.py @@ -0,0 +1,43 @@ +""" +some deprecated calls + +(c) 2008-2009, Holger Krekel and others +""" +import execnet + +def PopenGateway(python=None): + """ instantiate a gateway to a subprocess + started with the given 'python' executable. + """ + APIWARN("1.0.0b4", "use makegateway('popen')") + spec = execnet.XSpec("popen") + spec.python = python + return execnet.default_group.makegateway(spec) + +def SocketGateway(host, port): + """ This Gateway provides interaction with a remote process + by connecting to a specified socket. On the remote + side you need to manually start a small script + (py/execnet/script/socketserver.py) that accepts + SocketGateway connections or use the experimental + new_remote() method on existing gateways. + """ + APIWARN("1.0.0b4", "use makegateway('socket=host:port')") + spec = execnet.XSpec("socket=%s:%s" %(host, port)) + return execnet.default_group.makegateway(spec) + +def SshGateway(sshaddress, remotepython=None, ssh_config=None): + """ instantiate a remote ssh process with the + given 'sshaddress' and remotepython version. + you may specify an ssh_config file. + """ + APIWARN("1.0.0b4", "use makegateway('ssh=host')") + spec = execnet.XSpec("ssh=%s" % sshaddress) + spec.python = remotepython + spec.ssh_config = ssh_config + return execnet.default_group.makegateway(spec) + +def APIWARN(version, msg, stacklevel=3): + import warnings + Warn = DeprecationWarning("(since version %s) %s" %(version, msg)) + warnings.warn(Warn, stacklevel=stacklevel) From commits-noreply at bitbucket.org Fri Dec 11 15:10:29 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 11 Dec 2009 14:10:29 +0000 (UTC) Subject: [execnet-commit] execnet commit 3d00bf8eaea4: move socket-instantiation into own file, speeds up popen-gateway creation by another 10%. Message-ID: <20091211141029.DA64D7EF3B@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1260540574 -3600 # Node ID 3d00bf8eaea4e48e9cc621127e936d50f59727cb # Parent e5cbfedbd84e6dc2ee0f62f3b5ea70d865cd67ae move socket-instantiation into own file, speeds up popen-gateway creation by another 10%. --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +1.x (pending) +-------------------------------- + +- internally split socket gateways, speeds up popen-gateways + by 10% (now at 30 milliseconds per-gateway on my 3-year old machine) + 1.0.1 -------------------------------- --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -6,7 +6,7 @@ gateway code for initiating popen, socke import sys, os, inspect, socket, types import textwrap import execnet -from execnet.gateway_base import Message, Popen2IO, SocketIO +from execnet.gateway_base import Message, Popen2IO from execnet import gateway_base importdir = os.path.dirname(os.path.dirname(execnet.__file__)) @@ -174,59 +174,6 @@ def sendexec(io, *sources): source = "\n".join(sources) io.write((repr(source)+ "\n").encode('ascii')) -class SocketGateway(Gateway): - """ This Gateway provides interaction with a remote process - by connecting to a specified socket. On the remote - side you need to manually start a small script - (py/execnet/script/socketserver.py) that accepts - SocketGateway connections. - """ - _remotesetup = "io = SocketIO(clientsock)" - - def __init__(self, host, port, id): - """ instantiate a gateway to a process accessed - via a host/port specified socket. - """ - self.host = host = str(host) - self.port = port = int(port) - self.remoteaddress = '%s:%d' % (self.host, self.port) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect((host, port)) - except socket.gaierror: - raise HostNotFound(str(sys.exc_info()[1])) - io = SocketIO(sock) - super(SocketGateway, self).__init__(io=io, id=id) - - def new_remote(cls, gateway, id, hostport=None): - """ return a new (connected) socket gateway, - instantiated through the given 'gateway'. - """ - if hostport is None: - host, port = ('localhost', 0) - else: - host, port = hostport - - mydir = os.path.dirname(__file__) - socketserver = os.path.join(mydir, 'script', 'socketserver.py') - socketserverbootstrap = "\n".join([ - open(socketserver, 'r').read(), """if 1: - import socket - sock = bind_and_listen((%r, %r)) - port = sock.getsockname() - channel.send(port) - startserver(sock) - """ % (host, port)]) - # execute the above socketserverbootstrap on the other side - channel = gateway.remote_exec(socketserverbootstrap) - (realhost, realport) = channel.receive() - #self._trace("new_remote received" - # "port=%r, hostname = %r" %(realport, hostname)) - if not realhost or realhost=="0.0.0.0": - realhost = "localhost" - return cls(realhost, realport, id=id) - new_remote = classmethod(new_remote) - class HostNotFound(Exception): pass --- /dev/null +++ b/execnet/gateway_socket.py @@ -0,0 +1,92 @@ +import socket +from execnet.gateway import Gateway, HostNotFound +import os, sys, inspect + +class SocketIO: + error = (socket.error, EOFError) + def __init__(self, sock): + self.sock = sock + try: + sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY + sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + except (AttributeError, socket.error): + e = sys.exc_info()[1] + sys.stderr.write("WARNING: cannot set socketoption") + + def read(self, numbytes): + "Read exactly 'bytes' bytes from the socket." + buf = bytes() + while len(buf) < numbytes: + t = self.sock.recv(numbytes - len(buf)) + if not t: + raise EOFError + buf += t + return buf + + def write(self, data): + assert isinstance(data, bytes) + self.sock.sendall(data) + + def close_read(self): + try: + self.sock.shutdown(0) + except socket.error: + pass + def close_write(self): + try: + self.sock.shutdown(1) + except socket.error: + pass + +class SocketGateway(Gateway): + """ This Gateway provides interaction with a remote process + by connecting to a specified socket. On the remote + side you need to manually start a small script + (py/execnet/script/socketserver.py) that accepts + SocketGateway connections. + """ + _remotesetup = "import socket\n%s\nio = SocketIO(clientsock)" % inspect.getsource(SocketIO) + + def __init__(self, host, port, id): + """ instantiate a gateway to a process accessed + via a host/port specified socket. + """ + self.host = host = str(host) + self.port = port = int(port) + self.remoteaddress = '%s:%d' % (self.host, self.port) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + except socket.gaierror: + raise HostNotFound(str(sys.exc_info()[1])) + io = SocketIO(sock) + super(SocketGateway, self).__init__(io=io, id=id) + + def new_remote(cls, gateway, id, hostport=None): + """ return a new (connected) socket gateway, + instantiated through the given 'gateway'. + """ + if hostport is None: + host, port = ('localhost', 0) + else: + host, port = hostport + + mydir = os.path.dirname(__file__) + socketserver = os.path.join(mydir, 'script', 'socketserver.py') + socketserverbootstrap = "\n".join([ + open(socketserver, 'r').read(), """if 1: + import socket + sock = bind_and_listen((%r, %r)) + port = sock.getsockname() + channel.send(port) + startserver(sock) + """ % (host, port)]) + # execute the above socketserverbootstrap on the other side + channel = gateway.remote_exec(socketserverbootstrap) + (realhost, realport) = channel.receive() + #self._trace("new_remote received" + # "port=%r, hostname = %r" %(realport, hostname)) + if not realhost or realhost=="0.0.0.0": + realhost = "localhost" + return cls(realhost, realport, id=id) + new_remote = classmethod(new_remote) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -6,7 +6,7 @@ NOTE: aims to be compatible to Python 2. (C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others """ import sys, os, weakref -import threading, traceback, socket, struct +import threading, traceback, struct try: import queue except ImportError: @@ -56,47 +56,6 @@ else: pass -# ___________________________________________________________________________ -# -# input output classes -# ___________________________________________________________________________ - -class SocketIO: - error = (socket.error, EOFError) - def __init__(self, sock): - self.sock = sock - try: - sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY - sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) - except (AttributeError, socket.error): - e = sys.exc_info()[1] - sys.stderr.write("WARNING: cannot set socketoption") - - def read(self, numbytes): - "Read exactly 'bytes' bytes from the socket." - buf = bytes() - while len(buf) < numbytes: - t = self.sock.recv(numbytes - len(buf)) - if not t: - raise EOFError - buf += t - return buf - - def write(self, data): - assert isinstance(data, bytes) - self.sock.sendall(data) - - def close_read(self): - try: - self.sock.shutdown(0) - except socket.error: - pass - def close_write(self): - try: - self.sock.shutdown(1) - except socket.error: - pass - class Popen2IO: error = (IOError, OSError, EOFError) --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -4,7 +4,7 @@ package for connecting to local and remo (c) 2009, Holger Krekel and others """ -__version__ = "1.0.1" +__version__ = "1.0.1.post1" import execnet.apipkg --- a/execnet/multi.py +++ b/execnet/multi.py @@ -81,13 +81,14 @@ class Group: elif spec.socket: assert not spec.python, ( "socket: specifying python executables not yet supported") + from execnet.gateway_socket import SocketGateway gateway_id = spec.installvia if gateway_id: viagw = self[gateway_id] - gw = gateway.SocketGateway.new_remote(viagw, id=id) + gw = SocketGateway.new_remote(viagw, id=id) else: host, port = spec.socket.split(":") - gw = gateway.SocketGateway(host, port, id=id) + gw = SocketGateway(host, port, id=id) else: raise ValueError("no gateway type found for %r" % (spec._spec,)) gw.spec = spec --- a/testing/test_xspec.py +++ b/testing/test_xspec.py @@ -65,7 +65,7 @@ class TestMakegateway: assert gw.spec.popen assert gw.spec.python == None rinfo = gw._rinfo() - assert rinfo.executable == py.std.sys.executable + #assert rinfo.executable == py.std.sys.executable assert rinfo.cwd == py.std.os.getcwd() assert rinfo.version_info == py.std.sys.version_info @@ -113,7 +113,7 @@ class TestMakegateway: py.test.skip("cpython2.6 not found") gw = execnet.makegateway("popen//python=%s" % cpython26) rinfo = gw._rinfo() - assert rinfo.executable == cpython26 + #assert rinfo.executable == cpython26 assert rinfo.cwd == py.std.os.getcwd() assert rinfo.version_info[:2] == (2,6) From commits-noreply at bitbucket.org Sun Dec 20 17:10:08 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 16:10:08 +0000 (UTC) Subject: [execnet-commit] execnet commit 9068ad88e86a: adding issues, fixing docs, fixating release-version Message-ID: <20091220161008.0EC9C7EE7A@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261314055 -3600 # Node ID 9068ad88e86a345d49d29a36fa3d20b28f18cac6 # Parent 3d00bf8eaea4e48e9cc621127e936d50f59727cb adding issues, fixing docs, fixating release-version --- a/ISSUES.txt +++ b/ISSUES.txt @@ -78,3 +78,36 @@ some first simple implemenation might lo ch.send(xspec) subchannel = ch.receive() return ChannelGateway(subchannel) + +serialize channels everywhere +------------------------------------------------- +tags: 1.0.2 feature + +currently sending channels only works if they are send +as a single item. So they cannot be contained in a list or dict. +By making channel passing a feature of the serializer we should +be able to lift this limitation, allowing channels in all places. + +creating new channels +------------------------------------------------- +tags: 1.0.2 feature + +document, provide examples and refine support for creating new channels. +Defining closing semantics - does a channel created from a channel +automatically close when its parent channel closes? This currently +depends on whether it is garbage collected. + +creating virtualenv environments on the fly +----------------------------------------------- +tags: 1.1 wish + +there should be a (plugin-provided?) way to create +a virtual environment: + gw = makegateway("ssh=codespeak.net") + newgw = xdeploy.create_venv_gateway(gw, "venvname", depstring) + newgw # lives in home/ dir of remote virtualenv + # maybe have get_temproot default to tmp/ there? + sf = xdeploy.SetupFile("..setup.py") + sf.sdist_remote_install(newgw) + + --- a/doc/conf.py +++ b/doc/conf.py @@ -46,9 +46,9 @@ copyright = '2009, holger krekel and oth # # The short X.Y version. import execnet -version = execnet.__version__ +version = "1.0.1" # The full version, including alpha/beta/rc tags. -release = execnet.__version__ +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. --- a/doc/example/hybridpython.txt +++ b/doc/example/hybridpython.txt @@ -90,6 +90,6 @@ using Mono 2.0 and IronPython-1.1 this w Using IronPython needs more testing, likely newer versions will work better. please feedback if you have information. -.. _IronPython: http://www.IronPython.org +.. _IronPython: http://ironpython.net --- a/doc/index.txt +++ b/doc/index.txt @@ -41,7 +41,8 @@ Known uses * `py.test`_ uses it for its `distributed testing`_ mechanism. * Jacob Perkins uses it for his `Distributed NTLK with execnet`_ - project to launch computation processes through ssh. + project to launch computation processes through ssh. He also + compares `disco and execnet`_ in a subsequent post. * Ronny Pfannschmidt uses it for his `anyvc`_ VCS-abstraction project to bridge the Python2/Python3 version gap. @@ -51,6 +52,7 @@ Known uses .. _`py.test`: http://pytest.org .. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html .. _`Distributed NTLK with execnet`: http://streamhacker.com/2009/11/29/distributed-nltk-execnet/ +.. _`disco and execnet`: http://streamhacker.com/2009/12/14/execnet-disco-distributed-nltk/ .. _`anyvc`: http://bitbucket.org/RonnyPfannschmidt/anyvc/ Project status From commits-noreply at bitbucket.org Sun Dec 20 17:10:09 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 16:10:09 +0000 (UTC) Subject: [execnet-commit] execnet commit 8a4fe45e6508: avoid unneccessary lower() Message-ID: <20091220161009.D07437EE8A@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261314724 -3600 # Node ID 8a4fe45e650874536a521f736ada5b93a84b2614 # Parent 9068ad88e86a345d49d29a36fa3d20b28f18cac6 avoid unneccessary lower() --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -896,14 +896,14 @@ class Serializer(object): try: dispatch = self.dispatch[tp] except KeyError: - methodname = 'save_' + tp.__name__.lower() + methodname = 'save_' + tp.__name__ meth = getattr(self, methodname, None) if meth is None: raise SerializationError("can't serialize %s" % (tp,)) dispatch = self.dispatch[tp] = meth dispatch(obj) - def save_nonetype(self, non): + def save_NoneType(self, non): self._write(opcode.NONE) def save_bool(self, boolean): From commits-noreply at bitbucket.org Sun Dec 20 17:10:12 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 16:10:12 +0000 (UTC) Subject: [execnet-commit] execnet commit f9a82b8fb819: generalize channel-over-channel sending, add examples. Message-ID: <20091220161012.06BD37EEE2@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261323576 -3600 # Node ID f9a82b8fb819440a42ffca843ff065e001d29fba # Parent 8a4fe45e650874536a521f736ada5b93a84b2614 generalize channel-over-channel sending, add examples. --- a/doc/example/test_info.txt +++ b/doc/example/test_info.txt @@ -1,10 +1,23 @@ basic local and remote communication ========================================= -Comparing current working directories +Execute code in subprocess, communicate through a channel +--------------------------------------------------------- + +You can instantiate a subprocess gateway, execute code +in it and exchange data dynamically:: + + >>> import execnet + >>> gw = execnet.makegateway() + >>> channel = gw.remote_exec("channel.send(channel.receive()+1)") + >>> channel.send(1) + >>> channel.receive() + 2 + +Compare current working directories ---------------------------------------- -A local popen gateway has the same working directory as the instantiatior:: +A local subprocess gateway has the same working directory as the instantiatior:: >>> import execnet, os >>> gw = execnet.makegateway() @@ -14,7 +27,7 @@ A local popen gateway has the same worki "ssh" gateways default to the login home directory. -Getting information from remote ssh account +Get information from remote ssh account -------------------------------------------- Use simple execution to obtain information from remote environments:: @@ -31,6 +44,43 @@ Use simple execution to obtain informati >>> version_info (2, 4, 2, 'final', 0) +Use a callback instead of receive() and wait for completion +------------------------------------------------------------- + +Set a channel callback to immediately react on incoming data:: + + >>> import execnet + >>> gw = execnet.makegateway() + >>> channel = gw.remote_exec("for i in range(10): channel.send(i)") + >>> l = [] + >>> channel.setcallback(l.append, endmarker=None) + >>> channel.waitclose() # waits for closing, i.e. remote exec finish + >>> l + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, None] + +Note that the callback function will execute in the receiver thread +so it should not block on IO or long to execute. + +Sending channels over channels +------------------------------------------------------ + +You can create and transfer a channel over an existing channel +and use it to transfer information:: + + >>> import execnet + >>> gw = execnet.makegateway() + >>> channel = gw.remote_exec(""" + ... ch1, ch2 = channel.receive() + ... ch2.send("world") + ... ch1.send("hello") + ... """) + >>> c1 = gw.newchannel() # create new channel + >>> c2 = gw.newchannel() # create another channel + >>> channel.send((c1, c2)) # send them over + >>> c1.receive() + 'hello' + >>> c2.receive() + 'world' Avoid "inlined" source strings with remote_exec -------------------------------------------------------------- --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -141,13 +141,6 @@ def _setupmessages(): channel = gateway._channelfactory.new(self.channelid) gateway._local_schedulexec(channel=channel, sourcetask=self.data) - class CHANNEL_NEW(Message): - def received(self, gateway): - """ receive a remotely created new (sub)channel. """ - newid = self.data - newchannel = gateway._channelfactory.new(newid) - gateway._channelfactory._local_receive(self.channelid, newchannel) - class CHANNEL_DATA(Message): def received(self, gateway): gateway._channelfactory._local_receive(self.channelid, self.data) @@ -172,7 +165,7 @@ def _setupmessages(): raise SystemExit(0) classes = [ - STATUS, CHANNEL_OPEN, CHANNEL_NEW, CHANNEL_DATA, CHANNEL_CLOSE, + STATUS, CHANNEL_OPEN, CHANNEL_DATA, CHANNEL_CLOSE, CHANNEL_CLOSE_ERROR, CHANNEL_LAST_MESSAGE, GATEWAY_TERMINATE ] for i, cls in enumerate(classes): @@ -385,10 +378,7 @@ class Channel(object): """ if self.isclosed(): raise IOError("cannot send to %r" %(self,)) - if isinstance(item, Channel): - data = Message.CHANNEL_NEW(self.id, item.id) - else: - data = Message.CHANNEL_DATA(self.id, item) + data = Message.CHANNEL_DATA(self.id, item) self.gateway._send(data) def receive(self, timeout=-1): @@ -455,8 +445,10 @@ class ChannelFactory(object): if id is None: id = self.count self.count += 2 - channel = Channel(self.gateway, id) - self._channels[id] = channel + try: + channel = self._channels[id] + except KeyError: + channel = self._channels[id] = Channel(self.gateway, id) return channel finally: self._writelock.release() @@ -597,7 +589,7 @@ class BaseGateway(object): def _thread_receiver(self): self._trace("starting to receive") - unserializer = Unserializer(self._io) + unserializer = Unserializer(self._io, self._channelfactory) try: while 1: try: @@ -607,6 +599,7 @@ class BaseGateway(object): _receivelock.acquire() try: msg.received(self) + del msg finally: _receivelock.release() except sysex: @@ -639,6 +632,7 @@ class BaseGateway(object): # _____________________________________________________________________ # def newchannel(self): + """ return a new independent channel. """ return self._channelfactory.new() def join(self, timeout=None): @@ -729,8 +723,9 @@ class Unserializer(object): py2str_as_py3str = True # True py3str_as_py2str = False # false means py2 will get unicode - def __init__(self, stream): + def __init__(self, stream, channelfactory=None): self.stream = stream + self.channelfactory = channelfactory def load(self): self.stack = [] @@ -748,7 +743,7 @@ class Unserializer(object): except _Stop: if len(self.stack) != 1: raise UnserializationError("internal unserialization error") - return self.stack[0] + return self.stack.pop(0) else: raise UnserializationError("didn't get STOP") @@ -851,6 +846,11 @@ class Unserializer(object): def load_stop(self): raise _Stop + def load_channel(self): + id = self._read_int4() + newchannel = self.channelfactory.new(id) + self.stack.append(newchannel) + # automatically build opcodes and byte-encoding class opcode: @@ -998,6 +998,10 @@ class Serializer(object): def save_frozenset(self, s): self._write_set(s, opcode.FROZENSET) + def save_Channel(self, channel): + self._write(opcode.CHANNEL) + self._write_int4(channel.id) + def init_popen_io(): if not hasattr(os, 'dup'): # jython io = Popen2IO(sys.stdout, sys.stdin) --- a/doc/basics.txt +++ b/doc/basics.txt @@ -61,6 +61,7 @@ get sent for remote execution. ``remote a channel object whose symmetric counterpart channel is available to the remotely executing source. + .. _`Channel`: .. _`channel-api`: @@ -83,6 +84,7 @@ two asynchronously running programs. .. autoattribute:: Channel.RemoteError .. autoattribute:: Channel.TimeoutError + .. _Group: Grouped Gateways and robust termination --- a/testing/test_channel.py +++ b/testing/test_channel.py @@ -88,6 +88,30 @@ class TestChannelBasicBehaviour: l = list(channel) assert l == [0, 1, 2] + def test_channel_pass_in_structure(self, gw): + channel = gw.remote_exec(''' + ch1, ch2 = channel.receive() + data = ch1.receive() + ch2.send(data+1) + ''') + newchan1 = gw.newchannel() + newchan2 = gw.newchannel() + channel.send((newchan1, newchan2)) + newchan1.send(1) + data = newchan2.receive() + assert data ==2 + + def test_channel_multipass(self, gw): + channel = gw.remote_exec(''' + channel.send(channel) + xchan = channel.receive() + assert xchan == channel + ''') + newchan = channel.receive() + assert newchan == channel + channel.send(newchan) + channel.waitclose() + def test_channel_passing_over_channel(self, gw): channel = gw.remote_exec(''' c = channel.gateway.newchannel() --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 1.x (pending) -------------------------------- +- generalize channel-over-channel sending: you can now have channels + anywhere in a data structure (i.e. as an item of a container type). + Add according examples. + - internally split socket gateways, speeds up popen-gateways by 10% (now at 30 milliseconds per-gateway on my 3-year old machine) From commits-noreply at bitbucket.org Sun Dec 20 17:10:14 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 16:10:14 +0000 (UTC) Subject: [execnet-commit] execnet commit 1d9442698ab8: rename CHANNEL_OPEN to CHANNEL_EXEC and add test_debug.txt to versioning Message-ID: <20091220161014.621667EEEF@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261324797 -3600 # Node ID 1d9442698ab81618293b3436d304d4808b528aaf # Parent f9a82b8fb819440a42ffca843ff065e001d29fba rename CHANNEL_OPEN to CHANNEL_EXEC and add test_debug.txt to versioning --- /dev/null +++ b/doc/example/test_debug.txt @@ -0,0 +1,43 @@ + +Debugging execnet / Wire messages +=============================================================== + +By setting the environment variable ``EXECNET_DEBUG`` you can +configure the execnet tracing mechanism: + +:EXECNET_DEBUG=1: write per-process trace-files to ``${TEMPROOT}/execnet-debug-PID`` +:EXECNET_DEBUG=2: perform tracing to stderr (popen-gateway slaves will send this to their instantiator) + +Here is a simple example to see what goes on with a simple execution:: + + EXECNET_DEBUG=2 # or "set EXECNET_DEBUG=2" on windows + + python -c 'import execnet ; execnet.makegateway().remote_exec("42")' + +which will show PID-prefixed trace entries:: + + [2326] gw0 starting to receive + [2326] gw0 sent + [2327] creating slavegateway on + [2327] gw0-slave starting to receive + [2327] gw0-slave received + [2327] gw0-slave execution starts[1]: '42' + [2327] gw0-slave execution finished + [2327] gw0-slave sent + [2327] gw0-slave 1 sent channel close message + [2326] gw0 received + [2326] gw0 1 channel.__del__ + [2326] === atexit cleanup === + [2326] gw0 gateway.exit() called + [2326] gw0 --> sending GATEWAY_TERMINATE + [2326] gw0 sent + [2326] gw0 joining receiver thread + [2327] gw0-slave received + [2327] gw0-slave putting None to execqueue + [2327] gw0-slave io.close_read() + [2327] gw0-slave leaving + [2327] gw0-slave 1 channel.__del__ + [2327] gw0-slave io.close_write() + [2327] gw0-slave slavegateway.serve finished + [2327] gw0-slave gateway.join() called while receiverthread already finished + [2326] gw0 leaving --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -93,7 +93,7 @@ class Gateway(gateway_base.BaseGateway): else: source = textwrap.dedent(str(source)) channel = self.newchannel() - self._send(Message.CHANNEL_OPEN(channel.id, source)) + self._send(Message.CHANNEL_EXEC(channel.id, source)) return channel def remote_init_threads(self, num=None): --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -136,7 +136,7 @@ def _setupmessages(): } gateway._send(Message.CHANNEL_DATA(self.channelid, d)) - class CHANNEL_OPEN(Message): + class CHANNEL_EXEC(Message): def received(self, gateway): channel = gateway._channelfactory.new(self.channelid) gateway._local_schedulexec(channel=channel, sourcetask=self.data) @@ -165,7 +165,7 @@ def _setupmessages(): raise SystemExit(0) classes = [ - STATUS, CHANNEL_OPEN, CHANNEL_DATA, CHANNEL_CLOSE, + STATUS, CHANNEL_EXEC, CHANNEL_DATA, CHANNEL_CLOSE, CHANNEL_CLOSE_ERROR, CHANNEL_LAST_MESSAGE, GATEWAY_TERMINATE ] for i, cls in enumerate(classes): From commits-noreply at bitbucket.org Sun Dec 20 17:15:08 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 16:15:08 +0000 (UTC) Subject: [execnet-commit] execnet commit bbd403fddf54: remove the just committed issues Message-ID: <20091220161508.D32C37EEDE@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261325694 -3600 # Node ID bbd403fddf5418af115c8ece205ce688b69052e0 # Parent 1d9442698ab81618293b3436d304d4808b528aaf remove the just committed issues --- a/ISSUES.txt +++ b/ISSUES.txt @@ -79,24 +79,6 @@ some first simple implemenation might lo subchannel = ch.receive() return ChannelGateway(subchannel) -serialize channels everywhere -------------------------------------------------- -tags: 1.0.2 feature - -currently sending channels only works if they are send -as a single item. So they cannot be contained in a list or dict. -By making channel passing a feature of the serializer we should -be able to lift this limitation, allowing channels in all places. - -creating new channels -------------------------------------------------- -tags: 1.0.2 feature - -document, provide examples and refine support for creating new channels. -Defining closing semantics - does a channel created from a channel -automatically close when its parent channel closes? This currently -depends on whether it is garbage collected. - creating virtualenv environments on the fly ----------------------------------------------- tags: 1.1 wish From commits-noreply at bitbucket.org Sun Dec 20 19:24:57 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 18:24:57 +0000 (UTC) Subject: [execnet-commit] execnet commit 7c99c22b2bbb: better handle errors in callbacks, closing the channel and leaving Message-ID: <20091220182457.C88097EEE2@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261332944 -3600 # Node ID 7c99c22b2bbb4b217641fff9122588bf8d41ed6e # Parent bbd403fddf5418af115c8ece205ce688b69052e0 better handle errors in callbacks, closing the channel and leaving other execution/communication useable --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,15 +1,3 @@ -report back errors when calling callbacks ---------------------------------------------------- - -tags: 1.0 -bb: http://bitbucket.org/hpk42/py-trunk/issue/37/ -path: execnet/gateway_base.py - -Callbacks are invoked in the receiver-thread -and any exception will kill the whole receiver-thread. -This is especially bad if it happens remotely as -it leaves the other side inaccesssible. - rsync links from posix to windows ----------------------------------------- tags: 1.1 feature --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -502,7 +502,16 @@ class ChannelFactory(object): else: queue.put(data) else: - callback(data) # even if channel may be already closed + try: + callback(data) # even if channel may be already closed + except KeyboardInterrupt: + raise + except: + excinfo = sys.exc_info() + self.gateway._trace("exception during callback: %s" % excinfo[1]) + errortext = geterrortext(excinfo) + self.gateway._send(Message.CHANNEL_CLOSE_ERROR(id, errortext)) + self._local_close(id, errortext) def _finished_receiving(self): self._writelock.acquire() --- a/testing/test_channel.py +++ b/testing/test_channel.py @@ -244,6 +244,24 @@ class TestChannelBasicBehaviour: assert err assert str(err).find("ValueError") != -1 + def test_channel_callback_error(self, gw): + channel = gw.remote_exec(""" + def f(item): + raise ValueError(42) + ch = channel.gateway.newchannel() + ch.setcallback(f) + channel.send(ch) + channel.receive() + assert ch.isclosed() + """) + subchan = channel.receive() + subchan.send(1) + excinfo = py.test.raises(subchan.RemoteError, + "subchan.waitclose(TESTTIMEOUT)") + assert "42" in excinfo.value.formatted + channel.send(1) + channel.waitclose() + class TestChannelFile: def test_channel_file_write(self, gw): channel = gw.remote_exec(""" --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,11 @@ 1.x (pending) anywhere in a data structure (i.e. as an item of a container type). Add according examples. +- automatically close a channel when a remote callback raises + an exception, makes communication more robust because until + now failing callbacks rendered the receiverthread unuseable + leaving the remote side in-accessible. + - internally split socket gateways, speeds up popen-gateways by 10% (now at 30 milliseconds per-gateway on my 3-year old machine) From commits-noreply at bitbucket.org Sun Dec 20 19:40:30 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 18:40:30 +0000 (UTC) Subject: [execnet-commit] execnet commit 755fc3032507: fixing some python3 issues, everything passes Message-ID: <20091220184030.1E5777EEE2@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261334418 -3600 # Node ID 755fc30325075889d3abc2ea9cf7b4d324bad848 # Parent 7c99c22b2bbb4b217641fff9122588bf8d41ed6e fixing some python3 issues, everything passes --- a/testing/test_basics.py +++ b/testing/test_basics.py @@ -11,11 +11,11 @@ def test_subprocess_interaction(anypytho popen = subprocess.Popen(args, bufsize=0, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) def send(line): - popen.stdin.write(line.encode('ascii')) + popen.stdin.write(line) if sys.version_info > (3,0) or sys.platform.startswith("java"): popen.stdin.flush() def receive(): - return popen.stdout.readline().decode('ascii') + return popen.stdout.readline() try: source = py.code.Source(read_write_loop, "read_write_loop()") --- a/testing/test_multi.py +++ b/testing/test_multi.py @@ -121,9 +121,9 @@ class TestGroup: assert len(gwlist) == 3 idlist = [x.id for x in gwlist] assert idlist == list('325') - print group + print (group) group.terminate() - print group + print (group) assert not group assert repr(group) == "" --- a/conftest.py +++ b/conftest.py @@ -73,7 +73,7 @@ def getexecutable(name, cache={}): if executable: if name == "jython": popen = subprocess.Popen([str(executable), "--version"], - stderr=subprocess.PIPE) + universal_newlines=True, stderr=subprocess.PIPE) out, err = popen.communicate() if not err or "2.5" not in err: executable = None From commits-noreply at bitbucket.org Sun Dec 20 22:43:05 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 20 Dec 2009 21:43:05 +0000 (UTC) Subject: [execnet-commit] execnet commit 6007ee61a482: bumping version to 1.0.2, fixing bits, removing unused imports Message-ID: <20091220214305.05EAE7EE8A@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261345365 -3600 # Node ID 6007ee61a482f19d765917da3d75a25ff2049793 # Parent 755fc30325075889d3abc2ea9cf7b4d324bad848 bumping version to 1.0.2, fixing bits, removing unused imports --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -1.x (pending) +1.0.2 -------------------------------- - generalize channel-over-channel sending: you can now have channels @@ -11,7 +11,7 @@ 1.x (pending) leaving the remote side in-accessible. - internally split socket gateways, speeds up popen-gateways - by 10% (now at 30 milliseconds per-gateway on my 3-year old machine) + by 10% (now at <50 milliseconds per-gateway on a 1.5 GHZ machine) 1.0.1 -------------------------------- --- a/doc/index.txt +++ b/doc/index.txt @@ -6,8 +6,8 @@ Python_ is a mature dynamic language whose interpreters can interact with all major computing platforms today. -**execnet** provides carefully tested means to ad-hoc interact with Python -interpreters across version, platform and network barriers. It provides +**execnet** provides carefully tested means to easily interact with Python +interpreters across version, platform and network barriers. It has a minimal and fast API targetting the following uses: * distribute tasks to local or remote CPUs @@ -28,7 +28,7 @@ Features * easy creation, handling and termination of multiple processes -* well tested interactions between CPython 2.4-3.1, Jython 2.5.1 +* well tested interactions between CPython 2.4-2.7, CPython3.1, Jython 2.5.1 and PyPy 1.1 interpreters. * fully interoperable between Windows and Unix-ish systems. --- a/doc/conf.py +++ b/doc/conf.py @@ -46,7 +46,7 @@ copyright = '2009, holger krekel and oth # # The short X.Y version. import execnet -version = "1.0.1" +version = "1.0.2" # The full version, including alpha/beta/rc tags. release = version --- a/execnet/gateway.py +++ b/execnet/gateway.py @@ -3,7 +3,7 @@ gateway code for initiating popen, socke (c) 2004-2009, Holger Krekel and others """ -import sys, os, inspect, socket, types +import sys, os, inspect, types import textwrap import execnet from execnet.gateway_base import Message, Popen2IO --- a/execnet/__init__.py +++ b/execnet/__init__.py @@ -1,10 +1,9 @@ """ -execnet: Elastic Python Deployment. -package for connecting to local and remote Python Interpreters. +execnet: pure python lib for connecting to local and remote Python Interpreters. (c) 2009, Holger Krekel and others """ -__version__ = "1.0.1.post1" +__version__ = "1.0.2" import execnet.apipkg --- a/execnet/gateway_socket.py +++ b/execnet/gateway_socket.py @@ -2,6 +2,9 @@ import socket from execnet.gateway import Gateway, HostNotFound import os, sys, inspect +try: bytes +except NameError: bytes = str + class SocketIO: error = (socket.error, EOFError) def __init__(self, sock): @@ -10,7 +13,6 @@ class SocketIO: sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) except (AttributeError, socket.error): - e = sys.exc_info()[1] sys.stderr.write("WARNING: cannot set socketoption") def read(self, numbytes): @@ -24,7 +26,6 @@ class SocketIO: return buf def write(self, data): - assert isinstance(data, bytes) self.sock.sendall(data) def close_read(self): From commits-noreply at bitbucket.org Wed Dec 23 20:17:40 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 23 Dec 2009 19:17:40 +0000 (UTC) Subject: [execnet-commit] execnet commit 58aeb70cf80a: fix internal timeout waiting bug (thanks ronny) Message-ID: <20091223191740.70A4E7EF2A@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261595843 -3600 # Node ID 58aeb70cf80a127af7ebc8193396c0f5bc2ee248 # Parent 6007ee61a482f19d765917da3d75a25ff2049793 fix internal timeout waiting bug (thanks ronny) --- a/execnet/gateway_base.py +++ b/execnet/gateway_base.py @@ -212,6 +212,7 @@ class Channel(object): """Communication channel between two Python Interpreter execution points.""" RemoteError = RemoteError TimeoutError = TimeoutError + _INTERNALWAKEUP = 1000 _executing = False def __init__(self, gateway, id): @@ -395,7 +396,7 @@ class Channel(object): if itemqueue is None: raise IOError("cannot receive(), channel has receiver callback") if timeout < 0: - internal_timeout = 1000 + internal_timeout = self._INTERNALWAKEUP else: internal_timeout = timeout @@ -404,7 +405,7 @@ class Channel(object): x = itemqueue.get(timeout=internal_timeout) break except queue.Empty: - if internal_timeout < 0: + if timeout < 0: continue raise self.TimeoutError("no item after %r seconds" %(timeout)) if x is ENDMARKER: --- a/testing/test_channel.py +++ b/testing/test_channel.py @@ -41,6 +41,15 @@ class TestChannelBasicBehaviour: channel.send(1) x = channel.receive(timeout=0.1) + def test_channel_receive_internal_timeout(self, gw, monkeypatch): + channel = gw.remote_exec(""" + import time + time.sleep(0.5) + channel.send(1) + """) + monkeypatch.setattr(channel.__class__, '_INTERNALWAKEUP', 0.2) + x = channel.receive() + def test_channel_close_and_then_receive_error_multiple(self, gw): channel = gw.remote_exec('channel.send(42) ; raise ValueError') x = channel.receive() --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,9 @@ 1.0.2 - internally split socket gateways, speeds up popen-gateways by 10% (now at <50 milliseconds per-gateway on a 1.5 GHZ machine) +- fix bug in channel.receive() that would wrongly raise a TimeoutError + after 1000 seconds (thanks Ronny Pfannschmidt) + 1.0.1 -------------------------------- From commits-noreply at bitbucket.org Wed Dec 23 21:30:17 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 23 Dec 2009 20:30:17 +0000 (UTC) Subject: [execnet-commit] execnet commit 330b0032a09e: fixes for jython Message-ID: <20091223203017.32CF67EF3F@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261600136 -3600 # Node ID 330b0032a09e13e38644c74d4a679d3760cdcdcf # Parent 58aeb70cf80a127af7ebc8193396c0f5bc2ee248 fixes for jython --- a/testing/test_termination.py +++ b/testing/test_termination.py @@ -74,6 +74,7 @@ def test_close_initiating_remote_no_erro stdout, stderr = popen.communicate() assert not stderr + at py.test.mark.skipif("not hasattr(os, 'dup')") def test_terminate_implicit_does_trykill(testdir, anypython, capfd): p = testdir.makepyfile(""" import sys --- a/testing/test_gateway.py +++ b/testing/test_gateway.py @@ -49,13 +49,14 @@ class TestBasicGateway: assert numchan2 == numchan def test_gateway_status_busy(self, gw): + numchannels = gw.remote_status().numchannels ch1 = gw.remote_exec("channel.send(1); channel.receive()") ch2 = gw.remote_exec("channel.receive()") ch1.receive() status = gw.remote_status() assert status.numexecuting == 1 # number of active execution threads assert status.execqsize == 1 # one more queued - assert status.numchannels == 2 + assert status.numchannels == numchannels + 2 ch1.send(None) ch2.send(None) ch1.waitclose() From commits-noreply at bitbucket.org Wed Dec 23 21:33:54 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 23 Dec 2009 20:33:54 +0000 (UTC) Subject: [execnet-commit] execnet commit 73176c8168f2: Added tag 1.0.2 for changeset 330b0032a09e Message-ID: <20091223203354.938F97EF1E@bitbucket.org> # HG changeset patch -- Bitbucket.org # Project execnet # URL http://bitbucket.org/hpk42/execnet/overview/ # User holger krekel # Date 1261600417 -3600 # Node ID 73176c8168f280a27ee9e2b79ab6b3488a943e23 # Parent 330b0032a09e13e38644c74d4a679d3760cdcdcf Added tag 1.0.2 for changeset 330b0032a09e --- a/.hgtags +++ b/.hgtags @@ -3,3 +3,4 @@ 7cf86a1811b11aa58112a255e6afba870c8d855e 7cf86a1811b11aa58112a255e6afba870c8d855e 1.0.0 5d1c438e335dc369834207882ec5f3227d365c11 1.0.0 66760dc71203dbceeb08851cf4cf74e2687156d0 1.0.1 +330b0032a09e13e38644c74d4a679d3760cdcdcf 1.0.2