Skip to content

Commit

Permalink
Merge pull request #30 from ncoghlan/issue-29-switch-to-calver-update…
Browse files Browse the repository at this point in the history
…-min-py-version

Issue #29: Switch to CalVer, require Python >= 3.6
  • Loading branch information
ncoghlan authored Jun 26, 2021
2 parents 5f8498c + eed9606 commit 94f3881
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 146 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -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 <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)
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.0.post1
21.6.0
124 changes: 32 additions & 92 deletions contextlib2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from collections import deque
from functools import wraps

from _collections_abc import _check_methods

__all__ = ["contextmanager", "closing", "nullcontext",
"AbstractContextManager",
"ContextDecorator", "ExitStack",
Expand All @@ -14,43 +16,7 @@
# 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):
Expand Down Expand Up @@ -167,7 +133,7 @@ def __exit__(self, type, value, traceback):
# 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:
Expand Down Expand Up @@ -313,58 +279,32 @@ def __exit__(self, exctype, excinst, exctb):
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

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
# 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


# Inspired by discussions on http://bugs.python.org/issue13585
Expand Down Expand Up @@ -407,7 +347,7 @@ def push(self, exit):
"""
# 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:
Expand Down Expand Up @@ -437,7 +377,7 @@ def enter_context(self, cm):
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)
Expand Down
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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',
Expand All @@ -19,13 +19,13 @@
'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',
],

)
65 changes: 25 additions & 40 deletions test_contextlib2.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
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")

Expand Down Expand Up @@ -421,9 +418,6 @@ def test(x):
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
Expand Down Expand Up @@ -592,18 +586,16 @@ def __exit__(self, *exc_details):
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
Expand All @@ -624,18 +616,16 @@ def suppress_exc(*exc_details):
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
Expand Down Expand Up @@ -689,12 +679,11 @@ def gets_the_context_right(exc):
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
Expand All @@ -716,16 +705,13 @@ def raise_nested(inner_exc, outer_exc):
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):
Expand Down Expand Up @@ -824,10 +810,9 @@ def first():
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:
Expand Down
9 changes: 3 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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

0 comments on commit 94f3881

Please sign in to comment.