From e42cd73fe9857366154d783e2a85e3d8a72ce5e7 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 20:43:11 +1000 Subject: [PATCH 1/6] Issue #33: convert to package and include typeshed type hints --- CONTRIBUTING.md | 4 +- LICENSE.txt | 4 +- MANIFEST.in | 7 +- README.rst | 19 +++- contextlib2.py => contextlib2/__init__.py | 0 contextlib2/contextlib.pyi | 122 ++++++++++++++++++++++ contextlib2/py.typed | 0 setup.py | 4 +- 8 files changed, 153 insertions(+), 7 deletions(-) rename contextlib2.py => contextlib2/__init__.py (100%) create mode 100644 contextlib2/contextlib.pyi create mode 100644 contextlib2/py.typed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad78220..c3fec94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,5 @@ [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) -This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). +This is a [Jazzband](https://jazzband.co/) project. By contributing you agree +to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) +and follow the [guidelines](https://jazzband.co/about/guidelines). diff --git a/LICENSE.txt b/LICENSE.txt index 5de2027..e40caa1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,6 @@ - +Note: The type hints included in this package come from the typeshed project, +and are hence distributed under the Apache License 2.0 rather than under the +Python Software License that covers the module implementation and test suite. A. HISTORY OF THE SOFTWARE ========================== diff --git a/MANIFEST.in b/MANIFEST.in index 5b839f9..46fdec8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,5 @@ -include *.py *.txt *.rst *.md *.ini MANIFEST.in -recursive-include dev test docs *.rst *.py make.bat Makefile +include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in +recursive-include contextlib2 *.py *.pyi py.typed +recursive-include docs *.rst *.py make.bat Makefile +recursive-include test *.py +recursive-include dev *.patch diff --git a/README.rst b/README.rst index d64ba87..2927e6f 100644 --- a/README.rst +++ b/README.rst @@ -18,9 +18,19 @@ contextlib2 is a backport of the `standard library's contextlib module `_ to earlier Python versions. -It also serves as a real world proving ground for possible future +It also sometimes serves as a real world proving ground for possible future enhancements to the standard library version. +Licensing +--------- + +As a backport of Python standard library software, the implementation, test +suite and other supporting files for this project are distributed under the +Python Software License used for the CPython reference implementation. + +The one exception is the included type hints file, which comes from the +``typeshed`` project, and is hence distributed under the Apache License 2.0. + Development ----------- @@ -53,10 +63,15 @@ Updating to a new stdlib reference version As of Python 3.10, 3 files needed to be copied from the CPython reference implementation to contextlib2: -* ``Lib/contextlib.py`` -> ``contextlib2.py`` +* ``Lib/contextlib.py`` -> ``contextlib2/__init__.py`` * ``Lib/test/test_contextlib.py`` -> ``test/test_contextlib.py`` * ``Lib/test/test_contextlib_async.py`` -> ``test/test_contextlib_async.py`` +The corresponding version of ``contextlib2/__init__.py`` also needs to be +retrieved from the ``typeshed`` project:: + + wget https://raw.githubusercontent.com/python/typeshed/master/stdlib/contextlib.pyi + For the 3.10 sync, the only changes needed to the test files were to import from ``contextlib2`` rather than ``contextlib``. The test directory is laid out so that the test suite's imports from ``test.support`` work the same way they do in diff --git a/contextlib2.py b/contextlib2/__init__.py similarity index 100% rename from contextlib2.py rename to contextlib2/__init__.py diff --git a/contextlib2/contextlib.pyi b/contextlib2/contextlib.pyi new file mode 100644 index 0000000..c2674fe --- /dev/null +++ b/contextlib2/contextlib.pyi @@ -0,0 +1,122 @@ +# Type hints copied from the typeshed project under the Apache License 2.0 +# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE + +import sys +from types import TracebackType +from typing import ( + IO, + Any, + AsyncContextManager, + AsyncIterator, + Awaitable, + Callable, + ContextManager, + Iterator, + Optional, + Type, + TypeVar, + overload, +) +from typing_extensions import ParamSpec, Protocol + +AbstractContextManager = ContextManager +if sys.version_info >= (3, 7): + AbstractAsyncContextManager = AsyncContextManager + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_T_io = TypeVar("_T_io", bound=Optional[IO[str]]) +_F = TypeVar("_F", bound=Callable[..., Any]) +_P = ParamSpec("_P") + +_ExitFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], bool] +_CM_EF = TypeVar("_CM_EF", ContextManager[Any], _ExitFunc) + +class _GeneratorContextManager(ContextManager[_T_co]): + def __call__(self, func: _F) -> _F: ... + +# type ignore to deal with incomplete ParamSpec support in mypy +def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, _GeneratorContextManager[_T]]: ... # type: ignore + +if sys.version_info >= (3, 7): + def asynccontextmanager(func: Callable[_P, AsyncIterator[_T]]) -> Callable[_P, AsyncContextManager[_T]]: ... # type: ignore + +class _SupportsClose(Protocol): + def close(self) -> object: ... + +_SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) + +class closing(ContextManager[_SupportsCloseT]): + def __init__(self, thing: _SupportsCloseT) -> None: ... + +if sys.version_info >= (3, 10): + class _SupportsAclose(Protocol): + async def aclose(self) -> object: ... + _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) + class aclosing(AsyncContextManager[_SupportsAcloseT]): + def __init__(self, thing: _SupportsAcloseT) -> None: ... + _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) + class AsyncContextDecorator: + def __call__(self, func: _AF) -> _AF: ... + +class suppress(ContextManager[None]): + def __init__(self, *exceptions: Type[BaseException]) -> None: ... + def __exit__( + self, exctype: Optional[Type[BaseException]], excinst: Optional[BaseException], exctb: Optional[TracebackType] + ) -> bool: ... + +class redirect_stdout(ContextManager[_T_io]): + def __init__(self, new_target: _T_io) -> None: ... + +class redirect_stderr(ContextManager[_T_io]): + def __init__(self, new_target: _T_io) -> None: ... + +class ContextDecorator: + def __call__(self, func: _F) -> _F: ... + +_U = TypeVar("_U", bound=ExitStack) + +class ExitStack(ContextManager[ExitStack]): + def __init__(self) -> None: ... + def enter_context(self, cm: ContextManager[_T]) -> _T: ... + def push(self, exit: _CM_EF) -> _CM_EF: ... + def callback(self, callback: Callable[..., Any], *args: Any, **kwds: Any) -> Callable[..., Any]: ... + def pop_all(self: _U) -> _U: ... + def close(self) -> None: ... + def __enter__(self: _U) -> _U: ... + def __exit__( + self, + __exc_type: Optional[Type[BaseException]], + __exc_value: Optional[BaseException], + __traceback: Optional[TracebackType], + ) -> bool: ... + +if sys.version_info >= (3, 7): + _S = TypeVar("_S", bound=AsyncExitStack) + + _ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]] + _CallbackCoroFunc = Callable[..., Awaitable[Any]] + _ACM_EF = TypeVar("_ACM_EF", AsyncContextManager[Any], _ExitCoroFunc) + class AsyncExitStack(AsyncContextManager[AsyncExitStack]): + def __init__(self) -> None: ... + def enter_context(self, cm: ContextManager[_T]) -> _T: ... + def enter_async_context(self, cm: AsyncContextManager[_T]) -> Awaitable[_T]: ... + def push(self, exit: _CM_EF) -> _CM_EF: ... + def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... + def callback(self, callback: Callable[..., Any], *args: Any, **kwds: Any) -> Callable[..., Any]: ... + def push_async_callback(self, callback: _CallbackCoroFunc, *args: Any, **kwds: Any) -> _CallbackCoroFunc: ... + def pop_all(self: _S) -> _S: ... + def aclose(self) -> Awaitable[None]: ... + def __aenter__(self: _S) -> Awaitable[_S]: ... + def __aexit__( + self, + __exc_type: Optional[Type[BaseException]], + __exc_value: Optional[BaseException], + __traceback: Optional[TracebackType], + ) -> Awaitable[bool]: ... + +if sys.version_info >= (3, 7): + @overload + def nullcontext(enter_result: _T) -> ContextManager[_T]: ... + @overload + def nullcontext() -> ContextManager[None]: ... diff --git a/contextlib2/py.typed b/contextlib2/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index afa7938..dc64785 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ setup( name='contextlib2', version=open('VERSION.txt').read().strip(), python_requires='>=3.6', - py_modules=['contextlib2'], + packages=['contextlib2'], + include_package_data=True, license='PSF License', description='Backports and enhancements for the contextlib module', long_description=open('README.rst').read(), @@ -17,6 +18,7 @@ setup( url='http://contextlib2.readthedocs.org', classifiers=[ 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: Apache Software License', 'License :: OSI Approved :: Python Software Foundation License', # These are the Python versions tested, it may work on others # It definitely won't work on versions without native async support From 802688462ce09774867a2c33d9506b2253671ea6 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 21:43:16 +1000 Subject: [PATCH 2/6] Update README (including for docs backport) --- README.rst | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 2927e6f..47c21ed 100644 --- a/README.rst +++ b/README.rst @@ -60,14 +60,15 @@ Versions currently tested in both tox and GitHub Actions are: Updating to a new stdlib reference version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -As of Python 3.10, 3 files needed to be copied from the CPython reference +As of Python 3.10, 4 files needed to be copied from the CPython reference implementation to contextlib2: +* ``Doc/contextlib.rst`` -> ``docs/contextlib2.rst`` * ``Lib/contextlib.py`` -> ``contextlib2/__init__.py`` * ``Lib/test/test_contextlib.py`` -> ``test/test_contextlib.py`` * ``Lib/test/test_contextlib_async.py`` -> ``test/test_contextlib_async.py`` -The corresponding version of ``contextlib2/__init__.py`` also needs to be +The corresponding version of ``contextlib2/__init__.pyi`` also needs to be retrieved from the ``typeshed`` project:: wget https://raw.githubusercontent.com/python/typeshed/master/stdlib/contextlib.pyi @@ -77,6 +78,14 @@ For the 3.10 sync, the only changes needed to the test files were to import from that the test suite's imports from ``test.support`` work the same way they do in the main CPython test suite. -The changes made to the ``contextlib2.py`` file to get it to run on the older -versions (and to add back in the deprecated APIs that never graduated to the -standard library version) are saved as a patch file in the ``dev`` directory. +The following patch files are saved in the ``dev`` directory: + +* changes made to ``contextlib2/__init__.py`` to get it to run on the older + versions (and to add back in the deprecated APIs that never graduated to + the standard library version) +* changes made to ``contextlib2/__init__.pyi`` to make the Python version + guards unconditional (since the ``contextlib2`` API is the same on all + supported versions) +* changes made to ``docs/contextlib2.rst`` to use ``contextlib2`` version + numbers in the version added/changed notes and to integrate the module + documentation with the rest of the project documentation From 72c47e462910541fe23a8b4a2fa0d2af9054d591 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 21:43:44 +1000 Subject: [PATCH 3/6] Fix type hint filename, make API spec version independent --- contextlib2/{contextlib.pyi => __init__.pyi} | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) rename contextlib2/{contextlib.pyi => __init__.pyi} (95%) diff --git a/contextlib2/contextlib.pyi b/contextlib2/__init__.pyi similarity index 95% rename from contextlib2/contextlib.pyi rename to contextlib2/__init__.pyi index c2674fe..5fe29b7 100644 --- a/contextlib2/contextlib.pyi +++ b/contextlib2/__init__.pyi @@ -19,8 +19,11 @@ from typing import ( ) from typing_extensions import ParamSpec, Protocol +# Note: the various 'if True:' guards replace sys.version checks in the +# original typeshed file that don't apply to the contextlib2 backport API + AbstractContextManager = ContextManager -if sys.version_info >= (3, 7): +if True: AbstractAsyncContextManager = AsyncContextManager _T = TypeVar("_T") @@ -38,7 +41,7 @@ class _GeneratorContextManager(ContextManager[_T_co]): # type ignore to deal with incomplete ParamSpec support in mypy def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, _GeneratorContextManager[_T]]: ... # type: ignore -if sys.version_info >= (3, 7): +if True: def asynccontextmanager(func: Callable[_P, AsyncIterator[_T]]) -> Callable[_P, AsyncContextManager[_T]]: ... # type: ignore class _SupportsClose(Protocol): @@ -49,7 +52,7 @@ _SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) class closing(ContextManager[_SupportsCloseT]): def __init__(self, thing: _SupportsCloseT) -> None: ... -if sys.version_info >= (3, 10): +if True: class _SupportsAclose(Protocol): async def aclose(self) -> object: ... _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) @@ -91,7 +94,7 @@ class ExitStack(ContextManager[ExitStack]): __traceback: Optional[TracebackType], ) -> bool: ... -if sys.version_info >= (3, 7): +if True: _S = TypeVar("_S", bound=AsyncExitStack) _ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]] @@ -115,7 +118,7 @@ if sys.version_info >= (3, 7): __traceback: Optional[TracebackType], ) -> Awaitable[bool]: ... -if sys.version_info >= (3, 7): +if True: @overload def nullcontext(enter_result: _T) -> ContextManager[_T]: ... @overload From 841d0ae73a157befa0fc3fc795e645b63b12dee3 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 21:49:03 +1000 Subject: [PATCH 4/6] Include the pyi patch file --- ...10_contextlib_pyi_to_contextlib2_pyi.patch | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch diff --git a/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch b/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch new file mode 100644 index 0000000..6e1f7e1 --- /dev/null +++ b/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch @@ -0,0 +1,58 @@ +--- ../contextlib.pyi 2021-06-26 21:36:16.491964153 +1000 ++++ contextlib2/__init__.pyi 2021-06-26 21:41:08.109598690 +1000 +@@ -1,3 +1,6 @@ ++# Type hints copied from the typeshed project under the Apache License 2.0 ++# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE ++ + import sys + from types import TracebackType + from typing import ( +@@ -16,8 +19,11 @@ + ) + from typing_extensions import ParamSpec, Protocol + ++# Note: the various 'if True:' guards replace sys.version checks in the ++# original typeshed file that don't apply to the contextlib2 backport API ++ + AbstractContextManager = ContextManager +-if sys.version_info >= (3, 7): ++if True: + AbstractAsyncContextManager = AsyncContextManager + + _T = TypeVar("_T") +@@ -35,7 +41,7 @@ + # type ignore to deal with incomplete ParamSpec support in mypy + def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, _GeneratorContextManager[_T]]: ... # type: ignore + +-if sys.version_info >= (3, 7): ++if True: + def asynccontextmanager(func: Callable[_P, AsyncIterator[_T]]) -> Callable[_P, AsyncContextManager[_T]]: ... # type: ignore + + class _SupportsClose(Protocol): +@@ -46,7 +52,7 @@ + class closing(ContextManager[_SupportsCloseT]): + def __init__(self, thing: _SupportsCloseT) -> None: ... + +-if sys.version_info >= (3, 10): ++if True: + class _SupportsAclose(Protocol): + async def aclose(self) -> object: ... + _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) +@@ -88,7 +94,7 @@ + __traceback: Optional[TracebackType], + ) -> bool: ... + +-if sys.version_info >= (3, 7): ++if True: + _S = TypeVar("_S", bound=AsyncExitStack) + + _ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]] +@@ -112,7 +118,7 @@ + __traceback: Optional[TracebackType], + ) -> Awaitable[bool]: ... + +-if sys.version_info >= (3, 7): ++if True: + @overload + def nullcontext(enter_result: _T) -> ContextManager[_T]: ... + @overload From 0fb391a30c14a85f8e3d1eca412b16311123e278 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 22:12:55 +1000 Subject: [PATCH 5/6] Actually run a typecheck in CI --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 6b0c293..5f7972a 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,11 @@ commands = coverage run -m unittest discover -t . -s test coverage report coverage xml + # mypy won't install on PyPy, so only run the typechecking on CPython + !pypy3: mypy contextlib2 deps = coverage + !pypy3: mypy [gh-actions] python = From 4b353b9815f16acbbb64a88e779f3320ba8eab7d Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 22:34:23 +1000 Subject: [PATCH 6/6] Update NEWS file --- NEWS.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index cfbe814..979e833 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -4,8 +4,12 @@ Release History 21.6.0 (2021-06-27) ^^^^^^^^^^^^^^^^^^^ -* Switched to calendar based versioning rather than continuing with pre-1.0 - semantic versioning (`#29 `__) +* License update: due to the inclusion of type hints from the ``typeshed`` + project, the ``contextlib2`` project is now under a combination of the + Python Software License (existing license) and the Apache License 2.0 + (``typeshed`` license) +* Switched to calendar based versioning using a "year"-"month"-"serial" scheme, + rather than continuing with pre-1.0 semantic versioning * Due to the inclusion of asynchronous features from Python 3.7+, the minimum supported Python version is now Python 3.6 (`#29 `__) @@ -20,6 +24,12 @@ Release History * ``AsyncExitStack`` (added in Python 3.7) * async support in ``nullcontext`` (Python 3.10) +* ``contextlib2`` now includes an adapted copy of the ``contextlib`` + type hints from ``typeshed`` (the adaptation removes the Python version + dependencies from the API definition) + (`#33 `__) +* to incorporate the type hints stub file and the ``py.typed`` marker file, + ``contextlib2`` is now installed as a package rather than as a module * Updates to the default compatibility testing matrix: * Added: CPython 3.9, CPython 3.10