mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-05-23 22:25:52 +00:00
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:
commit
94f3881963
8 changed files with 94 additions and 145 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
27
NEWS.rst
27
NEWS.rst
|
|
@ -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)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
0.6.0.post1
|
21.6.0
|
||||||
|
|
|
||||||
122
contextlib2.py
122
contextlib2.py
|
|
@ -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)
|
||||||
|
|
|
||||||
10
setup.py
10
setup.py
|
|
@ -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',
|
||||||
],
|
],
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
9
tox.ini
9
tox.ini
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue