diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be21a4c..6df9b58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,13 +7,17 @@ on: jobs: pyright: + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: 3.x + python-version: ${{ matrix.python-version }} - uses: actions/cache@v3 with: path: ~/.cache/pip @@ -21,7 +25,7 @@ jobs: - name: Install dependencies run: pip install -e . pyright - name: Run pyright - run: pyright --verifytypes exceptiongroup + run: pyright --verifytypes exceptiongroup --verbose test: strategy: diff --git a/pyproject.toml b/pyproject.toml index ce06f3f..d57f1c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,15 +70,17 @@ source = ["exceptiongroup"] relative_files = true [tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:" +exclude_also = [ + "if TYPE_CHECKING:", + "@overload", ] [tool.tox] legacy_tox_ini = """ [tox] envlist = py37, py38, py39, py310, py311, py312, pypy3 +labels = + pyright = py{310,311,312}-pyright skip_missing_interpreters = true minversion = 4.0 @@ -87,7 +89,7 @@ extras = test commands = python -m pytest {posargs} usedevelop = true -[testenv:pyright] +[testenv:{py37-,py38-,py39-,py310-,py311-,py312-,}pyright] deps = pyright commands = pyright --verifytypes exceptiongroup usedevelop = true diff --git a/src/exceptiongroup/_exceptions.py b/src/exceptiongroup/_exceptions.py index 82a129c..2986fb6 100644 --- a/src/exceptiongroup/_exceptions.py +++ b/src/exceptiongroup/_exceptions.py @@ -5,13 +5,13 @@ from inspect import getmro, isclass from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload -if TYPE_CHECKING: - from typing import Self - _BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True) _BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException) _ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True) _ExceptionT = TypeVar("_ExceptionT", bound=Exception) +# using typing.Self would require a typing_extensions dependency on py<3.11 +_ExceptionGroupSelf = TypeVar("_ExceptionGroupSelf", bound="ExceptionGroup") +_BaseExceptionGroupSelf = TypeVar("_BaseExceptionGroupSelf", bound="BaseExceptionGroup") def check_direct_subclass( @@ -46,8 +46,10 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]): """A combination of multiple unrelated exceptions.""" def __new__( - cls, __message: str, __exceptions: Sequence[_BaseExceptionT_co] - ) -> Self: + cls: _BaseExceptionGroupSelf, + __message: str, + __exceptions: Sequence[_BaseExceptionT_co], + ) -> _BaseExceptionGroupSelf: if not isinstance(__message, str): raise TypeError(f"argument 1 must be str, not {type(__message)}") if not isinstance(__exceptions, Sequence): @@ -119,7 +121,8 @@ def subgroup( @overload def subgroup( - self, __condition: Callable[[_BaseExceptionT_co | Self], bool] + self, + __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... @@ -127,7 +130,7 @@ def subgroup( self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co | Self], bool], + | Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], ) -> BaseExceptionGroup[_BaseExceptionT] | None: condition = get_condition_filter(__condition) modified = False @@ -179,7 +182,8 @@ def split( @overload def split( - self, __condition: Callable[[_BaseExceptionT_co | Self], bool] + self, + __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], ) -> tuple[ BaseExceptionGroup[_BaseExceptionT_co] | None, BaseExceptionGroup[_BaseExceptionT_co] | None, @@ -224,14 +228,14 @@ def split( else: nonmatching_exceptions.append(exc) - matching_group: Self | None = None + matching_group: _BaseExceptionGroupSelf | None = None if matching_exceptions: matching_group = self.derive(matching_exceptions) matching_group.__cause__ = self.__cause__ matching_group.__context__ = self.__context__ matching_group.__traceback__ = self.__traceback__ - nonmatching_group: Self | None = None + nonmatching_group: _BaseExceptionGroupSelf | None = None if nonmatching_exceptions: nonmatching_group = self.derive(nonmatching_exceptions) nonmatching_group.__cause__ = self.__cause__ @@ -269,7 +273,9 @@ def __repr__(self) -> str: class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception): - def __new__(cls, __message: str, __exceptions: Sequence[_ExceptionT_co]) -> Self: + def __new__( + cls, __message: str, __exceptions: Sequence[_ExceptionT_co] + ) -> _ExceptionGroupSelf: return super().__new__(cls, __message, __exceptions) if TYPE_CHECKING: @@ -288,7 +294,7 @@ def subgroup( @overload def subgroup( - self, __condition: Callable[[_ExceptionT_co | Self], bool] + self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] ) -> ExceptionGroup[_ExceptionT_co] | None: ... @@ -310,14 +316,14 @@ def split( @overload def split( - self, __condition: Callable[[_ExceptionT_co | Self], bool] + self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] ) -> tuple[ ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None ]: ... def split( - self: Self, + self: _ExceptionGroupSelf, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] | Callable[[_ExceptionT_co], bool], diff --git a/src/exceptiongroup/_suppress.py b/src/exceptiongroup/_suppress.py index 6741563..baed57f 100644 --- a/src/exceptiongroup/_suppress.py +++ b/src/exceptiongroup/_suppress.py @@ -1,20 +1,33 @@ import sys from contextlib import AbstractContextManager +from types import TracebackType +from typing import TYPE_CHECKING, Optional, Type if sys.version_info < (3, 11): from ._exceptions import BaseExceptionGroup +if TYPE_CHECKING: + # requires python 3.9 + BaseClass = AbstractContextManager[None] +else: + BaseClass = AbstractContextManager -class suppress(AbstractContextManager): + +class suppress(BaseClass): """Backport of :class:`contextlib.suppress` from Python 3.12.1.""" - def __init__(self, *exceptions): + def __init__(self, *exceptions: BaseException): self._exceptions = exceptions - def __enter__(self): + def __enter__(self) -> None: pass - def __exit__(self, exctype, excinst, exctb): + def __exit__( + self, + exctype: Optional[Type[BaseException]], + excinst: Optional[BaseException], + exctb: Optional[TracebackType], + ) -> bool: # Unlike isinstance and issubclass, CPython exception handling # currently only looks at the concrete type hierarchy (ignoring # the instance and subclass checking hooks). While Guido considers @@ -25,7 +38,7 @@ def __exit__(self, exctype, excinst, exctb): # # See http://bugs.python.org/issue12029 for more details if exctype is None: - return + return False if issubclass(exctype, self._exceptions): return True