envdir
Advanced tools
+6
-4
| Authors | ||
| ======= | ||
| George Yoshida | ||
| Horst Gutmann | ||
| Jannis Leidel | ||
| Kuba Janoszek | ||
| - Daniel Hahler | ||
| - George Yoshida | ||
| - Horst Gutmann | ||
| - Jannis Leidel | ||
| - Kuba Janoszek | ||
| - Nicolas Delaby |
| Changelog | ||
| --------- | ||
| 0.7 (08/10/2014) | ||
| ^^^^^^^^^^^^^^^^ | ||
| * Use `exec` (`os.execvpe`) to replace the envdir process with the child | ||
| process (fixes #20). | ||
| * Change `isenvvar()` to only check for `=` in var names. | ||
| 0.6.1 (12/23/2013) | ||
@@ -5,0 +13,0 @@ ^^^^^^^^^^^^^^^^^^ |
+1
-1
@@ -55,3 +55,3 @@ # -*- coding: utf-8 -*- | ||
| # The short X.Y version. | ||
| version = '0.6.1' | ||
| version = '0.6' | ||
| # The full version, including alpha/beta/rc tags. | ||
@@ -58,0 +58,0 @@ release = '0.6.1' |
+20
-10
@@ -39,16 +39,22 @@ Installation | ||
| Next step is downloading the actual standalone script. On Windows this entails | ||
| using your web browser to download the following URL:: | ||
| using your web browser to download the following URL: | ||
| https://github.com/jezdez/envdir/releases/download/0.6/envdir-0.6.pyz | ||
| .. parsed-literal:: | ||
| \https://github.com/jezdez/envdir/releases/download/|release|/envdir-|release|.pyz | ||
| Or simply run this on the command line to trigger the download with your | ||
| default web browser:: | ||
| default web browser: | ||
| C:\Windows\Explorer.exe https://github.com/jezdez/envdir/releases/download/0.6/envdir-0.6.pyz | ||
| .. parsed-literal:: | ||
| C:\\Windows\Explorer.exe \https://github.com/jezdez/envdir/releases/download/|release|/envdir-|release|.pyz | ||
| Then -- from the location you downloaded the file to -- run the envdir script | ||
| like you would any other script:: | ||
| like you would any other script: | ||
| C:\Users\jezdez\Desktop>.\envdir-0.6.pyz .. | ||
| .. parsed-literal:: | ||
| C:\\Users\\jezdez\\Desktop>.\\envdir-|release|.pyz .. | ||
| Linux, Mac OS, others | ||
@@ -58,11 +64,15 @@ ^^^^^^^^^^^^^^^^^^^^^ | ||
| On Linux, Mac OS and other platforms with a shell like bash simply download | ||
| the standalone file from Github:: | ||
| the standalone file from Github: | ||
| $ curl -O https://github.com/jezdez/envdir/releases/download/0.6/envdir-0.6.pyz | ||
| .. parsed-literal:: | ||
| $ curl -LO \https://github.com/jezdez/envdir/releases/download/|release|/envdir-|release|.pyz | ||
| and then run the file like you would do when running the script installed by | ||
| the envdir package (see above):: | ||
| the envdir package (see above): | ||
| $ ./envdir-0.6.pyz .. | ||
| .. parsed-literal:: | ||
| $ ./envdir-|release|.pyz .. | ||
| .. _`PEP 441`: http://www.python.org/dev/peps/pep-0441/ | ||
@@ -69,0 +79,0 @@ .. _`PEP 397`: http://www.python.org/dev/peps/pep-0397/ |
| Metadata-Version: 1.1 | ||
| Name: envdir | ||
| Version: 0.6.1 | ||
| Version: 0.7 | ||
| Summary: A Python port of daemontools' envdir. | ||
@@ -86,2 +86,10 @@ Home-page: http://envdir.readthedocs.org/ | ||
| 0.7 (08/10/2014) | ||
| ^^^^^^^^^^^^^^^^ | ||
| * Use `exec` (`os.execvpe`) to replace the envdir process with the child | ||
| process (fixes #20). | ||
| * Change `isenvvar()` to only check for `=` in var names. | ||
| 0.6.1 (12/23/2013) | ||
@@ -88,0 +96,0 @@ ^^^^^^^^^^^^^^^^^^ |
@@ -7,3 +7,2 @@ AUTHORS.rst | ||
| tox.ini | ||
| docs/.DS_Store | ||
| docs/Makefile | ||
@@ -17,3 +16,2 @@ docs/api.rst | ||
| docs/usage.rst | ||
| docs/_build/.DS_Store | ||
| envdir/__init__.py | ||
@@ -20,0 +18,0 @@ envdir/__main__.py |
+1
-3
@@ -12,5 +12,3 @@ import glob | ||
| root, name = os.path.split(name) | ||
| return (name == name.upper() and | ||
| not name.startswith('_') and | ||
| not '=' in name) | ||
| return '=' not in name | ||
@@ -17,0 +15,0 @@ |
+12
-48
| import optparse | ||
| import os | ||
| import signal | ||
| import subprocess | ||
@@ -10,11 +9,3 @@ import sys | ||
| # must have shell = True on Windows | ||
| is_windows = sys.platform == 'win32' | ||
| if is_windows: | ||
| params = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP} | ||
| else: | ||
| params = {'preexec_fn': os.setsid} | ||
| class Response(Exception): | ||
@@ -34,3 +25,2 @@ def __init__(self, message='', status=0): | ||
| self.parser.prog = 'envdir' | ||
| signal.signal(signal.SIGTERM, self.terminate) | ||
@@ -50,3 +40,6 @@ def path(self, path): | ||
| frame = sys._getframe() | ||
| get_parent = lambda frame: frame.f_back | ||
| def get_parent(frame): | ||
| return frame.f_back | ||
| for _ in range(stacklevel): | ||
@@ -68,3 +61,3 @@ frame = get_parent(frame) | ||
| if len(args) == 0: | ||
| raise Response("%s\nError: incorrect number of arguments" % | ||
| raise Response("%s\nError: incorrect number of arguments\n" % | ||
| (self.parser.get_usage()), 2) | ||
@@ -81,10 +74,7 @@ | ||
| try: | ||
| subprocess.check_call([shell], | ||
| universal_newlines=True, | ||
| bufsize=0, | ||
| close_fds=not is_windows, | ||
| **params) | ||
| subprocess.call([shell]) | ||
| except OSError as err: | ||
| if err.errno == 2: | ||
| raise Response("Unable to find shell %s" % shell, err.errno) | ||
| raise Response("Unable to find shell %s" % shell, | ||
| status=err.errno) | ||
| else: | ||
@@ -115,33 +105,7 @@ raise Response("An error occurred: %s" % err, | ||
| try: | ||
| self.process = subprocess.Popen(args, | ||
| universal_newlines=True, | ||
| bufsize=0, | ||
| close_fds=not is_windows, | ||
| **params) | ||
| self.process.wait() | ||
| os.execvpe(args[0], args, os.environ) | ||
| except OSError as err: | ||
| if err.errno == 2: | ||
| raise Response("Unable to find command %s" % | ||
| args[0], err.errno) | ||
| else: | ||
| raise Response(status=err.errno) | ||
| except KeyboardInterrupt: | ||
| self.terminate() | ||
| raise Response(status=self.process.returncode) | ||
| raise Response("Unable to run command %s: %s" % | ||
| (args[0], err), status=err.errno) | ||
| def terminate(self, *args, **kwargs): | ||
| # first send mellow signal | ||
| self.quit(signal.SIGTERM) | ||
| if self.process.poll() is None: | ||
| # still running, kill it | ||
| self.quit(signal.SIGKILL) | ||
| def quit(self, signal): | ||
| if self.process.poll() is None: | ||
| proc_pgid = os.getpgid(self.process.pid) | ||
| if os.getpgrp() == proc_pgid: | ||
| # Just kill the proc, don't kill ourselves too | ||
| os.kill(self.process.pid, signal) | ||
| else: | ||
| # Kill the whole process group | ||
| os.killpg(proc_pgid, signal) | ||
| raise Response() |
+100
-21
@@ -0,3 +1,7 @@ | ||
| import functools | ||
| import os | ||
| import signal | ||
| import subprocess | ||
| import threading | ||
| import py | ||
@@ -8,3 +12,2 @@ import pytest | ||
| from envdir.runner import Response | ||
| from envdir.__main__ import go | ||
@@ -30,3 +33,30 @@ | ||
| original_execvpe = os.execvpe | ||
| def mocked_execvpe(monkeypatch, name, args, env, with_timeout=None, | ||
| signal_type=signal.SIGINT): | ||
| monkeypatch.setattr('os.execvpe', original_execvpe) | ||
| try: | ||
| process = subprocess.Popen(args, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| env=os.environ.copy()) | ||
| if with_timeout: | ||
| def killer(pid): | ||
| os.kill(pid, signal_type) | ||
| timer = threading.Timer(with_timeout, | ||
| functools.partial(killer, process.pid)) | ||
| timer.start() | ||
| stdout, stderr = process.communicate() | ||
| if process.returncode != 0: | ||
| raise OSError(process.returncode, stderr) | ||
| finally: | ||
| monkeypatch.setattr('os.execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| def test_usage(run): | ||
@@ -40,4 +70,6 @@ "Testing the usage" | ||
| def test_default(run, tmpenvdir): | ||
| def test_default(run, tmpenvdir, monkeypatch): | ||
| "Default cases." | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('DEFAULT').write('test') | ||
@@ -65,4 +97,6 @@ with py.test.raises(Response) as response: | ||
| def test_reset(run, tmpenvdir): | ||
| def test_reset(run, tmpenvdir, monkeypatch): | ||
| "Resetting an env var with an empty file" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('RESET').write('') | ||
@@ -77,4 +111,6 @@ os.environ['RESET'] = 'test3' | ||
| def test_multiline(run, tmpenvdir): | ||
| def test_multiline(run, tmpenvdir, monkeypatch): | ||
| "Multiline envdir file" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('MULTI_LINE').write("""multi | ||
@@ -88,4 +124,32 @@ line | ||
| def test_translate_nulls(run, tmpenvdir): | ||
| def test_lowercase_var_names(run, tmpenvdir, monkeypatch): | ||
| "Lowercase env var name" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('lowercase-variable').write("test") | ||
| with py.test.raises(Response) as response: | ||
| run('envdir', str(tmpenvdir), 'ls') | ||
| assert 'lowercase-variable' in os.environ | ||
| assert os.environ['lowercase-variable'] == 'test' | ||
| assert response.value.status == 0 | ||
| assert response.value.message == '' | ||
| def test_var_names_prefixed_by_underscore(run, tmpenvdir, monkeypatch): | ||
| "Underscore prefixed env var name" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('_UNDERSCORE_VAR').write("test") | ||
| with py.test.raises(Response) as response: | ||
| run('envdir', str(tmpenvdir), 'ls') | ||
| assert '_UNDERSCORE_VAR' in os.environ | ||
| assert os.environ['_UNDERSCORE_VAR'] == 'test' | ||
| assert response.value.status == 0 | ||
| assert response.value.message == '' | ||
| def test_translate_nulls(run, tmpenvdir, monkeypatch): | ||
| "NULLs are translated into newline" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('NULL_CHARS').write("""null\x00character""") | ||
@@ -97,4 +161,6 @@ with py.test.raises(Response): | ||
| def test_incorrect_no_args(run, tmpenvdir): | ||
| def test_incorrect_no_args(run, tmpenvdir, monkeypatch): | ||
| "Incorrect number of arguments" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| with py.test.raises(Response) as response: | ||
@@ -106,3 +172,5 @@ run('envdir', str(tmpenvdir)) | ||
| def test_doesnt_exist(run, tmpdir): | ||
| def test_doesnt_exist(run, tmpdir, monkeypatch): | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| with py.test.raises(Response) as response: | ||
@@ -115,8 +183,13 @@ run('envdir', str(tmpdir.join('missing')), 'ls') | ||
| run('envdir', str(tmpdir), 'doesnt-exist') | ||
| assert 'Unable to find command' in response.value.message | ||
| result = ('Unable to run command' in response.value.message or | ||
| 'Unable to find command' in response.value.message) | ||
| assert result | ||
| assert 2 == response.value.status | ||
| def test_must_be_directory(run, tmpdir): | ||
| def test_must_be_directory(run, tmpdir, monkeypatch): | ||
| "The envdir must be a directory" | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpdir.join('not-a-directory').write('') | ||
@@ -129,3 +202,5 @@ with py.test.raises(Response) as response: | ||
| def test_error_code(run, tmpenvdir): | ||
| def test_error_code(run, tmpenvdir, monkeypatch): | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| with py.test.raises(Response) as response: | ||
@@ -137,3 +212,5 @@ run('envdir', str(tmpenvdir), | ||
| def test_equal_sign(run, tmpenvdir): | ||
| def test_equal_sign(run, tmpenvdir, monkeypatch): | ||
| monkeypatch.setattr(os, 'execvpe', functools.partial(mocked_execvpe, | ||
| monkeypatch)) | ||
| tmpenvdir.join('EQUAL_SIGN=').write('test') | ||
@@ -148,12 +225,14 @@ with py.test.raises(Response): | ||
| @py.test.mark.skipif(timeout is None, | ||
| reason="(g)timeout command not found") | ||
| def test_keyboard_interrupt(run, tmpenvdir): | ||
| with py.test.raises(SystemExit) as exit: | ||
| go(run, (str(timeout), '--signal=SIGTERM', '--', '1', 'envdir', | ||
| str(tmpenvdir), 'ls')) | ||
| if py.std.sys.version_info[:2] == (2, 6): | ||
| assert exit.value == 2 | ||
| else: | ||
| assert exit.value.code == 2 | ||
| def test_keyboard_interrupt(run, tmpenvdir, monkeypatch): | ||
| monkeypatch.setattr(os, 'execvpe', | ||
| functools.partial(mocked_execvpe, | ||
| monkeypatch, | ||
| with_timeout=.0000001)) | ||
| with py.test.raises(Response) as response: | ||
| run('envdir', str(tmpenvdir), 'sleep', '1') | ||
| # Minus sign is added by subprocess to distinguish signals from exit codes. | ||
| # Since we send a signal within the test to stop the process, it is the | ||
| # intended behaviour. | ||
| # signal.SIGINT is equivalent to KeyboardInterrupt on POSIX. | ||
| assert response.value.status == -signal.SIGINT | ||
@@ -160,0 +239,0 @@ |
@@ -1,1 +0,1 @@ | ||
| __version__ = '0.6.1' # noqa | ||
| __version__ = '0.7' # noqa |
+9
-1
| Metadata-Version: 1.1 | ||
| Name: envdir | ||
| Version: 0.6.1 | ||
| Version: 0.7 | ||
| Summary: A Python port of daemontools' envdir. | ||
@@ -86,2 +86,10 @@ Home-page: http://envdir.readthedocs.org/ | ||
| 0.7 (08/10/2014) | ||
| ^^^^^^^^^^^^^^^^ | ||
| * Use `exec` (`os.execvpe`) to replace the envdir process with the child | ||
| process (fixes #20). | ||
| * Change `isenvvar()` to only check for `=` in var names. | ||
| 0.6.1 (12/23/2013) | ||
@@ -88,0 +96,0 @@ ^^^^^^^^^^^^^^^^^^ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
766
4.08%71845
-11.28%27
-6.9%