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/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 diff --git a/README.rst b/README.rst index d64ba87..47c21ed 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 ----------- @@ -50,18 +60,32 @@ 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: -* ``Lib/contextlib.py`` -> ``contextlib2.py`` +* ``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__.pyi`` 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 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 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/__init__.pyi b/contextlib2/__init__.pyi new file mode 100644 index 0000000..5fe29b7 --- /dev/null +++ b/contextlib2/__init__.pyi @@ -0,0 +1,125 @@ +# 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 + +# 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 True: + 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 True: + 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 True: + 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 True: + _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 True: + @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/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 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 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 =