英文:
PyTest for @singledispatch and @typechecked not raising expected error
问题
目标: 成功通过test_score_not_implemented_error()
中的NotImplementedError
测试用例。
@singledispatch
中def score()
的目的是在提供给count_neg
和count_pos
的参数不匹配Tuple[int, int]
或Tuple[List[int], List[int]]
时引发NotImplementedError
异常。
我想要通过test_score_not_implemented_error()
来测试异常处理。
然而,出乎意料的是,在其他多态函数上实现了@typechecked
后,我遇到了错误。
我对需要多态函数的方法有信心,我的测试函数具有适当的测试用例。我怀疑问题出在我如何实现def score()
的多态函数上。
调整:
从多态函数中删除@typechecked
会引发以下错误:
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
跟踪:
(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:
<details>
<summary>英文:</summary>
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 ============================================================================================================
</details>
# 答案1
**得分**: 1
以下是翻译好的部分:
问题在于您期望所有测试都引发`NotImplementedError`,但在几种情况下,您传递了不正确类型的参数,因此实际上引发了`TypeCheckError`。让我们来看看您的测试用例。
- `('0', 0)` -- 通过
这引发了预期的`NotImplementedError`,因为除了第一个参数是字符串的情况下,您的所有`score`方法都不接受字符串。
- `(0, '0')` -- 失败
这会引发`TypeCheckError`,因为`score_int`需要`(int, int)`,但您传递了`(int, str)`。
- ('0', '0') -- 通过
这引发了预期的`NotImplementedError`,因为除了第一个参数是字符串的情况下,您的所有`score`方法都不接受字符串。
- `(['0'], [0])` - 失败
这会引发`TypeCheckError`,因为`score_list`需要`list[int]`,但您传递了`list[str]`。
- `([0], ['0'])` -- 失败
这与前一个测试失败的原因相同,只是问题出现在`count_pos`参数而不是`count_neg`参数。
- `(['0'], ['0'])` -- 失败
这与前两个测试失败的原因相同。
- `(None, None)])` -- 通过
这通过了,因为没有`score`方法接受None作为参数类型。
为了使所有测试用例都通过,您需要对`error`进行参数化。也许可以像这样:
```python
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)
请注意,我已经做了以下更改:
-
我使用了pytest内置的
parametrize
装饰器,而不是pytest_cases
的装饰器。 -
我使用了更现代的类型注释。
-
我修改了初始的
score
定义,以返回Any
而不是None
;这可以防止与其他方法的类型错误。 -
我从您的测试中删除了
assert
,因为这是由with pytest.raises()
逻辑隐式执行的(如果引发的异常与预期的异常不同,测试将失败)。
英文:
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)
-- PASSESThis raises a
NotImplementedError
as expected because none of yourscore
methods except a string in the first parameter. -
(0, '0')
-- FAILSThis fails with a
TypeCheckError
becausescore_int
requires(int, int)
, but you're passing(int, str)
. -
('0', '0')
-- PASSESThis raises a
NotImplementedError
as expected because none of yourscore
methods except a string in the first parameter. -
(['0'], [0])
- FAILSThis raises a
TypeCheckError
becausescore_list
requireslist[int]
but you've passed it alist[str]
. -
([0], ['0'])
-- FAILSThis fails because for the same reason as the previous test, except the problem is with the
count_pos
parameter instead ofcount_neg
. -
(['0'], ['0'])
-- FAILSThis fails for the same reason as the previous two tests.
-
(None, None)])
-- PASSESThis 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 frompytest_cases
. -
I'm use more modern type annotations.
-
I've modified the initial
score
definition to returnAny
rather thanNone
; this prevents typing errors with the other methods. -
I've removed the
assert
from your test because that's performed implicitly by thewith pytest.raises()
logic (which will fail the test if it raises an exception other than the expected one).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论