diff -Nru execnet-1.0.9/CHANGELOG execnet-1.4.1/CHANGELOG --- execnet-1.0.9/CHANGELOG 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/CHANGELOG 2015-09-02 18:50:05.000000000 +0000 @@ -1,3 +1,144 @@ +1.4.1 +------ + +- fix a regression of the Serializer created by the implied opcode ordering + which resulted in a incompatible opcode mapping + + *warning* stored serialized objects created with 1.4.0 are incompatible + with previous versions and future versions + additionally stored serialized objects containing complex objects will + have a incompatible opcode when read with execnet < 1.4.0 + and wont be loadable with execnet 1.4.0 either + + its strongly suggested to avoid using the Serializer of execnet 1.4.0 + this affects devpi and the external pytest-cache plugin + +1.4 +---- + +- de-vendor apipkg and use the pypi dependency instead + (this also fixes the bpython interaction issues) + +- Fix issue38: provide ability to connect to Vagrant VMs easily + using :code:`vagrant_ssh=defaut` or :code:`vagrant_ssh=machinename` + this feature is experimental and will be refined in future releases. + Thanks Christian Theune for the discussion and the initial pull request. + +- add support for serializing the "complex" type. Thanks Sebastian + Koslowski. + + +1.3 +-------------------------------- + +- fix issue33: index.txt to correctly mention MIT instead of GPL. + +- fix issue35: adapt some doctests, fix some channel tests for py3. + +- use subprocess32 when available for python < 3. + +- try to be a bit more careful when interpreter is shutting down + to avoid random exceptions, thanks Alfredo Deza. + +- ignore errors on orphan file removal when rsyncing + +- fix issue34: limit use of import based bootstrap + +1.2 +-------------------------------- + +- fix issue22 -- during interpreter shutdown don't throw + an exception when we can't send a termination sequence + anymore as we are about to die anyway. + +- fix issue24 -- allow concurrent creation of gateways + by guarding automatic id creation by a look. + Thanks tlecomte. + +- majorly refactor internal thread and IO handling. + execnet can now operate on different thread models, + defaults to "thread" but allows for eventlet and + gevent if it is installed. + +- gateway.remote_exec() will now execute in multiple + threads on the other side by default. The previous + neccessity of running "gateway.remote_init_threads()" + to allow for such concurrency is gone. The latter + method is now a no-op and will be removed in future + versions of execnet. + +- fix issue20: prevent AttributError at interpreter shutdown + by not trying to send close/last_message messages if the + world around is half destroyed. + +- fix issue21: allow to create local gateways with sudo aka + makegateway("popen//python=sudo python"). + Thanks Alfredo Deza for the PR. + +- streamline gateway termination and simplify proxy + implementation. add more internal tracing. + +- if execution hangs in computation, we now try to + send a SIGINT to ourselves on Unix platforms + instead of just calling thread.interrupt_main() + +- change license from GPL to MIT + +- introduce execnet.dump/load variants of dumps/loads + serializing/unserializing mechanism. + +- improve channel.receive() communication latency on python2 + by changing the default timeout of the underlying Queue.get + to a regular None instead of the previous default -1 + which caused an internal positive timeout value + (a hack probably introduced to allow CTRL-C to pass + through for Thu, 14 Dec 2017 17:53:54 +0000 + +execnet (1.4.1-4) unstable; urgency=medium + + * package orphaned: + + deb/control: maintainer set to Debian QA Group. + + -- Daniel Stender Mon, 19 Jun 2017 09:24:54 +0200 + +execnet (1.4.1-3.1) unstable; urgency=medium + + * Non-maintainer upload. + * Disable tests that are often failing (Closes: #854494, #858189). + + -- Thomas Goirand Tue, 04 Apr 2017 20:30:05 +0000 + +execnet (1.4.1-3) unstable; urgency=medium + + * add test_gateway-fix-race-condition.patch (Closes: #840823 [thanks + to Andreas Cadhalpun]. + * deb/clean: add .cache/. + * run DEP-8 tests for Python3 without warnings (Closes: #849466). + + -- Daniel Stender Sun, 22 Jan 2017 16:05:01 +0100 + +execnet (1.4.1-2) unstable; urgency=medium + + * deb/control: + + add Breaks & Replaces for execnet-doc package (Closes: #840108). + + add relationships between packages and docs (Suggests and Recommends). + + -- Daniel Stender Sat, 08 Oct 2016 13:08:22 +0200 + +execnet (1.4.1-1) unstable; urgency=medium + + * build with execnet_1.4.1.orig.tar.xz (stripped Files-Excluded). + * build with Python 3 package (Closes: #782919): + + add package description in deb/control. + + add Python 3 packages to build-deps in deb/control. + + build with dh_python3 in deb/rules. + * deb/control: + + put myself into Maintainer field. + + bump Debhelper version to 9 (also in deb/compat). + + add dh-python and pytest to build-deps. + + bump standards to 3.9.8 (no changes needed). + + add Vcs fields (source is at collab-maint). + + remove Provides and Breaks (obsolete). + + cosmetics (wrap runtime deps, remove trailing whitespace, fix typo). + * deb/copyright: + + rewritten to meet DEP-5. + + add egg-info/ and .hg-s to Files-Excluded. + * deb/rules: + + build with buildsystem=pybuild, add export for PYBUILD_NAME. + + add override for dh_auto_test (invoking pytest). + + add override for dh_installchangelogs. + + use dh_installdocs to contribute ISSUES.txt. + + cosmetics, remove spurious and obsolete lines and overrides. + * deb/clean: clean whole egg-info/ folder. + * deb/watch: watch pypi.debian.net. + * add: + + deb/gbp.conf. + + conf.py-int-for-linkcheck_timeout.patch. + + remove-privacy-breach-google-adsense.patch + * build execnet-doc package: + + add package description in deb/control. + + build with dh_sphinxdoc in deb/rules. + + rename deb/python-execnet.doc-base, updated. + + remove deb/docs (not needed anymore, README.txt obsolete). + * run DEP-8 tests. + + -- Daniel Stender Thu, 06 Oct 2016 23:21:30 +0200 + +execnet (1.4.1-0.1) unstable; urgency=medium + + * Non-maintainer upload. + * New upstream release (Closes: #790677). + * deb/control: + + added version to apipkg build-dep. + + added setuptools and setuptools-scm to build-deps. + + wrapped build-deps. + * drop deb/patches/external-apipkg (not needed anymore). + + -- Daniel Stender Mon, 26 Sep 2016 19:42:42 +0200 + execnet (1.0.9-0.1) unstable; urgency=low * Non-maintainer upload diff -Nru execnet-1.0.9/debian/clean execnet-1.4.1/debian/clean --- execnet-1.0.9/debian/clean 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/clean 2016-12-04 15:15:51.000000000 +0000 @@ -1 +1,2 @@ -execnet.egg-info/SOURCES.txt +execnet.egg-info/ +.cache/ \ No newline at end of file diff -Nru execnet-1.0.9/debian/compat execnet-1.4.1/debian/compat --- execnet-1.0.9/debian/compat 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/compat 2016-10-06 18:44:31.000000000 +0000 @@ -1 +1 @@ -7 +9 diff -Nru execnet-1.0.9/debian/control execnet-1.4.1/debian/control --- execnet-1.0.9/debian/control 2011-02-26 21:31:29.000000000 +0000 +++ execnet-1.4.1/debian/control 2017-06-19 07:24:51.000000000 +0000 @@ -1,21 +1,82 @@ Source: execnet Priority: optional -Maintainer: Adam Schmalhofer -Build-Depends: debhelper (>= 7.0.50~), python-all (>= 2.6.6-3), python-sphinx, python-apipkg -Standards-Version: 3.9.1 +Maintainer: Debian QA Group +Build-Depends: + debhelper (>= 9), + dh-python, + python-all (>= 2.6.6-3), + python3-all, + python-apipkg (>= 1.4), + python3-apipkg, + python-setuptools, + python3-setuptools, + python-setuptools-scm, + python3-setuptools-scm, + python-pytest, + python3-pytest, + python-sphinx +Standards-Version: 3.9.8 Section: python Homepage: http://codespeak.net/execnet/ +Vcs-Git: https://anonscm.debian.org/git/collab-maint/execnet.git +Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/execnet.git Package: python-execnet Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, libjs-jquery, python-apipkg -Provides: ${python:Provides} -Breaks: ${python:Breaks} -Description: rapid multi-Python deployment +Depends: + ${python:Depends}, + ${misc:Depends}, + python-apipkg (>= 1.4) +Suggests: + execnet-doc +Description: rapid multi-Python deployment (Python 2) 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: + interpreters across version, platform and network barriers. It provides + a minimal and fast API targeting the following uses: . * distribute tasks to local or remote CPUs * write and deploy hybrid multi-process applications * write scripts to administer a bunch of exec environments + . + This package contains execnet for Python 2. + +Package: python3-execnet +Architecture: all +Depends: + ${python3:Depends}, + ${misc:Depends} +Suggests: + execnet-doc +Description: rapid multi-Python deployment (Python 3) + 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 targeting the following uses: + . + * distribute tasks to local or remote CPUs + * write and deploy hybrid multi-process applications + * write scripts to administer a bunch of exec environments + . + This package contains execnet for Python 3. + +Package: execnet-doc +Architecture: all +Section: doc +Depends: + ${misc:Depends}, + ${sphinxdoc:Depends} +Recommends: + python-execnet | python3-execnet +Breaks: + python-execnet (<= 1.4.1-0.1) +Replaces: + python-execnet (<= 1.4.1-0.1) +Description: rapid multi-Python deployment (docs) + 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 targeting the following uses: + . + * distribute tasks to local or remote CPUs + * write and deploy hybrid multi-process applications + * write scripts to administer a bunch of exec environments + . + This package contains the documentation. diff -Nru execnet-1.0.9/debian/copyright execnet-1.4.1/debian/copyright --- execnet-1.0.9/debian/copyright 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/copyright 2016-10-06 20:50:48.000000000 +0000 @@ -1,62 +1,34 @@ -This work was packaged for Debian by: - - Adam Schmalhofer on Wed, 02 Dec 2009 08:13:35 +0100 - -It was downloaded from http://pypi.python.org/packages/source/e/execnet/ - -Upstream Author: - - Holger Krekel - -Copyright: - - 2009 Holger Krekel - -License: - - The execnet package is released under the provisions of the Gnu Public - License (GPL), version 2 or later. - - This package contains some code and contributions from - - Armin Rigo, Benjamin Peterson, Carl Friedrich Bolz, - Maciej Fijalkowski, Ronny Pfannschmidt and others - - which are usable 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 contact Holger Krekel. - - execnet/apipkg.py is Copyright 2009 Holger Krekel and licensed under the - MIT license. - - On Debian systems, the complete text of the GNU General Public - License, version 2, can be found in /usr/share/common-licenses/GPL-2. - - The MIT licenses is: - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to - deal in the Software without restriction, including without limitation the - rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. - -The Debian packaging is: - - Copyright (C) 2009 Adam Schmalhofer - - and is licensed under the GPL version 2, or any later version - see `/usr/share/common-licenses/GPL-2'. +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: execnet +Source: https://bitbucket.org/hpk42/execnet +Files-Excluded: execnet.egg-info + .hgignore + .hgtags + +Files: * +Copyright: 2009-2016 Holger Krekel +License: Expat + +Files: debian/* +Copyright: 2009-2011 Adam Schmalhofer + 2016 Daniel Stender +License: Expat + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff -Nru execnet-1.0.9/debian/docs execnet-1.4.1/debian/docs --- execnet-1.0.9/debian/docs 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/docs 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -README.txt -doc/_build/html diff -Nru execnet-1.0.9/debian/execnet-doc.doc-base execnet-1.4.1/debian/execnet-doc.doc-base --- execnet-1.0.9/debian/execnet-doc.doc-base 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/execnet-doc.doc-base 2016-10-06 19:16:51.000000000 +0000 @@ -0,0 +1,9 @@ +Document: execnet +Title: python-execnet Manual +Author: Holger Krekel +Abstract: Examples and API documentation for python-execnet +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/execnet-doc/html/index.html +Files: /usr/share/doc/execnet-doc/html/*.html diff -Nru execnet-1.0.9/debian/gbp.conf execnet-1.4.1/debian/gbp.conf --- execnet-1.0.9/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/gbp.conf 2016-10-06 18:58:44.000000000 +0000 @@ -0,0 +1,8 @@ +[DEFAULT] +upstream-tag = upstream/%(version)s +debian-tag = debian/%(version)s +pristine-tar = True + +[import-orig] +upstream-branch = upstream +debian-branch = master diff -Nru execnet-1.0.9/debian/patches/conf.py-int-for-linkcheck_timeout.patch execnet-1.4.1/debian/patches/conf.py-int-for-linkcheck_timeout.patch --- execnet-1.0.9/debian/patches/conf.py-int-for-linkcheck_timeout.patch 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/patches/conf.py-int-for-linkcheck_timeout.patch 2016-10-06 19:27:56.000000000 +0000 @@ -0,0 +1,16 @@ +Description: don't use float for linkcheck_timeout in doc/conf.py +Author: Daniel Stender +Forwarded: no +Last-Update: 2016-10-06 + +--- a/doc/conf.py ++++ b/doc/conf.py +@@ -77,7 +77,7 @@ + # unit titles (such as .. function::). + add_module_names = True + +-linkcheck_timeout = 20.0 ++linkcheck_timeout = 20 + + # If true, sectionauthor and moduleauthor directives will be shown in the + # output. They are ignored by default. diff -Nru execnet-1.0.9/debian/patches/external-apipkg execnet-1.4.1/debian/patches/external-apipkg --- execnet-1.0.9/debian/patches/external-apipkg 2012-05-26 14:26:59.000000000 +0000 +++ execnet-1.4.1/debian/patches/external-apipkg 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -Prepare use (separately) packaged version of apipkg instead of embedded one. -execnet/apipkg.{py,pyc} are deleted via debian/rules to avoid patch not -applying on upstream upgrades. - ---- a/execnet/__init__.py -+++ b/execnet/__init__.py -@@ -5,9 +5,9 @@ - """ - __version__ = '1.0.9' - --import execnet.apipkg -+import apipkg - --execnet.apipkg.initpkg(__name__, { -+apipkg.initpkg(__name__, { - 'PopenGateway': '.deprecated:PopenGateway', - 'SocketGateway': '.deprecated:SocketGateway', - 'SshGateway': '.deprecated:SshGateway', diff -Nru execnet-1.0.9/debian/patches/remove-broken-tests.patch execnet-1.4.1/debian/patches/remove-broken-tests.patch --- execnet-1.0.9/debian/patches/remove-broken-tests.patch 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/patches/remove-broken-tests.patch 2017-04-05 08:24:38.000000000 +0000 @@ -0,0 +1,58 @@ +Description: Remove broken tests + As per the bug reports, the 2 tests disabled by this patch are flaky, + and produce random FTBFS. +Author: Thomas Goirand +Bug-Debian: https://bugs.debian.org/854494 +Bug-Debian: https://bugs.debian.org/858189 +Forwarded: no +Last-Update: 2017-04-04 + +--- execnet-1.4.1.orig/testing/test_gateway.py ++++ execnet-1.4.1/testing/test_gateway.py +@@ -237,20 +237,6 @@ class TestPopenGateway: + assert rinfo.cwd == py.std.os.getcwd() + assert rinfo.version_info == py.std.sys.version_info + +- def test_waitclose_on_remote_killed(self, makegateway): +- gw = 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_sysexit(self, gw): + channel = gw.remote_exec(""" + raise SystemExit() +--- execnet-1.4.1.orig/testing/test_xspec.py ++++ execnet-1.4.1/testing/test_xspec.py +@@ -118,23 +118,6 @@ class TestMakegateway: + assert rinfo.cwd == os.getcwd() + assert rinfo.version_info == sys.version_info + +- @pytest.mark.skipif("not hasattr(os, 'nice')") +- def test_popen_nice(self, makegateway): +- gw = makegateway("popen") +- +- def getnice(channel): +- import os +- if hasattr(os, 'nice'): +- channel.send(os.nice(0)) +- else: +- channel.send(None) +- remotenice = gw.remote_exec(getnice).receive() +- gw.exit() +- if remotenice is not None: +- gw = makegateway("popen//nice=5") +- remotenice2 = gw.remote_exec(getnice).receive() +- assert remotenice2 == remotenice + 5 +- + def test_popen_env(self, makegateway): + gw = makegateway("popen//env:NAME123=123") + ch = gw.remote_exec(""" diff -Nru execnet-1.0.9/debian/patches/remove-privacy-breach-google-adsense.patch execnet-1.4.1/debian/patches/remove-privacy-breach-google-adsense.patch --- execnet-1.0.9/debian/patches/remove-privacy-breach-google-adsense.patch 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/patches/remove-privacy-breach-google-adsense.patch 2016-10-06 19:43:31.000000000 +0000 @@ -0,0 +1,21 @@ +Description: remove google-analytics from docs +Author: Daniel Stender +Forwarded: not-needed +Last-Update: 2016-10-06 + +--- a/doc/_templates/layout.html ++++ b/doc/_templates/layout.html +@@ -21,13 +21,4 @@ + + {% block footer %} + {{ super() }} +- +- + {% endblock %} diff -Nru execnet-1.0.9/debian/patches/series execnet-1.4.1/debian/patches/series --- execnet-1.0.9/debian/patches/series 2011-02-26 21:34:43.000000000 +0000 +++ execnet-1.4.1/debian/patches/series 2017-04-05 08:24:38.000000000 +0000 @@ -1 +1,4 @@ -external-apipkg +test_gateway-fix-race-condition.patch +remove-privacy-breach-google-adsense.patch +conf.py-int-for-linkcheck_timeout.patch +remove-broken-tests.patch diff -Nru execnet-1.0.9/debian/patches/test_gateway-fix-race-condition.patch execnet-1.4.1/debian/patches/test_gateway-fix-race-condition.patch --- execnet-1.0.9/debian/patches/test_gateway-fix-race-condition.patch 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/patches/test_gateway-fix-race-condition.patch 2017-01-22 14:58:42.000000000 +0000 @@ -0,0 +1,31 @@ +Description: stall some loops in test_gateway a little bit to prevent + test failure and build break which occurs sometimes on this. +Author: Andreas Cadhalpun +Bug-Debian: https://bugs.debian.org/840823 + +--- a/testing/test_gateway.py ++++ b/testing/test_gateway.py +@@ -2,6 +2,7 @@ + mostly functional tests of gateways. + """ + import os ++import time + import py + import pytest + import execnet +@@ -84,6 +85,7 @@ + status = gw.remote_status() + if status.numexecuting == 0: + break ++ time.sleep(0.1) + else: + pytest.fail("did not get correct remote status") + # race condition +@@ -358,6 +360,7 @@ + rstatus = gw.remote_status() + if rstatus.numexecuting == 0: + return ++ time.sleep(0.1) + assert 0, "numexecuting didn't drop to zero" + + diff -Nru execnet-1.0.9/debian/python-execnet.doc-base execnet-1.4.1/debian/python-execnet.doc-base --- execnet-1.0.9/debian/python-execnet.doc-base 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/python-execnet.doc-base 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -Document: execnet -Title: python-execnet Manual -Author: Holger Krekel -Abstract: Examples and API documentation for python-execnet -Section: Programming/Python - -Format: HTML -Index: /usr/share/doc/python-execnet/html/index.html -Files: /usr/share/doc/python-execnet/html/*.html diff -Nru execnet-1.0.9/debian/rules execnet-1.4.1/debian/rules --- execnet-1.0.9/debian/rules 2011-02-26 21:31:14.000000000 +0000 +++ execnet-1.4.1/debian/rules 2016-10-06 21:15:44.000000000 +0000 @@ -1,24 +1,15 @@ #!/usr/bin/make -f -# -*- makefile -*- +export PYBUILD_NAME=execnet %: - dh --with python2 $@ + dh $@ --buildsystem=pybuild --with python2,python3,sphinxdoc -override_dh_installdocs: - make -C doc html - dh_installdocs - dh_link /usr/share/javascript/jquery/jquery.min.js \ - /usr/share/doc/python-execnet/html/_static/jquery.js - dh_link /usr/share/doc/python-execnet/html/_sources/ \ - /usr/share/doc/python-execnet/rst - -override_dh_auto_build: - rm -f execnet/apipkg.py execnet/apipkg.pyc - dh_auto_build +override_dh_auto_test: + PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS="{interpreter} -m pytest -v -x -rs testing/" dh_auto_test -override_dh_clean: - dh_clean - make -C doc clean +override_dh_installdocs: + dh_installdocs -A ISSUES.txt + PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -b html -N doc/ debian/execnet-doc/usr/share/doc/execnet-doc/html/ -override_dh_compress: - dh_compress -X.js -X.html +override_dh_installchangelogs: + dh_installchangelogs CHANGELOG diff -Nru execnet-1.0.9/debian/tests/control execnet-1.4.1/debian/tests/control --- execnet-1.0.9/debian/tests/control 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/tests/control 2016-10-06 20:34:11.000000000 +0000 @@ -1,2 +1,5 @@ -Tests: pytest -Depends: python-codespeak-lib +Tests: python-execnet +Depends: python-all, python-apipkg, python-pytest + +Tests: python3-execnet +Depends: python3-all, python3-apipkg, python3-pytest diff -Nru execnet-1.0.9/debian/tests/pytest execnet-1.4.1/debian/tests/pytest --- execnet-1.0.9/debian/tests/pytest 2010-05-02 14:12:44.000000000 +0000 +++ execnet-1.4.1/debian/tests/pytest 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh - -py.test diff -Nru execnet-1.0.9/debian/tests/python3-execnet execnet-1.4.1/debian/tests/python3-execnet --- execnet-1.0.9/debian/tests/python3-execnet 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/tests/python3-execnet 2017-01-12 23:18:17.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +for py in $(py3versions -i); do echo "[*] testing on $py:"; $py -m pytest -v -x -rs testing/ 2>&1; done diff -Nru execnet-1.0.9/debian/tests/python-execnet execnet-1.4.1/debian/tests/python-execnet --- execnet-1.0.9/debian/tests/python-execnet 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/debian/tests/python-execnet 2016-10-06 20:33:20.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +for py in $(pyversions -i); do echo "[*] testing on $py:"; $py -Wd -m pytest -v -x -rs testing/ 2>&1; done diff -Nru execnet-1.0.9/debian/watch execnet-1.4.1/debian/watch --- execnet-1.0.9/debian/watch 2012-05-26 14:38:56.000000000 +0000 +++ execnet-1.4.1/debian/watch 2016-10-06 18:57:36.000000000 +0000 @@ -1,2 +1,3 @@ version=3 -http://pypi.python.org/packages/source/e/execnet/ execnet-(.+).tar.gz +opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ +https://pypi.debian.net/execnet/execnet-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru execnet-1.0.9/doc/basics.txt execnet-1.4.1/doc/basics.txt --- execnet-1.0.9/doc/basics.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/basics.txt 2015-04-01 15:46:53.000000000 +0000 @@ -10,11 +10,11 @@ .. image:: _static/basic1.png +.. currentmodule:: execnet + Gateways: bootstrapping Python interpreters =================================================== -.. currentmodule:: execnet - All Gateways are instantiated via a call to ``makegateway()`` passing it a gateway specification or URL. @@ -32,20 +32,43 @@ examples for valid gateway specifications ------------------------------------------- -* ``ssh=wyvern//python=python2.4//chdir=mycache`` specifies a Python2.4 +* ``ssh=wyvern//python=python3.3//chdir=mycache`` specifies a Python3.3 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. +* ``ssh=-p 5000 myhost`` makes execnet pass "-p 5000 myhost" arguments + to the underlying ssh client binary, effectively specifying a custom port. + +* ``vagrant_ssh=default`` makes execnet connect to a Vagrant VM named + ``default`` via SSH through Vagrant's ``vagrant ssh`` command. It supports + the same additional parameters as regular SSH connections. + +* ``popen//python=python2.6//nice=20`` specification of + a python subprocess using the ``python2.6`` executable which must be + discoverable through the system ``PATH``; running with the lowest + CPU priority ("nice" level). By default current dir will be the + current dir of the instantiator. + +* ``popen//dont_write_bytecode`` uses the same executable as the current + Python, and also passes the ``-B`` flag on startup, which tells Python not + write ``.pyc`` or ``.pyo`` files. Note that this only works under CPython + 2.6 and newer. * ``popen//env:NAME=value`` specifies a subprocess that uses the same interpreter as the one it is initiated from and additionally remotely sets an environment variable ``NAME`` to ``value``. +* ``popen//execmodel=eventlet`` specifies a subprocess that uses the + same interpreter as the one it is initiated from but will run the + other side using eventlet for handling IO and dispatching threads. + * ``socket=192.168.1.4:8888`` specifies a Python Socket server - process that listens on 192.168.1.4:8888`` + process that listens on ``192.168.1.4:8888`` + +.. versionadded:: 1.5 + +* ``vagarant_ssh`` opens a python interpreter via the vagarant ssh command + .. _`remote execute code`: @@ -55,7 +78,7 @@ .. currentmodule:: execnet.gateway All gateways offer a simple method to execute source code -in the connected interpreter: +in the instantiated subprocess-interpreter: .. automethod:: Gateway.remote_exec(source) @@ -115,6 +138,49 @@ processes then you often want to call ``group.terminate()`` yourself and specify a larger or not timeout. + +threading models: gevent, eventlet, thread +=========================================== + +.. versionadded:: 1.2 (status: experimental!) + +execnet supports "thread", "eventlet" and "gevent" as thread models +on each of the two sides. You need to decide which model to use +before you create any gateways:: + + # content of threadmodel.py + import execnet + # locally use "eventlet", remotely use "thread" model + execnet.set_execmodel("eventlet", "thread") + gw = execnet.makegateway() + print (gw) + print (gw.remote_status()) + print (gw.remote_exec("channel.send(1)").receive()) + +You need to have eventlet installed in your environment and then +you can execute this little test file:: + + $ python threadmodel.py + + + 1 + +.. note:: + + With python3 you can (as of December 2013) only use the thread model + because neither eventlet-0.14.0 nor gevent-1.0 support Python3. + When they start to support Python3, execnet will probably just work + because it is itself Python3 compatible. + +How to execute in the main thread +------------------------------------------------ + +When the remote side of a gateway uses the 'thread' model, execution +will preferably run in the main thread. This allows GUI loops +or other code to behave correctly. If you, however, start multiple +executions concurrently, they will run in non-main threads. + + remote_status: get low-level execution info =================================================== @@ -127,9 +193,7 @@ 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 is busy executing code -and can thus be used to query status in a non-blocking manner. +channels are active. rsync: synchronise filesystem with remote =============================================================== @@ -151,7 +215,6 @@ .. autoclass:: RSync :members: add_target,send - Debugging execnet =============================================================== @@ -161,3 +224,27 @@ :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) + +.. _`dumps/loads`: +.. _`dumps/loads API`: + +cross-interpreter serialization of python objects +======================================================= + +.. versionadded:: 1.1 + +Execnet exposes a function pair which you can safely use to +store and load values from different Python interpreters +(e.g. Python2 and Python3, PyPy and Jython). Here is +a basic example:: + + >>> import execnet + >>> dump = execnet.dumps([1,2,3]) + >>> execnet.loads(dump) + [1,2,3] + +For more examples see :ref:`dumps/loads examples`. + +.. autofunction:: execnet.dumps(spec) +.. autofunction:: execnet.loads(spec) + diff -Nru execnet-1.0.9/doc/check_sphinx.py execnet-1.4.1/doc/check_sphinx.py --- execnet-1.0.9/doc/check_sphinx.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/check_sphinx.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,16 +1,17 @@ -import py import subprocess + + def test_linkcheck(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") - subprocess.check_call( - ["sphinx-build", "-W", "-blinkcheck", - "-d", str(doctrees), ".", str(htmldir)]) + subprocess.check_call([ + "sphinx-build", "-W", "-blinkcheck", + "-d", str(doctrees), ".", str(htmldir)]) + def test_build_docs(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") subprocess.check_call([ "sphinx-build", "-W", "-bhtml", - "-d", str(doctrees), ".", str(htmldir)]) - + "-d", str(doctrees), ".", str(htmldir)]) diff -Nru execnet-1.0.9/doc/conf.py execnet-1.4.1/doc/conf.py --- execnet-1.0.9/doc/conf.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/conf.py 2015-04-01 14:42:44.000000000 +0000 @@ -3,7 +3,7 @@ # execnet documentation build configuration file, created by # sphinx-quickstart on Wed Sep 30 21:16:59 2009. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the wd set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,16 +11,17 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os - +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.dirname(os.path.dirname(__file__))) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be extensions +# Add any Sphinx extension module names here, as strings. +# They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] @@ -31,99 +32,101 @@ source_suffix = '.txt' # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = 'execnet' -copyright = '2009, holger krekel and others' +copyright = '2012, holger krekel and others' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -import execnet -version = "1.0.8" -# The full version, including alpha/beta/rc tags. -release = version +version = '1.4' +# The full version, inpipcluding alpha/beta/rc tags. +release = '1.4.0.dev1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# The reST default role (used for this markup: `text`) to use for all documents +# dfault_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True +linkcheck_timeout = 20.0 + # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'sphinxdoc' -#html_index = 'index.html' -html_sidebars = {'index': 'indexsidebar.html', +# html_index = 'index.html' +html_sidebars = { + 'index': 'indexsidebar.html', } # 'basics': 'indexsidebar.html', -#} -#html_additional_pages = {'index': 'index.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. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = "codespeak.png" +# html_logo = "codespeak.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -132,20 +135,20 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # If false, no module index is generated. html_use_modindex = False # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False @@ -153,25 +156,25 @@ # 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 # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'execnetdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ('index', 'execnet.tex', 'execnet Documentation', 'holger krekel and others', 'manual'), @@ -179,17 +182,17 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True diff -Nru execnet-1.0.9/doc/example/hybridpython.txt execnet-1.4.1/doc/example/hybridpython.txt --- execnet-1.0.9/doc/example/hybridpython.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/hybridpython.txt 2015-02-12 20:06:23.000000000 +0000 @@ -1,6 +1,38 @@ Connecting different Python interpreters ========================================== +.. _`dumps/loads examples`: + +Dumping and loading values across interpreter versions +---------------------------------------------------------- + +.. versionadded:: 1.1 + +Execnet offers a new safe and fast :ref:`dumps/loads API` which you +can use to dump builtin python data structures and load them +later with the same or a different python interpreter (including +between Python2 and Python3). The standard library offers +the pickle and marshal modules but they do not work safely +between different interpreter versions. Using xml/json +requires a mapping of Python objects and is not easy to +get right. Moreover, execnet allows to control handling +of bytecode/strings/unicode types. Here is an example:: + + # using python2 + import execnet + with open("data.py23", "wb") as f: + f.write(execnet.dumps(["hello", "world"])) + + # using Python3 + import execnet + with open("data.py23", "rb") as f: + val = execnet.loads(f.read(), py2str_as_py3str=True) + assert val == ["hello", "world"] + +See the :ref:`dumps/loads API` for more details on string +conversion options. Please note, that you can not dump +user-level instances, only builtin python types. + Connect to Python2/Numpy from Python3 ---------------------------------------- @@ -41,27 +73,27 @@ ------------------------------------------------------------- Sometimes the default configuration of string coercion (2str to 3str, 3str to 2unicode) -is inconvient, thus it can be reconfigured via `gw.reconfigure`. +is inconvient, thus it can be reconfigured via `gw.reconfigure` and `channel.reconfigure`. Here is an example session on a Python2 interpreter:: >>> import execnet - >>> execnet.makegateway("popen//python=python3.1") - - >>> gw=execnet.makegateway("popen//python=python3.1") + >>> execnet.makegateway("popen//python=python3.2") + + >>> gw=execnet.makegateway("popen//python=python3.2") >>> gw.remote_exec("channel.send('hello')").receive() u'hello' >>> gw.reconfigure(py3str_as_py2str=True) >>> gw.remote_exec("channel.send('hello')").receive() 'hello' - >>> ch = gw.remote_exec("channel.send(isinstance(channel.receive(), bytes)") + >>> ch = gw.remote_exec('channel.send(type(channel.receive()).__name__)') >>> ch.send('a') >>> ch.receive() - False - >>> gw.reconfigure(py2str_as_py3str=False) - >>> ch = gw.remote_exec("channel.send(isinstance(channel.receive(), bytes)") + 'str' + >>> ch = gw.remote_exec('channel.send(type(channel.receive()).__name__)') + >>> ch.reconfigure(py2str_as_py3str=False) >>> ch.send('a') >>> ch.receive() - True + u'bytes' Work with Java objects from CPython diff -Nru execnet-1.0.9/doc/example/py3topy2.py execnet-1.4.1/doc/example/py3topy2.py --- execnet-1.0.9/doc/example/py3topy2.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/py3topy2.py 2015-02-14 23:21:47.000000000 +0000 @@ -1,6 +1,6 @@ import execnet -gw = execnet.PopenGateway("python2.6") +gw = execnet.makegateway("popen//python=python2") channel = gw.remote_exec(""" import numpy array = numpy.array([1,2,3]) diff -Nru execnet-1.0.9/doc/example/redirect_remote_output.py execnet-1.4.1/doc/example/redirect_remote_output.py --- execnet-1.0.9/doc/example/redirect_remote_output.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/redirect_remote_output.py 2015-02-12 20:06:23.000000000 +0000 @@ -7,9 +7,7 @@ - setting a callback for receiving channel data """ - -import py - +import execnet gw = execnet.makegateway() outchan = gw.remote_exec(""" diff -Nru execnet-1.0.9/doc/example/remotecmd.py execnet-1.4.1/doc/example/remotecmd.py --- execnet-1.0.9/doc/example/remotecmd.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/remotecmd.py 2015-02-12 20:06:23.000000000 +0000 @@ -6,7 +6,7 @@ def listdir(path): return os.listdir(path) - + if __name__ == '__channelexec__': for item in channel: channel.send(eval(item)) diff -Nru execnet-1.0.9/doc/example/svn-sync-repo.py execnet-1.4.1/doc/example/svn-sync-repo.py --- execnet-1.0.9/doc/example/svn-sync-repo.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/svn-sync-repo.py 2015-02-15 23:58:19.000000000 +0000 @@ -6,21 +6,24 @@ uses execnet. """ +import execnet +import py +import sys +import os -import py, execnet -import sys, os def usage(): arg0 = sys.argv[0] - print "%s [user@]remote-host:/repo/location localrepo [ssh-config-file]" % (arg0,) + print arg0, \ + "[user@]remote-host:/repo/location localrepo [ssh-config-file]" 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: + raise SystemExit("localrepo %s does not exist" % (localrepo,)) + if len(args) == 3: configfile = args[2] else: configfile = None @@ -73,7 +76,7 @@ """) c.send((local_rev, path)) - print "checking revisions from %d in %s" %(local_rev, remote) + print "checking revisions from %d in %s" % (local_rev, remote) while 1: revstart, revend = c.receive() dumpchannel = c.receive() @@ -81,13 +84,14 @@ 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 (execnet does not control # RAM usage or receive queue sizes) dumpchannel.send(maxcount) - f = os.popen("svnadmin load -q %s" %(repo, ), "w") + f = os.popen("svnadmin load -q %s" % (repo,), "w") count = maxcount for x in dumpchannel: sys.stdout.write(".") @@ -100,10 +104,12 @@ print >>sys.stdout f.close() + def get_svn_youngest(repo): rev = py.process.cmdexec('svnlook youngest "%s"' % repo) return int(rev) + def getgateway(host, configfile=None): xspec = "ssh=%s" % host if configfile is not None: @@ -116,4 +122,3 @@ raise SystemExit(1) main(sys.argv[1:]) - diff -Nru execnet-1.0.9/doc/example/sysinfo.py execnet-1.4.1/doc/example/sysinfo.py --- execnet-1.0.9/doc/example/sysinfo.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/sysinfo.py 2015-02-15 23:58:19.000000000 +0000 @@ -3,7 +3,7 @@ obtain system info from remote machine. -(c) Holger Krekel, GPLv2 or 3 +(c) Holger Krekel, MIT license """ import py @@ -12,10 +12,15 @@ 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)") +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) @@ -28,6 +33,7 @@ l.append(sshname) return l + class RemoteInfo: def __init__(self, gateway): self.gw = gateway @@ -44,7 +50,7 @@ return self.exreceive(""" import %s channel.send(%s) - """ %(module, modpath)) + """ % (module, modpath)) def islinux(self): return self.getmodattr('sys.platform').find("linux") != -1 @@ -90,16 +96,20 @@ 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): - import execnet - spec = "ssh=%s" % sshname if ssh_config: - spec += "ssh_config=%s" % ssh_config + spec = "ssh=-F %s %s" % (ssh_config, sshname) + else: + spec += "ssh=%s" % sshname debug("connecting to", repr(spec)) try: gw = execnet.makegateway(spec) @@ -107,14 +117,14 @@ error("could not get sshgatway", sshname) else: ri = RemoteInfo(gw) - #print "%s info:" % sshname + # 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.write("%s %s: " % (prefix, attr,)) loginfo.flush() value = ri.getmodattr(attr) loginfo.write(str(value)) @@ -122,7 +132,7 @@ loginfo.flush() memswap = ri.getmemswap() if memswap: - mem,swap = memswap + mem, swap = memswap print >>loginfo, prefix, "Memory:", mem, "Swap:", swap cpuinfo = ri.getcpuinfo() if cpuinfo: @@ -143,4 +153,3 @@ for host in hosts: if host not in ignores: getinfo(host, ssh_config=ssh_config) - diff -Nru execnet-1.0.9/doc/example/taskserver.py execnet-1.4.1/doc/example/taskserver.py --- execnet-1.0.9/doc/example/taskserver.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/doc/example/taskserver.py 2015-02-15 23:58:19.000000000 +0000 @@ -0,0 +1,49 @@ + + +import execnet + +group = execnet.Group() +for i in range(4): # 4 CPUs + group.makegateway() + + +def process_item(channel): + # task processor, sits on each CPU + import time + import random + channel.send("ready") + for x in channel: + if x is None: # we can shutdown + break + # sleep random time, send result + time.sleep(random.randrange(3)) + channel.send(x*10) + +# execute taskprocessor everywhere +mch = group.remote_exec(process_item) + +# get a queue that gives us results +q = mch.make_receive_queue(endmarker=-1) +tasks = range(10) # a list of tasks, here just integers +terminated = 0 +while 1: + channel, item = q.get() + if item == -1: + terminated += 1 + print "terminated %s" % channel.gateway.id + if terminated == len(mch): + print "got all results, terminating" + break + continue + if item != "ready": + print "other side %s returned %r" % (channel.gateway.id, item) + if not tasks: + print "no tasks remain, sending termination request to all" + mch.send_each(None) + tasks = -1 + if tasks and tasks != -1: + task = tasks.pop() + channel.send(task) + print "sent task %r to %s" % (task, channel.gateway.id) + +group.terminate() diff -Nru execnet-1.0.9/doc/example/test_group.txt execnet-1.4.1/doc/example/test_group.txt --- execnet-1.0.9/doc/example/test_group.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/test_group.txt 2015-02-12 20:06:23.000000000 +0000 @@ -14,7 +14,7 @@ >>> group >>> list(group) - [, ] + [, ] >>> 'gw0' in group and 'gw1' in group True >>> group['gw0'] == group[0] @@ -37,7 +37,7 @@ >>> gw = group.makegateway("popen//id=sub1") >>> assert gw.id == "sub1" >>> group['sub1'] - + Getting (auto) IDs before instantiation ------------------------------------------------------ diff -Nru execnet-1.0.9/doc/example/test_info.txt execnet-1.4.1/doc/example/test_info.txt --- execnet-1.0.9/doc/example/test_info.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/test_info.txt 2015-02-12 20:06:23.000000000 +0000 @@ -95,13 +95,13 @@ >>> gw = execnet.makegateway("ssh=codespeak.net") >>> channel = gw.remote_exec(""" ... import sys, os - ... channel.send((sys.platform, sys.version_info, os.getpid())) + ... channel.send((sys.platform, tuple(sys.version_info), os.getpid())) ... """) >>> platform, version_info, remote_pid = channel.receive() >>> platform 'linux2' >>> version_info - (2, 4, 2, 'final', 0) + (2, 6, 6, 'final', 0) Use a callback instead of receive() and wait for completion ------------------------------------------------------------- @@ -186,7 +186,7 @@ itself into the remote socket endpoint:: import execnet - gw = execnet.SocketGateway("TARGET-IP:8888") + gw = execnet.makegateway("socket=TARGET-IP:8888") That's it, you can now use the gateway object just like a popen- or ssh-based one. diff -Nru execnet-1.0.9/doc/example/test_multi.txt execnet-1.4.1/doc/example/test_multi.txt --- execnet-1.0.9/doc/example/test_multi.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/example/test_multi.txt 2015-02-12 20:06:23.000000000 +0000 @@ -73,3 +73,19 @@ ... assert res1 == 42 >>> group + + + +saturate multiple Hosts and CPUs with tasks to process +-------------------------------------------------------- + +If you have multiple CPUs or hosts you can create as many +gateways and then have a process sit on each CPU and wait +for a task to proceed. One complication is that we +want to ensure clean termination of all processes +and loose no result. Here is an example that just uses +local subprocesses and does the task: + +.. include:: taskserver.py + :literal: + diff -Nru execnet-1.0.9/doc/example/test_proxy.txt execnet-1.4.1/doc/example/test_proxy.txt --- execnet-1.0.9/doc/example/test_proxy.txt 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/doc/example/test_proxy.txt 2015-02-12 20:06:23.000000000 +0000 @@ -0,0 +1,28 @@ +Managing Proxyed gateways +========================== + +Simple Proxying +---------------- + +Using the via arg of specs we can create a gateway +whose io os created on a remote gateway and +proxyed to the master. + +The simlest use case, is where one creates one master process +and uses it to controll new slaves and their environment + +:: + + >>> import execnet + >>> group = execnet.Group() + >>> group.defaultspec = 'popen//via=master' + >>> master = group.makegateway('popen//id=master') + >>> master + + >>> slave = group.makegateway() + >>> slave + + >>> group + + + diff -Nru execnet-1.0.9/doc/examples.txt execnet-1.4.1/doc/examples.txt --- execnet-1.0.9/doc/examples.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/examples.txt 2015-02-12 20:06:23.000000000 +0000 @@ -2,8 +2,8 @@ examples ============================================================================== -.. _`execnet-dev`: http://codespeak.net/mailman/listinfo/execnet-dev -.. _`execnet-commit`: http://codespeak.net/mailman/listinfo/execnet-commit +.. _`execnet-dev`: http://mail.python.org/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://mail.python.org/mailman/listinfo/execnet-commit Note: all examples with `>>>` prompts are automatically tested. @@ -12,6 +12,7 @@ example/test_info.txt example/test_group.txt + example/test_proxy.txt example/test_multi.txt example/hybridpython.txt example/test_debug.txt diff -Nru execnet-1.0.9/doc/index.txt execnet-1.4.1/doc/index.txt --- execnet-1.0.9/doc/index.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/index.txt 2015-04-01 14:42:44.000000000 +0000 @@ -26,16 +26,19 @@ * automatic bootstrapping: no manual remote installation. -* safe and simple serialization of python builtin types (no pickle) - for sending/receiving structured data messages. +* safe and simple serialization of python builtin + types for sending/receiving structured data messages. + (New in 1.1) execnet offers a new :ref:`dumps/loads ` + API which allows cross-interpreter compatible serialization + of Python builtin types. * flexible communication: synchronous send/receive as well as callback/queue mechanisms supported * easy creation, handling and termination of multiple processes -* well tested interactions between CPython 2.4-2.7, CPython3.1, Jython 2.5.1 - and PyPy 1.1 interpreters. +* well tested interactions between CPython 2.5-2.7, CPython-3.3, Jython 2.5.1 + and PyPy interpreters. * fully interoperable between Windows and Unix-ish systems. @@ -46,6 +49,8 @@ * `py.test`_ uses it for its `distributed testing`_ mechanism. +* `quora`_ uses it for `connecting CPython and PyPy`_. + * Jacob Perkins uses it for his `Distributed NTLK with execnet`_ project to launch computation processes through ssh. He also compares `disco and execnet`_ in a subsequent post. @@ -55,8 +60,11 @@ * sysadmins and developers are using it for ad-hoc custom scripting +.. _`quora`: http://quora.com +.. _`connecting CPython and PyPy`: http://www.quora.com/Quora-Infrastructure/Did-Quoras-switch-to-PyPy-result-in-increased-memory-consumption + .. _`py.test`: http://pytest.org -.. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html +.. _`distributed testing`: https://pypi.python.org/pypi/pytest-xdist .. _`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/ @@ -69,9 +77,7 @@ 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. +The package is licensed under the MIT license since version 1.2. .. _`basic API`: basics.html .. _`actively developed`: http://bitbucket.org/hpk42/execnet/changesets diff -Nru execnet-1.0.9/doc/Makefile execnet-1.4.1/doc/Makefile --- execnet-1.0.9/doc/Makefile 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/Makefile 2015-04-01 14:52:10.000000000 +0000 @@ -30,7 +30,7 @@ clean: -rm -rf $(BUILDDIR)/* -install: clean html +install: clean html rsync -avz $(BUILDDIR)/html/ code:www-execnet/ html: diff -Nru execnet-1.0.9/doc/rel-1.1.txt execnet-1.4.1/doc/rel-1.1.txt --- execnet-1.0.9/doc/rel-1.1.txt 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/doc/rel-1.1.txt 2015-02-12 20:06:23.000000000 +0000 @@ -0,0 +1,45 @@ +refactoring +execnet-1.1 is a backward compatible beta release. execnet provides a +share-nothing model with channel-send/receive communication for +distributing execution across many Python interpreters across version, +platform and network barriers. + +See below for more change info and here for extensive documentation +and tested examples: + + http://codespeak.net/execnet + +Particular thanks to Ronny Pfannschmidt for a lot of internal cleanups +and to Alex Gaynor for providing feature patches. + +Have fun, +holger + + +1.1 (compared to 1.0.9) +-------------------------------- + +- introduce execnet.dumps/loads providing serialization between + python interpreters. + +- group.remote_exec now supports kwargs as well + +- support per channel string coercion configuration + +- Popen2IO.read now reads correct amounts of bytes from nonblocking fd's + +- added a ``dont_write_bytecode`` option to Popen gateways, this sets the + ``sys.dont_write_bytecode`` flag on the spawned process, this only works on + CPython 2.6 and higher. Thanks to Alex Gaynor. + +- added a pytest --broken-isp option to skip tests that assume + DNS queries for unknown hosts actually are resolved as such (Thanks + Alex Gaynor) + +- fix issue 1 - decouple string coercion of channels and gateway + +- fix issue #2 - properly reconfigure the channels string coercion for rsync, + so it can send from python2 to python3 + +- refactor socketserver, so it can be directly remote_exec'd for starting a socket gateway on a remote + diff -Nru execnet-1.0.9/doc/support.txt execnet-1.4.1/doc/support.txt --- execnet-1.0.9/doc/support.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/support.txt 2015-02-12 20:06:23.000000000 +0000 @@ -13,7 +13,7 @@ .. _`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 +.. _`tetamap blog`: http://holgerkrekel.net +.. _`execnet-dev`: http://mail.python.org/mailman/listinfo/execnet-dev +.. _`execnet-commit`: http://mail.python.org/mailman/listinfo/execnet-commit .. _`bitbucket repository`: http://bitbucket.org/hpk42/execnet diff -Nru execnet-1.0.9/doc/_templates/indexsidebar.html execnet-1.4.1/doc/_templates/indexsidebar.html --- execnet-1.0.9/doc/_templates/indexsidebar.html 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/_templates/indexsidebar.html 2015-02-12 20:06:23.000000000 +0000 @@ -16,6 +16,6 @@

Questions? Suggestions?

-

Join +

Join execnet-dev mailing list

come to #pylib on FreeNode

diff -Nru execnet-1.0.9/doc/_templates/layout.html execnet-1.4.1/doc/_templates/layout.html --- execnet-1.0.9/doc/_templates/layout.html 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/doc/_templates/layout.html 2015-02-12 20:06:23.000000000 +0000 @@ -8,7 +8,7 @@
-

execnet: rapid multi-Python deployment

+

execnet: Distributed Python deployment and communication

home |  install |  diff -Nru execnet-1.0.9/execnet/apipkg.py execnet-1.4.1/execnet/apipkg.py --- execnet-1.0.9/execnet/apipkg.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/apipkg.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,95 +0,0 @@ -""" -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.0b6" - -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__', '0') - for name in ('__path__', '__loader__'): - if hasattr(oldmod, name): - setattr(mod, name, getattr(oldmod, name)) - 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__ = [x for x in importspec if x != '__onfirstaccess__'] - 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): - l = [] - if hasattr(self, '__version__'): - l.append("version=" + repr(self.__version__)) - if hasattr(self, '__file__'): - l.append('from ' + repr(self.__file__)) - if l: - return '' % (self.__name__, " ".join(l)) - return '' % (self.__name__,) - - def __makeattr(self, name): - """lazily compute value for name or raise AttributeError if unknown.""" - target = None - if '__onfirstaccess__' in self.__map__: - target = self.__map__.pop('__onfirstaccess__') - importobj(*target)() - try: - modpath, attrname = self.__map__[name] - except KeyError: - if target is not None and name != '__onfirstaccess__': - # retry, onfirstaccess might have set attrs - return getattr(self, name) - raise AttributeError(name) - else: - result = importobj(modpath, attrname) - setattr(self, name, result) - try: - del self.__map__[name] - except KeyError: - pass # in a recursive-import situation a double-del can happen - return result - - __getattr__ = __makeattr - - 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: - hasattr(self, 'some') - for name in self.__all__: - try: - self.__makeattr(name) - except AttributeError: - pass - return dict - __dict__ = property(__dict__) diff -Nru execnet-1.0.9/execnet/deprecated.py execnet-1.4.1/execnet/deprecated.py --- execnet-1.0.9/execnet/deprecated.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/deprecated.py 2015-02-15 23:58:19.000000000 +0000 @@ -5,6 +5,7 @@ """ import execnet + def PopenGateway(python=None): """ instantiate a gateway to a subprocess started with the given 'python' executable. @@ -14,6 +15,7 @@ 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 @@ -23,9 +25,10 @@ new_remote() method on existing gateways. """ APIWARN("1.0.0b4", "use makegateway('socket=host:port')") - spec = execnet.XSpec("socket=%s:%s" %(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. @@ -37,7 +40,8 @@ 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)) + Warn = DeprecationWarning("(since version %s) %s" % (version, msg)) warnings.warn(Warn, stacklevel=stacklevel) diff -Nru execnet-1.0.9/execnet/gateway_base.py execnet-1.4.1/execnet/gateway_base.py --- execnet-1.0.9/execnet/gateway_base.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/gateway_base.py 2015-09-02 18:15:40.000000000 +0000 @@ -1,25 +1,38 @@ """ base execnet gateway code send to the other side for bootstrapping. -NOTE: aims to be compatible to Python 2.3-3.1, Jython and IronPython +NOTE: aims to be compatible to Python 2.5-3.X, Jython and IronPython -(C) 2004-2009 Holger Krekel, Armin Rigo, Benjamin Peterson, and others +:copyright: 2004-2015 +:authors: + - Holger Krekel + - Armin Rigo + - Benjamin Peterson + - Ronny Pfannschmidt + - many others """ -import sys, os, weakref -import threading, traceback, struct -try: - import queue -except ImportError: - import Queue as queue +from __future__ import with_statement +import sys +import os +import weakref +import traceback +import struct + +# NOTE that we want to avoid try/except style importing +# to avoid setting sys.exc_info() during import +# ISPY3 = sys.version_info >= (3, 0) if ISPY3: + from io import BytesIO exec("def do_exec(co, loc): exec(co, loc)\n" "def reraise(cls, val, tb): raise val\n") unicode = str _long_type = int from _thread import interrupt_main + SUBPROCESS32 = False else: + from StringIO import StringIO as BytesIO exec("def do_exec(co, loc): exec co in loc\n" "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str @@ -28,6 +41,289 @@ from thread import interrupt_main except ImportError: interrupt_main = None + try: + import subprocess32 # NOQA + SUBPROCESS32 = True + except ImportError: + SUBPROCESS32 = False + sys.exc_clear() + + +# f = open("/tmp/execnet-%s" % os.getpid(), "w") +# def log_extra(*msg): +# f.write(" ".join([str(x) for x in msg]) + "\n") + + +class EmptySemaphore: + acquire = release = lambda self: None + + +def get_execmodel(backend): + if hasattr(backend, "backend"): + return backend + if backend == "thread": + importdef = { + 'get_ident': ['thread::get_ident', '_thread::get_ident'], + '_start_new_thread': ['thread::start_new_thread', + '_thread::start_new_thread'], + 'threading': ["threading"], + 'queue': ["queue", "Queue"], + 'sleep': ['time::sleep'], + 'subprocess': ['subprocess32' if SUBPROCESS32 else 'subprocess'], + 'socket': ['socket'], + '_fdopen': ['os::fdopen'], + '_lock': ['threading'], + '_event': ['threading'], + } + + def exec_start(self, func, args=()): + self._start_new_thread(func, args) + + elif backend == "eventlet": + importdef = { + 'get_ident': ['eventlet.green.thread::get_ident'], + '_spawn_n': ['eventlet::spawn_n'], + 'threading': ['eventlet.green.threading'], + 'queue': ["eventlet.queue"], + 'sleep': ['eventlet::sleep'], + 'subprocess': ['eventlet.green.subprocess'], + 'socket': ['eventlet.green.socket'], + '_fdopen': ['eventlet.green.os::fdopen'], + '_lock': ['eventlet.green.threading'], + '_event': ['eventlet.green.threading'], + } + + def exec_start(self, func, args=()): + self._spawn_n(func, *args) + elif backend == "gevent": + importdef = { + 'get_ident': ['gevent.thread::get_ident'], + '_spawn_n': ['gevent::spawn'], + 'threading': ['threading'], + 'queue': ["gevent.queue"], + 'sleep': ['gevent::sleep'], + 'subprocess': ['gevent.subprocess'], + 'socket': ['gevent.socket'], + # XXX + '_fdopen': ['gevent.fileobject::FileObjectThread'], + '_lock': ['gevent.lock'], + '_event': ['gevent.event'], + } + + def exec_start(self, func, args=()): + self._spawn_n(func, *args) + else: + raise ValueError("unknown execmodel %r" % (backend,)) + + class ExecModel: + def __init__(self, name): + self._importdef = importdef + self.backend = name + self._count = 0 + + def __repr__(self): + return "" % self.backend + + def __getattr__(self, name): + locs = self._importdef.get(name) + if locs is None: + raise AttributeError(name) + for loc in locs: + parts = loc.split("::") + loc = parts.pop(0) + try: + mod = __import__(loc, None, None, "__doc__") + except ImportError: + pass + else: + if parts: + mod = getattr(mod, parts[0]) + setattr(self, name, mod) + return mod + raise AttributeError(name) + + start = exec_start + + def fdopen(self, fd, mode, bufsize=1): + return self._fdopen(fd, mode, bufsize) + + def WorkerPool(self, hasprimary=False): + return WorkerPool(self, hasprimary=hasprimary) + + def Semaphore(self, size=None): + if size is None: + return EmptySemaphore() + return self._lock.Semaphore(size) + + def Lock(self): + return self._lock.RLock() + + def RLock(self): + return self._lock.RLock() + + def Event(self): + event = self._event.Event() + if sys.version_info < (2, 7): + # patch wait function to return event state instead of None + real_wait = event.wait + + def wait(timeout=None): + real_wait(timeout=timeout) + return event.isSet() + event.wait = wait + return event + + def PopenPiped(self, args): + PIPE = self.subprocess.PIPE + return self.subprocess.Popen(args, stdout=PIPE, stdin=PIPE) + + return ExecModel(backend) + + +class Reply(object): + """ reply instances provide access to the result + of a function execution that got dispatched + through WorkerPool.spawn() + """ + def __init__(self, task, threadmodel): + self.task = task + self._result_ready = threadmodel.Event() + self.running = True + + def get(self, timeout=None): + """ get the result object from an asynchronous function execution. + if the function execution raised an exception, + then calling get() will reraise that exception + including its traceback. + """ + self.waitfinish(timeout) + try: + return self._result + except AttributeError: + reraise(*(self._excinfo[:3])) # noqa + + def waitfinish(self, timeout=None): + if not self._result_ready.wait(timeout): + raise IOError("timeout waiting for %r" % (self.task, )) + + def run(self): + func, args, kwargs = self.task + try: + try: + self._result = func(*args, **kwargs) + except: + # sys may be already None when shutting down the interpreter + if sys is not None: + self._excinfo = sys.exc_info() + finally: + self._result_ready.set() + self.running = False + + +class WorkerPool(object): + """ A WorkerPool allows to spawn function executions + to threads, returning a reply object on which you + can ask for the result (and get exceptions reraised). + + This implementation allows the main thread to integrate + itself into performing function execution through + calling integrate_as_primary_thread() which will return + when the pool received a trigger_shutdown(). + """ + def __init__(self, execmodel, hasprimary=False): + """ by default allow unlimited number of spawns. """ + self.execmodel = execmodel + self._running_lock = self.execmodel.Lock() + self._running = set() + self._shuttingdown = False + self._waitall_events = [] + if hasprimary: + if self.execmodel.backend != "thread": + raise ValueError("hasprimary=True requires thread model") + self._primary_thread_task_ready = self.execmodel.Event() + else: + self._primary_thread_task_ready = None + + def integrate_as_primary_thread(self): + """ integrate the thread with which we are called as a primary + thread for executing functions triggered with spawn(). + """ + assert self.execmodel.backend == "thread", self.execmodel + primary_thread_task_ready = self._primary_thread_task_ready + # interacts with code at REF1 + while 1: + primary_thread_task_ready.wait() + reply = self._primary_thread_task + if reply is None: # trigger_shutdown() woke us up + break + self._perform_spawn(reply) + # we are concurrent with trigger_shutdown and spawn + with self._running_lock: + if self._shuttingdown: + break + primary_thread_task_ready.clear() + + def trigger_shutdown(self): + with self._running_lock: + self._shuttingdown = True + if self._primary_thread_task_ready is not None: + self._primary_thread_task = None + self._primary_thread_task_ready.set() + + def active_count(self): + return len(self._running) + + def _perform_spawn(self, reply): + reply.run() + with self._running_lock: + self._running.remove(reply) + if not self._running: + while self._waitall_events: + waitall_event = self._waitall_events.pop() + waitall_event.set() + + def _try_send_to_primary_thread(self, reply): + # REF1 in 'thread' model we give priority to running in main thread + # note that we should be called with _running_lock hold + primary_thread_task_ready = self._primary_thread_task_ready + if primary_thread_task_ready is not None: + if not primary_thread_task_ready.isSet(): + self._primary_thread_task = reply + # wake up primary thread + primary_thread_task_ready.set() + return True + return False + + def spawn(self, func, *args, **kwargs): + """ return Reply object for the asynchronous dispatch + of the given func(*args, **kwargs). + """ + reply = Reply((func, args, kwargs), self.execmodel) + with self._running_lock: + if self._shuttingdown: + raise ValueError("pool is shutting down") + self._running.add(reply) + if not self._try_send_to_primary_thread(reply): + self.execmodel.start(self._perform_spawn, (reply,)) + return reply + + def terminate(self, timeout=None): + """ trigger shutdown and wait for completion of all executions. """ + self.trigger_shutdown() + return self.waitall(timeout=timeout) + + def waitall(self, timeout=None): + """ wait until all active spawns have finished executing. """ + with self._running_lock: + if not self._running: + return True + # if a Reply still runs, we let run_and_release + # signal us -- note that we are still holding the + # _running_lock to avoid race conditions + my_waitall_event = self.execmodel.Event() + self._waitall_events.append(my_waitall_event) + return my_waitall_event.wait(timeout=timeout) + sysex = (KeyboardInterrupt, SystemExit) @@ -41,11 +337,14 @@ sys.stderr.write("[%s] %s\n" % (pid, line)) sys.stderr.flush() except Exception: - pass # nothing we can do, likely interpreter-shutdown + pass # nothing we can do, likely interpreter-shutdown elif DEBUG: - import tempfile, os.path + import tempfile + import os fn = os.path.join(tempfile.gettempdir(), 'execnet-debug-%d' % pid) + # sys.stderr.write("execnet-debug at %r" % (fn,)) debugfile = open(fn, 'w') + def trace(*msg): try: line = " ".join(map(str, msg)) @@ -53,18 +352,19 @@ debugfile.flush() except Exception: try: - v = exc_info()[1] + v = sys.exc_info()[1] sys.stderr.write( "[%s] exception during tracing: %r\n" % (pid, v)) except Exception: - pass # nothing we can do, likely interpreter-shutdown + pass # nothing we can do, likely interpreter-shutdown else: notrace = trace = lambda *msg: None + class Popen2IO: error = (IOError, OSError, EOFError) - def __init__(self, outfile, infile): + def __init__(self, outfile, infile, execmodel): # we need raw byte streams self.outfile, self.infile = outfile, infile if sys.platform == "win32": @@ -76,15 +376,17 @@ pass self._read = getattr(infile, "buffer", infile).read self._write = getattr(outfile, "buffer", outfile).write + self.execmodel = execmodel def read(self, numbytes): """Read exactly 'numbytes' bytes from the pipe. """ # a file in non-blocking mode may return less bytes, so we loop buf = bytes() - while len(buf) < numbytes: - data = self._read(numbytes) + while numbytes > len(buf): + data = self._read(numbytes-len(buf)) if not data: - raise EOFError("expected %d bytes, got %d" %(numbytes, len(buf))) + raise EOFError( + "expected %d bytes, got %d" % (numbytes, len(buf))) buf += data return buf @@ -100,6 +402,7 @@ def close_write(self): self.outfile.close() + class Message: """ encapsulates Messages and their wire protocol. """ _types = [] @@ -109,33 +412,48 @@ self.channelid = channelid self.data = data + @staticmethod + def from_io(io): + try: + header = io.read(9) # type 1, channel 4, payload 4 + if not header: + raise EOFError("empty read") + except EOFError: + e = sys.exc_info()[1] + raise EOFError('couldnt load message header, ' + e.args[0]) + msgtype, channel, payload = struct.unpack('!bii', header) + return Message(msgtype, channel, io.read(payload)) + + def to_io(self, io): + header = struct.pack('!bii', self.msgcode, self.channelid, + len(self.data)) + io.write(header+self.data) + def received(self, gateway): self._types[self.msgcode](self, gateway) def __repr__(self): name = self._types[self.msgcode].__name__.upper() - r = repr(self.data) - if len(r) > 50: - return "" %(name, - self.channelid, len(r)) - else: - return "" %(name, - self.channelid, self.data) + return "" % ( + name, self.channelid, len(self.data)) + + +class GatewayReceivedTerminate(Exception): + """ Receiverthread got termination message. """ + def _setupmessages(): def status(message, gateway): # we use the 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 = {'execqsize': gateway._execqueue.qsize(), - 'numchannels': len(active_channels), - 'numexecuting': numexec + d = { + 'numchannels': len(gateway._channelfactory._channels), + 'numexecuting': gateway._execpool.active_count(), + 'execmodel': gateway.execmodel.backend, } - gateway._send(Message.CHANNEL_DATA, message.channelid, d) + gateway._send(Message.CHANNEL_DATA, message.channelid, + dumps_internal(d)) + gateway._send(Message.CHANNEL_CLOSE, message.channelid) def channel_exec(message, gateway): channel = gateway._channelfactory.new(message.channelid) @@ -148,20 +466,21 @@ gateway._channelfactory._local_close(message.channelid) def channel_close_error(message, gateway): - remote_error = RemoteError(message.data) + remote_error = RemoteError(loads_internal(message.data)) gateway._channelfactory._local_close(message.channelid, remote_error) def channel_last_message(message, gateway): gateway._channelfactory._local_close(message.channelid, sendonly=True) def gateway_terminate(message, gateway): - gateway._terminate_execution() - raise SystemExit(0) + raise GatewayReceivedTerminate(gateway) def reconfigure(message, gateway): - py2str_as_py3str, py3str_as_py2str = message.data - gateway._unserializer.py2str_as_py3str = py2str_as_py3str - gateway._unserializer.py3str_as_py2str = py3str_as_py2str + if message.channelid == 0: + target = gateway + else: + target = gateway._channelfactory.new(message.channelid) + target._strconfig = loads_internal(message.data, gateway) types = [ status, reconfigure, gateway_terminate, @@ -174,8 +493,9 @@ _setupmessages() + def geterrortext(excinfo, - format_exception=traceback.format_exception, sysex=sysex): + format_exception=traceback.format_exception, sysex=sysex): try: l = format_exception(*excinfo) errortext = "".join(l) @@ -186,6 +506,7 @@ excinfo[1]) return errortext + class RemoteError(Exception): """ Exception containing a stringified error from the other side. """ def __init__(self, formatted): @@ -196,12 +517,14 @@ return self.formatted def __repr__(self): - return "%s: %s" %(self.__class__.__name__, self.formatted) + return "%s: %s" % (self.__class__.__name__, self.formatted) def warn(self): if self.formatted != INTERRUPT_TEXT: # XXX do this better - sys.stderr.write("Warning: unhandled %r\n" % (self,)) + sys.stderr.write("[%s] Warning: unhandled %r\n" + % (os.getpid(), self,)) + class TimeoutError(IOError): """ Exception indicating that a timeout was reached. """ @@ -209,8 +532,9 @@ NO_ENDMARKER_WANTED = object() + class Channel(object): - """Communication channel between two Python Interpreter execution points.""" + "Communication channel between two Python Interpreter execution points." RemoteError = RemoteError TimeoutError = TimeoutError _INTERNALWAKEUP = 1000 @@ -219,10 +543,12 @@ def __init__(self, gateway, id): assert isinstance(id, int) self.gateway = gateway + # XXX: defaults copied from Unserializer + self._strconfig = getattr(gateway, '_strconfig', (True, False)) self.id = id - self._items = queue.Queue() + self._items = self.gateway.execmodel.queue.Queue() self._closed = False - self._receiveclosed = threading.Event() + self._receiveclosed = self.gateway.execmodel.Event() self._remoteerrors = [] def _trace(self, *msg): @@ -239,30 +565,30 @@ be called with the endmarker when the channel closes. """ _callbacks = self.gateway._channelfactory._callbacks - _receivelock = self.gateway._receivelock - _receivelock.acquire() - try: + with self.gateway._receivelock: if self._items is None: - raise IOError("%r has callback already registered" %(self,)) + raise IOError("%r has callback already registered" % (self,)) items = self._items self._items = None while 1: try: olditem = items.get(block=False) - except queue.Empty: + except self.gateway.execmodel.queue.Empty: if not (self._closed or self._receiveclosed.isSet()): - _callbacks[self.id] = (callback, endmarker) + _callbacks[self.id] = ( + callback, + endmarker, + self._strconfig, + ) break else: if olditem is ENDMARKER: - items.put(olditem) # for other receivers + items.put(olditem) # for other receivers if endmarker is not NO_ENDMARKER_WANTED: callback(endmarker) break else: callback(olditem) - finally: - _receivelock.release() def __repr__(self): flag = self.isclosed() and "closed" or "open" @@ -283,14 +609,19 @@ pass else: # state transition "opened" --> "deleted" - if self._items is None: # has_callback - msgcode = Message.CHANNEL_LAST_MESSAGE - else: - msgcode = Message.CHANNEL_CLOSE - try: - self.gateway._send(msgcode, self.id) - except (IOError, ValueError): # ignore problems with sending - pass + # check if we are in the middle of interpreter shutdown + # in which case the process will go away and we probably + # don't need to try to send a closing or last message + # (and often it won't work anymore to send things out) + if Message is not None: + if self._items is None: # has_callback + msgcode = Message.CHANNEL_LAST_MESSAGE + else: + msgcode = Message.CHANNEL_CLOSE + try: + self.gateway._send(msgcode, self.id) + except (IOError, ValueError): # ignore problems with sending + pass def _getremoteerror(self): try: @@ -320,12 +651,13 @@ return ChannelFileWrite(channel=self, proxyclose=proxyclose) elif mode == "r": return ChannelFileRead(channel=self, proxyclose=proxyclose) - raise ValueError("mode %r not availabe" %(mode,)) + raise ValueError("mode %r not availabe" % (mode,)) def close(self, error=None): """ 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. + automatically at the end of execution and cannot + be done explicitely. """ if self._executing: raise IOError("cannot explicitly close channel within remote_exec") @@ -340,7 +672,8 @@ if not self._receiveclosed.isSet(): put = self.gateway._send if error is not None: - put(Message.CHANNEL_CLOSE_ERROR, self.id, error) + put(Message.CHANNEL_CLOSE_ERROR, self.id, + dumps_internal(error)) else: put(Message.CHANNEL_CLOSE, self.id) self._trace("sent channel close message") @@ -364,7 +697,8 @@ 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 + # wait for non-"opened" state + self._receiveclosed.wait(timeout=timeout) if not self._receiveclosed.isSet(): raise self.TimeoutError("Timeout after %r seconds" % timeout) error = self._getremoteerror() @@ -379,15 +713,14 @@ raised if the write pipe was prematurely closed. """ if self.isclosed(): - raise IOError("cannot send to %r" %(self,)) - self.gateway._send(Message.CHANNEL_DATA, self.id, item) + raise IOError("cannot send to %r" % (self,)) + self.gateway._send(Message.CHANNEL_DATA, self.id, dumps_internal(item)) - def receive(self, timeout=-1): + def receive(self, timeout=None): """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. + timeout: None [default] blocked waiting. 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. @@ -395,19 +728,10 @@ itemqueue = self._items if itemqueue is None: raise IOError("cannot receive(), channel has receiver callback") - if timeout < 0: - internal_timeout = self._INTERNALWAKEUP - else: - internal_timeout = timeout - - while 1: - try: - x = itemqueue.get(timeout=internal_timeout) - break - except queue.Empty: - if timeout < 0: - continue - raise self.TimeoutError("no item after %r seconds" %(timeout)) + try: + x = itemqueue.get(timeout=timeout) + except self.gateway.execmodel.queue.Empty: + raise self.TimeoutError("no item after %r seconds" % (timeout)) if x is ENDMARKER: itemqueue.put(x) # for other receivers raise self._getremoteerror() or EOFError() @@ -424,23 +748,33 @@ raise StopIteration __next__ = next + def reconfigure(self, py2str_as_py3str=True, py3str_as_py2str=False): + """ + set the string coercion for this channel + the default is to try to convert py2 str as py3 str, + but not to try and convert py3 str to py2 str + """ + self._strconfig = (py2str_as_py3str, py3str_as_py2str) + data = dumps_internal(self._strconfig) + self.gateway._send(Message.RECONFIGURE, self.id, data=data) + ENDMARKER = object() INTERRUPT_TEXT = "keyboard-interrupted" + class ChannelFactory(object): def __init__(self, gateway, startcount=1): self._channels = weakref.WeakValueDictionary() self._callbacks = {} - self._writelock = threading.Lock() + self._writelock = gateway.execmodel.Lock() self.gateway = gateway self.count = startcount self.finished = False - self._list = list # needed during interp-shutdown + self._list = list # needed during interp-shutdown def new(self, id=None): """ create a new Channel with 'id' (or create new id if None). """ - self._writelock.acquire() - try: + with self._writelock: if self.finished: raise IOError("connexion already closed: %s" % (self.gateway,)) if id is None: @@ -451,8 +785,6 @@ except KeyError: channel = self._channels[id] = Channel(self.gateway, id) return channel - finally: - self._writelock.release() def channels(self): return self._list(self._channels.values()) @@ -466,7 +798,7 @@ except KeyError: pass try: - callback, endmarker = self._callbacks.pop(id) + callback, endmarker, strconfig = self._callbacks.pop(id) except KeyError: pass else: @@ -488,44 +820,44 @@ if queue is not None: queue.put(ENDMARKER) self._no_longer_opened(id) - if not sendonly: # otherwise #--> "sendonly" + if not sendonly: # otherwise #--> "sendonly" channel._closed = True # --> "closed" channel._receiveclosed.set() def _local_receive(self, id, data): # executes in receiver thread + channel = self._channels.get(id) try: - callback, endmarker = self._callbacks[id] + callback, endmarker, strconfig = self._callbacks[id] except KeyError: - channel = self._channels.get(id) queue = channel and channel._items if queue is None: pass # drop data else: - queue.put(data) + item = loads_internal(data, channel) + queue.put(item) else: try: + data = loads_internal(data, channel, strconfig) callback(data) # even if channel may be already closed - except KeyboardInterrupt: - raise - except: + except Exception: excinfo = sys.exc_info() - self.gateway._trace("exception during callback: %s" % excinfo[1]) + self.gateway._trace("exception during callback: %s" % + excinfo[1]) errortext = self.gateway._geterrortext(excinfo) - self.gateway._send(Message.CHANNEL_CLOSE_ERROR, id, errortext) + self.gateway._send(Message.CHANNEL_CLOSE_ERROR, + id, dumps_internal(errortext)) self._local_close(id, errortext) def _finished_receiving(self): - self._writelock.acquire() - try: + with self._writelock: self.finished = True - finally: - self._writelock.release() for id in self._list(self._channels): self._local_close(id, sendonly=True) for id in self._list(self._callbacks): self._no_longer_opened(id) + class ChannelFile(object): def __init__(self, channel, proxyclose=True): self.channel = channel @@ -540,7 +872,8 @@ def __repr__(self): state = self.channel.isclosed() and 'closed' or 'open' - return '' %(self.channel.id, state) + return '' % (self.channel.id, state) + class ChannelFileWrite(ChannelFile): def write(self, out): @@ -549,27 +882,35 @@ def flush(self): pass + class ChannelFileRead(ChannelFile): def __init__(self, channel, proxyclose=True): super(ChannelFileRead, self).__init__(channel, proxyclose) - self._buffer = "" + self._buffer = None def read(self, n): - while len(self._buffer) < n: - try: + try: + if self._buffer is None: + self._buffer = self.channel.receive() + while len(self._buffer) < n: self._buffer += self.channel.receive() - except EOFError: - self.close() - break - ret = self._buffer[:n] - self._buffer = self._buffer[n:] + except EOFError: + self.close() + if self._buffer is None: + ret = "" + else: + ret = self._buffer[:n] + self._buffer = self._buffer[n:] return ret def readline(self): - i = self._buffer.find("\n") - if i != -1: - return self.read(i+1) - line = self.read(len(self._buffer)+1) + if self._buffer is not None: + i = self._buffer.find("\n") + if i != -1: + return self.read(i+1) + line = self.read(len(self._buffer)+1) + else: + line = self.read(1) while line and line[-1] != "\n": c = self.read(1) if not c: @@ -577,75 +918,76 @@ line += c return line + class BaseGateway(object): exc_info = sys.exc_info _sysex = sysex id = "" - class _StopExecLoop(Exception): - pass - def __init__(self, io, id, _startcount=2): + self.execmodel = io.execmodel self._io = io self.id = id + self._strconfig = (Unserializer.py2str_as_py3str, + Unserializer.py3str_as_py2str) self._channelfactory = ChannelFactory(self, _startcount) - self._unserializer = Unserializer(self._io, self._channelfactory) - self._receivelock = threading.RLock() + self._receivelock = self.execmodel.RLock() # globals may be NONE at process-termination - self._trace = trace + self.__trace = trace self._geterrortext = geterrortext + self._receivepool = self.execmodel.WorkerPool() def _trace(self, *msg): - self._trace(self.id, *msg) + self.__trace(self.id, *msg) def _initreceive(self): - self._receiverthread = threading.Thread(name="receiver", - target=self._thread_receiver) - self._receiverthread.setDaemon(1) - self._receiverthread.start() + self._receivepool.spawn(self._thread_receiver) def _thread_receiver(self): - self._trace("RECEIVERTHREAD: starting to run") - eof = False + def log(*msg): + self._trace("[receiver-thread]", *msg) + + log("RECEIVERTHREAD: starting to run") + io = self._io try: - try: - while 1: - msg = Message(*self._unserializer.load()) - self._trace("received", msg) - _receivelock = self._receivelock - _receivelock.acquire() - try: - msg.received(self) - del msg - finally: - _receivelock.release() - except self._sysex: - self._trace("RECEIVERTHREAD: doing io.close_read()") - self._io.close_read() - except EOFError: - self._trace("RECEIVERTHREAD: got EOFError") - self._trace("RECEIVERTHREAD: traceback was: ", - self._geterrortext(self.exc_info())) - self._error = self.exc_info()[1] - eof = True - except: - self._trace("RECEIVERTHREAD", self._geterrortext(self.exc_info())) - finally: - try: - self._trace('RECEIVERTHREAD', 'entering finalization') - if eof: - self._terminate_execution() - self._channelfactory._finished_receiving() - self._trace('RECEIVERTHREAD', 'leaving finalization') - except: - pass # XXX be silent at interp-shutdown + while 1: + msg = Message.from_io(io) + log("received", msg) + with self._receivelock: + msg.received(self) + del msg + except (KeyboardInterrupt, GatewayReceivedTerminate): + pass + except EOFError: + log("EOF without prior gateway termination message") + self._error = self.exc_info()[1] + except Exception: + log(self._geterrortext(self.exc_info())) + log('finishing receiving thread') + # wake up and terminate any execution waiting to receive + self._channelfactory._finished_receiving() + log('terminating execution') + self._terminate_execution() + log('closing read') + self._io.close_read() + log('closing write') + self._io.close_write() + log('terminating our receive pseudo pool') + self._receivepool.trigger_shutdown() def _terminate_execution(self): pass - def _send(self, msgcode, channelid=0, data=''): - serialize(self._io, (msgcode, channelid, data)) - self._trace('sent', Message(msgcode, channelid, data)) + def _send(self, msgcode, channelid=0, data=bytes()): + message = Message(msgcode, channelid, data) + try: + message.to_io(self._io) + self._trace('sent', message) + except (IOError, ValueError): + e = sys.exc_info()[1] + self._trace('failed to send', message, e) + # ValueError might be because the IO is already closed + raise IOError("cannot send (already closed?)") def _local_schedulexec(self, channel, sourcetask): channel.close("execution disallowed") @@ -661,54 +1003,53 @@ 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(timeout) - else: - self._trace("gateway.join() called while receiverthread " - "already finished") + self._trace("waiting for receiver thread to finish") + self._receivepool.waitall() + class SlaveGateway(BaseGateway): + def _local_schedulexec(self, channel, sourcetask): - self._execqueue.put((channel, sourcetask)) + sourcetask = loads_internal(sourcetask) + self._execpool.spawn(self.executetask, ((channel, sourcetask))) def _terminate_execution(self): # called from receiverthread - self._trace("putting None to execqueue") - self._execqueue.put(None) - if interrupt_main: - self._trace("calling interrupt_main()") - interrupt_main() - self._execfinished.wait(10.0) - if not self._execfinished.isSet(): - self._trace("execution did not finish in 10 secs, calling os._exit()") - os._exit(1) - - def serve(self, joining=True): + self._trace("shutting down execution pool") + self._execpool.trigger_shutdown() + if not self._execpool.waitall(5.0): + self._trace( + "execution ongoing after 5 secs,"" trying interrupt_main") + # We try hard to terminate execution based on the assumption + # that there is only one gateway object running per-process. + if sys.platform != "win32": + self._trace("sending ourselves a SIGINT") + os.kill(os.getpid(), 2) # send ourselves a SIGINT + elif interrupt_main is not None: + self._trace("calling interrupt_main()") + interrupt_main() + if not self._execpool.waitall(10.0): + self._trace("execution did not finish in another 10 secs, " + "calling os._exit()") + os._exit(1) + + def serve(self): + def trace(msg): + self._trace("[serve] " + msg) + hasprimary = self.execmodel.backend == "thread" + self._execpool = self.execmodel.WorkerPool(hasprimary=hasprimary) + trace("spawning receiver thread") + self._initreceive() try: - try: - self._execqueue = queue.Queue() - self._execfinished = threading.Event() - self._initreceive() - while 1: - item = self._execqueue.get() - if item is None: - break - try: - self.executetask(item) - except self._StopExecLoop: - break - finally: - self._execfinished.set() - self._trace("io.close_write()") - self._io.close_write() - self._trace("slavegateway.serve finished") - if joining: - self.join() + if hasprimary: + # this will return when we are in shutdown + trace("integrating as primary thread") + self._execpool.integrate_as_primary_thread() + trace("joining receiver thread") + self.join() except KeyboardInterrupt: # in the slave we can't really do anything sensible - self._trace("swallowing keyboardinterrupt in main-thread") + trace("swallowing keyboardinterrupt, serve finished") def executetask(self, item): try: @@ -722,13 +1063,13 @@ name = name.encode('ascii') newkwargs[name] = value kwargs = newkwargs - loc = {'channel' : channel, '__name__': '__channelexec__'} + loc = {'channel': channel, '__name__': '__channelexec__'} self._trace("execution starts[%s]: %s" % - (channel.id, repr(source)[:50])) + (channel.id, repr(source)[:50])) channel._executing = True try: - co = compile(source+'\n', '', 'exec') - do_exec(co, loc) + co = compile(source+'\n', '', 'exec') + do_exec(co, loc) # noqa if call_name: self._trace('calling %s(**%60r)' % (call_name, kwargs)) function = loc[call_name] @@ -736,57 +1077,75 @@ finally: channel._executing = False self._trace("execution finished") - except self._StopExecLoop: - channel.close() - raise except KeyboardInterrupt: channel.close(INTERRUPT_TEXT) raise except: excinfo = self.exc_info() - self._trace("got exception: %s" % (excinfo[1],)) - errortext = self._geterrortext(excinfo) - channel.close(errortext) - else: - channel.close() + if not isinstance(excinfo[1], EOFError): + if not channel.gateway._channelfactory.finished: + self._trace("got exception: %r" % (excinfo[1],)) + errortext = self._geterrortext(excinfo) + channel.close(errortext) + return + self._trace("ignoring EOFError because receiving finished") + channel.close() # # Cross-Python pickling code, tested from test_serializer.py # -class SerializeError(Exception): + +class DataFormatError(Exception): pass -class SerializationError(SerializeError): + +class DumpError(DataFormatError): """Error while serializing an object.""" -class UnserializationError(SerializeError): + +class LoadError(DataFormatError): """Error while unserializing an object.""" + if ISPY3: def bchr(n): return bytes([n]) else: bchr = chr +DUMPFORMAT_VERSION = bchr(1) + FOUR_BYTE_INT_MAX = 2147483647 FLOAT_FORMAT = "!d" FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) +COMPLEX_FORMAT = "!dd" +COMPLEX_FORMAT_SIZE = struct.calcsize(COMPLEX_FORMAT) + class _Stop(Exception): pass -class Unserializer(object): - num2func = {} # is filled after this class definition - py2str_as_py3str = True # True - py3str_as_py2str = False # false means py2 will get unicode - def __init__(self, stream, channelfactory=None): +class Unserializer(object): + num2func = {} # is filled after this class definition + py2str_as_py3str = True # True + py3str_as_py2str = False # false means py2 will get unicode + + def __init__(self, stream, channel_or_gateway=None, strconfig=None): + gateway = getattr(channel_or_gateway, 'gateway', channel_or_gateway) + strconfig = getattr(channel_or_gateway, '_strconfig', strconfig) + if strconfig: + self.py2str_as_py3str, self.py3str_as_py2str = strconfig self.stream = stream - self.channelfactory = channelfactory + self.channelfactory = getattr(gateway, '_channelfactory', gateway) - def load(self): + def load(self, versioned=False): + if versioned: + ver = self.stream.read(1) + if ver != DUMPFORMAT_VERSION: + raise LoadError("wrong dumpformat version %r" % ver) self.stack = [] try: while True: @@ -796,15 +1155,16 @@ try: loader = self.num2func[opcode] except KeyError: - raise UnserializationError("unkown opcode %r - " + raise LoadError( + "unkown opcode %r - " "wire protocol corruption?" % (opcode,)) loader(self) except _Stop: if len(self.stack) != 1: - raise UnserializationError("internal unserialization error") + raise LoadError("internal unserialization error") return self.stack.pop(0) else: - raise UnserializationError("didn't get STOP") + raise LoadError("didn't get STOP") def load_none(self): self.stack.append(None) @@ -839,6 +1199,10 @@ binary = self.stream.read(FLOAT_FORMAT_SIZE) self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) + def load_complex(self): + binary = self.stream.read(COMPLEX_FORMAT_SIZE) + self.stack.append(complex(*struct.unpack(COMPLEX_FORMAT, binary))) + def _read_int4(self): return struct.unpack("!i", self.stream.read(4))[0] @@ -876,7 +1240,7 @@ def load_setitem(self): if len(self.stack) < 3: - raise UnserializationError("not enough items for setitem") + raise LoadError("not enough items for setitem") value = self.stack.pop() key = self.stack.pop() self.stack[-1][key] = value @@ -912,17 +1276,23 @@ # automatically build opcodes and byte-encoding + class opcode: """ container for name -> num mappings. """ + def _buildopcodes(): l = [] + later_added = { + 'COMPLEX': 1, + } 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): + l.sort(key=lambda x: (later_added.get(x[0], 0), x[0])) + + for i, (opname, func) in enumerate(l): assert i < 26, "xxx" i = bchr(64+i) Unserializer.num2func[i] = func @@ -930,28 +1300,82 @@ _buildopcodes() -def serialize(io, obj): - _Serializer(io).save(obj) + +def dumps(obj): + """ return a serialized bytestring of the given obj. + + The obj and all contained objects must be of a builtin + python type (so nested dicts, sets, etc. are all ok but + not user-level instances). + """ + return _Serializer().save(obj, versioned=True) + + +def dump(byteio, obj): + """ write a serialized bytestring of the given obj to the given stream. """ + _Serializer(write=byteio.write).save(obj, versioned=True) + + +def loads(bytestring, py2str_as_py3str=False, py3str_as_py2str=False): + """ return the object as deserialized from the given bytestring. + + py2str_as_py3str: if true then string (str) objects previously + dumped on Python2 will be loaded as Python3 + strings which really are text objects. + py3str_as_py2str: if true then string (str) objects previously + dumped on Python3 will be loaded as Python2 + strings instead of unicode objects. + + if the bytestring was dumped with an incompatible protocol + version or if the bytestring is corrupted, the + ``execnet.DataFormatError`` will be raised. + """ + io = BytesIO(bytestring) + return load(io, + py2str_as_py3str=py2str_as_py3str, + py3str_as_py2str=py3str_as_py2str) + + +def load(io, py2str_as_py3str=False, py3str_as_py2str=False): + """ derserialize an object form the specified stream. + + Behaviour and parameters are otherwise the same as with ``loads`` + """ + strconfig = (py2str_as_py3str, py3str_as_py2str) + return Unserializer(io, strconfig=strconfig).load(versioned=True) + + +def loads_internal(bytestring, channelfactory=None, strconfig=None): + io = BytesIO(bytestring) + return Unserializer(io, channelfactory, strconfig).load() + + +def dumps_internal(obj): + return _Serializer().save(obj) + class _Serializer(object): _dispatch = {} - def __init__(self, stream): - self._stream = stream - self._streamlist = [] - - def _write(self, data): - self._streamlist.append(data) + def __init__(self, write=None): + if write is None: + self._streamlist = [] + write = self._streamlist.append + self._write = write - def save(self, obj): + def save(self, obj, versioned=False): # calling here is not re-entrant but multiple instances # may write to the same stream because of the common platform # atomic-write guaruantee (concurrent writes each happen atomicly) + if versioned: + self._write(DUMPFORMAT_VERSION) self._save(obj) self._write(opcode.STOP) - s = type(self._streamlist[0])().join(self._streamlist) - # atomic write - self._stream.write(s) + try: + streamlist = self._streamlist + except AttributeError: + return None + return type(streamlist[0])().join(streamlist) def _save(self, obj): tp = type(obj) @@ -961,7 +1385,7 @@ methodname = 'save_' + tp.__name__ meth = getattr(self.__class__, methodname, None) if meth is None: - raise SerializationError("can't serialize %s" % (tp,)) + raise DumpError("can't serialize %s" % (tp,)) dispatch = self._dispatch[tp] = meth dispatch(self, obj) @@ -995,7 +1419,7 @@ try: as_bytes = s.encode("utf-8") except UnicodeEncodeError: - raise SerializationError("strings must be utf-8 encodable") + raise DumpError("strings must be utf-8 encodable") self._write_byte_sequence(as_bytes) def _write_byte_sequence(self, bytes_): @@ -1020,10 +1444,14 @@ self._write(opcode.FLOAT) self._write(struct.pack(FLOAT_FORMAT, flt)) + def save_complex(self, cpx): + self._write(opcode.COMPLEX) + self._write(struct.pack(COMPLEX_FORMAT, cpx.real, cpx.imag)) + 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) + raise DumpError(error) self._write(struct.pack("!i", i)) def save_list(self, L): @@ -1064,9 +1492,10 @@ 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) + +def init_popen_io(execmodel): + if not hasattr(os, 'dup'): # jython + io = Popen2IO(sys.stdout, sys.stdin, execmodel) import tempfile sys.stdin = tempfile.TemporaryFile('r') sys.stdout = tempfile.TemporaryFile('w') @@ -1079,26 +1508,27 @@ else: devnull = '/dev/null' # stdin - stdin = os.fdopen(os.dup(0), 'r', 1) + stdin = execmodel.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) + stdout = execmodel.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) + sys.stderr = execmodel.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) + io = Popen2IO(stdout, stdin, execmodel) + sys.stdin = execmodel.fdopen(0, 'r', 1) + sys.stdout = execmodel.fdopen(1, 'w', 1) return io + def serve(io, id): - trace("creating slavegateway on %r" %(io,)) + trace("creating slavegateway on %r" % (io,)) SlaveGateway(io=io, id=id, _startcount=2).serve() diff -Nru execnet-1.0.9/execnet/gateway_bootstrap.py execnet-1.4.1/execnet/gateway_bootstrap.py --- execnet-1.0.9/execnet/gateway_bootstrap.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/execnet/gateway_bootstrap.py 2015-04-01 14:52:10.000000000 +0000 @@ -0,0 +1,101 @@ +""" +code to initialize the remote side of a gateway once the io is created +""" +import os +import inspect +import execnet +from execnet import gateway_base +from execnet.gateway import Gateway +importdir = os.path.dirname(os.path.dirname(execnet.__file__)) + + +class HostNotFound(Exception): + pass + + +def bootstrap_import(io, spec): + sendexec( + io, + "import sys", + "sys.path.insert(0, %r)" % importdir, + "from execnet.gateway_base import serve, init_popen_io, get_execmodel", + "sys.stdout.write('1')", + "sys.stdout.flush()", + "execmodel = get_execmodel(%r)" % spec.execmodel, + "serve(init_popen_io(execmodel), id='%s-slave')" % spec.id, + ) + s = io.read(1) + assert s == "1".encode('ascii'), repr(s) + + +def bootstrap_exec(io, spec): + try: + sendexec( + io, + inspect.getsource(gateway_base), + "execmodel = get_execmodel(%r)" % spec.execmodel, + 'io = init_popen_io(execmodel)', + "io.write('1'.encode('ascii'))", + "serve(io, id='%s-slave')" % spec.id, + ) + s = io.read(1) + assert s == "1".encode('ascii') + except EOFError: + ret = io.wait() + if ret == 255: + raise HostNotFound(io.remoteaddress) + + +def bootstrap_socket(io, id): + # XXX: switch to spec + from execnet.gateway_socket import SocketIO + + sendexec( + io, + inspect.getsource(gateway_base), + 'import socket', + inspect.getsource(SocketIO), + "try: execmodel", + "except NameError:", + " execmodel = get_execmodel('thread')", + "io = SocketIO(clientsock, execmodel)", + "io.write('1'.encode('ascii'))", + "serve(io, id='%s-slave')" % id, + ) + s = io.read(1) + assert s == "1".encode('ascii') + + +def sendexec(io, *sources): + source = "\n".join(sources) + io.write((repr(source) + "\n").encode('ascii')) + + +def fix_pid_for_jython_popen(gw): + """ + fix for jython 2.5.1 + """ + spec, io = gw.spec, gw._io + if spec.popen and not spec.via: + # XXX: handle the case of remote being jython + # and not having the popen pid + if io.popen.pid is None: + io.popen.pid = gw.remote_exec( + "import os; channel.send(os.getpid())").receive() + + +def bootstrap(io, spec): + if spec.popen: + if spec.via or spec.python: + bootstrap_exec(io, spec) + else: + bootstrap_import(io, spec) + elif spec.ssh or spec.vagrant_ssh: + bootstrap_exec(io, spec) + elif spec.socket: + bootstrap_socket(io, spec) + else: + raise ValueError('unknown gateway type, cant bootstrap') + gw = Gateway(io, spec) + fix_pid_for_jython_popen(gw) + return gw diff -Nru execnet-1.0.9/execnet/gateway_io.py execnet-1.4.1/execnet/gateway_io.py --- execnet-1.0.9/execnet/gateway_io.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/execnet/gateway_io.py 2015-08-18 06:02:21.000000000 +0000 @@ -0,0 +1,242 @@ +""" +execnet io initialization code + +creates io instances used for gateway io +""" +import os +import sys + +try: + from execnet.gateway_base import Popen2IO, Message +except ImportError: + from __main__ import Popen2IO, Message + + +class Popen2IOMaster(Popen2IO): + def __init__(self, args, execmodel): + self.popen = p = execmodel.PopenPiped(args) + Popen2IO.__init__(self, p.stdin, p.stdout, execmodel=execmodel) + + def wait(self): + try: + return self.popen.wait() + except OSError: + pass # subprocess probably dead already + + def kill(self): + killpopen(self.popen) + + +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" or getattr(os, '_name', None) == 'nt': + try: + import ctypes + except ImportError: + import subprocess + # 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 EnvironmentError("no method to kill %s" % (pid,)) + + +popen_bootstrapline = "import sys;exec(eval(sys.stdin.readline()))" + + +def popen_args(spec): + python = spec.python or sys.executable + args = str(python).split(' ') + args.append('-u') + if spec is not None and spec.dont_write_bytecode: + args.append("-B") + # Slight gymnastics in ordering these arguments because CPython (as of + # 2.7.1) ignores -B if you provide `python -c "something" -B` + args.extend(['-c', popen_bootstrapline]) + return args + + +def ssh_args(spec): + # NOTE: If changing this, you need to sync those changes to vagrant_args + # as well, or, take some time to further refactor the commonalities of + # ssh_args and vagrant_args. + remotepython = spec.python or "python" + args = ["ssh", "-C"] + if spec.ssh_config is not None: + args.extend(['-F', str(spec.ssh_config)]) + + args.extend(spec.ssh.split()) + remotecmd = '%s -c "%s"' % (remotepython, popen_bootstrapline) + args.append(remotecmd) + return args + + +def vagrant_ssh_args(spec): + # This is the vagrant-wrapped version of SSH. Unfortunately the + # command lines are incompatible to just channel through ssh_args + # due to ordering/templating issues. + # NOTE: This should be kept in sync with the ssh_args behaviour. + # spec.vagrant is identical to spec.ssh in that they both carry + # the remote host "address". + remotepython = spec.python or 'python' + args = ['vagrant', 'ssh', spec.vagrant_ssh, '--', '-C'] + if spec.ssh_config is not None: + args.extend(['-F', str(spec.ssh_config)]) + remotecmd = '%s -c "%s"' % (remotepython, popen_bootstrapline) + args.extend([remotecmd]) + return args + + +def create_io(spec, execmodel): + if spec.popen: + args = popen_args(spec) + return Popen2IOMaster(args, execmodel) + if spec.ssh: + args = ssh_args(spec) + io = Popen2IOMaster(args, execmodel) + io.remoteaddress = spec.ssh + return io + if spec.vagrant_ssh: + args = vagrant_ssh_args(spec) + io = Popen2IOMaster(args, execmodel) + io.remoteaddress = spec.vagrant_ssh + return io + +# +# Proxy Gateway handling code +# +# master: proxy initiator +# forwarder: forwards between master and sub +# sub: sub process that is proxied to the initiator + +RIO_KILL = 1 +RIO_WAIT = 2 +RIO_REMOTEADDRESS = 3 +RIO_CLOSE_WRITE = 4 + + +class ProxyIO(object): + """ A Proxy IO object allows to instantiate a Gateway + through another "via" gateway. A master:ProxyIO object + provides an IO object effectively connected to the sub + via the forwarder. To achieve this, master:ProxyIO interacts + with forwarder:serve_proxy_io() which itself + instantiates and interacts with the sub. + """ + def __init__(self, proxy_channel, execmodel): + # after exchanging the control channel we use proxy_channel + # for messaging IO + self.controlchan = proxy_channel.gateway.newchannel() + proxy_channel.send(self.controlchan) + self.iochan = proxy_channel + self.iochan_file = self.iochan.makefile('r') + self.execmodel = execmodel + + def read(self, nbytes): + return self.iochan_file.read(nbytes) + + def write(self, data): + return self.iochan.send(data) + + def _controll(self, event): + self.controlchan.send(event) + return self.controlchan.receive() + + def close_write(self): + self._controll(RIO_CLOSE_WRITE) + + def kill(self): + self._controll(RIO_KILL) + + def wait(self): + return self._controll(RIO_WAIT) + + @property + def remoteaddress(self): + return self._controll(RIO_REMOTEADDRESS) + + def __repr__(self): + return '' % (self.iochan.gateway.id, ) + + +class PseudoSpec: + def __init__(self, vars): + self.__dict__.update(vars) + + def __getattr__(self, name): + return None + + +def serve_proxy_io(proxy_channelX): + execmodel = proxy_channelX.gateway.execmodel + _trace = proxy_channelX.gateway._trace + tag = "serve_proxy_io:%s " % proxy_channelX.id + + def log(*msg): + _trace(tag + msg[0], *msg[1:]) + spec = PseudoSpec(proxy_channelX.receive()) + # create sub IO object which we will proxy back to our proxy initiator + sub_io = create_io(spec, execmodel) + control_chan = proxy_channelX.receive() + log("got control chan", control_chan) + + # read data from master, forward it to the sub + # XXX writing might block, thus blocking the receiver thread + def forward_to_sub(data): + log("forward data to sub, size %s" % len(data)) + sub_io.write(data) + proxy_channelX.setcallback(forward_to_sub) + + def controll(data): + if data == RIO_WAIT: + control_chan.send(sub_io.wait()) + elif data == RIO_KILL: + control_chan.send(sub_io.kill()) + elif data == RIO_REMOTEADDRESS: + control_chan.send(sub_io.remoteaddress) + elif data == RIO_CLOSE_WRITE: + control_chan.send(sub_io.close_write()) + control_chan.setcallback(controll) + + # write data to the master coming from the sub + forward_to_master_file = proxy_channelX.makefile("w") + + # read bootstrap byte from sub, send it on to master + log('reading bootstrap byte from sub', spec.id) + initial = sub_io.read(1) + assert initial == '1'.encode('ascii'), initial + log('forwarding bootstrap byte from sub', spec.id) + forward_to_master_file.write(initial) + + # enter message forwarding loop + while True: + try: + message = Message.from_io(sub_io) + except EOFError: + log('EOF from sub, terminating proxying loop', spec.id) + break + message.to_io(forward_to_master_file) + # proxy_channelX will be closed from remote_exec's finalization code + +if __name__ == "__channelexec__": + serve_proxy_io(channel) # noqa diff -Nru execnet-1.0.9/execnet/gateway.py execnet-1.4.1/execnet/gateway.py --- execnet-1.0.9/execnet/gateway.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/gateway.py 2015-08-18 06:00:54.000000000 +0000 @@ -1,23 +1,32 @@ """ gateway code for initiating popen, socket and ssh connections. -(c) 2004-2009, Holger Krekel and others +(c) 2004-2013, Holger Krekel and others """ -import sys, os, inspect, types, linecache +import sys +import os +import inspect +import types +import linecache import textwrap import execnet -from execnet.gateway_base import Message, Popen2IO +from execnet.gateway_base import Message 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. """ - def __init__(self, io, id): - super(Gateway, self).__init__(io=io, id=id, _startcount=1) - self._remote_bootstrap_gateway(io) + def __init__(self, io, spec): + super(Gateway, self).__init__(io=io, id=spec.id, _startcount=1) + self.spec = spec self._initreceive() + @property + def remoteaddress(self): + return self._io.remoteaddress + def __repr__(self): """ return string representing gateway type and status. """ try: @@ -26,8 +35,13 @@ except AttributeError: r = "uninitialized" i = "no" - return "<%s id=%r %s, %s active channels>" %( - self.__class__.__name__, self.id, r, i) + return "<%s id=%r %s, %s model, %s active channels>" % ( + self.__class__.__name__, + self.id, + r, + self.execmodel.backend, + i, + ) def exit(self): """ trigger gateway exit. Defer waiting for finishing @@ -39,11 +53,12 @@ self._trace("gateway already unregistered with group") return self._group._unregister(self) - self._trace("--> sending GATEWAY_TERMINATE") try: + self._trace("--> sending GATEWAY_TERMINATE") self._send(Message.GATEWAY_TERMINATE) + self._trace("--> io.close_write") self._io.close_write() - except IOError: + except (ValueError, EOFError, IOError): v = sys.exc_info()[1] self._trace("io-error: could not send termination sequence") self._trace(" exception: %r" % v) @@ -54,22 +69,9 @@ the default is to try to convert py2 str as py3 str, but not to try and convert py3 str to py2 str """ - self._unserializer.py2str_as_py3str = py2str_as_py3str - self._unserializer.py3str_as_py2str = py3str_as_py2str - self._send(Message.RECONFIGURE, data=(py2str_as_py3str, py3str_as_py2str)) - - 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. - """ - sendexec(io, - inspect.getsource(gateway_base), - self._remotesetup, - "io.write('1'.encode('ascii'))", - "serve(io, id='%s-slave')" % self.id, - ) - s = io.read(1) - assert s == "1".encode('ascii') + self._strconfig = (py2str_as_py3str, py3str_as_py2str) + data = gateway_base.dumps_internal(self._strconfig) + self._send(Message.RECONFIGURE, data=data) def _rinfo(self, update=False): """ return some sys/env information from remote. """ @@ -80,7 +82,7 @@ def hasreceiver(self): """ return True if gateway is able to receive data. """ - return self._receiverthread.isAlive() # approxmimation + return self._receivepool.active_count() > 0 def remote_status(self): """ return information object about remote execution status. """ @@ -124,44 +126,37 @@ channel = self.newchannel() self._send(Message.CHANNEL_EXEC, channel.id, - (source, call_name, kwargs)) + gateway_base.dumps_internal((source, call_name, kwargs))) return channel def remote_init_threads(self, num=None): - """ start up to 'num' threads for subsequent - remote_exec() invocations to allow concurrent - execution. - """ - if hasattr(self, '_remotechannelthread'): - raise IOError("remote threads already running") - from execnet import threadpool - source = inspect.getsource(threadpool) - self._remotechannelthread = self.remote_exec(source) - self._remotechannelthread.send(num) - status = self._remotechannelthread.receive() - assert status == "ok", status + """ DEPRECATED. Is currently a NO-OPERATION already.""" + print ("WARNING: remote_init_threads()" + " is a no-operation in execnet-1.2") + class RInfo: def __init__(self, kwargs): self.__dict__.update(kwargs) def __repr__(self): - info = ", ".join(["%s=%s" % item - for item in self.__dict__.items()]) + info = ", ".join( + "%s=%s" % item for item in sorted(self.__dict__.items())) return "" % info RemoteStatus = RInfo -rinfo_source = """ -import sys, os -channel.send(dict( - executable = sys.executable, - version_info = sys.version_info[:5], - platform = sys.platform, - cwd = os.getcwd(), - pid = os.getpid(), -)) -""" + +def rinfo_source(channel): + import sys + import os + channel.send(dict( + executable=sys.executable, + version_info=sys.version_info[:5], + platform=sys.platform, + cwd=os.getcwd(), + pid=os.getpid(), + )) def _find_non_builtin_globals(source, codeobj): @@ -175,29 +170,29 @@ import builtins as __builtin__ vars = dict.fromkeys(codeobj.co_varnames) - all = [] - for node in ast.walk(ast.parse(source)): - if (isinstance(node, ast.Name) and node.id not in vars and - node.id not in __builtin__.__dict__): - all.append(node.id) - return all + return [ + node.id for node in ast.walk(ast.parse(source)) + if isinstance(node, ast.Name) and + node.id not in vars and + node.id not in __builtin__.__dict__ + ] def _source_of_function(function): if function.__name__ == '': raise ValueError("can't evaluate lambda functions'") - #XXX: we dont check before remote instanciation - # if arguments are used propperly + # XXX: we dont check before remote instanciation + # if arguments are used propperly args, varargs, keywords, defaults = inspect.getargspec(function) if args[0] != 'channel': raise ValueError('expected first function argument to be `channel`') - if sys.version_info < (3,0): - closure = function.func_closure - codeobj = function.func_code - else: + if gateway_base.ISPY3: closure = function.__closure__ codeobj = function.__code__ + else: + closure = function.func_closure + codeobj = function.func_code if closure is not None: raise ValueError("functions with closures can't be passed") @@ -207,7 +202,7 @@ except IOError: raise ValueError("can't find source file for %s" % function) - source = textwrap.dedent(source) # just for inner functions + source = textwrap.dedent(source) # just for inner functions used_globals = _find_non_builtin_globals(source, codeobj) if used_globals: @@ -217,78 +212,3 @@ ) return source - -class PopenCmdGateway(Gateway): - _remotesetup = "io = init_popen_io()" - 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, 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): - """ This Gateway provides interaction with a newly started - python subprocess. - """ - 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), '-u', '-c', popen_bootstrapline] - super(PopenGateway, self).__init__(args, id=id) - - def _remote_bootstrap_gateway(self, io): - sendexec(io, - "import sys", - "sys.stdout.write('1')", - "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(), id='%s-slave')" % self.id, - ) - 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 HostNotFound(Exception): - pass - -class SshGateway(PopenCmdGateway): - """ This Gateway provides interaction with a remote Python process, - established via the 'ssh' command line binary. - The remote side needs to have a Python interpreter executable. - """ - 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. - """ - self.remoteaddress = sshaddress - if remotepython is None: - remotepython = "python" - args = ['ssh', '-C' ] - if ssh_config is not None: - args.extend(['-F', str(ssh_config)]) - remotecmd = '%s -c "%s"' %(remotepython, popen_bootstrapline) - args.extend([sshaddress, remotecmd]) - super(SshGateway, self).__init__(args, id=id) - - def _remote_bootstrap_gateway(self, io): - try: - super(SshGateway, self)._remote_bootstrap_gateway(io) - except EOFError: - ret = self._popen.wait() - if ret == 255: - raise HostNotFound(self.remoteaddress) diff -Nru execnet-1.0.9/execnet/gateway_socket.py execnet-1.4.1/execnet/gateway_socket.py --- execnet-1.0.9/execnet/gateway_socket.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/gateway_socket.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,16 +1,20 @@ -import socket -from execnet.gateway import Gateway, HostNotFound -import os, sys, inspect +from execnet.gateway_bootstrap import HostNotFound +import sys + +try: + bytes +except NameError: + bytes = str -try: bytes -except NameError: bytes = str class SocketIO: - error = (socket.error, EOFError) - def __init__(self, sock): + def __init__(self, sock, execmodel): self.sock = sock + self.execmodel = execmodel + socket = execmodel.socket try: - sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY + # IPTOS_LOWDELAY + sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10) sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) except (AttributeError, socket.error): sys.stderr.write("WARNING: cannot set socketoption") @@ -31,63 +35,60 @@ def close_read(self): try: self.sock.shutdown(0) - except socket.error: + except self.execmodel.socket.error: pass + def close_write(self): try: self.sock.shutdown(1) - except socket.error: + except self.execmodel.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 wait(self): + pass - 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) + def kill(self): + pass + + +def start_via(gateway, hostport=None): + """ return a host, port tuple, + after instanciating a socketserver on the given gateway + """ + if hostport is None: + host, port = ('localhost', 0) + else: + host, port = hostport + + from execnet.script import socketserver + + # execute the above socketserverbootstrap on the other side + channel = gateway.remote_exec(socketserver) + channel.send((host, port)) + (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 realhost, realport + + +def create_io(spec, group, execmodel): + assert not spec.python, ( + "socket: specifying python executables not yet supported") + gateway_id = spec.installvia + if gateway_id: + host, port = start_via(group[gateway_id]) + else: + host, port = spec.socket.split(":") + port = int(port) + + socket = execmodel.socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + io = SocketIO(sock, execmodel) + io.remoteaddress = '%s:%d' % (host, port) + try: + sock.connect((host, port)) + except execmodel.socket.gaierror: + raise HostNotFound(str(sys.exc_info()[1])) + return io diff -Nru execnet-1.0.9/execnet/__init__.py execnet-1.4.1/execnet/__init__.py --- execnet-1.0.9/execnet/__init__.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/__init__.py 2015-08-18 06:11:23.000000000 +0000 @@ -1,18 +1,21 @@ """ -execnet: pure python lib for connecting to local and remote Python Interpreters. +execnet +------- -(c) 2010, Holger Krekel and others -""" -__version__ = '1.0.9' +pure python lib for connecting to local and remote Python Interpreters. -import execnet.apipkg +(c) 2012, Holger Krekel and others +""" +import apipkg -execnet.apipkg.initpkg(__name__, { +apipkg.initpkg(__name__, { + '__version__': '._version:version', 'PopenGateway': '.deprecated:PopenGateway', 'SocketGateway': '.deprecated:SocketGateway', 'SshGateway': '.deprecated:SshGateway', 'makegateway': '.multi:makegateway', - 'HostNotFound': '.gateway:HostNotFound', + 'set_execmodel': '.multi:set_execmodel', + 'HostNotFound': '.gateway_bootstrap:HostNotFound', 'RemoteError': '.gateway_base:RemoteError', 'TimeoutError': '.gateway_base:TimeoutError', 'XSpec': '.xspec:XSpec', @@ -20,4 +23,9 @@ 'MultiChannel': '.multi:MultiChannel', 'RSync': '.rsync:RSync', 'default_group': '.multi:default_group', + 'dumps': '.gateway_base:dumps', + 'loads': '.gateway_base:loads', + 'load': '.gateway_base:load', + 'dump': '.gateway_base:dump', + 'DataFormatError': '.gateway_base:DataFormatError', }) diff -Nru execnet-1.0.9/execnet/multi.py execnet-1.4.1/execnet/multi.py --- execnet-1.0.9/execnet/multi.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/multi.py 2015-04-01 14:52:10.000000000 +0000 @@ -1,33 +1,72 @@ """ Managing Gateway Groups and interactions with multiple channels. -(c) 2008-2009, Holger Krekel and others +(c) 2008-2014, Holger Krekel and others """ -import os, sys, atexit -import execnet +import sys +import atexit + from execnet import XSpec -from execnet import gateway -from execnet.gateway_base import queue, reraise, trace, TimeoutError +from execnet import gateway_io, gateway_bootstrap +from execnet.gateway_base import reraise, trace, get_execmodel +from threading import Lock NO_ENDMARKER_WANTED = object() -class Group: + +class Group(object): """ Gateway Groups. """ defaultspec = "popen" - def __init__(self, xspecs=()): - """ initialize group and make gateways as specified. """ - # Gateways may evolve to become GC-collectable + + def __init__(self, xspecs=(), execmodel="thread"): + """ initialize group and make gateways as specified. + execmodel can be 'thread' or 'eventlet'. + """ self._gateways = [] self._autoidcounter = 0 + self._autoidlock = Lock() self._gateways_to_join = [] + # we use the same execmodel for all of the Gateway objects + # we spawn on our side. Probably we should not allow different + # execmodels between different groups but not clear. + # Note that "other side" execmodels may differ and is typically + # specified by the spec passed to makegateway. + self.set_execmodel(execmodel) for xspec in xspecs: self.makegateway(xspec) atexit.register(self._cleanup_atexit) + @property + def execmodel(self): + return self._execmodel + + @property + def remote_execmodel(self): + return self._remote_execmodel + + def set_execmodel(self, execmodel, remote_execmodel=None): + """ Set the execution model for local and remote site. + + execmodel can be one of "thread" or "eventlet" (XXX gevent). + It determines the execution model for any newly created gateway. + If remote_execmodel is not specified it takes on the value + of execmodel. + + NOTE: Execution models can only be set before any gateway is created. + + """ + if self._gateways: + raise ValueError("can not set execution models if " + "gateways have been created already") + if remote_execmodel is None: + remote_execmodel = execmodel + self._execmodel = get_execmodel(execmodel) + self._remote_execmodel = get_execmodel(remote_execmodel) + def __repr__(self): idgateways = [gw.id for gw in self] - return "" %(idgateways) + return "" % (idgateways) def __getitem__(self, key): if isinstance(key, int): @@ -63,6 +102,7 @@ id= specifies the gateway id python= specifies which python interpreter to execute + execmodel=model 'thread', 'eventlet', 'gevent' model for execution chdir= specifies to which directory to change nice= specifies process priority of new process env:NAME=value specifies a remote environment variable setting. @@ -74,22 +114,22 @@ if not isinstance(spec, XSpec): spec = XSpec(spec) self.allocate_id(spec) - if spec.popen: - gw = gateway.PopenGateway(python=spec.python, id=spec.id) - elif spec.ssh: - gw = gateway.SshGateway(spec.ssh, remotepython=spec.python, - ssh_config=spec.ssh_config, id=spec.id) + if spec.execmodel is None: + spec.execmodel = self.remote_execmodel.backend + if spec.via: + assert not spec.socket + master = self[spec.via] + proxy_channel = master.remote_exec(gateway_io) + proxy_channel.send(vars(spec)) + proxy_io_master = gateway_io.ProxyIO(proxy_channel, self.execmodel) + gw = gateway_bootstrap.bootstrap(proxy_io_master, spec) + elif spec.popen or spec.ssh or spec.vagrant_ssh: + io = gateway_io.create_io(spec, execmodel=self.execmodel) + gw = gateway_bootstrap.bootstrap(io, spec) 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 = SocketGateway.new_remote(viagw, id=spec.id) - else: - host, port = spec.socket.split(":") - gw = SocketGateway(host, port, id=spec.id) + from execnet import gateway_socket + io = gateway_socket.create_io(spec, self, execmodel=self.execmodel) + gw = gateway_bootstrap.bootstrap(io, spec) else: raise ValueError("no gateway type found for %r" % (spec._spec,)) gw.spec = spec @@ -114,13 +154,14 @@ return gw def allocate_id(self, spec): - """ allocate id for the given xspec object. """ + """ (re-entrant) allocate id for the given xspec object. """ if spec.id is None: - id = "gw" + str(self._autoidcounter) - self._autoidcounter += 1 - if id in self: - raise ValueError("already have gateway with id %r" %(id,)) - spec.id = id + with self._autoidlock: + id = "gw" + str(self._autoidcounter) + self._autoidcounter += 1 + if id in self: + raise ValueError("already have gateway with id %r" % (id,)) + spec.id = id def _register(self, gateway): assert not hasattr(gateway, '_group') @@ -134,7 +175,7 @@ self._gateways_to_join.append(gateway) def _cleanup_atexit(self): - trace("=== atexit cleanup %r ===" %(self,)) + trace("=== atexit cleanup %r ===" % (self,)) self.terminate(timeout=1.0) def terminate(self, timeout=None): @@ -144,39 +185,39 @@ and ssh-gateways. Timeout defaults to None meaning open-ended waiting and no kill attempts. """ - for gw in self: - gw.exit() - def join_receiver_and_wait_for_subprocesses(): - for gw in self._gateways_to_join: + + while self: + vias = {} + for gw in self: + if gw.spec.via: + vias[gw.spec.via] = True + for gw in self: + if gw.id not in vias: + gw.exit() + + def join_wait(gw): gw.join() - while self._gateways_to_join: - gw = self._gateways_to_join[0] - if hasattr(gw, '_popen'): - gw._popen.wait() - del self._gateways_to_join[0] - from execnet.threadpool import WorkerPool - pool = WorkerPool(1) - reply = pool.dispatch(join_receiver_and_wait_for_subprocesses) - 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) + gw._io.wait() + + def kill(gw): + trace("Gateways did not come down after timeout: %r" % gw) + gw._io.kill() + + safe_terminate(self.execmodel, timeout, [ + (lambda: join_wait(gw), lambda: kill(gw)) + for gw in self._gateways_to_join]) + self._gateways_to_join[:] = [] - def remote_exec(self, source): + def remote_exec(self, source, **kwargs): """ remote_exec source on all member gateways and return MultiChannel connecting to all sub processes. """ channels = [] for gw in self: - channels.append(gw.remote_exec(source)) + channels.append(gw.remote_exec(source, **kwargs)) return MultiChannel(channels) + class MultiChannel: def __init__(self, channels): self._channels = channels @@ -212,8 +253,11 @@ try: return self._queue except AttributeError: - self._queue = queue.Queue() + self._queue = None for ch in self._channels: + if self._queue is None: + self._queue = ch.gateway.execmodel.queue.Queue() + def putreceived(obj, channel=ch): self._queue.put((channel, obj)) if endmarker is NO_ENDMARKER_WANTED: @@ -222,7 +266,6 @@ ch.setcallback(putreceived, endmarker=endmarker) return self._queue - def waitclose(self): first = None for ch in self._channels: @@ -234,38 +277,26 @@ if first: reraise(*first) -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" or getattr(os, '_name', None) == 'nt': + +def safe_terminate(execmodel, timeout, list_of_paired_functions): + workerpool = execmodel.WorkerPool() + + def termkill(termfunc, killfunc): + termreply = workerpool.spawn(termfunc) try: - import ctypes - except ImportError: - import subprocess - # 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 EnvironmentError("no method to kill %s" %(pid,)) + termreply.get(timeout=timeout) + except IOError: + killfunc() + + replylist = [] + for termfunc, killfunc in list_of_paired_functions: + reply = workerpool.spawn(termkill, termfunc, killfunc) + replylist.append(reply) + for reply in replylist: + reply.get() + workerpool.waitall() + default_group = Group() makegateway = default_group.makegateway - +set_execmodel = default_group.set_execmodel diff -Nru execnet-1.0.9/execnet/rsync.py execnet-1.4.1/execnet/rsync.py --- execnet-1.0.9/execnet/rsync.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/rsync.py 2015-02-15 23:58:19.000000000 +0000 @@ -3,7 +3,8 @@ (c) 2006-2009, Armin Rigo, Holger Krekel, Maciej Fijalkowski """ -import os, stat +import os +import stat try: from hashlib import md5 @@ -17,6 +18,7 @@ import execnet.rsync_remote + class RSync(object): """ This class allows to send a directory structure (recursively) to one or multiple remote filesystems. @@ -86,7 +88,7 @@ if channel not in self._to_send: self._to_send[channel] = [] self._to_send[channel].append(modified_rel_path) - #print "sending", modified_rel_path, data and len(data) or 0, checksum + # print "sending", modified_rel_path, data and len(data) or 0, checksum if data is not None: f.close() @@ -98,7 +100,7 @@ def _report_send_file(self, gateway, modified_rel_path): if self._verbose: - print("%s <= %s" %(gateway, modified_rel_path)) + print("%s <= %s" % (gateway, modified_rel_path)) def send(self, raises=True): """ Sends a sourcedir to all added targets. Flag indicates @@ -149,10 +151,12 @@ """ for name in options: assert name in ('delete',) + def itemcallback(req): self._receivequeue.put((channel, req)) channel = gateway.remote_exec(execnet.rsync_remote) - channel.setcallback(itemcallback, endmarker = None) + channel.reconfigure(py2str_as_py3str=False, py3str_as_py2str=False) + channel.setcallback(itemcallback, endmarker=None) channel.send((str(destdir), options)) self._channels[channel] = finishedcallback @@ -181,7 +185,8 @@ linkpoint = os.readlink(path) basename = path[len(self._sourcedir) + 1:] if linkpoint.startswith(self._sourcedir): - self._send_link("linkbase", basename, + self._send_link( + "linkbase", basename, linkpoint[len(self._sourcedir) + 1:]) else: # relative or absolute link, just send it @@ -203,4 +208,3 @@ self._send_link_structure(path) else: raise ValueError("cannot sync %r" % (path,)) - diff -Nru execnet-1.0.9/execnet/rsync_remote.py execnet-1.4.1/execnet/rsync_remote.py --- execnet-1.0.9/execnet/rsync_remote.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/rsync_remote.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,8 +1,12 @@ """ -(c) 2006-2009, Armin Rigo, Holger Krekel, Maciej Fijalkowski +(c) 2006-2013, Armin Rigo, Holger Krekel, Maciej Fijalkowski """ + + def serve_rsync(channel): - import os, stat, shutil + import os + import stat + import shutil try: from hashlib import md5 except ImportError: @@ -16,7 +20,7 @@ os.unlink(path) except OSError: # assume it's a dir - shutil.rmtree(path) + shutil.rmtree(path, True) def receive_directory_structure(path, relcomponents): try: @@ -36,7 +40,8 @@ entrynames = {} for entryname in msg: destpath = os.path.join(path, entryname) - receive_directory_structure(destpath, relcomponents + [entryname]) + receive_directory_structure( + destpath, relcomponents + [entryname]) entrynames[entryname] = True if options.get('delete'): for othername in os.listdir(path): @@ -106,4 +111,4 @@ channel.send(("done", None)) if __name__ == '__channelexec__': - serve_rsync(channel) + serve_rsync(channel) # noqa diff -Nru execnet-1.0.9/execnet/script/loop_socketserver.py execnet-1.4.1/execnet/script/loop_socketserver.py --- execnet-1.0.9/execnet/script/loop_socketserver.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/script/loop_socketserver.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,5 +1,6 @@ -import os, sys +import os +import sys import subprocess if __name__ == '__main__': diff -Nru execnet-1.0.9/execnet/script/shell.py execnet-1.4.1/execnet/script/shell.py --- execnet-1.0.9/execnet/script/shell.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/script/shell.py 2015-02-14 23:21:47.000000000 +0000 @@ -4,24 +4,28 @@ for injection into startserver.py """ -import sys, os, socket, select +import sys +import os +import socket +import select -try: - clientsock -except NameError: +from traceback import print_exc +from threading import Thread + + +def clientside(): print("client side starting") - import sys - host, port = sys.argv[1].split(':') + host, port = sys.argv[1].split(':') port = int(port) myself = open(os.path.abspath(sys.argv[0]), 'rU').read() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) sock.sendall(repr(myself)+'\n') print("send boot string") - inputlist = [ sock, sys.stdin ] + inputlist = [sock, sys.stdin] try: while 1: - r,w,e = select.select(inputlist, [], []) + r, w, e = select.select(inputlist, [], []) if sys.stdin in r: line = raw_input() sock.sendall(line + '\n') @@ -35,14 +39,10 @@ sys.exit(1) -print("server side starting") -# server side -# -from traceback import print_exc -from threading import Thread class promptagent(Thread): def __init__(self, clientsock): + print("server side starting") Thread.__init__(self) self.clientsock = clientsock @@ -58,28 +58,32 @@ clientfile.write('%s %s >>> ' % loc) clientfile.flush() line = filein.readline() - if len(line)==0: raise EOFError("nothing") - #print >>sys.stderr,"got line: " + line + if not line: + raise EOFError("nothing") if line.strip(): oldout, olderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = clientfile, clientfile try: try: - exec(compile(line + '\n','', 'single')) + exec(compile(line + '\n', + '', 'single')) except: print_exc() finally: - sys.stdout=oldout - sys.stderr=olderr + sys.stdout = oldout + sys.stderr = olderr clientfile.flush() except EOFError: - e = sys.exc_info()[1] sys.stderr.write("connection close, prompt thread returns") break - #print >>sys.stdout, "".join(apply(format_exception,sys.exc_info())) self.clientsock.close() -prompter = promptagent(clientsock) -prompter.start() -print("promptagent - thread started") + +sock = globals().get('clientsock') +if sock is not None: + prompter = promptagent(sock) + prompter.start() + print("promptagent - thread started") +else: + clientside() diff -Nru execnet-1.0.9/execnet/script/socketserver.py execnet-1.4.1/execnet/script/socketserver.py --- execnet-1.0.9/execnet/script/socketserver.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/script/socketserver.py 2015-02-15 23:58:19.000000000 +0000 @@ -2,25 +2,40 @@ """ start socket based minimal readline exec server + + it can exeuted in 2 modes of operation + + 1. as normal script, that listens for new connections + + 2. via existing_gateway.remote_exec (as imported module) + """ # this part of the program only executes on the server side # +import sys +import os + progname = 'socket_readline_exec_server-1.2' -import sys, socket, os -try: - import fcntl -except ImportError: - fcntl = None + +def get_fcntl(): + try: + import fcntl + except ImportError: + fcntl = None + return fcntl + +fcntl = get_fcntl() debug = 0 -if debug: # and not os.isatty(sys.stdin.fileno()): +if debug: # and not os.isatty(sys.stdin.fileno()) f = open('/tmp/execnet-socket-pyout.log', 'w') old = sys.stdout, sys.stderr sys.stdout = sys.stderr = f + def print_(*args): print(" ".join(str(arg) for arg in args)) @@ -31,28 +46,35 @@ exec("""def exec_(source, locs): exec source in locs""") + def exec_from_one_connection(serversock): print_(progname, 'Entering Accept loop', serversock.getsockname()) - clientsock,address = serversock.accept() + clientsock, address = serversock.accept() print_(progname, 'got new connection from %s %s' % address) clientfile = clientsock.makefile('rb') print_("reading line") # rstrip so that we can use \r\n for telnet testing source = clientfile.readline().rstrip() clientfile.close() - g = {'clientsock' : clientsock, 'address' : address} + g = { + 'clientsock': clientsock, + 'address': address, + 'execmodel': execmodel, + } source = eval(source) if source: co = compile(source+'\n', source, 'exec') print_(progname, 'compiled source, executing') try: - exec_(co, g) + exec_(co, g) # noqa finally: print_(progname, 'finished executing code') # background thread might hold a reference to this (!?) - #clientsock.close() + # clientsock.close() -def bind_and_listen(hostport): + +def bind_and_listen(hostport, execmodel): + socket = execmodel.socket if isinstance(hostport, str): host, port = hostport.split(':') hostport = (host, int(port)) @@ -69,6 +91,7 @@ serversock.listen(5) return serversock + def startserver(serversock, loop=False): try: while 1: @@ -91,10 +114,19 @@ if __name__ == '__main__': import sys - if len(sys.argv)>1: + if len(sys.argv) > 1: hostport = sys.argv[1] else: hostport = ':8888' - serversock = bind_and_listen(hostport) + from execnet.gateway_base import get_execmodel + execmodel = get_execmodel("thread") + serversock = bind_and_listen(hostport, execmodel) startserver(serversock, loop=False) - +elif __name__ == '__channelexec__': + chan = globals()['channel'] + execmodel = chan.gateway.execmodel + bindname = chan.receive() + sock = bind_and_listen(bindname, execmodel) + port = sock.getsockname() + chan.send(port) + startserver(sock) diff -Nru execnet-1.0.9/execnet/script/socketserverservice.py execnet-1.4.1/execnet/script/socketserverservice.py --- execnet-1.0.9/execnet/script/socketserverservice.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/script/socketserverservice.py 2015-02-15 23:58:19.000000000 +0000 @@ -7,8 +7,6 @@ """ import sys -import os -import time import win32serviceutil import win32service import win32event @@ -25,6 +23,7 @@ _svc_name_ = appname _svc_display_name_ = "%s" % appname _svc_deps_ = ["EventLog"] + def __init__(self, args): # The exe-file has messages for the Event Log Viewer. # Register the exe-file as event source. @@ -39,14 +38,12 @@ "Application") win32serviceutil.ServiceFramework.__init__(self, args) self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - self.WAIT_TIME = 1000 # in milliseconds - + self.WAIT_TIME = 1000 # in milliseconds def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self.hWaitStop) - def SvcDoRun(self): # Redirect stdout and stderr to prevent "IOError: [Errno 9] # Bad file descriptor". Windows services don't have functional @@ -56,7 +53,7 @@ # Write a 'started' event to the event log... win32evtlogutil.ReportEvent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTED, - 0, # category + 0, # category servicemanager.EVENTLOG_INFORMATION_TYPE, (self._svc_name_, '')) print("Begin: %s" % (self._svc_display_name_)) @@ -64,23 +61,24 @@ hostport = ':8888' print('Starting py.execnet SocketServer on %s' % hostport) serversock = socketserver.bind_and_listen(hostport) - thread = threading.Thread(target=socketserver.startserver, - args=(serversock,), - kwargs={'loop':True}) + thread = threading.Thread( + target=socketserver.startserver, + args=(serversock,), + kwargs={'loop': True}) thread.setDaemon(True) thread.start() # wait to be stopped or self.WAIT_TIME to pass while True: - result = win32event.WaitForSingleObject(self.hWaitStop, - self.WAIT_TIME) + result = win32event.WaitForSingleObject( + self.hWaitStop, self.WAIT_TIME) if result == win32event.WAIT_OBJECT_0: break # write a 'stopped' event to the event log. win32evtlogutil.ReportEvent(self._svc_display_name_, servicemanager.PYS_SERVICE_STOPPED, - 0, # category + 0, # category servicemanager.EVENTLOG_INFORMATION_TYPE, (self._svc_name_, '')) print("End: %s" % appname) diff -Nru execnet-1.0.9/execnet/script/xx.py execnet-1.4.1/execnet/script/xx.py --- execnet-1.0.9/execnet/script/xx.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/script/xx.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,7 +1,9 @@ import rlcompleter2 + +import register +import sys rlcompleter2.setup() -import register, sys try: hostport = sys.argv[1] except: diff -Nru execnet-1.0.9/execnet/threadpool.py execnet-1.4.1/execnet/threadpool.py --- execnet-1.0.9/execnet/threadpool.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/threadpool.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,183 +0,0 @@ -""" -dispatching execution to threads - -(c) 2009, holger krekel -""" -import threading -import time -import sys - -# py2/py3 compatibility -try: - import queue -except ImportError: - import Queue as queue -if sys.version_info >= (3,0): - exec ("def reraise(cls, val, tb): raise val") -else: - exec ("def reraise(cls, val, tb): raise cls, val, tb") - -ERRORMARKER = object() - -class Reply(object): - """ reply instances provide access to the result - of a function execution that got dispatched - through WorkerPool.dispatch() - """ - _excinfo = None - def __init__(self, task): - self.task = task - self._queue = queue.Queue() - - def _set(self, result): - self._queue.put(result) - - def _setexcinfo(self, excinfo): - self._excinfo = excinfo - self._queue.put(ERRORMARKER) - - def get(self, timeout=None): - """ get the result object from an asynchronous function execution. - if the function execution raised an exception, - then calling get() will reraise that exception - including its traceback. - """ - if self._queue is None: - raise EOFError("reply has already been delivered") - 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 - reraise(excinfo[0], excinfo[1], excinfo[2]) - return result - -class WorkerThread(threading.Thread): - def __init__(self, pool): - threading.Thread.__init__(self) - self._queue = queue.Queue() - self._pool = pool - self.setDaemon(1) - - def _run_once(self): - reply = self._queue.get() - if reply is SystemExit: - return False - assert self not in self._pool._ready - task = reply.task - try: - func, args, kwargs = task - result = func(*args, **kwargs) - except (SystemExit, KeyboardInterrupt): - return False - except: - reply._setexcinfo(sys.exc_info()) - else: - reply._set(result) - # at this point, reply, task and all other local variables go away - return True - - def run(self): - try: - while self._run_once(): - self._pool._ready[self] = True - finally: - del self._pool._alive[self] - try: - del self._pool._ready[self] - except KeyError: - pass - - def send(self, task): - reply = Reply(task) - self._queue.put(reply) - return reply - - def stop(self): - self._queue.put(SystemExit) - -class WorkerPool(object): - """ A WorkerPool allows to dispatch function executions - to threads. Each Worker Thread is reused for multiple - function executions. The dispatching operation - takes care to create and dispatch to existing - threads. - - You need to call shutdown() to signal - the WorkerThreads to terminate and join() - in order to wait until all worker threads - have terminated. - """ - _shuttingdown = False - def __init__(self, maxthreads=None): - """ init WorkerPool instance which may - create up to `maxthreads` worker threads. - """ - self.maxthreads = maxthreads - self._ready = {} - self._alive = {} - - def dispatch(self, func, *args, **kwargs): - """ return Reply object for the asynchronous dispatch - of the given func(*args, **kwargs) in a - separate worker thread. - """ - if self._shuttingdown: - raise IOError("WorkerPool is already shutting down") - try: - thread, _ = self._ready.popitem() - except KeyError: # pop from empty list - if self.maxthreads and len(self._alive) >= self.maxthreads: - raise IOError("can't create more than %d threads." % - (self.maxthreads,)) - thread = self._newthread() - return thread.send((func, args, kwargs)) - - def _newthread(self): - thread = WorkerThread(self) - self._alive[thread] = True - thread.start() - return thread - - def shutdown(self): - """ signal all worker threads to terminate. - call join() to wait until all threads termination. - """ - if not self._shuttingdown: - self._shuttingdown = True - for t in list(self._alive): - t.stop() - - def join(self, timeout=None): - """ wait until all worker threads have terminated. """ - current = threading.currentThread() - deadline = delta = None - if timeout is not None: - deadline = time.time() + timeout - for thread in list(self._alive): - if deadline: - delta = deadline - time.time() - if delta <= 0: - raise IOError("timeout while joining threads") - thread.join(timeout=delta) - if thread.isAlive(): - raise IOError("timeout while joining threads") - -if __name__ == '__channelexec__': - maxthreads = channel.receive() - execpool = WorkerPool(maxthreads=maxthreads) - gw = channel.gateway - channel.send("ok") - 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._trace("thread-dispatcher got None, exiting") - execpool.shutdown() - execpool.join() - raise gw._StopExecLoop - gw._trace("dispatching exec task to thread pool") - execpool.dispatch(gw.executetask, task) diff -Nru execnet-1.0.9/execnet/_version.py execnet-1.4.1/execnet/_version.py --- execnet-1.0.9/execnet/_version.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/execnet/_version.py 2015-09-02 18:54:22.000000000 +0000 @@ -0,0 +1,4 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '1.4.1' diff -Nru execnet-1.0.9/execnet/xspec.py execnet-1.4.1/execnet/xspec.py --- execnet-1.0.9/execnet/xspec.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet/xspec.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,7 +1,7 @@ """ -(c) 2008-2009, holger krekel +(c) 2008-2013, holger krekel """ -import execnet + class XSpec: """ Execution Specification: key1=value1//key2=value2 ... @@ -12,7 +12,8 @@ * if no "=value" is given, assume a boolean True value """ # XXX allow customization, for only allow specific key names - popen = ssh = socket = python = chdir = nice = None + popen = ssh = socket = python = chdir = nice = \ + dont_write_bytecode = execmodel = None def __init__(self, string): self._spec = string @@ -26,7 +27,7 @@ if key[0] == "_": raise AttributeError("%r not a valid XSpec key" % key) if key in self.__dict__: - raise ValueError("duplicate key: %r in %r" %(key, string)) + raise ValueError("duplicate key: %r in %r" % (key, string)) if key.startswith("env:"): self.env[key[4:]] = value else: @@ -38,17 +39,19 @@ return None def __repr__(self): - return "" %(self._spec,) + return "" % (self._spec,) + def __str__(self): return self._spec def __hash__(self): return hash(self._spec) + def __eq__(self, other): return self._spec == getattr(other, '_spec', None) + def __ne__(self, other): return self._spec != getattr(other, '_spec', None) def _samefilesystem(self): - return bool(self.popen and not self.chdir) - + return self.popen is not None and self.chdir is None diff -Nru execnet-1.0.9/execnet.egg-info/dependency_links.txt execnet-1.4.1/execnet.egg-info/dependency_links.txt --- execnet-1.0.9/execnet.egg-info/dependency_links.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru execnet-1.0.9/execnet.egg-info/PKG-INFO execnet-1.4.1/execnet.egg-info/PKG-INFO --- execnet-1.0.9/execnet.egg-info/PKG-INFO 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 @@ -1,55 +0,0 @@ -Metadata-Version: 1.0 -Name: execnet -Version: 1.0.9 -Summary: execnet: rapid multi-Python deployment -Home-page: http://codespeak.net/execnet -Author: holger krekel and others -Author-email: holger at merlinux.eu -License: GPL V2 or later -Description: - execnet: rapid multi-Python deployment - ======================================================== - - .. _execnet: http://codespeak.net/execnet - - 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 administer a bunch of exec 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. - -Platform: unix -Platform: linux -Platform: osx -Platform: cygwin -Platform: win32 -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: GNU General Public License (GPL) -Classifier: Operating System :: POSIX -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: MacOS :: MacOS X -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: System :: Distributed Computing -Classifier: Topic :: System :: Networking -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 diff -Nru execnet-1.0.9/execnet.egg-info/SOURCES.txt execnet-1.4.1/execnet.egg-info/SOURCES.txt --- execnet-1.0.9/execnet.egg-info/SOURCES.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,74 +0,0 @@ -CHANGELOG -LICENSE -MANIFEST.in -README.txt -conftest.py -setup.py -doc/Makefile -doc/__init__.py -doc/basics.txt -doc/changelog.txt -doc/check_sphinx.py -doc/conf.py -doc/examples.txt -doc/implnotes.txt -doc/index.txt -doc/install.txt -doc/rel-1.0.0.txt -doc/support.txt -doc/_static/basic1.png -doc/_static/codespeak.png -doc/_static/execnet.png -doc/_static/pythonring.png -doc/_templates/indexsidebar.html -doc/_templates/layout.html -doc/example/conftest.py -doc/example/funcmultiplier.py -doc/example/hybridpython.txt -doc/example/popen_read_multiple.py -doc/example/py3topy2.py -doc/example/redirect_remote_output.py -doc/example/remote1.py -doc/example/remotecmd.py -doc/example/servefiles.py -doc/example/svn-sync-repo.py -doc/example/sysinfo.py -doc/example/test_debug.txt -doc/example/test_funcmultiplier.py -doc/example/test_group.txt -doc/example/test_info.txt -doc/example/test_multi.txt -doc/example/test_ssh_fileserver.txt -execnet/__init__.py -execnet/apipkg.py -execnet/deprecated.py -execnet/gateway.py -execnet/gateway_base.py -execnet/gateway_socket.py -execnet/multi.py -execnet/rsync.py -execnet/rsync_remote.py -execnet/threadpool.py -execnet/xspec.py -execnet.egg-info/PKG-INFO -execnet.egg-info/SOURCES.txt -execnet.egg-info/dependency_links.txt -execnet.egg-info/top_level.txt -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 -testing/__init__.py -testing/runtest.py -testing/test_basics.py -testing/test_channel.py -testing/test_gateway.py -testing/test_multi.py -testing/test_rsync.py -testing/test_serializer.py -testing/test_termination.py -testing/test_threadpool.py -testing/test_xspec.py \ No newline at end of file diff -Nru execnet-1.0.9/execnet.egg-info/top_level.txt execnet-1.4.1/execnet.egg-info/top_level.txt --- execnet-1.0.9/execnet.egg-info/top_level.txt 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/execnet.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -execnet diff -Nru execnet-1.0.9/ISSUES.txt execnet-1.4.1/ISSUES.txt --- execnet-1.0.9/ISSUES.txt 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/ISSUES.txt 2015-02-12 20:06:23.000000000 +0000 @@ -0,0 +1,132 @@ +rsync links from posix to windows +----------------------------------------- +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. + +http process for mediating client/remote-subprocess execution +------------------------------------------------------------------ + +tags: 1.2 feature newdist + +A client client connects to an http-execserver. An http-execserver +routes incoming "POST /exec" requests to a subprocess and returns a PID. +The PID identifies the subprocess which is used for further communication. + +Client http-exec subprocess + +makegw("...") -> POST /popen -> Popen() with bootstrap + (bootstrap) + <- PID + +c=rexec(...) POST /exec/PID exec source +(source) + +c.send(1) POST /exec/PID x = c.receive() + (1) assert x == 1 + c.send(2) + +z = c.receive() GET /exec/PID + <- (2) +assert z == 2 + +The client connects via "POST /exec/PID" +and pushes data to the remotely-executing code +in the subprocess. + +The client connects via "GET /exec/PID" +and receives data from the remotely-executing code. + +The client constructs a Channel() object which triggers +these two connections, presenting a nice send/receive +abstractions. + +The code executing in the http-execserver controled +subprocess is a PopenGateway. It does not know that it +is connected via the http client/server machinery. + +The http-execserver proxies data from the subprocess +to the client. If there is no active GET request, the +data is queued. + +The http-execserver forwards data from the client to the +subprocess, using the PID. If the subprocess is not +connected, an ERROR code is returned. + +Example session: + + gw = group.makegateway("http=codespeak.net") + + def fun(channel): + return channel.send(channel.receive()+1) + + ch = gw.remote_exec(fun) + ch.send(1) + x = ch.receive() + assert x == 2 + + +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. + +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) + +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) + + +have callback accept errors / errback +----------------------------------------------- +tags: 1.1 wish + +If using channel.setcallback() it currently is not +possible to notice the exact errors that might happen +on the other side. Either introduce an "errback" +or (probably better) optionally allow an extra 'error' +parameter to execnet callbacks. + +fix rsync between python/jython +----------------------------------------------- +tags: 1.0 bug + +rsync does not complete with jython. diff -Nru execnet-1.0.9/LICENSE execnet-1.4.1/LICENSE --- execnet-1.0.9/LICENSE 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/LICENSE 2015-02-12 20:06:23.000000000 +0000 @@ -1,17 +1,19 @@ -The execnet package is released under the provisions of the Gnu Public -License (GPL), version 2 or later. -See http://www.fsf.org/licensing/licenses/ for more information. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. -This package contains some code and contributions from - - Armin Rigo, Benjamin Peterson, Carl Friedrich Bolz, - Maciej Fijalkowski, Ronny Pfannschmidt and others - -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 contact me. - -holger krekel, November 2009, holger at merlinux eu diff -Nru execnet-1.0.9/MANIFEST.in execnet-1.4.1/MANIFEST.in --- execnet-1.0.9/MANIFEST.in 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -include CHANGELOG -include conftest.py -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 diff -Nru execnet-1.0.9/PKG-INFO execnet-1.4.1/PKG-INFO --- execnet-1.0.9/PKG-INFO 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/PKG-INFO 2015-09-02 18:54:22.000000000 +0000 @@ -1,13 +1,13 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: execnet -Version: 1.0.9 +Version: 1.4.1 Summary: execnet: rapid multi-Python deployment Home-page: http://codespeak.net/execnet Author: holger krekel and others Author-email: holger at merlinux.eu -License: GPL V2 or later +License: MIT Description: - execnet: rapid multi-Python deployment + execnet: distributed Python deployment and communication ======================================================== .. _execnet: http://codespeak.net/execnet @@ -16,9 +16,9 @@ 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 processes * write and deploy hybrid multi-process applications - * write scripts to administer a bunch of exec environments + * write scripts to administer multiple hosts Features ------------------ @@ -32,19 +32,23 @@ * grouped creation and robust termination of processes - * well tested between CPython 2.4-3.1, Jython 2.5.1 and PyPy 1.1 + * well tested between CPython 2.6-3.X, Jython 2.5.1 and PyPy 2.2 interpreters. - * fully interoperable between Windows and Unix-ish systems. + * interoperable between Windows and Unix-ish systems. + + * integrates with different threading models, including standard + os threads, eventlet and gevent based systems. + Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 -Classifier: Development Status :: 5 - Production/Stable +Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: GNU General Public License (GPL) +Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X diff -Nru execnet-1.0.9/setup.cfg execnet-1.4.1/setup.cfg --- execnet-1.0.9/setup.cfg 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/setup.cfg 2015-09-02 18:54:22.000000000 +0000 @@ -1,3 +1,6 @@ +[wheel] +universal = true + [egg_info] tag_build = tag_date = 0 diff -Nru execnet-1.0.9/setup.py execnet-1.4.1/setup.py --- execnet-1.0.9/setup.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/setup.py 2015-09-02 18:41:10.000000000 +0000 @@ -1,5 +1,5 @@ """ -execnet: rapid multi-Python deployment +execnet: distributed Python deployment and communication ======================================================== .. _execnet: http://codespeak.net/execnet @@ -8,9 +8,9 @@ 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 processes * write and deploy hybrid multi-process applications -* write scripts to administer a bunch of exec environments +* write scripts to administer multiple hosts Features ------------------ @@ -24,33 +24,33 @@ * grouped creation and robust termination of processes -* well tested between CPython 2.4-3.1, Jython 2.5.1 and PyPy 1.1 +* well tested between CPython 2.6-3.X, Jython 2.5.1 and PyPy 2.2 interpreters. -* fully interoperable between Windows and Unix-ish systems. +* interoperable between Windows and Unix-ish systems. + +* integrates with different threading models, including standard + os threads, eventlet and gevent based systems. + """ -try: - from setuptools import setup, Command -except ImportError: - from distutils.core import setup, Command def main(): + from setuptools import setup setup( name='execnet', description='execnet: rapid multi-Python deployment', - long_description = __doc__, - version='1.0.9', + long_description=__doc__, + use_scm_version={'write_to': 'execnet/_version.py'}, url='http://codespeak.net/execnet', - license='GPL V2 or later', + license='MIT', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel and others', author_email='holger at merlinux.eu', - cmdclass = {'test': PyTest}, classifiers=[ - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', + 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', @@ -60,18 +60,9 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 3'], packages=['execnet', 'execnet.script'], + install_requires=['apipkg>=1.4'], + setup_requires=['setuptools_scm'], ) -class PyTest(Command): - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - import sys,subprocess - errno = subprocess.call([sys.executable, 'testing/runtest.py']) - raise SystemExit(errno) - if __name__ == '__main__': - main() \ No newline at end of file + main() diff -Nru execnet-1.0.9/testing/conftest.py execnet-1.4.1/testing/conftest.py --- execnet-1.0.9/testing/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/testing/conftest.py 2015-02-15 23:58:19.000000000 +0000 @@ -0,0 +1,208 @@ +import execnet +import py +import pytest +import sys +import subprocess +from execnet.gateway_base import get_execmodel, WorkerPool + +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', + 'python3.1': r'C:\Python31\python.exe', + 'python3.2': r'C:\Python32\python.exe', + 'python3.3': r'C:\Python33\python.exe', + 'python3.4': r'C:\Python34\python.exe', +} + + +def pytest_runtest_setup(item, __multicall__): + if item.fspath.purebasename in ('test_group', 'test_info'): + getspecssh(item.config) # will skip if no gx given + res = __multicall__.execute() + if 'pypy' in item.keywords and not item.config.option.pypy: + py.test.skip("pypy tests skipped, use --pypy to run them.") + return res + + +@pytest.fixture +def makegateway(request): + group = execnet.Group() + request.addfinalizer(lambda: group.terminate(0.5)) + return group.makegateway + +pytest_plugins = ['pytester', 'doctest'] + + +# configuration information for tests +def pytest_addoption(parser): + group = parser.getgroup("execnet", "execnet testing options") + group.addoption( + '--gx', action="append", dest="gspecs", default=None, + help=("add a global test environment, XSpec-syntax. ")) + group.addoption( + '--gwscope', action="store", dest="scope", default="session", + type="choice", choices=["session", "function"], + help=("set gateway setup scope, default: session.")) + group.addoption( + '--pypy', action="store_true", dest="pypy", + help=("run some tests also against pypy")) + group.addoption( + '--broken-isp', action="store_true", dest="broken_isp", + help=("Skips tests that assume your ISP doesn't put up a landing " + "page on invalid addresses")) + + +def pytest_report_header(config): + return [ + "gateway test setup scope: %s" % config.getvalue("scope"), + "execnet: %s -- %s" % (execnet.__file__, execnet.__version__), + ] + + +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 map(execnet.XSpec, 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', 'proxy'] + metafunc.parametrize("gw", gwtypes, indirect=True) + elif 'anypython' in metafunc.funcargnames: + metafunc.parametrize( + "anypython", indirect=True, argvalues=( + 'sys.executable', 'python3.3', 'python3.2', + 'python2.6', 'python2.7', 'pypy', 'jython', + ) + ) + + +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + if name == 'sys.executable': + return py.path.local(sys.executable) + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + popen = subprocess.Popen( + [str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + cache[name] = executable + return executable + + +@pytest.fixture +def anypython(request): + name = request.param + executable = getexecutable(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 + executable = None + py.test.skip("no %s found" % (name,)) + if "execmodel" in request.fixturenames and name != 'sys.executable': + backend = request.getfuncargvalue("execmodel").backend + if backend != "thread": + pytest.xfail( + "cannot run %r execmodel with bare %s" % (backend, name)) + return executable + + +@pytest.fixture +def gw(request, execmodel): + scope = request.config.option.scope + 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//execmodel=%s" + % execmodel.backend) + elif request.param == "socket": + # if execmodel.backend != "thread": + # pytest.xfail( + # "cannot set remote non-thread execmodel for sockets") + 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" + "//execmodel=%s" % (pname, execmodel.backend)) + gw.proxygw = proxygw + assert pname in group + elif request.param == "ssh": + sshhost = request.getfuncargvalue('specssh').ssh + # we don't use execmodel.backend here + # but you can set it when specifying the ssh spec + gw = group.makegateway("ssh=%s//id=ssh" % (sshhost,)) + elif request.param == 'proxy': + group.makegateway('popen//id=proxy-transport') + gw = group.makegateway('popen//via=proxy-transport//id=proxy' + '//execmodel=%s' % execmodel.backend) + return gw + + +@pytest.fixture(params=["thread", "eventlet", "gevent"], scope="session") +def execmodel(request): + if request.param != "thread": + pytest.importorskip(request.param) + if sys.platform == "win32": + pytest.xfail("eventlet/gevent do not work onwin32") + return get_execmodel(request.param) + + +@pytest.fixture +def pool(execmodel): + return WorkerPool(execmodel=execmodel) diff -Nru execnet-1.0.9/testing/__init__.py execnet-1.4.1/testing/__init__.py --- execnet-1.0.9/testing/__init__.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -# diff -Nru execnet-1.0.9/testing/runtest.py execnet-1.4.1/testing/runtest.py --- execnet-1.0.9/testing/runtest.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/runtest.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,2136 +0,0 @@ -#! /usr/bin/env python - -sources = """ -eNrsvduWI9mVGEbJl7FgSyNZlmRZlh1EuRQRXcioSzdnaIggV7O7eqZmyO5eXVUceiXT6EggMjOY -yAhUBFCZOSS9/A1+9Iu+wN/hH/AX+Eu8b+d+IoCs5pCjtVxkJ4CIcz/77PvZ+3//h797973s7b/e -3u+qfldsN/vLuilu2ua6ut+Wu9XVu3/w5v/90+99bzqdJtbTurlMymYNj1bX+P1i36x2dduUm3p3 -XyQJFJ9M6ptt2+2Stp8l/X0/mayri4T7WWL5srtcLq02s656t4eX+XySJNTEm6sq6ardvmuqdfLt -t1bZb79NpIlk27Xv63XVJ7urqq+o6lW12VZdclPtrto1vGhhnOv64j5pz39drXYwnnXNw+1qqNh2 -MMaiat7XXdvM5xNqA/9ZHRZ9tSt3uy6DJmZJU95Us+R9udnDR1fWPazB4k23r/Jo3XW18esergQd -1rvqJrspt1so6XQ62A3VeGg3MPPMmdG2q7ZVs158UW764b6Cakd0dt/Dl6uldJDhj5zX+9PNhnep -XpW4NX1yW8Oj8yrZN+u2qZLbq6rBPU4ETKAnqojwpOEvuaibur8CcKl3fVLdVas9PgaIRFj69lsZ -4rffUtVt2cH4dwAqa/x7UzcADfVFUiZ/Xd2/7DoEjC75FLauPt/vKnpCFdXQsDns64IGBkv5FNeF -BtICBNJEkquyT5o22QGwVrtCwTZ93tCyJAt7kTJeOpllUa7XMCc4V39bdRmXL3BFVCk8HdLOZNK0 -OxgEtMeADk1NVpuy7+329emSQsl1VW3pPENrq7ZbJ+1FItD+FOHpKWz0U9m5ZHVVNpdVX+gp4KFe -LmHVd8tl1lebCzm++A9/FktpC0Z1eha+wh74lW5OHTUsM0sOHDjTG04JZ1+q/YL9xnqAK2AXvv0W -GoKvgAy+/ZYa+vbbWXJ+jz2W+81ON0N7am86gIBssGl5Xa9hT3cAYTXskV4M/Ndu1tA8TOkyxBi8 -P+ZsQLsyGcKmUrXupaCZW3RgMLJs+rhTAGZG97ibJo9Nv3ke35Gibvqq22XPZok9SB6FUymO+nKz -ZQrDBVs2sld0VKrYdl107Y3aMHuPeAl0I+5SwIkf2CTANu/rdt9v7mP7BXuAhWAR3Unm7uqbrXKf -D+4MtaGLVoBJ3YrHbMUwCFnbw3vgUZjcOU5EFnhvgPK5mxgeIE0cYb2aHfzVGwOHhyq5K+icZXsm -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 -""" - -import sys -import base64 -import zlib -import imp - -class DictImporter(object): - def __init__(self, sources): - self.sources = sources - - def find_module(self, fullname, path=None): - if fullname in self.sources: - return self - if fullname+'.__init__' in self.sources: - return self - return None - - def load_module(self, fullname): - # print "load_module:", fullname - from types import ModuleType - try: - s = self.sources[fullname] - is_pkg = False - except KeyError: - s = self.sources[fullname+'.__init__'] - is_pkg = True - - co = compile(s, fullname, 'exec') - module = sys.modules.setdefault(fullname, ModuleType(fullname)) - module.__file__ = "%s/%s" % (__file__, fullname) - module.__loader__ = self - if is_pkg: - module.__path__ = [fullname] - - do_exec(co, module.__dict__) - return sys.modules[fullname] - - def get_source(self, name): - res = self.sources.get(name) - if res is None: - res = self.sources.get(name+'.__init__') - return res - -if __name__ == "__main__": - if sys.version_info >= (3,0): - exec("def do_exec(co, loc): exec(co, loc)\n") - import pickle - sources = sources.encode("ascii") # ensure bytes - sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) - else: - import cPickle as pickle - exec("def do_exec(co, loc): exec co in loc\n") - sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) - - importer = DictImporter(sources) - sys.meta_path.append(importer) - - entry = "import py; raise SystemExit(py.test.cmdline.main())" - do_exec(entry, locals()) diff -Nru execnet-1.0.9/testing/test_basics.py execnet-1.4.1/testing/test_basics.py --- execnet-1.0.9/testing/test_basics.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_basics.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,25 +1,75 @@ - +from __future__ import with_statement import py -import sys, os, subprocess, inspect +import pytest +import sys +import os +import subprocess +import inspect import execnet -from execnet import gateway_base, gateway -from execnet.gateway_base import Message, Channel, ChannelFactory, serialize, \ - Unserializer +from execnet import gateway_base, gateway, gateway_io +from execnet.gateway_base import Message, ChannelFactory, Popen2IO + +try: + from StringIO import StringIO as BytesIO +except: + from io import BytesIO + + +class TestSerializeAPI: + pytestmark = [ + pytest.mark.parametrize("val", [ + "123", 42, [1, 2, 3], ["23", 25]])] + + def test_serializer_api(self, val): + dumped = execnet.dumps(val) + val2 = execnet.loads(dumped) + assert val == val2 + + def test_mmap(self, tmpdir, val): + mmap = pytest.importorskip("mmap").mmap + p = tmpdir.join("data") + with p.open("wb") as f: + f.write(execnet.dumps(val)) + f = p.open("r+b") + m = mmap(f.fileno(), 0) + val2 = execnet.load(m) + assert val == val2 + + def test_bytesio(self, val): + f = py.io.BytesIO() + execnet.dump(f, val) + read = py.io.BytesIO(f.getvalue()) + val2 = execnet.load(read) + assert val == val2 + + +def test_serializer_api_version_error(monkeypatch): + bchr = gateway_base.bchr + monkeypatch.setattr(gateway_base, 'DUMPFORMAT_VERSION', bchr(1)) + dumped = execnet.dumps(42) + monkeypatch.setattr(gateway_base, 'DUMPFORMAT_VERSION', bchr(2)) + pytest.raises(execnet.DataFormatError, lambda: execnet.loads(dumped)) + def test_errors_on_execnet(): assert hasattr(execnet, 'RemoteError') assert hasattr(execnet, 'TimeoutError') + assert hasattr(execnet, 'DataFormatError') + def test_subprocess_interaction(anypython): - line = gateway.popen_bootstrapline + line = gateway_io.popen_bootstrapline compile(line, 'xyz', 'exec') args = [str(anypython), '-c', line] - popen = subprocess.Popen(args, bufsize=0, universal_newlines=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + popen = subprocess.Popen( + args, bufsize=0, universal_newlines=True, + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + def send(line): popen.stdin.write(line) - if sys.version_info > (3,0) or sys.platform.startswith("java"): + if sys.version_info > (3, 0) or sys.platform.startswith("java"): popen.stdin.flush() + def receive(): return popen.stdout.readline() @@ -36,14 +86,14 @@ send("world\n") s = receive() assert s == "received: world\n" - send('\n') # terminate loop + send('\n') # terminate loop finally: popen.stdin.close() popen.stdout.close() popen.wait() + def read_write_loop(): - import os, sys sys.stdout.write("ok\n") sys.stdout.flush() while 1: @@ -56,7 +106,8 @@ except (IOError, EOFError): break -def test_io_message(anypython, tmpdir): + +def test_io_message(anypython, tmpdir, execmodel): check = tmpdir.join("check.py") check.write(py.code.Source(gateway_base, """ try: @@ -66,48 +117,62 @@ import tempfile temp_out = BytesIO() temp_in = BytesIO() - io = Popen2IO(temp_out, temp_in) - unserializer = Unserializer(io) + io = Popen2IO(temp_out, temp_in, get_execmodel({backend!r})) for i, handler in enumerate(Message._types): print ("checking %s %s" %(i, handler)) for data in "hello", "hello".encode('ascii'): - msg1 = Message(i, i, data) - serialize(io, (i, i, data)) + msg1 = Message(i, i, dumps(data)) + msg1.to_io(io) 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(*unserializer.load()) + msg2 = Message.from_io(io) assert msg1.channelid == msg2.channelid, (msg1, msg2) - assert msg1.data == msg2.data + assert msg1.data == msg2.data, (msg1.data, msg2.data) assert msg1.msgcode == msg2.msgcode print ("all passed") - """)) - #out = py.process.cmdexec("%s %s" %(executable,check)) + """.format(backend=execmodel.backend))) + # out = py.process.cmdexec("%s %s" %(executable,check)) out = anypython.sysexec(check) print (out) assert "all passed" in out -def test_popen_io(anypython, tmpdir): + +def test_popen_io(anypython, tmpdir, execmodel): check = tmpdir.join("check.py") check.write(py.code.Source(gateway_base, """ - do_exec("io = init_popen_io()", globals()) + do_exec("io = init_popen_io(get_execmodel({backend!r}))", globals()) io.write("hello".encode('ascii')) s = io.read(1) assert s == "x".encode('ascii') - """)) + """.format(backend=execmodel.backend))) from subprocess import Popen, PIPE args = [str(anypython), str(check)] proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) proc.stdin.write("x".encode('ascii')) stdout, stderr = proc.communicate() print (stderr) - ret = proc.wait() + proc.wait() assert "hello".encode('ascii') in stdout +def test_popen_io_readloop(monkeypatch, execmodel): + sio = BytesIO('test'.encode('ascii')) + io = Popen2IO(sio, sio, execmodel) + real_read = io._read + + def newread(numbytes): + if numbytes > 1: + numbytes = numbytes-1 + return real_read(numbytes) + io._read = newread + result = io.read(3) + assert result == 'tes'.encode('ascii') + + def test_rinfo_source(anypython, tmpdir): check = tmpdir.join("check.py") check.write(py.code.Source(""" @@ -122,6 +187,7 @@ print (out) assert "all passed" in out + def test_geterrortext(anypython, tmpdir): check = tmpdir.join("check.py") check.write(py.code.Source(gateway_base, """ @@ -142,11 +208,11 @@ print (out) assert "all passed" in out -@py.test.mark.skipif("not hasattr(os, 'dup')") -def test_stdouterrin_setnull(): + +@pytest.mark.skipif("not hasattr(os, 'dup')") +def test_stdouterrin_setnull(execmodel): cap = py.io.StdCaptureFD() - io = gateway_base.init_popen_io() - import os + gateway_base.init_popen_io(execmodel) os.write(1, "hello".encode('ascii')) if os.name == "nt": os.write(2, "world") @@ -155,18 +221,27 @@ assert not out assert not err + class PseudoChannel: + class gateway: + class _channelfactory: + finished = False + def __init__(self): self._sent = [] self._closed = [] self.id = 1000 + def send(self, obj): self._sent.append(obj) + def close(self, errortext=None): self._closed.append(errortext) -def test_exectask(): + +def test_exectask(execmodel): io = py.io.BytesIO() + io.execmodel = execmodel gw = gateway_base.SlaveGateway(io, id="something") ch = PseudoChannel() gw.executetask((ch, ("raise ValueError()", None, {}))) @@ -178,66 +253,70 @@ for i, handler in enumerate(Message._types): one = py.io.BytesIO() data = '23'.encode('ascii') - serialize(one, (i, 42, data)) + Message(i, 42, data).to_io(one) two = py.io.BytesIO(one.getvalue()) - msg = Message(*Unserializer(two, None).load()) + msg = Message.from_io(two) assert msg.msgcode == i assert isinstance(msg, Message) assert msg.channelid == 42 assert msg.data == data assert isinstance(repr(msg), str) - # == "" %(msg.__class__.__name__, ) + class TestPureChannel: - def setup_method(self, method): - self.fac = ChannelFactory(None) + @pytest.fixture + def fac(self, execmodel): + class Gateway: + pass + Gateway.execmodel = execmodel + return ChannelFactory(Gateway) - def test_factory_create(self): - chan1 = self.fac.new() + def test_factory_create(self, fac): + chan1 = fac.new() assert chan1.id == 1 - chan2 = self.fac.new() + chan2 = fac.new() assert chan2.id == 3 - def test_factory_getitem(self): - chan1 = self.fac.new() - assert self.fac._channels[chan1.id] == chan1 - chan2 = self.fac.new() - assert self.fac._channels[chan2.id] == chan2 - - def test_channel_timeouterror(self): - channel = self.fac.new() - py.test.raises(IOError, channel.waitclose, timeout=0.01) - - def test_channel_makefile_incompatmode(self): - channel = self.fac.new() - py.test.raises(ValueError, 'channel.makefile("rw")') + def test_factory_getitem(self, fac): + chan1 = fac.new() + assert fac._channels[chan1.id] == chan1 + chan2 = fac.new() + assert fac._channels[chan2.id] == chan2 + + def test_channel_timeouterror(self, fac): + channel = fac.new() + pytest.raises(IOError, channel.waitclose, timeout=0.01) + + def test_channel_makefile_incompatmode(self, fac): + channel = fac.new() + with pytest.raises(ValueError): + channel.makefile("rw") class TestSourceOfFunction(object): def test_lambda_unsupported(self): - py.test.raises(ValueError, gateway._source_of_function, lambda:1) + pytest.raises(ValueError, gateway._source_of_function, lambda: 1) def test_wrong_prototype_fails(self): def prototype(wrong): pass - py.test.raises(ValueError, gateway._source_of_function, prototype) + pytest.raises(ValueError, gateway._source_of_function, prototype) def test_function_without_known_source_fails(self): # this one wont be able to find the source mess = {} py.builtin.exec_('def fail(channel): pass', mess, mess) - import inspect print(inspect.getsourcefile(mess['fail'])) - py.test.raises(ValueError, gateway._source_of_function, mess['fail']) + pytest.raises(ValueError, gateway._source_of_function, mess['fail']) def test_function_with_closure_fails(self): mess = {} + def closure(channel): print(mess) - py.test.raises(ValueError, gateway._source_of_function, closure) - + pytest.raises(ValueError, gateway._source_of_function, closure) def test_source_of_nested_function(self): def working(channel): @@ -249,9 +328,7 @@ class TestGlobalFinder(object): - - def setup_class(cls): - py.test.importorskip('ast') + pytestmark = pytest.mark.skipif('sys.version_info < (2, 6)') def check(self, func): src = py.code.Source(func) @@ -261,18 +338,17 @@ def test_local(self): def f(a, b, c): d = 3 - pass + return d assert self.check(f) == [] def test_global(self): def f(a, b): - c = 3 - glob + sys d = 4 + return d - assert self.check(f) == ['glob'] - + assert self.check(f) == ['sys'] def test_builtin(self): def f(): @@ -282,29 +358,41 @@ def test_function_with_global_fails(self): def func(channel): - test - py.test.raises(ValueError, gateway._source_of_function, func) + sys + pytest.raises(ValueError, gateway._source_of_function, func) + + def test_method_call(self): + # method names are reason + # for the simple code object based heusteric failing + def f(channel): + channel.send(dict(testing=2)) + assert self.check(f) == [] -def test_remote_exec_function_with_kwargs(anypython): - import sys +def test_remote_exec_function_with_kwargs(anypython, makegateway): def func(channel, data): channel.send(data) - group = execnet.Group() - gw = group.makegateway('popen//python=%s' % anypython) - print ("local version_info %r" %(sys.version_info,)) + gw = makegateway('popen//python=%s' % anypython) + print ("local version_info %r" % (sys.version_info,)) print ("remote info: %s" % (gw._rinfo(),)) ch = gw.remote_exec(func, data=1) result = ch.receive() assert result == 1 +def test_remote_exc__no_kwargs(makegateway): + gw = makegateway() + pytest.raises(TypeError, gw.remote_exec, gateway_base, kwarg=1) + pytest.raises(TypeError, gw.remote_exec, 'pass', kwarg=1) -def test_remote_exc_module_takes_no_kwargs(): - gw = execnet.makegateway() - py.test.raises(TypeError, gw.remote_exec, gateway_base, kwarg=1) - -def test_remote_exec_string_takes_no_kwargs(): - gw = execnet.makegateway() - py.test.raises(TypeError, gw.remote_exec, 'pass', kwarg=1) +def test_remote_exec_inspect_stack(makegateway): + gw = makegateway() + ch = gw.remote_exec(""" + import inspect + inspect.stack() + import traceback + channel.send('\\n'.join(traceback.format_stack())) + """) + assert 'File ""' in ch.receive() + ch.waitclose() diff -Nru execnet-1.0.9/testing/test_channel.py execnet-1.4.1/testing/test_channel.py --- execnet-1.0.9/testing/test_channel.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_channel.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,45 +1,45 @@ """ mostly functional tests of gateways. """ -import os, sys, time +import pytest +import 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')") +from test_gateway import _find_version +needs_early_gc = pytest.mark.skipif("not hasattr(sys, 'getrefcount')") +needs_osdup = pytest.mark.skipif("not hasattr(os, 'dup')") queue = py.builtin._tryimport('queue', 'Queue') +TESTTIMEOUT = 10.0 # seconds -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) + excinfo = pytest.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) + pytest.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) + pytest.raises(EOFError, channel.receive) + pytest.raises(EOFError, channel.receive) + pytest.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) + pytest.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)") + pytest.raises(channel.TimeoutError, "channel.receive(timeout=0.2)") channel.send(1) - x = channel.receive(timeout=TESTTIMEOUT) + channel.receive(timeout=TESTTIMEOUT) def test_channel_receive_internal_timeout(self, gw, monkeypatch): channel = gw.remote_exec(""" @@ -48,13 +48,13 @@ channel.send(1) """) monkeypatch.setattr(channel.__class__, '_INTERNALWAKEUP', 0.2) - x = channel.receive() + 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() assert x == 42 - py.test.raises(channel.RemoteError, channel.receive) + pytest.raises(channel.RemoteError, channel.receive) def test_channel__local_close(self, gw): channel = gw._channelfactory.new() @@ -64,30 +64,24 @@ 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) + channel.RemoteError("error")) + pytest.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') + excinfo = pytest.raises(channel.RemoteError, channel.receive) + msg = str(excinfo.value) + assert msg.startswith('Traceback (most recent call last):') + assert "NameError" in msg + assert "foobar" in msg 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 + excinfo = pytest.raises(channel.RemoteError, channel.receive) + msg = str(excinfo.value) + assert msg.startswith('Traceback (most recent call last):') + assert "SyntaxError" in msg def test_channel_iter(self, gw): channel = gw.remote_exec(""" @@ -108,7 +102,7 @@ channel.send((newchan1, newchan2)) newchan1.send(1) data = newchan2.receive() - assert data ==2 + assert data == 2 def test_channel_multipass(self, gw): channel = gw.remote_exec(''' @@ -133,7 +127,7 @@ # check that the both sides previous channels are really gone channel.waitclose(TESTTIMEOUT) - #assert c.id not in gw._channelfactory + # assert c.id not in gw._channelfactory newchan = gw.remote_exec(''' assert %d not in channel.gateway._channelfactory._channels ''' % (channel.id)) @@ -142,17 +136,17 @@ def test_channel_receiver_callback(self, gw): l = [] - #channel = gw.newchannel(receiver=l.append) + # 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) + pytest.raises(IOError, channel.receive) channel.waitclose(TESTTIMEOUT) assert len(l) == 3 - assert l[:2] == [42,13] + assert l[:2] == [42, 13] assert isinstance(l[2], channel.__class__) def test_channel_callback_after_receive(self, gw): @@ -165,7 +159,7 @@ x = channel.receive() assert x == 42 channel.setcallback(callback=l.append) - py.test.raises(IOError, channel.receive) + pytest.raises(IOError, channel.receive) channel.waitclose(TESTTIMEOUT) assert len(l) == 2 assert l[0] == 13 @@ -173,8 +167,10 @@ def test_waiting_for_callbacks(self, gw): l = [] + def callback(msg): - import time; time.sleep(0.2) + import time + time.sleep(0.2) l.append(msg) channel = gw.remote_exec(source=''' channel.send(42) @@ -187,6 +183,8 @@ self.check_channel_callback_stays_active(gw, earlyfree=True) def check_channel_callback_stays_active(self, gw, earlyfree=True): + if gw.spec.execmodel == "gevent": + pytest.xfail("investigate gevent failure") # with 'earlyfree==True', this tests the "sendonly" channel state. l = [] channel = gw.remote_exec(source=''' @@ -215,7 +213,7 @@ counter -= 1 print(counter) if not counter: - py.test.fail("timed out waiting for the answer[%d]" % len(l)) + pytest.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 @@ -234,15 +232,15 @@ channel.send(channel.gateway.newchannel()) ''') channel.setcallback(l.append, 999) - py.test.raises(IOError, channel.receive) + pytest.raises(IOError, channel.receive) channel.waitclose(TESTTIMEOUT) assert len(l) == 4 - assert l[:2] == [42,13] + 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() + q = gw.execmodel.queue.Queue() channel = gw.remote_exec(source=''' raise ValueError() ''') @@ -265,12 +263,14 @@ """) subchan = channel.receive() subchan.send(1) - excinfo = py.test.raises(subchan.RemoteError, + excinfo = pytest.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(""" @@ -289,7 +289,7 @@ f = channel.makefile() assert not f.isatty() channel.waitclose(TESTTIMEOUT) - py.test.raises(IOError, f.write, 'hello') + pytest.raises(IOError, f.write, 'hello') def test_channel_file_proxyclose(self, gw): channel = gw.remote_exec(""" @@ -300,7 +300,7 @@ """) first = channel.receive() assert first.strip() == 'hello world' - py.test.raises(channel.RemoteError, channel.receive) + pytest.raises(channel.RemoteError, channel.receive) def test_channel_file_read(self, gw): channel = gw.remote_exec(""" @@ -337,7 +337,65 @@ def test_channel_makefile_incompatmode(self, gw): channel = gw.newchannel() - py.test.raises(ValueError, 'channel.makefile("rw")') + with pytest.raises(ValueError): + channel.makefile("rw") +class TestStringCoerce: + @pytest.mark.skipif('sys.version>="3.0"') + def test_2to3(self, makegateway): + python = _find_version('3') + gw = makegateway('popen//python=%s' % python) + ch = gw.remote_exec('channel.send(channel.receive());'*2) + ch.send('a') + res = ch.receive() + assert isinstance(res, unicode) + + ch.reconfigure(py3str_as_py2str=True) + + ch.send('a') + res = ch.receive() + assert isinstance(res, str) + + gw.reconfigure(py3str_as_py2str=True) + ch = gw.remote_exec('channel.send(channel.receive());'*2) + + ch.send('a') + res = ch.receive() + assert isinstance(res, str) + ch.reconfigure(py3str_as_py2str=False, py2str_as_py3str=False) + + ch.send('a') + res = ch.receive() + assert isinstance(res, str) + gw.exit() + + @pytest.mark.skipif('sys.version<"3.0"') + def test_3to2(self, makegateway): + python = _find_version('2') + gw = makegateway('popen//python=%s' % python) + + ch = gw.remote_exec('channel.send(channel.receive());'*2) + ch.send(bytes('a', 'ascii')) + res = ch.receive() + assert isinstance(res, str) + + ch.reconfigure(py3str_as_py2str=True, py2str_as_py3str=False) + + ch.send('a') + res = ch.receive() + assert isinstance(res, bytes) + + gw.reconfigure(py3str_as_py2str=True, py2str_as_py3str=False) + ch = gw.remote_exec('channel.send(channel.receive());'*2) + + ch.send('a') + res = ch.receive() + assert isinstance(res, bytes) + + ch.reconfigure(py3str_as_py2str=False, py2str_as_py3str=True) + ch.send(bytes('a', 'ascii')) + res = ch.receive() + assert isinstance(res, str) + gw.exit() diff -Nru execnet-1.0.9/testing/test_compatibility_regressions.py execnet-1.4.1/testing/test_compatibility_regressions.py --- execnet-1.0.9/testing/test_compatibility_regressions.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/testing/test_compatibility_regressions.py 2015-09-02 18:47:00.000000000 +0000 @@ -0,0 +1,33 @@ +from execnet import gateway_base + + +def test_opcodes(): + data = vars(gateway_base.opcode) + computed = dict((k, v) for k, v in data.items() if '__' not in k) + assert computed == { + 'BUILDTUPLE': '@'.encode('ascii'), + 'BYTES': 'A'.encode('ascii'), + 'CHANNEL': 'B'.encode('ascii'), + 'FALSE': 'C'.encode('ascii'), + 'FLOAT': 'D'.encode('ascii'), + 'FROZENSET': 'E'.encode('ascii'), + 'INT': 'F'.encode('ascii'), + 'LONG': 'G'.encode('ascii'), + 'LONGINT': 'H'.encode('ascii'), + 'LONGLONG': 'I'.encode('ascii'), + 'NEWDICT': 'J'.encode('ascii'), + 'NEWLIST': 'K'.encode('ascii'), + 'NONE': 'L'.encode('ascii'), + 'PY2STRING': 'M'.encode('ascii'), + 'PY3STRING': 'N'.encode('ascii'), + 'SET': 'O'.encode('ascii'), + 'SETITEM': 'P'.encode('ascii'), + 'STOP': 'Q'.encode('ascii'), + 'TRUE': 'R'.encode('ascii'), + 'UNICODE': 'S'.encode('ascii'), + + # added in 1.4 + # causes a regression since it was ordered in + # between CHANNEL and FALSE as "C" moving the other items + 'COMPLEX': 'T'.encode('ascii'), + } diff -Nru execnet-1.0.9/testing/test_fixes.py execnet-1.4.1/testing/test_fixes.py --- execnet-1.0.9/testing/test_fixes.py 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/testing/test_fixes.py 2015-02-15 23:58:19.000000000 +0000 @@ -0,0 +1,37 @@ +from execnet import Group +from execnet.gateway_bootstrap import fix_pid_for_jython_popen + + +def test_jython_bootstrap_not_on_remote(): + group = Group() + try: + group.makegateway('popen//id=via') + group.makegateway('popen//via=via') + finally: + group.terminate(timeout=1.0) + + +def test_jython_bootstrap_fix(): + group = Group() + gw = group.makegateway('popen') + popen = gw._io.popen + real_pid = popen.pid + try: + # nothing happens when calling it on a normal seyup + fix_pid_for_jython_popen(gw) + assert popen.pid == real_pid + + # if there is no pid for a popen gw, restore + popen.pid = None + fix_pid_for_jython_popen(gw) + assert popen.pid == real_pid + + # if there is no pid for other gw, ignore - they are remote + gw.spec.popen = False + popen.pid = None + fix_pid_for_jython_popen(gw) + assert popen.pid is None + + finally: + popen.pid = real_pid + group.terminate(timeout=1) diff -Nru execnet-1.0.9/testing/test_gateway.py execnet-1.4.1/testing/test_gateway.py --- execnet-1.0.9/testing/test_gateway.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_gateway.py 2015-08-18 06:03:40.000000000 +0000 @@ -1,24 +1,31 @@ """ mostly functional tests of gateways. """ -import os, sys, time +import os import py +import pytest import execnet -from execnet import gateway_base, gateway -from testing.test_serializer import _find_version -TESTTIMEOUT = 10.0 # seconds +from execnet import gateway_base, gateway_io +from test_serializer import _find_version +TESTTIMEOUT = 10.0 # seconds needs_osdup = py.test.mark.skipif("not hasattr(os, 'dup')") + +def fails(*args, **kwargs): + 0/0 + + def test_deprecation(recwarn, monkeypatch): - execnet.PopenGateway() + execnet.PopenGateway().exit() assert recwarn.pop(DeprecationWarning) - monkeypatch.setattr(py.std.socket, 'socket', lambda *args: 0/0) + monkeypatch.setattr(py.std.socket, 'socket', fails) py.test.raises(Exception, 'execnet.SocketGateway("localhost", 8811)') assert recwarn.pop(DeprecationWarning) - monkeypatch.setattr(py.std.subprocess, 'Popen', lambda *args,**kwargs: 0/0) + monkeypatch.setattr(py.std.subprocess, 'Popen', fails) py.test.raises(Exception, 'execnet.SshGateway("not-existing")') assert recwarn.pop(DeprecationWarning) + class TestBasicGateway: def test_correct_setup(self, gw): assert gw.hasreceiver() @@ -36,12 +43,25 @@ def test_gateway_status_simple(self, gw): status = gw.remote_status() - assert not status.execqsize assert status.numexecuting == 0 + def test_exc_info_is_clear_after_gateway_startup(self, gw): + ch = gw.remote_exec(""" + import traceback, sys + excinfo = sys.exc_info() + if excinfo != (None, None, None): + r = traceback.format_exception(*excinfo) + else: + r = 0 + channel.send(r) + """) + res = ch.receive() + if res != 0: + pytest.fail("remote raised\n%s" % res) + def test_gateway_status_no_real_channel(self, gw): numchan = gw._channelfactory.channels() - st = gw.remote_status() + gw.remote_status() numchan2 = gw._channelfactory.channels() # note that on CPython this can not really # fail because refcounting leads to immediate @@ -54,18 +74,20 @@ 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.numexecuting == 2 # number of active execution threads assert status.numchannels == numchannels + 2 ch1.send(None) ch2.send(None) ch1.waitclose() ch2.waitclose() - status = gw.remote_status() - assert status.execqsize == 0 - assert status.numexecuting == 0 + for i in range(10): + status = gw.remote_status() + if status.numexecuting == 0: + break + else: + pytest.fail("did not get correct remote status") # race condition - assert status.numchannels <= numchannels + 1 + assert status.numchannels <= numchannels def test_remote_exec_module(self, tmpdir, gw): p = tmpdir.join("remotetest.py") @@ -87,7 +109,7 @@ """) remotemodules = channel.receive() assert 'py' not in remotemodules, ( - "py should not be imported on remote side") + "py should not be imported on remote side") def test_remote_exec_waitclose(self, gw): channel = gw.remote_exec('pass') @@ -108,7 +130,8 @@ def test_remote_exec_no_explicit_close(self, gw): channel = gw.remote_exec('channel.close()') - excinfo = py.test.raises(channel.RemoteError, + excinfo = py.test.raises( + channel.RemoteError, "channel.waitclose(TESTTIMEOUT)") assert "explicit" in excinfo.value.formatted @@ -156,7 +179,7 @@ assert rinfo.executable assert rinfo.cwd assert rinfo.version_info - s = repr(rinfo) + assert repr(rinfo) old = gw.remote_exec(""" import os.path cwd = os.getcwd() @@ -172,13 +195,14 @@ gw._cache_rinfo = rinfo gw.remote_exec("import os ; os.chdir(%r)" % old).waitclose() + class TestPopenGateway: gwtype = 'popen' - def test_chdir_separation(self, tmpdir): + def test_chdir_separation(self, tmpdir, makegateway): old = tmpdir.chdir() try: - gw = execnet.makegateway('popen') + gw = makegateway('popen') finally: waschangedir = old.chdir() c = gw.remote_exec("import os ; channel.send(os.getcwd())") @@ -186,30 +210,20 @@ assert x.lower() == str(waschangedir).lower() def test_remoteerror_readable_traceback(self, gw): - e = py.test.raises(gateway_base.RemoteError, + 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): + def test_many_popen(self, makegateway): num = 4 l = [] for i in range(num): - l.append(execnet.makegateway('popen')) + l.append(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() @@ -221,8 +235,8 @@ 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') + def test_waitclose_on_remote_killed(self, makegateway): + gw = makegateway('popen') channel = gw.remote_exec(""" import os import time @@ -241,21 +255,42 @@ """) py.test.raises(channel.RemoteError, channel.receive) -def test_socket_gw_host_not_found(gw): - py.test.raises(execnet.HostNotFound, - 'execnet.makegateway("socket=qwepoipqwe:9000")' - ) + @py.test.mark.skipif('sys.version_info < (2,6) or sys.dont_write_bytecode') + def test_dont_write_bytecode(self, makegateway): + check_sys_dont_write_bytecode = """ + import sys + channel.send(sys.dont_write_bytecode) + """ + + gw = makegateway('popen') + channel = gw.remote_exec(check_sys_dont_write_bytecode) + ret = channel.receive() + assert not ret + gw = makegateway('popen//dont_write_bytecode') + channel = gw.remote_exec(check_sys_dont_write_bytecode) + ret = channel.receive() + assert ret + + +@py.test.mark.skipif("config.option.broken_isp") +def test_socket_gw_host_not_found(gw, makegateway): + py.test.raises( + execnet.HostNotFound, lambda: + makegateway("socket=qwepoipqwe:9000")) + class TestSshPopenGateway: gwtype = "ssh" - def test_sshconfig_config_parsing(self, monkeypatch): - import subprocess + def test_sshconfig_config_parsing(self, monkeypatch, makegateway): l = [] - monkeypatch.setattr(subprocess, 'Popen', + monkeypatch.setattr( + gateway_io, "Popen2IOMaster", lambda *args, **kwargs: l.append(args[0])) - py.test.raises(AttributeError, - """execnet.makegateway("ssh=xyz//ssh_config=qwe")""") + py.test.raises( + AttributeError, lambda: + makegateway("ssh=xyz//ssh_config=qwe")) + assert len(l) == 1 popen_args = l[0] i = popen_args.index('-F') @@ -264,13 +299,15 @@ 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')") + def test_host_not_found(self, gw, makegateway): + py.test.raises( + execnet.HostNotFound, lambda: + makegateway('ssh=nowhere.codespeak.net')) + class TestThreads: - def test_threads(self): - gw = execnet.makegateway('popen') + def test_threads(self, makegateway): + gw = makegateway('popen') gw.remote_init_threads(3) c1 = gw.remote_exec("channel.send(channel.receive())") c2 = gw.remote_exec("channel.send(channel.receive())") @@ -281,9 +318,9 @@ res = c1.receive() assert res == 42 - def test_threads_race_sending(self): + def test_threads_race_sending(self, makegateway): # multiple threads sending data in parallel - gw = execnet.makegateway("popen") + gw = makegateway("popen") num = 5 gw.remote_init_threads(num) print ("remote_init_threads(%d)" % num) @@ -302,51 +339,52 @@ for ch in channels: ch.waitclose(TESTTIMEOUT) - def test_status_with_threads(self): - gw = execnet.makegateway('popen') - gw.remote_init_threads(3) + def test_status_with_threads(self, makegateway): + gw = makegateway('popen') 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 + assert rstatus.numexecuting == 2 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) + # there is a slight chance that an execution thread + # is still active although it's accompanying channel + # is already closed. + for i in range(10): + rstatus = gw.remote_status() + if rstatus.numexecuting == 0: + return + assert 0, "numexecuting didn't drop to zero" class TestTracing: - def test_popen_filetracing(self, testdir, monkeypatch): + def test_popen_filetracing(self, testdir, monkeypatch, makegateway): tmpdir = testdir.tmpdir monkeypatch.setenv("TMP", tmpdir) - monkeypatch.setenv("TEMP", tmpdir) # windows + monkeypatch.setenv("TEMP", tmpdir) # windows monkeypatch.setenv('EXECNET_DEBUG', "1") - gw = execnet.makegateway("popen") - pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() - slavefile = tmpdir.join("execnet-debug-%s" % pid) + gw = makegateway("popen") + # hack out the debuffilename + fn = gw.remote_exec( + "import execnet;channel.send(execnet.gateway_base.fn)" + ).receive() + slavefile = py.path.local(fn) assert slavefile.check() 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,)) + py.test.fail("did not find %r in tracefile" % (slave_line,)) gw.exit() - def test_popen_stderr_tracing(self, capfd, monkeypatch): + def test_popen_stderr_tracing(self, capfd, monkeypatch, makegateway): monkeypatch.setenv('EXECNET_DEBUG', "2") - gw = execnet.makegateway("popen") + gw = makegateway("popen") pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() out, err = capfd.readouterr() slave_line = "[%s] creating slavegateway" % pid @@ -355,13 +393,14 @@ def test_no_tracing_by_default(self): assert gateway_base.trace == gateway_base.notrace, \ - "trace does not to default to empty tracing" + "trace does not to default to empty tracing" + class TestStringCoerce: @py.test.mark.skipif('sys.version>="3.0"') - def test_2to3(self): + def test_2to3(self, makegateway): python = _find_version('3') - gw = execnet.makegateway('popen//python=%s'%python) + gw = makegateway('popen//python=%s' % python) ch = gw.remote_exec('channel.send(channel.receive())') ch.send('a') res = ch.receive() @@ -376,9 +415,9 @@ gw.exit() @py.test.mark.skipif('sys.version<"3.0"') - def test_3to2(self): + def test_3to2(self, makegateway): python = _find_version('2') - gw = execnet.makegateway('popen//python=%s'%python) + gw = makegateway('popen//python=%s' % python) ch = gw.remote_exec('channel.send(channel.receive())') ch.send(bytes('a', 'ascii')) @@ -392,4 +431,3 @@ res = ch.receive() assert isinstance(res, bytes) gw.exit() - diff -Nru execnet-1.0.9/testing/test_multi.py execnet-1.4.1/testing/test_multi.py --- execnet-1.0.9/testing/test_multi.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_multi.py 2015-02-15 23:58:19.000000000 +0000 @@ -2,14 +2,19 @@ tests for multi channels and gateway Groups """ +import pytest +from time import sleep import execnet import py from execnet.gateway_base import Channel from execnet import XSpec +from execnet.multi import safe_terminate +from execnet.multi import Group + class TestMultiChannelAndGateway: - def test_multichannel_container_basics(self): - mch = execnet.MultiChannel([Channel(None, i) for i in range(3)]) + def test_multichannel_container_basics(self, gw, execmodel): + mch = execnet.MultiChannel([Channel(gw, i) for i in range(3)]) assert len(mch) == 3 channels = list(mch) assert len(channels) == 3 @@ -33,7 +38,7 @@ assert len(l) == 2 assert l == [(pc1, 12), (pc2, 12)] l = multichannel.receive_each(withchannel=False) - assert l == [12,12] + assert l == [12, 12] def test_multichannel_send_each(self): gm = execnet.Group(["popen"] * 2) @@ -43,7 +48,20 @@ """) mc.send_each(41) l = mc.receive_each() - assert l == [42,42] + assert l == [42, 42] + + def test_Group_execmodel_setting(self): + gm = execnet.Group() + gm.set_execmodel("thread") + assert gm.execmodel.backend == "thread" + assert gm.remote_execmodel.backend == "thread" + gm._gateways.append(1) + try: + with pytest.raises(ValueError): + gm.set_execmodel("eventlet") + assert gm.execmodel.backend == "thread" + finally: + gm._gateways.pop() def test_multichannel_receive_queue_for_two_subprocesses(self): gm = execnet.Group(["popen"] * 2) @@ -61,6 +79,7 @@ def test_multichannel_waitclose(self): l = [] + class pseudochannel: def waitclose(self): l.append(0) @@ -69,7 +88,6 @@ assert len(l) == 2 -from execnet.multi import Group class TestGroup: def test_basic_group(self, monkeypatch): import atexit @@ -79,11 +97,23 @@ assert atexitlist == [group._cleanup_atexit] exitlist = [] joinlist = [] + + class PseudoIO: + def wait(self): + pass + + class PseudoSpec: + via = None + class PseudoGW: id = "9999" + _io = PseudoIO() + spec = PseudoSpec() + def exit(self): exitlist.append(self) group._unregister(self) + def join(self): joinlist.append(self) gw = PseudoGW() @@ -115,9 +145,9 @@ def test_group_ordering_and_termination(self): group = Group() - gw = group.makegateway("popen//id=3") - gw = group.makegateway("popen//id=2") - gw = group.makegateway("popen//id=5") + group.makegateway("popen//id=3") + group.makegateway("popen//id=2") + group.makegateway("popen//id=5") gwlist = list(group) assert len(gwlist) == 3 idlist = [x.id for x in gwlist] @@ -137,7 +167,8 @@ assert gw.id == "hello" gw = group.makegateway(specs[0]) assert gw.id == "gw0" - #py.test.raises(ValueError, group.allocate_id, XSpec("popen//id=hello")) + # py.test.raises(ValueError, + # group.allocate_id, XSpec("popen//id=hello")) group.terminate() def test_gateway_and_id(self): @@ -156,8 +187,68 @@ 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 - + try: + newlist = list(execnet.default_group) + assert len(newlist) == len(oldlist) + 1 + assert gw in newlist + assert gw not in oldlist + finally: + gw.exit() + + def test_remote_exec_args(self): + group = Group() + group.makegateway('popen') + + def fun(channel, arg): + channel.send(arg) + mch = group.remote_exec(fun, arg=1) + result = mch.receive_each() + assert result == [1] + + def test_terminate_with_proxying(self): + group = Group() + group.makegateway('popen//id=master') + group.makegateway('popen//via=master//id=slave') + group.terminate(1.0) + + +@pytest.mark.skipif("sys.version_info < (2,6)") +def test_safe_terminate(execmodel): + if execmodel.backend != "threading": + pytest.xfail("execution model %r does not support task count" % + execmodel.backend) + import threading + active = threading.active_count() + l = [] + + def term(): + py.std.time.sleep(3) + + def kill(): + l.append(1) + safe_terminate(execmodel, 1, [(term, kill)] * 10) + assert len(l) == 10 + sleep(0.1) + py.std.gc.collect() + assert execmodel.active_count() == active + + +@pytest.mark.skipif("sys.version_info < (2,6)") +def test_safe_terminate2(execmodel): + if execmodel.backend != "threading": + pytest.xfail("execution model %r does not support task count" % + execmodel.backend) + import threading + active = threading.active_count() + l = [] + + def term(): + return + + def kill(): + l.append(1) + safe_terminate(execmodel, 3, [(term, kill)] * 10) + assert len(l) == 0 + sleep(0.1) + py.std.gc.collect() + assert threading.active_count() == active diff -Nru execnet-1.0.9/testing/test_rsync.py execnet-1.4.1/testing/test_rsync.py --- execnet-1.0.9/testing/test_rsync.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_rsync.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,23 +1,47 @@ +import pytest import py from execnet import RSync import execnet +from test_serializer import _find_version -def pytest_funcarg__gw1(request): - return request.cached_setup( - setup=lambda: execnet.makegateway("popen"), - teardown=lambda val: val.exit(), - scope="module" - ) -pytest_funcarg__gw2 = pytest_funcarg__gw1 -def pytest_funcarg__dirs(request): - t = request.getfuncargvalue('tmpdir') +@pytest.fixture(scope='module') +def group(request): + group = execnet.Group() + request.addfinalizer(group.terminate) + return group + + +@pytest.fixture(scope='module') +def gw1(request, group): + gw = group.makegateway('popen//id=gw1') + request.addfinalizer(gw.exit) + return gw + + +@pytest.fixture(scope='module') +def gw2(request, group): + gw = group.makegateway('popen//id=gw2') + request.addfinalizer(gw.exit) + return gw + + +needssymlink = pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="py.path.local has no mksymlinkto() on this platform") + + +@pytest.fixture +def dirs(request, tmpdir): + t = tmpdir + class dirs: source = t.join("source") dest1 = t.join("dest1") dest2 = t.join("dest2") return dirs + class TestRSync: def test_notargets(self, dirs): rsync = RSync(dirs.source) @@ -128,7 +152,7 @@ mode = destdir.stat().mode assert mode & 511 == 504 - @py.test.mark.skipif("not hasattr(os, 'symlink')") + @needssymlink def test_symlink_rsync(self, dirs, gw1): source = dirs.source dest = dirs.dest1 @@ -144,7 +168,7 @@ assert dest.join('rellink').readlink() == "subdir/existant" assert dest.join('abslink').readlink() == expected - @py.test.mark.skipif("not hasattr(os, 'symlink')") + @needssymlink def test_symlink2_rsync(self, dirs, gw1): source = dirs.source dest = dirs.dest1 @@ -169,15 +193,20 @@ source.ensure("existant").write("a" * 100) source.ensure("existant2").write("a" * 10) total = {} + def callback(cmd, lgt, channel): total[(cmd, lgt)] = True rsync = RSync(source, callback=callback) - #rsync = RSync() + # rsync = RSync() rsync.add_target(gw1, dest) rsync.send() - assert total == {("list", 110):True, ("ack", 100):True, ("ack", 10):True} + assert total == { + ("list", 110): True, + ("ack", 100): True, + ("ack", 10): True, + } def test_file_disappearing(self, dirs, gw1): dest = dirs.dest1 @@ -200,3 +229,15 @@ assert len(dest.listdir()) == 1 assert len(source.listdir()) == 1 + @py.test.mark.skip_if('sys.version_info >= (3)') + def test_2_to_3_bridge_can_send_binary_files(self, tmpdir, makegateway): + python = _find_version('3') + gw = makegateway('popen//python=%s' % python) + source = tmpdir.ensure('source', dir=1) + for i, content in enumerate('foo bar baz \x10foo'): + source.join(str(i)).write(content) + rsync = RSync(source) + + target = tmpdir.join('target') + rsync.add_target(gw, target) + rsync.send() diff -Nru execnet-1.0.9/testing/test_serializer.py execnet-1.4.1/testing/test_serializer.py --- execnet-1.0.9/testing/test_serializer.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_serializer.py 2015-07-22 22:08:47.000000000 +0000 @@ -1,35 +1,38 @@ # -*- coding: utf-8 -*- import sys -import os import tempfile import subprocess import py import execnet -from execnet import gateway_base as serializer +import pytest + +MINOR_VERSIONS = { + '3': '543210', + '2': '76', +} def _find_version(suffix=""): name = "python" + suffix executable = py.path.local.sysfind(name) if executable is None: - if suffix == "2": - for name in ('python2.6', 'python2.7'): - executable = py.path.local.sysfind(name) - if executable: - return executable - elif suffix == "3": - for name in ('python3.1', 'python3.2'): - executable = py.path.local.sysfind(name) - if executable: - return executable 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,)) + for tail in MINOR_VERSIONS.get(suffix, ''): + path = py.path.local.sysfind('%s.%s' % (name, tail)) + if path: + return path + + else: + py.test.skip("can't find a %r executable" % (name,)) return executable +TEMPDIR = _py2_wrapper = _py3_wrapper = None + + def setup_module(mod): mod.TEMPDIR = py.path.local(tempfile.mkdtemp()) if sys.version_info > (3, 0): @@ -39,10 +42,13 @@ mod._py3_wrapper = PythonWrapper(_find_version("3")) mod._py2_wrapper = PythonWrapper(py.path.local(sys.executable)) + def teardown_module(mod): TEMPDIR.remove(True) pyimportdir = str(py.path.local(execnet.__file__).dirpath().dirpath()) + + class PythonWrapper(object): def __init__(self, executable): @@ -56,7 +62,7 @@ from execnet import gateway_base as serializer if sys.version_info > (3, 0): # Need binary output sys.stdout = sys.stdout.detach() -saver = serializer.serialize(sys.stdout, %s) +sys.stdout.write(serializer.dumps_internal(%s)) """ % (pyimportdir, obj_rep,)) popen = subprocess.Popen([str(self.executable), str(script_file)], stdin=subprocess.PIPE, @@ -97,91 +103,83 @@ return "" % (self.executable,) -def pytest_funcarg__py2(request): +@pytest.fixture +def py2(request): return _py2_wrapper -def pytest_funcarg__py3(request): + +@pytest.fixture +def py3(request): return _py3_wrapper -def pytest_funcarg__dump(request): - py_dump = request.getfuncargvalue(request.param[0]) - return py_dump.dump - -def pytest_funcarg__load(request): - py_dump = request.getfuncargvalue(request.param[1]) - return py_dump.load - -def pytest_generate_tests(metafunc): - if 'dump' in metafunc.funcargnames and 'load' in metafunc.funcargnames: - pys = 'py2', 'py3' - for dump in pys: - for load in pys: - param = (dump, load) - conversion = '%s to %s'%param - if 'repr' not in metafunc.funcargnames: - metafunc.addcall(id=conversion, param=param) - else: - for tp, repr in simple_tests.items(): - metafunc.addcall( - id='%s:%s'%(tp, conversion), - param=param, - funcargs={'tp_name':tp, 'repr':repr}, - ) - - -simple_tests = { -# type: expected before/after repr - 'int': '4', - 'float':'3.25', - 'list': '[1, 2, 3]', - 'tuple': '(1, 2, 3)', - 'dict': '{(1, 2, 3): 32}', -} +@pytest.fixture(params=['py2', 'py3']) +def dump(request): + return request.getfuncargvalue(request.param).dump + + +@pytest.fixture(params=['py2', 'py3']) +def load(request): + return request.getfuncargvalue(request.param).load + + +simple_tests = [ + # type: expected before/after repr + ('int', '4'), + ('float', '3.25'), + ('complex', '(1.78+3.14j)'), + ('list', '[1, 2, 3]'), + ('tuple', '(1, 2, 3)'), + ('dict', '{(1, 2, 3): 32}'), +] + + +@py.test.mark.parametrize(["tp_name", "repr"], simple_tests) def test_simple(tp_name, repr, dump, load): p = dump(repr) - tp , v = load(p) + tp, v = load(p) 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])" # 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}" # 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" - 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()" + +def test_set(py2, py3, dump): + p = dump("set((1, 2, 3))") + tp, v = py2.load(p) + assert tp == "set" + # 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}" # 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" + assert v == "set([])" + tp, v = py3.load(p) + assert tp == "set" + assert v == "set()" + + +def test_frozenset(py2, 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()" + def test_long(py2, py3): really_big = "9223372036854775807324234" @@ -200,6 +198,7 @@ assert tp == "long" assert v == really_big + "L" + def test_small_long(py2, py3): p = py2.dump("123L") tp, s = py2.load(p) @@ -207,6 +206,7 @@ tp, s = py3.load(p) assert s == "123" + def test_bytes(py2, py3): p = py3.dump("b'hi'") tp, v = py2.load(p) @@ -216,6 +216,7 @@ assert tp == "bytes" assert v == "b'hi'" + def test_str(py2, py3): p = py2.dump("'xyz'") tp, s = py2.load(p) @@ -228,6 +229,7 @@ assert s == "b'xyz'" assert tp == "bytes" + def test_unicode(py2, py3): p = py2.dump("u'hi'") tp, s = py2.load(p) @@ -241,9 +243,11 @@ assert tp == "str" assert s == "'hi'" tp, s = py2.load(p) - assert tp == "unicode" # depends on unserialization defaults + # depends on unserialization defaults + assert tp == "unicode" assert s == "u'hi'" + def test_bool(py2, py3): p = py2.dump("True") tp, s = py2.load(p) @@ -256,13 +260,13 @@ 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) + +def test_none(dump, load): + p = dump("None") + tp, s = load(p) assert s == "None" + def test_tuple_nested_with_empty_in_between(py2): p = py2.dump("(1, (), 3)") tp, s = py2.load(p) diff -Nru execnet-1.0.9/testing/test_termination.py execnet-1.4.1/testing/test_termination.py --- execnet-1.0.9/testing/test_termination.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_termination.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,33 +1,32 @@ - -import sys, os +import pytest 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 +from 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(""" + +def test_exit_blocked_slave_execution_gateway(anypython, makegateway, pool): + gateway = makegateway('popen//python=%s' % anypython) + gateway.remote_exec(""" import time time.sleep(10.0) """) + def doit(): gateway.exit() return 17 - pool = WorkerPool() - reply = pool.dispatch(doit) + reply = pool.spawn(doit) x = reply.get(timeout=5.0) assert x == 17 -def test_endmarker_delivery_on_remote_killterm(): - gw = execnet.makegateway('popen') - q = queue.Queue() + +def test_endmarker_delivery_on_remote_killterm(makegateway, execmodel): + if execmodel.backend != "thread": + pytest.xfail("test and execnet not compatible to greenlets yet") + gw = makegateway('popen') + q = execmodel.queue.Queue() channel = gw.remote_exec(source=''' import os, time channel.send(os.getpid()) @@ -41,15 +40,15 @@ err = channel._getremoteerror() assert isinstance(err, EOFError) -def test_termination_on_remote_channel_receive(monkeypatch): + +def test_termination_on_remote_channel_receive(monkeypatch, makegateway): 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") + gw = makegateway("popen") pid = gw.remote_exec("import os ; channel.send(os.getpid())").receive() gw.remote_exec("channel.receive()") - group.terminate() + gw._group.terminate() command = ["ps", "-p", str(pid)] popen = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -57,31 +56,36 @@ out = py.builtin._totext(out, 'utf8') assert str(pid) not in out, out + def test_close_initiating_remote_no_error(testdir, anypython): - if '2.4' in str(anypython): - py.test.xfail("race/wait/interrupt_main/thread-loop issue with python2.4") p = testdir.makepyfile(""" import sys sys.path.insert(0, %r) import execnet gw = execnet.makegateway("popen") - gw.remote_init_threads(num=3) + print ("remote_exec1") ch1 = gw.remote_exec("channel.receive()") + print ("remote_exec1") ch2 = gw.remote_exec("channel.receive()") - ch3 = gw.remote_exec("channel.receive()") + print ("termination") execnet.default_group.terminate() """ % str(execnetdir)) - popen = subprocess.Popen([str(anypython), str(p)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE,) + popen = subprocess.Popen( + [str(anypython), str(p)], + stdout=None, stderr=subprocess.PIPE,) out, err = popen.communicate() - print (out) print (err) err = err.decode('utf8') - lines = [x for x in err.splitlines() - if '*sys-package' not in x] + lines = [ + x for x in err.splitlines() + if '*sys-package' not in x] + # print (lines) assert not lines -def test_terminate_implicit_does_trykill(testdir, anypython, capfd): + +def test_terminate_implicit_does_trykill(testdir, anypython, capfd, pool): + if pool.execmodel != "thread": + pytest.xfail("only os threading model supported") p = testdir.makepyfile(""" import sys sys.path.insert(0, %r) @@ -93,14 +97,23 @@ sys.stdout.write("1\\n") sys.stdout.flush() sys.stdout.close() + class FlushNoOp(object): + def flush(self): + pass + # replace stdout since some python implementations + # flush and print errors (for example 3.2) + # see Issue #5319 (from the release notes of 3.2 Alpha 2) + sys.stdout = FlushNoOp() + # 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) + popen.stdout.readline() + reply = pool.spawn(popen.communicate) reply.get(timeout=50) out, err = capfd.readouterr() - lines = [x for x in err.splitlines() - if '*sys-package' not in x] + lines = [ + x for x in err.splitlines() + if '*sys-package' not in x] assert not lines or "Killed" in err diff -Nru execnet-1.0.9/testing/test_threadpool.py execnet-1.4.1/testing/test_threadpool.py --- execnet-1.0.9/testing/test_threadpool.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_threadpool.py 2015-02-15 23:58:19.000000000 +0000 @@ -1,92 +1,209 @@ - +from __future__ import with_statement +import pytest import py import sys -from execnet.threadpool import queue, WorkerPool +import os +from execnet.gateway_base import WorkerPool + + +def test_execmodel(execmodel, tmpdir): + assert execmodel.backend + p = tmpdir.join("somefile") + p.write("content") + fd = os.open(str(p), os.O_RDONLY) + f = execmodel.fdopen(fd, "r") + assert f.read() == "content" + f.close() + + +def test_execmodel_basic_attrs(execmodel): + m = execmodel + assert callable(m.start) + assert m.get_ident() + -def test_some(): - pool = WorkerPool() - q = queue.Queue() +def test_simple(pool): + reply = pool.spawn(lambda: 42) + assert reply.get() == 42 + + +def test_some(pool, execmodel): + q = execmodel.queue.Queue() num = 4 def f(i): q.put(i) while q.qsize(): - py.std.time.sleep(0.01) + execmodel.sleep(0.01) for i in range(num): - pool.dispatch(f, i) + pool.spawn(f, i) for i in range(num): q.get() - assert len(pool._alive) == 4 - pool.shutdown() - # XXX I replaced the following join() with a time.sleep(1), which seems - # to fix the test on Windows, and doesn't break it on Linux... Completely - # unsure what the idea is, though, so it would be nice if someone with some - # more understanding of what happens here would either fix this better, or - # remove this comment... - # pool.join(timeout=1.0) - py.std.time.sleep(1) - assert len(pool._alive) == 0 - assert len(pool._ready) == 0 + # assert len(pool._running) == 4 + assert pool.waitall(timeout=1.0) + # execmodel.sleep(1) helps on windows? + assert len(pool._running) == 0 + + +def test_running_semnatics(pool, execmodel): + q = execmodel.queue.Queue() + + def first(): + q.get() -def test_get(): - pool = WorkerPool() + reply = pool.spawn(first) + assert reply.running + assert pool.active_count() == 1 + q.put(1) + assert pool.waitall() + assert pool.active_count() == 0 + assert not reply.running + + +def test_waitfinish_on_reply(pool): + l = [] + reply = pool.spawn(lambda: l.append(1)) + reply.waitfinish() + assert l == [1] + reply = pool.spawn(lambda: 0/0) + reply.waitfinish() # no exception raised + pytest.raises(ZeroDivisionError, reply.get) + + +@pytest.mark.xfail(reason="WorkerPool does not implement limited size") +def test_limited_size(execmodel): + pool = WorkerPool(execmodel, size=1) + q = execmodel.queue.Queue() + q2 = execmodel.queue.Queue() + q3 = execmodel.queue.Queue() + + def first(): + q.put(1) + q2.get() + pool.spawn(first) + assert q.get() == 1 + + def second(): + q3.put(3) + # we spawn a second pool to spawn the second function + # which should block + pool2 = WorkerPool(execmodel) + pool2.spawn(pool.spawn, second) + assert not pool2.waitall(1.0) + assert q3.qsize() == 0 + q2.put(2) + assert pool2.waitall() + assert pool.waitall() + + +def test_get(pool): def f(): return 42 - reply = pool.dispatch(f) + reply = pool.spawn(f) result = reply.get() assert result == 42 -def test_get_timeout(): - pool = WorkerPool() + +def test_get_timeout(execmodel, pool): def f(): - py.std.time.sleep(0.2) + execmodel.sleep(0.2) return 42 - reply = pool.dispatch(f) - py.test.raises(IOError, "reply.get(timeout=0.01)") + reply = pool.spawn(f) + with pytest.raises(IOError): + reply.get(timeout=0.01) -def test_get_excinfo(): - pool = WorkerPool() + +def test_get_excinfo(pool): def f(): raise ValueError("42") - reply = pool.dispatch(f) - excinfo = py.test.raises(ValueError, "reply.get(1.0)") - py.test.raises(EOFError, "reply.get(1.0)") + reply = pool.spawn(f) + with py.test.raises(ValueError): + reply.get(1.0) + with pytest.raises(ValueError): + reply.get(1.0) + + +def test_waitall_timeout(pool, execmodel): + q = execmodel.queue.Queue() -def test_maxthreads(): - pool = WorkerPool(maxthreads=1) - def f(): - py.std.time.sleep(0.5) - try: - pool.dispatch(f) - py.test.raises(IOError, pool.dispatch, f) - finally: - pool.shutdown() - -def test_join_timeout(): - pool = WorkerPool() - q = queue.Queue() def f(): q.get() - reply = pool.dispatch(f) - pool.shutdown() - py.test.raises(IOError, pool.join, 0.01) + reply = pool.spawn(f) + assert not pool.waitall(0.01) q.put(None) reply.get(timeout=1.0) - pool.join(timeout=0.1) + assert pool.waitall(timeout=0.1) + @py.test.mark.skipif("not hasattr(os, 'dup')") -def test_pool_clean_shutdown(): +def test_pool_clean_shutdown(pool): capture = py.io.StdCaptureFD() - pool = WorkerPool() + q = pool.execmodel.queue.Queue() + def f(): - pass - pool.dispatch(f) - pool.dispatch(f) - pool.shutdown() - pool.join(timeout=1.0) - assert not pool._alive - assert not pool._ready + q.get() + pool.spawn(f) + assert not pool.waitall(timeout=1.0) + pool.trigger_shutdown() + with pytest.raises(ValueError): + pool.spawn(f) + + def wait_then_put(): + pool.execmodel.sleep(0.1) + q.put(1) + pool.execmodel.start(wait_then_put) + assert pool.waitall() out, err = capture.reset() - print(out) + sys.stdout.write(out + "\n") sys.stderr.write(err + "\n") assert err == '' + + +def test_primary_thread_integration(execmodel): + if execmodel.backend != "thread": + with pytest.raises(ValueError): + WorkerPool(execmodel=execmodel, hasprimary=True) + return + pool = WorkerPool(execmodel=execmodel, hasprimary=True) + queue = execmodel.queue.Queue() + + def do_integrate(): + queue.put(execmodel.get_ident()) + pool.integrate_as_primary_thread() + execmodel.start(do_integrate) + + def func(): + queue.put(execmodel.get_ident()) + pool.spawn(func) + ident1 = queue.get() + ident2 = queue.get() + assert ident1 == ident2 + pool.terminate() + + +def test_primary_thread_integration_shutdown(execmodel): + if execmodel.backend != "thread": + pytest.skip("can only run with threading") + pool = WorkerPool(execmodel=execmodel, hasprimary=True) + queue = execmodel.queue.Queue() + + def do_integrate(): + queue.put(execmodel.get_ident()) + pool.integrate_as_primary_thread() + execmodel.start(do_integrate) + queue.get() + + queue2 = execmodel.queue.Queue() + + def get_two(): + queue.put(execmodel.get_ident()) + queue2.get() + reply = pool.spawn(get_two) + # make sure get_two is running and blocked on queue2 + queue.get() + # then shut down + pool.trigger_shutdown() + # and let get_two finish + queue2.put(1) + reply.get() + assert pool.waitall(5.0) diff -Nru execnet-1.0.9/testing/test_xspec.py execnet-1.4.1/testing/test_xspec.py --- execnet-1.0.9/testing/test_xspec.py 2010-11-25 17:59:02.000000000 +0000 +++ execnet-1.4.1/testing/test_xspec.py 2015-04-01 14:52:10.000000000 +0000 @@ -1,18 +1,25 @@ -import pytest, py +import os +import py +import sys +import pytest import execnet +import subprocess +from execnet.gateway_io import ssh_args, popen_args, vagrant_ssh_args XSpec = execnet.XSpec + class TestXSpec: def test_norm_attributes(self): - spec = XSpec("socket=192.168.102.2:8888//python=c:/this/python2.5//chdir=d:\hello") + spec = XSpec("socket=192.168.102.2:8888//python=c:/this/python2.5" + "//chdir=d:\hello") assert spec.socket == "192.168.102.2:8888" assert spec.python == "c:/this/python2.5" assert spec.chdir == "d:\hello" assert spec.nice is None assert not hasattr(spec, '_xyz') - py.test.raises(AttributeError, "spec._hello") + pytest.raises(AttributeError, "spec._hello") spec = XSpec("socket=192.168.102.2:8888//python=python2.5//nice=3") assert spec.socket == "192.168.102.2:8888" @@ -20,13 +27,50 @@ assert spec.chdir is None assert spec.nice == "3" - spec = XSpec("ssh=user@host//chdir=/hello/this//python=/usr/bin/python2.5") + spec = XSpec("ssh=user@host" + "//chdir=/hello/this//python=/usr/bin/python2.5") assert spec.ssh == "user@host" assert spec.python == "/usr/bin/python2.5" assert spec.chdir == "/hello/this" spec = XSpec("popen") - assert spec.popen == True + assert spec.popen is True + + def test_ssh_options(self): + spec = XSpec("ssh=-p 22100 user@host//python=python3") + assert spec.ssh == "-p 22100 user@host" + assert spec.python == "python3" + + spec = XSpec( + "ssh=-i ~/.ssh/id_rsa-passwordless_login -p 22100 user@host" + "//python=python3") + assert spec.ssh == \ + "-i ~/.ssh/id_rsa-passwordless_login -p 22100 user@host" + assert spec.python == "python3" + + def test_execmodel(self): + spec = XSpec("execmodel=thread") + assert spec.execmodel == "thread" + spec = XSpec("execmodel=eventlet") + assert spec.execmodel == "eventlet" + + def test_ssh_options_and_config(self): + spec = XSpec("ssh=-p 22100 user@host//python=python3") + spec.ssh_config = "/home/user/ssh_config" + assert ssh_args(spec)[:6] == [ + "ssh", "-C", "-F", spec.ssh_config, "-p", "22100"] + + def test_vagrant_options(self): + spec = XSpec("vagrant_ssh=default//python=python3") + assert vagrant_ssh_args(spec)[:-1] == [ + 'vagrant', 'ssh', 'default', '--', '-C'] + + def test_popen_with_sudo_python(self): + spec = XSpec("popen//python=sudo python3") + assert popen_args(spec) == [ + 'sudo', 'python3', '-u', '-c', + 'import sys;exec(eval(sys.stdin.readline()))' + ] def test_env(self): xspec = XSpec("popen//env:NAME=value1") @@ -42,8 +86,8 @@ assert XSpec(x)._spec == x def test_samekeyword_twice_raises(self): - py.test.raises(ValueError, "XSpec('popen//popen')") - py.test.raises(ValueError, "XSpec('popen//popen=123')") + pytest.raises(ValueError, XSpec, 'popen//popen') + pytest.raises(ValueError, XSpec, 'popen//popen=123') def test_unknown_keys_allowed(self): xspec = XSpec("hello=3") @@ -60,22 +104,24 @@ assert XSpec("popen//python=123") != XSpec("popen") assert hash(XSpec("socket=hello:8080")) != hash(XSpec("popen")) + class TestMakegateway: - def test_no_type(self): - py.test.raises(ValueError, "execnet.makegateway('hello')") + def test_no_type(self, makegateway): + pytest.raises(ValueError, lambda: makegateway('hello')) - def test_popen_default(self): - gw = execnet.makegateway("") + def test_popen_default(self, makegateway): + gw = makegateway("") assert gw.spec.popen - assert gw.spec.python == None + assert gw.spec.python is None 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 + # assert rinfo.executable == sys.executable + assert rinfo.cwd == os.getcwd() + assert rinfo.version_info == sys.version_info @pytest.mark.skipif("not hasattr(os, 'nice')") - def test_popen_nice(self): - gw = execnet.makegateway("popen") + def test_popen_nice(self, makegateway): + gw = makegateway("popen") + def getnice(channel): import os if hasattr(os, 'nice'): @@ -85,12 +131,12 @@ remotenice = gw.remote_exec(getnice).receive() gw.exit() if remotenice is not None: - gw = execnet.makegateway("popen//nice=5") + gw = makegateway("popen//nice=5") remotenice2 = gw.remote_exec(getnice).receive() assert remotenice2 == remotenice + 5 - def test_popen_env(self): - gw = execnet.makegateway("popen//env:NAME123=123") + def test_popen_env(self, makegateway): + gw = makegateway("popen//env:NAME123=123") ch = gw.remote_exec(""" import os channel.send(os.environ['NAME123']) @@ -98,56 +144,57 @@ value = ch.receive() assert value == "123" - def test_popen_explicit(self): - gw = execnet.makegateway("popen//python=%s" % py.std.sys.executable) - assert gw.spec.python == py.std.sys.executable - 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_popen_explicit(self, makegateway): + gw = makegateway("popen//python=%s" % sys.executable) + assert gw.spec.python == sys.executable + rinfo = gw._rinfo() + assert rinfo.executable == sys.executable + assert rinfo.cwd == os.getcwd() + assert rinfo.version_info == sys.version_info - def test_popen_cpython25(self): + def test_popen_cpython25(self, makegateway): for trypath in ('python2.5', r'C:\Python25\python.exe'): cpython25 = py.path.local.sysfind(trypath) if cpython25 is not None: cpython25 = cpython25.realpath() break else: - py.test.skip("cpython2.5 not found") - gw = execnet.makegateway("popen//python=%s" % cpython25) + pytest.skip("cpython2.5 not found") + gw = makegateway("popen//python=%s" % cpython25) rinfo = gw._rinfo() - if py.std.sys.platform != "darwin": # it's confusing there + if sys.platform != "darwin": # it's confusing there assert rinfo.executable == cpython25 - assert rinfo.cwd == py.std.os.getcwd() - assert rinfo.version_info[:2] == (2,5) + assert rinfo.cwd == os.getcwd() + assert rinfo.version_info[:2] == (2, 5) - def test_popen_cpython26(self): + def test_popen_cpython26(self, makegateway): for trypath in ('python2.6', r'C:\Python26\python.exe'): cpython26 = py.path.local.sysfind(trypath) if cpython26 is not None: break else: py.test.skip("cpython2.6 not found") - gw = execnet.makegateway("popen//python=%s" % cpython26) + gw = makegateway("popen//python=%s" % cpython26) rinfo = gw._rinfo() - #assert rinfo.executable == cpython26 - assert rinfo.cwd == py.std.os.getcwd() - assert rinfo.version_info[:2] == (2,6) + # assert rinfo.executable == cpython26 + assert rinfo.cwd == os.getcwd() + assert rinfo.version_info[:2] == (2, 6) - def test_popen_chdir_absolute(self, testdir): - gw = execnet.makegateway("popen//chdir=%s" % testdir.tmpdir) + def test_popen_chdir_absolute(self, testdir, makegateway): + gw = makegateway("popen//chdir=%s" % testdir.tmpdir) rinfo = gw._rinfo() assert rinfo.cwd == str(testdir.tmpdir.realpath()) - def test_popen_chdir_newsub(self, testdir): + def test_popen_chdir_newsub(self, testdir, makegateway): testdir.chdir() - gw = execnet.makegateway("popen//chdir=hello") + gw = makegateway("popen//chdir=hello") rinfo = gw._rinfo() - assert rinfo.cwd.lower() == str(testdir.tmpdir.join("hello").realpath()).lower() + expected = str(testdir.tmpdir.join("hello").realpath()).lower() + assert rinfo.cwd.lower() == expected - def test_ssh(self, specssh): + def test_ssh(self, specssh, makegateway): sshhost = specssh.ssh - gw = execnet.makegateway("ssh=%s//id=ssh1" % sshhost) + gw = makegateway("ssh=%s//id=ssh1" % sshhost) rinfo = gw._rinfo() assert gw.id == 'ssh1' gw2 = execnet.SshGateway(sshhost) @@ -156,8 +203,21 @@ assert rinfo.cwd == rinfo2.cwd assert rinfo.version_info == rinfo2.version_info - def test_socket(self, specsocket): - gw = execnet.makegateway("socket=%s//id=sock1" % specsocket.socket) + def test_vagrant(self, makegateway, tmpdir, monkeypatch): + vagrant = py.path.local.sysfind('vagrant') + if vagrant is None: + pytest.skip('Vagrant binary not in PATH') + monkeypatch.chdir(tmpdir) + subprocess.check_call("vagrant init hashicorp/precise32", shell=True) + subprocess.check_call("vagrant up --provider virtualbox", shell=True) + gw = makegateway("vagrant_ssh=default") + rinfo = gw._rinfo() + rinfo.cwd == '/home/vagrant' + rinfo.executable == '/usr/bin/python' + subprocess.check_call("vagrant halt", shell=True) + + def test_socket(self, specsocket, makegateway): + gw = makegateway("socket=%s//id=sock1" % specsocket.socket) rinfo = gw._rinfo() assert rinfo.executable assert rinfo.cwd @@ -165,11 +225,15 @@ assert gw.id == "sock1" # we cannot instantiate a second gateway - #gw2 = execnet.SocketGateway(*specsocket.socket.split(":")) - #rinfo2 = gw2._rinfo() - #assert rinfo.executable == rinfo2.executable - #assert rinfo.cwd == rinfo2.cwd - #assert rinfo.version_info == rinfo2.version_info + @pytest.mark.xfail(reason='we can\'t instantiate a second gateway') + def test_socket_second(self, specsocket, makegateway): + gw = makegateway("socket=%s//id=sock1" % specsocket.socket) + gw2 = makegateway("socket=%s//id=sock1" % specsocket.socket) + rinfo = gw._rinfo() + rinfo2 = gw2._rinfo() + 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() diff -Nru execnet-1.0.9/tox.ini execnet-1.4.1/tox.ini --- execnet-1.0.9/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ execnet-1.4.1/tox.ini 2015-04-01 14:42:44.000000000 +0000 @@ -0,0 +1,59 @@ +[tox] +envlist=py26,py27,py33,py34,pypy,flakes + +[testenv] +changedir=testing +deps= + apipkg + pytest + pytest-timeout +commands=py.test -rsfxX {posargs} + +[testenv:flakes] +changedir= +deps= flake8 +commands = flake8 setup.py execnet testing + +[testenv:jython] +commands=jython -m pytest -rsfxX {posargs} + +[testenv:docs] +basepython=python +changedir=doc +deps=sphinx + pytest + apipkg +commands= + py.test -rsfxX -v check_sphinx.py + +[testenv:subprocess32] +basepython=python2.7 +deps=pytest + pytest-timeout + subprocess32 + apipkg + +[testenv:upload-devpi] +changedir=. +deps=hgdistver + wheel + devpi +commands= + devpi upload --no-vcs --formats sdist,bdist_wheel + +[testenv:upload-release] +changedir=. +deps=hgdistver + wheel + setuptools +commands= + python setup.py register sdist bdist_wheel upload + + +[pytest] +timeout = 20 +addopts = -rxXs +rsyncdirs = execnet testing + +[flake8] +exclude = .env/,.tox/,build/,dist/,execnet/apipkg.py