summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Carrillo <ben@futeisha.org>2013-02-06 00:45:08 +0900
committerBen Carrillo <ben@futeisha.org>2013-02-06 00:45:08 +0900
commit552424322df98b71d25f5f12f87bd53eb93743d8 (patch)
treed3e32ff56f092a3519990b9ecb7e54b76374bedc
parent81f0b94ba74a6cc657743e8e3aa8478350da1979 (diff)
new upstream release (1.08)
-rw-r--r--PKG-INFO2
-rw-r--r--debian/changelog7
-rw-r--r--sh.py175
-rw-r--r--test.py94
4 files changed, 225 insertions, 53 deletions
diff --git a/PKG-INFO b/PKG-INFO
index ed247b3..cc7ba89 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.0
Name: sh
-Version: 1.07
+Version: 1.08
Summary: Python subprocess interface
Home-page: https://github.com/amoffat/sh
Author: Andrew Moffat
diff --git a/debian/changelog b/debian/changelog
index 388b282..3746f73 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-sh (1.08-1) unstable; urgency=low
+
+ * New upstream release
+ * Fix lintian errors
+
+ -- Ben Carrillo <ben@futeisha.org> Wed, 06 Feb 2013 00:17:32 +0900
+
python-sh (1.07-1) unstable; urgency=low
* Initial release. Closes: 699123
diff --git a/sh.py b/sh.py
index 54bf92d..0e46f14 100644
--- a/sh.py
+++ b/sh.py
@@ -21,7 +21,7 @@
#===============================================================================
-__version__ = "1.07"
+__version__ = "1.08"
__project_url__ = "https://github.com/amoffat/sh"
@@ -120,6 +120,17 @@ class ErrorReturnCode(Exception):
msg = "\n\n RAN: %r\n\n STDOUT:\n%s\n\n STDERR:\n%s" %\
(full_cmd, tstdout.decode(DEFAULT_ENCODING), tstderr.decode(DEFAULT_ENCODING))
super(ErrorReturnCode, self).__init__(msg)
+
+
+class SignalException(ErrorReturnCode): pass
+
+SIGNALS_THAT_SHOULD_THROW_EXCEPTION = (
+ signal.SIGKILL,
+ signal.SIGSEGV,
+ signal.SIGTERM,
+ signal.SIGINT,
+ signal.SIGQUIT
+)
# we subclass AttributeError because:
@@ -127,7 +138,7 @@ class ErrorReturnCode(Exception):
# https://github.com/amoffat/sh/issues/97#issuecomment-10610629
class CommandNotFound(AttributeError): pass
-rc_exc_regex = re.compile("ErrorReturnCode_(\d+)")
+rc_exc_regex = re.compile("(ErrorReturnCode|SignalException)_(\d+)")
rc_exc_cache = {}
def get_rc_exc(rc):
@@ -135,8 +146,13 @@ def get_rc_exc(rc):
try: return rc_exc_cache[rc]
except KeyError: pass
- name = "ErrorReturnCode_%d" % rc
- exc = type(name, (ErrorReturnCode,), {})
+ if rc > 0:
+ name = "ErrorReturnCode_%d" % rc
+ exc = type(name, (ErrorReturnCode,), {})
+ else:
+ name = "SignalException_%d" % abs(rc)
+ exc = type(name, (SignalException,), {})
+
rc_exc_cache[rc] = exc
return exc
@@ -222,6 +238,13 @@ class RunningCommand(object):
self.cmd = cmd
self.ran = " ".join(cmd)
self.process = None
+
+ # this flag is for whether or not we've handled the exit code (like
+ # by raising an exception). this is necessary because .wait() is called
+ # from multiple places, and wait() triggers the exit code to be
+ # processed. but we don't want to raise multiple exceptions, only
+ # one (if any at all)
+ self._handled_exit_code = False
self.should_wait = True
spawn_process = True
@@ -275,11 +298,18 @@ class RunningCommand(object):
# here we determine if we had an exception, or an error code that we weren't
# expecting to see. if we did, we create and raise an exception
def _handle_exit_code(self, code):
- if code not in self.call_args["ok_code"] and code >= 0: raise get_rc_exc(code)(
- " ".join(self.cmd),
- self.process.stdout,
- self.process.stderr
- )
+ if self._handled_exit_code: return
+ self._handled_exit_code = True
+
+ if code not in self.call_args["ok_code"] and \
+ (code > 0 or -code in SIGNALS_THAT_SHOULD_THROW_EXCEPTION):
+ raise get_rc_exc(code)(
+ " ".join(self.cmd),
+ self.process.stdout,
+ self.process.stderr
+ )
+
+
@property
def stdout(self):
@@ -416,6 +446,7 @@ class Command(object):
"iter_noblock": None,
"ok_code": 0,
"cwd": None,
+ "long_sep": "=",
# this is for programs that expect their input to be from a terminal.
# ssh is one of those programs
@@ -451,24 +482,48 @@ class Command(object):
("piped", "iter", "You cannot iterate when this command is being piped"),
)
+
+ # this method exists because of the need to have some way of letting
+ # manual object instantiation not perform the underscore-to-dash command
+ # conversion that resolve_program uses.
+ #
+ # there are 2 ways to create a Command object. using sh.Command(<program>)
+ # or by using sh.<program>. the method fed into sh.Command must be taken
+ # literally, and so no underscore-dash conversion is performed. the one
+ # for sh.<program> must do the underscore-dash converesion, because we
+ # can't type dashes in method names
@classmethod
- def _create(cls, program):
+ def _create(cls, program, **default_kwargs):
path = resolve_program(program)
if not path: raise CommandNotFound(program)
- return cls(path)
-
+
+ cmd = cls(path)
+ if default_kwargs: cmd = cmd.bake(**default_kwargs)
+
+ return cmd
+
+
def __init__(self, path):
- self._path = which(path)
+ path = which(path)
+ if not path: raise CommandNotFound(path)
+ self._path = path
+
self._partial = False
self._partial_baked_args = []
self._partial_call_args = {}
+ # bugfix for functools.wraps. issue #121
+ self.__name__ = repr(self)
+
+
def __getattribute__(self, name):
# convenience
getattr = partial(object.__getattribute__, self)
+
+ if name.startswith("_"): return getattr(name)
+ if name == "bake": return getattr("bake")
+ if name.endswith("_"): name = name[:-1]
- if name.startswith("_"): return getattr(name)
- if name == "bake": return getattr("bake")
return getattr("bake")(name)
@@ -497,12 +552,44 @@ class Command(object):
return call_args, kwargs
+ # this helper method is for normalizing an argument into a string in the
+ # system's default encoding. we can feed it a number or a string or
+ # whatever
def _format_arg(self, arg):
if IS_PY3: arg = str(arg)
- else: arg = unicode(arg).encode(DEFAULT_ENCODING)
+ else:
+ # if the argument is already unicode, or a number or whatever,
+ # this first call will fail.
+ try: arg = unicode(arg, DEFAULT_ENCODING).encode(DEFAULT_ENCODING)
+ except TypeError: arg = unicode(arg).encode(DEFAULT_ENCODING)
return arg
- def _compile_args(self, args, kwargs):
+
+ def _aggregate_keywords(self, keywords, sep, raw=False):
+ processed = []
+ for k, v in keywords.items():
+ # we're passing a short arg as a kwarg, example:
+ # cut(d="\t")
+ if len(k) == 1:
+ if v is not False:
+ processed.append("-" + k)
+ if v is not True:
+ processed.append(self._format_arg(v))
+
+ # we're doing a long arg
+ else:
+ if not raw: k = k.replace("_", "-")
+
+ if v is True:
+ processed.append("--" + k)
+ elif v is False:
+ pass
+ else:
+ processed.append("--%s%s%s" % (k, sep, self._format_arg(v)))
+ return processed
+
+
+ def _compile_args(self, args, kwargs, sep):
processed_args = []
# aggregate positional args
@@ -512,28 +599,18 @@ class Command(object):
warnings.warn("Empty list passed as an argument to %r. \
If you're using glob.glob(), please use sh.glob() instead." % self.path, stacklevel=3)
for sub_arg in arg: processed_args.append(self._format_arg(sub_arg))
- else: processed_args.append(self._format_arg(arg))
-
+ elif isinstance(arg, dict):
+ processed_args += self._aggregate_keywords(arg, sep, raw=True)
+ else:
+ processed_args.append(self._format_arg(arg))
+
# aggregate the keyword arguments
- for k,v in kwargs.items():
- # we're passing a short arg as a kwarg, example:
- # cut(d="\t")
- if len(k) == 1:
- if v is not False:
- processed_args.append("-"+k)
- if v is not True: processed_args.append(self._format_arg(v))
-
- # we're doing a long arg
- else:
- k = k.replace("_", "-")
-
- if v is True: processed_args.append("--"+k)
- elif v is False: pass
- else: processed_args.append("--%s=%s" % (k, self._format_arg(v)))
+ processed_args += self._aggregate_keywords(kwargs, sep)
return processed_args
+ # TODO needs documentation
def bake(self, *args, **kwargs):
fn = Command(self._path)
fn._partial = True
@@ -550,7 +627,8 @@ If you're using glob.glob(), please use sh.glob() instead." % self.path, stackle
fn._partial_call_args.update(self._partial_call_args)
fn._partial_call_args.update(pruned_call_args)
fn._partial_baked_args.extend(self._partial_baked_args)
- fn._partial_baked_args.extend(self._compile_args(args, kwargs))
+ sep = pruned_call_args.get("long_sep", self._call_args["long_sep"])
+ fn._partial_baked_args.extend(self._compile_args(args, kwargs, sep))
return fn
def __str__(self):
@@ -562,7 +640,7 @@ If you're using glob.glob(), please use sh.glob() instead." % self.path, stackle
except: return False
def __repr__(self):
- return str(self)
+ return "<Command %r>" % str(self)
def __unicode__(self):
baked_args = " ".join(self._partial_baked_args)
@@ -618,7 +696,7 @@ If you're using glob.glob(), please use sh.glob() instead." % self.path, stackle
else:
args.insert(0, first_arg)
- processed_args = self._compile_args(args, kwargs)
+ processed_args = self._compile_args(args, kwargs, call_args["long_sep"])
# makes sure our arguments are broken up correctly
split_args = self._partial_baked_args + processed_args
@@ -1455,9 +1533,10 @@ class StreamBufferer(object):
# the exec() statement used in this file requires the "globals" argument to
# be a dictionary
class Environment(dict):
- def __init__(self, globs):
+ def __init__(self, globs, baked_args={}):
self.globs = globs
-
+ self.baked_args = baked_args
+
def __setitem__(self, k, v):
self.globs[k] = v
@@ -1483,7 +1562,10 @@ Please import sh or import programs individually.")
try: return rc_exc_cache[k]
except KeyError:
m = rc_exc_regex.match(k)
- if m: return get_rc_exc(int(m.group(1)))
+ if m:
+ exit_code = int(m.group(2))
+ if m.group(1) == "SignalException": exit_code = -exit_code
+ return get_rc_exc(exit_code)
# is it a builtin?
try: return getattr(self["__builtins__"], k)
@@ -1505,7 +1587,10 @@ Please import sh or import programs individually.")
if builtin: return builtin
# it must be a command then
- return Command._create(k)
+ # we use _create instead of instantiating the class directly because
+ # _create uses resolve_program, which will automatically do underscore-
+ # to-dash conversions. instantiating directly does not use that
+ return Command._create(k, **self.baked_args)
# methods that begin with "b_" are custom builtins and will override any
@@ -1546,7 +1631,7 @@ def run_repl(env):
# system PATH worth of commands. in this case, we just proxy the
# import lookup to our Environment class
class SelfWrapper(ModuleType):
- def __init__(self, self_module):
+ def __init__(self, self_module, baked_args={}):
# this is super ugly to have to copy attributes like this,
# but it seems to be the only way to make reload() behave
# nicely. if i make these attributes dynamic lookups in
@@ -1558,7 +1643,7 @@ class SelfWrapper(ModuleType):
# if we set this to None. and 3.3 needs a value for __path__
self.__path__ = []
self.self_module = self_module
- self.env = Environment(globals())
+ self.env = Environment(globals(), baked_args)
def __setattr__(self, name, value):
if hasattr(self, "env"): self.env[name] = value
@@ -1568,6 +1653,10 @@ class SelfWrapper(ModuleType):
if name == "env": raise AttributeError
return self.env[name]
+ # accept special keywords argument to define defaults for all operations
+ # that will be processed with given by return SelfWrapper
+ def __call__(self, **kwargs):
+ return SelfWrapper(self.self_module, kwargs)
diff --git a/test.py b/test.py
index efa84d4..54b62dd 100644
--- a/test.py
+++ b/test.py
@@ -176,8 +176,7 @@ for l in "andrew":
out = tr("[:lower:]", "[:upper:]", _in="andrew").strip()
self.assertEqual(out, "ANDREW")
-
-
+
def test_manual_stdin_iterable(self):
from sh import tr
@@ -292,6 +291,11 @@ print(sh.HERP + " " + str(len(os.environ)))
import sh
sh.awoefaowejfw
self.assertRaises(CommandNotFound, do_import)
+
+ def do_import():
+ import sh
+ sh.Command("ofajweofjawoe")
+ self.assertRaises(CommandNotFound, do_import)
def test_command_wrapper_equivalence(self):
@@ -387,8 +391,38 @@ print(options.long_option.upper())
""")
self.assertTrue(python(py.name, long_option="testing").strip() == "TESTING")
self.assertTrue(python(py.name).strip() == "")
+
+ def test_raw_args(self):
+ py = create_tmp_test("""
+from optparse import OptionParser
+parser = OptionParser()
+parser.add_option("--long_option", action="store", default=None,
+ dest="long_option1")
+parser.add_option("--long-option", action="store", default=None,
+ dest="long_option2")
+options, args = parser.parse_args()
+
+if options.long_option1:
+ print(options.long_option1.upper())
+else:
+ print(options.long_option2.upper())
+""")
+ self.assertEqual(python(py.name,
+ {"long_option": "underscore"}).strip(), "UNDERSCORE")
+
+ self.assertEqual(python(py.name, long_option="hyphen").strip(), "HYPHEN")
+
+ def test_custom_separator(self):
+ py = create_tmp_test("""
+import sys
+print(sys.argv[1])
+""")
+ self.assertEqual(python(py.name,
+ {"long-option": "underscore"}, _long_sep="=custom=").strip(), "--long-option=custom=underscore")
+ # test baking too
+ python_baked = python.bake(py.name, {"long-option": "underscore"}, _long_sep="=baked=")
+ self.assertEqual(python_baked().strip(), "--long-option=baked=underscore")
-
def test_command_wrapper(self):
from sh import Command, which
@@ -791,8 +825,11 @@ for i in range(5):
process.terminate()
return True
- p = python(py.name, _out=agg, u=True)
- p.wait()
+ try:
+ p = python(py.name, _out=agg, u=True)
+ p.wait()
+ except sh.SignalException_15:
+ pass
self.assertEqual(p.process.exit_code, -signal.SIGTERM)
self.assertTrue("4" not in p)
@@ -802,6 +839,7 @@ for i in range(5):
def test_stdout_callback_kill(self):
import signal
+ import sh
py = create_tmp_test("""
import sys
@@ -821,8 +859,11 @@ for i in range(5):
process.kill()
return True
- p = python(py.name, _out=agg, u=True)
- p.wait()
+ try:
+ p = python(py.name, _out=agg, u=True)
+ p.wait()
+ except sh.SignalException_9:
+ pass
self.assertEqual(p.process.exit_code, -signal.SIGKILL)
self.assertTrue("4" not in p)
@@ -923,7 +964,7 @@ import os
import time
for letter in "andrew":
- time.sleep(0.5)
+ time.sleep(0.6)
print(letter)
""")
@@ -1155,7 +1196,8 @@ sys.stdin.read(1)
sleep_for = 3
timeout = 1
started = time()
- sh.sleep(sleep_for, _timeout=timeout).wait()
+ try: sh.sleep(sleep_for, _timeout=timeout).wait()
+ except sh.SignalException_9: pass
elapsed = time() - started
self.assertTrue(abs(elapsed - timeout) < 0.1)
@@ -1302,7 +1344,41 @@ sys.stdout.write("te漢字st")
p = python(py.name, _encoding="ascii", _decode_errors="ignore")
self.assertEqual(p, "test")
+
+
+ def test_shared_secial_args(self):
+ import sh
+
+ if IS_PY3:
+ from io import StringIO
+ from io import BytesIO as cStringIO
+ else:
+ from StringIO import StringIO
+ from cStringIO import StringIO as cStringIO
+
+ out1 = sh.ls('.')
+ out2 = StringIO()
+ sh_new = sh(_out=out2)
+ sh_new.ls('.')
+ self.assertEqual(out1, out2.getvalue())
+ out2.close()
+
+
+ def test_signal_exception(self):
+ from sh import SignalException, get_rc_exc
+ def throw_terminate_signal():
+ py = create_tmp_test("""
+import time
+while True: time.sleep(1)
+""")
+ to_kill = python(py.name, _bg=True)
+ to_kill.terminate()
+ to_kill.wait()
+
+ self.assertRaises(get_rc_exc(-15), throw_terminate_signal)
+
+
if __name__ == "__main__":
if len(sys.argv) > 1: