From 0a1dd69b3348e91d19b9d549916197258c7ab745 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Tue, 23 Jul 2024 15:00:49 +0100 Subject: [PATCH] Improve type annotation for APIObject constructor (#453) --- kr8s/_objects.py | 17 +++++++++-------- kr8s/_types.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/kr8s/_objects.py b/kr8s/_objects.py index d0962a90..9707a27d 100644 --- a/kr8s/_objects.py +++ b/kr8s/_objects.py @@ -34,6 +34,7 @@ ) from kr8s._exceptions import NotFoundError, ServerError from kr8s._exec import Exec +from kr8s._types import SpecType, SupportsKeysAndGetItem from kr8s.asyncio.portforward import PortForward as AsyncPortForward from kr8s.portforward import PortForward as SyncPortForward @@ -54,22 +55,22 @@ class APIObject: _asyncio: bool = True def __init__( - self, resource: dict, namespace: str | None = None, api: Api | None = None + self, resource: SpecType, namespace: str | None = None, api: Api | None = None ) -> None: """Initialize an APIObject.""" - with contextlib.suppress(TypeError, ValueError): - resource = dict(resource) - if isinstance(resource, str): - self.raw = {"metadata": {"name": resource}} - elif isinstance(resource, dict): + if isinstance(resource, dict): self.raw = resource + elif isinstance(resource, SupportsKeysAndGetItem): + self.raw = dict(resource) + elif isinstance(resource, str): + self.raw = {"metadata": {"name": resource}} elif hasattr(resource, "to_dict"): self.raw = resource.to_dict() - elif hasattr(resource, "obj"): + elif hasattr(resource, "obj") and isinstance(resource.obj, dict): self.raw = resource.obj else: raise ValueError( - "resource must be a dict, string, have an obj attribute or a to_dict method" + "resource must be a dict, string, have a to_dict method or an obj attribute containing a dict" ) if namespace is not None: self.raw["metadata"]["namespace"] = namespace # type: ignore diff --git a/kr8s/_types.py b/kr8s/_types.py index 019d35f7..e6983005 100644 --- a/kr8s/_types.py +++ b/kr8s/_types.py @@ -1,8 +1,18 @@ # SPDX-FileCopyrightText: Copyright (c) 2024, Kr8s Developers (See LICENSE for list) # SPDX-License-Identifier: BSD 3-Clause License from os import PathLike -from typing import TYPE_CHECKING, List, Protocol, Union, runtime_checkable +from typing import ( + TYPE_CHECKING, + Iterable, + List, + Protocol, + TypeVar, + Union, + runtime_checkable, +) +_KT = TypeVar("_KT") +_VT_co = TypeVar("_VT_co", covariant=True) PathType = Union[str, PathLike[str]] if TYPE_CHECKING: @@ -14,3 +24,27 @@ class APIObjectWithPods(Protocol): """An APIObject subclass that contains other Pod objects.""" async def async_ready_pods(self) -> List["Pod"]: ... + + +@runtime_checkable +class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): + """Copied from _typeshed.SupportsKeysAndGetItem to avoid importing it and make runtime checkable.""" + + def keys(self) -> Iterable[_KT]: ... + def __getitem__(self, key: _KT, /) -> _VT_co: ... + + +class SupportsToDict(Protocol): + """An object that can be converted to a dictionary.""" + + def to_dict(self) -> dict: ... + + +class SupportsObjAttr(Protocol): + """An object that has an `obj` dict attribute.""" + + obj: dict + + +# Type that can be converted to a Kubernetes spec. +SpecType = Union[dict, str, SupportsKeysAndGetItem, SupportsToDict, SupportsObjAttr]