diff -Nru python-plumbum-1.6.6/debian/changelog python-plumbum-1.6.7/debian/changelog --- python-plumbum-1.6.6/debian/changelog 2018-03-06 16:47:25.000000000 +0000 +++ python-plumbum-1.6.7/debian/changelog 2018-09-11 10:52:30.000000000 +0000 @@ -1,3 +1,12 @@ +python-plumbum (1.6.7-1) unstable; urgency=medium + + * New upstream version 1.6.7 + * Ran "cme fix dpkg" + * Enabled DH_VERBOSE in debian/rules + * Updated Standards-Version: 4.2.1 (no changes needed) + + -- Philipp Huebner Tue, 11 Sep 2018 12:52:30 +0200 + python-plumbum (1.6.6-1) unstable; urgency=medium * New upstream version 1.6.6 diff -Nru python-plumbum-1.6.6/debian/control python-plumbum-1.6.7/debian/control --- python-plumbum-1.6.6/debian/control 2018-03-06 16:44:38.000000000 +0000 +++ python-plumbum-1.6.7/debian/control 2018-09-11 10:48:37.000000000 +0000 @@ -1,17 +1,23 @@ Source: python-plumbum -Priority: optional Maintainer: Philipp Huebner -Build-Depends: debhelper (>= 11~), dh-python, python-all (>= 2.5), python-setuptools, python3-all, python3-setuptools -Standards-Version: 4.1.3 Section: python -Homepage: http://plumbum.readthedocs.org -Vcs-Git: https://salsa.debian.org/debian/python-plumbum.git -Vcs-Browser: https://salsa.debian.org/debian/python-plumbum Testsuite: autopkgtest-pkg-python +Priority: optional +Build-Depends: debhelper (>= 11~), + dh-python, + python-all, + python-setuptools, + python3-all, + python3-setuptools +Standards-Version: 4.2.1 +Vcs-Browser: https://salsa.debian.org/debian/python-plumbum +Vcs-Git: https://salsa.debian.org/debian/python-plumbum.git +Homepage: http://plumbum.readthedocs.org Package: python-plumbum Architecture: all -Depends: ${misc:Depends}, ${python:Depends} +Depends: ${misc:Depends}, + ${python:Depends} Description: library for writing shell script-like programs in Python 2 python-plumbum provides shell-like syntax and handy shortcuts for writing shell script one-liners in Python using shell combinators. It supports local and @@ -23,7 +29,8 @@ Package: python3-plumbum Architecture: all -Depends: ${misc:Depends}, ${python3:Depends} +Depends: ${misc:Depends}, + ${python3:Depends} Description: library for writing shell script-like programs in Python 3 python-plumbum provides shell-like syntax and handy shortcuts for writing shell script one-liners in Python using shell combinators. It supports local and diff -Nru python-plumbum-1.6.6/debian/copyright python-plumbum-1.6.7/debian/copyright --- python-plumbum-1.6.6/debian/copyright 2018-03-06 16:39:03.000000000 +0000 +++ python-plumbum-1.6.7/debian/copyright 2018-09-11 08:51:46.000000000 +0000 @@ -4,13 +4,13 @@ Files: * Copyright: 2013-2018 Tomer Filiba -License: MIT +License: Expat Files: debian/* Copyright: 2014-2018 Philipp Huebner -License: MIT +License: Expat -License: MIT +License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation diff -Nru python-plumbum-1.6.6/debian/rules python-plumbum-1.6.7/debian/rules --- python-plumbum-1.6.6/debian/rules 2018-02-21 13:48:39.000000000 +0000 +++ python-plumbum-1.6.7/debian/rules 2018-09-11 08:51:46.000000000 +0000 @@ -1,5 +1,5 @@ #!/usr/bin/make -f -#export DH_VERBOSE=1 +export DH_VERBOSE=1 DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/default.mk diff -Nru python-plumbum-1.6.6/PKG-INFO python-plumbum-1.6.7/PKG-INFO --- python-plumbum-1.6.6/PKG-INFO 2018-02-12 15:02:44.000000000 +0000 +++ python-plumbum-1.6.7/PKG-INFO 2018-08-10 13:04:46.000000000 +0000 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: plumbum -Version: 1.6.6 +Version: 1.6.7 Summary: Plumbum: shell combinators library Home-page: https://plumbum.readthedocs.io Author: Tomer Filiba Author-email: tomerfiliba@gmail.com License: MIT -Description-Content-Type: UNKNOWN Description: .. image:: https://readthedocs.org/projects/plumbum/badge/ :target: https://plumbum.readthedocs.io/en/latest/ :alt: Documentation Status @@ -223,6 +222,7 @@ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum diff -Nru python-plumbum-1.6.6/plumbum/cli/application.py python-plumbum-1.6.7/plumbum/cli/application.py --- python-plumbum-1.6.6/plumbum/cli/application.py 2018-01-24 09:59:44.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/application.py 2018-08-08 13:13:49.000000000 +0000 @@ -7,9 +7,10 @@ from plumbum.lib import six, getdoc from .terminal import get_terminal_size -from .switches import (SwitchError, UnknownSwitch, MissingArgument, WrongArgumentType, - MissingMandatorySwitch, SwitchCombinationError, PositionalArgumentsError, switch, - SubcommandError, Flag, CountOf) +from .switches import (SwitchError, UnknownSwitch, MissingArgument, + WrongArgumentType, MissingMandatorySwitch, + SwitchCombinationError, PositionalArgumentsError, + switch, SubcommandError, Flag, CountOf) from plumbum import colors, local from plumbum.cli.i18n import get_translation_for @@ -20,22 +21,30 @@ class ShowHelp(SwitchError): pass + + class ShowHelpAll(SwitchError): pass + + class ShowVersion(SwitchError): pass + class SwitchParseInfo(object): __slots__ = ["swname", "val", "index", "__weakref__"] + def __init__(self, swname, val, index): self.swname = swname self.val = val self.index = index + class Subcommand(object): def __init__(self, name, subapplication): self.name = name self.subapplication = subapplication + def get(self): if isinstance(self.subapplication, str): modname, clsname = self.subapplication.rsplit(".", 1) @@ -48,20 +57,23 @@ return self.subapplication def __repr__(self): - return T_("Subcommand({self.name}, {self.subapplication})").format(self=self) + return T_("Subcommand({self.name}, {self.subapplication})").format( + self=self) + _switch_groups = ['Switches', 'Meta-switches'] _switch_groups_l10n = [T_('Switches'), T_('Meta-switches')] + #=================================================================================================== # CLI Application base class #=================================================================================================== + class Application(object): - """ - The base class for CLI applications; your "entry point" class should derive from it, + """The base class for CLI applications; your "entry point" class should derive from it, define the relevant switch functions and attributes, and the ``main()`` function. The class defines two overridable "meta switches" for version (``-v``, ``--version``) - and help (``-h``, ``--help``). + help (``-h``, ``--help``), and help-all (``--help-all``). The signature of the main function matters: any positional arguments (e.g., non-switch arguments) given on the command line are passed to the ``main()`` function; if you wish @@ -69,7 +81,7 @@ of the arguments will be shown in the help message. The classmethod ``run`` serves as the entry point of the class. It parses the command-line - arguments, invokes switch functions and enter ``main``. You should **not override** this + arguments, invokes switch functions and enters ``main``. You should **not override** this method. Usage:: @@ -89,13 +101,19 @@ 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]``), can be in color. If only a color, will be applied to the name. + 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``, can be in color) * ``DESCRIPTION`` - a short description of your program (shown in help). If not set, the class' ``__doc__`` will be used. Can be in color. + * ``DESCRIPTION_MORE`` - a detailed description of your program (shown in help). The text will be printed + by paragraphs (specified by empty lines between them). The indentation of each paragraph will be the + indentation of its first line. List items are identified by their first non-whitespace character being + one of '-', '*', and '/'; so that they are not combined with preceding paragraphs. Bullet '/' is + "invisible", meaning that the bullet itself will not be printed to the output. + * ``USAGE`` - the usage line (shown in help) * ``COLOR_USAGE`` - The color of the usage line @@ -104,23 +122,30 @@ and Subcommands * ``SUBCOMMAND_HELPMSG`` - Controls the printing of extra "see subcommand -h" help message. - Default is a message, set to false to remove. + Default is a message, set to False to remove. + + * ``ALLOW_ABBREV`` - Controls whether partial switch names are supported, for example '--ver' will match + '--verbose'. Default is False for backward consistency with previous plumbum releases. Note that ambiguous + abbreviations will not match, for example if --foothis and --foothat are defined, then --foo will not match. 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. + ``None``. When it is used as a nested-command, ``parent`` will point to its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute will hold the chosen sub-application and its command-line arguments (a tuple); otherwise, it will be set to ``None`` + """ PROGNAME = None DESCRIPTION = None + DESCRIPTION_MORE = None VERSION = None USAGE = None COLOR_USAGE = None COLOR_GROUPS = None CALL_MAIN_IF_NESTED_COMMAND = True SUBCOMMAND_HELPMSG = T_("see '{parent} {sub} --help' for more info") + ALLOW_ABBREV = False parent = None nested_command = None @@ -128,10 +153,9 @@ def __new__(cls, executable=None): """Allows running the class directly as a shortcut for main. - This is neccisary for some setup scripts that want a single function, + This is necessary for some setup scripts that want a single function, instead of an expression with a dot in it.""" - if executable is None: return cls.run() # This return value was not a class instance, so __init__ is never called @@ -146,14 +170,16 @@ 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) + self.PROGNAME = colors.extract( + self.PROGNAME) | os.path.basename(executable) if self.DESCRIPTION is None: 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) + 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 @@ -168,8 +194,9 @@ if isinstance(obj, Subcommand): name = colors.filter(obj.name) if name.startswith("-"): - raise SubcommandError(T_("Subcommand names cannot start with '-'")) - # it's okay for child classes to override subcommands set by their parents + raise SubcommandError( + T_("Sub-command names cannot start with '-'")) + # it's okay for child classes to override sub-commands set by their parents self._subcommands[name] = obj continue @@ -181,13 +208,13 @@ continue if name in self._switches_by_name and not self._switches_by_name[name].overridable: raise SwitchError( - T_("Switch {name} already defined and is not overridable").format(name=name)) + T_("Switch {name} already defined and is not overridable" + ).format(name=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): return self.parent.root_app if self.parent else self @@ -203,10 +230,11 @@ MyApp.unbind_switches("--version") """ - cls._unbound_switches += tuple(name.lstrip("-") for name in switch_names if name) + cls._unbound_switches += tuple( + name.lstrip("-") for name in switch_names if name) @classmethod - def subcommand(cls, name, subapp = None): + def subcommand(cls, name, subapp=None): """Registers the given sub-application as a sub-command of this one. This method can be used both as a decorator and as a normal ``classmethod``:: @@ -221,19 +249,28 @@ .. versionadded:: 1.1 .. versionadded:: 1.3 - The subcommand can also be a string, in which case it is treated as a - fully-qualified class name and is imported on demand. For examples, + The sub-command can also be a string, in which case it is treated as a + fully-qualified class name and is imported on demand. For example, MyApp.subcommand("foo", "fully.qualified.package.FooApp") """ + def wrapper(subapp): attrname = "_subcommand_{0}".format( subapp if isinstance(subapp, str) else subapp.__name__) setattr(cls, attrname, Subcommand(name, subapp)) return subapp + return wrapper(subapp) if subapp else wrapper + def _get_partial_matches(self, partialname): + matches = [] + for switch in self._switches_by_name: + if switch.startswith(partialname): + matches += [switch, ] + return matches + def _parse_args(self, argv): tailargs = [] swfuncs = {} @@ -250,7 +287,9 @@ if a in self._subcommands: subcmd = self._subcommands[a].get() - self.nested_command = (subcmd, [self.PROGNAME + " " + self._subcommands[a].name] + argv) + self.nested_command = ( + subcmd, + [self.PROGNAME + " " + self._subcommands[a].name] + argv) break elif a.startswith("--") and len(a) >= 3: @@ -262,20 +301,34 @@ argv.insert(0, a[eqsign:]) else: name = a[2:] + + if self.ALLOW_ABBREV: + partials = self._get_partial_matches(name) + if len(partials) == 1: + name = partials[0] + elif len(partials) > 1: + raise UnknownSwitch( + T_("Ambiguous partial switch {0}").format("--" + name)) + swname = "--" + name if name not in self._switches_by_name: - raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) + raise UnknownSwitch( + T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if not argv: - raise MissingArgument(T_("Switch {0} requires an argument").format(swname)) + raise MissingArgument( + T_("Switch {0} requires an argument").format( + swname)) a = argv.pop(0) if a and a[0] == "=": if len(a) >= 2: val = a[1:] else: if not argv: - raise MissingArgument(T_("Switch {0} requires an argument").format(swname)) + raise MissingArgument( + T_("Switch {0} requires an argument") + .format(swname)) val = argv.pop(0) else: val = a @@ -285,14 +338,17 @@ name = a[1] swname = "-" + name if name not in self._switches_by_name: - raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) + raise UnknownSwitch( + T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if len(a) >= 3: val = a[2:] else: if not argv: - raise MissingArgument(T_("Switch {0} requires an argument").format(swname)) + raise MissingArgument( + T_("Switch {0} requires an argument").format( + swname)) val = argv.pop(0) elif len(a) >= 3: argv.insert(0, "-" + a[2:]) @@ -311,18 +367,21 @@ swfuncs[swinfo.func].val[0].append(val) else: if swfuncs[swinfo.func].swname == swname: - raise SwitchError(T_("Switch {0} already given").format(swname)) + raise SwitchError( + T_("Switch {0} already given").format(swname)) else: raise SwitchError( - T_("Switch {0} already given ({1} is equivalent)").format( - swfuncs[swinfo.func].swname, swname)) + T_("Switch {0} already given ({1} is equivalent)") + .format(swfuncs[swinfo.func].swname, swname)) else: if swinfo.list: - swfuncs[swinfo.func] = SwitchParseInfo(swname, ([val],), index) + swfuncs[swinfo.func] = SwitchParseInfo( + swname, ([val], ), index) elif val is NotImplemented: swfuncs[swinfo.func] = SwitchParseInfo(swname, (), index) else: - swfuncs[swinfo.func] = SwitchParseInfo(swname, (val,), index) + swfuncs[swinfo.func] = SwitchParseInfo( + swname, (val, ), index) # Extracting arguments from environment variables envindex = 0 @@ -340,11 +399,13 @@ 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) + 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) + swfuncs[swinfo.func] = SwitchParseInfo(envname, (val, ), + envindex) return swfuncs, tailargs @@ -360,8 +421,9 @@ return argtype(val) except (TypeError, ValueError): ex = sys.exc_info()[1] # compat - raise WrongArgumentType(T_("Argument of {name} expected to be {argtype}, not {val!r}:\n {ex!r}").format( - name=name, argtype=argtype, val=val, ex=ex)) + raise WrongArgumentType( + T_("Argument of {name} expected to be {argtype}, not {val!r}:\n {ex!r}" + ).format(name=name, argtype=argtype, val=val, ex=ex)) else: return NotImplemented @@ -377,10 +439,14 @@ exclusions = {} for swinfo in self._switches_by_func.values(): if swinfo.mandatory and not swinfo.func in swfuncs: - raise MissingMandatorySwitch(T_("Switch {0} is mandatory").format( - "/".join(("-" if len(n) == 1 else "--") + n for n in swinfo.names))) - requirements[swinfo.func] = set(self._switches_by_name[req] for req in swinfo.requires) - exclusions[swinfo.func] = set(self._switches_by_name[exc] for exc in swinfo.excludes) + raise MissingMandatorySwitch( + T_("Switch {0} is mandatory").format("/".join( + ("-" if len(n) == 1 else "--") + n + for n in swinfo.names))) + requirements[swinfo.func] = set( + self._switches_by_name[req] for req in swinfo.requires) + exclusions[swinfo.func] = set( + self._switches_by_name[exc] for exc in swinfo.excludes) # TODO: compute topological order @@ -396,7 +462,8 @@ if invalid: raise SwitchCombinationError( T_("Given {0}, the following are invalid {1}").format( - swfuncs[func].swname, [swfuncs[f].swname for f in invalid])) + swfuncs[func].swname, + [swfuncs[f].swname for f in invalid])) m = six.getfullargspec(self.main) max_args = six.MAXSIZE if m.varargs else len(m.args) - 1 @@ -409,22 +476,22 @@ min_args).format(min_args, tailargs)) elif len(tailargs) > max_args: raise PositionalArgumentsError( - ngettext( - "Expected at most {0} positional argument, got {1}", - "Expected at most {0} positional arguments, got {1}", - max_args).format(max_args, tailargs)) + ngettext("Expected at most {0} positional argument, got {1}", + "Expected at most {0} positional arguments, got {1}", + max_args).format(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) + 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) + positional = [None] * len(args_names) varargs = None - - # All args are positional, so convert kargs to positional + # All args are positional, so convert kargs to positional for item in m.annotations: if item == m.varargs: varargs = m.annotations[item] @@ -434,30 +501,35 @@ 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()])] + 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): + 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))): + 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]) + 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):]] + 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 + def run(cls, argv=None, exit=True): # @ReservedAssignment """ Runs the application, taking the arguments from ``sys.argv`` by default if nothing is passed. If ``exit`` is @@ -468,7 +540,7 @@ .. note:: Setting ``exit`` to ``False`` is intendend for testing/debugging purposes only -- do - not override it other situations. + not override it in other situations. """ if argv is None: argv = sys.argv @@ -502,7 +574,7 @@ if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst - inst, retcode = subapp.run(argv, exit = False) + inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() @@ -540,7 +612,7 @@ if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst - inst, retcode = subapp.run(argv, exit = False) + inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() @@ -554,16 +626,19 @@ switch = getattr(type(self), swname) swinfo = self._switches_by_func[switch._switch_info.func] if isinstance(switch, CountOf): - p = (range(val),) + p = (range(val), ) elif swinfo.list and not hasattr(val, "__iter__"): - raise SwitchError(T_("Switch {0} must be a sequence (iterable)").format(swname)) + raise SwitchError( + T_("Switch {0} must be a sequence (iterable)").format( + swname)) elif not swinfo.argtype: # a flag if val not in (True, False, None, Flag): - raise SwitchError(T_("Switch {0} is a boolean flag").format(swname)) + raise SwitchError( + T_("Switch {0} is a boolean flag").format(swname)) p = () else: - p = (val,) + p = (val, ) swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index) return swfuncs @@ -585,16 +660,18 @@ return 1 def cleanup(self, retcode): - """Called after ``main()`` and all subapplications have executed, to perform any necessary cleanup. + """Called after ``main()`` and all sub-applications have executed, to perform any necessary cleanup. :param retcode: the return code of ``main()`` """ @switch( - ["--help-all"], overridable = True, group = "Meta-switches", - help=T_("""Print help messages of all subcommands and quit""")) + ["--help-all"], + overridable=True, + group="Meta-switches", + help=T_("""Prints help messages of all sub-commands and quits""")) def helpall(self): - """Print help messages of all subcommands and quit""" + """Prints help messages of all sub-commands and quits""" self.help() print("") @@ -608,7 +685,9 @@ subapp.helpall() @switch( - ["-h", "--help"], overridable = True, group = "Meta-switches", + ["-h", "--help"], + overridable=True, + group="Meta-switches", help=T_("""Prints this help message and quits""")) def help(self): # @ReservedAssignment """Prints this help message and quits""" @@ -618,23 +697,136 @@ if self.DESCRIPTION: print(self.DESCRIPTION.strip() + '\n') + def split_indentation(s): + """Identifies the initial indentation (all spaces) of the string and returns the indentation as well + as the remainder of the line. + """ + i = 0 + while i < len(s) and s[i] == ' ': + i += 1 + return s[:i], s[i:] + + def paragraphs(text): + """Yields each paragraph of text along with its initial and subsequent indentations to be used by + textwrap.TextWrapper. + + Identifies list items from their first non-space character being one of bullets '-', '*', and '/'. + However, bullet '/' is invisible and is removed from the list item. + + :param text: The text to separate into paragraphs + """ + + paragraph = None + initial_indent = "" + subsequent_indent = "" + + def current(): + """Yields the current result if present. + """ + if paragraph: + yield paragraph, initial_indent, subsequent_indent + + for part in text.lstrip("\n").split("\n"): + indent, line = split_indentation(part) + + if len(line) == 0: + # Starting a new paragraph + for item in current(): + yield item + yield "", "", "" + + paragraph = None + initial_indent = "" + subsequent_indent = "" + else: + # Adding to current paragraph + def is_list_item(line): + """Returns true if the first element of 'line' is a bullet character. + """ + bullets = ['-', '*', '/'] + return line[0] in bullets + + def has_invisible_bullet(line): + """Returns true if the first element of 'line' is the invisible bullet ('/'). + """ + return line[0] == '/' + + if is_list_item(line): + # Done with current paragraph + for item in current(): + yield item + + if has_invisible_bullet(line): + line = line[1:] + + paragraph = line + initial_indent = indent + + # Calculate extra indentation for subsequent lines of this list item + i = 1 + while i < len(line) and line[i] == ' ': + i += 1 + subsequent_indent = indent + " " * i + else: + if not paragraph: + # Start a new paragraph + paragraph = line + initial_indent = indent + subsequent_indent = indent + else: + # Add to current paragraph + paragraph = paragraph + ' ' + line + + for item in current(): + yield item + + def wrapped_paragraphs(text, width): + """Yields each line of each paragraph of text after wrapping them on 'width' number of columns. + + :param text: The text to yield wrapped lines of + :param width: The width of the wrapped output + """ + if not text: + return + + width = max(width, 1) + + for paragraph, initial_indent, subsequent_indent in paragraphs( + text): + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent) + w = wrapper.wrap(paragraph) + for line in w: + yield line + if len(w) == 0: + yield "" + + cols, _ = get_terminal_size() + for line in wrapped_paragraphs(self.DESCRIPTION_MORE, cols): + print(line) + 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] = "[{0}={1}]".format(tailargs[-i - 1], d) if m.varargs: - tailargs.append("{0}...".format(m.varargs,)) + tailargs.append("{0}...".format(m.varargs, )) tailargs = " ".join(tailargs) with self.COLOR_USAGE: print(T_("Usage:")) if not self.USAGE: if self._subcommands: - self.USAGE = T_(" {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n") + self.USAGE = T_( + " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" + ) else: self.USAGE = T_(" {progname} [SWITCHES] {tailargs}\n") - print(self.USAGE.format(progname=colors.filter(self.PROGNAME), tailargs=tailargs)) + print(self.USAGE.format( + progname=colors.filter(self.PROGNAME), tailargs=tailargs)) by_groups = {} for si in self._switches_by_func.values(): @@ -643,20 +835,24 @@ by_groups[si.group].append(si) def switchs(by_groups, show_groups): - for grp, swinfos in sorted(by_groups.items(), key = lambda item: item[0]): + for grp, swinfos in sorted( + by_groups.items(), key=lambda item: item[0]): if show_groups: lgrp = T_(grp) if grp in _switch_groups else grp print(self.COLOR_GROUPS[grp] | lgrp + ':') - for si in sorted(swinfos, key = lambda si: si.names): - swnames = ", ".join(("-" if len(n) == 1 else "--") + n for n in si.names - if n in self._switches_by_name and self._switches_by_name[n] == si) + for si in sorted(swinfos, key=lambda si: si.names): + swnames = ", ".join(("-" if len(n) == 1 else "--") + n + for n in si.names + if n in self._switches_by_name + and self._switches_by_name[n] == si) if si.argtype: if hasattr(si.argtype, '__name__'): typename = si.argtype.__name__ else: typename = str(si.argtype) - argtype = " {0}:{1}".format(si.argname.upper(), typename) + argtype = " {0}:{1}".format(si.argname.upper(), + typename) else: argtype = "" prefix = swnames + argtype @@ -665,10 +861,10 @@ if show_groups: print("") - sw_width = max(len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4 - cols, _ = get_terminal_size() + sw_width = max( + len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4 description_indent = " {0}{1}{2}" - wrapper = TextWrapper(width = max(cols - min(sw_width, 60), 50) - 6) + wrapper = TextWrapper(width=max(cols - min(sw_width, 60), 50) - 6) indentation = "\n" + " " * (cols - wrapper.width) for switch_info, prefix, color in switchs(by_groups, True): @@ -678,52 +874,55 @@ if switch_info.mandatory: help += T_("; required") if switch_info.requires: - help += T_("; requires {0}").format( - ", ".join( - (("-" if len(switch) == 1 else "--") + switch) - for switch in switch_info.requires)) + help += T_("; requires {0}").format(", ".join( + (("-" if len(switch) == 1 else "--") + switch) + for switch in switch_info.requires)) if switch_info.excludes: - help += T_("; excludes {0}").format( - ", ".join( - (("-" if len(switch) == 1 else "--") + switch) - for switch in switch_info.excludes)) + help += T_("; excludes {0}").format(", ".join( + (("-" if len(switch) == 1 else "--") + switch) + for switch in switch_info.excludes)) - msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) + msg = indentation.join( + wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) if len(prefix) + wrapper.width >= cols: padding = indentation else: padding = " " * max(cols - wrapper.width - len(prefix) - 4, 1) - print(description_indent.format(color | prefix, padding, color | msg)) + print(description_indent.format(color | prefix, padding, + color | msg)) if self._subcommands: gc = self.COLOR_GROUPS["Subcommands"] - print(gc | T_("Subcommands:")) + print(gc | T_("Sub-commands:")) for name, subcls in sorted(self._subcommands.items()): with gc: subapp = subcls.get() - doc = subapp.DESCRIPTION if subapp.DESCRIPTION else getdoc(subapp) + doc = subapp.DESCRIPTION if subapp.DESCRIPTION else getdoc( + subapp) if self.SUBCOMMAND_HELPMSG: help = doc + "; " if doc else "" # @ReservedAssignment - help += self.SUBCOMMAND_HELPMSG.format(parent=self.PROGNAME, sub=name) + help += self.SUBCOMMAND_HELPMSG.format( + parent=self.PROGNAME, sub=name) else: - help = doc if doc else "" # @ReservedAssignment + help = doc if doc else "" # @ReservedAssignment - msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) + 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) + padding = " " * max( + cols - wrapper.width - len(name) - 4, 1) if colors.contains_colors(subcls.name): bodycolor = colors.extract(subcls.name) else: bodycolor = gc print(description_indent.format( - subcls.name, padding, - bodycolor | colors.filter(msg))) - + subcls.name, padding, bodycolor | colors.filter(msg))) def _get_prog_version(self): ver = None @@ -736,7 +935,9 @@ return ver @switch( - ["-v", "--version"], overridable = True, group = "Meta-switches", + ["-v", "--version"], + overridable=True, + group="Meta-switches", help=T_("""Prints the program's version and quits""")) def version(self): """Prints the program's version and quits""" diff -Nru python-plumbum-1.6.6/plumbum/cli/config.py python-plumbum-1.6.7/plumbum/cli/config.py --- python-plumbum-1.6.6/plumbum/cli/config.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/config.py 2018-07-04 10:09:22.000000000 +0000 @@ -6,9 +6,10 @@ import os try: - from configparser import ConfigParser, NoOptionError, NoSectionError # Py3 + from configparser import ConfigParser, NoOptionError, NoSectionError # Py3 except ImportError: - from ConfigParser import ConfigParser, NoOptionError, NoSectionError # type: ignore + from ConfigParser import ConfigParser, NoOptionError, NoSectionError # type: ignore + class ConfigBase(six.ABC): """Base class for Config parsers. @@ -81,6 +82,7 @@ def __setitem__(self, option, value): return self.set(option, value) + class ConfigINI(ConfigBase): DEFAULT_SECTION = 'DEFAULT' slots = "parser".split() @@ -105,7 +107,7 @@ if '.' not in option: sec = cls.DEFAULT_SECTION else: - sec, option = option.split('.',1) + sec, option = option.split('.', 1) return sec, option @_setdoc(ConfigBase) @@ -117,7 +119,6 @@ except (NoSectionError, NoOptionError): raise KeyError("{sec}:{option}".format(sec=sec, option=option)) - @_setdoc(ConfigBase) def _set(self, option, value): sec, option = self._sec_opt(option) Binary files /tmp/tmpERTH_4/njPSSe3kt4/python-plumbum-1.6.6/plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo and /tmp/tmpERTH_4/018fU3rgQT/python-plumbum-1.6.7/plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo differ Binary files /tmp/tmpERTH_4/njPSSe3kt4/python-plumbum-1.6.6/plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo and /tmp/tmpERTH_4/018fU3rgQT/python-plumbum-1.6.7/plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo differ Binary files /tmp/tmpERTH_4/njPSSe3kt4/python-plumbum-1.6.6/plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo and /tmp/tmpERTH_4/018fU3rgQT/python-plumbum-1.6.7/plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo differ Binary files /tmp/tmpERTH_4/njPSSe3kt4/python-plumbum-1.6.6/plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo and /tmp/tmpERTH_4/018fU3rgQT/python-plumbum-1.6.7/plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo differ diff -Nru python-plumbum-1.6.6/plumbum/cli/i18n.py python-plumbum-1.6.7/plumbum/cli/i18n.py --- python-plumbum-1.6.6/plumbum/cli/i18n.py 2018-01-24 09:59:44.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/i18n.py 2018-07-04 10:09:22.000000000 +0000 @@ -3,11 +3,13 @@ # High performance method for English (no translation needed) loc = locale.getlocale()[0] if loc is None or loc.startswith('en'): + class NullTranslation(object): def gettext(self, str): return str + def ngettext(self, str1, strN, n): - if n==1: + if n == 1: return str1.replace("{0}", str(n)) else: return strN.replace("{0}", str(n)) @@ -32,7 +34,8 @@ local_dir = os.path.basename(__file__) - def get_translation_for(package_name): # type: (str) -> gettext.NullTranslations + def get_translation_for( + package_name): # type: (str) -> gettext.NullTranslations '''Find and return gettext translation for package (Try to find folder manually if setuptools does not exist) ''' @@ -51,5 +54,5 @@ if localefile: break - return gettext.translation(package_name, localedir=localedir, fallback=True) - + return gettext.translation( + package_name, localedir=localedir, fallback=True) diff -Nru python-plumbum-1.6.6/plumbum/cli/image.py python-plumbum-1.6.7/plumbum/cli/image.py --- python-plumbum-1.6.6/plumbum/cli/image.py 2017-12-23 20:20:13.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/image.py 2018-07-04 10:09:22.000000000 +0000 @@ -5,6 +5,7 @@ from .. import cli import sys + class Image(object): __slots__ = "size char_ratio".split() @@ -14,20 +15,20 @@ self.char_ratio = char_ratio def best_aspect(self, orig, term): - """Select a best possible size matching the orignal aspect ratio. + """Select a best possible size matching the original aspect ratio. Size is width, height. The char_ratio option gives the height of each char with respect to its width, zero for no effect.""" - if not self.char_ratio: # Don't use if char ratio is 0 + if not self.char_ratio: # Don't use if char ratio is 0 return term - orig_ratio = orig[0]/orig[1]/self.char_ratio + orig_ratio = orig[0] / orig[1] / self.char_ratio - if int(term[1]/orig_ratio) <= term[0]: - new_size = int(term[1]/orig_ratio), term[1] + if int(term[1] / orig_ratio) <= term[0]: + new_size = int(term[1] / orig_ratio), term[1] else: - new_size = term[0], int(term[0]*orig_ratio) + new_size = term[0], int(term[0] * orig_ratio) return new_size def show(self, filename, double=False): @@ -53,9 +54,9 @@ new_im = im.resize(size).convert("RGB") for y in range(size[1]): - for x in range(size[0]-1): - pix = new_im.getpixel((x,y)) - print(colors.bg.rgb(*pix), ' ', sep='', end='') # u'\u2588' + for x in range(size[0] - 1): + pix = new_im.getpixel((x, y)) + print(colors.bg.rgb(*pix), ' ', sep='', end='') # u'\u2588' print(colors.reset, ' ', sep='') print(colors.reset) @@ -63,37 +64,48 @@ 'Show double resolution on some fonts' size = self._init_size(im) - size= (size[0], size[1]*2) + size = (size[0], size[1] * 2) new_im = im.resize(size).convert("RGB") - for y in range(size[1]//2): - for x in range(size[0]-1): - pix = new_im.getpixel((x,y*2)) - pixl = new_im.getpixel((x,y*2+1)) - print(colors.bg.rgb(*pixl) & colors.fg.rgb(*pix), u'\u2580', sep='', end='') + for y in range(size[1] // 2): + for x in range(size[0] - 1): + pix = new_im.getpixel((x, y * 2)) + pixl = new_im.getpixel((x, y * 2 + 1)) + print( + colors.bg.rgb(*pixl) & colors.fg.rgb(*pix), + u'\u2580', + sep='', + end='') print(colors.reset, ' ', sep='') print(colors.reset) + class ShowImageApp(cli.Application): 'Display an image on the terminal' - double = cli.Flag(['-d','--double'], help="Double resolution (only looks good with some fonts)") + double = cli.Flag( + ['-d', '--double'], + help="Double resolution (looks good only with some fonts)") - @cli.switch(['-c','--colors'], cli.Range(1,4), help="Level of color, 1-4") + @cli.switch( + ['-c', '--colors'], cli.Range(1, 4), help="Level of color, 1-4") def colors_set(self, n): colors.use_color = n - size = cli.SwitchAttr(['-s','--size'], help="Size, should be in the form 100x150") + size = cli.SwitchAttr( + ['-s', '--size'], help="Size, should be in the form 100x150") - ratio = cli.SwitchAttr(['--ratio'], float, default=2.45, help="Aspect ratio of the font") + ratio = cli.SwitchAttr( + ['--ratio'], float, default=2.45, help="Aspect ratio of the font") @cli.positional(cli.ExistingFile) def main(self, filename): - size=None + size = None if self.size: size = map(int, self.size.split('x')) Image(size, self.ratio).show(filename, self.double) + if __name__ == '__main__': ShowImageApp() diff -Nru python-plumbum-1.6.6/plumbum/cli/progress.py python-plumbum-1.6.7/plumbum/cli/progress.py --- python-plumbum-1.6.6/plumbum/cli/progress.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/progress.py 2018-07-04 10:09:22.000000000 +0000 @@ -10,6 +10,7 @@ from plumbum.cli.termsize import get_terminal_size import sys + class ProgressBase(six.ABC): """Base class for progress bars. Customize for types of progress bars. @@ -21,7 +22,13 @@ :param clear: Clear the progress bar afterwards, if applicable. """ - def __init__(self, iterator=None, length=None, timer=True, body=False, has_output=False, clear=True): + def __init__(self, + iterator=None, + length=None, + timer=True, + body=False, + has_output=False, + clear=True): if length is None: length = len(iterator) elif iterator is None: @@ -66,6 +73,7 @@ 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 @@ -85,20 +93,21 @@ if self.value < 1: return None, None elapsed_time = datetime.datetime.now() - self._start_time - time_each = (elapsed_time.days*24*60*60 - + elapsed_time.seconds - + elapsed_time.microseconds/1000000.0) / self.value + time_each = (elapsed_time.days * 24 * 60 * 60 + elapsed_time.seconds + + elapsed_time.microseconds / 1000000.0) / self.value time_remaining = time_each * (self.length - self.value) - return elapsed_time, datetime.timedelta(0,time_remaining,0) + return elapsed_time, datetime.timedelta(0, time_remaining, 0) 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]) + 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): @@ -113,11 +122,10 @@ @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) + return cls(iterator, length, body=True, **kargs) class Progress(ProgressBase): - def start(self): super(Progress, self).start() self.display() @@ -130,28 +138,31 @@ else: print() - def __str__(self): - percent = max(self.value,0)/self.length - width = get_terminal_size(default=(0,0))[0] + 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 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:.0%} complete: {1}".format(percent, self.str_time_remaining()) + return "{0:.0%} complete: {1}".format( + percent, self.str_time_remaining()) else: return "{0:.0%} complete".format(percent) else: self.width = width - len(ending) - 2 - 1 - nstars = int(percent*self.width) - pbar = '[' + '*'*nstars + ' '*(self.width-nstars) + ']' + ending + nstars = int(percent * self.width) + pbar = '[' + '*' * nstars + ' ' * ( + self.width - nstars) + ']' + ending str_percent = ' {0:.0%} '.format(percent) - return pbar[:self.width//2 - 2] + str_percent + pbar[self.width//2+len(str_percent) - 2:] - + return pbar[:self.width // 2 - + 2] + str_percent + pbar[self.width // 2 + + len(str_percent) - 2:] def display(self): disptxt = str(self) @@ -163,7 +174,7 @@ sys.stdout.flush() -class ProgressIPy(ProgressBase): # pragma: no cover +class ProgressIPy(ProgressBase): # pragma: no cover HTMLBOX = '
{}
' def __init__(self, *args, **kargs): @@ -172,9 +183,9 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore") try: - from ipywidgets import IntProgress, HTML, HBox # type: ignore - except ImportError: # Support IPython < 4.0 - from IPython.html.widgets import IntProgress, HTML, HBox # type: ignore + from ipywidgets import IntProgress, HTML, HBox # type: ignore + except ImportError: # Support IPython < 4.0 + from IPython.html.widgets import IntProgress, HTML, HBox # type: ignore super(ProgressIPy, self).__init__(*args, **kargs) self.prog = IntProgress(max=self.length) @@ -182,7 +193,7 @@ self._box = HBox((self.prog, self._label)) def start(self): - from IPython.display import display # type: ignore + from IPython.display import display # type: ignore display(self._box) super(ProgressIPy, self).start() @@ -190,6 +201,7 @@ 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 @@ -216,14 +228,15 @@ :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: # pragma: no cover + try: # pragma: no cover __IPYTHON__ try: - from traitlets import TraitError # type: ignore - except ImportError: # Support for IPython < 4.0 - from IPython.utils.traitlets import TraitError # type: ignore + from traitlets import TraitError # type: ignore + except ImportError: # Support for IPython < 4.0 + from IPython.utils.traitlets import TraitError # type: ignore try: return ProgressIPy(*args, **kargs) @@ -232,14 +245,17 @@ 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.6.6/plumbum/cli/switches.py python-plumbum-1.6.7/plumbum/cli/switches.py --- python-plumbum-1.6.6/plumbum/cli/switches.py 2017-12-28 14:25:17.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/switches.py 2018-07-04 10:09:22.000000000 +0000 @@ -7,30 +7,45 @@ _translation = get_translation_for(__name__) _, ngettext = _translation.gettext, _translation.ngettext + class SwitchError(Exception): """A general switch related-error (base class of all other switch errors)""" pass + + class PositionalArgumentsError(SwitchError): """Raised when an invalid number of positional arguments has been given""" pass + + class SwitchCombinationError(SwitchError): """Raised when an invalid combination of switches has been given""" pass + + class UnknownSwitch(SwitchError): """Raised when an unrecognized switch has been given""" pass + + class MissingArgument(SwitchError): """Raised when a switch requires an argument, but one was not provided""" pass + + class MissingMandatorySwitch(SwitchError): """Raised when a mandatory switch has not been given""" pass + + class WrongArgumentType(SwitchError): """Raised when a switch expected an argument of some type, but an argument of a wrong type has been given""" pass + + class SubcommandError(SwitchError): - """Raised when there's something wrong with subcommands""" + """Raised when there's something wrong with sub-commands""" pass @@ -42,8 +57,18 @@ for k, v in kwargs.items(): setattr(self, k, v) -def switch(names, argtype = None, argname = None, list = False, mandatory = False, requires = (), - excludes = (), help = None, overridable = False, group = "Switches", envname=None): + +def switch(names, + argtype=None, + argname=None, + list=False, + mandatory=False, + requires=(), + excludes=(), + help=None, + overridable=False, + group="Switches", + envname=None): """ A decorator that exposes functions as command-line switches. Usage:: @@ -147,20 +172,35 @@ help2 = getdoc(func) if help is None else help if not help2: help2 = str(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) + 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 + return deco + def autoswitch(*args, **kwargs): """A decorator that exposes a function as a switch, "inferring" the name of the switch from the function's name (converting to lower-case, and replacing underscores with hyphens). The arguments are the same as for :func:`switch `.""" + def deco(func): return switch(func.__name__.replace("_", "-"), *args, **kwargs)(func) + return deco + #=================================================================================================== # Switch Attributes #=================================================================================================== @@ -183,12 +223,19 @@ """ ATTR_NAME = '__plumbum_switchattr_dict__' - def __init__(self, names, argtype = str, default = None, list = False, argname = _("VALUE"), **kwargs): + def __init__(self, + names, + argtype=str, + default=None, + list=False, + argname=_("VALUE"), + **kwargs): self.__doc__ = "Sets an attribute" # to prevent the help message from showing SwitchAttr's docstring if "help" in kwargs and default and argtype is not None: kwargs["help"] += _("; the default is {0}").format(default) - switch(names, argtype = argtype, argname = argname, list = list, **kwargs)(self) + switch( + names, argtype=argtype, argname=argname, list=list, **kwargs)(self) listtype = type([]) if list: if default is None: @@ -207,20 +254,22 @@ if inst is None: return self else: - return getattr(inst, self.ATTR_NAME, {}).get(self, self._default_value) + return getattr(inst, self.ATTR_NAME, {}).get( + self, self._default_value) def __set__(self, inst, val): if inst is None: raise AttributeError("cannot set an unbound SwitchAttr") else: if not hasattr(inst, self.ATTR_NAME): - setattr(inst, self.ATTR_NAME, {self : val}) + setattr(inst, self.ATTR_NAME, {self: val}) else: getattr(inst, self.ATTR_NAME)[self] = val + class Flag(SwitchAttr): """A specialized :class:`SwitchAttr ` for boolean flags. If the flag is not - given, the value of this attribute is the ``default``; if it is given, the value changes + given, the value of this attribute is ``default``; if it is given, the value changes to ``not default``. Usage:: class MyApp(Application): @@ -231,11 +280,15 @@ :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ - def __init__(self, names, default = False, **kwargs): - SwitchAttr.__init__(self, names, argtype = None, default = default, list = False, **kwargs) + + def __init__(self, names, default=False, **kwargs): + SwitchAttr.__init__( + self, names, argtype=None, default=default, list=False, **kwargs) + def __call__(self, inst): self.__set__(inst, not self._default_value) + class CountOf(SwitchAttr): """A specialized :class:`SwitchAttr ` that counts the number of occurrences of the switch in the command line. Usage:: @@ -250,18 +303,21 @@ :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ - def __init__(self, names, default = 0, **kwargs): - SwitchAttr.__init__(self, names, argtype = None, default = default, list = True, **kwargs) + + def __init__(self, names, default=0, **kwargs): + SwitchAttr.__init__( + self, names, argtype=None, default=default, list=True, **kwargs) self._default_value = default # issue #118 + def __call__(self, inst, v): 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. @@ -299,16 +355,16 @@ m = six.getfullargspec(function) args_names = list(m.args[1:]) - positional = [None]*len(args_names) + positional = [None] * len(args_names) varargs = None - for i in range(min(len(positional),len(self.args))): + 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 + # All args are positional, so convert kargs to positional for item in self.kargs: if item == m.varargs: varargs = self.kargs[item] @@ -319,6 +375,7 @@ function.positional_varargs = varargs return function + class Validator(six.ABC): __slots__ = () @@ -341,6 +398,7 @@ mystrs = ("{0} = {1}".format(name, slots[name]) for name in slots) return "{0}({1})".format(self.__class__.__name__, ", ".join(mystrs)) + #=================================================================================================== # Switch type validators #=================================================================================================== @@ -360,16 +418,21 @@ def __init__(self, start, end): self.start = start self.end = end + def __repr__(self): return "[{0:d}..{1:d}]".format(self.start, self.end) + def __call__(self, obj): obj = int(obj) if obj < self.start or obj > self.end: - raise ValueError(_("Not in range [{0:d}..{1:d}]").format(self.start, self.end)) + raise ValueError( + _("Not in range [{0:d}..{1:d}]").format(self.start, self.end)) return obj + def choices(self, partial=""): # TODO: Add partial handling - return set(range(self.start, self.end+1)) + return set(range(self.start, self.end + 1)) + class Set(Validator): """ @@ -383,34 +446,49 @@ :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) if kwargs: - raise TypeError(_("got unexpected keyword argument(s): {0}").format(kwargs.keys())) - self.values = dict(((v if self.case_sensitive else v.lower()), v) for v in values) + raise TypeError( + _("got unexpected keyword argument(s): {0}").format( + kwargs.keys())) + self.values = dict( + ((v if self.case_sensitive else v.lower()), v) for v in values) + def __repr__(self): - return "{{{0}}}".format(", ".join(repr(v) for v in self.values.values())) + return "{{{0}}}".format(", ".join( + repr(v) for v in self.values.values())) + def __call__(self, obj): if not self.case_sensitive: obj = obj.lower() if obj not in self.values: - raise ValueError(_("Expected one of {0}").format(list(self.values.values()))) + raise ValueError( + _("Expected one of {0}").format(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""" + def __init__(self, func): self.func = func + def __str__(self): 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""" @@ -424,11 +502,13 @@ def MakeDirectory(val): p = local.path(val) if p.is_file(): - raise ValueError('{0} is a file, should be nonexistent, or a directory'.format(val)) + raise ValueError( + '{0} is a file, should be nonexistent, or a directory'.format(val)) elif not p.exists(): p.mkdir() return p + @Predicate def ExistingFile(val): """A switch-type validator that ensures that the given argument is an existing file""" @@ -437,6 +517,7 @@ raise ValueError(_("{0} is not a file").format(val)) return p + @Predicate def NonexistentPath(val): """A switch-type validator that ensures that the given argument is a nonexistent path""" diff -Nru python-plumbum-1.6.6/plumbum/cli/terminal.py python-plumbum-1.6.7/plumbum/cli/terminal.py --- python-plumbum-1.6.6/plumbum/cli/terminal.py 2017-08-27 23:40:09.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/terminal.py 2018-07-04 10:09:22.000000000 +0000 @@ -10,13 +10,15 @@ from .termsize import get_terminal_size from .progress import Progress -def readline(message = ""): + +def readline(message=""): """Gets a line of input from the user (stdin)""" sys.stdout.write(message) sys.stdout.flush() return sys.stdin.readline() -def ask(question, default = None): + +def ask(question, default=None): """ Presents the user with a yes/no question. @@ -48,8 +50,9 @@ else: sys.stdout.write("Invalid response, please try again\n") -def choose(question, options, default = None): - """Prompts the user with a question and a set of options, from which the user need choose. + +def choose(question, options, default=None): + """Prompts the user with a question and a set of options, from which the user needs to 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 @@ -74,7 +77,7 @@ choices = {} defindex = None for i, item in enumerate(options): - i = i + 1 # python2.5 + i = i + 1 # python2.5 if isinstance(item, (tuple, list)) and len(item) == 2: text = item[0] val = item[1] @@ -87,9 +90,9 @@ sys.stdout.write("(%d) %s\n" % (i, text)) if default is not None: if defindex is None: - msg = "Choice [%s]: " % (default,) + msg = "Choice [%s]: " % (default, ) else: - msg = "Choice [%d]: " % (defindex,) + msg = "Choice [%d]: " % (defindex, ) else: msg = "Choice: " while True: @@ -108,8 +111,11 @@ continue return choices[choice] -def prompt(question, type = str, default = NotImplemented, validator = lambda val: True): +def prompt(question, + type=str, + default=NotImplemented, + validator=lambda val: True): """ Presents the user with a validated question, keeps asking if validation does not pass. @@ -122,7 +128,7 @@ """ question = question.rstrip(" \t:") if default is not NotImplemented: - question += " [%s]" % (default,) + question += " [%s]" % (default, ) question += ": " while True: try: @@ -138,23 +144,26 @@ try: ans = type(ans) except (TypeError, ValueError) as ex: - sys.stdout.write("Invalid value (%s), please try again\n" % (ex,)) + sys.stdout.write("Invalid value (%s), please try again\n" % (ex, )) continue try: valid = validator(ans) except ValueError as ex: - sys.stdout.write("%s, please try again\n" % (ex,)) + sys.stdout.write("%s, please try again\n" % (ex, )) continue if not valid: - sys.stdout.write("Value not in specified range, please try again\n") + sys.stdout.write( + "Value not in specified range, please try again\n") continue return ans -def hexdump(data_or_stream, bytes_per_line = 16, aggregate = True): + +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, with possible aggregation of identical lines. Returns a generator of formatted lines. """ if hasattr(data_or_stream, "read"): + def read_chunk(): while True: buf = data_or_stream.read(bytes_per_line) @@ -162,13 +171,15 @@ break yield buf else: + def read_chunk(): for i in range(0, len(data_or_stream), bytes_per_line): yield data_or_stream[i:i + bytes_per_line] + prev = None skipped = False for i, chunk in enumerate(read_chunk()): - hexd = " ".join("%02x" % (ord(ch),) for ch in chunk) + hexd = " ".join("%02x" % (ord(ch), ) for ch in chunk) text = "".join(ch if 32 <= ord(ch) < 127 else "." for ch in chunk) if aggregate and prev == chunk: skipped = True @@ -176,11 +187,12 @@ prev = chunk if skipped: yield "*" - yield "%06x | %s| %s" % (i * bytes_per_line, hexd.ljust(bytes_per_line * 3, " "), text) + yield "%06x | %s| %s" % (i * bytes_per_line, + hexd.ljust(bytes_per_line * 3, " "), text) skipped = False -def pager(rows, pagercmd = None): # pragma: no cover +def pager(rows, pagercmd=None): # pragma: no cover """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``) @@ -191,10 +203,10 @@ if hasattr(rows, "splitlines"): rows = rows.splitlines() - pg = pagercmd.popen(stdout = None, stderr = None) + pg = pagercmd.popen(stdout=None, stderr=None) try: for row in rows: - line = "%s\n" % (row,) + line = "%s\n" % (row, ) try: pg.stdin.write(line) pg.stdin.flush() @@ -213,8 +225,3 @@ except Exception: pass os.system("reset") - - - - - diff -Nru python-plumbum-1.6.6/plumbum/cli/termsize.py python-plumbum-1.6.7/plumbum/cli/termsize.py --- python-plumbum-1.6.6/plumbum/cli/termsize.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/cli/termsize.py 2018-07-04 10:09:22.000000000 +0000 @@ -17,38 +17,44 @@ Originally from: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python """ current_os = platform.system() - if current_os == 'Windows': # pragma: no cover + if current_os == 'Windows': # pragma: no cover 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', 'SunOS') or current_os.startswith('CYGWIN'): + elif current_os in ('Linux', 'Darwin', 'FreeBSD', + 'SunOS') or current_os.startswith('CYGWIN'): size = _get_terminal_size_linux() - else: # pragma: no cover - warnings.warn("Plumbum does not know the type of the current OS for term size, defaulting to UNIX") + else: # pragma: no cover + warnings.warn( + "Plumbum does not know the type of the current OS for term size, defaulting to UNIX" + ) 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 + 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(): # pragma: no cover + +def _get_terminal_size_windows(): # pragma: no cover try: - from ctypes import windll, create_string_buffer # type: ignore + from ctypes import windll, create_string_buffer # type: ignore 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) + _, _, _, _, _, 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(): # pragma: no cover + +def _get_terminal_size_tput(): # pragma: no cover # get terminal width # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window try: @@ -59,6 +65,7 @@ except Exception: return None + def _ioctl_GWINSZ(fd): yx = Struct("hh") try: @@ -68,6 +75,7 @@ except Exception: return None + def _get_terminal_size_linux(): cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) if not cr: diff -Nru python-plumbum-1.6.6/plumbum/colorlib/factories.py python-plumbum-1.6.7/plumbum/colorlib/factories.py --- python-plumbum-1.6.6/plumbum/colorlib/factories.py 2017-08-27 23:40:09.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colorlib/factories.py 2018-07-04 10:09:22.000000000 +0000 @@ -13,7 +13,6 @@ class ColorFactory(object): - """This creates color names given fg = True/False. It usually will be called as part of a StyleFactory.""" @@ -24,35 +23,41 @@ # 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))) - + 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)) + 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)) + 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)) + 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)) + 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)) + 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""" @@ -76,7 +81,7 @@ except ColorNotFound: return self.hex(val) - def __call__(self, val_or_r=None, g = None, b = None): + 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() @@ -84,7 +89,8 @@ 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)) + 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.""" @@ -110,13 +116,13 @@ """Simple representation of the class by name.""" return "<{0}>".format(self.__class__.__name__) -class StyleFactory(ColorFactory): +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) + super(StyleFactory, self).__init__(True, style) self.fg = ColorFactory(True, style) self.bg = ColorFactory(False, style) @@ -125,7 +131,7 @@ self.reset = style(reset=True) for item in style.attribute_names: - setattr(self, item, style(attributes={item:True})) + setattr(self, item, style(attributes={item: True})) self.load_stylesheet(default_styles) @@ -146,6 +152,7 @@ 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 @@ -172,11 +179,10 @@ prev = self if styleslist: - prev = reduce(lambda a,b: a & b, 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): diff -Nru python-plumbum-1.6.6/plumbum/colorlib/__init__.py python-plumbum-1.6.7/plumbum/colorlib/__init__.py --- python-plumbum-1.6.6/plumbum/colorlib/__init__.py 2017-08-27 23:40:09.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colorlib/__init__.py 2018-07-04 10:09:22.000000000 +0000 @@ -12,22 +12,22 @@ ansicolors = StyleFactory(ANSIStyle) htmlcolors = StyleFactory(HTMLStyle) -def load_ipython_extension(ipython): # pragma: no cover + +def load_ipython_extension(ipython): # pragma: no cover try: from ._ipython_ext import OutputMagics except ImportError: print("IPython required for the IPython extension to be loaded.") raise - ipython.push({"colors":htmlcolors}) + ipython.push({"colors": htmlcolors}) ipython.register_magics(OutputMagics) -def main(): # pragma: no cover + +def main(): # pragma: no cover """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.use_color = True ansicolors.get_colors_from_string(color).now() - - diff -Nru python-plumbum-1.6.6/plumbum/colorlib/_ipython_ext.py python-plumbum-1.6.7/plumbum/colorlib/_ipython_ext.py --- python-plumbum-1.6.6/plumbum/colorlib/_ipython_ext.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colorlib/_ipython_ext.py 2018-07-04 10:09:22.000000000 +0000 @@ -1,36 +1,39 @@ -from IPython.core.magic import (Magics, magics_class, # type: ignore - cell_magic, needs_local_scope) # type: ignore -import IPython.display # type: ignore +from IPython.core.magic import ( + Magics, + magics_class, # type: ignore + cell_magic, + needs_local_scope) # type: ignore +import IPython.display # type: ignore try: from io import StringIO except ImportError: try: - from cStringIO import StringIO # type: ignore + from cStringIO import StringIO # type: ignore except ImportError: - from StringIO import StringIO # type: ignore + from StringIO import StringIO # type: ignore import sys valid_choices = [x[8:] for x in dir(IPython.display) if 'display_' == x[:8]] -@magics_class -class OutputMagics(Magics): # pragma: no cover +@magics_class +class OutputMagics(Magics): # pragma: no cover @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) + 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) + 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.6.6/plumbum/colorlib/names.py python-plumbum-1.6.7/plumbum/colorlib/names.py --- python-plumbum-1.6.6/plumbum/colorlib/names.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colorlib/names.py 2018-07-04 10:09:22.000000000 +0000 @@ -266,24 +266,32 @@ 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:]) +_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)) +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, @@ -293,7 +301,7 @@ reverse=7, hidden=8, strikeout=9, - ) +) # Stylesheet default_styles = dict( @@ -303,14 +311,15 @@ 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 @@ -319,24 +328,28 @@ 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 + 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 + 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__) + 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 + 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] @@ -345,34 +358,43 @@ 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__) + 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)) + 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 + 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__)] + 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)) + 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.6.6/plumbum/colorlib/styles.py python-plumbum-1.6.7/plumbum/colorlib/styles.py --- python-plumbum-1.6.6/plumbum/colorlib/styles.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colorlib/styles.py 2018-08-08 13:13:49.000000000 +0000 @@ -21,16 +21,21 @@ try: from abc import ABC except ImportError: - from abc import ABCMeta # type: ignore - ABC = ABCMeta('ABC', (object,), {'__module__':__name__, '__slots__':('__weakref__')}) # type: ignore + from abc import ABCMeta # type: ignore + ABC = ABCMeta('ABC', (object, ), { + '__module__': __name__, + '__slots__': ('__weakref__') + }) # type: ignore try: from typing import IO, Dict, Union except ImportError: pass -__all__ = ['Color', 'Style', 'ANSIStyle', 'HTMLStyle', 'ColorNotFound', 'AttributeNotFound'] - +__all__ = [ + 'Color', 'Style', 'ANSIStyle', 'HTMLStyle', 'ColorNotFound', + 'AttributeNotFound' +] _lower_camel_names = [n.replace('_', '') for n in color_names] @@ -40,7 +45,7 @@ if not sys.stdout.isatty(): return False - term =os.environ.get("TERM", "") + term = os.environ.get("TERM", "") # Some terminals set TERM=xterm for compatibility if term.endswith("256color") or term == "xterm": @@ -54,14 +59,17 @@ else: return 3 + 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.""" @@ -120,13 +128,14 @@ """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'): + 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.isreset = True # Starts as reset color + self.rgb = (0, 0, 0) self.number = None 'Number of the original color, or closest color' @@ -137,7 +146,7 @@ self.exact = True 'This is false if the named color does not match the real color' - if None in (g,b): + if None in (g, b): if not r_or_color: return try: @@ -149,7 +158,7 @@ self._from_hex(r_or_color) elif None not in (r_or_color, g, b): - self.rgb = (r_or_color,g,b) + self.rgb = (r_or_color, g, b) self._init_number() else: raise ColorNotFound("Invalid parameters for a color!") @@ -173,7 +182,6 @@ if not self.exact: self.number = number - @classmethod def from_simple(cls, color, fg=True): """Creates a color from simple name or color number""" @@ -184,8 +192,8 @@ def _from_simple(self, color): try: color = color.lower() - color = color.replace(' ','') - color = color.replace('_','') + color = color.replace(' ', '') + color = color.replace('_', '') except AttributeError: pass @@ -216,8 +224,8 @@ def _from_full(self, color): try: color = color.lower() - color = color.replace(' ','') - color = color.replace('_','') + color = color.replace(' ', '') + color = color.replace('_', '') except AttributeError: pass @@ -266,11 +274,12 @@ @property def name_camelcase(self): """The camelcase name of the color""" - return self.name.replace("_", " ").title().replace(" ","") + 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 = ['Deactivated:', ' Basic:', '', ' Full:', + ' True:'][self.representation] name += '' if self.fg else ' Background' name += ' ' + self.name_camelcase name += '' if self.exact else ' ' + self.hex_code @@ -294,13 +303,14 @@ ansi_addition = 30 if self.fg else 40 if self.isreset: - return (ansi_addition+9,) + return (ansi_addition + 9, ) elif self.representation < 3: - return (color_codes_simple[self.number]+ansi_addition,) + return (color_codes_simple[self.number] + ansi_addition, ) elif self.representation == 3: - return (ansi_addition+8, 5, self.number) + return (ansi_addition + 8, 5, self.number) else: - return (ansi_addition+8, 2, self.rgb[0], self.rgb[1], self.rgb[2]) + return (ansi_addition + 8, 2, self.rgb[0], self.rgb[1], + self.rgb[2]) @property def hex_code(self): @@ -333,22 +343,20 @@ 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', '__weakref__') + __slots__ = ('attributes', 'fg', 'bg', 'isreset', '__weakref__') color_class = Color """The class of color to use. Never hardcode ``Color`` call when writing a Style method.""" - attribute_names = None # type: Union[Dict[str,str], Dict[str,int]] - _stdout = None # type: IO + attribute_names = None # type: Union[Dict[str,str], Dict[str,int]] + _stdout = None # type: IO end = '\n' """The endline character. Override if needed in subclasses.""" @@ -362,15 +370,22 @@ It will use current sys.stdout if set to None (default). Unfortunately, it only works on an instance.. """ + # Import sys repeated here to make calling this stable in atexit function + import sys 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): + 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'): + 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() @@ -379,7 +394,8 @@ 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)) + raise AttributeNotFound("Attribute(s) not valid: " + + ", ".join(invalid_attributes)) @classmethod def from_color(cls, color): @@ -389,7 +405,6 @@ self = cls(bgcolor=color) return self - def invert(self): """This resets current color(s) and flips the value of all attributes present""" @@ -504,11 +519,10 @@ 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) + file.write(self.wrap(sep.join(map(str, printables))) + end) if flush: file.flush() - print_ = print """Shortcut just in case user not using __future__""" @@ -540,8 +554,8 @@ 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 ) + codes.append(attributes_ansi[attribute] + + 20 if attributes_ansi[attribute] != 1 else 22) if self.fg: codes.extend(self.fg.ansi_codes) @@ -563,10 +577,13 @@ 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]) + 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) + 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') @@ -578,8 +595,7 @@ return other.isreset else: return (self.attributes == other.attributes - and self.fg == other.fg - and self.bg == other.bg) + and self.fg == other.fg and self.bg == other.bg) else: return str(self) == other @@ -588,18 +604,17 @@ """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): + 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(';')) + sequence = map(int, group.split(';')) result.add_ansi(sequence, filter_resets) return result - def add_ansi(self, sequence, filter_resets = False): + 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) @@ -614,7 +629,8 @@ if fg: self.fg = self.color_class.from_full(value) else: - self.bg = self.color_class.from_full(value, fg=False) + self.bg = self.color_class.from_full( + value, fg=False) elif value == 2: r = next(values) g = next(values) @@ -624,27 +640,30 @@ 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: + 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()): + 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) + self.fg = self.color_class.from_simple(value - 30) elif 40 <= value <= 47: - self.bg = self.color_class.from_simple(value-40, fg=False) + 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) + 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) + 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() @@ -652,7 +671,8 @@ if filter_resets is False: self.bg = self.color_class(fg=False) else: - raise ColorNotFound("The code {0} is not recognised".format(value)) + raise ColorNotFound( + "The code {0} is not recognised".format(value)) except StopIteration: return @@ -666,7 +686,6 @@ """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) @@ -689,7 +708,6 @@ other.bg = other.bg.limit_representation(rep) return other - @property def basic(self): """The color in the 8 color representation.""" @@ -710,6 +728,7 @@ """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. @@ -728,12 +747,21 @@ 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') + 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): @@ -744,7 +772,8 @@ result = '' if self.bg and not self.bg.isreset: - result += ''.format(self.bg.hex_code) + 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): diff -Nru python-plumbum-1.6.6/plumbum/colors.py python-plumbum-1.6.7/plumbum/colors.py --- python-plumbum-1.6.6/plumbum/colors.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/colors.py 2018-07-04 10:09:22.000000000 +0000 @@ -13,11 +13,10 @@ _reset = ansicolors.reset.now if __name__ == '__main__': main() -else: # Don't register an exit if this is called using -m! +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.6.6/plumbum/commands/base.py python-plumbum-1.6.7/plumbum/commands/base.py --- python-plumbum-1.6.6/plumbum/commands/base.py 2017-11-27 14:24:20.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/commands/base.py 2018-07-04 10:09:22.000000000 +0000 @@ -2,11 +2,13 @@ import functools from contextlib import contextmanager from plumbum.commands.processes import run_proc, iter_lines +import plumbum.commands.modifiers from plumbum.lib import six from tempfile import TemporaryFile from subprocess import PIPE, Popen from types import MethodType + class RedirectionError(Exception): """Raised when an attempt is made to redirect an process' standard handle, which was already redirected to/from a file""" @@ -18,6 +20,8 @@ # modified from the stdlib pipes module for windows _safechars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@%_-+=:,./' _funnychars = '"`$\\' + + def shquote(text): """Quotes the given text with shell escaping (assumes as syntax similar to ``sh``)""" if not text: @@ -32,9 +36,11 @@ return text if "'" not in text: return "'" + text + "'" - res = six.str("").join((six.str('\\' + c) if c in _funnychars else c) for c in text) + res = six.str("").join( + (six.str('\\' + c) if c in _funnychars else c) for c in text) return six.str('"') + res + six.str('"') + def shquote_list(seq): return [shquote(item) for item in seq] @@ -69,6 +75,7 @@ def __lt__(self, file): """Redirects the given file into the process' stdin""" return StdinRedirection(self, file) + def __lshift__(self, data): """Redirects the given data into the process' stdin""" return StdinDataRedirection(self, data) @@ -77,7 +84,9 @@ """Creates a bound-command with the given arguments. Shortcut for bound_command.""" if not isinstance(args, (tuple, list)): - args = [args, ] + args = [ + args, + ] return self.bound_command(*args) def bound_command(self, *args): @@ -89,7 +98,6 @@ else: return BoundCommand(self, args) - def __call__(self, *args, **kwargs): """A shortcut for `run(args)`, returning only the process' stdout""" return self.run(args, **kwargs)[1] @@ -109,7 +117,7 @@ def machine(self): raise NotImplementedError() - def formulate(self, level = 0, args = ()): + def formulate(self, level=0, args=()): """Formulates the command into a command-line, i.e., a list of shell-quoted strings that can be executed by ``Popen`` or shells. @@ -122,7 +130,7 @@ """ raise NotImplementedError() - def popen(self, args = (), **kwargs): + def popen(self, args=(), **kwargs): """Spawns the given command, returning a ``Popen``-like object. .. note:: @@ -141,12 +149,17 @@ """ raise NotImplementedError() - def nohup(self, command, cwd='.', stdout='nohup.out', stderr=None, append=True): + 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): + def bgrun(self, args=(), **kwargs): """Runs the given command as a context manager, allowing you to create a `pipeline `_ (not in the UNIX sense) of programs, parallelizing their work. In other words, instead of running programs @@ -179,6 +192,7 @@ timeout = kwargs.pop("timeout", None) p = self.popen(args, **kwargs) was_run = [False] + def runner(): if was_run[0]: return # already done @@ -192,11 +206,12 @@ f.close() except Exception: pass + p.run = runner yield p runner() - def run(self, args = (), **kwargs): + def run(self, args=(), **kwargs): """Runs the given command (equivalent to popen() followed by :func:`run_proc `). If the exit code of the process does not match the expected one, :class:`ProcessExecutionError @@ -224,61 +239,135 @@ with self.bgrun(args, **kwargs) as p: return p.run() + def _use_modifier(self, modifier, args): + """ + Applies a modifier to the current object (e.g. FG, NOHUP) + :param modifier: The modifier class to apply (e.g. FG) + :param args: A dictionary of arguments to pass to this modifier + :return: + """ + modifier_instance = modifier(**args) + return self & modifier_instance + + def run_bg(self, **kwargs): + """ + Run this command in the background. Uses all arguments from the BG construct + :py:class: `plumbum.commands.modifiers.BG` + """ + return self._use_modifier(plumbum.commands.modifiers.BG, kwargs) + + def run_fg(self, **kwargs): + """ + Run this command in the foreground. Uses all arguments from the FG construct + :py:class: `plumbum.commands.modifiers.FG` + """ + return self._use_modifier(plumbum.commands.modifiers.FG, kwargs) + + def run_tee(self, **kwargs): + """ + Run this command using the TEE construct. Inherits all arguments from TEE + :py:class: `plumbum.commands.modifiers.TEE` + """ + return self._use_modifier(plumbum.commands.modifiers.TEE, kwargs) + + def run_tf(self, **kwargs): + """ + Run this command using the TF construct. Inherits all arguments from TF + :py:class: `plumbum.commands.modifiers.TF` + """ + return self._use_modifier(plumbum.commands.modifiers.TF, kwargs) + + def run_retcode(self, **kwargs): + """ + Run this command using the RETCODE construct. Inherits all arguments from RETCODE + :py:class: `plumbum.commands.modifiers.RETCODE` + """ + return self._use_modifier(plumbum.commands.modifiers.RETCODE, kwargs) + + def run_nohup(self, **kwargs): + """ + Run this command using the NOHUP construct. Inherits all arguments from NOHUP + :py:class: `plumbum.commands.modifiers.NOHUP` + """ + return self._use_modifier(plumbum.commands.modifiers.NOHUP, kwargs) + class BoundCommand(BaseCommand): __slots__ = ("cmd", "args") + def __init__(self, cmd, args): self.cmd = cmd self.args = list(args) + def __repr__(self): return "BoundCommand(%r, %r)" % (self.cmd, self.args) + def _get_encoding(self): return self.cmd._get_encoding() - def formulate(self, level = 0, args = ()): + + def formulate(self, level=0, args=()): return self.cmd.formulate(level + 1, self.args + list(args)) + @property def machine(self): return self.cmd.machine - def popen(self, args = (), **kwargs): + + def popen(self, args=(), **kwargs): if isinstance(args, six.string_types): - args = [args, ] + args = [ + args, + ] return self.cmd.popen(self.args + list(args), **kwargs) + class BoundEnvCommand(BaseCommand): __slots__ = ("cmd", "envvars") + def __init__(self, cmd, envvars): self.cmd = cmd self.envvars = envvars + def __repr__(self): return "BoundEnvCommand(%r, %r)" % (self.cmd, self.envvars) + def _get_encoding(self): return self.cmd._get_encoding() - def formulate(self, level = 0, args = ()): + + def formulate(self, level=0, args=()): return self.cmd.formulate(level, args) + @property def machine(self): return self.cmd.machine - def popen(self, args = (), **kwargs): + + def popen(self, args=(), **kwargs): with self.machine.env(**self.envvars): return self.cmd.popen(args, **kwargs) + class Pipeline(BaseCommand): __slots__ = ("srccmd", "dstcmd") + def __init__(self, srccmd, dstcmd): self.srccmd = srccmd self.dstcmd = dstcmd + def __repr__(self): return "Pipeline(%r, %r)" % (self.srccmd, self.dstcmd) + def _get_encoding(self): return self.srccmd._get_encoding() or self.dstcmd._get_encoding() - def formulate(self, level = 0, args = ()): - return self.srccmd.formulate(level + 1) + ["|"] + self.dstcmd.formulate(level + 1, args) + + def formulate(self, level=0, args=()): + return self.srccmd.formulate(level + 1) + ["|" + ] + self.dstcmd.formulate( + level + 1, args) @property def machine(self): return self.srccmd.machine - def popen(self, args = (), **kwargs): + def popen(self, args=(), **kwargs): src_kwargs = kwargs.copy() src_kwargs["stdout"] = PIPE if "stdin" in kwargs: @@ -297,15 +386,18 @@ # monkey-patch .wait() to wait on srcproc as well (it's expected to die when dstproc dies) dstproc_wait = dstproc.wait + @functools.wraps(Popen.wait) def wait2(*args, **kwargs): rc_dst = dstproc_wait(*args, **kwargs) rc_src = srcproc.wait(*args, **kwargs) dstproc.returncode = rc_dst or rc_src return dstproc.returncode + dstproc._proc.wait = wait2 dstproc_verify = dstproc.verify + def verify(proc, retcode, timeout, stdout, stderr): #TODO: right now it's impossible to specify different expected # return codes for different stages of the pipeline @@ -313,41 +405,50 @@ or_retcode = [0] + list(retcode) except TypeError: if (retcode is None): - or_retcode = None # no-retcode-verification acts "greedily" + or_retcode = None # no-retcode-verification acts "greedily" else: or_retcode = [0, retcode] proc.srcproc.verify(or_retcode, timeout, stdout, stderr) dstproc_verify(retcode, timeout, stdout, stderr) + dstproc.verify = MethodType(verify, dstproc) dstproc.stdin = srcproc.stdin return dstproc + class BaseRedirection(BaseCommand): __slots__ = ("cmd", "file") - SYM = None # type: str - KWARG = None # type: str - MODE = None # type: str + SYM = None # type: str + KWARG = None # type: str + MODE = None # type: str def __init__(self, cmd, file): self.cmd = cmd self.file = file + def _get_encoding(self): return self.cmd._get_encoding() + def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.cmd, self.file) - def formulate(self, level = 0, args = ()): - return self.cmd.formulate(level + 1, args) + [self.SYM, shquote(getattr(self.file, "name", self.file))] + + def formulate(self, level=0, args=()): + return self.cmd.formulate(level + 1, args) + [ + self.SYM, shquote(getattr(self.file, "name", self.file)) + ] + @property def machine(self): return self.cmd.machine - def popen(self, args = (), **kwargs): + + def popen(self, args=(), **kwargs): from plumbum.machines.local import LocalPath from plumbum.machines.remote import RemotePath if self.KWARG in kwargs and kwargs[self.KWARG] not in (PIPE, None): - raise RedirectionError("%s is already redirected" % (self.KWARG,)) - if isinstance(self.file, six.string_types + (LocalPath,)): + raise RedirectionError("%s is already redirected" % (self.KWARG, )) + if isinstance(self.file, six.string_types + (LocalPath, )): f = kwargs[self.KWARG] = open(str(self.file), self.MODE) elif isinstance(self.file, RemotePath): raise TypeError("Cannot redirect to/from remote paths") @@ -360,37 +461,46 @@ if f: f.close() + class StdinRedirection(BaseRedirection): __slots__ = () SYM = "<" KWARG = "stdin" MODE = "r" + class StdoutRedirection(BaseRedirection): __slots__ = () SYM = ">" KWARG = "stdout" MODE = "w" + class AppendingStdoutRedirection(BaseRedirection): __slots__ = () SYM = ">>" KWARG = "stdout" MODE = "a" + class StderrRedirection(BaseRedirection): __slots__ = () SYM = "2>" KWARG = "stderr" MODE = "w" + class _ERROUT(int): def __repr__(self): return "ERROUT" + def __str__(self): return "&1" + + ERROUT = _ERROUT(subprocess.STDOUT) + class StdinDataRedirection(BaseCommand): __slots__ = ("cmd", "data") CHUNK_SIZE = 16000 @@ -398,19 +508,26 @@ def __init__(self, cmd, data): self.cmd = cmd self.data = data + def _get_encoding(self): return self.cmd._get_encoding() - def formulate(self, level = 0, args = ()): - return ["echo %s" % (shquote(self.data),), "|", self.cmd.formulate(level + 1, args)] + def formulate(self, level=0, args=()): + return [ + "echo %s" % (shquote(self.data), ), "|", + self.cmd.formulate(level + 1, args) + ] + @property def machine(self): return self.cmd.machine - def popen(self, args = (), **kwargs): + + def popen(self, args=(), **kwargs): if "stdin" in kwargs and kwargs["stdin"] != PIPE: raise RedirectionError("stdin is already redirected") data = self.data - if isinstance(data, six.unicode_type) and self._get_encoding() is not None: + if isinstance(data, + six.unicode_type) and self._get_encoding() is not None: data = data.encode(self._get_encoding()) f = TemporaryFile() while data: @@ -419,13 +536,15 @@ data = data[self.CHUNK_SIZE:] f.seek(0) # try: - return self.cmd.popen(args, stdin = f, **kwargs) + return self.cmd.popen(args, stdin=f, **kwargs) # finally: # f.close() + class ConcreteCommand(BaseCommand): - QUOTE_LEVEL = None # type: int + QUOTE_LEVEL = None # type: int __slots__ = ("executable", "custom_encoding") + def __init__(self, executable, encoding): self.executable = executable self.custom_encoding = encoding @@ -441,7 +560,7 @@ def _get_encoding(self): return self.custom_encoding - def formulate(self, level = 0, args = ()): + def formulate(self, level=0, args=()): argv = [six.str(self.executable)] for a in args: if a is None: @@ -452,10 +571,12 @@ else: argv.extend(a.formulate(level + 1)) elif isinstance(a, (list, tuple)): - argv.extend(shquote(b) if level >= self.QUOTE_LEVEL else six.str(b) for b in a) + argv.extend( + shquote(b) if level >= self.QUOTE_LEVEL else six.str(b) + for b in a) else: - argv.append(shquote(a) if level >= self.QUOTE_LEVEL else six.str(a)) + argv.append( + shquote(a) if level >= self.QUOTE_LEVEL else six.str(a)) # if self.custom_encoding: # argv = [a.encode(self.custom_encoding) for a in argv if isinstance(a, six.string_types)] return argv - diff -Nru python-plumbum-1.6.6/plumbum/commands/daemons.py python-plumbum-1.6.7/plumbum/commands/daemons.py --- python-plumbum-1.6.6/plumbum/commands/daemons.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/commands/daemons.py 2018-07-04 10:09:22.000000000 +0000 @@ -7,13 +7,17 @@ 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 @@ -35,12 +39,17 @@ 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()) + proc = command.popen( + cwd=cwd, + close_fds=True, + stdin=stdin.fileno(), + stdout=stdout.fileno(), + stderr=stderr.fileno()) os.write(wfd, str(proc.pid).encode("utf8")) except: rc = 1 - tbtext = "".join(traceback.format_exception(*sys.exc_info()))[-MAX_SIZE:] + tbtext = "".join( + traceback.format_exception(*sys.exc_info()))[-MAX_SIZE:] os.write(wfd, tbtext.encode("utf8")) finally: os.close(wfd) @@ -71,8 +80,8 @@ proc._communication_started = False proc.args = argv proc.argv = argv - - def poll(self = proc): + + def poll(self=proc): if self.returncode is None: try: os.kill(self.pid, 0) @@ -84,13 +93,13 @@ else: raise return self.returncode - - def wait(self = proc): + + def wait(self=proc): while self.returncode is None: if self.poll() is None: time.sleep(0.5) - return proc.returncode - + return proc.returncode + proc.poll = poll proc.wait = wait return proc @@ -105,11 +114,9 @@ stdin = open(os.devnull, "r") 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) - - - - - - + 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.6.6/plumbum/commands/modifiers.py python-plumbum-1.6.7/plumbum/commands/modifiers.py --- python-plumbum-1.6.6/plumbum/commands/modifiers.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/commands/modifiers.py 2018-07-04 10:09:22.000000000 +0000 @@ -6,7 +6,7 @@ from itertools import chain from plumbum.commands.processes import run_proc, ProcessExecutionError -from plumbum.commands.base import AppendingStdoutRedirection, StdoutRedirection +import plumbum.commands.base from plumbum.lib import read_fd_decode_safely @@ -15,52 +15,64 @@ object and the expected exit code, and provides poll(), wait(), returncode, stdout, and stderr. """ - def __init__(self, proc, expected_retcode, timeout = None): + + def __init__(self, proc, expected_retcode, timeout=None): self.proc = proc self._expected_retcode = expected_retcode self._timeout = timeout self._returncode = None self._stdout = None self._stderr = None + def __repr__(self): - return "" % (self.proc.argv, self._returncode if self.ready() else "running",) + return "" % ( + self.proc.argv, + self._returncode if self.ready() else "running", + ) + def poll(self): """Polls the underlying process for termination; returns ``False`` if still running, or ``True`` if terminated""" if self.proc.poll() is not None: self.wait() return self._returncode is not None + ready = poll + def wait(self): """Waits for the process to terminate; will raise a :class:`plumbum.commands.ProcessExecutionError` in case of failure""" if self._returncode is not None: return - self._returncode, self._stdout, self._stderr = run_proc(self.proc, - self._expected_retcode, self._timeout) + self._returncode, self._stdout, self._stderr = run_proc( + self.proc, self._expected_retcode, self._timeout) + @property def stdout(self): """The process' stdout; accessing this property will wait for the process to finish""" self.wait() return self._stdout + @property def stderr(self): """The process' stderr; accessing this property will wait for the process to finish""" self.wait() return self._stderr + @property def returncode(self): """The process' returncode; accessing this property will wait for the process to finish""" self.wait() return self._returncode + #=================================================================================================== # execution modifiers #=================================================================================================== class ExecutionModifier(object): - __slots__ = ("__weakref__",) + __slots__ = ("__weakref__", ) def __repr__(self): """Automatically creates a representation for given subclass with slots. @@ -69,7 +81,7 @@ for cls in self.__class__.__mro__: slots_list = getattr(cls, "__slots__", ()) if isinstance(slots_list, str): - slots_list = (slots_list,) + slots_list = (slots_list, ) for prop in slots_list: if prop[0] != '_': slots[prop] = getattr(self, prop) @@ -80,6 +92,7 @@ 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 @@ -106,10 +119,13 @@ self.timeout = timeout def __rand__(self, cmd): - return Future(cmd.popen(**self.kargs), self.retcode, timeout=self.timeout) + return Future( + cmd.popen(**self.kargs), self.retcode, timeout=self.timeout) + BG = _BG() + class _FG(ExecutionModifier): """ An execution modifier that runs the given command in the foreground, passing it the @@ -130,8 +146,12 @@ self.timeout = timeout def __rand__(self, cmd): - cmd(retcode = self.retcode, stdin = None, stdout = None, stderr = None, - timeout = self.timeout) + cmd(retcode=self.retcode, + stdin=None, + stdout=None, + stderr=None, + timeout=self.timeout) + FG = _FG() @@ -159,10 +179,13 @@ self.buffered = buffered self.timeout = timeout - def __rand__(self, cmd): - with cmd.bgrun(retcode=self.retcode, stdin=None, stdout=PIPE, stderr=PIPE, - timeout=self.timeout) as p: + with cmd.bgrun( + retcode=self.retcode, + stdin=None, + stdout=PIPE, + stderr=PIPE, + timeout=self.timeout) as p: outbuf = [] errbuf = [] out = p.stdout @@ -193,8 +216,10 @@ stderr = ''.join([x.decode('utf-8') for x in errbuf]) return p.returncode, stdout, stderr + TEE = _TEE() + class _TF(ExecutionModifier): """ An execution modifier that runs the given command, but returns True/False depending on the retcode. @@ -229,14 +254,18 @@ def __rand__(self, cmd): try: if self.FG: - cmd(retcode = self.retcode, stdin = None, stdout = None, stderr = None, - timeout = self.timeout) + cmd(retcode=self.retcode, + stdin=None, + stdout=None, + stderr=None, + timeout=self.timeout) else: - cmd(retcode = self.retcode, timeout = self.timeout) + cmd(retcode=self.retcode, timeout=self.timeout) return True except ProcessExecutionError: return False + TF = _TF() @@ -256,7 +285,7 @@ __slots__ = ("foreground", "timeout") - def __init__(self, FG=False, timeout=None): + def __init__(self, FG=False, timeout=None): """`FG` to True to run in the foreground. """ self.foreground = FG @@ -267,11 +296,16 @@ return cls(*args, **kwargs) def __rand__(self, cmd): - if self.foreground: - return cmd.run(retcode = None, stdin = None, stdout = None, stderr = None, - timeout = self.timeout)[0] - else: - return cmd.run(retcode = None, timeout = self.timeout)[0] + if self.foreground: + return cmd.run( + retcode=None, + stdin=None, + stdout=None, + stderr=None, + timeout=self.timeout)[0] + else: + return cmd.run(retcode=None, timeout=self.timeout)[0] + RETCODE = _RETCODE() @@ -310,11 +344,11 @@ self.append = append def __rand__(self, cmd): - if isinstance(cmd, StdoutRedirection): + if isinstance(cmd, plumbum.commands.base.StdoutRedirection): stdout = cmd.file append = False cmd = cmd.cmd - elif isinstance(cmd, AppendingStdoutRedirection): + elif isinstance(cmd, plumbum.commands.base.AppendingStdoutRedirection): stdout = cmd.file append = True cmd = cmd.cmd @@ -323,4 +357,5 @@ append = self.append return cmd.nohup(cmd, self.cwd, stdout, self.stderr, append) + NOHUP = _NOHUP() diff -Nru python-plumbum-1.6.6/plumbum/commands/processes.py python-plumbum-1.6.7/plumbum/commands/processes.py --- python-plumbum-1.6.6/plumbum/commands/processes.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/commands/processes.py 2018-08-08 13:13:49.000000000 +0000 @@ -8,12 +8,12 @@ try: from queue import Queue, Empty as QueueEmpty except ImportError: - from Queue import Queue, Empty as QueueEmpty # type: ignore + from Queue import Queue, Empty as QueueEmpty # type: ignore try: from io import StringIO except ImportError: - from cStringIO import StringIO # type: ignore + from cStringIO import StringIO # type: ignore #=================================================================================================== @@ -23,17 +23,20 @@ proc.verify(retcode, timeout, stdout, stderr) return proc.returncode, stdout, stderr + def _iter_lines(proc, decode, linesize): try: from selectors import DefaultSelector, EVENT_READ except ImportError: # Pre Python 3.4 implementation from select import select + def selector(): while True: rlist, _, _ = select([proc.stdout, proc.stderr], [], []) for stream in rlist: - yield (stream is proc.stderr), decode(stream.readline(linesize)) + yield (stream is proc.stderr), decode( + stream.readline(linesize)) else: # Python 3.4 implementation def selector(): @@ -63,6 +66,7 @@ `. It contains the process' return code, stdout, and stderr, as well as the command line used to create the process (``argv``) """ + def __init__(self, argv, retcode, stdout, stderr): Exception.__init__(self, argv, retcode, stdout, stderr) self.argv = argv @@ -73,51 +77,66 @@ stderr = six.ascii(stderr) self.stdout = stdout self.stderr = stderr + def __str__(self): stdout = "\n | ".join(str(self.stdout).splitlines()) stderr = "\n | ".join(str(self.stderr).splitlines()) - lines = ["Command line: %r" % (self.argv,), "Exit code: %s" % (self.retcode)] + lines = [ + "Command line: %r" % (self.argv, ), + "Exit code: %s" % (self.retcode) + ] if stdout: - lines.append("Stdout: | %s" % (stdout,)) + lines.append("Stdout: | %s" % (stdout, )) if stderr: - lines.append("Stderr: | %s" % (stderr,)) + lines.append("Stderr: | %s" % (stderr, )) return "\n".join(lines) + class ProcessTimedOut(Exception): """Raises by :func:`run_proc ` when a ``timeout`` has been specified and it has elapsed before the process terminated""" + def __init__(self, msg, argv): Exception.__init__(self, msg, argv) self.argv = argv + class CommandNotFound(AttributeError): """Raised by :func:`local.which ` and :func:`RemoteMachine.which ` when a command was not found in the system's ``PATH``""" + def __init__(self, program, path): Exception.__init__(self, program, path) self.program = program self.path = path + #=================================================================================================== # Timeout thread #=================================================================================================== class MinHeap(object): - def __init__(self, items = ()): + def __init__(self, items=()): self._items = list(items) heapq.heapify(self._items) + def __len__(self): return len(self._items) + def push(self, item): heapq.heappush(self._items, item) + def pop(self): heapq.heappop(self._items) + def peek(self): return self._items[0] + _timeout_queue = Queue() _shutting_down = False + def _timeout_thread_func(): waiting = MinHeap() try: @@ -128,7 +147,7 @@ else: timeout = None try: - proc, time_to_kill = _timeout_queue.get(timeout = timeout) + proc, time_to_kill = _timeout_queue.get(timeout=timeout) if proc is SystemExit: # terminate return @@ -154,27 +173,34 @@ else: raise -bgthd = Thread(target = _timeout_thread_func, name = "PlumbumTimeoutThread") + +bgthd = Thread(target=_timeout_thread_func, name="PlumbumTimeoutThread") bgthd.setDaemon(True) bgthd.start() + def _register_proc_timeout(proc, timeout): if timeout is not None: _timeout_queue.put((proc, time.time() + timeout)) + def _shutdown_bg_threads(): global _shutting_down _shutting_down = True - _timeout_queue.put((SystemExit, 0)) - # grace period - bgthd.join(0.1) + # Make sure this still exists (don't throw error in atexit!) + if _timeout_queue: + _timeout_queue.put((SystemExit, 0)) + # grace period + bgthd.join(0.1) + atexit.register(_shutdown_bg_threads) + #=================================================================================================== # run_proc #=================================================================================================== -def run_proc(proc, retcode, timeout = None): +def run_proc(proc, retcode, timeout=None): """Waits for the given process to terminate, with the expected exit code :param proc: a running Popen-like object, with all the expected methods. @@ -209,7 +235,11 @@ #=================================================================================================== # iter_lines #=================================================================================================== -def iter_lines(proc, retcode = 0, timeout = None, linesize = -1, _iter_lines = _iter_lines): +def iter_lines(proc, + retcode=0, + timeout=None, + linesize=-1, + _iter_lines=_iter_lines): """Runs the given process (equivalent to run_proc()) and yields a tuples of (out, err) line pairs. If the exit code of the process does not match the expected one, :class:`ProcessExecutionError ` is raised. diff -Nru python-plumbum-1.6.6/plumbum/fs/atomic.py python-plumbum-1.6.7/plumbum/fs/atomic.py --- python-plumbum-1.6.6/plumbum/fs/atomic.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/fs/atomic.py 2018-07-04 10:09:22.000000000 +0000 @@ -10,7 +10,6 @@ from plumbum.machines.local import local from plumbum.lib import six - if not hasattr(threading, "get_ident"): try: import thread @@ -19,7 +18,6 @@ threading.get_ident = thread.get_ident del thread - try: import fcntl except ImportError: @@ -29,14 +27,17 @@ from win32file import LockFileEx, UnlockFile, OVERLAPPED from win32con import LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY except ImportError: - raise ImportError("On Windows, we require Python for Windows Extensions (pywin32)") + raise ImportError( + "On Windows, we require Python for Windows Extensions (pywin32)") @contextmanager - def locked_file(fileno, blocking = True): + def locked_file(fileno, blocking=True): hndl = msvcrt.get_osfhandle(fileno) try: - LockFileEx(hndl, LOCKFILE_EXCLUSIVE_LOCK | (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), - 0xffffffff, 0xffffffff, OVERLAPPED()) + LockFileEx( + hndl, LOCKFILE_EXCLUSIVE_LOCK | + (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), 0xffffffff, + 0xffffffff, OVERLAPPED()) except WinError: _, ex, _ = sys.exc_info() raise WindowsError(*ex.args) @@ -46,17 +47,21 @@ UnlockFile(hndl, 0, 0, 0xffffffff, 0xffffffff) else: if hasattr(fcntl, "lockf"): + @contextmanager - def locked_file(fileno, blocking = True): - fcntl.lockf(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) + def locked_file(fileno, blocking=True): + fcntl.lockf(fileno, + fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: fcntl.lockf(fileno, fcntl.LOCK_UN) else: + @contextmanager - def locked_file(fileno, blocking = True): - fcntl.flock(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) + def locked_file(fileno, blocking=True): + fcntl.flock(fileno, + fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: @@ -77,7 +82,7 @@ CHUNK_SIZE = 32 * 1024 - def __init__(self, filename, ignore_deletion = False): + def __init__(self, filename, ignore_deletion=False): self.path = local.path(filename) self._ignore_deletion = ignore_deletion self._thdlock = threading.Lock() @@ -86,12 +91,15 @@ self.reopen() def __repr__(self): - return "" % (self.path,) if self._fileobj else "" + return "" % ( + self.path, ) if self._fileobj else "" def __del__(self): self.close() + def __enter__(self): return self + def __exit__(self, t, v, tb): self.close() @@ -106,10 +114,11 @@ by a different process """ self.close() - self._fileobj = os.fdopen(os.open(str(self.path), os.O_CREAT | os.O_RDWR, 384), "r+b", 0) + self._fileobj = os.fdopen( + os.open(str(self.path), os.O_CREAT | os.O_RDWR, 384), "r+b", 0) @contextmanager - def locked(self, blocking = True): + def locked(self, blocking=True): """ A context manager that locks the file; this function is reentrant by the thread currently holding the lock. @@ -186,7 +195,7 @@ .. versionadded:: 1.3 """ - def __init__(self, atomicfile, initial = 0): + def __init__(self, atomicfile, initial=0): """ :param atomicfile: an :class:`AtomicFile ` instance :param initial: the initial value (used when the first time the file is created) @@ -196,8 +205,10 @@ def __enter__(self): return self + def __exit__(self, t, v, tb): self.close() + def close(self): self.atomicfile.close() @@ -208,7 +219,7 @@ """ return cls(AtomicFile(filename)) - def reset(self, value = None): + def reset(self, value=None): """ Reset the counter's value to the one given. If ``None``, it will default to the initial value provided to the constructor @@ -216,7 +227,8 @@ if value is None: value = self.initial if not isinstance(value, six.integer_types): - raise TypeError("value must be an integer, not %r" % (type(value),)) + raise TypeError( + "value must be an integer, not %r" % (type(value), )) self.atomicfile.write_atomic(str(value).encode("utf8")) def next(self): @@ -239,10 +251,12 @@ derives from ``SystemExit``, so unless explicitly handled, it will terminate the process cleanly """ + def __init__(self, msg, pid): SystemExit.__init__(self, msg) self.pid = pid + class PidFile(object): """ A PID file is a file that's locked by some process from the moment it starts until it dies @@ -256,15 +270,19 @@ def __init__(self, filename): self.atomicfile = AtomicFile(filename) self._ctx = None + def __enter__(self): self.acquire() + def __exit__(self, t, v, tb): self.release() + def __del__(self): try: self.release() except Exception: pass + def close(self): self.atomicfile.close() @@ -276,7 +294,7 @@ """ if self._ctx is not None: return - self._ctx = self.atomicfile.locked(blocking = False) + self._ctx = self.atomicfile.locked(blocking=False) try: self._ctx.__enter__() except (IOError, OSError): @@ -285,7 +303,9 @@ pid = self.atomicfile.read_shared().strip().decode("utf8") except (IOError, OSError): pid = "Unknown" - raise PidFileTaken("PID file %r taken by process %s" % (self.atomicfile.path, pid), pid) + raise PidFileTaken( + "PID file %r taken by process %s" % (self.atomicfile.path, + pid), pid) else: self.atomicfile.write_atomic(str(os.getpid()).encode("utf8")) atexit.register(self.release) @@ -301,7 +321,3 @@ self._ctx.__exit__(None, None, None) finally: self._ctx = None - - - - diff -Nru python-plumbum-1.6.6/plumbum/fs/mounts.py python-plumbum-1.6.7/plumbum/fs/mounts.py --- python-plumbum-1.6.6/plumbum/fs/mounts.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/fs/mounts.py 2018-07-04 10:09:22.000000000 +0000 @@ -1,24 +1,31 @@ import re + class MountEntry(object): """ Represents a mount entry (device file, mount point and file system type) """ + def __init__(self, dev, point, fstype, options): self.dev = dev self.point = point self.fstype = fstype self.options = options.split(",") + def __str__(self): - return "%s on %s type %s (%s)" % (self.dev, self.point, self.fstype, ",".join(self.options)) + return "%s on %s type %s (%s)" % (self.dev, self.point, self.fstype, + ",".join(self.options)) + + +MOUNT_PATTERN = re.compile( + r"(.+?)\s+on\s+(.+?)\s+type\s+(\S+)(?:\s+\((.+?)\))?") -MOUNT_PATTERN = re.compile(r"(.+?)\s+on\s+(.+?)\s+type\s+(\S+)(?:\s+\((.+?)\))?") def mount_table(): """returns the system's current mount table (a list of :class:`MountEntry ` objects)""" from plumbum.cmd import mount - + table = [] for line in mount().splitlines(): m = MOUNT_PATTERN.match(line) @@ -27,11 +34,9 @@ table.append(MountEntry(*m.groups())) return table + def mounted(fs): """ Indicates if a the given filesystem (device file or mount point) is currently mounted """ return any(fs == entry.dev or fs == entry.point for entry in mount_table()) - - - diff -Nru python-plumbum-1.6.6/plumbum/__init__.py python-plumbum-1.6.7/plumbum/__init__.py --- python-plumbum-1.6.6/plumbum/__init__.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/__init__.py 2018-07-04 10:09:22.000000000 +0000 @@ -43,7 +43,6 @@ __author__ = "Tomer Filiba (tomerfiliba@gmail.com)" __version__ = version - #=================================================================================================== # Module hack: ``from plumbum.cmd import ls`` #=================================================================================================== @@ -55,18 +54,22 @@ except ImportError: pass + class LocalModule(ModuleType): """The module-hack that allows us to use ``from plumbum.cmd import some_program``""" __all__ = () # to make help() happy __package__ = __name__ + def __getattr__(self, name): try: return local[name] except CommandNotFound: raise AttributeError(name) - __path__ = [] # type: List[str] + + __path__ = [] # type: List[str] __file__ = __file__ + cmd = LocalModule(__name__ + ".cmd", LocalModule.__doc__) sys.modules[cmd.__name__] = cmd diff -Nru python-plumbum-1.6.6/plumbum/lib.py python-plumbum-1.6.7/plumbum/lib.py --- python-plumbum-1.6.6/plumbum/lib.py 2017-11-08 16:04:22.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/lib.py 2018-07-04 10:09:22.000000000 +0000 @@ -6,22 +6,30 @@ 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) + func.__doc__ = getattr( + getattr(super, func.__name__, None), "__doc__", None) return func + return deco + class ProcInfo(object): def __init__(self, pid, uid, stat, args): self.pid = pid self.uid = uid self.stat = stat self.args = args + def __repr__(self): - return "ProcInfo(%r, %r, %r, %r)" % (self.pid, self.uid, self.stat, self.args) + return "ProcInfo(%r, %r, %r, %r)" % (self.pid, self.uid, self.stat, + self.args) + class six(object): """ @@ -31,18 +39,22 @@ try: from abc import ABC except ImportError: - from abc import ABCMeta # type: ignore - ABC = ABCMeta('ABC', (object,), {'__module__':__name__, '__slots__':()}) # type: ignore + from abc import ABCMeta # type: ignore + ABC = ABCMeta('ABC', (object, ), { + '__module__': __name__, + '__slots__': () + }) # type: ignore # 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 + getfullargspec = staticmethod( + inspect.getargspec) # extra fields will not be available if PY3: - integer_types = (int,) - string_types = (str,) + integer_types = (int, ) + string_types = (str, ) MAXSIZE = sys.maxsize ascii = ascii # @UndefinedVariable bytes = bytes # @ReservedAssignment @@ -51,9 +63,11 @@ @staticmethod def b(s): return s.encode("latin-1", "replace") + @staticmethod def u(s): return s + @staticmethod def get_method_function(m): return m.__func__ @@ -62,31 +76,34 @@ string_types = (str, unicode) MAXSIZE = getattr(sys, "maxsize", sys.maxint) ascii = repr # @ReservedAssignment - bytes = str # @ReservedAssignment + bytes = str # @ReservedAssignment unicode_type = unicode @staticmethod def b(st): return st + @staticmethod def u(s): return s.decode("unicode-escape") + @staticmethod def get_method_function(m): return m.im_func str = unicode_type + # 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 # type: ignore + from StringIO import StringIO # type: ignore @contextmanager -def captured_stdout(stdin = ""): +def captured_stdout(stdin=""): """ Captures stdout (similar to the redirect_stdout in Python 3.4+, but with slightly different arguments) """ @@ -100,9 +117,11 @@ 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__ @@ -124,6 +143,7 @@ return None return inspect.cleandoc(doc) + def read_fd_decode_safely(fd, size=4096): """ This reads a utf-8 file descriptor and returns a chunck, growing up to diff -Nru python-plumbum-1.6.6/plumbum/machines/base.py python-plumbum-1.6.7/plumbum/machines/base.py --- python-plumbum-1.6.6/plumbum/machines/base.py 2017-03-02 03:05:23.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/base.py 2018-07-04 10:09:22.000000000 +0000 @@ -2,6 +2,7 @@ from plumbum.commands.processes import ProcessExecutionError from plumbum.commands.processes import ProcessTimedOut + class PopenAddons(object): """This adds a verify to popen objects to that the correct command is attributed when an error is thrown.""" @@ -9,24 +10,26 @@ def verify(self, retcode, timeout, stdout, stderr): """This verifies that the correct command is attributed.""" if getattr(self, "_timed_out", False): - raise ProcessTimedOut("Process did not terminate within %s seconds" % (timeout,), + raise ProcessTimedOut( + "Process did not terminate within %s seconds" % (timeout, ), getattr(self, "argv", None)) if retcode is not None: if hasattr(retcode, "__contains__"): if self.returncode not in retcode: - raise ProcessExecutionError(getattr(self, "argv", None), self.returncode, - stdout, stderr) + raise ProcessExecutionError( + getattr(self, "argv", None), self.returncode, stdout, + stderr) elif self.returncode != retcode: - raise ProcessExecutionError(getattr(self, "argv", None), self.returncode, - stdout, stderr) + raise ProcessExecutionError( + getattr(self, "argv", None), self.returncode, stdout, + stderr) 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 @@ -42,12 +45,12 @@ try: command = self[cmd] if not command.executable.exists(): - raise CommandNotFound(cmd,command.executable) + raise CommandNotFound(cmd, command.executable) else: return command except CommandNotFound: if othercommands: - return self.get(othercommands[0],*othercommands[1:]) + return self.get(othercommands[0], *othercommands[1:]) else: raise @@ -66,11 +69,15 @@ def encoding(self): 'This is a wrapper for custom_encoding' return self.custom_encoding + @encoding.setter def encoding(self, value): self.custom_encoding = value - - def daemonic_popen(self, command, cwd = "/", stdout=None, stderr=None, append=True): - raise NotImplementedError("This is not implemented on this machine!") - + 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.6.6/plumbum/machines/env.py python-plumbum-1.6.7/plumbum/machines/env.py --- python-plumbum-1.6.6/plumbum/machines/env.py 2016-12-28 02:59:15.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/env.py 2018-07-04 10:09:22.000000000 +0000 @@ -8,20 +8,28 @@ def __init__(self, path_factory, pathsep): self._path_factory = path_factory self._pathsep = pathsep + def append(self, path): list.append(self, self._path_factory(path)) + def extend(self, paths): list.extend(self, (self._path_factory(p) for p in paths)) + def insert(self, index, path): list.insert(self, index, self._path_factory(path)) + def index(self, path): list.index(self, self._path_factory(path)) + def __contains__(self, path): return list.__contains__(self, self._path_factory(path)) + def remove(self, path): list.remove(self, self._path_factory(path)) + def update(self, text): self[:] = [self._path_factory(p) for p in text.split(self._pathsep)] + def join(self): return self._pathsep.join(str(p) for p in self) @@ -60,30 +68,39 @@ """Returns an iterator over the items ``(key, value)`` of current environment (like dict.items)""" return iter(self._curr.items()) + def __hash__(self): raise TypeError("unhashable type") + def __len__(self): """Returns the number of elements of the current environment""" return len(self._curr) + def __contains__(self, name): """Tests whether an environment variable exists in the current environment""" return (name if self.CASE_SENSITIVE else name.upper()) in self._curr + def __getitem__(self, name): """Returns the value of the given environment variable from current environment, raising a ``KeyError`` if it does not exist""" return self._curr[name if self.CASE_SENSITIVE else name.upper()] + def keys(self): """Returns the keys of the current environment (like dict.keys)""" return self._curr.keys() + def items(self): """Returns the items of the current environment (like dict.items)""" return self._curr.items() + def values(self): """Returns the values of the current environment (like dict.values)""" return self._curr.values() + def get(self, name, *default): """Returns the keys of the current environment (like dict.keys)""" - return self._curr.get((name if self.CASE_SENSITIVE else name.upper()), *default) + return self._curr.get((name if self.CASE_SENSITIVE else name.upper()), + *default) def __delitem__(self, name): """Deletes an environment variable from the current environment""" @@ -91,12 +108,14 @@ del self._curr[name] if name == "PATH": self._update_path() + def __setitem__(self, name, value): """Sets/replaces an environment variable's value in the current environment""" name = name if self.CASE_SENSITIVE else name.upper() self._curr[name] = value if name == "PATH": self._update_path() + def pop(self, name, *default): """Pops an element from the current environment (like dict.pop)""" name = name if self.CASE_SENSITIVE else name.upper() @@ -104,10 +123,12 @@ if name == "PATH": self._update_path() return res + def clear(self): """Clears the current environment (like dict.clear)""" self._curr.clear() self._update_path() + def update(self, *args, **kwargs): """Updates the current environment (like dict.update)""" self._curr.update(*args, **kwargs) @@ -129,20 +150,23 @@ def _get_home(self): if "HOME" in self: return self._path_factory(self["HOME"]) - elif "USERPROFILE" in self: # pragma: no cover + elif "USERPROFILE" in self: # pragma: no cover return self._path_factory(self["USERPROFILE"]) - elif "HOMEPATH" in self: # pragma: no cover - return self._path_factory(self.get("HOMEDRIVE", ""), self["HOMEPATH"]) + elif "HOMEPATH" in self: # pragma: no cover + return self._path_factory( + self.get("HOMEDRIVE", ""), self["HOMEPATH"]) return None + def _set_home(self, p): if "HOME" in self: self["HOME"] = str(p) - elif "USERPROFILE" in self: # pragma: no cover + elif "USERPROFILE" in self: # pragma: no cover self["USERPROFILE"] = str(p) - elif "HOMEPATH" in self: # pragma: no cover + elif "HOMEPATH" in self: # pragma: no cover self["HOMEPATH"] = str(p) - else: # pragma: no cover + else: # pragma: no cover self["HOME"] = str(p) + home = property(_get_home, _set_home) """Get or set the home path""" @@ -150,7 +174,8 @@ def user(self): """Return the user name, or ``None`` if it is not set""" # adapted from getpass.getuser() - for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'): # pragma: no branch + for name in ('LOGNAME', 'USER', 'LNAME', + 'USERNAME'): # pragma: no branch if name in self: return self[name] try: @@ -160,4 +185,3 @@ return None else: return pwd.getpwuid(os.getuid())[0] # @UndefinedVariable - diff -Nru python-plumbum-1.6.6/plumbum/machines/local.py python-plumbum-1.6.7/plumbum/machines/local.py --- python-plumbum-1.6.6/plumbum/machines/local.py 2017-11-27 14:24:20.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/local.py 2018-07-04 10:09:22.000000000 +0000 @@ -32,6 +32,7 @@ from subprocess import Popen, PIPE has_new_subprocess = False + class PlumbumLocalPopen(PopenAddons): iter_lines = iter_lines @@ -44,6 +45,7 @@ def __getattr__(self, name): return getattr(self._proc, name) + if IS_WIN32: from plumbum.machines._windows import get_pe_subsystem, IMAGE_SUBSYSTEM_WINDOWS_CUI @@ -95,6 +97,7 @@ os.environ = prev return output + #=================================================================================================== # Local Commands #=================================================================================================== @@ -102,21 +105,26 @@ __slots__ = () QUOTE_LEVEL = 2 - def __init__(self, executable, encoding = "auto"): - ConcreteCommand.__init__(self, executable, - local.custom_encoding if encoding == "auto" else encoding) + def __init__(self, executable, encoding="auto"): + ConcreteCommand.__init__( + self, executable, local.custom_encoding + if encoding == "auto" else encoding) @property def machine(self): return local - def popen(self, args = (), cwd = None, env = None, **kwargs): + def popen(self, args=(), cwd=None, env=None, **kwargs): if isinstance(args, six.string_types): - args = (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, + args = (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 #=================================================================================================== @@ -144,7 +152,8 @@ self._as_user_stack = [] if IS_WIN32: - _EXTENSIONS = [""] + env.get("PATHEXT", ":.exe:.bat").lower().split(os.path.pathsep) + _EXTENSIONS = [""] + env.get("PATHEXT", ":.exe:.bat").lower().split( + os.path.pathsep) @classmethod def _which(cls, progname): @@ -156,6 +165,7 @@ return fn return None else: + @classmethod def _which(cls, progname): for p in cls.env.path: @@ -193,7 +203,7 @@ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, RemotePath): - raise TypeError("Cannot construct LocalPath from %r" % (p,)) + raise TypeError("Cannot construct LocalPath from %r" % (p, )) parts2.append(self.env.expanduser(str(p))) return LocalPath(os.path.join(*parts2)) @@ -224,22 +234,34 @@ # search for command return LocalCommand(self.which(cmd)) else: - raise TypeError("cmd must not be a RemotePath: %r" % (cmd,)) + raise TypeError("cmd must not be a RemotePath: %r" % (cmd, )) - def _popen(self, executable, argv, stdin = PIPE, stdout = PIPE, stderr = PIPE, - cwd = None, env = None, new_session = False, **kwargs): + 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 IS_WIN32: - kwargs["creationflags"] = kwargs.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP + kwargs["creationflags"] = kwargs.get( + "creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP else: - def preexec_fn(prev_fn = kwargs.get("preexec_fn", lambda: None)): + + def preexec_fn(prev_fn=kwargs.get("preexec_fn", lambda: None)): os.setsid() prev_fn() + kwargs["preexec_fn"] = preexec_fn - if IS_WIN32 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): subsystem = get_pe_subsystem(str(executable)) if subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI: @@ -254,7 +276,8 @@ sui.wShowWindow = subprocess.SW_HIDE # @UndefinedVariable if not has_new_subprocess and "close_fds" not in kwargs: - if IS_WIN32 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: @@ -271,15 +294,26 @@ argv, executable = self._as_user_stack[-1](argv) logger.debug("Running %r", argv) - proc = PlumbumLocalPopen(argv, executable = str(executable), stdin = stdin, stdout = stdout, - stderr = stderr, cwd = str(cwd), env = env, **kwargs) # bufsize = 4096 + proc = PlumbumLocalPopen( + argv, + executable=str(executable), + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=str(cwd), + env=env, + **kwargs) # bufsize = 4096 proc._start_time = time.time() proc.custom_encoding = self.custom_encoding proc.argv = argv return proc - - def daemonic_popen(self, command, cwd = "/", stdout=None, stderr=None, append=True): + def daemonic_popen(self, + command, + cwd="/", + stdout=None, + stderr=None, + append=True): """ On POSIX systems: @@ -303,6 +337,7 @@ return posix_daemonize(command, cwd, stdout, stderr, append) if IS_WIN32: + def list_processes(self): """ Returns information about all running processes (on Windows: using ``tasklist``) @@ -323,9 +358,10 @@ statidx = header.index('Status') useridx = header.index('User Name') for row in rows: - yield ProcInfo(int(row[pididx]), row[useridx], - row[statidx], row[imgidx]) + yield ProcInfo( + int(row[pididx]), row[useridx], row[statidx], row[imgidx]) else: + def list_processes(self): """ Returns information about all running processes (on POSIX systems: using ``ps``) @@ -334,10 +370,12 @@ """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() - lines.pop(0) # header + lines.pop(0) # header for line in lines: parts = line.strip().split() - yield ProcInfo(int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:])) + yield ProcInfo( + int(parts[0]), int(parts[1]), parts[2], + " ".join(parts[3:])) def pgrep(self, pattern): """ @@ -348,10 +386,10 @@ if pat.search(procinfo.args): yield procinfo - def session(self, new_session = False): + def session(self, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes ``/bin/sh`` and executes commands on it over stdin/stdout/stderr""" - return ShellSession(self["sh"].popen(new_session = new_session)) + return ShellSession(self["sh"].popen(new_session=new_session)) @contextmanager def tempdir(self): @@ -364,7 +402,7 @@ dir.delete() @contextmanager - def as_user(self, username = None): + def as_user(self, username=None): """Run nested commands as the given user. For example:: head = local["head"] @@ -381,7 +419,8 @@ '"' + " ".join(str(a) for a in argv) + '"'], self.which("runas"))) else: if username is None: - self._as_user_stack.append(lambda argv: (["sudo"] + list(argv), self.which("sudo"))) + self._as_user_stack.append( + lambda argv: (["sudo"] + list(argv), self.which("sudo"))) else: self._as_user_stack.append(lambda argv: (["sudo", "-u", username] + list(argv), self.which("sudo"))) try: @@ -396,6 +435,7 @@ python = LocalCommand(sys.executable, custom_encoding) """A command that represents the current python interpreter (``sys.executable``)""" + local = LocalMachine() """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, diff -Nru python-plumbum-1.6.6/plumbum/machines/paramiko_machine.py python-plumbum-1.6.7/plumbum/machines/paramiko_machine.py --- python-plumbum-1.6.6/plumbum/machines/paramiko_machine.py 2017-12-05 20:08:12.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/paramiko_machine.py 2018-07-04 10:09:22.000000000 +0000 @@ -13,20 +13,31 @@ # Sigh... we need to gracefully-import paramiko for Sphinx builds, etc import paramiko except ImportError: + class paramiko(object): def __nonzero__(self): return False + __bool__ = __nonzero__ + def __getattr__(self, name): raise ImportError("No module named paramiko") - paramiko = paramiko() + paramiko = paramiko() logger = logging.getLogger("plumbum.paramiko") + class ParamikoPopen(PopenAddons): - def __init__(self, argv, stdin, stdout, stderr, encoding, stdin_file = None, - stdout_file = None, stderr_file = None): + def __init__(self, + argv, + stdin, + stdout, + stderr, + encoding, + stdin_file=None, + stdout_file=None, + stderr_file=None): self.argv = argv self.channel = stdout.channel self.stdin = stdin @@ -38,34 +49,43 @@ self.stdin_file = stdin_file self.stdout_file = stdout_file self.stderr_file = stderr_file + def poll(self): if self.returncode is None: if self.channel.exit_status_ready(): return self.wait() return self.returncode + def wait(self): if self.returncode is None: self.channel.recv_exit_status() self.returncode = self.channel.exit_status self.close() return self.returncode + def close(self): self.channel.shutdown_read() self.channel.shutdown_write() self.channel.close() + def kill(self): # possible way to obtain pid: # "(cmd ; echo $?) & echo ?!" # and then client.exec_command("kill -9 %s" % (pid,)) - raise EnvironmentError("Cannot kill remote processes, we don't have their PIDs") + raise EnvironmentError( + "Cannot kill remote processes, we don't have their PIDs") + terminate = kill + def send_signal(self, sig): raise NotImplementedError() + def communicate(self): stdout = [] stderr = [] infile = self.stdin_file - sources = [("1", stdout, self.stdout, self.stdout_file), ("2", stderr, self.stderr, self.stderr_file)] + sources = [("1", stdout, self.stdout, self.stdout_file), + ("2", stderr, self.stderr, self.stderr_file)] i = 0 while sources: if infile: @@ -100,11 +120,14 @@ def iter_lines(self, timeout=None, **kwargs): if timeout is not None: - raise NotImplementedError("The 'timeout' parameter is not supported with ParamikoMachine") + raise NotImplementedError( + "The 'timeout' parameter is not supported with ParamikoMachine" + ) return iter_lines(self, _iter_lines=_iter_lines, **kwargs) __iter__ = iter_lines + class ParamikoMachine(BaseRemoteMachine): """ An implementation of :class:`remote machine ` @@ -158,21 +181,38 @@ class RemoteCommand(BaseRemoteMachine.RemoteCommand): def __or__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") + def __gt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") + def __rshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") + def __ge__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") + def __lt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") + def __lshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") - def __init__(self, host, user = None, port = None, password = None, keyfile = None, - load_system_host_keys = True, missing_host_policy = None, encoding = "utf8", - look_for_keys = None, connect_timeout = None, keep_alive = 0, gss_auth = False, - gss_kex = None, gss_deleg_creds = None, gss_host = None): + def __init__(self, + host, + user=None, + port=None, + password=None, + keyfile=None, + load_system_host_keys=True, + missing_host_policy=None, + encoding="utf8", + look_for_keys=None, + connect_timeout=None, + keep_alive=0, + gss_auth=False, + gss_kex=None, + gss_deleg_creds=None, + gss_host=None): self.host = host kwargs = {} if user: @@ -208,7 +248,7 @@ BaseRemoteMachine.__init__(self, encoding, connect_timeout) def __str__(self): - return "paramiko://%s" % (self._fqhost,) + return "paramiko://%s" % (self._fqhost, ) def close(self): BaseRemoteMachine.close(self) @@ -225,7 +265,12 @@ return self._sftp @_setdoc(BaseRemoteMachine) - def session(self, isatty = False, term = "vt100", width = 80, height = 24, new_session = False): + def session(self, + isatty=False, + term="vt100", + width=80, + height=24, + new_session=False): # new_session is ignored for ParamikoMachine trans = self._client.get_transport() trans.set_keepalive(self._keep_alive) @@ -237,11 +282,18 @@ stdin = chan.makefile('wb', -1) stdout = chan.makefile('rb', -1) stderr = chan.makefile_stderr('rb', -1) - proc = ParamikoPopen([""], stdin, stdout, stderr, self.custom_encoding) + proc = ParamikoPopen([""], stdin, stdout, stderr, + self.custom_encoding) return ShellSession(proc, self.custom_encoding, isatty) @_setdoc(BaseRemoteMachine) - def popen(self, args, stdin = None, stdout = None, stderr = None, new_session = False, cwd = None): + def popen(self, + args, + stdin=None, + stdout=None, + stderr=None, + new_session=False, + cwd=None): # new_session is ignored for ParamikoMachine argv = [] envdelta = self.env.getdelta() @@ -253,19 +305,28 @@ cmdline = " ".join(argv) logger.debug(cmdline) si, so, se = streams = self._client.exec_command(cmdline, 1) - return ParamikoPopen(argv, si, so, se, self.custom_encoding, stdin_file = stdin, - stdout_file = stdout, stderr_file = stderr) + return ParamikoPopen( + argv, + si, + so, + se, + self.custom_encoding, + stdin_file=stdin, + stdout_file=stdout, + stderr_file=stderr) @_setdoc(BaseRemoteMachine) def download(self, src, dst): if isinstance(src, LocalPath): - raise TypeError("src of download cannot be %r" % (src,)) + raise TypeError("src of download cannot be %r" % (src, )) if isinstance(src, RemotePath) and src.remote != self: - raise TypeError("src %r points to a different remote machine" % (src,)) + raise TypeError( + "src %r points to a different remote machine" % (src, )) if isinstance(dst, RemotePath): - raise TypeError("dst of download cannot be %r" % (dst,)) - return self._download(src if isinstance(src, RemotePath) else self.path(src), - dst if isinstance(dst, LocalPath) else LocalPath(dst)) + raise TypeError("dst of download cannot be %r" % (dst, )) + return self._download( + src if isinstance(src, RemotePath) else self.path(src), dst + if isinstance(dst, LocalPath) else LocalPath(dst)) def _download(self, src, dst): if src.is_dir(): @@ -281,13 +342,15 @@ @_setdoc(BaseRemoteMachine) def upload(self, src, dst): if isinstance(src, RemotePath): - raise TypeError("src of upload cannot be %r" % (src,)) + raise TypeError("src of upload cannot be %r" % (src, )) if isinstance(dst, LocalPath): - raise TypeError("dst of upload cannot be %r" % (dst,)) + raise TypeError("dst of upload cannot be %r" % (dst, )) if isinstance(dst, RemotePath) and dst.remote != self: - raise TypeError("dst %r points to a different remote machine" % (dst,)) - return self._upload(src if isinstance(src, LocalPath) else LocalPath(src), - dst if isinstance(dst, RemotePath) else self.path(dst)) + raise TypeError( + "dst %r points to a different remote machine" % (dst, )) + return self._upload( + src if isinstance(src, LocalPath) else LocalPath(src), dst + if isinstance(dst, RemotePath) else self.path(dst)) def _upload(self, src, dst): if src.is_dir(): @@ -300,7 +363,7 @@ else: self.sftp.put(str(src), str(dst)) - def connect_sock(self, dport, dhost = "localhost", ipv6 = False): + def connect_sock(self, dport, dhost="localhost", ipv6=False): """Returns a Paramiko ``Channel``, connected to dhost:dport on the remote machine. The ``Channel`` behaves like a regular socket; you can ``send`` and ``recv`` on it and the data will pass encrypted over SSH. Usage:: @@ -330,12 +393,14 @@ data = f.read() f.close() return data + def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) f = self.sftp.open(str(fn), 'wb') f.write(data) f.close() + def _path_stat(self, fn): try: st = self.sftp.stat(str(fn)) @@ -343,8 +408,8 @@ if e.errno == errno.ENOENT: return None raise OSError(e.errno) - res = StatRes((st.st_mode, 0, 0, 0, st.st_uid, st.st_gid, - st.st_size, st.st_atime, st.st_mtime, 0)) + res = StatRes((st.st_mode, 0, 0, 0, st.st_uid, st.st_gid, st.st_size, + st.st_atime, st.st_mtime, 0)) if stat.S_ISDIR(st.st_mode): res.text_mode = 'directory' @@ -353,7 +418,6 @@ return res - ################################################################################################### # Make paramiko.Channel adhere to the socket protocol, namely, send and recv should fail # when the socket has been closed @@ -361,12 +425,15 @@ class SocketCompatibleChannel(object): def __init__(self, chan): self._chan = chan + def __getattr__(self, name): return getattr(self._chan, name) + def send(self, s): if self._chan.closed: raise socket.error(errno.EBADF, 'Bad file descriptor') return self._chan.send(s) + def recv(self, count): if self._chan.closed: raise socket.error(errno.EBADF, 'Bad file descriptor') @@ -383,6 +450,7 @@ except ImportError: # Pre Python 3.4 implementation from select import select + def selector(): while True: rlist, _, _ = select([proc.stdout.channel], [], []) diff -Nru python-plumbum-1.6.6/plumbum/machines/remote.py python-plumbum-1.6.7/plumbum/machines/remote.py --- python-plumbum-1.6.6/plumbum/machines/remote.py 2017-09-21 17:52:14.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/remote.py 2018-07-04 10:09:22.000000000 +0000 @@ -22,38 +22,41 @@ # from plain env. env0 = session.run("env -0; echo") if env0[0] == 0 and not env0[2].rstrip(): - self._curr = dict(line.split('=', 1) - for line in env0[1].split('\x00') - if '=' in line) + self._curr = dict( + line.split('=', 1) for line in env0[1].split('\x00') + if '=' in line) else: lines = session.run("env; echo")[1].splitlines() split = (line.split('=', 1) for line in lines) - keys = (line[0] for line in split if len(line)>1) + keys = (line[0] for line in split if len(line) > 1) runs = ((key, session.run('printenv "%s"; echo' % key)) for key in keys) - self._curr = dict((key, run[1].rstrip('\n')) for (key, run) in runs - if run[0] == 0 and run[1].rstrip('\n') - and not run[2]) + self._curr = dict( + (key, run[1].rstrip('\n')) for (key, run) in runs + if run[0] == 0 and run[1].rstrip('\n') and not run[2]) self._orig = self._curr.copy() BaseEnv.__init__(self, self.remote.path, ":") @_setdoc(BaseEnv) def __delitem__(self, name): BaseEnv.__delitem__(self, name) - self.remote._session.run("unset %s" % (name,)) + self.remote._session.run("unset %s" % (name, )) + @_setdoc(BaseEnv) def __setitem__(self, name, value): BaseEnv.__setitem__(self, name, value) self.remote._session.run("export %s=%s" % (name, shquote(value))) + @_setdoc(BaseEnv) def pop(self, name, *default): BaseEnv.pop(self, name, *default) - self.remote._session.run("unset %s" % (name,)) + self.remote._session.run("unset %s" % (name, )) + @_setdoc(BaseEnv) def update(self, *args, **kwargs): BaseEnv.update(self, *args, **kwargs) - self.remote._session.run("export " + - " ".join("%s=%s" % (k, shquote(v)) for k, v in self.getdict().items())) + self.remote._session.run("export " + " ".join( + "%s=%s" % (k, shquote(v)) for k, v in self.getdict().items())) def expand(self, expr): """Expands any environment variables and home shortcuts found in ``expr`` @@ -100,32 +103,42 @@ __slots__ = ["remote", "executable"] QUOTE_LEVEL = 1 - def __init__(self, remote, executable, encoding = "auto"): + def __init__(self, remote, executable, encoding="auto"): self.remote = remote - ConcreteCommand.__init__(self, executable, - remote.custom_encoding if encoding == "auto" else encoding) + ConcreteCommand.__init__( + self, executable, remote.custom_encoding + if encoding == "auto" else encoding) + @property def machine(self): return self.remote + def __repr__(self): return "RemoteCommand(%r, %r)" % (self.remote, self.executable) - def popen(self, args = (), **kwargs): + + 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 self.machine.daemonic_popen(self, cwd, stdout, stderr, append) + class ClosedRemoteMachine(Exception): pass + class ClosedRemote(object): __slots__ = ["_obj", "__weakref__"] + def __init__(self, obj): self._obj = obj + def close(self): pass + def __getattr__(self, name): - raise ClosedRemoteMachine("%r has been closed" % (self._obj,)) + raise ClosedRemoteMachine("%r has been closed" % (self._obj, )) class BaseRemoteMachine(BaseMachine): @@ -153,20 +166,22 @@ self._cwd = RemoteWorkdir(self) return self._cwd - def __init__(self, encoding = "utf8", connect_timeout = 10, new_session = False): + def __init__(self, encoding="utf8", connect_timeout=10, new_session=False): self.custom_encoding = encoding self.connect_timeout = connect_timeout - self._session = self.session(new_session = new_session) + self._session = self.session(new_session=new_session) self.uname = self._get_uname() self.env = RemoteEnv(self) self._python = None def _get_uname(self): - rc, out, _ = self._session.run("uname", retcode = None) + rc, out, _ = self._session.run("uname", retcode=None) if rc == 0: return out.strip() else: - rc, out, _ = self._session.run("python -c 'import platform;print(platform.uname()[0])'", retcode = None) + rc, out, _ = self._session.run( + "python -c 'import platform;print(platform.uname()[0])'", + retcode=None) if rc == 0: return out.strip() else: @@ -178,8 +193,10 @@ def __enter__(self): return self + def __exit__(self, t, v, tb): self.close() + def close(self): """closes the connection to the remote machine; all paths and programs will become defunct""" @@ -193,7 +210,7 @@ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, LocalPath): - raise TypeError("Cannot construct RemotePath from %r" % (p,)) + raise TypeError("Cannot construct RemotePath from %r" % (p, )) parts2.append(self.expanduser(str(p))) return RemotePath(self, *parts2) @@ -232,14 +249,16 @@ if cmd.remote is self: return self.RemoteCommand(self, cmd) else: - raise TypeError("Given path does not belong to this remote machine: %r" % (cmd,)) + raise TypeError( + "Given path does not belong to this remote machine: %r" % + (cmd, )) elif not isinstance(cmd, LocalPath): if "/" in cmd or "\\" in cmd: return self.RemoteCommand(self, self.path(cmd)) else: return self.RemoteCommand(self, self.which(cmd)) else: - raise TypeError("cmd must not be a LocalPath: %r" % (cmd,)) + raise TypeError("cmd must not be a LocalPath: %r" % (cmd, )) @property def python(self): @@ -248,7 +267,7 @@ self._python = self["python"] return self._python - def session(self, isatty = False, new_session = False): + def session(self, isatty=False, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes the user's shell on the remote machine and executes commands on it over stdin/stdout/stderr""" raise NotImplementedError() @@ -281,10 +300,11 @@ """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() - lines.pop(0) # header + lines.pop(0) # header for line in lines: parts = line.strip().split() - yield ProcInfo(int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:])) + yield ProcInfo( + int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:])) def pgrep(self, pattern): """ @@ -310,51 +330,65 @@ # Path implementation # def _path_listdir(self, fn): - files = self._session.run("ls -a %s" % (shquote(fn),))[1].splitlines() + files = self._session.run("ls -a %s" % (shquote(fn), ))[1].splitlines() files.remove(".") files.remove("..") return files + def _path_glob(self, fn, pattern): # shquote does not work here due to the way bash loops use space as a seperator pattern = pattern.replace(" ", r"\ ") fn = fn.replace(" ", r"\ ") - matches = self._session.run(r'for fn in {0}/{1}; do echo $fn; done'.format(fn,pattern))[1].splitlines() + matches = self._session.run( + r'for fn in {0}/{1}; do echo $fn; done'.format( + fn, pattern))[1].splitlines() if len(matches) == 1 and not self._path_stat(matches[0]): return [] # pattern expansion failed return matches def _path_getuid(self, fn): - stat_cmd = "stat -c '%u,%U' " if self.uname not in ('Darwin', 'FreeBSD') 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 not in ('Darwin', 'FreeBSD') 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 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' " - rc, out, _ = self._session.run(stat_cmd + shquote(fn), retcode = None) + rc, out, _ = self._session.run(stat_cmd + shquote(fn), retcode=None) if rc != 0: return None statres = out.strip().split(",") text_mode = statres.pop(0).lower() - res = StatRes((int(statres[0], 16),) + tuple(int(sr) for sr in statres[1:])) + res = StatRes((int(statres[0], 16), ) + tuple( + int(sr) for sr in statres[1:])) res.text_mode = text_mode return res def _path_delete(self, fn): - self._session.run("rm -rf %s" % (shquote(fn),)) + self._session.run("rm -rf %s" % (shquote(fn), )) + def _path_move(self, src, dst): self._session.run("mv %s %s" % (shquote(src), shquote(dst))) + def _path_copy(self, src, dst): self._session.run("cp -r %s %s" % (shquote(src), shquote(dst))) + def _path_mkdir(self, fn): - self._session.run("mkdir -p %s" % (shquote(fn),)) + self._session.run("mkdir -p %s" % (shquote(fn), )) + def _path_chmod(self, mode, fn): self._session.run("chmod %o %s" % (mode, shquote(fn))) + def _path_touch(self, path): self._session.run("touch {path}".format(path=path)) + def _path_chown(self, fn, owner, group, recursive): args = ["chown"] if recursive: @@ -364,7 +398,7 @@ elif owner is not None: args.append(str(owner)) elif group is not None: - args.append(":%s" % (group,)) + args.append(":%s" % (group, )) args.append(shquote(fn)) self._session.run(" ".join(args)) @@ -373,6 +407,7 @@ if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) return data + def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) @@ -383,15 +418,18 @@ self.upload(f.name, fn) def _path_link(self, src, dst, symlink): - self._session.run("ln %s %s %s" % ("-s" if symlink else "", shquote(src), shquote(dst))) + self._session.run( + "ln %s %s %s" % ("-s" + if symlink else "", shquote(src), shquote(dst))) @_setdoc(BaseEnv) def expand(self, expr): - return self._session.run("echo %s" % (expr,))[1].strip() + return self._session.run("echo %s" % (expr, ))[1].strip() @_setdoc(BaseEnv) def expanduser(self, expr): if not any(part.startswith("~") for part in expr.split("/")): return expr # we escape all $ signs to avoid expanding env-vars - return self._session.run("echo %s" % (expr.replace("$", "\\$"),))[1].strip() + return self._session.run( + "echo %s" % (expr.replace("$", "\\$"), ))[1].strip() diff -Nru python-plumbum-1.6.6/plumbum/machines/session.py python-plumbum-1.6.7/plumbum/machines/session.py --- python-plumbum-1.6.6/plumbum/machines/session.py 2018-02-12 14:38:16.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/session.py 2018-07-04 10:09:22.000000000 +0000 @@ -12,19 +12,24 @@ :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""" + class IncorrectLogin(SSHCommsError): """Raises when incorrect login credentials are provided""" + class HostPublicKeyUnknown(SSHCommsError): """Raises when the host public key isn't known""" + shell_logger = logging.getLogger("plumbum.shell") @@ -35,17 +40,20 @@ """A pipe-like object from which you can read lines; the pipe will return report EOF (the empty string) when a special marker is detected""" __slots__ = ["pipe", "marker", "__weakref__"] + def __init__(self, pipe, marker): self.pipe = pipe self.marker = marker if six.PY3: self.marker = six.bytes(self.marker, "ascii") + def close(self): - """'Closes' the marked pipe; following calls to ``readline`` will return """"" + """'Closes' the marked pipe; following calls to ``readline`` will return """ "" # consume everything while self.readline(): pass self.pipe = None + def readline(self): """Reads the next line from the pipe; returns "" when the special marker is reached. Raises ``EOFError`` if the underlying pipe has closed""" @@ -63,6 +71,7 @@ class SessionPopen(PopenAddons): """A shell-session-based ``Popen``-like object (has the following attributes: ``stdin``, ``stdout``, ``stderr``, ``returncode``)""" + def __init__(self, proc, argv, isatty, stdin, stdout, stderr, encoding): self.proc = proc self.argv = argv @@ -73,17 +82,20 @@ self.custom_encoding = encoding self.returncode = None self._done = False + def poll(self): """Returns the process' exit code or ``None`` if it's still running""" if self._done: return self.returncode else: return None + def wait(self): """Waits for the process to terminate and returns its exit code""" self.communicate() return self.returncode - def communicate(self, input = None): + + def communicate(self, input=None): """Consumes the process' stdout and stderr until the it terminates. :param input: An optional bytes/buffer object to send to the process over stdin @@ -113,12 +125,15 @@ self.proc.poll() returncode = self.proc.returncode if returncode == 5: - raise IncorrectLogin("Incorrect username or password provided") + raise IncorrectLogin( + "Incorrect username or password provided") elif returncode == 6: - raise HostPublicKeyUnknown("The authenticity of the host can't be established") + raise HostPublicKeyUnknown( + "The authenticity of the host can't be established") 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) + raise SSHCommsChannel2Error( + msgerr) if name == "2" else SSHCommsError(msg) if not line: del sources[i] else: @@ -155,15 +170,19 @@ :param connect_timeout: The timeout to connect to the shell, after which, if no prompt is seen, the shell process is killed """ - def __init__(self, proc, encoding = "auto", isatty = False, connect_timeout = 5): + + def __init__(self, proc, encoding="auto", isatty=False, connect_timeout=5): self.proc = proc self.custom_encoding = proc.custom_encoding if encoding == "auto" else encoding self.isatty = isatty self._current = None if connect_timeout: + def closer(): - shell_logger.error("Connection to %s timed out (%d sec)", proc, connect_timeout) + shell_logger.error("Connection to %s timed out (%d sec)", proc, + connect_timeout) self.close() + timer = threading.Timer(connect_timeout, self.close) timer.start() try: @@ -174,8 +193,10 @@ def __enter__(self): return self + def __exit__(self, t, v, tb): self.close() + def __del__(self): try: self.close() @@ -218,31 +239,33 @@ if self.proc is None: raise ShellSessionError("Shell session has already been closed") if self._current and not self._current._done: - raise ShellSessionError("Each shell may start only one process at a time") + raise ShellSessionError( + "Each shell may start only one process at a time") if isinstance(cmd, BaseCommand): full_cmd = cmd.formulate(1) else: full_cmd = cmd - marker = "--.END%s.--" % (time.time() * random.random(),) + marker = "--.END%s.--" % (time.time() * random.random(), ) if full_cmd.strip(): full_cmd += " ; " else: full_cmd = "true ; " - full_cmd += "echo $? ; echo '%s'" % (marker,) + full_cmd += "echo $? ; echo '%s'" % (marker, ) if not self.isatty: - full_cmd += " ; echo '%s' 1>&2" % (marker,) + full_cmd += " ; echo '%s' 1>&2" % (marker, ) if self.custom_encoding: full_cmd = full_cmd.encode(self.custom_encoding) shell_logger.debug("Running %r", full_cmd) self.proc.stdin.write(full_cmd + six.b("\n")) self.proc.stdin.flush() - self._current = SessionPopen(self.proc, full_cmd, self.isatty, self.proc.stdin, - MarkedPipe(self.proc.stdout, marker), MarkedPipe(self.proc.stderr, marker), - self.custom_encoding) + self._current = SessionPopen( + self.proc, full_cmd, self.isatty, self.proc.stdin, + MarkedPipe(self.proc.stdout, marker), + MarkedPipe(self.proc.stderr, marker), self.custom_encoding) return self._current - def run(self, cmd, retcode = 0): + def run(self, cmd, retcode=0): """Runs the given command :param cmd: The command (string or :class:`Command ` object) @@ -252,4 +275,3 @@ :returns: A tuple of (return code, stdout, stderr) """ return run_proc(self.popen(cmd), retcode) - diff -Nru python-plumbum-1.6.6/plumbum/machines/ssh_machine.py python-plumbum-1.6.7/plumbum/machines/ssh_machine.py --- python-plumbum-1.6.6/plumbum/machines/ssh_machine.py 2018-01-24 09:56:38.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/machines/ssh_machine.py 2018-07-04 10:09:22.000000000 +0000 @@ -12,17 +12,22 @@ """An object representing an SSH tunnel (created by :func:`SshMachine.tunnel `)""" __slots__ = ["_session", "__weakref__"] + def __init__(self, session): self._session = session + def __repr__(self): if self._session.alive(): - return "" % (self._session.proc,) + return "" % (self._session.proc, ) else: return "" + def __enter__(self): return self + def __exit__(self, t, v, tb): self.close() + def close(self): """Closes(terminates) the tunnel""" self._session.close() @@ -70,9 +75,19 @@ Ctrl+C (SIGINT) """ - def __init__(self, host, user = None, port = None, keyfile = None, ssh_command = None, - scp_command = None, ssh_opts = (), scp_opts = (), password = None, encoding = "utf8", - connect_timeout = 10, new_session = False): + def __init__(self, + host, + user=None, + port=None, + keyfile=None, + ssh_command=None, + scp_command=None, + ssh_opts=(), + scp_opts=(), + password=None, + encoding="utf8", + connect_timeout=10, + new_session=False): if ssh_command is None: if password is not None: @@ -102,14 +117,17 @@ scp_args.extend(scp_opts) self._ssh_command = ssh_command[tuple(ssh_args)] self._scp_command = scp_command[tuple(scp_args)] - BaseRemoteMachine.__init__(self, encoding = encoding, connect_timeout = connect_timeout, - new_session = new_session) + BaseRemoteMachine.__init__( + self, + encoding=encoding, + connect_timeout=connect_timeout, + new_session=new_session) def __str__(self): - return "ssh://%s" % (self._fqhost,) + return "ssh://%s" % (self._fqhost, ) @_setdoc(BaseRemoteMachine) - def popen(self, args, ssh_opts = (), **kwargs): + def popen(self, args, ssh_opts=(), **kwargs): cmdline = [] cmdline.extend(ssh_opts) cmdline.append(self._fqhost) @@ -131,10 +149,17 @@ 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): + 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. @@ -154,24 +179,36 @@ args = ["cd", str(cwd), "&&"] args.append("nohup") args.extend(command.formulate()) - args.extend([(">>" if append else ">")+str(stdout), - "2"+(">>" if (append and stderr!="&1") else ">")+str(stderr), ">" if append else ">") + str(stdout), + "2" + (">>" + if (append and stderr != "&1") else ">") + str(stderr), + "` and @@ -30,16 +32,21 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, str(self)) + def __div__(self, other): """Joins two paths""" return self.join(other) + __truediv__ = __div__ + def __floordiv__(self, expr): """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" return self.glob(expr) + def __iter__(self): """Iterate over the files in this directory""" return iter(self.list()) + def __eq__(self, other): if isinstance(other, Path): return self._get_info() == other._get_info() @@ -50,26 +57,34 @@ return str(self).lower() == other.lower() else: return NotImplemented + def __ne__(self, other): return not (self == other) + def __gt__(self, other): return str(self) > str(other) + def __ge__(self, other): return str(self) >= str(other) + def __lt__(self, other): return str(self) < str(other) + def __le__(self, other): return str(self) <= str(other) + def __hash__(self): if self.CASE_SENSITIVE: return hash(str(self)) else: return hash(str(self).lower()) + def __nonzero__(self): return bool(str(self)) + __bool__ = __nonzero__ - def __fpath__(self): + def __fspath__(self): """Added for Python 3.6 support""" return str(self) @@ -84,10 +99,12 @@ def _form(self, *parts): pass - def up(self, count = 1): + def up(self, count=1): """Go up in ``count`` directories (the default is 1)""" return self.join("../" * count) - def walk(self, filter = lambda p: True, dir_filter = lambda p: True): # @ReservedAssignment + + def walk(self, filter=lambda p: True, + dir_filter=lambda p: True): # @ReservedAssignment """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 @@ -133,6 +150,7 @@ @abstractproperty def suffix(self): """The suffix of this file""" + @abstractproperty def suffixes(self): """This is a list of all suffixes""" @@ -156,46 +174,59 @@ @abstractmethod def _get_info(self): pass + @abstractmethod def join(self, *parts): """Joins this path with any number of paths""" + @abstractmethod def list(self): """Returns the files in this directory""" + @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""" + 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""" + 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""" + @abstractmethod def exists(self): """Returns ``True`` if this path exists, ``False`` otherwise""" + @abstractmethod def stat(self): """Returns the os.stats for a file""" pass + @abstractmethod def with_name(self, name): """Returns a path with the name replaced""" + @abstractmethod def with_suffix(self, suffix, depth=1): """Returns a path with the suffix replaced. Up to last ``depth`` suffixes will be @@ -210,41 +241,52 @@ 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""" + @abstractmethod def delete(self): """Deletes this path (recursively, if a directory)""" + @abstractmethod def move(self, dst): """Moves this path to a different location""" + 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): + def copy(self, dst, override=False): """Copies this path (recursively, if a directory) to the destination path. Raises TypeError if dst exists and override is False.""" + @abstractmethod def mkdir(self): """Creates a directory at this path; if the directory already exists, silently ignore""" + @abstractmethod - def open(self, mode = "r"): + def open(self, mode="r"): """opens this path as a file""" + @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'``""" + @abstractmethod def write(self, data, encoding=None): """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'``""" + @abstractmethod def touch(self): """Update the access time. Creates an empty file if none exists.""" + @abstractmethod - def chown(self, owner = None, group = None, recursive = None): + 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 @@ -253,6 +295,7 @@ Only meaningful when ``self`` is a directory. If ``None``, the value will default to ``True`` if ``self`` is a directory, ``False`` otherwise. """ + @abstractmethod def chmod(self, mode): """Change the mode of path to the numeric mode. @@ -261,13 +304,19 @@ """ @staticmethod - def _access_mode_to_flags(mode, flags = {"f" : os.F_OK, "w" : os.W_OK, "r" : os.R_OK, "x" : os.X_OK}): + 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): + def access(self, mode=0): """Test file existence or permission bits :param mode: a bitwise-or of access bits, or a string-representation thereof: @@ -324,8 +373,12 @@ source = self._form(source) parts = self.split() baseparts = source.split() - ancestors = len(list(itertools.takewhile(lambda p: p[0] == p[1], zip(parts, baseparts)))) - return RelativePath([".."] * (len(baseparts) - ancestors) + parts[ancestors:]) + ancestors = len( + list( + itertools.takewhile(lambda p: p[0] == p[1], + zip(parts, baseparts)))) + return RelativePath([".."] * (len(baseparts) - ancestors) + + parts[ancestors:]) def __sub__(self, other): """Same as ``self.relative_to(other)``""" @@ -351,8 +404,9 @@ @property def parents(self): """Pathlib like sequence of ancestors""" - join = lambda x,y: self.__class__(x) / y - as_list = (reduce(join,self.parts[:i],self.parts[0]) for i in range(len(self.parts)-1,0,-1)) + join = lambda x, y: self.__class__(x) / y + as_list = (reduce(join, self.parts[:i], self.parts[0]) + for i in range(len(self.parts) - 1, 0, -1)) return tuple(as_list) @property @@ -373,40 +427,50 @@ def __init__(self, parts): self.parts = parts + def __str__(self): return "/".join(self.parts) + def __iter__(self): return iter(self.parts) + def __len__(self): return len(self.parts) + def __getitem__(self, index): return self.parts[index] + def __repr__(self): - return "RelativePath(%r)" % (self.parts,) + return "RelativePath(%r)" % (self.parts, ) def __eq__(self, other): return str(self) == str(other) + def __ne__(self, other): return not (self == other) + def __gt__(self, other): return str(self) > str(other) + def __ge__(self, other): return str(self) >= str(other) + def __lt__(self, other): return str(self) < str(other) + def __le__(self, other): return str(self) <= str(other) + def __hash__(self): return hash(str(self)) + def __nonzero__(self): return bool(str(self)) + __bool__ = __nonzero__ - def up(self, count = 1): + def up(self, count=1): return RelativePath(self.parts[:-count]) def __radd__(self, path): return path.join(*self.parts) - - - diff -Nru python-plumbum-1.6.6/plumbum/path/local.py python-plumbum-1.6.7/plumbum/path/local.py --- python-plumbum-1.6.6/plumbum/path/local.py 2017-08-28 21:14:56.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/path/local.py 2018-07-04 10:09:22.000000000 +0000 @@ -12,22 +12,26 @@ from pwd import getpwuid, getpwnam from grp import getgrgid, getgrnam except ImportError: - def getpwuid(x): # type: ignore - return (None,) - def getgrgid(x): # type: ignore - return (None,) - def getpwnam(x): # type: ignore + + def getpwuid(x): # type: ignore + return (None, ) + + def getgrgid(x): # type: ignore + return (None, ) + + def getpwnam(x): # type: ignore raise OSError("`getpwnam` not supported") - def getgrnam(x): # type: ignore + + def getgrnam(x): # type: ignore raise OSError("`getgrnam` not supported") -try: # Py3 + +try: # Py3 import urllib.parse as urlparse import urllib.request as urllib except ImportError: - import urlparse # type: ignore - import urllib # type: ignore - + import urlparse # type: ignore + import urllib # type: ignore logger = logging.getLogger("plumbum.local") @@ -48,9 +52,12 @@ if not parts: raise TypeError("At least one path part is required (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)))) + 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) @@ -61,17 +68,17 @@ def _form(self, *parts): return LocalPath(*parts) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def name(self): return os.path.basename(str(self)) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def dirname(self): return LocalPath(os.path.dirname(str(self))) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def suffix(self): return os.path.splitext(str(self))[1] @@ -87,14 +94,14 @@ else: return list(reversed(exts)) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def uid(self): uid = self.stat().st_uid name = getpwuid(uid)[0] return FSUser(uid, name) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def gid(self): gid = self.stat().st_gid @@ -140,17 +147,19 @@ def with_name(self, name): return LocalPath(self.dirname) / name - @property # type: ignore + @property # type: ignore @_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): + if (suffix and not suffix.startswith(os.path.extsep) + or suffix == os.path.extsep): raise ValueError("Invalid suffix %r" % (suffix)) name = self.name - depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) + 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) return LocalPath(self.dirname) / (name + suffix) @@ -169,7 +178,7 @@ else: try: os.remove(str(self)) - except OSError: # pragma: no cover + except OSError: # pragma: no cover # file might already been removed (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.ENOENT: @@ -183,7 +192,7 @@ return LocalPath(dst) @_setdoc(Path) - def copy(self, dst, override = False, overwrite = True): + def copy(self, dst, override=False, overwrite=True): if isinstance(dst, RemotePath): raise TypeError("Cannot copy local path %s to %r" % (self, dst)) dst = LocalPath(dst) @@ -205,18 +214,18 @@ if not self.exists(): try: os.makedirs(str(self)) - except OSError: # pragma: no cover + except OSError: # pragma: no cover # directory might already exist (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.EEXIST: raise @_setdoc(Path) - def open(self, mode = "r"): + def open(self, mode="r"): return open(str(self), mode) @_setdoc(Path) - def read(self, encoding=None, mode = 'r'): + def read(self, encoding=None, mode='r'): if encoding and 'b' not in mode: mode = mode + 'b' with self.open(mode) as f: @@ -226,7 +235,7 @@ return data @_setdoc(Path) - def write(self, data, encoding=None, mode = None): + def write(self, data, encoding=None, mode=None): if encoding: data = data.encode(encoding) if mode is None: @@ -243,11 +252,13 @@ os.utime(str(self), None) @_setdoc(Path) - def chown(self, owner = None, group = None, recursive = None): + def chown(self, owner=None, group=None, recursive=None): if not hasattr(os, "chown"): raise OSError("os.chown() not supported") - 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]) + 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.is_dir()): for subpath in self.walk(): @@ -260,13 +271,14 @@ os.chmod(str(self), mode) @_setdoc(Path) - def access(self, mode = 0): + def access(self, mode=0): return os.access(str(self), self._access_mode_to_flags(mode)) @_setdoc(Path) def link(self, dst): if isinstance(dst, RemotePath): - raise TypeError("Cannot create a hardlink from local path %s to %r" % (self, dst)) + raise TypeError("Cannot create a hardlink from local path %s to %r" + % (self, dst)) if hasattr(os, "link"): os.link(str(self), str(dst)) else: @@ -280,7 +292,8 @@ @_setdoc(Path) def symlink(self, dst): if isinstance(dst, RemotePath): - raise TypeError("Cannot create a symlink from local path %s to %r" % (self, dst)) + raise TypeError("Cannot create a symlink from local path %s to %r" + % (self, dst)) if hasattr(os, "symlink"): os.symlink(str(self), str(dst)) else: @@ -299,7 +312,7 @@ else: # windows: use rmdir for directories and directory symlinks os.rmdir(str(self)) - except OSError: # pragma: no cover + except OSError: # pragma: no cover # file might already been removed (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.ENOENT: @@ -307,26 +320,26 @@ @_setdoc(Path) def as_uri(self, scheme='file'): - return urlparse.urljoin(str(scheme)+':', urllib.pathname2url(str(self))) + return urlparse.urljoin( + str(scheme) + ':', urllib.pathname2url(str(self))) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def drive(self): return os.path.splitdrive(str(self))[0] - @property # type: ignore + @property # type: ignore @_setdoc(Path) def root(self): return os.path.sep - class LocalWorkdir(LocalPath): """Working directory manipulator""" - def __hash__(self): raise TypeError("unhashable type") + def __new__(cls): return super(LocalWorkdir, cls).__new__(cls, os.getcwd()) @@ -336,13 +349,15 @@ :param newdir: The destination director (a string or a ``LocalPath``) """ if isinstance(newdir, RemotePath): - raise TypeError("newdir cannot be %r" % (newdir,)) + raise TypeError("newdir cannot be %r" % (newdir, )) logger.debug("Chdir to %s", newdir) os.chdir(str(newdir)) return self.__class__() + def getpath(self): """Returns the current working directory as a ``LocalPath`` object""" return LocalPath(self._path) + @contextmanager def __call__(self, newdir): """A context manager used to ``chdir`` into a directory and then ``chdir`` back to @@ -356,4 +371,3 @@ yield newdir finally: self.chdir(prev) - diff -Nru python-plumbum-1.6.6/plumbum/path/remote.py python-plumbum-1.6.7/plumbum/path/remote.py --- python-plumbum-1.6.6/plumbum/path/remote.py 2018-01-24 09:56:38.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/path/remote.py 2018-07-04 10:09:22.000000000 +0000 @@ -4,17 +4,21 @@ from plumbum.lib import _setdoc, six from plumbum.commands import shquote -try: # Py3 +try: # Py3 import urllib.request as urllib except ImportError: - import urllib # type: ignore + import urllib # type: ignore + class StatRes(object): """POSIX-like stat result""" + def __init__(self, tup): self._tup = tuple(tup) + def __getitem__(self, index): return self._tup[index] + st_mode = mode = property(lambda self: self[0]) st_ino = ino = property(lambda self: self[1]) st_dev = dev = property(lambda self: self[2]) @@ -30,7 +34,6 @@ class RemotePath(Path): """The class implementing remote-machine paths""" - def __new__(cls, remote, *parts): if not parts: raise TypeError("At least one path part is required (none given)") @@ -38,9 +41,10 @@ normed = [] # Simple skip if path is absolute - if parts[0] and parts[0][0] not in ("/","\\"): - cwd = (remote._cwd if hasattr(remote, '_cwd') else remote._session.run("pwd")[1].strip()) - parts = (cwd,) + parts + if parts[0] and parts[0][0] not in ("/", "\\"): + cwd = (remote._cwd if hasattr(remote, '_cwd') else + remote._session.run("pwd")[1].strip()) + parts = (cwd, ) + parts for p in parts: if windows: @@ -60,7 +64,7 @@ normed.append(item) if windows: self = super(RemotePath, cls).__new__(cls, "\\".join(normed)) - self.CASE_SENSITIVE = False # On this object only + self.CASE_SENSITIVE = False # On this object only else: self = super(RemotePath, cls).__new__(cls, "/" + "/".join(normed)) self.CASE_SENSITIVE = True @@ -75,42 +79,42 @@ def _path(self): return str(self) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def name(self): if not "/" in str(self): return str(self) return str(self).rsplit("/", 1)[1] - @property # type: ignore + @property # type: ignore @_setdoc(Path) def dirname(self): if not "/" in str(self): return str(self) return self.__class__(self.remote, str(self).rsplit("/", 1)[0]) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def suffix(self): - return '.' + self.name.rsplit('.',1)[1] + return '.' + self.name.rsplit('.', 1)[1] - @property # type: ignore + @property # type: ignore @_setdoc(Path) def suffixes(self): name = self.name exts = [] while '.' in name: - name, ext = name.rsplit('.',1) + name, ext = name.rsplit('.', 1) exts.append('.' + ext) return list(reversed(exts)) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def uid(self): uid, name = self.remote._path_getuid(self) return FSUser(int(uid), name) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def gid(self): gid, name = self.remote._path_getgid(self) @@ -176,9 +180,10 @@ if (suffix and not suffix.startswith('.') or suffix == '.'): raise ValueError("Invalid suffix %r" % (suffix)) name = self.name - depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) + depth = len(self.suffixes) if depth is None else min( + depth, len(self.suffixes)) for i in range(depth): - name, ext = name.rsplit('.',1) + name, ext = name.rsplit('.', 1) return self.__class__(self.remote, self.dirname) / (name + suffix) @_setdoc(Path) @@ -200,18 +205,20 @@ if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): - raise TypeError("dst must be a string or a RemotePath (to the same remote machine), " - "got %r" % (dst,)) + raise TypeError( + "dst must be a string or a RemotePath (to the same remote machine), " + "got %r" % (dst, )) self.remote._path_move(self, dst) @_setdoc(Path) - def copy(self, dst, override = False): + def copy(self, dst, override=False): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): - raise TypeError("dst must be a string or a RemotePath (to the same remote machine), " - "got %r" % (dst,)) + raise TypeError( + "dst must be a string or a RemotePath (to the same remote machine), " + "got %r" % (dst, )) if override: if isinstance(dst, six.string_types): dst = RemotePath(self.remote, dst) @@ -234,6 +241,7 @@ if encoding: data = data.decode(encoding) return data + @_setdoc(Path) def write(self, data, encoding=None): if encoding: @@ -245,14 +253,17 @@ self.remote._path_touch(str(self)) @_setdoc(Path) - def chown(self, owner = None, group = None, recursive = None): - self.remote._path_chown(self, owner, group, self.is_dir() if recursive is None else recursive) + def chown(self, owner=None, group=None, recursive=None): + 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) @_setdoc(Path) - def access(self, mode = 0): + def access(self, mode=0): mode = self._access_mode_to_flags(mode) res = self.remote._path_stat(self) if res is None: @@ -266,8 +277,9 @@ if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): - raise TypeError("dst must be a string or a RemotePath (to the same remote machine), " - "got %r" % (dst,)) + raise TypeError( + "dst must be a string or a RemotePath (to the same remote machine), " + "got %r" % (dst, )) self.remote._path_link(self, dst, False) @_setdoc(Path) @@ -276,43 +288,50 @@ if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): - raise TypeError("dst must be a string or a RemotePath (to the same remote machine), " - "got %r" % (dst,)) + 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))) + def as_uri(self, scheme='ssh'): + return '{0}://{1}{2}'.format(scheme, self.remote._fqhost, + urllib.pathname2url(str(self))) - @property # type: ignore + @property # type: ignore @_setdoc(Path) def stem(self): return self.name.rsplit('.')[0] - @property # type: ignore + @property # type: ignore @_setdoc(Path) def root(self): return '/' - @property # type: ignore + @property # type: ignore @_setdoc(Path) def drive(self): return '' + class RemoteWorkdir(RemotePath): """Remote working directory manipulator""" def __new__(cls, remote): - self = super(RemoteWorkdir, cls).__new__(cls, remote, remote._session.run("pwd")[1].strip()) + 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.remote._session.run("cd %s" % (shquote(newdir), )) if hasattr(self.remote, '_cwd'): del self.remote._cwd return self.__class__(self.remote) @@ -336,6 +355,3 @@ yield changed_dir finally: self.chdir(prev) - - - diff -Nru python-plumbum-1.6.6/plumbum/path/utils.py python-plumbum-1.6.7/plumbum/path/utils.py --- python-plumbum-1.6.6/plumbum/path/utils.py 2016-06-05 15:24:55.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/path/utils.py 2018-07-04 10:09:22.000000000 +0000 @@ -18,13 +18,15 @@ elif hasattr(p, "__iter__"): delete(*p) else: - raise TypeError("Cannot delete %r" % (p,)) + raise TypeError("Cannot delete %r" % (p, )) + def _move(src, dst): ret = copy(src, dst) delete(src) return ret + def move(src, dst): """Moves the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or @@ -40,7 +42,9 @@ if not dst.exists(): dst.mkdir() elif not dst.is_dir(): - raise ValueError("When using multiple sources, dst %r must be a directory" % (dst,)) + raise ValueError( + "When using multiple sources, dst %r must be a directory" % + (dst, )) for src2 in src: move(src2, dst) return dst @@ -59,6 +63,7 @@ else: return _move(src, dst) + def copy(src, dst): """ Copy (recursively) the source path onto the destination path; ``src`` and ``dst`` can be @@ -75,7 +80,9 @@ if not dst.exists(): dst.mkdir() elif not dst.is_dir(): - raise ValueError("When using multiple sources, dst %r must be a directory" % (dst,)) + raise ValueError( + "When using multiple sources, dst %r must be a directory" % + (dst, )) for src2 in src: copy(src2, dst) return dst @@ -103,7 +110,7 @@ def gui_open(filename): """This selects the proper gui open function. This can also be achieved with webbrowser, but that is not supported.""" - if(hasattr(os, "startfile")): + if (hasattr(os, "startfile")): os.startfile(filename) else: local.get('xdg-open', 'open')(filename) diff -Nru python-plumbum-1.6.6/plumbum/_testtools.py python-plumbum-1.6.7/plumbum/_testtools.py --- python-plumbum-1.6.6/plumbum/_testtools.py 2016-07-16 09:52:09.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/_testtools.py 2018-07-04 10:09:22.000000000 +0000 @@ -3,17 +3,20 @@ import sys import platform -skip_without_chown = pytest.mark.skipif(not hasattr(os, "chown"), - reason="os.chown not supported") +skip_without_chown = pytest.mark.skipif( + not hasattr(os, "chown"), reason="os.chown not supported") -skip_without_tty = pytest.mark.skipif(not sys.stdin.isatty(), - reason="Not a TTY") +skip_without_tty = pytest.mark.skipif( + not sys.stdin.isatty(), reason="Not a TTY") -skip_on_windows = pytest.mark.skipif(sys.platform == "win32", - reason="Windows not supported for this test (yet)") +skip_on_windows = pytest.mark.skipif( + sys.platform == "win32", + reason="Windows not supported for this test (yet)") -xfail_on_windows = pytest.mark.xfail(sys.platform == "win32", - reason="Windows not supported for this test (yet)") +xfail_on_windows = pytest.mark.xfail( + sys.platform == "win32", + reason="Windows not supported for this test (yet)") -xfail_on_pypy = pytest.mark.xfail(platform.python_implementation() == 'PyPy', - reason="PyPy is currently not working on this test!") +xfail_on_pypy = pytest.mark.xfail( + platform.python_implementation() == 'PyPy', + reason="PyPy is currently not working on this test!") diff -Nru python-plumbum-1.6.6/plumbum/version.py python-plumbum-1.6.7/plumbum/version.py --- python-plumbum-1.6.6/plumbum/version.py 2018-02-12 14:27:59.000000000 +0000 +++ python-plumbum-1.6.7/plumbum/version.py 2018-08-08 13:24:21.000000000 +0000 @@ -1,3 +1,3 @@ -version = (1, 6, 6) -version_string = ".".join(map(str,version)) -release_date = "2018.02.12" +version = (1, 6, 7) +version_string = ".".join(map(str, version)) +release_date = "2018.08.08" diff -Nru python-plumbum-1.6.6/plumbum.egg-info/PKG-INFO python-plumbum-1.6.7/plumbum.egg-info/PKG-INFO --- python-plumbum-1.6.6/plumbum.egg-info/PKG-INFO 2018-02-12 15:02:44.000000000 +0000 +++ python-plumbum-1.6.7/plumbum.egg-info/PKG-INFO 2018-08-10 13:04:46.000000000 +0000 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: plumbum -Version: 1.6.6 +Version: 1.6.7 Summary: Plumbum: shell combinators library Home-page: https://plumbum.readthedocs.io Author: Tomer Filiba Author-email: tomerfiliba@gmail.com License: MIT -Description-Content-Type: UNKNOWN Description: .. image:: https://readthedocs.org/projects/plumbum/badge/ :target: https://plumbum.readthedocs.io/en/latest/ :alt: Documentation Status @@ -223,6 +222,7 @@ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum diff -Nru python-plumbum-1.6.6/setup.py python-plumbum-1.6.7/setup.py --- python-plumbum-1.6.6/setup.py 2017-10-15 19:40:05.000000000 +0000 +++ python-plumbum-1.6.7/setup.py 2018-08-08 13:23:00.000000000 +0000 @@ -81,6 +81,7 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Build Tools", "Topic :: System :: Systems Administration", ],