Skip to content

Commit

Permalink
annotate Client.get() with overload signatures
Browse files Browse the repository at this point in the history
Signed-off-by: flashdagger <flashdagger@googlemail.com>
  • Loading branch information
flashdagger committed Mar 30, 2024
1 parent e9fade7 commit 050f89d
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 36 deletions.
5 changes: 5 additions & 0 deletions docs/api/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ Client

.. autoclass:: Client
:members:
:exclude-members: get

.. method:: get(*args, params=None)

shortcut for :meth:`request` with parameter ``("GET", *args, params)``
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ disable = [
"missing-module-docstring",
"raw-checker-failed",
"suppressed-message",
"unsubscriptable-object",
"use-symbolic-message-instead",
"useless-suppression",
]
Expand Down
15 changes: 8 additions & 7 deletions tests/test_types_objects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
client.tickets(1).missing_method()
out: |
main:4: error: "None" not callable \[misc\]
main:4: error: "bool" not callable \[operator\]
main:4: error: "int" not callable \[operator\]
main:4: error: "float" not callable \[operator\]
main:4: error: "str" not callable \[operator\]
main:4: error: "[Ll]ist\[JsonType\]" not callable \[operator\]
main:4: error: "[Dd]ict\[str, JsonType\]" not callable \[operator\]
main:4: error: "datetime" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
main:4: error: "[^\"]+" not callable \[operator\]
- case: client_attributes_are_of_proper_type
skip: sys.implementation.name == "pypy"
main: |
Expand Down
5 changes: 4 additions & 1 deletion zammadoo/articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from os import PathLike

from .client import Client
from .resource import TypedResourceDict
from .tickets import Ticket
from .utils import JsonDict

Expand Down Expand Up @@ -204,7 +205,9 @@ def __init__(self, client: "Client"):
super().__init__(client, "ticket_articles")

def by_ticket(self, tid: int) -> List[Article]:
items = self.client.get(self.endpoint, "by_ticket", tid)
items: List["TypedResourceDict"] = self.client.get(
self.endpoint, "by_ticket", tid, _erase_return_type=True
)
return [self(item["id"], info=item) for item in items]

