diff --git a/combadge/core/response.py b/combadge/core/response.py index 5345213..8d83adf 100644 --- a/combadge/core/response.py +++ b/combadge/core/response.py @@ -4,10 +4,10 @@ from abc import ABC, abstractmethod from collections.abc import Iterable -from typing import Any, ClassVar, Generic, NoReturn +from typing import Any, ClassVar, Generic, Self from pydantic import BaseModel -from typing_extensions import Self +from typing_extensions import Never from combadge.core.errors import CombadgeError from combadge.core.typevars import ResponseT @@ -25,7 +25,7 @@ class BaseResponse(ABC, BaseModel): """ @abstractmethod - def raise_for_result(self, exception: BaseException | None = None) -> None | NoReturn: + def raise_for_result(self, exception: BaseException | None = None) -> None | Never: """ Raise an exception if the service call has failed. @@ -33,7 +33,7 @@ def raise_for_result(self, exception: BaseException | None = None) -> None | NoR ErrorResponse.Error: an error derived from `ErrorResponse` Returns: - always `None` + always `#!python None` Tip: Calling `raise_for_result()` is always possible `#!python BaseResponse`, `#!python SuccessfulResponse`, and @@ -43,12 +43,12 @@ def raise_for_result(self, exception: BaseException | None = None) -> None | NoR raise NotImplementedError @abstractmethod - def unwrap(self) -> Self | NoReturn: + def unwrap(self) -> Self | Never: """ - Return itself if the call was successful, raises an exception otherwise. + Return a response if the call was successful, raise an exception otherwise. This method allows «unpacking» a response with proper type hinting. - The trick here is that all error responses' `unwrap()` are annotated with `NoReturn`, + The trick here is that all error responses' `unwrap()` are annotated with `Never`, which suggests a type linter, that `unwrap()` may never return an error. Tip: Calling `unwrap()` is always possible @@ -75,7 +75,7 @@ def unwrap(self) -> Self | NoReturn: ErrorResponse.Error: an error derived from `ErrorResponse` Returns: - always returns `Self` + returns `self` by default, may be overridden in user response models """ raise NotImplementedError @@ -91,11 +91,11 @@ def raise_for_result(self, exception: BaseException | None = None) -> None: """ Do nothing. - This call is a no-op since the response is successful. + This call is a no-op since the response is successful by definition. """ def unwrap(self) -> Self: - """Return itself since there's no error.""" + """Return the response since there's no error by definition.""" return self @@ -178,19 +178,27 @@ class DerivedException(*exception_bases): # type: ignore[misc] DerivedException.__doc__ = cls.__doc__ or DerivedException.__doc__ cls.Error = DerivedException - def raise_for_result(self, exception: BaseException | None = None) -> NoReturn: + def raise_for_result(self, exception: BaseException | None = None) -> Never: """ Raise the derived exception. Args: exception: if set, raise the specified exception instead of the derived one. + + Raises: + Self.Error: derived error """ if not exception: raise self.Error(self) raise exception from self.as_exception() - def unwrap(self) -> NoReturn: - """Raise the derived exception.""" + def unwrap(self) -> Never: + """ + Raise the derived exception. + + Raises: + Self.Error: derived error + """ raise self.as_exception() def as_exception(self) -> _BaseDerivedError: diff --git a/tests/core/test_response.py b/tests/core/test_response.py index 28bb49d..de3ecbf 100644 --- a/tests/core/test_response.py +++ b/tests/core/test_response.py @@ -1,6 +1,9 @@ +from typing import TYPE_CHECKING + import pytest +from typing_extensions import assert_type -from combadge.core.response import ErrorResponse, _BaseDerivedError +from combadge.core.response import BaseResponse, ErrorResponse, SuccessfulResponse, _BaseDerivedError def test_error_inheritance() -> None: @@ -54,3 +57,12 @@ class CustomError(ErrorResponse): assert CustomError.Error.__module__ == "tests.core.test_response" assert CustomError.Error.__name__ == "CustomError.Error" assert CustomError.Error.__qualname__ == "test_derived_error_magic_attributes..CustomError.Error" + + +if TYPE_CHECKING: + + def test_unwrap_type() -> None: + class Response(SuccessfulResponse): + pass + + assert_type(BaseResponse.unwrap(Response()), Response)