简体   繁体   English

Python 断言类型注释的单元测试 Object

[英]Python Unit Test to Assert Type Annotation of Object

In Python versions <3.11 where the assert_type ( source ) isn't available, how does one assert a type annotation via the unittest TestCase class?在 Python 版本 <3.11 中, assert_type ( source ) 不可用,如何通过单元测试TestCase unittest断言类型注释? Example of the problem:问题示例:

from typing import List
from unittest import TestCase


def dummy() -> List[str]:
    return ["one", "two", "three"]


class TestDummy(TestCase):

    def test_dummy(self):
        self.assertEqual(
            List[str],
            type(dummy())
        )

The test fails with the following output:测试失败并显示以下 output:

<class 'list'> != typing.List[str]

Expected :typing.List[str]
Actual   :<class 'list'>
<Click to see difference>

Traceback (most recent call last):
  File "C:\Users\z\dev\mercata\scratch\mock_testing.py", line 12, in test_dummy
    self.assertEqual(
AssertionError: typing.List[str] != <class 'list'>

The approach I currently use is as follows:我目前使用的方法如下:

data = dummy()
self.assertTrue(
    type(data) == list
)
self.assertTrue(all([
    type(d) == str for d in data
]))

This works but requires iterating the entirety of the object which is unwieldy with larger datasets.这可行,但需要迭代整个object ,这对于较大的数据集来说很笨重。 Is there a more efficient approach for Python versions <3.11 (not requiring a third-party package)?对于 Python 版本 <3.11(不需要第三方包)是否有更有效的方法?

assert_type is used to ask a static type checker to confirm a value is of some type. assert_type用于询问 static 类型检查器以确认值是某种类型。 At normal runtime this method doesn't do anything.在正常运行时,此方法不执行任何操作。 If you want to use it, then you should use static analysis tooling, for example mypy or pyright.如果你想使用它,那么你应该使用 static 分析工具,例如 mypy 或 pyright。 Checking assertEqual is a runtime operation, and unlike some languages, instances of generics in python do not retain their type info at runtime, which is why the class is being shown as the standard <class 'list'> and not the generic one from the method type annotation.检查assertEqual是一个运行时操作,与某些语言不同,python 中的 generics 实例在运行时不保留其类型信息,这就是为什么 class 显示为标准<class 'list'>而不是来自方法类型注解。

Because assert_type doesn't perform anything at runtime, it will not check for the contents of the actual list.因为assert_type在运行时不执行任何操作,所以它不会检查实际列表的内容。 It is used to add an explicit typecheck into the code, and only useful if all of the inputs for how a variable was constructed have been properly type checked as well.它用于在代码中添加显式类型检查,并且仅当所有关于变量构造方式的输入也都经过正确类型检查时才有用。 So it would also not be useful within unit testing as you have it.因此,它在您拥有的单元测试中也没有用。

For example, the following script only produces one error:例如,以下脚本只会产生一个错误:

from typing import assert_type

def dummy() -> list[str]:
    return [1]

res = dummy()
assert_type(res, list[str])
(venv) $ mypy test.py
test.py:4: error: List item 0 has incompatible type "int"; expected "str"  [list-item]
Found 1 error in 1 file (checked 1 source file)

This detects the error of an int list being returned by dummy , but the assert_type succeeds because it would be correct if dummy had respected its contract.这检测到dummy返回的 int 列表的错误,但assert_type成功,因为如果dummy遵守其合同,它将是正确的。

If we fixed dummy like below, then at this point we would get the expected assert_type error:如果我们像下面那样修复虚拟对象,那么此时我们将得到预期的assert_type错误:

from typing import assert_type

def dummy() -> list[int]:
    return [1]

res = dummy()
assert_type(res, list[str])
(venv) $ mypy test.py
test.py:7: error: Expression is of type "List[int]", not "List[str]"  [assert-type]
Found 1 error in 1 file (checked 1 source file)

While I agree with the general sentiment the commenters have expressed that this type of thing should probably be left to static type checkers rather than unit tests, just for academic purposes, you can construct your own assertion without too much effort.虽然我同意评论者表达的普遍观点,即此类事情可能应该留给 static 类型检查器而不是单元测试,但仅出于学术目的,您可以毫不费力地构建自己的断言。

Something like list[str] is a specified version of the generic type list . list[str]之类的东西是通用类型list的指定版本。 By subscripting a type like like list , you are actually calling its __class_getitem__ method, which returns the specified type.通过下标像list这样的类型,您实际上是在调用它的__class_getitem__方法,该方法返回指定的类型。 The type argument is actually stored and the typing module provides the get_args / get_origin functions to extract more detailed type information from generic types at runtime. type 参数实际上被存储并且 typing 模块提供了get_args / get_origin函数以在运行时从泛型类型中提取更详细的类型信息。

from typing import get_args

print(get_args(list[str]))  # (<class 'str'>,)

The problem is more that any concrete list object (like ["one", "two", "three"] ) does not store any information about the type of the items it holds (for obvious reasons).问题更多的是任何具体list object(如["one", "two", "three"] )不存储有关其持有的项目类型的任何信息(出于显而易见的原因)。 This means, at runtime, we would have to check the type of the elements ourselves.这意味着,在运行时,我们必须自己检查元素的类型。

The question thus becomes how pedantic you want your check to be.因此,问题就变成了你希望你的支票有多迂腐。 Lists for example can be as long as you want them to be (or as your memory allows).例如,列表的长度可以是您希望的长度(或者您的 memory 允许的长度)。 If you a list object of a million elements, do you really want to check each and every one of them?如果您列出 object 一百万个元素,您真的要检查每个元素吗? A possible compromise might be to only check the very first element's type or something like that.一个可能的妥协可能是只检查第一个元素的类型或类似的东西。

Here is an example of a function checking arbitrary iterable types that are parameterized by "regular" types only (ie not something like list[tuple[int]] ):这是一个 function 检查任意可迭代类型的示例,这些类型仅由“常规”类型参数化(即不像list[tuple[int]] ):

from collections.abc import Iterable
from types import GenericAlias
from typing import Union, cast, get_origin, get_args


def is_of_iter_type(
    value: object,
    type_: Union[type[Iterable[object]], GenericAlias],
    pedantic: bool = False,
) -> bool:
    if isinstance(type_, type):  # something like unspecified `list`
        return isinstance(value, type_)
    if isinstance(type_, GenericAlias):  # a specified generic like `list[str]`
        origin, args = get_origin(type_), get_args(type_)
        if not isinstance(origin, type) or not issubclass(origin, Iterable):
            raise TypeError
        arg = cast(type, args[0])
        if not isinstance(arg, type):  # type arg is a type var or another generic alias
            raise TypeError
        if not isinstance(value, origin):
            return False
        if pedantic:
            return all(isinstance(item, arg) for item in value)
        else:
            return isinstance(next(iter(value)), arg)
    raise TypeError

Note also that depending on what iterable you actually pass to this function, it may be a terrible idea to (try to) consume the resulting iterator (via next or all ).另请注意,根据您实际传递给此 function 的可迭代对象,(尝试)使用生成的迭代器(通过nextall )可能是一个糟糕的主意。 It would be up to you to ensure that this does not have any bad side effects.由您来确保这不会产生任何不良副作用。

Here is a demo:这是一个演示:

print(is_of_iter_type("a", list[str]))  # False
print(is_of_iter_type(["a"], list[str]))  # True
print(is_of_iter_type(["a"], list))  # True
print(is_of_iter_type(["a", 1], list[str]))  # True
print(is_of_iter_type(["a", 1], list[str], pedantic=True))  # False

To incorporate it into a unittest.TestCase you could do this:要将其合并到unittest.TestCase中,您可以这样做:

...
from unittest import TestCase


class ExtendedTestCase(TestCase):
    def assert_is_of_iter_type(
        self,
        value: object,
        type_: Union[type[Iterable[object]], GenericAlias],
        pedantic: bool = False,
    ) -> None:
        if not is_of_iter_type(value, type_, pedantic=pedantic):
            self.fail(f"{value} is not of type {type_}")

    def test(self) -> None:
        self.assert_is_of_iter_type(["a", 1], list[str], pedantic=True)

But again, this is most likely not a good idea because something like mypy in --strict mode will probably do a better job at ensuring type safety throughout your code than you could hope to do at runtime.但同样,这很可能不是一个好主意,因为在--strict模式下类似mypy的东西在确保整个代码的类型安全方面可能比你希望在运行时做的更好。 Meaning if you declare def dummy() -> list[str]: ... , but the in the function body you return ["a", 1] , then mypy will pick that up and yell at you.意思是如果你声明def dummy() -> list[str]: ... ,但是在 function 正文中你return ["a", 1] ,那么mypy会捡起它并对你大喊大叫。 Thus, there would be no need for such a test.因此,不需要进行这样的测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM