add result pattern including relevant test cases
This commit is contained in:
parent
a6ffc2ebf4
commit
8fdfbebb75
@ -1,16 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses as dc
|
import dataclasses as dc
|
||||||
import inspect
|
import inspect
|
||||||
import typing as t
|
import typing as t
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from logging import Logger
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
P = t.ParamSpec("P")
|
P = t.ParamSpec("P")
|
||||||
T = t.TypeVar("T")
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
# ** Exceptions for result wrappers
|
# ** Exceptions for result wrappers
|
||||||
class WAccessResultDespiteError(Exception):
|
class WrapperAccessResultDespiteError(Exception):
|
||||||
"""wrapped results exception: raised if result is accessed, even though
|
"""wrapped results exception: raised if result is accessed, even though
|
||||||
there was an error in the underlying procedure"""
|
there was an error in the underlying procedure"""
|
||||||
|
|
||||||
@ -24,6 +29,8 @@ class Status:
|
|||||||
|
|
||||||
|
|
||||||
class StatusHandler:
|
class StatusHandler:
|
||||||
|
__slots__ = ("logger", "_SUCCESS")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
logger: Logger | None = None,
|
logger: Logger | None = None,
|
||||||
@ -120,15 +127,18 @@ STATUS_HANDLER: t.Final[StatusHandler] = StatusHandler()
|
|||||||
|
|
||||||
|
|
||||||
class ResultWrapper(t.Generic[T]):
|
class ResultWrapper(t.Generic[T]):
|
||||||
|
__slots__ = ("_result", "status")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
result: T | NotSet,
|
result: T | NotSet,
|
||||||
exception: Exception | None,
|
exception: Exception | None,
|
||||||
code_on_error: int,
|
code_on_error: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert (isinstance(result, NotSet) and exception is not None) or (
|
if isinstance(result, NotSet) and exception is None:
|
||||||
not isinstance(result, NotSet) and exception is None
|
raise ValueError("[ResultWrapper] Set >NotSet< without exception")
|
||||||
), "set >NotSet< without exception or result with exception"
|
elif not isinstance(result, NotSet) and exception is not None:
|
||||||
|
raise ValueError("[ResultWrapper] Set result with exception")
|
||||||
|
|
||||||
self._result = result
|
self._result = result
|
||||||
status: Status = STATUS_HANDLER.SUCCESS
|
status: Status = STATUS_HANDLER.SUCCESS
|
||||||
@ -139,7 +149,9 @@ class ResultWrapper(t.Generic[T]):
|
|||||||
@property
|
@property
|
||||||
def result(self) -> T:
|
def result(self) -> T:
|
||||||
if isinstance(self._result, NotSet):
|
if isinstance(self._result, NotSet):
|
||||||
raise WAccessResultDespiteError("Can not access result because it is not set")
|
raise WrapperAccessResultDespiteError(
|
||||||
|
"Can not access result because it is not set"
|
||||||
|
)
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
268
tests/test_result_pattern.py
Normal file
268
tests/test_result_pattern.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_basics import result_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def status_hdlr() -> result_pattern.StatusHandler:
|
||||||
|
return result_pattern.StatusHandler()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def dummy_logger() -> logging.Logger:
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
handlers = tuple(logger.handlers)
|
||||||
|
for handler in handlers:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
logger.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_NotSet():
|
||||||
|
not_set = result_pattern.NotSet()
|
||||||
|
# using slots, dynamic attribute generation should not be possible
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
not_set.test = "try to set value" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_properties(status_hdlr, dummy_logger):
|
||||||
|
assert status_hdlr.logger is None
|
||||||
|
assert status_hdlr._SUCCESS is not None
|
||||||
|
assert status_hdlr.SUCCESS is not None
|
||||||
|
assert isinstance(status_hdlr.SUCCESS, result_pattern.Status)
|
||||||
|
assert status_hdlr.SUCCESS.code == 0
|
||||||
|
assert status_hdlr.SUCCESS.description == "SUCCESS"
|
||||||
|
status_hdlr.logger = dummy_logger
|
||||||
|
assert status_hdlr.logger is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_FailCodeZero(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 0
|
||||||
|
with pytest.raises(ValueError, match="must not be zero since this value"):
|
||||||
|
status_hdlr.error_state(code, description, message, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_FailCodeNegative(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = -100
|
||||||
|
with pytest.raises(ValueError, match="must not be smaller than zero"):
|
||||||
|
status_hdlr.error_state(code, description, message, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_SuccessWithoutException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, None)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_SuccessWithException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, exception)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_to_exception_FailWithoutException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, None)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is None
|
||||||
|
with pytest.raises(ValueError, match="state where no exception is defined"):
|
||||||
|
status_hdlr.error_to_exception(err_state)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_to_exception_SuccessWithException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, exception)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is ValueError
|
||||||
|
exc = status_hdlr.error_to_exception(err_state)
|
||||||
|
assert isinstance(exc, ValueError)
|
||||||
|
assert str(exc) == message
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_exception_to_error_Success(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
|
||||||
|
class TestException(Exception):
|
||||||
|
"""test"""
|
||||||
|
|
||||||
|
exc = TestException(message)
|
||||||
|
err_state = status_hdlr.exception_to_error(exc, code)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is TestException
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_raise_for_status_Success(dummy_logger):
|
||||||
|
status_hdlr = result_pattern.StatusHandler(dummy_logger)
|
||||||
|
state = status_hdlr.SUCCESS
|
||||||
|
|
||||||
|
assert status_hdlr.raise_for_status(state) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_raise_for_status_RaiseException(dummy_logger):
|
||||||
|
status_hdlr = result_pattern.StatusHandler(dummy_logger)
|
||||||
|
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = result_pattern.Status(
|
||||||
|
code=code, description=description, message=message, ExceptionType=type(exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(type(exception), match=message):
|
||||||
|
status_hdlr.raise_for_status(err_state)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailInitNotSetNoException():
|
||||||
|
error_code = 146
|
||||||
|
test_result = result_pattern.NotSet()
|
||||||
|
exception = None
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Set >NotSet< without exception"):
|
||||||
|
_: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailInitSetResultWithException():
|
||||||
|
error_code = 146
|
||||||
|
test_result = 10
|
||||||
|
message = "Test of error message"
|
||||||
|
exception = ValueError(message)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Set result with exception"):
|
||||||
|
_: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_Success(status_hdlr):
|
||||||
|
# successful operation
|
||||||
|
test_result = 10
|
||||||
|
error_code = 146
|
||||||
|
twrapper: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=None,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status == status_hdlr.SUCCESS
|
||||||
|
assert twrapper.result == test_result
|
||||||
|
assert twrapper.unwrap() == test_result
|
||||||
|
assert twrapper.status.code != error_code
|
||||||
|
assert twrapper.status.code == status_hdlr.SUCCESS.code
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailureAccess(status_hdlr):
|
||||||
|
# test for no result
|
||||||
|
error_code = 146
|
||||||
|
test_result = result_pattern.NotSet()
|
||||||
|
message = "Test of error message"
|
||||||
|
exception = ValueError(message)
|
||||||
|
twrapper: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status != status_hdlr.SUCCESS
|
||||||
|
assert twrapper.status.code == error_code
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
twrapper.result
|
||||||
|
with pytest.raises(type(exception), match=message):
|
||||||
|
_ = twrapper.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_ExceptionRaised():
|
||||||
|
MESSAGE = "Test case wrapped function decorator"
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_1() -> None:
|
||||||
|
raise ValueError(MESSAGE)
|
||||||
|
|
||||||
|
res = test_func_1()
|
||||||
|
assert isinstance(res, result_pattern.ResultWrapper)
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.message == MESSAGE
|
||||||
|
assert res.status.ExceptionType is ValueError
|
||||||
|
with pytest.raises(ValueError, match=MESSAGE):
|
||||||
|
res.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_Success(status_hdlr):
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "1")
|
||||||
|
assert res.result == 2
|
||||||
|
assert res.unwrap() == 2
|
||||||
|
assert res.status == status_hdlr.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_FailureZeroDivisionError():
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "0")
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
res.result
|
||||||
|
with pytest.raises(ZeroDivisionError):
|
||||||
|
res.unwrap()
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.ExceptionType is ZeroDivisionError
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_FailureValueErrorWithLogger(dummy_logger):
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code, logger=dummy_logger)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "test")
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
res.result
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
res.unwrap()
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.ExceptionType is ValueError
|
||||||
Loading…
x
Reference in New Issue
Block a user