diff --git a/.travis.yml b/.travis.yml index b4988e054..5b6794b0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,7 @@ script: # Run unit and integration tests, generating coverage report (skipping html) # This will fail if the tests fail. - "inv coverage --no-html" + - "LC_ALL=C inv coverage --no-html" # Websites build OK? - inv sites # Did we break setup.py? diff --git a/integration/main.py b/integration/main.py index 1f4e0d404..9eed3f475 100644 --- a/integration/main.py +++ b/integration/main.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import sys @@ -5,6 +7,7 @@ from invoke import run from invoke._version import __version__ +from invoke.exceptions import Failure from invoke.platform import WINDOWS @@ -84,18 +87,56 @@ def basic_nonstandard_characters(self): cmd = "type tree.out" else: cmd = "cat tree.out" - run(cmd, hide='both') + run(cmd) def nonprinting_bytes(self): # Seriously non-printing characters (i.e. non UTF8) also don't # asplode - run("echo '\xff'", hide='both') + try: + run(b"echo '\xff'") + except TypeError: + if sys.version_info > (3, 0) and sys.version_info < (3, 3): + # Python 3.2 is known to raise TypeError here + pass + else: + raise def nonprinting_bytes_pty(self): if WINDOWS: return # PTY use adds another utf-8 decode spot which can also fail. - run("echo '\xff'", pty=True, hide='both') + run(b"echo '\xff'", pty=True) + + def nonprinting_bytes_in_unicode(self): + # Seriously non-printing characters (i.e. non UTF8) should fail as + # we cannot just automatically ignore/replace encoding issues in + # user commands + try: + run("echo '\xff'") + except (TypeError, UnicodeEncodeError): + if sys.stdout.encoding == 'UTF-8': + raise + else: + if sys.stdout.encoding != 'UTF-8': + raise Exception( + "Non-printing bytes in unicode command must fail" + ) + + def nonprinting_bytes_in_unicode_pty(self): + if WINDOWS: + return + # PTY use adds another utf-8 decode spot which should also fail + # when user command has non-encodable symbols. + try: + run("echo '\xff'", pty=True) + except Failure: + if sys.stdout.encoding == 'UTF-8': + raise + else: + if sys.stdout.encoding != 'UTF-8': + raise Exception( + "Non-printing bytes in unicode command must fail" + ) def pty_puts_both_streams_in_stdout(self): if WINDOWS: diff --git a/invoke/exceptions.py b/invoke/exceptions.py index e73f4ce5b..1c51fc841 100644 --- a/invoke/exceptions.py +++ b/invoke/exceptions.py @@ -6,6 +6,8 @@ condition in a way easily told apart from other, truly unexpected errors". """ +from __future__ import unicode_literals + from collections import namedtuple from traceback import format_exception from pprint import pformat @@ -19,6 +21,7 @@ def __init__(self, name, start): self.start = start +@six.python_2_unicode_compatible class Failure(Exception): """ Exception subclass representing failure of a command execution. @@ -35,15 +38,12 @@ def __str__(self): if self.result.pty: err_label = "Stdout (pty=True; no stderr possible)" err_text = self.result.stdout - return """Command execution failure! - -Exit code: {0} - -{1}: - -{2} - -""".format(self.result.exited, err_label, err_text) + return ( + "Command execution failure!\n" + "Exit code: {0}\n" + "{1}:\n" + "{2}" + ).format(self.result.exited, err_label, err_text) def __repr__(self): return str(self) @@ -133,6 +133,7 @@ def _printable_kwargs(kwargs): printable[key] = item return printable +@six.python_2_unicode_compatible class ThreadException(Exception): """ One or more exceptions were raised within background (usually I/O) threads. @@ -175,9 +176,7 @@ def __str__(self): ", ".join(x.type.__name__ for x in self.exceptions), "\n\n".join(details), ) - return """ -Saw {0} exceptions within threads ({1}): - - -{2} -""".format(*args) + return ( + "Saw {0} exceptions within threads ({1}):\n" + "{2}" + ).format(*args) diff --git a/invoke/runners.py b/invoke/runners.py index 2d1c59e35..c7699e30b 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import codecs import locale @@ -254,7 +255,18 @@ def run(self, command, **kwargs): env = self.generate_env(opts['env'], opts['replace_env']) # Echo running command if opts['echo']: - print("\033[1;37m{0}\033[0m".format(command)) + if isinstance(command, six.text_type): + unicode_command = command + else: + if six.PY2: + unicode_command = "b{0}".format(repr(command)) + else: + unicode_command = repr(command) + # Use a wrapped sys.stdout to avoid encoding errors (errors will + # be replaced with backslashed unicode codes) + self._wrap_output(sys.stdout).write( + "\033[1;37m{0}\033[0m\n".format(unicode_command) + ) # Start executing the actual command (runs in background) self.start(command, shell, env) # Arrive at final encoding if neither config nor kwargs had one @@ -372,10 +384,10 @@ def _run_opts(self, kwargs): # Derive stream objects out_stream = opts['out_stream'] if out_stream is None: - out_stream = sys.stdout + out_stream = self._wrap_output(sys.stdout) err_stream = opts['err_stream'] if err_stream is None: - err_stream = sys.stderr + err_stream = self._wrap_output(sys.stderr) in_stream = opts['in_stream'] if in_stream is None: in_stream = sys.stdin @@ -635,6 +647,27 @@ def generate_env(self, env, replace_env): """ return env if replace_env else dict(os.environ, **env) + def _wrap_output(self, stream): + """ + Wraps a ``stream`` with ``codecs.StreamWriter`` where encoding errors + are replaced. + """ + if hasattr(stream, 'buffer'): + output_buffer = stream.buffer + else: + output_buffer = stream + + # NOTE: sys.stdout.encoding returns None on Python 2.x when + # C locale is used + stream_encoding = getattr(stream, 'encoding', None) + if stream_encoding is None: + stream_encoding = self.default_encoding() + + return codecs.getwriter(stream_encoding)( + output_buffer, + errors='backslashreplace' + ) + def should_use_pty(self, pty, fallback): """ Should execution attempt to use a pseudo-terminal? @@ -786,22 +819,33 @@ def start(self, command, shell, env): # shell, just as subprocess does; this replaces our process - whose # pipes are all hooked up to the PTY - with the "real" one. if self.pid == 0: - # TODO: both pty.spawn() and pexpect.spawn() do a lot of - # setup/teardown involving tty.setraw, getrlimit, signal. - # Ostensibly we'll want some of that eventually, but if - # possible write tests - integration-level if necessary - - # before adding it! - # - # Set pty window size based on what our own controlling - # terminal's window size appears to be. - # TODO: make subroutine? - winsize = struct.pack('HHHH', rows, cols, 0, 0) - fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, winsize) - # Use execve for bare-minimum "exec w/ variable # args + env" - # behavior. No need for the 'p' (use PATH to find executable) - # for now. - # TODO: see if subprocess is using equivalent of execvp... - os.execve(shell, [shell, '-c', command], env) + try: + # TODO: both pty.spawn() and pexpect.spawn() do a lot of + # setup/teardown involving tty.setraw, getrlimit, signal. + # Ostensibly we'll want some of that eventually, but if + # possible write tests - integration-level if necessary - + # before adding it! + # + # Set pty window size based on what our own controlling + # terminal's window size appears to be. + # TODO: make subroutine? + winsize = struct.pack(b'HHHH', rows, cols, 0, 0) + fcntl.ioctl( + sys.stdout.fileno(), termios.TIOCSWINSZ, winsize + ) + # Use execve for bare-minimum + # "exec w/ variable args + env" behavior. No need for the + # 'p' (use PATH to find executable) # for now. + # TODO: see if subprocess is using equivalent of execvp... + os.execve(shell, [shell, '-c', command], env) + except Exception: + # Prevent process hanging when something wrong happened + # here, for example, encoding error in `execve` + import traceback + traceback.print_exc() + # We forcefully terminate this fork branch to simulate + # no-op `execve` + os._exit(1) else: self.process = Popen( command, @@ -814,6 +858,20 @@ def start(self, command, shell, env): ) def default_encoding(self): + # Based on some experiments there is an issue with + # `locale.getpreferredencoding(do_setlocale=False)` in Python 2.x on + # Linux and OS X, and `locale.getpreferredencoding(do_setlocale=True)` + # triggers some global state changes: + # https://github.com/pyinvoke/invoke/pull/274/ + if six.PY2 and not WINDOWS: + # This is a workaround, please, report if there is a case when it + # doesn't work. + default_locale_encoding = locale.getdefaultlocale()[1] + # It is known that when default locale is C (POSIX), Python + # returns None. + if default_locale_encoding is not None: + return default_locale_encoding + # This is a preferred way of getting a default encoding. return locale.getpreferredencoding(False) def wait(self): @@ -854,6 +912,7 @@ def returncode(self): return self.process.returncode +@six.python_2_unicode_compatible class Result(object): """ A container for information about the result of a command execution. @@ -903,9 +962,10 @@ def __str__(self): ret = ["Command exited with status {0}.".format(self.exited)] for x in ('stdout', 'stderr'): val = getattr(self, x) - ret.append("""=== {0} === -{1} -""".format(x, val.rstrip()) if val else "(no {0})".format(x)) + if val: + ret.append("=== {0} ===\n{1}".format(x, val.rstrip())) + else: + ret.append("(no {0})".format(x)) return "\n".join(ret) @property diff --git a/invoke/vendor/six.py b/invoke/vendor/six.py index 58cb95b3c..ffa3fe166 100644 --- a/invoke/vendor/six.py +++ b/invoke/vendor/six.py @@ -1,6 +1,6 @@ """Utilities for writing code that runs on Python 2 and 3""" -# Copyright (c) 2010-2014 Benjamin Peterson +# Copyright (c) 2010-2015 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,12 +20,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import absolute_import + +import functools +import itertools import operator import sys import types __author__ = "Benjamin Peterson " -__version__ = "1.5.2" +__version__ = "1.9.0" # Useful for very coarse version differentiation. @@ -85,8 +89,12 @@ def __init__(self, name): def __get__(self, obj, tp): result = self._resolve() setattr(obj, self.name, result) # Invokes __set__. - # This is a bit ugly, but it avoids running this again. - delattr(obj.__class__, self.name) + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass return result @@ -105,16 +113,6 @@ def _resolve(self): return _import_module(self.mod) def __getattr__(self, attr): - # Hack around the Django autoreloader. The reloader tries to get - # __file__ or __name__ of every module in sys.modules. This doesn't work - # well if this MovedModule is for an module that is unavailable on this - # machine (like winreg on Unix systems). Thus, we pretend __file__ and - # __name__ don't exist if the module hasn't been loaded yet. We give - # __path__ the same treatment for Google AppEngine. See issues #51, #53 - # and #56. - if (attr in ("__file__", "__name__", "__path__") and - self.mod not in sys.modules): - raise AttributeError _module = self._resolve() value = getattr(_module, attr) setattr(self, attr, value) @@ -161,9 +159,72 @@ def _resolve(self): return getattr(module, self.attr) +class _SixMetaPathImporter(object): + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + class _MovedItems(_LazyModule): """Lazy loading of moved objects""" + __path__ = [] # mark as package _moved_attributes = [ @@ -171,11 +232,15 @@ class _MovedItems(_LazyModule): MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), MovedAttribute("map", "itertools", "builtins", "imap", "map"), MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("reload_module", "__builtin__", "imp", "reload"), MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), MovedAttribute("UserString", "UserString", "collections"), MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), @@ -185,12 +250,14 @@ class _MovedItems(_LazyModule): MovedModule("configparser", "ConfigParser"), MovedModule("copyreg", "copy_reg"), MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), MovedModule("http_cookies", "Cookie", "http.cookies"), MovedModule("html_entities", "htmlentitydefs", "html.entities"), MovedModule("html_parser", "HTMLParser", "html.parser"), MovedModule("http_client", "httplib", "http.client"), MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), @@ -224,18 +291,19 @@ class _MovedItems(_LazyModule): MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "xmlrpclib", "xmlrpc.server"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), MovedModule("winreg", "_winreg"), ] for attr in _moved_attributes: setattr(_MovedItems, attr.name, attr) if isinstance(attr, MovedModule): - sys.modules[__name__ + ".moves." + attr.name] = attr + _importer._add_module(attr, "moves." + attr.name) del attr _MovedItems._moved_attributes = _moved_attributes -moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves") +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") class Module_six_moves_urllib_parse(_LazyModule): @@ -259,6 +327,13 @@ class Module_six_moves_urllib_parse(_LazyModule): MovedAttribute("unquote_plus", "urllib", "urllib.parse"), MovedAttribute("urlencode", "urllib", "urllib.parse"), MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), ] for attr in _urllib_parse_moved_attributes: setattr(Module_six_moves_urllib_parse, attr.name, attr) @@ -266,7 +341,8 @@ class Module_six_moves_urllib_parse(_LazyModule): Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes -sys.modules[__name__ + ".moves.urllib_parse"] = sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse") +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") class Module_six_moves_urllib_error(_LazyModule): @@ -284,7 +360,8 @@ class Module_six_moves_urllib_error(_LazyModule): Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes -sys.modules[__name__ + ".moves.urllib_error"] = sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error") +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") class Module_six_moves_urllib_request(_LazyModule): @@ -332,7 +409,8 @@ class Module_six_moves_urllib_request(_LazyModule): Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes -sys.modules[__name__ + ".moves.urllib_request"] = sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request") +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") class Module_six_moves_urllib_response(_LazyModule): @@ -351,7 +429,8 @@ class Module_six_moves_urllib_response(_LazyModule): Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes -sys.modules[__name__ + ".moves.urllib_response"] = sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response") +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") class Module_six_moves_urllib_robotparser(_LazyModule): @@ -367,22 +446,24 @@ class Module_six_moves_urllib_robotparser(_LazyModule): Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes -sys.modules[__name__ + ".moves.urllib_robotparser"] = sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser") +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") class Module_six_moves_urllib(types.ModuleType): """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - parse = sys.modules[__name__ + ".moves.urllib_parse"] - error = sys.modules[__name__ + ".moves.urllib_error"] - request = sys.modules[__name__ + ".moves.urllib_request"] - response = sys.modules[__name__ + ".moves.urllib_response"] - robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"] + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") def __dir__(self): return ['parse', 'error', 'request', 'response', 'robotparser'] - -sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib") +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") def add_move(move): @@ -409,11 +490,6 @@ def remove_move(name): _func_code = "__code__" _func_defaults = "__defaults__" _func_globals = "__globals__" - - _iterkeys = "keys" - _itervalues = "values" - _iteritems = "items" - _iterlists = "lists" else: _meth_func = "im_func" _meth_self = "im_self" @@ -423,11 +499,6 @@ def remove_move(name): _func_defaults = "func_defaults" _func_globals = "func_globals" - _iterkeys = "iterkeys" - _itervalues = "itervalues" - _iteritems = "iteritems" - _iterlists = "iterlists" - try: advance_iterator = next @@ -476,21 +547,49 @@ def next(self): get_function_globals = operator.attrgetter(_func_globals) -def iterkeys(d, **kw): - """Return an iterator over the keys of a dictionary.""" - return iter(getattr(d, _iterkeys)(**kw)) +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") -def itervalues(d, **kw): - """Return an iterator over the values of a dictionary.""" - return iter(getattr(d, _itervalues)(**kw)) + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return iter(d.iterkeys(**kw)) + + def itervalues(d, **kw): + return iter(d.itervalues(**kw)) + + def iteritems(d, **kw): + return iter(d.iteritems(**kw)) + + def iterlists(d, **kw): + return iter(d.iterlists(**kw)) -def iteritems(d, **kw): - """Return an iterator over the (key, value) pairs of a dictionary.""" - return iter(getattr(d, _iteritems)(**kw)) + viewkeys = operator.methodcaller("viewkeys") -def iterlists(d, **kw): - """Return an iterator over the (key, [values]) pairs of a dictionary.""" - return iter(getattr(d, _iterlists)(**kw)) + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") if PY3: @@ -511,6 +610,9 @@ def int2byte(i): import io StringIO = io.StringIO BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" else: def b(s): return s @@ -523,19 +625,35 @@ def byte2int(bs): return ord(bs[0]) def indexbytes(buf, i): return ord(buf[i]) - def iterbytes(buf): - return (ord(byte) for byte in buf) + iterbytes = functools.partial(itertools.imap, ord) import StringIO StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" _add_doc(b, """Byte literal""") _add_doc(u, """Text literal""") +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + if PY3: exec_ = getattr(moves.builtins, "exec") def reraise(tp, value, tb=None): + if value is None: + value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value @@ -559,6 +677,21 @@ def exec_(_code_, _globs_=None, _locs_=None): """) +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + print_ = getattr(moves.builtins, "print", None) if print_ is None: def print_(*args, **kwargs): @@ -613,25 +746,93 @@ def write(data): write(sep) write(arg) write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() _add_doc(reraise, """Reraise an exception.""") +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps def with_metaclass(meta, *bases): """Create a base class with a metaclass.""" - return meta("NewBase", bases, {}) + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" def wrapper(cls): orig_vars = cls.__dict__.copy() - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) slots = orig_vars.get('__slots__') if slots is not None: if isinstance(slots, str): slots = [slots] for slots_var in slots: orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/tests/runners.py b/tests/runners.py index 4ed38d53d..6fb52bb88 100644 --- a/tests/runners.py +++ b/tests/runners.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import locale import os import sys @@ -12,6 +14,7 @@ from invoke import Runner, Local, Context, Config, Failure, ThreadException from invoke.platform import WINDOWS +from invoke.vendor import six from _util import mock_subprocess, mock_pty, skip_if_windows @@ -269,11 +272,18 @@ def kwarg_beats_config(self): assert_contains(sys.stdout.getvalue(), "yup") @trap - def uses_ansi_bold(self): + def uses_ansi_bold_for_unicode(self): self._run("my command", echo=True) # TODO: vendor & use a color module eq_(sys.stdout.getvalue(), "\x1b[1;37mmy command\x1b[0m\n") + @trap + def uses_ansi_bold_for_bytes(self): + self._run(b"my command", echo=True) + # TODO: vendor & use a color module + eq_(sys.stdout.getvalue(), "\x1b[1;37mb'my command'\x1b[0m\n") + + class encoding: # Use UTF-7 as a valid encoding unlikely to be a real default def defaults_to_encoding_method_result(self): @@ -432,7 +442,7 @@ def can_be_overridden(self): def exceptions_get_logged(self, mock_debug): # Make write_stdin asplode klass = self._mock_stdin_writer() - klass.write_stdin.side_effect = OhNoz("oh god why") + klass.write_stdin.side_effect = OhNoz(str("oh god why")) # Execute with some stdin to trigger that asplode (but skip the # actual bubbled-up raising of it so we can check things out) try: @@ -873,6 +883,12 @@ def uses_locale_module_for_desired_encoding(self): with patch('invoke.runners.codecs') as codecs: self._run(_) local_encoding = locale.getpreferredencoding(False) + # Please, read more about this in + # invoke.runners.Local.default_encoding. + if six.PY2 and not WINDOWS: + default_locale_encoding = locale.getdefaultlocale()[1] + if default_locale_encoding is not None: + local_encoding = default_locale_encoding _expect_encoding(codecs, local_encoding) class send_interrupt: