Merge pull request #30 from ncoghlan/issue-29-switch-to-calver-update-min-py-version

Issue #29: Switch to CalVer, require Python >= 3.6
This commit is contained in:
Nick Coghlan 2021-06-26 16:14:45 +10:00 committed by GitHub
commit 94f3881963
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 145 deletions

View file

@ -18,7 +18,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.9
- name: Get pip cache dir - name: Get pip cache dir
id: pip-cache id: pip-cache

View file

@ -8,7 +8,7 @@ jobs:
strategy: strategy:
max-parallel: 5 max-parallel: 5
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -1,6 +1,33 @@
Release History Release History
--------------- ---------------
21.6.0 (2021-06-TBD)
^^^^^^^^^^^^^^^^^^^^^^^^
* Switched to calendar based versioning rather than continuing with pre-1.0
semantic versioning (`#29 <https://github.com/jazzband/contextlib2/issues/29>`__)
* Due to the inclusion of asynchronous features from Python 3.7+, the
minimum supported Python version is now Python 3.6
(`#29 <https://github.com/jazzband/contextlib2/issues/29>`__)
* (WIP) Synchronised with the Python 3.10 version of contextlib, bringing the
following new features to Python 3.6+ (
`#12 <https://github.com/jazzband/contextlib2/issues/12>`__,
`#19 <https://github.com/jazzband/contextlib2/issues/19>`__,
`#27 <https://github.com/jazzband/contextlib2/issues/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) 0.6.0.post1 (2019-10-10)
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -1 +1 @@
0.6.0.post1 21.6.0

View file

@ -6,6 +6,8 @@ import warnings
from collections import deque from collections import deque
from functools import wraps from functools import wraps
from _collections_abc import _check_methods
__all__ = ["contextmanager", "closing", "nullcontext", __all__ = ["contextmanager", "closing", "nullcontext",
"AbstractContextManager", "AbstractContextManager",
"ContextDecorator", "ExitStack", "ContextDecorator", "ExitStack",
@ -14,43 +16,7 @@ __all__ = ["contextmanager", "closing", "nullcontext",
# Backwards compatibility # Backwards compatibility
__all__ += ["ContextStack"] __all__ += ["ContextStack"]
class AbstractContextManager(abc.ABC):
# 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):
"""An abstract base class for context managers.""" """An abstract base class for context managers."""
def __enter__(self): def __enter__(self):
@ -167,7 +133,7 @@ class _GeneratorContextManager(ContextDecorator):
# Likewise, avoid suppressing if a StopIteration exception # Likewise, avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError # was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479). # (see PEP 479).
if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: if exc.__cause__ is value:
return False return False
raise raise
except: except:
@ -313,58 +279,32 @@ class suppress(object):
return exctype is not None and issubclass(exctype, self._exceptions) return exctype is not None and issubclass(exctype, self._exceptions)
# Context manipulation is Python 3 only # Context manipulation helpers
_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 def _make_context_fixer(frame_exc):
if _HAVE_EXCEPTION_CHAINING: def _fix_exception_context(new_exc, old_exc):
def _make_context_fixer(frame_exc): # Context may not be correct, so find the end of the chain
def _fix_exception_context(new_exc, old_exc): while 1:
# Context may not be correct, so find the end of the chain exc_context = new_exc.__context__
while 1: if exc_context is old_exc:
exc_context = new_exc.__context__ # Context is already set correctly (see issue 20317)
if exc_context is old_exc: return
# Context is already set correctly (see issue 20317) if exc_context is None or exc_context is frame_exc:
return break
if exc_context is None or exc_context is frame_exc: new_exc = exc_context
break # Change the end of the chain to point to the exception
new_exc = exc_context # we expect it to reference
# Change the end of the chain to point to the exception new_exc.__context__ = old_exc
# we expect it to reference return _fix_exception_context
new_exc.__context__ = old_exc
return _fix_exception_context
def _reraise_with_existing_context(exc_details): def _reraise_with_existing_context(exc_details):
try: try:
# bare "raise exc_details[1]" replaces our carefully # bare "raise exc_details[1]" replaces our carefully
# set-up context # set-up context
fixed_ctx = exc_details[1].__context__ fixed_ctx = exc_details[1].__context__
raise exc_details[1] raise exc_details[1]
except BaseException: except BaseException:
exc_details[1].__context__ = fixed_ctx exc_details[1].__context__ = fixed_ctx
raise 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
# Inspired by discussions on http://bugs.python.org/issue13585 # 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 # We use an unbound method rather than a bound method to follow
# the standard lookup behaviour for special methods # the standard lookup behaviour for special methods
_cb_type = _get_type(exit) _cb_type = type(exit)
try: try:
exit_method = _cb_type.__exit__ exit_method = _cb_type.__exit__
except AttributeError: except AttributeError:
@ -437,7 +377,7 @@ class ExitStack(object):
returns the result of the __enter__ method. returns the result of the __enter__ method.
""" """
# We look up the special methods on the type to match the with statement # 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__ _exit = _cm_type.__exit__
result = _cm_type.__enter__(cm) result = _cm_type.__enter__(cm)
self._push_cm_exit(cm, _exit) self._push_cm_exit(cm, _exit)

View file

@ -7,7 +7,7 @@ except ImportError:
setup( setup(
name='contextlib2', name='contextlib2',
version=open('VERSION.txt').read().strip(), 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'], py_modules=['contextlib2'],
license='PSF License', license='PSF License',
description='Backports and enhancements for the contextlib module', description='Backports and enhancements for the contextlib module',
@ -19,13 +19,13 @@ setup(
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: Python Software Foundation License', 'License :: OSI Approved :: Python Software Foundation License',
# These are the Python versions tested, it may work on others # These are the Python versions tested, it may work on others
'Programming Language :: Python :: 2', # It definitely won't work on versions without native async support
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
], ],
) )

View file

@ -9,9 +9,6 @@ import __future__ # For PEP 479 conditional test
import contextlib2 import contextlib2
from contextlib2 import * # Tests __all__ from contextlib2 import * # Tests __all__
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
import unittest2 as unittest
requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2,
"Test requires docstrings") "Test requires docstrings")
@ -421,9 +418,6 @@ class TestContextDecorator(unittest.TestCase):
test('something else') test('something else')
self.assertEqual(state, [1, 'something else', 999]) 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): class TestExitStack(unittest.TestCase):
@requires_docstrings @requires_docstrings
@ -592,18 +586,16 @@ class TestExitStack(unittest.TestCase):
with RaiseExc(ValueError): with RaiseExc(ValueError):
1 / 0 1 / 0
except IndexError as exc: except IndexError as exc:
if check_exception_chaining: self.assertIsInstance(exc.__context__, KeyError)
self.assertIsInstance(exc.__context__, KeyError) self.assertIsInstance(exc.__context__.__context__, AttributeError)
self.assertIsInstance(exc.__context__.__context__, AttributeError) # Inner exceptions were suppressed
# Inner exceptions were suppressed self.assertIsNone(exc.__context__.__context__.__context__)
self.assertIsNone(exc.__context__.__context__.__context__)
else: else:
self.fail("Expected IndexError, but no exception was raised") self.fail("Expected IndexError, but no exception was raised")
# Check the inner exceptions # Check the inner exceptions
inner_exc = SuppressExc.saved_details[1] inner_exc = SuppressExc.saved_details[1]
self.assertIsInstance(inner_exc, ValueError) 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): def test_exit_exception_chaining(self):
# Ensure exception chaining matches the reference behaviour # Ensure exception chaining matches the reference behaviour
@ -624,18 +616,16 @@ class TestExitStack(unittest.TestCase):
stack.callback(raise_exc, ValueError) stack.callback(raise_exc, ValueError)
1 / 0 1 / 0
except IndexError as exc: except IndexError as exc:
if check_exception_chaining: self.assertIsInstance(exc.__context__, KeyError)
self.assertIsInstance(exc.__context__, KeyError) self.assertIsInstance(exc.__context__.__context__, AttributeError)
self.assertIsInstance(exc.__context__.__context__, AttributeError) # Inner exceptions were suppressed
# Inner exceptions were suppressed self.assertIsNone(exc.__context__.__context__.__context__)
self.assertIsNone(exc.__context__.__context__.__context__)
else: else:
self.fail("Expected IndexError, but no exception was raised") self.fail("Expected IndexError, but no exception was raised")
# Check the inner exceptions # Check the inner exceptions
inner_exc = saved_details[0][1] inner_exc = saved_details[0][1]
self.assertIsInstance(inner_exc, ValueError) 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): def test_exit_exception_non_suppressing(self):
# http://bugs.python.org/issue19092 # http://bugs.python.org/issue19092
@ -689,12 +679,11 @@ class TestExitStack(unittest.TestCase):
raise exc1 raise exc1
except Exception as exc: except Exception as exc:
self.assertIs(exc, exc4) self.assertIs(exc, exc4)
if check_exception_chaining: self.assertIs(exc.__context__, exc3)
self.assertIs(exc.__context__, exc3) self.assertIs(exc.__context__.__context__, exc2)
self.assertIs(exc.__context__.__context__, exc2) self.assertIs(exc.__context__.__context__.__context__, exc1)
self.assertIs(exc.__context__.__context__.__context__, exc1) self.assertIsNone(
self.assertIsNone( exc.__context__.__context__.__context__.__context__)
exc.__context__.__context__.__context__.__context__)
def test_exit_exception_with_existing_context(self): def test_exit_exception_with_existing_context(self):
# Addresses a lack of test coverage discovered after checking in a # Addresses a lack of test coverage discovered after checking in a
@ -716,16 +705,13 @@ class TestExitStack(unittest.TestCase):
raise exc1 raise exc1
except Exception as exc: except Exception as exc:
self.assertIs(exc, exc5) self.assertIs(exc, exc5)
if check_exception_chaining: self.assertIs(exc.__context__, exc4)
self.assertIs(exc.__context__, exc4) self.assertIs(exc.__context__.__context__, exc3)
self.assertIs(exc.__context__.__context__, exc3) self.assertIs(exc.__context__.__context__.__context__, exc2)
self.assertIs(exc.__context__.__context__.__context__, exc2) self.assertIs(
self.assertIs( exc.__context__.__context__.__context__.__context__, exc1)
exc.__context__.__context__.__context__.__context__, exc1) self.assertIsNone(
self.assertIsNone( exc.__context__.__context__.__context__.__context__.__context__)
exc.__context__.__context__.__context__.__context__.__context__)
def test_body_exception_suppress(self): def test_body_exception_suppress(self):
def suppress_exc(*exc_details): def suppress_exc(*exc_details):
@ -824,10 +810,9 @@ class TestExitStack(unittest.TestCase):
exc = err_ctx.exception exc = err_ctx.exception
self.assertIsInstance(exc, UniqueException) self.assertIsInstance(exc, UniqueException)
self.assertIsInstance(exc.__cause__, UniqueRuntimeError) self.assertIsInstance(exc.__cause__, UniqueRuntimeError)
if check_exception_chaining: self.assertIs(exc.__context__, exc.__cause__)
self.assertIs(exc.__context__, exc.__cause__) self.assertIsNone(exc.__cause__.__context__)
self.assertIsNone(exc.__cause__.__context__) self.assertIsNone(exc.__cause__.__cause__)
self.assertIsNone(exc.__cause__.__cause__)
class TestRedirectStream: class TestRedirectStream:

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py{27,35,36,37,py,py3} envlist = py{36,37,38,39,3_10,py3}
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
@ -9,15 +9,12 @@ commands =
coverage xml coverage xml
deps = deps =
coverage coverage
py27: unittest2
pypy: unittest2
[gh-actions] [gh-actions]
python = python =
2.7: py27
3.5: py35
3.6: py36 3.6: py36
3.7: py37 3.7: py37
3.8: py38 3.8: py38
pypy2: pypy 3.9: py39
3.10: py3_10
pypy3: pypy3 pypy3: pypy3