def create(
Expand Down
47 changes: 40 additions & 7 deletions zammadoo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
from dataclasses import dataclass
from functools import cached_property
from textwrap import shorten
from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union, cast
from typing import (
TYPE_CHECKING,
Any,
Literal,
Optional,
Sequence,
Tuple,
TypedDict,
TypeVar,
Union,
overload,
)

import requests
from requests import HTTPError, JSONDecodeError, Response
Expand All @@ -23,10 +34,13 @@
if TYPE_CHECKING:
from .utils import JsonType, StringKeyMapping


LOG = logging.getLogger(__name__)


class _TypedDict(TypedDict):
version: str


class APIException(HTTPError):
"""APIException(...)
Expand Down Expand Up @@ -67,7 +81,8 @@ def raise_or_return_json(response: requests.Response) -> "JsonType":
raise exception from exc

try:
return cast("JsonType", response.json())
json_response: "JsonType" = response.json()
return json_response
except JSONDecodeError:
return response.text

Expand All @@ -91,6 +106,8 @@ class Client:
"""

_T = TypeVar("_T")

@cached_property
def groups(self) -> Groups:
"""Manages the ``/groups`` endpoint."""
Expand Down Expand Up @@ -282,8 +299,24 @@ def response(
LOG.info("HTTP:%s %s", method, response.url)
return response

def get(self, *args, params: Optional["StringKeyMapping"] = None):
"""shortcut for :meth:`request` with parameter ``("GET", *args, params)``"""
@overload
def get(
self,
*args,
params: Optional["StringKeyMapping"] = ...,
_erase_return_type: Literal[False] = ...,
) -> "JsonType": ...

# this enforces type annotation in the assignment by mypy
@overload
def get(
self,
*args,
params: Optional["StringKeyMapping"] = ...,
_erase_return_type: Literal[True] = ...,
) -> Any: ...

def get(self, *args, params=None, _erase_return_type=False):
return self.request("GET", *args, params=params)

def post(self, *args, json: Optional["StringKeyMapping"] = None):
Expand All @@ -301,8 +334,8 @@ def delete(self, *args, json: Optional["StringKeyMapping"] = None):
@cached_property
def server_version(self) -> str:
"""the Zammad server version"""
version: str = self.get("version")["version"]
return version
info: "_TypedDict" = self.get("version", _erase_return_type=True)
return info["version"]

@cached_property
def weburl(self) -> str:
Expand Down
20 changes: 16 additions & 4 deletions zammadoo/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@
# -*- coding: UTF-8 -*-

from datetime import datetime
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union

from .resources import ResourcesT, _T_co
from .utils import DateTime, FrozenInfo, _AttributeBase

if TYPE_CHECKING:
from typing import Literal, overload

from .users import User
from .utils import AttributeT, JsonDict
from .utils import AttributeT, JsonDict, JsonType

class TypedResourceDict(JsonDict):
@overload
def __getitem__(self, item: Literal["id"]) -> int: ...

@overload
def __getitem__(self, item: str) -> "JsonType": ...

def __getitem__(self, item): ...


class Resource(FrozenInfo):
Expand Down Expand Up @@ -97,9 +108,10 @@ def update(self: _T_co, **kwargs) -> _T_co:
:return: a new instance of the updated resource
:rtype: same as object
"""
parent = cast("ResourcesT[_T_co]", self.parent)
parent = self.parent
updated_info = parent.client.put(parent.endpoint, self.id, json=kwargs)
return parent(updated_info["id"], info=updated_info)
updated_resource: _T_co = parent(updated_info["id"], info=updated_info)
return updated_resource

def delete(self) -> None:
"""Delete the resource. Requires the respective permission."""
Expand Down
11 changes: 8 additions & 3 deletions zammadoo/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __call__(self, rid: int, *, info: Optional["JsonDict"] = None) -> _T_co:
assert (
info.get("id") == rid
), "parameter info must contain 'id' and be equal with rid"
self.cache[f"{self.url}/{rid}"] = None if info is None else dict(info)
self.cache[f"{self.url}/{rid}"] = dict(info)

return self._RESOURCE_TYPE(self, rid, info=info)

Expand All @@ -71,7 +71,10 @@ def cached_info(self, rid: int, refresh=True, expand=False) -> "JsonDict":

if refresh or item not in cache:
response: "JsonDict" = self.client.get(
self.endpoint, rid, params={"expand": expand or None}
self.endpoint,
rid,
params={"expand": expand or None},
_erase_return_type=True,
)
cache[item] = response
return response
Expand Down Expand Up @@ -133,7 +136,9 @@ def iter(self, *args, **params) -> Iterator[_T_co]:
params["expand"] = params.get("expand", pagination.expand)

while True:
items = self.client.get(self.endpoint, *args, params=params)
items: List["JsonDict"] = self.client.get(
self.endpoint, *args, params=params, _erase_return_type=True
)
counter = YieldCounter()

yield from counter(self._iter_items(items))
Expand Down
17 changes: 13 additions & 4 deletions zammadoo/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class TypedTag(TypedDict):
count: Optional[int]


class _TypedDict(TypedTag, total=False):
value: str
tags: List[str]


class Tags:
"""Tags(...)
This class manages the ``/tags``, ``/tag_list`` and ``/tag_search`` endpoint.
Expand Down Expand Up @@ -48,8 +53,10 @@ def __contains__(self, item: str) -> bool:
def reload(self) -> None:
"""reloads the tag cache"""
cache = self.cache
items: List[TypedTag] = self.client.get(self.endpoint, _erase_return_type=True)

cache.clear()
cache.update((info["name"], info) for info in self.client.get(self.endpoint))
cache.update((info["name"], info) for info in items)
self._unintialized = False

def search(self, term: str) -> List[str]:
Expand All @@ -59,7 +66,9 @@ def search(self, term: str) -> List[str]:
:param term: search term
:return: search results
"""
items = self.client.get("tag_search", params={"term": term})
items: List["_TypedDict"] = self.client.get(
"tag_search", params={"term": term}, _erase_return_type=True
)
return list(info["value"] for info in items)

def create(self, name: str) -> None:
Expand Down Expand Up @@ -132,7 +141,7 @@ def by_ticket(self, tid: int) -> List[str]:
:param tid: the ticket id
:return: ticket tags
"""
items: "StringKeyMapping" = self.client.get(
"tags", params={"object": "Ticket", "o_id": tid}
items: "_TypedDict" = self.client.get(
"tags", params={"object": "Ticket", "o_id": tid}, _erase_return_type=True
)
return items.get("tags", [])
39 changes: 31 additions & 8 deletions zammadoo/tickets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union, cast, get_args
from typing import (
TYPE_CHECKING,
Dict,
List,
Literal,
Optional,
TypedDict,
Union,
get_args,
)

from .resource import MutableResource, NamedResource, UserProperty
from .resources import CreatableT, IterableT, SearchableT, _T_co
Expand All @@ -13,7 +22,7 @@
from .client import Client
from .groups import Group
from .organizations import Organization
from .resource import Resource
from .resource import Resource, TypedResourceDict
from .resources import ResourcesT
from .utils import JsonDict, StringKeyMapping

Expand All @@ -22,6 +31,13 @@
LINK_TYPES = get_args(LinkType)


class _TypedDict(TypedDict, total=False):
id: int
assets: Dict[str, Dict[str, "JsonDict"]]
history: List["StringKeyMapping"]
links: List["StringKeyMapping"]


class Priority(NamedResource):
"""Priority(...)"""

Expand Down Expand Up @@ -159,7 +175,9 @@ def time_accountings(self) -> List[TimeAccounting]:
parent = self.parent
client = parent.client
time_accountings = client.time_accountings
time_accountings_list = client.get(parent.endpoint, self.id, "time_accountings")
time_accountings_list: List["TypedResourceDict"] = client.get(
parent.endpoint, self.id, "time_accountings", _erase_return_type=True
)
return [
time_accountings(info["id"], info=info) for info in time_accountings_list
]
Expand Down Expand Up @@ -200,9 +218,12 @@ def links(self) -> Dict[str, List["Ticket"]]:
params = {"link_object": "Ticket", "link_object_value": self.id}
link_map = dict((key, []) for key in LINK_TYPES)

items = client.get("links", params=params)
cache_assets(client, items.get("assets", {}))
for item in items["links"]:
items: _TypedDict = client.get("links", params=params, _erase_return_type=True)
assets = items.get("assets", {})
cache_assets(client, assets)

links = items["links"]
for item in links:
assert item["link_object"] == "Ticket"
link_type = item["link_type"]
link_map.setdefault(link_type, []).append(parent(item["link_object_value"]))
Expand Down Expand Up @@ -347,8 +368,10 @@ def history(self) -> List["StringKeyMapping"]:
:return: the ticket's history
"""
info = self.parent.client.get("ticket_history", self.id)
return cast(List["StringKeyMapping"], info["history"])
info: _TypedDict = self.parent.client.get(
"ticket_history", self.id, _erase_return_type=True
)
return info["history"]

@property
def weburl(self) -> str:
Expand Down
5 changes: 4 additions & 1 deletion zammadoo/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
if TYPE_CHECKING:
from .client import Client
from .organizations import Organization
from .resource import TypedResourceDict
from .roles import Role


Expand Down Expand Up @@ -127,5 +128,7 @@ def me(self) -> User:
"""
:return: Return the authenticated user.
"""
info = self.client.get(self.endpoint, "me")
info: "TypedResourceDict" = self.client.get(
self.endpoint, "me", _erase_return_type=True
)
return self._RESOURCE_TYPE(self, info["id"], info=info)
4 changes: 3 additions & 1 deletion zammadoo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from types import MappingProxyType
from typing import Any, Dict, Generic, Iterable, List, Mapping, Optional, TypeVar, Union

JsonType = Union[None, bool, int, float, str, List["JsonType"], "JsonDict"]
JsonType = Union[
None, bool, int, float, str, List["JsonDict"], List["JsonType"], "JsonDict"
]
JsonDict = Dict[str, JsonType]
StringKeyMapping = Mapping[str, Any]

Expand Down

0 comments on commit 050f89d

Please sign in to comment.