diff --git a/.travis.yml b/.travis.yml index 6e20573f..da167c1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: + - "3.5" - "3.4" - "3.3" - "2.7" diff --git a/docs/whatsnew.rst b/docs/whatsnew.rst index 582b506b..f349ffef 100644 --- a/docs/whatsnew.rst +++ b/docs/whatsnew.rst @@ -5,14 +5,14 @@ What's New .. _whats-new-0.16.x: -What's new in version 0.16.0 (2016-09-22) +What's new in version 0.16.0 (2016-10-27) ========================================== This release removes the ``configparser`` package as an alias for -``ConfigParser`` on Py2 to improve compatibility with the backported -`configparser package `. Previously -``python-future`` and the PyPI ``configparser`` backport clashed, causing -various compatibility issues. (Issues #118, #181) +``ConfigParser`` on Py2 to improve compatibility with Lukasz Langa's +backported `configparser `_ package. +Previously ``python-future`` and the ``configparser`` backport clashed, +causing various compatibility issues. (Issues #118, #181) If your code previously relied on ``configparser`` being supplied by ``python-future``, the recommended upgrade path is to run ``pip install @@ -26,18 +26,18 @@ effect on your system. This releases also fixes these bugs: - Fix ``newbytes`` constructor bug. (Issue #163) -- Fix semantics of `bool()` with `newobject`. (Issue #211) -- Fix `standard_library.install_aliases()` on PyPy. (Issue #205) -- Fix assertRaises for `pow` and `compile` on Python 3.5. (Issue #183) -- Fix return argument of `future.utils.ensure_new_type` if conversion to +- Fix semantics of ``bool()`` with ``newobject``. (Issue #211) +- Fix ``standard_library.install_aliases()`` on PyPy. (Issue #205) +- Fix assertRaises for ``pow`` and ``compile``` on Python 3.5. (Issue #183) +- Fix return argument of ``future.utils.ensure_new_type`` if conversion to new type does not exist. (Issue #185) -- Add missing `cmp_to_key` for Py2.6. (Issue #189) -- Allow the `old_div` fixer to be disabled. (Issue #190) +- Add missing ``cmp_to_key`` for Py2.6. (Issue #189) +- Allow the ``old_div`` fixer to be disabled. (Issue #190) - Improve compatibility with Google App Engine. (Issue #231) -- Add some missing imports to the `tkinter` and `tkinter.filedialog` +- Add some missing imports to the ``tkinter`` and ``tkinter.filedialog`` package namespaces. (Issues #212 and #233) -- Fix ``raise_from`` on PY3 when the exception cannot be recreated from - its repr. (Issues #213 and #235, fix provided by Varriount) +- More complete implementation of ``raise_from`` on PY3. (Issues #141, + #213 and #235, fix provided by Varriount) What's new in version 0.15.2 (2015-09-11) diff --git a/src/future/builtins/__init__.py b/src/future/builtins/__init__.py index 94011f97..8ec7009b 100644 --- a/src/future/builtins/__init__.py +++ b/src/future/builtins/__init__.py @@ -34,7 +34,6 @@ newstr as str) from future import utils - if not utils.PY3: # We only import names that shadow the builtins on Py2. No other namespace # pollution on Py2. @@ -47,5 +46,44 @@ ] else: - # No namespace pollution on Py3 + # No namespace pollution on Py3.3+ __all__ = [] + + +# Exceptions +try: + BlockingIOError = builtins.BlockingIOError + BrokenPipeError = builtins.BrokenPipeError + ChildProcessError = builtins.ChildProcessError + ConnectionError = builtins.ConnectionError + ConnectionAbortedError = builtins.ConnectionAbortedError + ConnectionRefusedError = builtins.ConnectionRefusedError + ConnectionResetError = builtins.ConnectionResetError + FileExistsError = builtins.FileExistsError + FileNotFoundError = builtins.FileNotFoundError + InterruptedError = builtins.InterruptedError + IsADirectoryError = builtins.IsADirectoryError + NotADirectoryError = builtins.NotADirectoryError + PermissionError = builtins.PermissionError + ProcessLookupError = builtins.ProcessLookupError + TimeoutError = builtins.TimeoutError +except NameError: + from future.types.exceptions.pep3151 import * + import future.types.exceptions as fte + __all__ += [ + 'BlockingIOError', + 'BrokenPipeError', + 'ChildProcessError', + 'ConnectionError', + 'ConnectionAbortedError', + 'ConnectionRefusedError', + 'ConnectionResetError', + 'FileExistsError', + 'FileNotFoundError', + 'InterruptedError', + 'IsADirectoryError', + 'NotADirectoryError', + 'PermissionError', + 'ProcessLookupError', + 'TimeoutError', + ] diff --git a/src/future/builtins/exceptions_backup.py b/src/future/builtins/exceptions_backup.py new file mode 100644 index 00000000..92e0f4ff --- /dev/null +++ b/src/future/builtins/exceptions_backup.py @@ -0,0 +1,93 @@ +""" +This module is designed to be used as follows:: + + from future.builtins.exceptions import (FileNotFoundError, FileExistsError) + +And then, for example:: + + try: + args.func(args) + except FileExistsError as e: + parser.error('Refusing to clobber existing path ({err})'.format(err=e)) + +Note that this is standard Python 3 code, plus some imports that do +nothing on Python 3. + +The exceptions this brings in are:: + +- ``FileNotFoundError`` +- ``FileExistsError`` + +""" + +from __future__ import division, absolute_import, print_function + +import errno +import os +import sys + +from future.utils import with_metaclass + + +class BaseNewFileNotFoundError(type): + def __instancecheck__(cls, instance): + return hasattr(instance, 'errno') and instance.errno == errno.ENOENT + + def __subclasscheck__(cls, classinfo): + # This hook is called during the exception handling. + # Unfortunately, we would rather have exception handling call + # __instancecheck__, so we have to do that ourselves. But, + # that's not how it currently is. If you feel like proposing a + # patch for Python, check the function + # `PyErr_GivenExceptionMatches` in `Python/error.c`. + value = sys.exc_info()[1] + + # Double-check that the exception given actually somewhat + # matches the classinfo we received. If not, people are using + # `issubclass` directly, which is of course prone to errors. + if value.__class__ != classinfo: + print('Mismatch!\nvalue: {0}\nvalue.__class__: {1}\nclassinfo: {2}'.format( + value, value.__class__, classinfo) + ) + + return isinstance(value, cls) + + +class NewFileNotFoundError(with_metaclass(BaseNewFileNotFoundError, IOError)): + """ + A backport of the Python 3.3+ FileNotFoundError to Py2 + """ + def __init__(self, *args, **kwargs): + print('ARGS ARE: {}'.format(args)) + print('KWARGS ARE: {}'.format(kwargs)) + super(NewFileNotFoundError, self).__init__(*args, **kwargs) # cls, *args, **kwargs) + self.errno = errno.ENOENT + + def __str__(self): + # return 'FILE NOT FOUND!' + return self.message or os.strerror(self.errno) + + def __native__(self): + """ + Hook for the future.utils.native() function + """ + err = IOError(self.message) + err.errno = self.errno + return err + + +# try: +# FileNotFoundError +# except NameError: +# # Python < 3.3 +# class FileNotFoundError(IOError): +# def __init__(self, message=None, *args): +# super(FileNotFoundError, self).__init__(args) +# self.message = message +# self.errno = errno.ENOENT +# +# def __str__(self): +# return self.message or os.strerror(self.errno) + + +__all__ = ['NewFileNotFoundError'] diff --git a/src/future/types/exceptions/__init__.py b/src/future/types/exceptions/__init__.py new file mode 100644 index 00000000..f36b96f6 --- /dev/null +++ b/src/future/types/exceptions/__init__.py @@ -0,0 +1,2 @@ +from .pep3151 import * + diff --git a/src/future/types/exceptions/base.py b/src/future/types/exceptions/base.py new file mode 100644 index 00000000..e070b7ed --- /dev/null +++ b/src/future/types/exceptions/base.py @@ -0,0 +1,66 @@ +""" +Libraries are not as fine-grained with exception classes as one would like. By +using this hack, you can make create your own exceptions which behave as if +they are superclasses of the ones raised by the standard library. + +Tread with caution. + +Defines the base `instance_checking_exception` creator. +""" + +import re +import sys + + +def instance_checking_exception(base_exception=Exception): + def wrapped(instance_checker): + """ + Create an exception class which inspects the exception being raised. + + This is most easily used as a decorator: + + >>> @instance_checking_exception() + ... def Foo(inst): + ... return "Foo" in inst.message + >>> try: + ... raise Exception("Something Fooish") + ... except Foo as e: + ... print "True" + ... except Exception: + ... print "False" + True + + This is quite a powerful tool, mind you. + + Arguments: + instance_checker (callable): A function which checks if the given + instance should be treated as an instance of a (subclass) of this + exception. + + Returns: + Exception: (Actually: a new subclass of it), which calls the argument + `instance_checker` when it is checked against during exception + handling logic. + """ + class TemporaryClass(base_exception): + def __init__(self, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], TemporaryClass): + unwrap_me = args[0] + for attr in dir(unwrap_me): + if not attr.startswith('__'): + setattr(self, attr, getattr(unwrap_me, attr)) + else: + super(TemporaryClass, self).__init__(*args, **kwargs) + + class __metaclass__(type): + def __instancecheck__(cls, inst): + return instance_checker(inst) + + def __subclasscheck__(cls, classinfo): + value = sys.exc_info()[1] + return isinstance(value, cls) + + TemporaryClass.__name__ = instance_checker.__name__ + TemporaryClass.__doc__ = instance_checker.__doc__ + return TemporaryClass + return wrapped diff --git a/src/future/types/exceptions/pep3151.py b/src/future/types/exceptions/pep3151.py new file mode 100644 index 00000000..4b91a858 --- /dev/null +++ b/src/future/types/exceptions/pep3151.py @@ -0,0 +1,126 @@ +""" +Contains Python3-like EnvironmentError 'subclasses', except that it performs +a lot of under-the-hood magic to make it look like the standard library is +actually throwing these more specific versions instead of just OSError, OSError +and such. + +Based on https://github.com/sjoerdjob/exhacktion. +""" + +import errno + +from .base import instance_checking_exception + + +@instance_checking_exception(OSError) +def BlockingIOError(inst): + """I/O operation would block.""" + errnos = [errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, + errno.EINPROGRESS] + return hasattr(inst, 'errno') and inst.errno in errnos + + +@instance_checking_exception(OSError) +def BrokenPipeError(inst): + """Broken pipe.""" + errnos = [errno.EPIPE, errno.ESHUTDOWN] + return hasattr(inst, 'errno') and inst.errno in errnos + + +@instance_checking_exception(OSError) +def ChildProcessError(inst): + """Child process error.""" + return hasattr(inst, 'errno') and inst.errno == errno.ECHILD + + +@instance_checking_exception(OSError) +def ConnectionError(inst): + """Connection error.""" + errnos = [errno.EPIPE, errno.ESHUTDOWN, errno.ECONNABORTED, + errno.ECONNREFUSED, errno.ECONNRESET] + return hasattr(inst, 'errno') and inst.errno in errnos + + +@instance_checking_exception(OSError) +def ConnectionAbortedError(inst): + """Connection aborted.""" + return hasattr(inst, 'errno') and inst.errno == errno.ECONNABORTED + + +@instance_checking_exception(OSError) +def ConnectionRefusedError(inst): + """Connection refused.""" + return hasattr(inst, 'errno') and inst.errno == errno.ECONNREFUSED + + +@instance_checking_exception(OSError) +def ConnectionResetError(inst): + """Connection reset.""" + return hasattr(inst, 'errno') and inst.errno == errno.ECONNRESET + + +@instance_checking_exception(OSError) +def FileExistsError(inst): + """File already exists.""" + return hasattr(inst, 'errno') and inst.errno == errno.EEXIST + + +@instance_checking_exception(OSError) +def FileNotFoundError(inst): + """File not found.""" + return hasattr(inst, 'errno') and inst.errno == errno.ENOENT + + +@instance_checking_exception(OSError) +def InterruptedError(inst): + """Interrupted by signal.""" + return hasattr(inst, 'errno') and inst.errno == errno.EINTR + + +@instance_checking_exception(OSError) +def IsADirectoryError(inst): + """Operatino doesn't work on directories.""" + return hasattr(inst, 'errno') and inst.errno == errno.EISDIR + + +@instance_checking_exception(OSError) +def NotADirectoryError(inst): + """Operation only works on directories.""" + return hasattr(inst, 'errno') and inst.errno == errno.ENOTDIR + + +@instance_checking_exception(OSError) +def PermissionError(inst): + """Not enough permissions.""" + errnos = [errno.EACCES, errno.EPERM] + return hasattr(inst, 'errno') and inst.errno in errnos + + +@instance_checking_exception(OSError) +def ProcessLookupError(inst): + """Process not found.""" + return hasattr(inst, 'errno') and inst.errno == errno.ESRCH + + +@instance_checking_exception(OSError) +def TimeoutError(inst): + """Timeout expired.""" + return hasattr(inst, 'errno') and inst.errno == errno.ETIMEDOUT + +__all__ = [ + 'BlockingIOError', + 'BrokenPipeError', + 'ChildProcessError', + 'ConnectionError', + 'ConnectionAbortedError', + 'ConnectionRefusedError', + 'ConnectionResetError', + 'FileExistsError', + 'FileNotFoundError', + 'InterruptedError', + 'IsADirectoryError', + 'NotADirectoryError', + 'PermissionError', + 'ProcessLookupError', + 'TimeoutError', +] diff --git a/tests/test_future/test_pep3151_emulation.py b/tests/test_future/test_pep3151_emulation.py new file mode 100644 index 00000000..cd1c57dd --- /dev/null +++ b/tests/test_future/test_pep3151_emulation.py @@ -0,0 +1,69 @@ +import errno +import os +import re +import unittest + +from future.types.exceptions import pep3151 +from future.builtins import FileNotFoundError, NotADirectoryError, PermissionError + +MISSING_FILE = "/I/sure/hope/this/does/not.exist" + +class TestPEP3151LikeSuperClassing(unittest.TestCase): + def test_catching_enoent_from_open(self): + try: + open(MISSING_FILE) + except FileNotFoundError as e: + assert e.errno == errno.ENOENT + assert e.filename == MISSING_FILE + except Exception as e: + raise AssertionError("Could not create proper exception:" + str(e)) + + def test_does_not_catch_incorrect_exception(self): + """ + If PermissionError were just an alias for OSError, this would fail. + """ + try: + open(MISSING_FILE) + except PermissionError as e: + raise AssertionError('Wrong exception caught: PermissionError vs FileNotFoundError') + except FileNotFoundError as e: + pass + + def test_catching_enoent_from_remove(self): + try: + os.remove(MISSING_FILE) + except FileNotFoundError as e: + assert e.filename == MISSING_FILE + except Exception as e: + raise AssertionError("Could not create proper exception:" + str(e)) + + def test_not_catching_non_enoent(self): + try: + os.listdir(__file__) + except FileNotFoundError: + raise AssertionError( + "Opening `/` raised FileNotFoundError, should be ENOTDIR" + ) + pass + except OSError as e: + assert e.errno == errno.ENOTDIR + + def test_catching_enotdir_from_listdir(self): + try: + os.listdir(__file__) + except NotADirectoryError: + pass + except Exception as e: + raise AssertionError("Could not create proper exception:" + str(e)) + + def test_all_errnos_defined(self): + # An extra sanity check against typos in the errno definitions. + path = pep3151.__file__ + if path.endswith("pyc"): + path = path[:-1] + + with open(path, 'r') as fp: + contents = fp.read() + + for match in re.finditer(r"\berrno\.([A-Z]+)\b", contents): + assert hasattr(errno, match.group(1)), match.group(1)