Search code examples
pythonpytestsingle-dispatch

PyTest for @singledispatch and @typechecked not raising expected error


Goal: Successfully pass test cases, for NotImplementedError in test_score_not_implemented_error().

The purpose of @singledispatch def score() is to raise NotImplementedError, if when the provided arguments for count_neg and count_pos do not match Tuple[int, int] nor Tuple[List[int], List[int]].

I want to test for this exception handling via. test_score_not_implemented_error().

However, unexpectedly, I get an error for having implemented @typechecked on the other polymorphic functions.

I'm confident in my approach to needing a polymorphic function and my test function has the appropriate test cases. I suspect the issue lies in how I've implemented def score()'s polymorphic functions.

Tweak: Removing @typechecked from polymorphic functions throws:

FAILED tests/test_tps.py::test_score_not_implemented_error[0-01] - TypeError: can only concatenate str (not "int") to str
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg3-count_pos3] - TypeError: unsupported operand type(s) for +: 'int' and 'str'
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg4-count_pos4] - TypeError: unsupported operand type(s) for +: 'int' and 'str'
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg5-count_pos5] - TypeError: unsupported operand type(s) for +: 'int' and 'str'

tests/test_score.py:

from typeguard import typechecked
from functools import singledispatch

import pytest
from pytest_cases import parametrize
from typing import Any, List, Tuple, Type, Union


@singledispatch
def score(count_neg: Any, count_pos: Any) -> None:
    raise NotImplementedError(f'{type(count_neg)} and or {type(count_pos)} are not supported.')


@score.register(int)
@typechecked
def score_int(count_neg: int, count_pos: int) -> float:
    return round(100 * count_pos / (count_pos + count_neg), 1)


@score.register(list)
@typechecked
def score_list(count_neg: List[int], count_pos: List[int]) -> float:
    return round(100 * sum(count_pos) / (sum(count_pos) + sum(count_neg)), 1)


@parametrize('count_neg, count_pos',
             [('0', 0),
              (0, '0'),
              ('0', '0'),
              (['0'], [0]),
              ([0], ['0']),
              (['0'], ['0']),
              (None, None)])
def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                       count_pos: Union[str, int, List[str], List[int], None],
                                                       error: Type[BaseException] = NotImplementedError):
    with pytest.raises(error) as exc_info:
        score(count_neg, count_pos)

    assert exc_info.type is error

Traceback:

(venv) me@laptop:~/BitBucket/project $ python -m pytest tests/test_score.py 
===================================================================================================================== test session starts =====================================================================================================================
platform linux -- Python 3.9.16, pytest-7.4.0, pluggy-1.0.0
rootdir: /home/danielbell/BitBucket/pdl1-lung
plugins: hydra-core-1.3.2, typeguard-3.0.2, mock-3.11.1, cases-3.6.13, dvc-3.2.3, anyio-3.5.0
collected 7 items                                                                                                                                                                                                                                             

tests/test_tps.py .F.FFF.                                                                                                                                                                                                                               [100%]

========================================================================================================================== FAILURES ===========================================================================================================================
___________________________________________________________________________________________________________ test_score_not_implemented_error[0-01] ____________________________________________________________________________________________________________

count_neg = 0, count_pos = '0', error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:16: in score_int
    def score_int(count_neg: int, count_pos: int) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f591213ccc0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: argument "count_pos" (str) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg3-count_pos3] ___________________________________________________________________________________________________

count_neg = ['0'], count_pos = [0], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f5854383770>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg4-count_pos4] ___________________________________________________________________________________________________

count_neg = [0], count_pos = ['0'], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f58540d45e0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_pos" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg5-count_pos5] ___________________________________________________________________________________________________

count_neg = ['0'], count_pos = ['0'], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f58541444f0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
====================================================================================================================== warnings summary =======================================================================================================================
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/cytomine/models/collection.py:26
  /home/danielbell/miniconda3/envs/pdl1lung/lib/python3.9/site-packages/cytomine/models/collection.py:26: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working
    from collections import MutableSequence

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=================================================================================================================== short test summary info ===================================================================================================================
FAILED tests/test_tps.py::test_score_not_implemented_error[0-01] - typeguard.TypeCheckError: argument "count_pos" (str) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg3-count_pos3] - typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg4-count_pos4] - typeguard.TypeCheckError: item 0 of argument "count_pos" (list) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg5-count_pos5] - typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int
=========================================================================================================== 4 failed, 3 passed, 1 warning in 0.94s ============================================================================================================

Solution

  • The problem is that you're expecting all of your tests to raise a NotImplementedError, but in several cases you're passing parameters of the incorrect type so instead you're getting a TypeCheckError. Let's take a look at your test cases..

    • ('0', 0) -- PASSES

      This raises a NotImplementedError as expected because none of your score methods except a string in the first parameter.

    • (0, '0') -- FAILS

      This fails with a TypeCheckError because score_int requires (int, int), but you're passing (int, str).

    • ('0', '0') -- PASSES

      This raises a NotImplementedError as expected because none of your score methods except a string in the first parameter.

    • (['0'], [0]) - FAILS

      This raises a TypeCheckError because score_list requires list[int] but you've passed it a list[str].

    • ([0], ['0']) -- FAILS

      This fails because for the same reason as the previous test, except the problem is with the count_pos parameter instead of count_neg.

    • (['0'], ['0']) -- FAILS

      This fails for the same reason as the previous two tests.

    • (None, None)]) -- PASSES

      This passes because no score method accepts None as a parameter type.


    To get this test to pass for all your test cases you're going to need to parametrize error as well. Maybe something like:

    from typeguard import typechecked, TypeCheckError
    from functools import singledispatch
    
    import pytest
    from typing import Any, Type
    
    
    @singledispatch
    def score(count_neg: Any, count_pos: Any) -> Any:
        raise NotImplementedError(
            f"{type(count_neg)} and or {type(count_pos)} are not supported."
        )
    
    
    @score.register(int)
    @typechecked
    def score_int(count_neg: int, count_pos: int) -> float:
        return round(100 * count_pos / (count_pos + count_neg), 1)
    
    
    @score.register(list)
    @typechecked
    def score_list(count_neg: list[int], count_pos: list[int]) -> float:
        return round(100 * sum(count_pos) / (sum(count_pos) + sum(count_neg)), 1)
    
    
    @pytest.mark.parametrize(
        "count_neg, count_pos, error",
        [
            ("0", 0, NotImplementedError),
            (0, "0", TypeCheckError),
            ("0", "0", NotImplementedError),
            (["0"], [0], TypeCheckError),
            ([0], ["0"], TypeCheckError),
            (["0"], ["0"], TypeCheckError),
            (None, None, NotImplementedError),
        ],
    )
    def test_score_not_implemented_error(
        count_neg: str | int | list[str] | list[int] | None,
        count_pos: str | int | list[str] | list[int] | None,
        error: Type[BaseException],
    ):
        with pytest.raises(error):
            score(count_neg, count_pos)
    

    Note that I've made the following changes:

    • I'm using pytest's built-in parametrize decorator, rather than the one from pytest_cases.

    • I'm use more modern type annotations.

    • I've modified the initial score definition to return Any rather than None; this prevents typing errors with the other methods.

    • I've removed the assert from your test because that's performed implicitly by the with pytest.raises() logic (which will fail the test if it raises an exception other than the expected one).