简体   繁体   English

类型提示返回子类的 class 装饰器

[英]Type hinting a class decorator that returns a subclass

I have a set of unrelated classes (some imported) which all have a common attribute (or property) a of type dict[str, Any] .我有一组不相关的类(一些是导入的),它们都有a dict[str, Any]类型的公共属性(或属性)。

Within a there should be another dict under the key "b" , which I would like to expose on any of these classes as an attribute b to simplify inst.a.get("b", {})[some_key] to inst.b[some_key] .a中,键"b"下应该有另一个字典,我想在这些类中的任何一个上公开它作为属性b以简化inst.a.get("b", {})[some_key]inst.b[some_key]

I have made the following subclass factory to work as a class decorator for local classes and a function for imported classes.我已将以下子类工厂用作本地类的 class 装饰器和导入类的 function 装饰器。

But so far I'm failing to type hint its cls argument and return value correctly.但到目前为止,我未能正确输入提示其cls参数和返回值。

from functools import wraps

def access_b(cls):
    @wraps(cls, updated=())
    class Wrapper(cls):
        @property
        def b(self) -> dict[str, bool]:
            return self.a.get("b", {})
    return Wrapper

MRE of my latest typing attemp (with mypy 0.971 errors):我最新的打字尝试的 MRE(带有mypy 0.971错误):

from functools import wraps
from typing import Any, Protocol, TypeVar

class AProtocol(Protocol):
    a: dict[str, Any]

class BProtocol(AProtocol, Protocol):
    b: dict[str, bool]

T_a = TypeVar("T_a", bound=AProtocol)
T_b = TypeVar("T_b", bound=BProtocol)

def access_b(cls: type[T_a]) -> type[T_b]:
    @wraps(cls, updated=())
    class Wrapper(cls):  # Variable "cls" is not valid as a type & Invalid base class "cls"
        @property
        def b(self) -> dict[str, bool]:
            return self.a.get("b", {})
    return Wrapper

@access_b
class Demo1:
    """Local class."""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()

demo1 = Demo1({"b": {"allow_X": True}})
demo1.b["allow_X"]  # "Demo1" has no attribute "b"

class Demo2:
    """Consider me an imported class."""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()

demo2 = access_b(Demo2)({"b": {"allow_X": True}})  # Cannot instantiate type "Type[<nothing>]"
demo2.b["allow_X"]
  1. I do not understand why cls is not valid as a type, even after reading https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases .我不明白为什么cls作为一种类型无效,即使在阅读https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases之后也是如此。

  2. I understand I should probably not return a Protocol (I suspect that is the source of Type[<nothing>] ), but I don't see how I could specify "returns the original type with an extension".我知道我可能不应该返回协议(我怀疑这是Type[<nothing>]的来源),但我不知道如何指定“返回带有扩展名的原始类型”。

PS1. PS1。 I have also tried with a decorator which adds b dynamically, still failed to type it...我也尝试过使用动态添加 b 的装饰器,但仍然无法输入...

PS2. PS2。 ...and with a decorator which uses a mixin as per @DaniilFajnberg's answer, still failing. ...并且根据@DaniilFajnberg的回答使用了一个使用mixin的装饰器,仍然失败。


References:参考:

  1. functools.wraps(cls, update=()) from https://stackoverflow.com/a/65470430/17676984 functools.wraps(cls, update=())来自https://stackoverflow.com/a/65470430/17676984

(Type) Variables as base classes? (类型)变量作为基类?

This is actually a really interesting question and I am curious about what solutions other people come up with.这实际上是一个非常有趣的问题,我很好奇其他人提出了什么解决方案。

I read up a little on these two errors:我对这两个错误进行了一些阅读:

Variable "cls" is not valid as a type / Invalid base class "cls"变量“cls”作为类型无效/无效基 class“cls”

There seems to be an issue here with mypy that has been open for a long time now.这里似乎有一个问题mypy已经打开了很长时间。 There doesn't seem to be a workaround yet.似乎还没有解决方法。

The problem, as I understand it, is that no matter how you annotate it, the function argument cls will always be a type variable and that is considered invalid as a base class.据我了解,问题在于无论您如何注释它,function 参数cls将始终是一个类型变量,并且作为基础 class 被认为是无效的。 The reasoning is apparently that there is no way to make sure that the value of that variable isn't overwritten somewhere.原因显然是没有办法确保该变量的值不会在某处被覆盖。

I honestly don't understand the intricacies well enough, but it is really strange to me that mypy seems to treat a class A defined via class A: ... different than a variable of Type[A] since the former should essentially just be syntactic sugar for this:老实说,我不太了解其中的复杂性,但我真的很奇怪mypy似乎处理了通过 class A定义的class A: ...不同于Type[A]的变量,因为前者本质上应该只是语法糖:

A = type('A', (object,), {})

There was also a related discussion in the mypy issue tracker. mypy问题跟踪器中也有相关讨论 Again, hoping someone can shine some light onto this.再次,希望有人可以对此有所启发。


Adding a convenience property添加便利属性

In any case, from your example I gather that you are not dealing with foreign classes, but that you define them yourself.无论如何,从您的示例中,我了解到您不是在处理外国类,而是您自己定义它们。 If that is the case, a Mix-in would be the simplest solution:如果是这种情况,Mix-in 将是最简单的解决方案:

from typing import Any, Protocol


class AProtocol(Protocol):
    a: dict[str, Any]


class MixinAccessB:
    @property
    def b(self: AProtocol) -> dict[str, bool]:
        return self.a.get("b", {})


class SomeBase:
    ...


class OwnClass(MixinAccessB, SomeBase):
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()


demo1 = OwnClass({"b": {"allow_X": True}})
print(demo1.b["allow_X"])

Output: True Output: True

No mypy issues in --strict mode. --strict模式下没有mypy问题。


Mixin with a foreign class Mixin用国外class

If you are dealing with foreign classes, you could still use the Mix-in and then use functools.update_wrapper like this:如果您正在处理外部类,您仍然可以使用 Mix-in,然后像这样使用functools.update_wrapper

from functools import update_wrapper
from typing import Any, Protocol


class AProtocol(Protocol):
    a: dict[str, Any]


class MixinAccessB:
    """My mixin"""
    @property
    def b(self: AProtocol) -> dict[str, bool]:
        return self.a.get("b", {})


class Foreign:
    """Foreign class documentation"""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()


class MixedForeign(MixinAccessB, Foreign):
    """foo"""
    pass


update_wrapper(MixedForeign, Foreign, updated=())


demo2 = MixedForeign({"b": {"allow_X": True}})
print(demo2.b["allow_X"])
print(f'{MixedForeign.__name__=} {MixedForeign.__doc__=}')

Output: Output:

True
MixedForeign.__name__='Foreign' MixedForeign.__doc__='Foreign class documentation'

Also no mypy issues in --strict mode.--strict模式下也没有mypy问题。

Note that you still need the AProtocol to make it clear that whatever self will be in that property follows that protocol, ie has an attribute a with the type dict[str, Any] .请注意,您仍然需要AProtocol来明确该属性中的任何self都遵循该协议,即具有类型为dict[str, Any]的属性a


I hope I understood your requirements correctly and this at least provides a solution for your particular situation, even though I could not enlighten you on the type variable issue.我希望我正确理解了您的要求,这至少为您的特定情况提供了解决方案,即使我无法就类型变量问题向您提供启发。

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

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