From c5de6cd12c9077f66bcb6ecea7daa82e64b8dbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Piku=C5=82a?= Date: Mon, 17 Apr 2023 22:25:23 +0000 Subject: [PATCH 1/8] Make Message.from_dict() a class method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek PikuĊ‚a --- src/betterproto/__init__.py | 145 +++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 44 deletions(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 40be27fa1..7cbc0fb8b 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -23,6 +23,7 @@ Callable, Dict, Generator, + Generic, Iterable, List, Mapping, @@ -30,6 +31,7 @@ Set, Tuple, Type, + TypeVar, Union, get_type_hints, ) @@ -601,6 +603,43 @@ def _get_cls_by_field( return field_cls +HybridT = TypeVar("HybridT") + + +class hybridmethod(Generic[HybridT]): + """Hybrid method decorator. + + Can be used to have both classmethod and instance method with the same name and + execute the right one depending on context. + + Source: https://stackoverflow.com/a/28238047 + """ + + def __init__( + self, + fclass: Type[HybridT], + finstance: Optional[HybridT] = None, + doc: Optional[str] = None, + ): + self.fclass = fclass + self.finstance = finstance + self.__doc__ = doc or fclass.__doc__ + # support use on abstract base classes + self.__isabstractmethod__ = bool(getattr(fclass, "__isabstractmethod__", False)) + + def classmethod(self, fclass: Type[HybridT]): + return type(self)(fclass, self.finstance, None) + + def instancemethod(self, finstance: HybridT): + return type(self)(self.fclass, finstance, self.__doc__) + + def __get__(self, instance: Optional[T], cls: Type[T]): + if instance is None or self.finstance is None: + # either bound to the class, or no instance method available + return self.fclass.__get__(cls, None) + return self.finstance.__get__(instance, cls) + + class Message(ABC): """ The base class for protobuf messages, all generated messages will inherit from @@ -1178,61 +1217,40 @@ def to_dict( output[cased_name] = value return output - def from_dict(self: T, value: Mapping[str, Any]) -> T: - """ - Parse the key/value pairs into the current message instance. This returns the - instance itself and is therefore assignable and chainable. - - Parameters - ----------- - value: Dict[:class:`str`, Any] - The dictionary to parse from. - - Returns - -------- - :class:`Message` - The initialized message. - """ - self._serialized_on_wire = True + @classmethod + def _from_dict_init(cls: Type[T], value: Mapping[str, Any]) -> Mapping[str, Any]: + metadata = ProtoClassMetadata(cls) + init_dict: Dict[str, Any] = {} for key in value: field_name = safe_snake_case(key) - meta = self._betterproto.meta_by_field_name.get(field_name) + meta = metadata.meta_by_field_name.get(field_name) if not meta: continue if value[key] is not None: if meta.proto_type == TYPE_MESSAGE: - v = getattr(self, field_name) - cls = self._betterproto.cls_by_field[field_name] - if isinstance(v, list): - if cls == datetime: + sub_cls = metadata.cls_by_field[field_name] + if isinstance(value[key], list): + if sub_cls == datetime: v = [isoparse(item) for item in value[key]] - elif cls == timedelta: + elif sub_cls == timedelta: v = [ timedelta(seconds=float(item[:-1])) for item in value[key] ] else: - v = [cls().from_dict(item) for item in value[key]] - elif cls == datetime: + v = [sub_cls.from_dict(item) for item in value[key]] + elif sub_cls == datetime: v = isoparse(value[key]) - setattr(self, field_name, v) - elif cls == timedelta: + elif sub_cls == timedelta: v = timedelta(seconds=float(value[key][:-1])) - setattr(self, field_name, v) elif meta.wraps: - setattr(self, field_name, value[key]) - elif v is None: - setattr(self, field_name, cls().from_dict(value[key])) + v = value[key] else: - # NOTE: `from_dict` mutates the underlying message, so no - # assignment here is necessary. - v.from_dict(value[key]) + v = sub_cls.from_dict(value[key]) elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: - v = getattr(self, field_name) - cls = self._betterproto.cls_by_field[f"{field_name}.value"] - for k in value[key]: - v[k] = cls().from_dict(value[key][k]) + sub_cls = metadata.cls_by_field[f"{field_name}.value"] + v = {k: sub_cls.from_dict(value[key][k]) for k in value[key]} else: v = value[key] if meta.proto_type in INT_64_TYPES: @@ -1246,11 +1264,11 @@ def from_dict(self: T, value: Mapping[str, Any]) -> T: else: v = b64decode(value[key]) elif meta.proto_type == TYPE_ENUM: - enum_cls = self._betterproto.cls_by_field[field_name] - if isinstance(v, list): - v = [enum_cls.from_string(e) for e in v] - elif isinstance(v, str): - v = enum_cls.from_string(v) + enum_cls = metadata.cls_by_field[field_name] + if isinstance(value[key], list): + v = [enum_cls.from_string(e) for e in value[key]] + elif isinstance(value[key], str): + v = enum_cls.from_string(value[key]) elif meta.proto_type in (TYPE_FLOAT, TYPE_DOUBLE): if isinstance(value[key], list): v = [_parse_float(n) for n in value[key]] @@ -1258,7 +1276,47 @@ def from_dict(self: T, value: Mapping[str, Any]) -> T: v = _parse_float(value[key]) if v is not None: - setattr(self, field_name, v) + init_dict[field_name] = v + return init_dict + + @hybridmethod + def from_dict(cls: Type[T], value: Mapping[str, Any]) -> T: + """ + Parse the key/value pairs into the a new message instance. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + ret = cls(**cls._from_dict_init(value)) + ret._serialized_on_wire = True + return ret + + @from_dict.instancemethod + def from_dict(self, value: Mapping[str, Any]): + """ + Parse the key/value pairs into the current message instance. This returns the + instance itself and is therefore assignable and chainable. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + self._serialized_on_wire = True + for field, value in self._from_dict_init(value).items(): + setattr(self, field, value) return self def to_json( @@ -1479,7 +1537,6 @@ def _validate_field_groups(cls, values): field_name_to_meta = cls._betterproto_meta.meta_by_field_name # type: ignore for group, field_set in group_to_one_ofs.items(): - if len(field_set) == 1: (field,) = field_set field_name = field.name From e00529e4068823b32c9f8a2762f0d6038e5f22f5 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 14:34:19 +0100 Subject: [PATCH 2/8] Sync 1/2 of review comments --- src/betterproto/__init__.py | 171 ++++++++++++++---------------------- 1 file changed, 67 insertions(+), 104 deletions(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 7cbc0fb8b..a25eddec9 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -5,6 +5,7 @@ import struct import sys import typing +from typing_extensions import Self import warnings from abc import ABC from base64 import ( @@ -46,7 +47,7 @@ snake_case, ) from .grpc.grpclib_client import ServiceStub - +from .utils import classproperty, hybridmethod # Proto 3 data types TYPE_ENUM = "enum" @@ -603,42 +604,6 @@ def _get_cls_by_field( return field_cls -HybridT = TypeVar("HybridT") - - -class hybridmethod(Generic[HybridT]): - """Hybrid method decorator. - - Can be used to have both classmethod and instance method with the same name and - execute the right one depending on context. - - Source: https://stackoverflow.com/a/28238047 - """ - - def __init__( - self, - fclass: Type[HybridT], - finstance: Optional[HybridT] = None, - doc: Optional[str] = None, - ): - self.fclass = fclass - self.finstance = finstance - self.__doc__ = doc or fclass.__doc__ - # support use on abstract base classes - self.__isabstractmethod__ = bool(getattr(fclass, "__isabstractmethod__", False)) - - def classmethod(self, fclass: Type[HybridT]): - return type(self)(fclass, self.finstance, None) - - def instancemethod(self, finstance: HybridT): - return type(self)(self.fclass, finstance, self.__doc__) - - def __get__(self, instance: Optional[T], cls: Type[T]): - if instance is None or self.finstance is None: - # either bound to the class, or no instance method available - return self.fclass.__get__(cls, None) - return self.finstance.__get__(instance, cls) - class Message(ABC): """ @@ -774,18 +739,19 @@ def __deepcopy__(self: T, _: Any = {}) -> T: kwargs[name] = deepcopy(value) return self.__class__(**kwargs) # type: ignore - @property - def _betterproto(self) -> ProtoClassMetadata: + @classproperty + def _betterproto(cls) -> ProtoClassMetadata: """ Lazy initialize metadata for each protobuf class. It may be initialized multiple times in a multi-threaded environment, but that won't affect the correctness. """ - meta = getattr(self.__class__, "_betterproto_meta", None) - if not meta: - meta = ProtoClassMetadata(self.__class__) - self.__class__._betterproto_meta = meta # type: ignore - return meta + try: + return cls._betterproto_meta + except AttributeError: + cls._betterproto_meta = meta = ProtoClassMetadata(cls) + return meta + def __bytes__(self) -> bytes: """ @@ -1218,69 +1184,66 @@ def to_dict( return output @classmethod - def _from_dict_init(cls: Type[T], value: Mapping[str, Any]) -> Mapping[str, Any]: - metadata = ProtoClassMetadata(cls) + def _from_dict_init(cls, mapping: Mapping[str, Any]) -> Mapping[str, Any]: init_dict: Dict[str, Any] = {} - for key in value: + for key, value in mapping.items(): field_name = safe_snake_case(key) - meta = metadata.meta_by_field_name.get(field_name) - if not meta: + try: + meta = cls._betterproto.meta_by_field_name[field_name] + except KeyError: continue - if value[key] is not None: - if meta.proto_type == TYPE_MESSAGE: - sub_cls = metadata.cls_by_field[field_name] - if isinstance(value[key], list): - if sub_cls == datetime: - v = [isoparse(item) for item in value[key]] - elif sub_cls == timedelta: - v = [ - timedelta(seconds=float(item[:-1])) - for item in value[key] - ] - else: - v = [sub_cls.from_dict(item) for item in value[key]] - elif sub_cls == datetime: - v = isoparse(value[key]) - elif sub_cls == timedelta: - v = timedelta(seconds=float(value[key][:-1])) - elif meta.wraps: - v = value[key] + if value is None: + continue + if meta.proto_type == TYPE_MESSAGE: + sub_cls = cls._betterproto.cls_by_field[field_name] + if isinstance(value, list): + if sub_cls is datetime: + value = [isoparse(item) for item in value] + elif sub_cls is timedelta: + value = [ + timedelta(seconds=float(item[:-1])) + for item in value + ] else: - v = sub_cls.from_dict(value[key]) - elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: - sub_cls = metadata.cls_by_field[f"{field_name}.value"] - v = {k: sub_cls.from_dict(value[key][k]) for k in value[key]} - else: - v = value[key] - if meta.proto_type in INT_64_TYPES: - if isinstance(value[key], list): - v = [int(n) for n in value[key]] - else: - v = int(value[key]) - elif meta.proto_type == TYPE_BYTES: - if isinstance(value[key], list): - v = [b64decode(n) for n in value[key]] - else: - v = b64decode(value[key]) - elif meta.proto_type == TYPE_ENUM: - enum_cls = metadata.cls_by_field[field_name] - if isinstance(value[key], list): - v = [enum_cls.from_string(e) for e in value[key]] - elif isinstance(value[key], str): - v = enum_cls.from_string(value[key]) - elif meta.proto_type in (TYPE_FLOAT, TYPE_DOUBLE): - if isinstance(value[key], list): - v = [_parse_float(n) for n in value[key]] - else: - v = _parse_float(value[key]) + value = [sub_cls.from_dict(item) for item in value] + elif sub_cls == datetime: + value = isoparse(value) + elif sub_cls == timedelta: + value = timedelta(seconds=float(value[:-1])) + elif not meta.wraps: + value = sub_cls.from_dict(value) + elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: + sub_cls = cls._betterproto.cls_by_field[f"{field_name}.value"] + value = {k: sub_cls.from_dict(v) for k, v in value.items()} + else: + if meta.proto_type in INT_64_TYPES: + if isinstance(value, list): + value = [int(n) for n in value] + else: + value = int(value) + elif meta.proto_type == TYPE_BYTES: + if isinstance(value, list): + value = [b64decode(n) for n in value] + else: + value = b64decode(value) + elif meta.proto_type == TYPE_ENUM: + enum_cls = metadata.cls_by_field[field_name] + if isinstance(value, list): + value = [enum_cls.from_string(e) for e in value] + elif isinstance(value, str): + value = enum_cls.from_string(value) + elif meta.proto_type in (TYPE_FLOAT, TYPE_DOUBLE): + if isinstance(value, list): + value = [_parse_float(n) for n in value] + else: + value = _parse_float(value) - if v is not None: - init_dict[field_name] = v + init_dict[field_name] = value return init_dict @hybridmethod - def from_dict(cls: Type[T], value: Mapping[str, Any]) -> T: + def from_dict(cls, value: Mapping[str, Any]) -> Self: """ Parse the key/value pairs into the a new message instance. @@ -1294,12 +1257,12 @@ def from_dict(cls: Type[T], value: Mapping[str, Any]) -> T: :class:`Message` The initialized message. """ - ret = cls(**cls._from_dict_init(value)) - ret._serialized_on_wire = True - return ret + self = cls(**cls._from_dict_init(value)) + self._serialized_on_wire = True + return self @from_dict.instancemethod - def from_dict(self, value: Mapping[str, Any]): + def from_dict(self, value: Mapping[str, Any]) -> Self: """ Parse the key/value pairs into the current message instance. This returns the instance itself and is therefore assignable and chainable. @@ -1533,8 +1496,8 @@ def is_set(self, name: str) -> bool: @classmethod def _validate_field_groups(cls, values): - group_to_one_ofs = cls._betterproto_meta.oneof_field_by_group # type: ignore - field_name_to_meta = cls._betterproto_meta.meta_by_field_name # type: ignore + group_to_one_ofs = cls._betterproto.oneof_field_by_group + field_name_to_meta = cls._betterproto.meta_by_field_name for group, field_set in group_to_one_ofs.items(): if len(field_set) == 1: From 5e8c5d8722cec1dc40a7ce9a72a2a506035ace51 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 14:34:58 +0100 Subject: [PATCH 3/8] Sync other half --- src/betterproto/utils.py | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/betterproto/utils.py diff --git a/src/betterproto/utils.py b/src/betterproto/utils.py new file mode 100644 index 000000000..a5b4226f5 --- /dev/null +++ b/src/betterproto/utils.py @@ -0,0 +1,44 @@ +from typing_extensions import ParamSpec, Concatenate, Self +from typing import ( + Any, + Callable, + Generic, + Optional, + Type, + TypeVar, +) + + +SelfT = TypeVar("SelfT") +P = ParamSpec("P") +HybridT = TypeVar("HybridT", covariant=True) + + +class hybridmethod(Generic[SelfT, P, HybridT]): + def __init__( + self, + func: Callable[Concatenate[type[SelfT], P], HybridT] # Must be the classmethod version + ): + self.cls_func = func + self.__doc__ = func.__doc__ + + def instancemethod(self, func: Callable[Concatenate[SelfT, P], HybridT]) -> Self: + self.instance_func = func + return self + + def __get__(self, instance: Optional[SelfT], owner: Type[SelfT]) -> Callable[P, HybridT]: + if instance is None or self.instance_func is None: + # either bound to the class, or no instance method available + return self.cls_func.__get__(owner, None) + return self.instance_func.__get__(instance, owner) + +T_co = TypeVar("T_co") +TT_co = TypeVar("TT_co", bound="type[Any]") + + +class classproperty(Generic[TT_co, T_co]): + def __init__(self, func: Callable[[TT_co], T_co]): + self.__func__ = func + + def __get__(self, instance: Any, type: TT_co) -> T_co: + return self.__func__(type) From d3139b3d1dfbcfe2d01b27fef0a3f0ece4f1ecb0 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 14:46:24 +0100 Subject: [PATCH 4/8] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e3f65a83..aeecac66b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ ci: - autofix_prs: false + autofix_prs: true repos: - repo: https://github.com/pycqa/isort From 6a2efda6ba5427fd0f9574cd27928e6d7e9a4511 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 14:48:34 +0100 Subject: [PATCH 5/8] Update __init__.py --- src/betterproto/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 39365af5c..890697c71 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import dataclasses import enum as builtin_enum import json From 76fd31b6e5c076dc1a70c4c7250345d16021d33e Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 14:48:54 +0100 Subject: [PATCH 6/8] Update utils.py --- src/betterproto/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/betterproto/utils.py b/src/betterproto/utils.py index a5b4226f5..4beeb56e2 100644 --- a/src/betterproto/utils.py +++ b/src/betterproto/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing_extensions import ParamSpec, Concatenate, Self from typing import ( Any, From 52a2bd342d0dbb3e81c2e1af46928c57905aebee Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 24 Oct 2023 15:08:13 +0100 Subject: [PATCH 7/8] Update src/betterproto/__init__.py --- src/betterproto/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 890697c71..fdf81832c 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -890,7 +890,7 @@ def __copy__(self: T, _: Any = {}) -> T: return self.__class__(**kwargs) # type: ignore @classproperty - def _betterproto(self) -> ProtoClassMetadata: + def _betterproto(cls) -> ProtoClassMetadata: """ Lazy initialize metadata for each protobuf class. It may be initialized multiple times in a multi-threaded environment, From 90ce6e31f47b9ae9ba2e88784eeefc1258c4f245 Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 24 Oct 2023 16:04:35 +0100 Subject: [PATCH 8/8] Fixes --- src/betterproto/__init__.py | 24 ++++++++++++------------ src/betterproto/utils.py | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index fdf81832c..27e1a37b8 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -7,7 +7,6 @@ import struct import sys import typing -from typing_extensions import Self import warnings from abc import ABC from base64 import ( @@ -25,23 +24,22 @@ from typing import ( TYPE_CHECKING, Any, - BinaryIO, Callable, + ClassVar, Dict, Generator, - Generic, Iterable, Mapping, Optional, Set, Tuple, Type, - TypeVar, Union, get_type_hints, ) from dateutil.parser import isoparse +from typing_extensions import Self from ._types import T from ._version import __version__ @@ -52,7 +50,10 @@ ) from .enum import Enum as Enum from .grpc.grpclib_client import ServiceStub as ServiceStub -from .utils import classproperty, hybridmethod +from .utils import ( + classproperty, + hybridmethod, +) if TYPE_CHECKING: @@ -715,7 +716,6 @@ def _get_cls_by_field( return field_cls - class Message(ABC): """ The base class for protobuf messages, all generated messages will inherit from @@ -736,6 +736,7 @@ class Message(ABC): _serialized_on_wire: bool _unknown_fields: bytes _group_current: Dict[str, str] + _betterproto_meta: ClassVar[ProtoClassMetadata] def __post_init__(self) -> None: # Keep track of whether every field was default @@ -890,7 +891,7 @@ def __copy__(self: T, _: Any = {}) -> T: return self.__class__(**kwargs) # type: ignore @classproperty - def _betterproto(cls) -> ProtoClassMetadata: + def _betterproto(cls: type[Self]) -> ProtoClassMetadata: # type: ignore """ Lazy initialize metadata for each protobuf class. It may be initialized multiple times in a multi-threaded environment, @@ -902,7 +903,6 @@ def _betterproto(cls) -> ProtoClassMetadata: cls._betterproto_meta = meta = ProtoClassMetadata(cls) return meta - def dump(self, stream: "SupportsWrite[bytes]", delimit: bool = False) -> None: """ Dumps the binary encoded Protobuf message to the stream. @@ -1538,10 +1538,7 @@ def _from_dict_init(cls, mapping: Mapping[str, Any]) -> Mapping[str, Any]: if sub_cls is datetime: value = [isoparse(item) for item in value] elif sub_cls is timedelta: - value = [ - timedelta(seconds=float(item[:-1])) - for item in value - ] + value = [timedelta(seconds=float(item[:-1])) for item in value] else: value = [sub_cls.from_dict(item) for item in value] elif sub_cls == datetime: @@ -1861,6 +1858,9 @@ def _validate_field_groups(cls, values): return values +Message.__annotations__ = {} # HACK to avoid typing.get_type_hints breaking :) + + def serialized_on_wire(message: Message) -> bool: """ If this message was or should be serialized on the wire. This can be used to detect diff --git a/src/betterproto/utils.py b/src/betterproto/utils.py index 4beeb56e2..b977fc713 100644 --- a/src/betterproto/utils.py +++ b/src/betterproto/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing_extensions import ParamSpec, Concatenate, Self from typing import ( Any, Callable, @@ -10,6 +9,12 @@ TypeVar, ) +from typing_extensions import ( + Concatenate, + ParamSpec, + Self, +) + SelfT = TypeVar("SelfT") P = ParamSpec("P") @@ -19,7 +24,9 @@ class hybridmethod(Generic[SelfT, P, HybridT]): def __init__( self, - func: Callable[Concatenate[type[SelfT], P], HybridT] # Must be the classmethod version + func: Callable[ + Concatenate[type[SelfT], P], HybridT + ], # Must be the classmethod version ): self.cls_func = func self.__doc__ = func.__doc__ @@ -28,12 +35,15 @@ def instancemethod(self, func: Callable[Concatenate[SelfT, P], HybridT]) -> Self self.instance_func = func return self - def __get__(self, instance: Optional[SelfT], owner: Type[SelfT]) -> Callable[P, HybridT]: + def __get__( + self, instance: Optional[SelfT], owner: Type[SelfT] + ) -> Callable[P, HybridT]: if instance is None or self.instance_func is None: # either bound to the class, or no instance method available return self.cls_func.__get__(owner, None) return self.instance_func.__get__(instance, owner) + T_co = TypeVar("T_co") TT_co = TypeVar("TT_co", bound="type[Any]")