diff -Nru python-plumbum-1.5.0/debian/changelog python-plumbum-1.6.0/debian/changelog --- python-plumbum-1.5.0/debian/changelog 2015-08-05 11:34:37.000000000 +0000 +++ python-plumbum-1.6.0/debian/changelog 2015-10-31 14:12:17.000000000 +0000 @@ -1,3 +1,9 @@ +python-plumbum (1.6.0-1) unstable; urgency=low + + * Imported Upstream version 1.6.0 + + -- Philipp Huebner Sat, 31 Oct 2015 15:11:59 +0100 + python-plumbum (1.5.0-1) unstable; urgency=low * Imported Upstream version 1.5.0 diff -Nru python-plumbum-1.5.0/PKG-INFO python-plumbum-1.6.0/PKG-INFO --- python-plumbum-1.5.0/PKG-INFO 2015-07-17 07:24:05.000000000 +0000 +++ python-plumbum-1.6.0/PKG-INFO 2015-10-16 16:54:58.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: plumbum -Version: 1.5.0 +Version: 1.6.0 Summary: Plumbum: shell combinators library Home-page: http://plumbum.readthedocs.org Author: Tomer Filiba @@ -29,7 +29,11 @@ Cheat Sheet ----------- - **Basics** :: + + Basics + ****** + + .. code-block:: python >>> from plumbum import local >>> ls = local["ls"] @@ -42,13 +46,18 @@ u'' # Notepad window is closed by user, command returns Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can - also ``import`` commands:: + also ``import`` commands + + .. code-block:: python >>> from plumbum.cmd import grep, wc, cat, head >>> grep LocalCommand() - **Piping** :: + Piping + ****** + + .. code-block:: python >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> print chain @@ -56,7 +65,10 @@ >>> chain() u'13\n' - **Redirection** :: + Redirection + *********** + + .. code-block:: python >>> ((cat < "setup.py") | head["-n", 4])() u'#!/usr/bin/env python\nimport os\n\ntry:\n' @@ -65,16 +77,22 @@ >>> (cat["file.list"] | wc["-l"])() u'17\n' - **Working-directory manipulation** :: + Working-directory manipulation + ****************************** + + .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() - ... + ... u'15\n' - - **Foreground and background execution** :: + + Foreground and background execution + *********************************** + + .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep["\\.py"]) & FG # The output is printed to stdout directly @@ -83,60 +101,86 @@ setup.py >>> (ls["-a"] | grep["\\.py"]) & BG # The process runs "in the background" - - **Command nesting** :: + + Command nesting + *************** + + .. code-block:: python >>> from plumbum.cmd import sudo >>> print sudo[ifconfig["-a"]] /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG - lo Link encap:Local Loopback + lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 - **Remote commands (over SSH)** + Remote commands (over SSH) + ************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) - and `Paramiko `_ (a pure-Python implementation of SSH2) :: + and `Paramiko `_ (a pure-Python implementation of SSH2) + + .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() - ... + ... u'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' - **CLI applications** :: + CLI applications + **************** + + .. code-block:: python import logging from plumbum import cli - + class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") - + @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) - + def main(self, *srcfiles): print "Verbose:", self.verbose - print "Include dirs:", self.include_dirs + print "Include dirs:", self.include_dirs print "Compiling:", srcfiles - - + if __name__ == "__main__": MyCompiler.run() - Sample output:: + Sample output + +++++++++++++ + + :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') + Colors and Styles + ----------------- + + .. code-block:: python + + from plumbum import colors + with colors.red: + print("This library provides safe, flexible color access.") + print(colors.bold | "(and styles in general)", "are easy!") + print("The simple 16 colors or", + colors.orchid & colors.underline | '256 named colors,', + colors.rgb(18, 146, 64) | "or full rgb colors", + 'can be used.') + print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") + .. image:: https://d2weczhvl823v0.cloudfront.net/tomerfiliba/plumbum/trend.png @@ -144,7 +188,7 @@ :target: https://bitdeli.com/free -Keywords: path,local,remote,ssh,shell,pipe,popen,process,execution +Keywords: path,local,remote,ssh,shell,pipe,popen,process,execution,color,cli Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable @@ -157,6 +201,7 @@ Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum diff -Nru python-plumbum-1.5.0/plumbum/cli/application.py python-plumbum-1.6.0/plumbum/cli/application.py --- python-plumbum-1.5.0/plumbum/cli/application.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/application.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,13 +1,16 @@ +from __future__ import division, print_function, absolute_import import os import sys -import inspect import functools -from plumbum.lib import six from textwrap import TextWrapper +from collections import defaultdict + +from plumbum.lib import six, getdoc from plumbum.cli.terminal import get_terminal_size from plumbum.cli.switches import (SwitchError, UnknownSwitch, MissingArgument, WrongArgumentType, MissingMandatorySwitch, SwitchCombinationError, PositionalArgumentsError, switch, SubcommandError, Flag, CountOf) +from plumbum import colors, local class ShowHelp(SwitchError): @@ -80,15 +83,20 @@ There are several class-level attributes you may set: * ``PROGNAME`` - the name of the program; if ``None`` (the default), it is set to the - name of the executable (``argv[0]``) + name of the executable (``argv[0]``), can be in color. If only a color, will be applied to the name. - * ``VERSION`` - the program's version (defaults to ``1.0``) + * ``VERSION`` - the program's version (defaults to ``1.0``, can be in color) * ``DESCRIPTION`` - a short description of your program (shown in help). If not set, - the class' ``__doc__`` will be used. + the class' ``__doc__`` will be used. Can be in color. * ``USAGE`` - the usage line (shown in help) + * ``COLOR_USAGE`` - The color of the usage line + + * ``COLOR_GROUPS`` - A dictionary that sets colors for the groups, like Meta-switches, Switches, + and Subcommands + A note on sub-commands: when an application is the root, its ``parent`` attribute is set to ``None``. When it is used as a nested-command, ``parent`` will point to be its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute @@ -100,6 +108,8 @@ DESCRIPTION = None VERSION = None USAGE = None + COLOR_USAGE = None + COLOR_GROUPS = None CALL_MAIN_IF_NESTED_COMMAND = True parent = None @@ -107,23 +117,36 @@ _unbound_switches = () def __init__(self, executable): + # Filter colors + if self.PROGNAME is None: self.PROGNAME = os.path.basename(executable) + elif isinstance(self.PROGNAME, colors._style): + self.PROGNAME = self.PROGNAME | os.path.basename(executable) + elif colors.filter(self.PROGNAME) == '': + self.PROGNAME = colors.extract(self.PROGNAME) | os.path.basename(executable) if self.DESCRIPTION is None: - self.DESCRIPTION = inspect.getdoc(self) + self.DESCRIPTION = getdoc(self) + + # Allow None for the colors + self.COLOR_GROUPS=defaultdict(lambda:colors.do_nothing, dict() if type(self).COLOR_GROUPS is None else type(self).COLOR_GROUPS ) + if type(self).COLOR_USAGE is None: + self.COLOR_USAGE=colors.do_nothing self.executable = executable self._switches_by_name = {} self._switches_by_func = {} + self._switches_by_envar = {} self._subcommands = {} for cls in reversed(type(self).mro()): for obj in cls.__dict__.values(): if isinstance(obj, Subcommand): - if obj.name.startswith("-"): + name = colors.filter(obj.name) + if name.startswith("-"): raise SubcommandError("Subcommand names cannot start with '-'") # it's okay for child classes to override subcommands set by their parents - self._subcommands[obj.name] = obj + self._subcommands[name] = obj continue swinfo = getattr(obj, "_switch_info", None) @@ -136,6 +159,8 @@ raise SwitchError("Switch %r already defined and is not overridable" % (name,)) self._switches_by_name[name] = swinfo self._switches_by_func[swinfo.func] = swinfo + if swinfo.envname: + self._switches_by_envar[swinfo.envname] = swinfo @property def root_app(self): @@ -186,9 +211,11 @@ tailargs = [] swfuncs = {} index = 0 + while argv: index += 1 a = argv.pop(0) + val = None if a == "--": # end of options, treat the rest as tailargs tailargs.extend(argv) @@ -196,7 +223,7 @@ if a in self._subcommands: subcmd = self._subcommands[a].get() - self.nested_command = (subcmd, [self.PROGNAME + " " + a] + argv) + self.nested_command = (subcmd, [self.PROGNAME + " " + self._subcommands[a].name] + argv) break elif a.startswith("--") and len(a) >= 3: @@ -250,15 +277,7 @@ continue # handle argument - if swinfo.argtype: - try: - val = swinfo.argtype(val) - except (TypeError, ValueError): - ex = sys.exc_info()[1] # compat - raise WrongArgumentType("Argument of %s expected to be %r, not %r:\n %r" % ( - swname, swinfo.argtype, val, ex)) - else: - val = NotImplemented + val = self._handle_argument(val, swinfo.argtype, name) if swinfo.func in swfuncs: if swinfo.list: @@ -277,8 +296,47 @@ else: swfuncs[swinfo.func] = SwitchParseInfo(swname, (val,), index) + # Extracting arguments from environment variables + envindex = 0 + for env, swinfo in self._switches_by_envar.items(): + envindex -= 1 + envval = local.env.get(env) + if envval is None: + continue + + if swinfo.func in swfuncs: + continue # skip if overridden by command line arguments + + val = self._handle_argument(envval, swinfo.argtype, env) + envname = "$%s" % (env,) + if swinfo.list: + # multiple values over environment variables are not supported, + # this will require some sort of escaping and separator convention + swfuncs[swinfo.func] = SwitchParseInfo(envname, ([val],), envindex) + elif val is NotImplemented: + swfuncs[swinfo.func] = SwitchParseInfo(envname, (), envindex) + else: + swfuncs[swinfo.func] = SwitchParseInfo(envname, (val,), envindex) + return swfuncs, tailargs + @classmethod + def autocomplete(cls, argv): + """This is supplied to make subclassing and testing argument completion methods easier""" + pass + + @staticmethod + def _handle_argument(val, argtype, name): + if argtype: + try: + return argtype(val) + except (TypeError, ValueError): + ex = sys.exc_info()[1] # compat + raise WrongArgumentType("Argument of %s expected to be %r, not %r:\n %r" % ( + name, argtype, val, ex)) + else: + return NotImplemented + def _validate_args(self, swfuncs, tailargs): if six.get_method_function(self.help) in swfuncs: raise ShowHelp() @@ -309,20 +367,58 @@ raise SwitchCombinationError("Given %s, the following are invalid %r" % (swfuncs[func].swname, [swfuncs[f].swname for f in invalid])) - m_args, m_varargs, _, m_defaults = inspect.getargspec(self.main) - max_args = six.MAXSIZE if m_varargs else len(m_args) - 1 - min_args = len(m_args) - 1 - (len(m_defaults) if m_defaults else 0) + m = six.getfullargspec(self.main) + max_args = six.MAXSIZE if m.varargs else len(m.args) - 1 + min_args = len(m.args) - 1 - (len(m.defaults) if m.defaults else 0) if len(tailargs) < min_args: raise PositionalArgumentsError("Expected at least %d positional arguments, got %r" % (min_args, tailargs)) elif len(tailargs) > max_args: raise PositionalArgumentsError("Expected at most %d positional arguments, got %r" % - (max_args, tailargs)) + (max_args, tailargs)) + + # Positional arguement validataion + if hasattr(self.main, 'positional'): + tailargs = self._positional_validate(tailargs, self.main.positional, self.main.positional_varargs, m.args[1:], m.varargs) + + elif hasattr(m, 'annotations'): + args_names = list(m.args[1:]) + positional = [None]*len(args_names) + varargs = None + + + # All args are positional, so convert kargs to positional + for item in m.annotations: + if item == m.varargs: + varargs = m.annotations[item] + else: + positional[args_names.index(item)] = m.annotations[item] + + tailargs = self._positional_validate(tailargs, positional, varargs, + m.args[1:], m.varargs) ordered = [(f, a) for _, f, a in sorted([(sf.index, f, sf.val) for f, sf in swfuncs.items()])] return ordered, tailargs + def _positional_validate(self, args, validator_list, varargs, argnames, varargname): + """Makes sure args follows the validation given input""" + out_args = list(args) + + for i in range(min(len(args),len(validator_list))): + + if validator_list[i] is not None: + out_args[i] = self._handle_argument(args[i], validator_list[i], argnames[i]) + + if len(args) > len(validator_list): + if varargs is not None: + out_args[len(validator_list):] = [ + self._handle_argument(a, varargs, varargname) for a in args[len(validator_list):]] + else: + out_args[len(validator_list):] = args[len(validator_list):] + + return out_args + @classmethod def run(cls, argv = None, exit = True): # @ReservedAssignment """ @@ -339,6 +435,7 @@ """ if argv is None: argv = sys.argv + cls.autocomplete(argv) argv = list(argv) inst = cls(argv.pop(0)) retcode = 0 @@ -393,23 +490,8 @@ """ inst = cls("") - swfuncs = {} - for index, (swname, val) in enumerate(switches.items(), 1): - switch = getattr(cls, swname) - swinfo = inst._switches_by_func[switch._switch_info.func] - if isinstance(switch, CountOf): - p = (range(val),) - elif swinfo.list and not hasattr(val, "__iter__"): - raise SwitchError("Switch %r must be a sequence (iterable)" % (swname,)) - elif not swinfo.argtype: - # a flag - if val not in (True, False, None, Flag): - raise SwitchError("Switch %r is a boolean flag" % (swname,)) - p = () - else: - p = (val,) - swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index) - + + swfuncs = inst._parse_kwd_args(switches) ordered, tailargs = inst._validate_args(swfuncs, args) for f, a in ordered: f(inst, *a) @@ -428,6 +510,26 @@ return inst, retcode + def _parse_kwd_args(self, switches): + """Parses keywords (positional arguments), used by invoke.""" + swfuncs = {} + for index, (swname, val) in enumerate(switches.items(), 1): + switch = getattr(type(self), swname) + swinfo = self._switches_by_func[switch._switch_info.func] + if isinstance(switch, CountOf): + p = (range(val),) + elif swinfo.list and not hasattr(val, "__iter__"): + raise SwitchError("Switch %r must be a sequence (iterable)" % (swname,)) + elif not swinfo.argtype: + # a flag + if val not in (True, False, None, Flag): + raise SwitchError("Switch %r is a boolean flag" % (swname,)) + p = () + else: + p = (val,) + swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index) + return swfuncs + def main(self, *args): """Implement me (no need to call super)""" if self._subcommands: @@ -473,24 +575,25 @@ self.version() print("") if self.DESCRIPTION: - print(self.DESCRIPTION.strip()) + print(self.DESCRIPTION.strip() + '\n') - m_args, m_varargs, _, m_defaults = inspect.getargspec(self.main) - tailargs = m_args[1:] # skip self - if m_defaults: - for i, d in enumerate(reversed(m_defaults)): + m = six.getfullargspec(self.main) + tailargs = m.args[1:] # skip self + if m.defaults: + for i, d in enumerate(reversed(m.defaults)): tailargs[-i - 1] = "[%s=%r]" % (tailargs[-i - 1], d) - if m_varargs: - tailargs.append("%s..." % (m_varargs,)) + if m.varargs: + tailargs.append("%s..." % (m.varargs,)) tailargs = " ".join(tailargs) - print("Usage:") - if not self.USAGE: - if self._subcommands: - self.USAGE = " %(progname)s [SWITCHES] [SUBCOMMAND [SWITCHES]] %(tailargs)s\n" - else: - self.USAGE = " %(progname)s [SWITCHES] %(tailargs)s\n" - print(self.USAGE % {"progname": self.PROGNAME, "tailargs": tailargs}) + with self.COLOR_USAGE: + print("Usage:") + if not self.USAGE: + if self._subcommands: + self.USAGE = " %(progname)s [SWITCHES] [SUBCOMMAND [SWITCHES]] %(tailargs)s\n" + else: + self.USAGE = " %(progname)s [SWITCHES] %(tailargs)s\n" + print(self.USAGE % {"progname": colors.filter(self.PROGNAME), "tailargs": tailargs}) by_groups = {} for si in self._switches_by_func.values(): @@ -501,7 +604,7 @@ def switchs(by_groups, show_groups): for grp, swinfos in sorted(by_groups.items(), key = lambda item: item[0]): if show_groups: - print("%s:" % (grp,)) + print(self.COLOR_GROUPS[grp] | grp) for si in sorted(swinfos, key = lambda si: si.names): swnames = ", ".join(("-" if len(n) == 1 else "--") + n for n in si.names @@ -515,18 +618,18 @@ else: argtype = "" prefix = swnames + argtype - yield si, prefix + yield si, prefix, self.COLOR_GROUPS[grp] if show_groups: print("") - sw_width = max(len(prefix) for si, prefix in switchs(by_groups, False)) + 4 + sw_width = max(len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4 cols, _ = get_terminal_size() description_indent = " %s%s%s" wrapper = TextWrapper(width = max(cols - min(sw_width, 60), 50) - 6) indentation = "\n" + " " * (cols - wrapper.width) - for si, prefix in switchs(by_groups, True): + for si, prefix, color in switchs(by_groups, True): help = si.help # @ReservedAssignment if si.list: help += "; may be given multiple times" @@ -543,23 +646,27 @@ padding = indentation else: padding = " " * max(cols - wrapper.width - len(prefix) - 4, 1) - print(description_indent % (prefix, padding, msg)) + print(description_indent % (color | prefix, padding, color | msg)) if self._subcommands: - print("Subcommands:") + gc = self.COLOR_GROUPS["Subcommands"] + print(gc | "Subcommands:") for name, subcls in sorted(self._subcommands.items()): - subapp = subcls.get() - doc = subapp.DESCRIPTION if subapp.DESCRIPTION else inspect.getdoc(subapp) - help = doc + "; " if doc else "" # @ReservedAssignment - help += "see '%s %s --help' for more info" % (self.PROGNAME, name) + with gc: + subapp = subcls.get() + doc = subapp.DESCRIPTION if subapp.DESCRIPTION else getdoc(subapp) + help = doc + "; " if doc else "" # @ReservedAssignment + help += "see '%s %s --help' for more info" % (self.PROGNAME, name) + + msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) + + if len(name) + wrapper.width >= cols: + padding = indentation + else: + padding = " " * max(cols - wrapper.width - len(name) - 4, 1) + print(description_indent % (subcls.name, padding, gc | colors.filter(msg))) - msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) - if len(name) + wrapper.width >= cols: - padding = indentation - else: - padding = " " * max(cols - wrapper.width - len(name) - 4, 1) - print(description_indent % (name, padding, msg)) def _get_prog_version(self): ver = None @@ -575,8 +682,6 @@ def version(self): """Prints the program's version and quits""" ver = self._get_prog_version() - if sys.stdout.isatty() and os.name == "posix": - fmt = "\033[0;36m%s\033[0m %s" - else: - fmt = "%s %s" - print (fmt % (self.PROGNAME, ver if ver is not None else "(version not set)")) + ver_name = ver if ver is not None else "(version not set)" + print('{0} {1}'.format(self.PROGNAME, ver_name)) + diff -Nru python-plumbum-1.5.0/plumbum/cli/__init__.py python-plumbum-1.6.0/plumbum/cli/__init__.py --- python-plumbum-1.5.0/plumbum/cli/__init__.py 2013-09-06 08:15:03.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/__init__.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,3 +1,3 @@ -from plumbum.cli.switches import SwitchError, switch, autoswitch, SwitchAttr, Flag, CountOf +from plumbum.cli.switches import SwitchError, switch, autoswitch, SwitchAttr, Flag, CountOf, positional from plumbum.cli.switches import Range, Set, ExistingDirectory, ExistingFile, NonexistentPath, Predicate from plumbum.cli.application import Application diff -Nru python-plumbum-1.5.0/plumbum/cli/progress.py python-plumbum-1.6.0/plumbum/cli/progress.py --- python-plumbum-1.5.0/plumbum/cli/progress.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/progress.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,235 @@ +""" +Progress bar +------------ +""" +from __future__ import print_function, division +import warnings +from abc import abstractmethod +import datetime +from plumbum.lib import six +from plumbum.cli.termsize import get_terminal_size + +class ProgressBase(six.ABC): + """Base class for progress bars. Customize for types of progress bars. + + :param iterator: The iterator to wrap with a progress bar + :param length: The length of the iterator (will use ``__len__`` if None) + :param timer: Try to time the completion status of the iterator + :param body: True if the slow portion occurs outside the iterator (in a loop, for example) + """ + + def __init__(self, iterator=None, length=None, timer=True, body=False, has_output=False): + if length is None: + length = len(iterator) + elif iterator is None: + iterator = range(length) + elif length is None and iterator is None: + raise TypeError("Expected either an iterator or a length") + + self.length = length + self.iterator = iterator + self.timer = timer + self.body = body + self.has_output = has_output + + def __len__(self): + return self.length + + def __iter__(self): + self.start() + return self + + @abstractmethod + def start(self): + """This should initialize the progress bar and the iterator""" + self.iter = iter(self.iterator) + self.value = -1 if self.body else 0 + self._start_time = datetime.datetime.now() + + def __next__(self): + try: + rval = next(self.iter) + self.increment() + except StopIteration: + self.done() + raise + return rval + + def next(self): + return self.__next__() + + @property + def value(self): + """This is the current value, as a property so setting it can be customized""" + return self._value + @value.setter + def value(self, val): + self._value = val + + @abstractmethod + def display(self): + """Called to update the progress bar""" + pass + + def increment(self): + """Sets next value and displays the bar""" + self.value += 1 + self.display() + + def time_remaining(self): + """Get the time remaining for the progress bar, guesses""" + if self.value < 1: + return None, None + elapsed_time = datetime.datetime.now() - self._start_time + time_each = elapsed_time / self.value + time_remaining = time_each * (self.length - self.value) + return elapsed_time, time_remaining + + def str_time_remaining(self): + """Returns a string version of time remaining""" + if self.value < 1: + return "Starting... " + else: + elapsed_time, time_remaining = list(map(str,self.time_remaining())) + return "{0} completed, {1} remaining".format(elapsed_time.split('.')[0], + time_remaining.split('.')[0]) + + @abstractmethod + def done(self): + """Is called when the iterator is done.""" + pass + + @classmethod + def range(cls, value, **kargs): + """Fast shortcut to create a range based progress bar, assumes work done in body""" + return cls(range(value), value, body=True, **kargs) + + @classmethod + def wrap(cls, iterator, length=None, **kargs): + """Shortcut to wrap an iterator that does not do all the work internally""" + return cls(iterator, length, body = True, **kargs) + + +class Progress(ProgressBase): + + def start(self): + super(Progress, self).start() + self.display() + + def done(self): + self.value = self.length + self.display() + if self.has_output: + print() + + + def __str__(self): + percent = max(self.value,0)/self.length + width = get_terminal_size(default=(0,0))[0] + ending = ' ' + (self.str_time_remaining() + if self.timer else '{0} of {1} complete'.format(self.value, self.length)) + if width - len(ending) < 10 or self.has_output: + self.width = 0 + if self.timer: + return "{0:g}% complete: {1}".format(100*percent, self.str_time_remaining()) + else: + return "{0:g}% complete".format(100*percent) + + else: + self.width = width - len(ending) - 2 - 1 + nstars = int(percent*self.width) + pbar = '[' + '*'*nstars + ' '*(self.width-nstars) + ']' + ending + + str_percent = ' {0:.0f}% '.format(100*percent) + + return pbar[:self.width//2 - 2] + str_percent + pbar[self.width//2+len(str_percent) - 2:] + + + def display(self): + disptxt = str(self) + if self.width == 0 or self.has_output: + print(disptxt) + else: + print("\r", end='') + print(disptxt, end='', flush=True) + + +class ProgressIPy(ProgressBase): + HTMLBOX = '
{}
' + + def __init__(self, *args, **kargs): + + # Ipython gives warnings when using widgets about the API potentially changing + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + try: + from ipywidgets import IntProgress, HTML, HBox + except ImportError: # Support IPython < 4.0 + from IPython.html.widgets import IntProgress, HTML, HBox + + super(ProgressIPy, self).__init__(*args, **kargs) + self.prog = IntProgress(max=self.length) + self._label = HTML() + self._box = HBox((self.prog, self._label)) + + def start(self): + from IPython.display import display + display(self._box) + super(ProgressIPy, self).start() + + @property + def value(self): + """This is the current value, -1 allowed (automatically fixed for display)""" + return self._value + @value.setter + def value(self, val): + self._value = val + self.prog.value = max(val, 0) + self.prog.description = "{0:.2f}%".format(100*self.value / self.length) + if self.timer and val > 0: + self._label.value = self.HTMLBOX.format(self.str_time_remaining()) + + def display(self): + pass + + def done(self): + self._box.close() + + +class ProgressAuto(ProgressBase): + """Automatically selects the best progress bar (IPython HTML or text). Does not work with qtconsole + (as that is correctly identified as identical to notebook, since the kernel is the same); it will still + iterate, but no graphical indication will be diplayed. + + :param iterator: The iterator to wrap with a progress bar + :param length: The length of the iterator (will use ``__len__`` if None) + :param timer: Try to time the completion status of the iterator + :param body: True if the slow portion occurs outside the iterator (in a loop, for example) + """ + def __new__(cls, *args, **kargs): + """Uses the generator trick that if a cls instance is returned, the __init__ method is not called.""" + try: + __IPYTHON__ + try: + from traitlets import TraitError + except ImportError: # Support for IPython < 4.0 + from IPython.utils.traitlets import TraitError + + try: + return ProgressIPy(*args, **kargs) + except TraitError: + raise NameError() + except (NameError, ImportError): + return Progress(*args, **kargs) + +ProgressAuto.register(ProgressIPy) +ProgressAuto.register(Progress) + +def main(): + import time + tst = Progress.range(20) + for i in tst: + time.sleep(1) + +if __name__ == '__main__': + main() diff -Nru python-plumbum-1.5.0/plumbum/cli/switches.py python-plumbum-1.6.0/plumbum/cli/switches.py --- python-plumbum-1.5.0/plumbum/cli/switches.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/switches.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,7 +1,8 @@ -import inspect -from plumbum.lib import six +from plumbum.lib import six, getdoc from plumbum import local +from abc import abstractmethod + class SwitchError(Exception): """A general switch related-error (base class of all other switch errors)""" @@ -39,7 +40,7 @@ setattr(self, k, v) def switch(names, argtype = None, argname = None, list = False, mandatory = False, requires = (), - excludes = (), help = None, overridable = False, group = "Switches"): + excludes = (), help = None, overridable = False, group = "Switches", envname=None): """ A decorator that exposes functions as command-line switches. Usage:: @@ -63,6 +64,8 @@ but it can be used for clarity. Single-letter names are prefixed by ``-``, while longer names are prefixed by ``--`` + :param envname: Name of environment variable to extract value from, as alternative to argv + :param argtype: If this function takes an argument, you need to specify its type. The default is ``None``, which means the function takes no argument. The type is more of a "validator" than a real type; it can be any callable object @@ -131,17 +134,17 @@ def deco(func): if argname is None: - argspec = inspect.getargspec(func)[0] + argspec = six.getfullargspec(func).args if len(argspec) == 2: argname2 = argspec[1] else: argname2 = "VALUE" else: argname2 = argname - help2 = inspect.getdoc(func) if help is None else help + help2 = getdoc(func) if help is None else help if not help2: help2 = str(func) - func._switch_info = SwitchInfo(names = names, argtype = argtype, list = list, func = func, + func._switch_info = SwitchInfo(names = names, envname=envname, argtype = argtype, list = list, func = func, mandatory = mandatory, overridable = overridable, group = group, requires = requires, excludes = excludes, argname = argname2, help = help2) return func @@ -251,9 +254,94 @@ self.__set__(inst, len(v)) #=================================================================================================== +# Decorator for function that adds argument checking +#=================================================================================================== + + + +class positional(object): + """ + Runs a validator on the main function for a class. + This should be used like this:: + + class MyApp(cli.Application): + @cli.positional(cli.Range(1,10), cli.ExistingFile) + def main(self, x, *f): + # x is a range, f's are all ExistingFile's) + + Or, Python 3 only:: + + class MyApp(cli.Application): + def main(self, x : cli.Range(1,10), *f : cli.ExistingFile): + # x is a range, f's are all ExistingFile's) + + + If you do not want to validate on the annotations, use this decorator ( + even if empty) to override annotation validation. + + Validators should be callable, and should have a ``.choices()`` function with + possible choices. (For future argument completion, for example) + + Default arguments do not go through the validator. + + #TODO: Check with MyPy + + """ + + def __init__(self, *args, **kargs): + self.args = args + self.kargs = kargs + + def __call__(self, function): + m = six.getfullargspec(function) + args_names = list(m.args[1:]) + + positional = [None]*len(args_names) + varargs = None + + for i in range(min(len(positional),len(self.args))): + positional[i] = self.args[i] + + if len(args_names) + 1 == len(self.args): + varargs = self.args[-1] + + # All args are positional, so convert kargs to positional + for item in self.kargs: + if item == m.varargs: + varargs = self.kargs[item] + else: + positional[args_names.index(item)] = self.kargs[item] + + function.positional = positional + function.positional_varargs = varargs + return function + +class Validator(six.ABC): + __slots__ = () + + @abstractmethod + def __call__(self, obj): + "Must be implemented for a Validator to work" + + def choices(self, partial=""): + """Should return set of valid choices, can be given optional partial info""" + return set() + + def __repr__(self): + """If not overridden, will print the slots as args""" + + slots = {} + for cls in self.__mro__: + for prop in getattr(cls, "__slots__", ()): + if prop[0] != '_': + slots[prop] = getattr(self, prop) + mystrs = ("{0} = {1}".format(name, slots[name]) for name in slots) + return "{0}({1})".format(self.__class__.__name__, ", ".join(mystrs)) + +#=================================================================================================== # Switch type validators #=================================================================================================== -class Range(object): +class Range(Validator): """ A switch-type validator that checks for the inclusion of a value in a certain range. Usage:: @@ -264,6 +352,8 @@ :param start: The minimal value :param end: The maximal value """ + __slots__ = ("start", "end") + def __init__(self, start, end): self.start = start self.end = end @@ -274,18 +364,21 @@ if obj < self.start or obj > self.end: raise ValueError("Not in range [%d..%d]" % (self.start, self.end)) return obj + def choices(self, partial=""): + # TODO: Add partial handling + return set(range(self.start, self.end+1)) -class Set(object): +class Set(Validator): """ A switch-type validator that checks that the value is contained in a defined set of values. Usage:: class MyApp(Application): - mode = SwitchAttr(["--mode"], Set("TCP", "UDP", case_insensitive = False)) + mode = SwitchAttr(["--mode"], Set("TCP", "UDP", case_sensitive = False)) :param values: The set of values (strings) - :param case_insensitive: A keyword argument that indicates whether to use case-sensitive - comparison or not. The default is ``True`` + :param case_sensitive: A keyword argument that indicates whether to use case-sensitive + comparison or not. The default is ``False`` """ def __init__(self, *values, **kwargs): self.case_sensitive = kwargs.pop("case_sensitive", False) @@ -300,6 +393,9 @@ if obj not in self.values: raise ValueError("Expected one of %r" % (list(self.values.values()),)) return self.values[obj] + def choices(self, partial=""): + # TODO: Add case sensitive/insensitive parital completion + return set(self.values) class Predicate(object): """A wrapper for a single-argument function with pretty printing""" @@ -309,12 +405,14 @@ return self.func.__name__ def __call__(self, val): return self.func(val) + def choices(self, partial=""): + return set() @Predicate def ExistingDirectory(val): """A switch-type validator that ensures that the given argument is an existing directory""" p = local.path(val) - if not p.isdir(): + if not p.is_dir(): raise ValueError("%r is not a directory" % (val,)) return p @@ -322,7 +420,7 @@ def ExistingFile(val): """A switch-type validator that ensures that the given argument is an existing file""" p = local.path(val) - if not p.isfile(): + if not p.is_file(): raise ValueError("%r is not a file" % (val,)) return p diff -Nru python-plumbum-1.5.0/plumbum/cli/terminal.py python-plumbum-1.6.0/plumbum/cli/terminal.py --- python-plumbum-1.5.0/plumbum/cli/terminal.py 2014-06-26 18:11:20.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/terminal.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,97 +1,28 @@ """ Terminal-related utilities +-------------------------- """ + +from __future__ import division, print_function, absolute_import import sys import os -import platform -from struct import Struct from plumbum import local +from plumbum.cli.termsize import get_terminal_size +from plumbum.cli.progress import Progress - -def get_terminal_size(): - """ - Get width and height of console; works on linux, os x, windows and cygwin - - Adapted from https://gist.github.com/jtriley/1108174 - Originally from: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python - """ - current_os = platform.system() - if current_os == 'Windows': - size = _get_terminal_size_windows() - if not size: - # needed for window's python in cygwin's xterm! - size = _get_terminal_size_tput() - elif current_os in ('Linux', 'Darwin', 'FreeBSD') or current_os.startswith('CYGWIN'): - size = _get_terminal_size_linux() - - if size is None: # we'll assume the standard 80x25 if for any reason we don't know the terminal size - size = (80, 25) - return size - -def _get_terminal_size_windows(): - try: - from ctypes import windll, create_string_buffer - STDERR_HANDLE = -12 - h = windll.kernel32.GetStdHandle(STDERR_HANDLE) - csbi_struct = Struct("hhhhHhhhhhh") - csbi = create_string_buffer(csbi_struct.size) - res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) - if res: - _, _, _, _, _, left, top, right, bottom, _, _ = csbi_struct.unpack(csbi.raw) - return right - left + 1, bottom - top + 1 - return None - except Exception: - return None - -def _get_terminal_size_tput(): - # get terminal width - # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window - try: - from plumbum.cmd import tput - cols = int(tput('cols')) - rows = int(tput('lines')) - return (cols, rows) - except Exception: - return None - -def _ioctl_GWINSZ(fd): - yx = Struct("hh") - try: - import fcntl - import termios - return yx.unpack(fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) - except Exception: - return None - -def _get_terminal_size_linux(): - cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) - if not cr: - try: - fd = os.open(os.ctermid(), os.O_RDONLY) - cr = _ioctl_GWINSZ(fd) - os.close(fd) - except Exception: - pass - if not cr: - try: - cr = (int(os.environ['LINES']), int(os.environ['COLUMNS'])) - except Exception: - return None - return cr[1], cr[0] - -def readline(message = ""): +def readline(message = ""): """Gets a line of input from the user (stdin)""" sys.stdout.write(message) return sys.stdin.readline() def ask(question, default = None): """ - Presents the user with a yes/no question. - + Presents the user with a yes/no question. + :param question: The question to ask - :param default: If ``None``, the user must answer. If ``True`` or ``False``, lack of response is + :param default: If ``None``, the user must answer. If ``True`` or ``False``, lack of response is interpreted as the default option - + :returns: the user's choice """ question = question.rstrip().rstrip("?").rstrip() + "?" @@ -101,7 +32,7 @@ question += " [Y/n] " else: question += " [y/N] " - + while True: try: answer = readline(question).strip().lower() @@ -118,21 +49,21 @@ def choose(question, options, default = None): """Prompts the user with a question and a set of options, from which the user need choose. - + :param question: The question to ask - :param options: A set of options. It can be a list (of strings or two-tuples, mapping text + :param options: A set of options. It can be a list (of strings or two-tuples, mapping text to returned-object) or a dict (mapping text to returned-object).`` :param default: If ``None``, the user must answer. Otherwise, lack of response is interpreted as this answer - + :returns: The user's choice - + Example:: - + ans = choose("What is your favorite color?", ["blue", "yellow", "green"], default = "yellow") # `ans` will be one of "blue", "yellow" or "green" - ans = choose("What is your favorite color?", + ans = choose("What is your favorite color?", {"blue" : 0x0000ff, "yellow" : 0xffff00 , "green" : 0x00ff00}, default = 0x00ff00) # this will display "blue", "yellow" and "green" but return a numerical value """ @@ -204,7 +135,7 @@ return ans def hexdump(data_or_stream, bytes_per_line = 16, aggregate = True): - """Convert the given bytes (or a stream with a buffering ``read()`` method) to hexdump-formatted lines, + """Convert the given bytes (or a stream with a buffering ``read()`` method) to hexdump-formatted lines, with possible aggregation of identical lines. Returns a generator of formatted lines. """ if hasattr(data_or_stream, "read"): @@ -235,7 +166,7 @@ def pager(rows, pagercmd = None): """Opens a pager (e.g., ``less``) to display the given text. Requires a terminal. - + :param rows: a ``bytes`` or a list/iterator of "rows" (``bytes``) :param pagercmd: the pager program to run. Defaults to ``less -RSin`` """ diff -Nru python-plumbum-1.5.0/plumbum/cli/termsize.py python-plumbum-1.6.0/plumbum/cli/termsize.py --- python-plumbum-1.5.0/plumbum/cli/termsize.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/cli/termsize.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,80 @@ +""" +Terminal size utility +--------------------- +""" +from __future__ import division, print_function, absolute_import +import os +import platform +from struct import Struct + + +def get_terminal_size(default=(80, 25)): + """ + Get width and height of console; works on linux, os x, windows and cygwin + + Adapted from https://gist.github.com/jtriley/1108174 + Originally from: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python + """ + current_os = platform.system() + if current_os == 'Windows': + size = _get_terminal_size_windows() + if not size: + # needed for window's python in cygwin's xterm! + size = _get_terminal_size_tput() + elif current_os in ('Linux', 'Darwin', 'FreeBSD') or current_os.startswith('CYGWIN'): + size = _get_terminal_size_linux() + + if size is None: # we'll assume the standard 80x25 if for any reason we don't know the terminal size + size = default + return size + +def _get_terminal_size_windows(): + try: + from ctypes import windll, create_string_buffer + STDERR_HANDLE = -12 + h = windll.kernel32.GetStdHandle(STDERR_HANDLE) + csbi_struct = Struct("hhhhHhhhhhh") + csbi = create_string_buffer(csbi_struct.size) + res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) + if res: + _, _, _, _, _, left, top, right, bottom, _, _ = csbi_struct.unpack(csbi.raw) + return right - left + 1, bottom - top + 1 + return None + except Exception: + return None + +def _get_terminal_size_tput(): + # get terminal width + # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window + try: + from plumbum.cmd import tput + cols = int(tput('cols')) + rows = int(tput('lines')) + return (cols, rows) + except Exception: + return None + +def _ioctl_GWINSZ(fd): + yx = Struct("hh") + try: + import fcntl + import termios + return yx.unpack(fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + except Exception: + return None + +def _get_terminal_size_linux(): + cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + cr = _ioctl_GWINSZ(fd) + os.close(fd) + except Exception: + pass + if not cr: + try: + cr = (int(os.environ['LINES']), int(os.environ['COLUMNS'])) + except Exception: + return None + return cr[1], cr[0] diff -Nru python-plumbum-1.5.0/plumbum/colorlib/factories.py python-plumbum-1.6.0/plumbum/colorlib/factories.py --- python-plumbum-1.5.0/plumbum/colorlib/factories.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/factories.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,196 @@ +""" +Color-related factories. They produce Styles. + +""" + +from __future__ import print_function +import sys +from functools import reduce +from plumbum.colorlib.names import color_names, default_styles +from plumbum.colorlib.styles import ColorNotFound + +__all__ = ['ColorFactory', 'StyleFactory'] + + +class ColorFactory(object): + + """This creates color names given fg = True/False. It usually will + be called as part of a StyleFactory.""" + + def __init__(self, fg, style): + self._fg = fg + self._style = style + self.reset = style.from_color(style.color_class(fg=fg)) + + # Adding the color name shortcuts for foreground colors + for item in color_names[:16]: + setattr(self, item, style.from_color(style.color_class.from_simple(item, fg=fg))) + + + def __getattr__(self, item): + """Full color names work, but do not populate __dir__.""" + try: + return self._style.from_color(self._style.color_class(item, fg=self._fg)) + except ColorNotFound: + raise AttributeError(item) + + def full(self, name): + """Gets the style for a color, using standard name procedure: either full + color name, html code, or number.""" + return self._style.from_color(self._style.color_class.from_full(name, fg=self._fg)) + + def simple(self, name): + """Return the extended color scheme color for a value or name.""" + return self._style.from_color(self._style.color_class.from_simple(name, fg=self._fg)) + + def rgb(self, r, g=None, b=None): + """Return the extended color scheme color for a value.""" + if g is None and b is None: + return self.hex(r) + else: + return self._style.from_color(self._style.color_class(r, g, b, fg=self._fg)) + + def hex(self, hexcode): + """Return the extended color scheme color for a value.""" + return self._style.from_color(self._style.color_class.from_hex(hexcode, fg=self._fg)) + + def ansi(self, ansiseq): + """Make a style from an ansi text sequence""" + return self._style.from_ansi(ansiseq) + + def __getitem__(self, val): + """\ + Shortcut to provide way to access colors numerically or by slice. + If end <= 16, will stay to simple ANSI version.""" + if isinstance(val, slice): + (start, stop, stride) = val.indices(256) + if stop <= 16: + return [self.simple(v) for v in range(start, stop, stride)] + else: + return [self.full(v) for v in range(start, stop, stride)] + elif isinstance(val, tuple): + return self.rgb(*val) + + try: + return self.full(val) + except ColorNotFound: + return self.hex(val) + + def __call__(self, val_or_r=None, g = None, b = None): + """Shortcut to provide way to access colors.""" + if val_or_r is None or (isinstance(val_or_r, str) and val_or_r == ''): + return self._style() + if isinstance(val_or_r, self._style): + return self._style(val_or_r) + if isinstance(val_or_r, str) and '\033' in val_or_r: + return self.ansi(val_or_r) + return self._style.from_color(self._style.color_class(val_or_r, g, b, fg=self._fg)) + + def __iter__(self): + """Iterates through all colors in extended colorset.""" + return (self.full(i) for i in range(256)) + + def __invert__(self): + """Allows clearing a color with ~""" + return self.reset + + def __enter__(self): + """This will reset the color on leaving the with statement.""" + return self + + def __exit__(self, type, value, traceback): + """This resets a FG/BG color or all styles, + due to different definition of RESET for the + factories.""" + + self.reset.now() + return False + + def __repr__(self): + """Simple representation of the class by name.""" + return "<{0}>".format(self.__class__.__name__) + +class StyleFactory(ColorFactory): + + """Factory for styles. Holds font styles, FG and BG objects representing colors, and + imitates the FG ColorFactory to a large degree.""" + + def __init__(self, style): + super(StyleFactory,self).__init__(True, style) + + self.fg = ColorFactory(True, style) + self.bg = ColorFactory(False, style) + + self.do_nothing = style() + self.reset = style(reset=True) + + for item in style.attribute_names: + setattr(self, item, style(attributes={item:True})) + + self.load_stylesheet(default_styles) + + @property + def use_color(self): + """Shortcut for setting color usage on Style""" + return self._style.use_color + + @use_color.setter + def use_color(self, val): + self._style.use_color = val + + def from_ansi(self, ansi_sequence): + """Calling this is a shortcut for creating a style from an ANSI sequence.""" + return self._style.from_ansi(ansi_sequence) + + @property + def stdout(self): + """This is a shortcut for getting stdout from a class without an instance.""" + return self._style._stdout if self._style._stdout is not None else sys.stdout + @stdout.setter + def stdout(self, newout): + self._style._stdout = newout + + def get_colors_from_string(self, color=''): + """ + Sets color based on string, use `.` or space for separator, + and numbers, fg/bg, htmlcodes, etc all accepted (as strings). + """ + + names = color.replace('.', ' ').split() + prev = self + styleslist = [] + for name in names: + try: + prev = getattr(prev, name) + except AttributeError: + try: + prev = prev(int(name)) + except (ColorNotFound, ValueError): + prev = prev(name) + if isinstance(prev, self._style): + styleslist.append(prev) + prev = self + + if styleslist: + prev = reduce(lambda a,b: a & b, styleslist) + + return prev if isinstance(prev, self._style) else prev.reset + + + def filter(self, colored_string): + """Filters out colors in a string, returning only the name.""" + if isinstance(colored_string, self._style): + return colored_string + return self._style.string_filter_ansi(colored_string) + + def contains_colors(self, colored_string): + """Checks to see if a string contains colors.""" + return self._style.string_contains_colors(colored_string) + + def extract(self, colored_string): + """Gets colors from an ansi string, returns those colors""" + return self._style.from_ansi(colored_string, True) + + def load_stylesheet(self, stylesheet=default_styles): + for item in stylesheet: + setattr(self, item, self.get_colors_from_string(stylesheet[item])) diff -Nru python-plumbum-1.5.0/plumbum/colorlib/__init__.py python-plumbum-1.6.0/plumbum/colorlib/__init__.py --- python-plumbum-1.5.0/plumbum/colorlib/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/__init__.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,31 @@ +"""\ +The ``ansicolor`` object provides ``bg`` and ``fg`` to access colors, +and attributes like bold and +underlined text. It also provides ``reset`` to recover the normal font. +""" + +from plumbum.colorlib.factories import StyleFactory +from plumbum.colorlib.styles import Style, ANSIStyle, HTMLStyle, ColorNotFound + +ansicolors = StyleFactory(ANSIStyle) +htmlcolors = StyleFactory(HTMLStyle) + +def load_ipython_extension(ipython): + try: + from plumbum.colorlib._ipython_ext import OutputMagics + except ImportError: + print("IPython required for the IPython extension to be loaded.") + raise + + ipython.push({"colors":htmlcolors}) + ipython.register_magics(OutputMagics) + +def main(): + """Color changing script entry. Call using + python -m plumbum.colors, will reset if no arguments given.""" + import sys + color = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else '' + ansicolors.use_color=True + ansicolors.get_colors_from_string(color).now() + + diff -Nru python-plumbum-1.5.0/plumbum/colorlib/_ipython_ext.py python-plumbum-1.6.0/plumbum/colorlib/_ipython_ext.py --- python-plumbum-1.5.0/plumbum/colorlib/_ipython_ext.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/_ipython_ext.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,36 @@ +from IPython.core.magic import (Magics, magics_class, + cell_magic, needs_local_scope) +import IPython.display + +try: + from io import StringIO +except ImportError: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO +import sys + +valid_choices = [x[8:] for x in dir(IPython.display) if 'display_' == x[:8]] + +@magics_class +class OutputMagics(Magics): + + @needs_local_scope + @cell_magic + def to(self, line, cell, local_ns=None): + choice = line.strip() + assert choice in valid_choices, "Valid choices for '%%to' are: "+str(valid_choices) + display_fn = getattr(IPython.display, "display_"+choice) + + "Captures stdout and renders it in the notebook with some ." + with StringIO() as out: + old_out = sys.stdout + try: + sys.stdout = out + exec(cell, self.shell.user_ns, local_ns) + out.seek(0) + display_fn(out.getvalue(), raw=True) + finally: + sys.stdout = old_out + diff -Nru python-plumbum-1.5.0/plumbum/colorlib/__main__.py python-plumbum-1.6.0/plumbum/colorlib/__main__.py --- python-plumbum-1.5.0/plumbum/colorlib/__main__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/__main__.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,9 @@ +""" +This is provided as a quick way to recover your terminal. Simply run +``python -m plumbum.colorlib`` +to recover terminal color. +""" + +from plumbum.colorlib import main + +main() diff -Nru python-plumbum-1.5.0/plumbum/colorlib/names.py python-plumbum-1.6.0/plumbum/colorlib/names.py --- python-plumbum-1.5.0/plumbum/colorlib/names.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/names.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,378 @@ +''' +Names for the standard and extended color set. +Extended set is similar to `vim wiki `_, `colored `_, etc. Colors based on `wikipedia `_. + +You can access the index of the colors with names.index(name). You can access the +rgb values with ``r=int(html[n][1:3],16)``, etc. +''' + +from __future__ import division, print_function + +color_names = '''\ +black +red +green +yellow +blue +magenta +cyan +light_gray +dark_gray +light_red +light_green +light_yellow +light_blue +light_magenta +light_cyan +white +grey_0 +navy_blue +dark_blue +blue_3 +blue_3a +blue_1 +dark_green +deep_sky_blue_4 +deep_sky_blue_4a +deep_sky_blue_4b +dodger_blue_3 +dodger_blue_2 +green_4 +spring_green_4 +turquoise_4 +deep_sky_blue_3 +deep_sky_blue_3a +dodger_blue_1 +green_3 +spring_green_3 +dark_cyan +light_sea_green +deep_sky_blue_2 +deep_sky_blue_1 +green_3a +spring_green_3a +spring_green_2 +cyan_3 +dark_turquoise +turquoise_2 +green_1 +spring_green_2a +spring_green_1 +medium_spring_green +cyan_2 +cyan_1 +dark_red +deep_pink_4 +purple_4 +purple_4a +purple_3 +blue_violet +orange_4 +grey_37 +medium_purple_4 +slate_blue_3 +slate_blue_3a +royal_blue_1 +chartreuse_4 +dark_sea_green_4 +pale_turquoise_4 +steel_blue +steel_blue_3 +cornflower_blue +chartreuse_3 +dark_sea_green_4a +cadet_blue +cadet_blue_a +sky_blue_3 +steel_blue_1 +chartreuse_3a +pale_green_3 +sea_green_3 +aquamarine_3 +medium_turquoise +steel_blue_1a +chartreuse_2a +sea_green_2 +sea_green_1 +sea_green_1a +aquamarine_1 +dark_slate_gray_2 +dark_red_a +deep_pink_4a +dark_magenta +dark_magenta_a +dark_violet +purple +orange_4a +light_pink_4 +plum_4 +medium_purple_3 +medium_purple_3a +slate_blue_1 +yellow_4 +wheat_4 +grey_53 +light_slate_grey +medium_purple +light_slate_blue +yellow_4_a +dark_olive_green_3 +dark_sea_green +light_sky_blue_3 +light_sky_blue_3a +sky_blue_2 +chartreuse_2 +dark_olive_green_3a +pale_green_3a +dark_sea_green_3 +dark_slate_gray_3 +sky_blue_1 +chartreuse_1 +light_green_a +light_green_b +pale_green_1 +aquamarine_1a +dark_slate_gray_1 +red_3 +deep_pink_4b +medium_violet_red +magenta_3 +dark_violet_a +purple_a +dark_orange_3 +indian_red +hot_pink_3 +medium_orchid_3 +medium_orchid +medium_purple_2 +dark_goldenrod +light_salmon_3 +rosy_brown +grey_63 +medium_purple_2a +medium_purple_1 +gold_3 +dark_khaki +navajo_white_3 +grey_69 +light_steel_blue_3 +light_steel_blue +yellow_3 +dark_olive_green_3b +dark_sea_green_3a +dark_sea_green_2 +light_cyan_3 +light_sky_blue_1 +green_yellow +dark_olive_green_2 +pale_green_1a +dark_sea_green_2a +dark_sea_green_1 +pale_turquoise_1 +red_3a +deep_pink_3 +deep_pink_3a +magenta_3a +magenta_3b +magenta_2 +dark_orange_3a +indian_red_a +hot_pink_3a +hot_pink_2 +orchid +medium_orchid_1 +orange_3 +light_salmon_3a +light_pink_3 +pink_3 +plum_3 +violet +gold_3a +light_goldenrod_3 +tan +misty_rose_3 +thistle_3 +plum_2 +yellow_3a +khaki_3 +light_goldenrod_2 +light_yellow_3 +grey_84 +light_steel_blue_1 +yellow_2 +dark_olive_green_1 +dark_olive_green_1a +dark_sea_green_1a +honeydew_2 +light_cyan_1 +red_1 +deep_pink_2 +deep_pink_1 +deep_pink_1a +magenta_2a +magenta_1 +orange_red_1 +indian_red_1 +indian_red_1a +hot_pink +hot_pink_a +medium_orchid_1a +dark_orange +salmon_1 +light_coral +pale_violet_red_1 +orchid_2 +orchid_1 +orange_1 +sandy_brown +light_salmon_1 +light_pink_1 +pink_1 +plum_1 +gold_1 +light_goldenrod_2a +light_goldenrod_2b +navajo_white_1 +misty_rose_1 +thistle_1 +yellow_1 +light_goldenrod_1 +khaki_1 +wheat_1 +cornsilk_1 +grey_10_0 +grey_3 +grey_7 +grey_11 +grey_15 +grey_19 +grey_23 +grey_27 +grey_30 +grey_35 +grey_39 +grey_42 +grey_46 +grey_50 +grey_54 +grey_58 +grey_62 +grey_66 +grey_70 +grey_74 +grey_78 +grey_82 +grey_85 +grey_89 +grey_93'''.split() + +_greys = (3.4, 7.4, 11, 15, 19, 23, 26.7, 30.49, 34.6, 38.6, 42.4, 46.4, 50, 54, 58, 62, 66, 69.8, 73.8, 77.7, 81.6, 85.3, 89.3, 93) +_grey_vals = [int(x/100.0*16*16) for x in _greys] + +_grey_html = ['#' + format(x,'02x')*3 for x in _grey_vals] + +_normals = [int(x,16) for x in '0 5f 87 af d7 ff'.split()] +_normal_html = ['#' + format(_normals[n//36],'02x') + format(_normals[n//6%6],'02x') + format(_normals[n%6],'02x') for n in range(16-16,232-16)] + +_base_pattern = [(n//4,n//2%2,n%2) for n in range(8)] +_base_html = (['#{2:02x}{1:02x}{0:02x}'.format(x[0]*192,x[1]*192,x[2]*192) for x in _base_pattern] + + ['#808080'] + + ['#{2:02x}{1:02x}{0:02x}'.format(x[0]*255,x[1]*255,x[2]*255) for x in _base_pattern][1:]) +color_html = _base_html + _normal_html + _grey_html + +color_codes_simple = list(range(8)) + list(range(60,68)) +"""Simple colors, remember that reset is #9, second half is non as common.""" + + +# Attributes +attributes_ansi = dict( + bold=1, + dim=2, + italics=3, + underline=4, + reverse=7, + hidden=8, + strikeout=9, + ) + +# Stylesheet +default_styles = dict( + warn="fg red", + title="fg cyan underline bold", + fatal="fg red bold", + highlight="bg yellow", + info="fg blue", + success="fg green", + ) + + +#Functions to be used for color name operations + +class FindNearest(object): + """This is a class for finding the nearest color given rgb values. + Different find methods are available.""" + def __init__(self, r, g, b): + self.r = r + self.b = b + self.g = g + + def only_basic(self): + """This will only return the first 8 colors! + Breaks the colorspace into cubes, returns color""" + midlevel = 0x40 # Since bright is not included + + # The colors are organised so that it is a + # 3D cube, black at 0,0,0, white at 1,1,1 + # Compressed to linear_integers r,g,b + # [[[0,1],[2,3]],[[4,5],[6,7]]] + # r*1 + g*2 + b*4 + return (self.r>=midlevel)*1 + (self.g>=midlevel)*2 + (self.b>=midlevel)*4 + + def all_slow(self, color_slice=slice(None, None, None)): + """This is a slow way to find the nearest color.""" + distances = [self._distance_to_color(color) for color in color_html[color_slice]] + return min(range(len(distances)), key=distances.__getitem__) + + def _distance_to_color(self, color): + """This computes the distance to a color, should be minimized.""" + rgb = (int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)) + return (self.r-rgb[0])**2 + (self.g-rgb[1])**2 + (self.b-rgb[2])**2 + + def _distance_to_color_number(self, n): + color = color_html[n] + return self._distance_to_color(color) + + def only_colorblock(self): + """This finds the nearest color based on block system, only works + for 17-232 color values.""" + rint = min(range(len(_normals)), key=[abs(x-self.r) for x in _normals].__getitem__) + bint = min(range(len(_normals)), key=[abs(x-self.b) for x in _normals].__getitem__) + gint = min(range(len(_normals)), key=[abs(x-self.g) for x in _normals].__getitem__) + return (16 + 36 * rint + 6 * gint + bint) + + def only_simple(self): + """Finds the simple color-block color.""" + return self.all_slow(slice(0,16,None)) + + def only_grey(self): + """Finds the greyscale color.""" + rawval = (self.r + self.b + self.g) / 3 + n = min(range(len(_grey_vals)), key=[abs(x-rawval) for x in _grey_vals].__getitem__) + return n+232 + + def all_fast(self): + """Runs roughly 8 times faster than the slow version.""" + colors = [self.only_simple(), self.only_colorblock(), self.only_grey()] + distances = [self._distance_to_color_number(n) for n in colors] + return colors[min(range(len(distances)), key=distances.__getitem__)] + +def from_html(color): + """Convert html hex code to rgb.""" + if len(color) != 7 or color[0] != '#': + raise ValueError("Invalid length of html code") + return (int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)) + +def to_html(r, g, b): + """Convert rgb to html hex code.""" + return "#{0:02x}{1:02x}{2:02x}".format(r, g, b) + diff -Nru python-plumbum-1.5.0/plumbum/colorlib/styles.py python-plumbum-1.6.0/plumbum/colorlib/styles.py --- python-plumbum-1.5.0/plumbum/colorlib/styles.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colorlib/styles.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,750 @@ +""" +This file provides two classes, `Color` and `Style`. + +``Color`` is rarely used directly, +but merely provides the workhorse for finding and manipulating colors. + +With the ``Style`` class, any color can be directly called or given to a with statement. +""" + +from __future__ import print_function +import sys +import os +import re +from copy import copy +from plumbum.colorlib.names import color_names, color_html +from plumbum.colorlib.names import color_codes_simple, from_html +from plumbum.colorlib.names import FindNearest, attributes_ansi +from plumbum.lib import six +from plumbum import local +from abc import abstractmethod +import platform +ABC = six.ABC + +__all__ = ['Color', 'Style', 'ANSIStyle', 'HTMLStyle', 'ColorNotFound', 'AttributeNotFound'] + + +_lower_camel_names = [n.replace('_', '') for n in color_names] + + +def get_color_repr(): + """Gets best colors for current system.""" + if not sys.stdout.isatty(): + return False + + term =local.env.get("TERM", "") + + # Some terminals set TERM=xterm for compatibility + if term == "xterm-256color" or term == "xterm": + return 3 if platform.system() == 'Darwin' else 4 + elif term == "xterm-16color": + return 2 + elif os.name == 'nt': + return 1 + else: + return False + +class ColorNotFound(Exception): + """Thrown when a color is not valid for a particular method.""" + pass + +class AttributeNotFound(Exception): + """Similar to color not found, only for attributes.""" + pass + +class ResetNotSupported(Exception): + """An exception indicating that Reset is not available + for this Style.""" + pass + + +class Color(ABC): + """\ + Loaded with ``(r, g, b, fg)`` or ``(color, fg=fg)``. The second signature is a short cut + and will try full and hex loading. + + This class stores the idea of a color, rather than a specific implementation. + It provides as many different tools for representations as possible, and can be subclassed + to add more representations, though that should not be needed for most situations. ``.from_`` class methods provide quick ways to create colors given different representations. + You will not usually interact with this class. + + Possible colors:: + + reset = Color() # The reset color by default + background_reset = Color(fg=False) # Can be a background color + blue = Color(0,0,255) # Red, Green, Blue + green = Color.from_full("green") # Case insensitive name, from large colorset + red = Color.from_full(1) # Color number + white = Color.from_html("#FFFFFF") # HTML supported + yellow = Color.from_simple("red") # Simple colorset + + + The attributes are: + + .. data:: reset + + True it this is a reset color (following attributes don't matter if True) + + .. data:: rgb + + The red/green/blue tuple for this color + + .. data:: simple + + If true will stay to 16 color mode. + + .. data:: number + + The color number given the mode, closest to rgb + if not rgb not exact, gives position of closest name. + + .. data:: fg + + This is a foreground color if True. Background color if False. + + """ + + __slots__ = ('fg', 'isreset', 'rgb', 'number', 'representation', 'exact') + + def __init__(self, r_or_color=None, g=None, b=None, fg=True): + """This works from color values, or tries to load non-simple ones.""" + + if isinstance(r_or_color, type(self)): + for item in ('fg', 'isreset', 'rgb', 'number', 'representation', 'exact'): + setattr(self, item, getattr(r_or_color, item)) + return + + self.fg = fg + self.isreset = True # Starts as reset color + self.rgb = (0,0,0) + + self.number = None + 'Number of the original color, or closest color' + + self.representation = 4 + '0 for off, 1 for 8 colors, 2 for 16 colors, 3 for 256 colors, 4 for true color' + + self.exact = True + 'This is false if the named color does not match the real color' + + if None in (g,b): + if not r_or_color: + return + try: + self._from_simple(r_or_color) + except ColorNotFound: + try: + self._from_full(r_or_color) + except ColorNotFound: + self._from_hex(r_or_color) + + elif None not in (r_or_color, g, b): + self.rgb = (r_or_color,g,b) + self._init_number() + else: + raise ColorNotFound("Invalid parameters for a color!") + + def _init_number(self): + """Should always be called after filling in r, g, b, and representation. + Color will not be a reset color anymore.""" + + if self.representation in (0, 1): + number = FindNearest(*self.rgb).only_basic() + elif self.representation == 2: + number = FindNearest(*self.rgb).only_simple() + elif self.representation in (3, 4): + number = FindNearest(*self.rgb).all_fast() + + if self.number is None: + self.number = number + + self.isreset = False + self.exact = self.rgb == from_html(color_html[self.number]) + if not self.exact: + self.number = number + + + @classmethod + def from_simple(cls, color, fg=True): + """Creates a color from simple name or color number""" + self = cls(fg=fg) + self._from_simple(color) + return self + + def _from_simple(self, color): + try: + color = color.lower() + color = color.replace(' ','') + color = color.replace('_','') + except AttributeError: + pass + + if color == 'reset': + return + + elif color in _lower_camel_names[:16]: + self.number = _lower_camel_names.index(color) + self.rgb = from_html(color_html[self.number]) + + elif isinstance(color, int) and 0 <= color < 16: + self.number = color + self.rgb = from_html(color_html[color]) + + else: + raise ColorNotFound("Did not find color: " + repr(color)) + + self.representation = 2 + self._init_number() + + @classmethod + def from_full(cls, color, fg=True): + """Creates a color from full name or color number""" + self = cls(fg=fg) + self._from_full(color) + return self + + def _from_full(self, color): + try: + color = color.lower() + color = color.replace(' ','') + color = color.replace('_','') + except AttributeError: + pass + + if color == 'reset': + return + + elif color in _lower_camel_names: + self.number = _lower_camel_names.index(color) + self.rgb = from_html(color_html[self.number]) + + elif isinstance(color, int) and 0 <= color <= 255: + self.number = color + self.rgb = from_html(color_html[color]) + + else: + raise ColorNotFound("Did not find color: " + repr(color)) + + self.representation = 3 + self._init_number() + + @classmethod + def from_hex(cls, color, fg=True): + """Converts #123456 values to colors.""" + + self = cls(fg=fg) + self._from_hex(color) + return self + + def _from_hex(self, color): + try: + self.rgb = from_html(color) + except (TypeError, ValueError): + raise ColorNotFound("Did not find htmlcode: " + repr(color)) + + self.representation = 4 + self._init_number() + + @property + def name(self): + """The (closest) name of the current color""" + if self.isreset: + return 'reset' + else: + return color_names[self.number] + + @property + def name_camelcase(self): + """The camelcase name of the color""" + return self.name.replace("_", " ").title().replace(" ","") + + def __repr__(self): + """This class has a smart representation that shows name and color (if not unique).""" + name = ['Deactivated:', ' Basic:', '', ' Full:', ' True:'][self.representation] + name += '' if self.fg else ' Background' + name += ' ' + self.name_camelcase + name += '' if self.exact else ' ' + self.hex_code + return name[1:] + + def __eq__(self, other): + """Reset colors are equal, otherwise rgb have to match.""" + if self.isreset: + return other.isreset + else: + return self.rgb == other.rgb + + @property + def ansi_sequence(self): + """This is the ansi sequence as a string, ready to use.""" + return '\033[' + ';'.join(map(str, self.ansi_codes)) + 'm' + + @property + def ansi_codes(self): + """This is the full ANSI code, can be reset, simple, 256, or full color.""" + ansi_addition = 30 if self.fg else 40 + + if self.isreset: + return (ansi_addition+9,) + elif self.representation < 3: + return (color_codes_simple[self.number]+ansi_addition,) + elif self.representation == 3: + return (ansi_addition+8, 5, self.number) + else: + return (ansi_addition+8, 2, self.rgb[0], self.rgb[1], self.rgb[2]) + + @property + def hex_code(self): + """This is the hex code of the current color, html style notation.""" + if self.isreset: + return '#000000' + else: + return '#' + '{0[0]:02X}{0[1]:02X}{0[2]:02X}'.format(self.rgb) + + def __str__(self): + """This just prints it's simple name""" + return self.name + + def to_representation(self, val): + """Converts a color to any representation""" + other = copy(self) + other.representation = val + if self.isreset: + return other + other.number = None + other._init_number() + return other + + def limit_representation(self, val): + """Only converts if val is lower than representation""" + + if self.representation <= val: + return self + else: + return self.to_representation(val) + + + + +class Style(object): + """This class allows the color changes to be called directly + to write them to stdout, ``[]`` calls to wrap colors (or the ``.wrap`` method) + and can be called in a with statement. + """ + + __slots__ = ('attributes','fg', 'bg', 'isreset') + + color_class = Color + """The class of color to use. Never hardcode ``Color`` call when writing a Style + method.""" + + attribute_names = None # should be a dict of valid names + _stdout = None + end = '\n' + """The endline character. Override if needed in subclasses.""" + + ANSI_REG = re.compile('\033' + r'\[([\d;]+)m') + """The regular expression that finds ansi codes in a string.""" + + @property + def stdout(self): + """\ + This property will allow custom, class level control of stdout. + It will use current sys.stdout if set to None (default). + Unfortunately, it only works on an instance.. + """ + return self.__class__._stdout if self.__class__._stdout is not None else sys.stdout + @stdout.setter + def stdout(self, newout): + self.__class__._stdout = newout + + def __init__(self, attributes=None, fgcolor=None, bgcolor=None, reset=False): + """This is usually initialized from a factory.""" + if isinstance(attributes, type(self)): + for item in ('attributes','fg', 'bg', 'isreset'): + setattr(self, item, copy(getattr(attributes, item))) + return + self.attributes = attributes if attributes is not None else dict() + self.fg = fgcolor + self.bg = bgcolor + self.isreset = reset + invalid_attributes = set(self.attributes) - set(self.attribute_names) + if len(invalid_attributes) > 0: + raise AttributeNotFound("Attribute(s) not valid: " + ", ".join(invalid_attributes)) + + @classmethod + def from_color(cls, color): + if color.fg: + self = cls(fgcolor=color) + else: + self = cls(bgcolor=color) + return self + + + def invert(self): + """This resets current color(s) and flips the value of all + attributes present""" + + other = self.__class__() + + # Opposite of reset is reset + if self.isreset: + other.isreset = True + return other + + # Flip all attributes + for attribute in self.attributes: + other.attributes[attribute] = not self.attributes[attribute] + + # Reset only if color present + if self.fg: + other.fg = self.fg.__class__() + + if self.bg: + other.bg = self.bg.__class__() + + return other + + @property + def reset(self): + """Shortcut to access reset as a property.""" + return self.invert() + + def __copy__(self): + """Copy is supported, will make dictionary and colors unique.""" + result = self.__class__() + result.isreset = self.isreset + result.fg = copy(self.fg) + result.bg = copy(self.bg) + result.attributes = copy(self.attributes) + return result + + def __invert__(self): + """This allows ~color.""" + return self.invert() + + def __add__(self, other): + """Adding two matching Styles results in a new style with + the combination of both. Adding with a string results in + the string concatenation of a style. + + Addition is non-commutative, with the rightmost Style property + being taken if both have the same property. + (Not safe)""" + if type(self) == type(other): + result = copy(other) + + result.isreset = self.isreset or other.isreset + for attribute in self.attributes: + if attribute not in result.attributes: + result.attributes[attribute] = self.attributes[attribute] + if not result.fg: + result.fg = self.fg + if not result.bg: + result.bg = self.bg + return result + else: + return other.__class__(self) + other + + def __radd__(self, other): + """This only gets called if the string is on the left side. (Not safe)""" + return other + other.__class__(self) + + def wrap(self, wrap_this): + """Wrap a sting in this style and its inverse.""" + return self + wrap_this + ~self + + def __and__(self, other): + """This class supports ``color & color2`` syntax, + and ``color & "String" syntax too.``""" + if type(self) == type(other): + return self + other + else: + return self.wrap(other) + + def __rand__(self, other): + """This class supports ``"String:" & color`` syntax, excpet in Python 2.6 due to bug with that Python.""" + return self.wrap(other) + + def __ror__(self, other): + """Support for "String" | color syntax""" + return self.wrap(other) + + def __or__(self, other): + """This class supports ``color | color2`` syntax. It also supports + ``"color | "String"`` syntax too. """ + return self.__and__(other) + + def __call__(self): + """\ + This is a shortcut to print color immediately to the stdout. (Not safe) + """ + + self.now() + + def now(self): + '''Immediately writes color to stdout. (Not safe)''' + self.stdout.write(str(self)) + + def print(self, *printables, **kargs): + """\ + This acts like print; will print that argument to stdout wrapped + in Style with the same syntax as the print function in 3.4.""" + + end = kargs.get('end', self.end) + sep = kargs.get('sep', ' ') + file = kargs.get('file', self.stdout) + flush = kargs.get('flush', False) + file.write(self.wrap(sep.join(map(str,printables))) + end) + if flush: + file.flush() + + + print_ = print + """Shortcut just in case user not using __future__""" + + def __getitem__(self, wrapped): + """The [] syntax is supported for wrapping""" + return self.wrap(wrapped) + + def __enter__(self): + """Context manager support""" + self.stdout.write(str(self)) + + def __exit__(self, type, value, traceback): + """Runs even if exception occurred, does not catch it.""" + self.stdout.write(str(~self)) + return False + + @property + def ansi_codes(self): + """Generates the full ANSI code sequence for a Style""" + + if self.isreset: + return [0] + + codes = [] + for attribute in self.attributes: + if self.attributes[attribute]: + codes.append(attributes_ansi[attribute]) + else: + # Fixing bold inverse being 22 instead of 21 on some terminals: + codes.append(attributes_ansi[attribute] + + 20 if attributes_ansi[attribute]!=1 else 22 ) + + if self.fg: + codes.extend(self.fg.ansi_codes) + + if self.bg: + self.bg.fg = False + codes.extend(self.bg.ansi_codes) + + return codes + + @property + def ansi_sequence(self): + """This is the string ANSI sequence.""" + codes = self.ansi_codes + if codes: + return '\033[' + ';'.join(map(str, self.ansi_codes)) + 'm' + else: + return '' + + def __repr__(self): + name = self.__class__.__name__ + attributes = ', '.join(a for a in self.attributes if self.attributes[a]) + neg_attributes = ', '.join('-'+a for a in self.attributes if not self.attributes[a]) + colors = ', '.join(repr(c) for c in [self.fg, self.bg] if c) + string = '; '.join(s for s in [attributes, neg_attributes, colors] if s) + if self.isreset: + string = 'reset' + return "<{0}: {1}>".format(name, string if string else 'empty') + + def __eq__(self, other): + """Equality is true only if reset, or if attributes, fg, and bg match.""" + if type(self) == type(other): + if self.isreset: + return other.isreset + else: + return (self.attributes == other.attributes + and self.fg == other.fg + and self.bg == other.bg) + else: + return str(self) == other + + @abstractmethod + def __str__(self): + """Base Style does not implement a __str__ representation. This is the one + required method of a subclass.""" + + + @classmethod + def from_ansi(cls, ansi_string, filter_resets = False): + """This generated a style from an ansi string. Will ignore resets if filter_resets is True.""" + result = cls() + res = cls.ANSI_REG.search(ansi_string) + for group in res.groups(): + sequence = map(int,group.split(';')) + result.add_ansi(sequence, filter_resets) + return result + + def add_ansi(self, sequence, filter_resets = False): + """Adds a sequence of ansi numbers to the class. Will ignore resets if filter_resets is True.""" + + values = iter(sequence) + try: + while True: + value = next(values) + if value == 38 or value == 48: + fg = value == 38 + value = next(values) + if value == 5: + value = next(values) + if fg: + self.fg = self.color_class.from_full(value) + else: + self.bg = self.color_class.from_full(value, fg=False) + elif value == 2: + r = next(values) + g = next(values) + b = next(values) + if fg: + self.fg = self.color_class(r, g, b) + else: + self.bg = self.color_class(r, g, b, fg=False) + else: + raise ColorNotFound("the value 5 or 2 should follow a 38 or 48") + elif value==0: + if filter_resets is False: + self.isreset = True + elif value in attributes_ansi.values(): + for name in attributes_ansi: + if value == attributes_ansi[name]: + self.attributes[name] = True + elif value in (20+n for n in attributes_ansi.values()): + if filter_resets is False: + for name in attributes_ansi: + if value == attributes_ansi[name] + 20: + self.attributes[name] = False + elif 30 <= value <= 37: + self.fg = self.color_class.from_simple(value-30) + elif 40 <= value <= 47: + self.bg = self.color_class.from_simple(value-40, fg=False) + elif 90 <= value <= 97: + self.fg = self.color_class.from_simple(value-90+8) + elif 100 <= value <= 107: + self.bg = self.color_class.from_simple(value-100+8, fg=False) + elif value == 39: + if filter_resets is False: + self.fg = self.color_class() + elif value == 49: + if filter_resets is False: + self.bg = self.color_class(fg=False) + else: + raise ColorNotFound("The code {0} is not recognised".format(value)) + except StopIteration: + return + + @classmethod + def string_filter_ansi(cls, colored_string): + """Filters out colors in a string, returning only the name.""" + return cls.ANSI_REG.sub('', colored_string) + + @classmethod + def string_contains_colors(cls, colored_string): + """Checks to see if a string contains colors.""" + return len(cls.ANSI_REG.findall(colored_string)) > 0 + + + def to_representation(self, rep): + """This converts both colors to a specific representation""" + other = copy(self) + if other.fg: + other.fg = other.fg.to_representation(rep) + if other.bg: + other.bg = other.bg.to_representation(rep) + return other + + def limit_representation(self, rep): + """This only converts if true representation is higher""" + + if rep is True or rep is False: + return self + + other = copy(self) + if other.fg: + other.fg = other.fg.limit_representation(rep) + if other.bg: + other.bg = other.bg.limit_representation(rep) + return other + + + @property + def basic(self): + """The color in the 8 color representation.""" + return self.to_representation(1) + + @property + def simple(self): + """The color in the 16 color representation.""" + return self.to_representation(2) + + @property + def full(self): + """The color in the 256 color representation.""" + return self.to_representation(3) + + @property + def true(self): + """The color in the true color representation.""" + return self.to_representation(4) + +class ANSIStyle(Style): + """This is a subclass for ANSI styles. Use it to get + color on sys.stdout tty terminals on posix systems. + + Set ``use_color = True/False`` if you want to control color + for anything using this Style.""" + + __slots__ = () + use_color = get_color_repr() + + attribute_names = attributes_ansi + + def __str__(self): + if not self.use_color: + return '' + else: + return self.limit_representation(self.use_color).ansi_sequence + +class HTMLStyle(Style): + """This was meant to be a demo of subclassing Style, but + actually can be a handy way to quickly color html text.""" + + __slots__ = () + attribute_names = dict(bold='b', em='em', italics='i', li='li', underline='span style="text-decoration: underline;"', code='code', ol='ol start=0', strikeout='s') + end = '
\n' + + def __str__(self): + + if self.isreset: + raise ResetNotSupported("HTML does not support global resets!") + + result = '' + + if self.bg and not self.bg.isreset: + result += ''.format(self.bg.hex_code) + if self.fg and not self.fg.isreset: + result += ''.format(self.fg.hex_code) + for attr in sorted(self.attributes): + if self.attributes[attr]: + result += '<' + self.attribute_names[attr] + '>' + + for attr in reversed(sorted(self.attributes)): + if not self.attributes[attr]: + result += '' + if self.fg and self.fg.isreset: + result += '' + if self.bg and self.bg.isreset: + result += '' + + return result diff -Nru python-plumbum-1.5.0/plumbum/colors.py python-plumbum-1.6.0/plumbum/colors.py --- python-plumbum-1.5.0/plumbum/colors.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/colors.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,23 @@ +""" +This module imitates a real module, providing standard syntax +like from `plumbum.colors` and from `plumbum.colors.bg` to work alongside +all the standard syntax for colors. +""" + +from __future__ import print_function +import sys +import os +import atexit + +from plumbum.colorlib import ansicolors, main +_reset = ansicolors.reset.now +if __name__ == '__main__': + main() +else: # Don't register an exit if this is called using -m! + atexit.register(_reset) + +# Oddly, the order here matters for Python2, but not Python3 +sys.modules[__name__ + '.fg'] = ansicolors.fg +sys.modules[__name__ + '.bg'] = ansicolors.bg +sys.modules[__name__] = ansicolors + diff -Nru python-plumbum-1.5.0/plumbum/commands/base.py python-plumbum-1.6.0/plumbum/commands/base.py --- python-plumbum-1.5.0/plumbum/commands/base.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/commands/base.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement import subprocess import functools from contextlib import contextmanager @@ -135,6 +134,10 @@ :returns: A ``Popen``-like object """ raise NotImplementedError() + + def nohup(self, command, cwd='.', stdout='nohup.out', stderr=None, append=True): + """Runs a command detached.""" + return self.machine.daemonic_popen(self, cwd, stdout, stderr, append) @contextmanager def bgrun(self, args = (), **kwargs): @@ -403,8 +406,13 @@ self.encoding = encoding self.cwd = None self.env = None + def __str__(self): return str(self.executable) + + def __repr__(self): + return "{0}({1})".format(type(self).__name__, self.executable) + def _get_encoding(self): return self.encoding @@ -426,7 +434,8 @@ # argv = [a.encode(self.encoding) for a in argv if isinstance(a, six.string_types)] return argv - + + diff -Nru python-plumbum-1.5.0/plumbum/commands/daemons.py python-plumbum-1.6.0/plumbum/commands/daemons.py --- python-plumbum-1.5.0/plumbum/commands/daemons.py 2013-08-02 16:39:46.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/commands/daemons.py 2015-10-16 16:54:14.000000000 +0000 @@ -7,8 +7,19 @@ import traceback from plumbum.commands.processes import ProcessExecutionError +class _fake_lock(object): + """Needed to allow normal os.exit() to work without error""" + def acquire(self, val): + return True + def release(self): + pass + +def posix_daemonize(command, cwd, stdout=None, stderr=None, append=True): + if stdout is None: + stdout = os.devnull + if stderr is None: + stderr = stdout -def posix_daemonize(command, cwd): MAX_SIZE = 16384 rfd, wfd = os.pipe() argv = command.formulate() @@ -21,8 +32,8 @@ os.setsid() os.umask(0) stdin = open(os.devnull, "r") - stdout = open(os.devnull, "w") - stderr = open(os.devnull, "w") + stdout = open(stdout, "a" if append else "w") + stderr = open(stderr, "a" if append else "w") signal.signal(signal.SIGHUP, signal.SIG_IGN) proc = command.popen(cwd = cwd, close_fds = True, stdin = stdin.fileno(), stdout = stdout.fileno(), stderr = stderr.fileno()) @@ -56,6 +67,7 @@ proc.pid = secondpid proc.universal_newlines = False proc._input = None + proc._waitpid_lock = _fake_lock() proc._communication_started = False proc.args = argv proc.argv = argv @@ -84,11 +96,15 @@ return proc -def win32_daemonize(command, cwd): +def win32_daemonize(command, cwd, stdout=None, stderr=None, append=True): + if stdout is None: + stdout = os.devnull + if stderr is None: + stderr = stdout DETACHED_PROCESS = 0x00000008 stdin = open(os.devnull, "r") - stdout = open(os.devnull, "w") - stderr = open(os.devnull, "w") + stdout = open(stdout, "a" if append else "w") + stderr = open(stderr, "a" if append else "w") return command.popen(cwd = cwd, stdin = stdin.fileno(), stdout = stdout.fileno(), stderr = stderr.fileno(), creationflags = subprocess.CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS) diff -Nru python-plumbum-1.5.0/plumbum/commands/__init__.py python-plumbum-1.6.0/plumbum/commands/__init__.py --- python-plumbum-1.5.0/plumbum/commands/__init__.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/commands/__init__.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,4 @@ from plumbum.commands.base import shquote, shquote_list, BaseCommand, ERROUT, ConcreteCommand -from plumbum.commands.modifiers import ExecutionModifier, Future, FG, BG, TF, RETCODE +from plumbum.commands.modifiers import ExecutionModifier, Future, FG, BG, TF, RETCODE, NOHUP from plumbum.commands.processes import run_proc from plumbum.commands.processes import ProcessExecutionError, ProcessTimedOut, CommandNotFound diff -Nru python-plumbum-1.5.0/plumbum/commands/modifiers.py python-plumbum-1.6.0/plumbum/commands/modifiers.py --- python-plumbum-1.5.0/plumbum/commands/modifiers.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/commands/modifiers.py 2015-10-16 16:54:14.000000000 +0000 @@ -2,23 +2,12 @@ from select import select from subprocess import PIPE import sys +from itertools import chain from plumbum.commands.processes import run_proc, ProcessExecutionError +from plumbum.commands.base import AppendingStdoutRedirection, StdoutRedirection -#=================================================================================================== -# execution modifiers (background, foreground) -#=================================================================================================== -class ExecutionModifier(object): - __slots__ = ["retcode"] - def __init__(self, retcode = 0): - self.retcode = retcode - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, self.retcode) - @classmethod - def __call__(cls, retcode): - return cls(retcode) - class Future(object): """Represents a "future result" of a running process. It basically wraps a ``Popen`` object and the expected exit code, and provides poll(), wait(), returncode, stdout, @@ -63,6 +52,29 @@ self.wait() return self._returncode +#=================================================================================================== +# execution modifiers +#=================================================================================================== + + +class ExecutionModifier(object): + __slots__ = () + + def __repr__(self): + """Automatically creates a representation for given subclass with slots. + Ignore hidden properties.""" + slots = {} + for cls in self.__mro__: + for prop in getattr(cls, "__slots__", ()): + if prop[0] != '_': + slots[prop] = getattr(self, prop) + mystrs = ("{0} = {1}".format(name, slots[name]) for name in slots) + return "{0}({1})".format(self.__class__.__name__, ", ".join(mystrs)) + + @classmethod + def __call__(cls, *args, **kwargs): + return cls(*args, **kwargs) + class BG(ExecutionModifier): """ An execution modifier that runs the given command in the background, returning a @@ -81,11 +93,16 @@ every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ """ - __slots__ = [] + __slots__ = ("retcode",) + + def __init__(self, retcode=0): + self.retcode = retcode + def __rand__(self, cmd): return Future(cmd.popen(), self.retcode) BG = BG() + """ An execution modifier that runs the given command in the background, returning a :class:`Future ` object. In order to mimic shell syntax, it applies @@ -117,7 +134,11 @@ vim & FG # run vim in the foreground, expecting an exit code of 0 vim & FG(7) # run vim in the foreground, expecting an exit code of 7 """ - __slots__ = [] + __slots__ = ("retcode",) + + def __init__(self, retcode=0): + self.retcode = retcode + def __rand__(self, cmd): cmd(retcode = self.retcode, stdin = None, stdout = None, stderr = None) @@ -135,6 +156,9 @@ Returns a tuple of (return code, stdout, stderr), just like ``run()``. """ + + __slots__ = ("retcode", "buffered") + def __init__(self, retcode=0, buffered=True): """`retcode` is the return code to expect to mean "success". Set `buffered` to False to disable line-buffering the output, which may @@ -143,9 +167,6 @@ self.retcode = retcode self.buffered = buffered - @classmethod - def __call__(cls, *args, **kwargs): - return cls(*args, **kwargs) def __rand__(self, cmd): with cmd.bgrun(retcode=self.retcode, stdin=None, stdout=PIPE, stderr=PIPE) as p: @@ -195,12 +216,14 @@ local['touch']['/root/test'] & TF(FG=True) * Returns False, will show error message """ + __slots__ = ("retcode", "FG") + def __init__(self, retcode=0, FG=False): """`retcode` is the return code to expect to mean "success". Set `FG` to True to run in the foreground. """ self.retcode = retcode - self.foreground = FG + self.FG = FG @classmethod def __call__(cls, *args, **kwargs): @@ -208,7 +231,7 @@ def __rand__(self, cmd): try: - if self.foreground: + if self.FG: cmd(retcode = self.retcode, stdin = None, stdout = None, stderr = None) else: cmd(retcode = self.retcode) @@ -233,6 +256,8 @@ local['touch']['/root/test'] & RETCODE(FG=True) * Returns 1, will show error message """ + __slots__ = ("foreground",) + def __init__(self, FG=False): """`FG` to True to run in the foreground. """ @@ -250,3 +275,53 @@ RETCODE = RETCODE() + +class NOHUP(ExecutionModifier): + """ + An execution modifier that runs the given command in the background, disconnected + from the current process, returning a + standard popen object. It will keep running even if you close the current process. + In order to slightly mimic shell syntax, it applies + when you right-and it with a command. If you wish to use a diffent working directory + or different stdout, stderr, you can use named arguments. The default is ``NOHUP( + cwd=local.cwd, stdout='nohup.out', stderr=None)``. If stderr is None, stderr will be + sent to stdout. Use ``os.devnull`` for null output. Will respect redirected output. + Example:: + + sleep[5] & NOHUP # Outputs to nohup.out + sleep[5] & NOHUP(stdout=os.devnull) # No output + + The equivelent bash command would be + + .. code-block:: bash + + nohup sleep 5 & + + """ + __slots__ = ('cwd', 'stdout', 'stderr', 'append') + + def __init__(self, cwd='.', stdout='nohup.out', stderr=None, append=True): + """ Set ``cwd``, ``stdout``, or ``stderr``. + Runs as a forked process. You can set ``append=False``, too. + """ + self.cwd = cwd + self.stdout = stdout + self.stderr = stderr + self.append = append + + + def __rand__(self, cmd): + if isinstance(cmd, StdoutRedirection): + stdout = cmd.file + append = False + cmd = cmd.cmd + elif isinstance(cmd, AppendingStdoutRedirection): + stdout = cmd.file + append = True + cmd = cmd.cmd + else: + stdout = self.stdout + append = self.append + return cmd.nohup(cmd, self.cwd, stdout, self.stderr, append) + +NOHUP = NOHUP() diff -Nru python-plumbum-1.5.0/plumbum/commands/processes.py python-plumbum-1.6.0/plumbum/commands/processes.py --- python-plumbum-1.5.0/plumbum/commands/processes.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/commands/processes.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement import time import atexit import heapq @@ -17,42 +16,6 @@ from cStringIO import StringIO -if not hasattr(Popen, "kill"): - # python 2.5 compatibility - import os - import sys - import signal - if IS_WIN32: - import _subprocess - - def _Popen_terminate(self): - """taken from subprocess.py of python 2.7""" - try: - _subprocess.TerminateProcess(self._handle, 1) - except OSError: - ex = sys.exc_info()[1] - # ERROR_ACCESS_DENIED (winerror 5) is received when the - # process already died. - if ex.winerror != 5: - raise - rc = _subprocess.GetExitCodeProcess(self._handle) - if rc == _subprocess.STILL_ACTIVE: - raise - self.returncode = rc - - Popen.kill = _Popen_terminate - Popen.terminate = _Popen_terminate - else: - def _Popen_kill(self): - os.kill(self.pid, signal.SIGKILL) - def _Popen_terminate(self): - os.kill(self.pid, signal.SIGTERM) - def _Popen_send_signal(self, sig): - os.kill(self.pid, sig) - Popen.kill = _Popen_kill - Popen.terminate = _Popen_kill - Popen.send_signal = _Popen_send_signal - #=================================================================================================== # utility functions #=================================================================================================== @@ -138,7 +101,7 @@ Exception.__init__(self, msg, argv) self.argv = argv -class CommandNotFound(Exception): +class CommandNotFound(AttributeError): """Raised by :func:`local.which ` and :func:`RemoteMachine.which ` when a command was not found in the system's ``PATH``""" diff -Nru python-plumbum-1.5.0/plumbum/fs/atomic.py python-plumbum-1.6.0/plumbum/fs/atomic.py --- python-plumbum-1.5.0/plumbum/fs/atomic.py 2014-02-27 21:29:30.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/fs/atomic.py 2015-10-16 16:54:14.000000000 +0000 @@ -2,7 +2,6 @@ Atomic file operations """ -from __future__ import with_statement import os import threading import sys @@ -31,12 +30,12 @@ from win32con import LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY except ImportError: raise ImportError("On Windows, we require Python for Windows Extensions (pywin32)") - + @contextmanager def locked_file(fileno, blocking = True): hndl = msvcrt.get_osfhandle(fileno) try: - LockFileEx(hndl, LOCKFILE_EXCLUSIVE_LOCK | (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), + LockFileEx(hndl, LOCKFILE_EXCLUSIVE_LOCK | (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), 0xffffffff, 0xffffffff, OVERLAPPED()) except WinError: _, ex, _ = sys.exc_info() @@ -77,7 +76,7 @@ """ CHUNK_SIZE = 32 * 1024 - + def __init__(self, filename, ignore_deletion = False): self.path = local.path(filename) self._ignore_deletion = ignore_deletion @@ -95,12 +94,12 @@ return self def __exit__(self, t, v, tb): self.close() - + def close(self): if self._fileobj is not None: self._fileobj.close() self._fileobj = None - + def reopen(self): """ Close and reopen the file; useful when the file was deleted from the file system @@ -148,7 +147,7 @@ if len(buf) < self.CHUNK_SIZE: break return six.b("").join(data) - + def read_atomic(self): """Atomically read the entire file""" with self.locked(): diff -Nru python-plumbum-1.5.0/plumbum/__init__.py python-plumbum-1.6.0/plumbum/__init__.py --- python-plumbum-1.5.0/plumbum/__init__.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/__init__.py 2015-10-16 16:54:14.000000000 +0000 @@ -35,7 +35,7 @@ See http://plumbum.readthedocs.org for full details """ from plumbum.commands import ProcessExecutionError, CommandNotFound, ProcessTimedOut -from plumbum.commands import FG, BG, TF, RETCODE, ERROUT +from plumbum.commands import FG, BG, TF, RETCODE, ERROUT, NOHUP from plumbum.path import Path, LocalPath, RemotePath from plumbum.machines import local, BaseRemoteMachine, SshMachine, PuttyMachine from plumbum.version import version diff -Nru python-plumbum-1.5.0/plumbum/lib.py python-plumbum-1.6.0/plumbum/lib.py --- python-plumbum-1.5.0/plumbum/lib.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/lib.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,9 +1,13 @@ import sys - +from contextlib import contextmanager +from abc import ABCMeta +import inspect IS_WIN32 = (sys.platform == "win32") def _setdoc(super): # @ReservedAssignment + """This inherits the docs on the current class. Not really needed for Python 3.5, + due to new behavoir of inspect.getdoc, but still doesn't hurt.""" def deco(func): func.__doc__ = getattr(getattr(super, func.__name__, None), "__doc__", None) return func @@ -23,7 +27,14 @@ A light-weight version of six (which works on IronPython) """ PY3 = sys.version_info[0] >= 3 - + ABC = ABCMeta('ABC', (object,), {'__module__':__name__, '__slots__':()}) + + # Be sure to use named-tuple access, so that usage is not affected + try: + getfullargspec = staticmethod(inspect.getfullargspec) + except AttributeError: + getfullargspec = staticmethod(inspect.getargspec) # extra fields will not be available + if PY3: integer_types = (int,) string_types = (str,) @@ -31,7 +42,7 @@ ascii = ascii # @UndefinedVariable bytes = bytes # @ReservedAssignment unicode_type = str - + @staticmethod def b(s): return s.encode("latin-1", "replace") @@ -59,3 +70,50 @@ def get_method_function(m): return m.im_func +# Try/except fails because io has the wrong StringIO in Python2 +# You'll get str/unicode errors +if six.PY3: + from io import StringIO +else: + from StringIO import StringIO + + +@contextmanager +def captured_stdout(stdin = ""): + """ + Captures stdout (similar to the redirect_stdout in Python 3.4+, but with slightly different arguments) + """ + prevstdin = sys.stdin + prevstdout = sys.stdout + sys.stdin = StringIO(six.u(stdin)) + sys.stdout = StringIO() + try: + yield sys.stdout + finally: + sys.stdin = prevstdin + sys.stdout = prevstdout + +class StaticProperty(object): + """This acts like a static property, allowing access via class or object. + This is a non-data descriptor.""" + def __init__(self, function): + self._function = function + self.__doc__ = function.__doc__ + + def __get__(self, obj, klass=None): + return self._function() + + +def getdoc(object): + """ + This gets a docstring if avaiable, and cleans it, but does not look up docs in + inheritance tree (Pre 3.5 behavior of ``inspect.getdoc``). + """ + try: + doc = object.__doc__ + except AttributeError: + return None + if not isinstance(doc, str): + return None + return inspect.cleandoc(doc) + diff -Nru python-plumbum-1.5.0/plumbum/machines/base.py python-plumbum-1.6.0/plumbum/machines/base.py --- python-plumbum-1.5.0/plumbum/machines/base.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/base.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,47 @@ +from plumbum.commands.processes import CommandNotFound + + +class BaseMachine(object): + """This is a base class for other machines. It contains common code to + all machines in Plumbum.""" + + + def get(self, cmd, *othercommands): + """This works a little like the ``.get`` method with dict's, only + it supports an unlimited number of arguments, since later arguments + are tried as commands and could also fail. It + will try to call the first command, and if that is not found, + it will call the next, etc. Will raise if no file named for the + executable if a path is given, unlike ``[]`` access. + + Usage:: + + best_zip = local.get('pigz','gzip') + """ + try: + command = self[cmd] + if not command.executable.exists(): + raise CommandNotFound(cmd,command.executable) + else: + return command + except CommandNotFound: + if othercommands: + return self.get(othercommands[0],*othercommands[1:]) + else: + raise + + def __contains__(self, cmd): + """Tests for the existance of the command, e.g., ``"ls" in plumbum.local``. + ``cmd`` can be anything acceptable by ``__getitem__``. + """ + try: + self[cmd] + except CommandNotFound: + return False + else: + return True + + def daemonic_popen(self, command, cwd = "/", stdout=None, stderr=None, append=True): + raise NotImplementedError("This is not implemented on this machine!") + + diff -Nru python-plumbum-1.5.0/plumbum/machines/local.py python-plumbum-1.6.0/plumbum/machines/local.py --- python-plumbum-1.5.0/plumbum/machines/local.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/local.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement import os import sys import subprocess @@ -13,9 +12,10 @@ from plumbum.path.remote import RemotePath from plumbum.commands import CommandNotFound, ConcreteCommand from plumbum.machines.session import ShellSession -from plumbum.lib import ProcInfo, IS_WIN32, six +from plumbum.lib import ProcInfo, IS_WIN32, six, StaticProperty from plumbum.commands.daemons import win32_daemonize, posix_daemonize from plumbum.commands.processes import iter_lines +from plumbum.machines.base import BaseMachine from plumbum.machines.env import BaseEnv if sys.version_info >= (3, 2): @@ -94,8 +94,6 @@ def __init__(self, executable, encoding = "auto"): ConcreteCommand.__init__(self, executable, local.encoding if encoding == "auto" else encoding) - def __repr__(self): - return "LocalCommand(%r)" % (self.executable,) @property def machine(self): @@ -104,14 +102,16 @@ def popen(self, args = (), cwd = None, env = None, **kwargs): if isinstance(args, six.string_types): args = (args,) - return local._popen(self.executable, self.formulate(0, args), + return self.machine._popen(self.executable, self.formulate(0, args), cwd = self.cwd if cwd is None else cwd, env = self.env if env is None else env, **kwargs) #=================================================================================================== # Local Machine #=================================================================================================== -class LocalMachine(object): + + +class LocalMachine(BaseMachine): """The *local machine* (a singleton object). It serves as an entry point to everything related to the local machine, such as working directory and environment manipulation, command creation, etc. @@ -122,8 +122,10 @@ * ``env`` - the local environment * ``encoding`` - the local machine's default encoding (``sys.getfilesystemencoding()``) """ - cwd = LocalWorkdir() + + cwd = StaticProperty(LocalWorkdir) env = LocalEnv() + encoding = sys.getfilesystemencoding() uname = platform.uname()[0] @@ -192,6 +194,7 @@ ls = local["ls"] """ + if isinstance(cmd, LocalPath): return LocalCommand(cmd) elif not isinstance(cmd, RemotePath): @@ -204,23 +207,12 @@ else: raise TypeError("cmd must not be a RemotePath: %r" % (cmd,)) - def __contains__(self, cmd): - """Tests for the existance of the command, e.g., ``"ls" in plumbum.local``. - ``cmd`` can be anything acceptable by ``__getitem__``. - """ - try: - self[cmd] - except CommandNotFound: - return False - else: - return True - def _popen(self, executable, argv, stdin = PIPE, stdout = PIPE, stderr = PIPE, cwd = None, env = None, new_session = False, **kwargs): if new_session: if has_new_subprocess: kwargs["start_new_session"] = True - elif subprocess.mswindows: + elif IS_WIN32: kwargs["creationflags"] = kwargs.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP else: def preexec_fn(prev_fn = kwargs.get("preexec_fn", lambda: None)): @@ -228,7 +220,7 @@ prev_fn() kwargs["preexec_fn"] = preexec_fn - if subprocess.mswindows and "startupinfo" not in kwargs and stdin not in (sys.stdin, None): + if IS_WIN32 and "startupinfo" not in kwargs and stdin not in (sys.stdin, None): from plumbum.machines._windows import get_pe_subsystem, IMAGE_SUBSYSTEM_WINDOWS_CUI subsystem = get_pe_subsystem(str(executable)) @@ -244,7 +236,7 @@ sui.wShowWindow = subprocess.SW_HIDE # @UndefinedVariable if not has_new_subprocess and "close_fds" not in kwargs: - if subprocess.mswindows and (stdin is not None or stdout is not None or stderr is not None): + if IS_WIN32 and (stdin is not None or stdout is not None or stderr is not None): # we can't close fds if we're on windows and we want to redirect any std handle kwargs["close_fds"] = False else: @@ -268,7 +260,8 @@ proc.argv = argv return proc - def daemonic_popen(self, command, cwd = "/"): + + def daemonic_popen(self, command, cwd = "/", stdout=None, stderr=None, append=True): """ On POSIX systems: @@ -287,9 +280,9 @@ .. versionadded:: 1.3 """ if IS_WIN32: - return win32_daemonize(command, cwd) + return win32_daemonize(command, cwd, stdout, stderr, append) else: - return posix_daemonize(command, cwd) + return posix_daemonize(command, cwd, stdout, stderr, append) if IS_WIN32: def list_processes(self): @@ -300,16 +293,16 @@ """ import csv tasklist = local["tasklist"] - lines = tasklist("/V", "/FO", "CSV").encode("utf8").splitlines() + lines = tasklist("/V", "/FO", "CSV").splitlines() rows = csv.reader(lines) - header = rows.next() + header = next(rows) imgidx = header.index('Image Name') pididx = header.index('PID') statidx = header.index('Status') useridx = header.index('User Name') for row in rows: - yield ProcInfo(int(row[pididx]), row[useridx].decode("utf8"), - row[statidx].decode("utf8"), row[imgidx].decode("utf8")) + yield ProcInfo(int(row[pididx]), row[useridx], + row[statidx], row[imgidx]) else: def list_processes(self): """ diff -Nru python-plumbum-1.5.0/plumbum/machines/paramiko_machine.py python-plumbum-1.6.0/plumbum/machines/paramiko_machine.py --- python-plumbum-1.5.0/plumbum/machines/paramiko_machine.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/paramiko_machine.py 2015-10-16 16:54:14.000000000 +0000 @@ -249,13 +249,13 @@ dst if isinstance(dst, LocalPath) else LocalPath(dst)) def _download(self, src, dst): - if src.isdir(): + if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: - self._download(fn, dst / fn.basename) - elif dst.isdir(): - self.sftp.get(str(src), str(dst / src.basename)) + self._download(fn, dst / fn.name) + elif dst.is_dir(): + self.sftp.get(str(src), str(dst / src.name)) else: self.sftp.get(str(src), str(dst)) @@ -271,13 +271,13 @@ dst if isinstance(dst, RemotePath) else self.path(dst)) def _upload(self, src, dst): - if src.isdir(): + if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: - self._upload(fn, dst / fn.basename) - elif dst.isdir(): - self.sftp.put(str(src), str(dst / src.basename)) + self._upload(fn, dst / fn.name) + elif dst.is_dir(): + self.sftp.put(str(src), str(dst / src.name)) else: self.sftp.put(str(src), str(dst)) diff -Nru python-plumbum-1.5.0/plumbum/machines/remote.py python-plumbum-1.6.0/plumbum/machines/remote.py --- python-plumbum-1.5.0/plumbum/machines/remote.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/remote.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,10 +1,10 @@ -from __future__ import with_statement import re from contextlib import contextmanager from plumbum.commands import CommandNotFound, shquote, ConcreteCommand from plumbum.lib import _setdoc, ProcInfo, six from plumbum.machines.local import LocalPath from tempfile import NamedTemporaryFile +from plumbum.machines.base import BaseMachine from plumbum.machines.env import BaseEnv from plumbum.path.remote import RemotePath, RemoteWorkdir, StatRes @@ -113,6 +113,9 @@ return "RemoteCommand(%r, %r)" % (self.remote, self.executable) def popen(self, args = (), **kwargs): return self.remote.popen(self[args], **kwargs) + def nohup(self, cwd='.', stdout='nohup.out', stderr=None, append=True): + """Runs a command detached.""" + return machine.nohup(self, cwd, stdout, stderr, append) class ClosedRemoteMachine(Exception): pass @@ -127,7 +130,7 @@ raise ClosedRemoteMachine("%r has been closed" % (self._obj,)) -class BaseRemoteMachine(object): +class BaseRemoteMachine(BaseMachine): """Represents a *remote machine*; serves as an entry point to everything related to that remote machine, such as working directory and environment manipulation, command creation, etc. @@ -142,14 +145,16 @@ # allow inheritors to override the RemoteCommand class RemoteCommand = RemoteCommand - + + @property + def cwd(self): + return RemoteWorkdir(self) def __init__(self, encoding = "utf8", connect_timeout = 10, new_session = False): self.encoding = encoding self.connect_timeout = connect_timeout self._session = self.session(new_session = new_session) self.uname = self._get_uname() - self.cwd = RemoteWorkdir(self) self.env = RemoteEnv(self) self._python = None @@ -236,17 +241,6 @@ else: raise TypeError("cmd must not be a LocalPath: %r" % (cmd,)) - def __contains__(self, cmd): - """Tests for the existance of the command, e.g., ``"ls" in remote_machine``. - ``cmd`` can be anything acceptable by ``__getitem__``. - """ - try: - self[cmd] - except CommandNotFound: - return False - else: - return True - @property def python(self): """A command that represents the default remote python interpreter""" @@ -327,13 +321,13 @@ return matches def _path_getuid(self, fn): - stat_cmd = "stat -c '%u,%U' " if self.uname != 'Darwin' else "stat -f '%u,%Su' " + stat_cmd = "stat -c '%u,%U' " if self.uname not in ('Darwin', 'FreeBSD') else "stat -f '%u,%Su' " return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_getgid(self, fn): - stat_cmd = "stat -c '%g,%G' " if self.uname != 'Darwin' else "stat -f '%g,%Sg' " + stat_cmd = "stat -c '%g,%G' " if self.uname not in ('Darwin', 'FreeBSD') else "stat -f '%g,%Sg' " return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_stat(self, fn): - if self.uname != 'Darwin': + if self.uname not in ('Darwin', 'FreeBSD'): stat_cmd = "stat -c '%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z' " else: stat_cmd = "stat -f '%HT,%Xp,%i,%d,%l,%u,%g,%z,%a,%m,%c' " diff -Nru python-plumbum-1.5.0/plumbum/machines/session.py python-plumbum-1.6.0/plumbum/machines/session.py --- python-plumbum-1.5.0/plumbum/machines/session.py 2013-09-27 14:13:55.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/session.py 2015-10-16 16:54:14.000000000 +0000 @@ -11,6 +11,13 @@ :func:`ShellSession.popen `""" pass +class SSHCommsError(EOFError): + """Raises when the communication channel can't be created on the + remote host or it times out.""" + +class SSHCommsChannel2Error(SSHCommsError): + """Raises when channel 2 (stderr) is not available""" + shell_logger = logging.getLogger("plumbum.shell") @@ -89,8 +96,14 @@ input = input[1000:] i = (i + 1) % len(sources) name, coll, pipe = sources[i] - line = pipe.readline() - shell_logger.debug("%s> %r", name, line) + try: + line = pipe.readline() + shell_logger.debug("%s> %r", name, line) + except EOFError: + shell_logger.debug("%s> Nothing returned.", name) + msg = "No communication channel detected. Does the remote exist?" + msgerr = "No stderr result detected. Does the remote have Bash as the default shell?" + raise SSHCommsChannel2Error(msgerr) if name=="2" else SSHCommsError(msg) if not line: del sources[i] else: diff -Nru python-plumbum-1.5.0/plumbum/machines/ssh_machine.py python-plumbum-1.6.0/plumbum/machines/ssh_machine.py --- python-plumbum-1.5.0/plumbum/machines/ssh_machine.py 2015-01-30 17:04:35.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/machines/ssh_machine.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement from plumbum.lib import _setdoc, IS_WIN32 from plumbum.machines.remote import BaseRemoteMachine from plumbum.machines.session import ShellSession @@ -6,6 +5,7 @@ from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath from plumbum.commands import ProcessExecutionError, shquote +import warnings class SshTunnel(object): @@ -61,7 +61,7 @@ NOTE: THIS IS A SECURITY RISK! :param encoding: the remote machine's encoding (defaults to UTF8) - + :param connect_timeout: specify a connection timeout (the time until shell prompt is seen). The default is 10 seconds. Set to ``None`` to disable @@ -127,13 +127,35 @@ def nohup(self, command): """ - Runs the given command using ``nohup`` and redirects std handles to ``/dev/null``, + Runs the given command using ``nohup`` and redirects std handles, + allowing the command to run "detached" from its controlling TTY or parent. + Does not return anything. Depreciated (use command.nohup or daemonic_popen). + """ + warnings.warn("Use .nohup on the command or use daemonic_popen)", DeprecationWarning) + self.daemonic_popen(command, cwd='.', stdout=None, stderr=None, append=False) + + def daemonic_popen(self, command, cwd='.', stdout=None, stderr=None, append=True): + """ + Runs the given command using ``nohup`` and redirects std handles, allowing the command to run "detached" from its controlling TTY or parent. Does not return anything. + + .. versionadded:: 1.6.0 + """ - args = ["nohup"] + if stdout is None: + stdout = "/dev/null" + if stderr is None: + stderr = "&1" + + if str(cwd) == '.': + args = [] + else: + args = ["cd", str(cwd), "&&"] + args.append("nohup") args.extend(command.formulate()) - args.extend([">/dev/null", "2>/dev/null", ">" if append else ">")+str(stdout), + "2"+(">>" if (append and stderr!="&1") else ">")+str(stderr), "` and :class:`RemotePath `. """ - __slots__ = [] CASE_SENSITIVE = True def __repr__(self): @@ -68,8 +69,9 @@ return bool(str(self)) __bool__ = __nonzero__ + @abstractmethod def _form(self, *parts): - raise NotImplementedError() + pass def up(self, count = 1): """Go up in ``count`` directories (the default is 1)""" @@ -78,8 +80,8 @@ """traverse all (recursive) sub-elements under this directory, that match the given filter. By default, the filter accepts everything; you can provide a custom filter function that takes a path as an argument and returns a boolean - - :param filter: the filter (predicate function) for matching results. Only paths matching + + :param filter: the filter (predicate function) for matching results. Only paths matching this predicate are returned. Defaults to everything. :param dir_filter: the filter (predicate function) for matching directories. Only directories matching this predicate are recursed into. Defaults to everything. @@ -87,150 +89,194 @@ for p in self.list(): if filter(p): yield p - if p.isdir() and dir_filter(p): + if p.is_dir() and dir_filter(p): for p2 in p.walk(filter, dir_filter): yield p2 - @property - def basename(self): + @abstractproperty + def name(self): """The basename component of this path""" - raise NotImplementedError() + @property + def basename(self): + """Included for compatibility with older Plumbum code""" + warnings.warn("Use .name instead", DeprecationWarning) + return self.name + + @abstractproperty + def stem(self): + """The name without an extension, or the last component of the path""" + + @abstractproperty def dirname(self): """The dirname component of this path""" - raise NotImplementedError() - - @property + + @abstractproperty + def root(self): + """The root of the file tree (`/` on Unix)""" + + @abstractproperty + def drive(self): + """The drive letter (on Windows)""" + + @abstractproperty def suffix(self): """The suffix of this file""" - raise NotImplementedError() - @property + @abstractproperty def suffixes(self): """This is a list of all suffixes""" - raise NotImplementedError() - - @property + + @abstractproperty def uid(self): """The user that owns this path. The returned value is a :class:`FSUser ` object which behaves like an ``int`` (as expected from ``uid``), but it also has a ``.name`` attribute that holds the string-name of the user""" - raise NotImplementedError() - @property + + @abstractproperty def gid(self): """The group that owns this path. The returned value is a :class:`FSUser ` object which behaves like an ``int`` (as expected from ``gid``), but it also has a ``.name`` attribute that holds the string-name of the group""" - raise NotImplementedError() + @abstractmethod + def as_uri(self, scheme=None): + """Returns a universal resource identifier. Use ``scheme`` to force a scheme.""" + + @abstractmethod def _get_info(self): - raise NotImplementedError() + pass + @abstractmethod def join(self, *parts): """Joins this path with any number of paths""" - raise NotImplementedError() + @abstractmethod def list(self): """Returns the files in this directory""" - raise NotImplementedError() - def isdir(self): + @abstractmethod + def iterdir(self): + """Returns an iterator over the directory. Might be slightly faster on Python 3.5 than .list()""" + @abstractmethod + def is_dir(self): """Returns ``True`` if this path is a directory, ``False`` otherwise""" - raise NotImplementedError() - def isfile(self): + def isdir(self): + """Included for compatibility with older Plumbum code""" + warnings.warn("Use .is_dir() instead", DeprecationWarning) + return self.is_dir() + @abstractmethod + def is_file(self): """Returns ``True`` if this path is a regular file, ``False`` otherwise""" - raise NotImplementedError() + def isfile(self): + """Included for compatibility with older Plumbum code""" + warnings.warn("Use .is_file() instead", DeprecationWarning) + return self.is_file() def islink(self): + """Included for compatibility with older Plumbum code""" + warnings.warn("Use is_symlink instead", DeprecationWarning) + return self.is_symlink() + @abstractmethod + def is_symlink(self): """Returns ``True`` if this path is a symbolic link, ``False`` otherwise""" - raise NotImplementedError() + @abstractmethod def exists(self): """Returns ``True`` if this path exists, ``False`` otherwise""" - raise NotImplementedError() + @abstractmethod def stat(self): - raise NotImplementedError() + """Returns the os.stats for a file""" + pass + @abstractmethod def with_name(self, name): """Returns a path with the name replaced""" - raise NotImplementedError() + @abstractmethod def with_suffix(self, suffix, depth=1): """Returns a path with the suffix replaced. Up to last ``depth`` suffixes will be replaces. None will replace all suffixes. If there are less than ``depth`` suffixes, this will replace all suffixes. ``.tar.gz`` is an example where ``depth=2`` or ``depth=None`` is useful""" - raise NotImplementedError() + + def preferred_suffix(self, suffix): + """Adds a suffix if one does not currently exist (otherwise, no change). Useful + for loading files with a default suffix""" + if len(self.suffixes) > 0: + return self + else: + return self.with_suffix(suffix) + @abstractmethod def glob(self, pattern): """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" - raise NotImplementedError() + @abstractmethod def delete(self): """Deletes this path (recursively, if a directory)""" - raise NotImplementedError() + @abstractmethod def move(self, dst): """Moves this path to a different location""" - raise NotImplementedError() def rename(self, newname): """Renames this path to the ``new name`` (only the basename is changed)""" return self.move(self.up() / newname) + @abstractmethod def copy(self, dst, override = False): """Copies this path (recursively, if a directory) to the destination path""" - raise NotImplementedError() + @abstractmethod def mkdir(self): """Creates a directory at this path; if the directory already exists, silently ignore""" - raise NotImplementedError() + @abstractmethod def open(self, mode = "r"): """opens this path as a file""" - raise NotImplementedError() + @abstractmethod def read(self, encoding=None): """returns the contents of this file. By default the data is binary (``bytes``), but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``""" - raise NotImplementedError() + @abstractmethod def write(self, data, encoding=None): - """writes the given data to this file. By default the data is expected to be binary (``bytes``), + """writes the given data to this file. By default the data is expected to be binary (``bytes``), but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``""" - raise NotImplementedError() + @abstractmethod def chown(self, owner = None, group = None, recursive = None): """Change ownership of this path. :param owner: The owner to set (either ``uid`` or ``username``), optional - :param owner: The group to set (either ``gid`` or ``groupname``), optional + :param group: The group to set (either ``gid`` or ``groupname``), optional :param recursive: whether to change ownership of all contained files and subdirectories. Only meaningful when ``self`` is a directory. If ``None``, the value will default to ``True`` if ``self`` is a directory, ``False`` otherwise. """ - raise NotImplementedError() + @abstractmethod def chmod(self, mode): """Change the mode of path to the numeric mode. :param mode: file mode as for os.chmod """ - raise NotImplementedError() @staticmethod def _access_mode_to_flags(mode, flags = {"f" : os.F_OK, "w" : os.W_OK, "r" : os.R_OK, "x" : os.X_OK}): if isinstance(mode, str): mode = reduce(operator.or_, [flags[m] for m in mode.lower()], 0) return mode - + + @abstractmethod def access(self, mode = 0): """Test file existence or permission bits - - :param mode: a bitwise-or of access bits, or a string-representation thereof: - ``'f'``, ``'x'``, ``'r'``, ``'w'`` for ``os.F_OK``, ``os.X_OK``, + + :param mode: a bitwise-or of access bits, or a string-representation thereof: + ``'f'``, ``'x'``, ``'r'``, ``'w'`` for ``os.F_OK``, ``os.X_OK``, ``os.R_OK``, ``os.W_OK`` """ - raise NotImplementedError() + @abstractmethod def link(self, dst): """Creates a hard link from ``self`` to ``dst`` :param dst: the destination path """ - raise NotImplementedError() + @abstractmethod def symlink(self, dst): """Creates a symbolic link from ``self`` to ``dst`` :param dst: the destination path """ - raise NotImplementedError() + @abstractmethod def unlink(self): """Deletes a symbolic link""" - raise NotImplementedError() def split(self): """Splits the path on directory separators, yielding a list of directories, e.g, @@ -239,10 +285,15 @@ parts = [] path = self while path != path.dirname: - parts.append(path.basename) + parts.append(path.name) path = path.dirname return parts[::-1] + @property + def parts(self): + """Splits the directory into parts, including the base directroy, returns a tuple""" + return tuple([self.root] + self.split()) + def relative_to(self, source): """Computes the "relative path" require to get from ``source`` to ``self``. They satisfy the invariant ``source_path + (target_path - source_path) == target_path``. For example:: @@ -265,16 +316,28 @@ """Same as ``self.relative_to(other)``""" return self.relative_to(other) + def _glob(self, pattern, fn): + """Applies a glob string or list/tuple/iterable to the current path, using ``fn``""" + if isinstance(pattern, str): + return fn(pattern) + else: + results = [] + for single_pattern in pattern: + results.extend(fn(single_pattern)) + return sorted(list(set(results))) + + class RelativePath(object): """ Relative paths are the "delta" required to get from one path to another. Note that relative path do not point at anything, and thus are not paths. - Therefore they are system agnostic (but closed under addition) + Therefore they are system agnostic (but closed under addition) Paths are always absolute and point at "something", whether existent or not. - + Relative paths are created by subtracting paths (``Path.relative_to``) """ + def __init__(self, parts): self.parts = parts def __str__(self): @@ -305,10 +368,10 @@ def __nonzero__(self): return bool(str(self)) __bool__ = __nonzero__ - + def up(self, count = 1): return RelativePath(self.parts[:-count]) - + def __radd__(self, path): return path.join(*self.parts) diff -Nru python-plumbum-1.5.0/plumbum/path/local.py python-plumbum-1.6.0/plumbum/path/local.py --- python-plumbum-1.5.0/plumbum/path/local.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/path/local.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement import os import sys import glob @@ -22,6 +21,14 @@ def getgrnam(x): raise OSError("`getgrnam` not supported") +try: # Py3 + import urllib.parse as urlparse + import urllib.request as urllib +except ImportError: + import urlparse + import urllib + + logger = logging.getLogger("plumbum.local") @@ -31,23 +38,23 @@ class LocalPath(Path): """The class implementing local-machine paths""" - __slots__ = ["_path"] CASE_SENSITIVE = not IS_WIN32 - def __init__(self, *parts): - if not parts: - raise TypeError("At least one path part is require (none given)") - if any(isinstance(path, RemotePath) for path in parts): - raise TypeError("LocalPath cannot be constructed from %r" % (parts,)) - self._path = os.path.normpath(os.path.join(*(str(p) for p in parts))) def __new__(cls, *parts): if len(parts) == 1 and \ isinstance(parts[0], cls) and \ not isinstance(parts[0], LocalWorkdir): return parts[0] - return object.__new__(cls) - def __str__(self): - return self._path + if not parts: + raise TypeError("At least one path part is require (none given)") + if any(isinstance(path, RemotePath) for path in parts): + raise TypeError("LocalPath cannot be constructed from %r" % (parts,)) + self = super(LocalPath, cls).__new__(cls, os.path.normpath(os.path.join(*(str(p) for p in parts)))) + return self + @property + def _path(self): + return str(self) + def _get_info(self): return self._path def __getstate__(self): @@ -58,7 +65,7 @@ @property @_setdoc(Path) - def basename(self): + def name(self): return os.path.basename(str(self)) @property @@ -70,7 +77,7 @@ @_setdoc(Path) def suffix(self): return os.path.splitext(str(self))[1] - + @property def suffixes(self): exts = [] @@ -103,17 +110,24 @@ @_setdoc(Path) def list(self): return [self / fn for fn in os.listdir(str(self))] + + @_setdoc(Path) + def iterdir(self): + try: + return (self.__class__(fn.name) for fn in os.scandir(str(self))) + except NameError: + return (self / fn for fn in os.listdir(str(self))) @_setdoc(Path) - def isdir(self): + def is_dir(self): return os.path.isdir(str(self)) @_setdoc(Path) - def isfile(self): + def is_file(self): return os.path.isfile(str(self)) @_setdoc(Path) - def islink(self): + def is_symlink(self): return os.path.islink(str(self)) @_setdoc(Path) @@ -123,16 +137,21 @@ @_setdoc(Path) def stat(self): return os.stat(str(self)) - + @_setdoc(Path) def with_name(self, name): return LocalPath(self.dirname) / name - + + @property + @_setdoc(Path) + def stem(self): + return self.name.rsplit(os.path.extsep)[0] + @_setdoc(Path) def with_suffix(self, suffix, depth=1): if (suffix and not suffix.startswith(os.path.extsep) or suffix == os.path.extsep): raise ValueError("Invalid suffix %r" % (suffix)) - name = self.basename + name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for i in range(depth): name, ext = os.path.splitext(name) @@ -140,13 +159,14 @@ @_setdoc(Path) def glob(self, pattern): - return [LocalPath(fn) for fn in glob.glob(str(self / pattern))] + fn = lambda pat: [LocalPath(m) for m in glob.glob(str(self / pat))] + return self._glob(pattern, fn) @_setdoc(Path) def delete(self): if not self.exists(): return - if self.isdir(): + if self.is_dir(): shutil.rmtree(str(self)) else: try: @@ -171,7 +191,7 @@ dst = LocalPath(dst) if override: dst.delete() - if self.isdir(): + if self.is_dir(): shutil.copytree(str(self), str(dst)) else: dst_dir = LocalPath(dst).dirname @@ -217,7 +237,7 @@ uid = self.uid if owner is None else (owner if isinstance(owner, int) else getpwnam(owner)[2]) gid = self.gid if group is None else (group if isinstance(group, int) else getgrnam(group)[2]) os.chown(str(self), uid, gid) - if recursive or (recursive is None and self.isdir()): + if recursive or (recursive is None and self.is_dir()): for subpath in self.walk(): os.chown(str(subpath), uid, gid) @@ -240,7 +260,7 @@ else: from plumbum.machines.local import local # windows: use mklink - if self.isdir(): + if self.is_dir(): local["cmd"]("/C", "mklink", "/D", "/H", str(dst), str(self)) else: local["cmd"]("/C", "mklink", "/H", str(dst), str(self)) @@ -254,7 +274,7 @@ else: from plumbum.machines.local import local # windows: use mklink - if self.isdir(): + if self.is_dir(): local["cmd"]("/C", "mklink", "/D", str(dst), str(self)) else: local["cmd"]("/C", "mklink", str(dst), str(self)) @@ -262,7 +282,7 @@ @_setdoc(Path) def unlink(self): try: - if hasattr(os, "symlink") or not self.isdir(): + if hasattr(os, "symlink") or not self.is_dir(): os.unlink(str(self)) else: # windows: use rmdir for directories and directory symlinks @@ -273,17 +293,30 @@ if ex.errno != errno.ENOENT: raise + @_setdoc(Path) + def as_uri(self, scheme='file'): + return urlparse.urljoin(str(scheme)+':', urllib.pathname2url(str(self))) + + @property + @_setdoc(Path) + def drive(self): + return os.path.splitdrive(str(self))[0] + + @property + @_setdoc(Path) + def root(self): + return os.path.sep + + class LocalWorkdir(LocalPath): """Working directory manipulator""" - __slots__ = [] - def __init__(self): - LocalPath.__init__(self, os.getcwd()) + def __hash__(self): raise TypeError("unhashable type") def __new__(cls): - return object.__new__(cls) + return super(LocalWorkdir, cls).__new__(cls, os.getcwd()) def chdir(self, newdir): """Changes the current working directory to the given one @@ -294,7 +327,7 @@ raise TypeError("newdir cannot be %r" % (newdir,)) logger.debug("Chdir to %s", newdir) os.chdir(str(newdir)) - self._path = os.path.normpath(os.getcwd()) + return self.__class__() def getpath(self): """Returns the current working directory as a ``LocalPath`` object""" return LocalPath(self._path) @@ -306,10 +339,9 @@ :param newdir: The destination director (a string or a ``LocalPath``) """ prev = self._path - self.chdir(newdir) + newdir = self.chdir(newdir) try: - yield + yield newdir finally: self.chdir(prev) - diff -Nru python-plumbum-1.5.0/plumbum/path/remote.py python-plumbum-1.6.0/plumbum/path/remote.py --- python-plumbum-1.5.0/plumbum/path/remote.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/path/remote.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,10 +1,13 @@ -from __future__ import with_statement import errno from contextlib import contextmanager from plumbum.path.base import Path, FSUser from plumbum.lib import _setdoc, six from plumbum.commands import shquote +try: # Py3 + import urllib.request as urllib +except ImportError: + import urllib class StatRes(object): """POSIX-like stat result""" @@ -27,14 +30,13 @@ class RemotePath(Path): """The class implementing remote-machine paths""" - __slots__ = ["_path", "remote"] - def __init__(self, remote, *parts): + + def __new__(cls, remote, *parts): if not parts: - raise TypeError("At least one path part is require (none given)") - self.remote = remote - windows = (self.remote.uname.lower() == "windows") + raise TypeError("At least one path part is required (none given)") + windows = (remote.uname.lower() == "windows") normed = [] - parts = (self.remote.cwd,) + parts + parts = (remote._session.run("pwd")[1].strip(),) + parts for p in parts: if windows: plist = str(p).replace("\\", "/").split("/") @@ -52,20 +54,25 @@ else: normed.append(item) if windows: - self.CASE_SENSITIVE = False - self._path = "\\".join(normed) + self = super(RemotePath, cls).__new__(cls, "\\".join(normed)) + self.CASE_SENSITIVE = False # On this object only else: - self._path = "/" + "/".join(normed) + self = super(RemotePath, cls).__new__(cls, "/" + "/".join(normed)) + self.CASE_SENSITIVE = True + + self.remote = remote + return self def _form(self, *parts): return RemotePath(self.remote, *parts) - def __str__(self): - return self._path + @property + def _path(self): + return str(self) @property @_setdoc(Path) - def basename(self): + def name(self): if not "/" in str(self): return str(self) return str(self).rsplit("/", 1)[1] @@ -76,16 +83,16 @@ if not "/" in str(self): return str(self) return self.__class__(self.remote, str(self).rsplit("/", 1)[0]) - + @property @_setdoc(Path) def suffix(self): - return '.' + self.basename.rsplit('.',1)[1] - + return '.' + self.name.rsplit('.',1)[1] + @property @_setdoc(Path) def suffixes(self): - name = self.basename + name = self.name exts = [] while '.' in name: name, ext = name.rsplit('.',1) @@ -113,26 +120,32 @@ @_setdoc(Path) def list(self): - if not self.isdir(): + if not self.is_dir(): return [] return [self.join(fn) for fn in self.remote._path_listdir(self)] + + @_setdoc(Path) + def iterdir(self): + if not self.is_dir(): + return () + return (self.join(fn) for fn in self.remote._path_listdir(self)) @_setdoc(Path) - def isdir(self): + def is_dir(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode == "directory" @_setdoc(Path) - def isfile(self): + def is_file(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode in ("regular file", "regular empty file") @_setdoc(Path) - def islink(self): + def is_symlink(self): res = self.remote._path_stat(self) if not res: return False @@ -152,12 +165,12 @@ @_setdoc(Path) def with_name(self, name): return self.__class__(self.remote, self.dirname) / name - + @_setdoc(Path) def with_suffix(self, suffix, depth=1): if (suffix and not suffix.startswith('.') or suffix == '.'): raise ValueError("Invalid suffix %r" % (suffix)) - name = self.basename + name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for i in range(depth): name, ext = name.rsplit('.',1) @@ -165,7 +178,8 @@ @_setdoc(Path) def glob(self, pattern): - return [RemotePath(self.remote, m) for m in self.remote._path_glob(self, pattern)] + fn = lambda pat: [RemotePath(self.remote, m) for m in self.remote._path_glob(self, pat)] + return self._glob(pattern, fn) @_setdoc(Path) def delete(self): @@ -217,7 +231,7 @@ @_setdoc(Path) def chown(self, owner = None, group = None, recursive = None): - self.remote._path_chown(self, owner, group, self.isdir() if recursive is None else recursive) + self.remote._path_chown(self, owner, group, self.is_dir() if recursive is None else recursive) @_setdoc(Path) def chmod(self, mode): self.remote._path_chmod(mode, self) @@ -250,21 +264,41 @@ raise TypeError("dst must be a string or a RemotePath (to the same remote machine), " "got %r" % (dst,)) self.remote._path_link(self, dst, True) + def open(self): + pass + + @_setdoc(Path) + def as_uri(self, scheme = 'ssh'): + return '{0}://{1}{2}'.format(scheme, self.remote._fqhost, urllib.pathname2url(str(self))) + @property + @_setdoc(Path) + def stem(self): + return self.name.rsplit('.')[0] + + @property + @_setdoc(Path) + def root(self): + return '/' + + @property + @_setdoc(Path) + def drive(self): + return '' class RemoteWorkdir(RemotePath): """Remote working directory manipulator""" - def __init__(self, remote): - self.remote = remote - self._path = self.remote._session.run("pwd")[1].strip() + def __new__(cls, remote): + self = super(RemoteWorkdir, cls).__new__(cls, remote, remote._session.run("pwd")[1].strip()) + return self def __hash__(self): raise TypeError("unhashable type") def chdir(self, newdir): """Changes the current working directory to the given one""" self.remote._session.run("cd %s" % (shquote(newdir),)) - self._path = self.remote._session.run("pwd")[1].strip() + return self.__class__(self.remote) def getpath(self): """Returns the current working directory as a @@ -280,9 +314,9 @@ :class:`RemotePath `) """ prev = self._path - self.chdir(newdir) + changed_dir = self.chdir(newdir) try: - yield + yield changed_dir finally: self.chdir(prev) diff -Nru python-plumbum-1.5.0/plumbum/path/utils.py python-plumbum-1.6.0/plumbum/path/utils.py --- python-plumbum-1.5.0/plumbum/path/utils.py 2013-09-27 16:09:09.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/path/utils.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,4 +1,3 @@ -from __future__ import with_statement from plumbum.path.base import Path from plumbum.lib import six from plumbum.machines.local import local, LocalPath @@ -29,8 +28,8 @@ """Moves the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or :class:`RemotePath `; any combination of the three will - work. - + work. + .. versionadded:: 1.3 ``src`` can also be a list of strings/paths, in which case ``dst`` must not exist or be a directory. """ @@ -39,7 +38,7 @@ if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() - elif not dst.isdir(): + elif not dst.is_dir(): raise ValueError("When using multiple sources, dst %r must be a directory" % (dst,)) for src2 in src: move(src2, dst) @@ -74,7 +73,7 @@ if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() - elif not dst.isdir(): + elif not dst.is_dir(): raise ValueError("When using multiple sources, dst %r must be a directory" % (dst,)) for src2 in src: copy(src2, dst) @@ -96,7 +95,7 @@ else: with local.tempdir() as tmp: copy(src, tmp) - copy(tmp / src.basename, dst) + copy(tmp / src.name, dst) return dst diff -Nru python-plumbum-1.5.0/plumbum/_testtools.py python-plumbum-1.6.0/plumbum/_testtools.py --- python-plumbum-1.5.0/plumbum/_testtools.py 1970-01-01 00:00:00.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/_testtools.py 2015-10-16 16:54:14.000000000 +0000 @@ -0,0 +1,35 @@ +import os +import sys +import unittest +from plumbum import local +from plumbum.lib import IS_WIN32 + +def ensure_skipIf(unittest): + """ + This will ensure that unittest has skipIf. Call like:: + + import unittest + ensure_skipIf(unittest) + """ + + if not hasattr(unittest, "skipIf"): + import logging + import functools + def skipIf(condition, reason): + def deco(func): + if not condition: + return func + else: + @functools.wraps(func) + def wrapper(*args, **kwargs): + logging.warn("skipping test: "+reason) + return wrapper + return deco + unittest.skipIf = skipIf + +ensure_skipIf(unittest) +skipIf = unittest.skipIf + +skip_on_windows = unittest.skipIf(IS_WIN32, "Does not work on Windows (yet)") +skip_without_chown = unittest.skipIf(not hasattr(os, "chown"), "os.chown not supported") +skip_without_tty = unittest.skipIf(not sys.stdin.isatty(), "Not a TTY") diff -Nru python-plumbum-1.5.0/plumbum/version.py python-plumbum-1.6.0/plumbum/version.py --- python-plumbum-1.5.0/plumbum/version.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/plumbum/version.py 2015-10-16 16:54:14.000000000 +0000 @@ -1,3 +1,3 @@ -version = (1, 5, 0) +version = (1, 6, 0) version_string = ".".join(map(str,version)) -release_date = "2015.07.17" +release_date = "2015.10.16" diff -Nru python-plumbum-1.5.0/plumbum.egg-info/PKG-INFO python-plumbum-1.6.0/plumbum.egg-info/PKG-INFO --- python-plumbum-1.5.0/plumbum.egg-info/PKG-INFO 2015-07-17 07:24:04.000000000 +0000 +++ python-plumbum-1.6.0/plumbum.egg-info/PKG-INFO 2015-10-16 16:54:58.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: plumbum -Version: 1.5.0 +Version: 1.6.0 Summary: Plumbum: shell combinators library Home-page: http://plumbum.readthedocs.org Author: Tomer Filiba @@ -29,7 +29,11 @@ Cheat Sheet ----------- - **Basics** :: + + Basics + ****** + + .. code-block:: python >>> from plumbum import local >>> ls = local["ls"] @@ -42,13 +46,18 @@ u'' # Notepad window is closed by user, command returns Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can - also ``import`` commands:: + also ``import`` commands + + .. code-block:: python >>> from plumbum.cmd import grep, wc, cat, head >>> grep LocalCommand() - **Piping** :: + Piping + ****** + + .. code-block:: python >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> print chain @@ -56,7 +65,10 @@ >>> chain() u'13\n' - **Redirection** :: + Redirection + *********** + + .. code-block:: python >>> ((cat < "setup.py") | head["-n", 4])() u'#!/usr/bin/env python\nimport os\n\ntry:\n' @@ -65,16 +77,22 @@ >>> (cat["file.list"] | wc["-l"])() u'17\n' - **Working-directory manipulation** :: + Working-directory manipulation + ****************************** + + .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() - ... + ... u'15\n' - - **Foreground and background execution** :: + + Foreground and background execution + *********************************** + + .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep["\\.py"]) & FG # The output is printed to stdout directly @@ -83,60 +101,86 @@ setup.py >>> (ls["-a"] | grep["\\.py"]) & BG # The process runs "in the background" - - **Command nesting** :: + + Command nesting + *************** + + .. code-block:: python >>> from plumbum.cmd import sudo >>> print sudo[ifconfig["-a"]] /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG - lo Link encap:Local Loopback + lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 - **Remote commands (over SSH)** + Remote commands (over SSH) + ************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) - and `Paramiko `_ (a pure-Python implementation of SSH2) :: + and `Paramiko `_ (a pure-Python implementation of SSH2) + + .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() - ... + ... u'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' - **CLI applications** :: + CLI applications + **************** + + .. code-block:: python import logging from plumbum import cli - + class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") - + @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) - + def main(self, *srcfiles): print "Verbose:", self.verbose - print "Include dirs:", self.include_dirs + print "Include dirs:", self.include_dirs print "Compiling:", srcfiles - - + if __name__ == "__main__": MyCompiler.run() - Sample output:: + Sample output + +++++++++++++ + + :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') + Colors and Styles + ----------------- + + .. code-block:: python + + from plumbum import colors + with colors.red: + print("This library provides safe, flexible color access.") + print(colors.bold | "(and styles in general)", "are easy!") + print("The simple 16 colors or", + colors.orchid & colors.underline | '256 named colors,', + colors.rgb(18, 146, 64) | "or full rgb colors", + 'can be used.') + print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") + .. image:: https://d2weczhvl823v0.cloudfront.net/tomerfiliba/plumbum/trend.png @@ -144,7 +188,7 @@ :target: https://bitdeli.com/free -Keywords: path,local,remote,ssh,shell,pipe,popen,process,execution +Keywords: path,local,remote,ssh,shell,pipe,popen,process,execution,color,cli Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable @@ -157,6 +201,7 @@ Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum diff -Nru python-plumbum-1.5.0/plumbum.egg-info/SOURCES.txt python-plumbum-1.6.0/plumbum.egg-info/SOURCES.txt --- python-plumbum-1.5.0/plumbum.egg-info/SOURCES.txt 2015-07-17 07:24:04.000000000 +0000 +++ python-plumbum-1.6.0/plumbum.egg-info/SOURCES.txt 2015-10-16 16:54:58.000000000 +0000 @@ -1,8 +1,11 @@ LICENSE MANIFEST.in README.rst +setup.cfg setup.py plumbum/__init__.py +plumbum/_testtools.py +plumbum/colors.py plumbum/lib.py plumbum/version.py plumbum.egg-info/PKG-INFO @@ -11,8 +14,16 @@ plumbum.egg-info/top_level.txt plumbum/cli/__init__.py plumbum/cli/application.py +plumbum/cli/progress.py plumbum/cli/switches.py plumbum/cli/terminal.py +plumbum/cli/termsize.py +plumbum/colorlib/__init__.py +plumbum/colorlib/__main__.py +plumbum/colorlib/_ipython_ext.py +plumbum/colorlib/factories.py +plumbum/colorlib/names.py +plumbum/colorlib/styles.py plumbum/commands/__init__.py plumbum/commands/base.py plumbum/commands/daemons.py @@ -23,6 +34,7 @@ plumbum/fs/mounts.py plumbum/machines/__init__.py plumbum/machines/_windows.py +plumbum/machines/base.py plumbum/machines/env.py plumbum/machines/local.py plumbum/machines/paramiko_machine.py diff -Nru python-plumbum-1.5.0/README.rst python-plumbum-1.6.0/README.rst --- python-plumbum-1.5.0/README.rst 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/README.rst 2015-10-16 16:54:14.000000000 +0000 @@ -21,7 +21,11 @@ Cheat Sheet ----------- -**Basics** :: + +Basics +****** + +.. code-block:: python >>> from plumbum import local >>> ls = local["ls"] @@ -34,13 +38,18 @@ u'' # Notepad window is closed by user, command returns Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can -also ``import`` commands:: +also ``import`` commands + +.. code-block:: python >>> from plumbum.cmd import grep, wc, cat, head >>> grep LocalCommand() -**Piping** :: +Piping +****** + +.. code-block:: python >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> print chain @@ -48,7 +57,10 @@ >>> chain() u'13\n' -**Redirection** :: +Redirection +*********** + +.. code-block:: python >>> ((cat < "setup.py") | head["-n", 4])() u'#!/usr/bin/env python\nimport os\n\ntry:\n' @@ -57,16 +69,22 @@ >>> (cat["file.list"] | wc["-l"])() u'17\n' -**Working-directory manipulation** :: +Working-directory manipulation +****************************** + +.. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() - ... + ... u'15\n' - -**Foreground and background execution** :: + +Foreground and background execution +*********************************** + +.. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep["\\.py"]) & FG # The output is printed to stdout directly @@ -75,60 +93,86 @@ setup.py >>> (ls["-a"] | grep["\\.py"]) & BG # The process runs "in the background" - -**Command nesting** :: + +Command nesting +*************** + +.. code-block:: python >>> from plumbum.cmd import sudo >>> print sudo[ifconfig["-a"]] /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG - lo Link encap:Local Loopback + lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 -**Remote commands (over SSH)** +Remote commands (over SSH) +************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) -and `Paramiko `_ (a pure-Python implementation of SSH2) :: +and `Paramiko `_ (a pure-Python implementation of SSH2) + +.. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() - ... + ... u'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' -**CLI applications** :: +CLI applications +**************** + +.. code-block:: python import logging from plumbum import cli - + class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") - + @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) - + def main(self, *srcfiles): print "Verbose:", self.verbose - print "Include dirs:", self.include_dirs + print "Include dirs:", self.include_dirs print "Compiling:", srcfiles - - + if __name__ == "__main__": MyCompiler.run() -Sample output:: +Sample output ++++++++++++++ + +:: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') +Colors and Styles +----------------- + +.. code-block:: python + + from plumbum import colors + with colors.red: + print("This library provides safe, flexible color access.") + print(colors.bold | "(and styles in general)", "are easy!") + print("The simple 16 colors or", + colors.orchid & colors.underline | '256 named colors,', + colors.rgb(18, 146, 64) | "or full rgb colors", + 'can be used.') + print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") + .. image:: https://d2weczhvl823v0.cloudfront.net/tomerfiliba/plumbum/trend.png diff -Nru python-plumbum-1.5.0/setup.cfg python-plumbum-1.6.0/setup.cfg --- python-plumbum-1.5.0/setup.cfg 2015-07-17 07:24:05.000000000 +0000 +++ python-plumbum-1.6.0/setup.cfg 2015-10-16 16:54:58.000000000 +0000 @@ -1,3 +1,7 @@ +[nosetests] +verbosity = 2 +detailed-errors = 1 + [egg_info] tag_build = tag_date = 0 diff -Nru python-plumbum-1.5.0/setup.py python-plumbum-1.6.0/setup.py --- python-plumbum-1.5.0/setup.py 2015-07-17 07:20:15.000000000 +0000 +++ python-plumbum-1.6.0/setup.py 2015-10-16 16:54:14.000000000 +0000 @@ -2,13 +2,26 @@ import os try: - from setuptools import setup + from setuptools import setup, Command except ImportError: - from distutils.core import setup + from distutils.core import setup, Command HERE = os.path.dirname(__file__) exec(open(os.path.join(HERE, "plumbum", "version.py")).read()) +class PyDocs(Command): + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import subprocess + import sys + os.chdir('docs') + errno = subprocess.call(['make', 'html']) + sys.exit(errno) + setup(name = "plumbum", version = version_string, # @UndefinedVariable description = "Plumbum: shell combinators library", @@ -16,10 +29,12 @@ author_email = "tomerfiliba@gmail.com", license = "MIT", url = "http://plumbum.readthedocs.org", - packages = ["plumbum", "plumbum.cli", "plumbum.commands", "plumbum.machines", "plumbum.path", "plumbum.fs"], + packages = ["plumbum", "plumbum.cli", "plumbum.commands", "plumbum.machines", "plumbum.path", "plumbum.fs", "plumbum.colorlib"], platforms = ["POSIX", "Windows"], provides = ["plumbum"], - keywords = "path, local, remote, ssh, shell, pipe, popen, process, execution", + keywords = "path, local, remote, ssh, shell, pipe, popen, process, execution, color, cli", + cmdclass = { + 'docs':PyDocs}, # use_2to3 = False, # zip_safe = True, long_description = open(os.path.join(HERE, "README.rst"), "r").read(), @@ -34,6 +49,7 @@ "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Build Tools", "Topic :: System :: Systems Administration", ],