如何修复 pylint E1134:非映射值 X 用于映射上下文(非映射)

How to fix pylint E1134: Non-mapping value X is used in a mapping context (not-a-mapping)

pylint (2.12.2) 正在回归

E1134: Non-mapping value self.f_four is used in a mapping context (not-a-mapping)

对于下面的代码

from dataclasses import dataclass
@dataclass
class One:
    f_one: str
    f_two: str

@dataclass
class Two:
    f_three: str
    f_four: One
    def __post_init__(self):
        """Initialise nested dataclass fields from a dictionary"""
        self.f_four = One(**self.f_four)

data = {'f_three': 'three', 'f_four': {'f_one': 'one', 'f_two': 'two'}}

print(Two(**data)) # prints: Two(f_three='three', f_four=One(f_one='one', f_two='two'))

相对于dataclasses.field the documentation says:

metadata: This can be a mapping or None. None is treated as an empty dict. This value is wrapped in MappingProxyType() to make it read-only, and exposed on the Field object. It is not used at all by Data Classes, and is provided as a third-party extension mechanism. Multiple third-parties can each have their own key, to use as a namespace in the metadata.

我尝试用 f_four: One = field(metadata="Mapping") 更新 Two class。但这会导致 mypy 错误(但解决了 pylint 错误)因此,此 metadata 必须不正确或至少不足以正确修复此问题。 更改行的 Mypy 错误是

error: No overload variant of "field" matches argument type "str"
note: Possible overload variants:
note:     def [_T] field(*, default: _T, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[Any, Any]] = ...) -> _T
note:     def [_T] field(*, default_factory: Callable[[], _T], init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[Any, Any]] = ...) -> _T
note:     def field(*, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[Any, Any]] = ...) -> Any

那么如何正确解决这个pylint错误呢?

在你的代码中我看到两个错误,一个来自 pylint,一个来自 mypy:

pylint 错误

self.f_four = One(**self.f_four)
# Non-mapping value self.f_four is used in a mapping context

pylint 错误发生是因为数据的 specially generated __init__class:

def __init__(f_three: str, f_four: One)

f_four 是一个 class One 实例,它不接受 **.

pylint的一种解决方案是手动定义__init__的签名, 并将 __post_init__ 的正文移动到 __init__:

from typing import Union
@dataclass
class Two:
    f_three: str
    f_four: One

    # __init__ f_four accepts both One and dict
    def __init__(self, f_three: str, f_four: Union[One, dict]):
        self.f_three = f_three
        if isinstance(f_four, dict):
            self.f_four = One(**f_four)
        else:
            self.f_four = f_four

或者,如果您想保留 __post_init__,则允许​​ Two.f_four 为:Union[One, dict]:

from typing import Union
@dataclass
class Two:
    f_three: str
    f_four: Union[One, dict]

    def __post_init__(self):
        "Initialise nested dataclass fields from a dictionary"
        if isinstance(self.f_four, dict):
            self.f_four = One(**self.f_four)

mypy错误

print(Two(**data))
# Argument 1 to "Two" has incompatible type "**Dict[str, Collection[str]]";
#     expected "str"
# Argument 1 to "Two" has incompatible type "**Dict[str, Collection[str]]";
#     expected "One"

存在 mypy 错误,因为 **data 与中的类型提示不匹配 Two.__init__。要修复 mypy 错误,我们需要 TypedDict:

from typing import TypedDict

class TwoDict(TypedDict):
    f_three: str
    f_four: Union[One, dict]

并且我们必须对 mypy 类型提示说 data 具有类型 TwoDict:

data: TwoDict = {'f_three': 'three', 'f_four': {'f_one': 'one', 'f_two': 'two'}}