diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f905a1c..2043285 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Get pip cache dir id: pip-cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a02eaf0..8c88669 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 'pypy2', 'pypy3'] + python-version: [3.6, 3.7, 3.8, 3.9, '3.10.0-beta - 3.10', 'pypy3'] steps: - uses: actions/checkout@v2 diff --git a/NEWS.rst b/NEWS.rst index f1ad826..920275b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,33 @@ Release History --------------- +21.6.0 (2021-06-TBD) +^^^^^^^^^^^^^^^^^^^^^^^^ + +* Switched to calendar based versioning rather than continuing with pre-1.0 + semantic versioning (`#29 `__) +* Due to the inclusion of asynchronous features from Python 3.7+, the + minimum supported Python version is now Python 3.6 + (`#29 `__) +* (WIP) Synchronised with the Python 3.10 version of contextlib, bringing the + following new features to Python 3.6+ ( + `#12 `__, + `#19 `__, + `#27 `__): + + * ``asyncontextmanager`` (Python 3.7) + * ``aclosing`` (Python 3.10) + * ``AbstractAsyncContextManager`` (Python 3.7) + * ``AsyncContextDecorator`` (Python 3.10) + * ``AsyncExitStack`` (Python 3.7) + * async support in ``nullcontext`` (Python 3.10) + +* Updates to the default compatibility testing matrix: + + * Added: CPython 3.9, CPython 3.10 + * Dropped: CPython 2.7, CPython 3.5, PyPy2 + + 0.6.0.post1 (2019-10-10) ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/VERSION.txt b/VERSION.txt index 9db1064..986c3cc 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.6.0.post1 +21.6.0 diff --git a/contextlib2.py b/contextlib2.py index 3aae8f4..a70e8cd 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -6,6 +6,8 @@ import warnings from collections import deque from functools import wraps +from _collections_abc import _check_methods + __all__ = ["contextmanager", "closing", "nullcontext", "AbstractContextManager", "ContextDecorator", "ExitStack", @@ -14,43 +16,7 @@ __all__ = ["contextmanager", "closing", "nullcontext", # Backwards compatibility __all__ += ["ContextStack"] - -# Backport abc.ABC -if sys.version_info[:2] >= (3, 4): - _abc_ABC = abc.ABC -else: - _abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) - - -# Backport classic class MRO -def _classic_mro(C, result): - if C in result: - return - result.append(C) - for B in C.__bases__: - _classic_mro(B, result) - return result - - -# Backport _collections_abc._check_methods -def _check_methods(C, *methods): - try: - mro = C.__mro__ - except AttributeError: - mro = tuple(_classic_mro(C, [])) - - for method in methods: - for B in mro: - if method in B.__dict__: - if B.__dict__[method] is None: - return NotImplemented - break - else: - return NotImplemented - return True - - -class AbstractContextManager(_abc_ABC): +class AbstractContextManager(abc.ABC): """An abstract base class for context managers.""" def __enter__(self): @@ -167,7 +133,7 @@ class _GeneratorContextManager(ContextDecorator): # Likewise, avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError # (see PEP 479). - if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: + if exc.__cause__ is value: return False raise except: @@ -313,58 +279,32 @@ class suppress(object): return exctype is not None and issubclass(exctype, self._exceptions) -# Context manipulation is Python 3 only -_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 -if _HAVE_EXCEPTION_CHAINING: - def _make_context_fixer(frame_exc): - def _fix_exception_context(new_exc, old_exc): - # Context may not be correct, so find the end of the chain - while 1: - exc_context = new_exc.__context__ - if exc_context is old_exc: - # Context is already set correctly (see issue 20317) - return - if exc_context is None or exc_context is frame_exc: - break - new_exc = exc_context - # Change the end of the chain to point to the exception - # we expect it to reference - new_exc.__context__ = old_exc - return _fix_exception_context +# Context manipulation helpers +def _make_context_fixer(frame_exc): + def _fix_exception_context(new_exc, old_exc): + # Context may not be correct, so find the end of the chain + while 1: + exc_context = new_exc.__context__ + if exc_context is old_exc: + # Context is already set correctly (see issue 20317) + return + if exc_context is None or exc_context is frame_exc: + break + new_exc = exc_context + # Change the end of the chain to point to the exception + # we expect it to reference + new_exc.__context__ = old_exc + return _fix_exception_context - def _reraise_with_existing_context(exc_details): - try: - # bare "raise exc_details[1]" replaces our carefully - # set-up context - fixed_ctx = exc_details[1].__context__ - raise exc_details[1] - except BaseException: - exc_details[1].__context__ = fixed_ctx - raise -else: - # No exception context in Python 2 - def _make_context_fixer(frame_exc): - return lambda new_exc, old_exc: None - - # Use 3 argument raise in Python 2, - # but use exec to avoid SyntaxError in Python 3 - def _reraise_with_existing_context(exc_details): - exc_type, exc_value, exc_tb = exc_details - exec("raise exc_type, exc_value, exc_tb") - -# Handle old-style classes if they exist -try: - from types import InstanceType -except ImportError: - # Python 3 doesn't have old-style classes - _get_type = type -else: - # Need to handle old-style context managers on Python 2 - def _get_type(obj): - obj_type = type(obj) - if obj_type is InstanceType: - return obj.__class__ # Old-style class - return obj_type # New-style class +def _reraise_with_existing_context(exc_details): + try: + # bare "raise exc_details[1]" replaces our carefully + # set-up context + fixed_ctx = exc_details[1].__context__ + raise exc_details[1] + except BaseException: + exc_details[1].__context__ = fixed_ctx + raise # Inspired by discussions on http://bugs.python.org/issue13585 @@ -407,7 +347,7 @@ class ExitStack(object): """ # We use an unbound method rather than a bound method to follow # the standard lookup behaviour for special methods - _cb_type = _get_type(exit) + _cb_type = type(exit) try: exit_method = _cb_type.__exit__ except AttributeError: @@ -437,7 +377,7 @@ class ExitStack(object): returns the result of the __enter__ method. """ # We look up the special methods on the type to match the with statement - _cm_type = _get_type(cm) + _cm_type = type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) self._push_cm_exit(cm, _exit) diff --git a/setup.py b/setup.py index 28f403b..afa7938 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ except ImportError: setup( name='contextlib2', version=open('VERSION.txt').read().strip(), - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.6', py_modules=['contextlib2'], license='PSF License', description='Backports and enhancements for the contextlib module', @@ -19,13 +19,13 @@ setup( 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: Python Software Foundation License', # These are the Python versions tested, it may work on others - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', + # It definitely won't work on versions without native async support 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], ) diff --git a/test_contextlib2.py b/test_contextlib2.py index 7c19cb8..994798d 100755 --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -9,9 +9,6 @@ import __future__ # For PEP 479 conditional test import contextlib2 from contextlib2 import * # Tests __all__ -if not hasattr(unittest.TestCase, "assertRaisesRegex"): - import unittest2 as unittest - requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, "Test requires docstrings") @@ -421,9 +418,6 @@ class TestContextDecorator(unittest.TestCase): test('something else') self.assertEqual(state, [1, 'something else', 999]) -# Detailed exception chaining checks only make sense on Python 3 -check_exception_chaining = contextlib2._HAVE_EXCEPTION_CHAINING - class TestExitStack(unittest.TestCase): @requires_docstrings @@ -592,18 +586,16 @@ class TestExitStack(unittest.TestCase): with RaiseExc(ValueError): 1 / 0 except IndexError as exc: - if check_exception_chaining: - self.assertIsInstance(exc.__context__, KeyError) - self.assertIsInstance(exc.__context__.__context__, AttributeError) - # Inner exceptions were suppressed - self.assertIsNone(exc.__context__.__context__.__context__) + self.assertIsInstance(exc.__context__, KeyError) + self.assertIsInstance(exc.__context__.__context__, AttributeError) + # Inner exceptions were suppressed + self.assertIsNone(exc.__context__.__context__.__context__) else: self.fail("Expected IndexError, but no exception was raised") # Check the inner exceptions inner_exc = SuppressExc.saved_details[1] self.assertIsInstance(inner_exc, ValueError) - if check_exception_chaining: - self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) def test_exit_exception_chaining(self): # Ensure exception chaining matches the reference behaviour @@ -624,18 +616,16 @@ class TestExitStack(unittest.TestCase): stack.callback(raise_exc, ValueError) 1 / 0 except IndexError as exc: - if check_exception_chaining: - self.assertIsInstance(exc.__context__, KeyError) - self.assertIsInstance(exc.__context__.__context__, AttributeError) - # Inner exceptions were suppressed - self.assertIsNone(exc.__context__.__context__.__context__) + self.assertIsInstance(exc.__context__, KeyError) + self.assertIsInstance(exc.__context__.__context__, AttributeError) + # Inner exceptions were suppressed + self.assertIsNone(exc.__context__.__context__.__context__) else: self.fail("Expected IndexError, but no exception was raised") # Check the inner exceptions inner_exc = saved_details[0][1] self.assertIsInstance(inner_exc, ValueError) - if check_exception_chaining: - self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) def test_exit_exception_non_suppressing(self): # http://bugs.python.org/issue19092 @@ -689,12 +679,11 @@ class TestExitStack(unittest.TestCase): raise exc1 except Exception as exc: self.assertIs(exc, exc4) - if check_exception_chaining: - self.assertIs(exc.__context__, exc3) - self.assertIs(exc.__context__.__context__, exc2) - self.assertIs(exc.__context__.__context__.__context__, exc1) - self.assertIsNone( - exc.__context__.__context__.__context__.__context__) + self.assertIs(exc.__context__, exc3) + self.assertIs(exc.__context__.__context__, exc2) + self.assertIs(exc.__context__.__context__.__context__, exc1) + self.assertIsNone( + exc.__context__.__context__.__context__.__context__) def test_exit_exception_with_existing_context(self): # Addresses a lack of test coverage discovered after checking in a @@ -716,16 +705,13 @@ class TestExitStack(unittest.TestCase): raise exc1 except Exception as exc: self.assertIs(exc, exc5) - if check_exception_chaining: - self.assertIs(exc.__context__, exc4) - self.assertIs(exc.__context__.__context__, exc3) - self.assertIs(exc.__context__.__context__.__context__, exc2) - self.assertIs( - exc.__context__.__context__.__context__.__context__, exc1) - self.assertIsNone( - exc.__context__.__context__.__context__.__context__.__context__) - - + self.assertIs(exc.__context__, exc4) + self.assertIs(exc.__context__.__context__, exc3) + self.assertIs(exc.__context__.__context__.__context__, exc2) + self.assertIs( + exc.__context__.__context__.__context__.__context__, exc1) + self.assertIsNone( + exc.__context__.__context__.__context__.__context__.__context__) def test_body_exception_suppress(self): def suppress_exc(*exc_details): @@ -824,10 +810,9 @@ class TestExitStack(unittest.TestCase): exc = err_ctx.exception self.assertIsInstance(exc, UniqueException) self.assertIsInstance(exc.__cause__, UniqueRuntimeError) - if check_exception_chaining: - self.assertIs(exc.__context__, exc.__cause__) - self.assertIsNone(exc.__cause__.__context__) - self.assertIsNone(exc.__cause__.__cause__) + self.assertIs(exc.__context__, exc.__cause__) + self.assertIsNone(exc.__cause__.__context__) + self.assertIsNone(exc.__cause__.__cause__) class TestRedirectStream: diff --git a/tox.ini b/tox.ini index d682619..77b66ed 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35,36,37,py,py3} +envlist = py{36,37,38,39,3_10,py3} skip_missing_interpreters = True [testenv] @@ -9,15 +9,12 @@ commands = coverage xml deps = coverage - py27: unittest2 - pypy: unittest2 [gh-actions] python = - 2.7: py27 - 3.5: py35 3.6: py36 3.7: py37 3.8: py38 - pypy2: pypy + 3.9: py39 + 3.10: py3_10 pypy3: pypy3