Skip to content

Commit d744e13

Browse files
committed
tighten addon_toolkit types
1 parent 59253d2 commit d744e13

File tree

13 files changed

+120
-74
lines changed

13 files changed

+120
-74
lines changed

addon_imps/storage/box_dot_com.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def _params_from_cursor(self, cursor: str = "") -> dict[str, str]:
6161
# https://developer.box.com/guides/api-calls/pagination/offset-based/
6262
try:
6363
_cursor = OffsetCursor.from_str(cursor)
64-
return {"offset": _cursor.offset, "limit": _cursor.limit}
64+
return {"offset": str(_cursor.offset), "limit": str(_cursor.limit)}
6565
except ValueError:
6666
return {}
6767

addon_service/common/network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(
6666

6767
# abstract method from HttpRequestor:
6868
@contextlib.asynccontextmanager
69-
async def do_send(self, request: HttpRequestInfo):
69+
async def send_request(self, request: HttpRequestInfo):
7070
try:
7171
async with self._try_send(request) as _response:
7272
yield _response

addon_toolkit/constrained_network/http.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,25 @@ class HttpRequestor(typing.Protocol):
6161
def response_info_cls(self) -> type[HttpResponseInfo]: ...
6262

6363
# abstract method for subclasses
64-
def do_send(
64+
def send_request(
6565
self, request: HttpRequestInfo
6666
) -> contextlib.AbstractAsyncContextManager[HttpResponseInfo]: ...
6767

6868
@contextlib.asynccontextmanager
69-
async def request(
69+
async def _request(
7070
self,
7171
http_method: HTTPMethod,
7272
uri_path: str,
7373
query: Multidict | KeyValuePairs | None = None,
7474
headers: Multidict | KeyValuePairs | None = None,
75-
):
75+
) -> typing.Any: # loose type; method-specific methods below are more accurate
7676
_request_info = HttpRequestInfo(
7777
http_method=http_method,
7878
uri_path=uri_path,
7979
query=(query if isinstance(query, Multidict) else Multidict(query)),
8080
headers=(headers if isinstance(headers, Multidict) else Multidict(headers)),
8181
)
82-
async with self.do_send(_request_info) as _response:
82+
async with self.send_request(_request_info) as _response:
8383
yield _response
8484

8585
# TODO: streaming send/receive (only if/when needed)
@@ -88,10 +88,10 @@ async def request(
8888
# convenience methods for http methods
8989
# (same call signature as self.request, minus `http_method`)
9090

91-
OPTIONS: _MethodRequestMethod = partialmethod(request, HTTPMethod.OPTIONS)
92-
HEAD: _MethodRequestMethod = partialmethod(request, HTTPMethod.HEAD)
93-
GET: _MethodRequestMethod = partialmethod(request, HTTPMethod.GET)
94-
PATCH: _MethodRequestMethod = partialmethod(request, HTTPMethod.PATCH)
95-
POST: _MethodRequestMethod = partialmethod(request, HTTPMethod.POST)
96-
PUT: _MethodRequestMethod = partialmethod(request, HTTPMethod.PUT)
97-
DELETE: _MethodRequestMethod = partialmethod(request, HTTPMethod.DELETE)
91+
OPTIONS: _MethodRequestMethod = partialmethod(_request, HTTPMethod.OPTIONS)
92+
HEAD: _MethodRequestMethod = partialmethod(_request, HTTPMethod.HEAD)
93+
GET: _MethodRequestMethod = partialmethod(_request, HTTPMethod.GET)
94+
PATCH: _MethodRequestMethod = partialmethod(_request, HTTPMethod.PATCH)
95+
POST: _MethodRequestMethod = partialmethod(_request, HTTPMethod.POST)
96+
PUT: _MethodRequestMethod = partialmethod(_request, HTTPMethod.PUT)
97+
DELETE: _MethodRequestMethod = partialmethod(_request, HTTPMethod.DELETE)

addon_toolkit/credentials.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44

55
@dataclasses.dataclass(frozen=True)
66
class Credentials(typing.Protocol):
7-
def asdict(self):
7+
def asdict(self) -> dict[str, typing.Any]:
88
return dataclasses.asdict(self)
99

1010
def iter_headers(self) -> typing.Iterator[tuple[str, str]]:
11-
return
12-
yield
11+
yield from () # no headers unless implemented by subclass
1312

1413

1514
@dataclasses.dataclass(frozen=True, kw_only=True)
1615
class AccessTokenCredentials(Credentials):
1716
access_token: str
1817

19-
def iter_headers(self):
18+
def iter_headers(self) -> typing.Iterator[tuple[str, str]]:
2019
yield ("Authorization", f"Bearer {self.access_token}")
2120

2221

addon_toolkit/cursor.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
import base64
22
import dataclasses
33
import json
4-
from typing import (
5-
ClassVar,
6-
Protocol,
7-
)
4+
import typing
85

96

10-
def encode_cursor_dataclass(dataclass_instance) -> str:
7+
class DataclassInstance(typing.Protocol):
8+
__dataclass_fields__: typing.ClassVar[dict[str, typing.Any]]
9+
10+
11+
SomeDataclassInstance = typing.TypeVar("SomeDataclassInstance", bound=DataclassInstance)
12+
13+
14+
def encode_cursor_dataclass(dataclass_instance: DataclassInstance) -> str:
1115
_as_json = json.dumps(dataclasses.astuple(dataclass_instance))
1216
_cursor_bytes = base64.b64encode(_as_json.encode())
1317
return _cursor_bytes.decode()
1418

1519

16-
def decode_cursor_dataclass(cursor: str, dataclass_class):
20+
def decode_cursor_dataclass(
21+
cursor: str, dataclass_class: type[SomeDataclassInstance]
22+
) -> SomeDataclassInstance:
1723
_as_list = json.loads(base64.b64decode(cursor))
1824
return dataclass_class(*_as_list)
1925

2026

21-
class Cursor(Protocol):
27+
class Cursor(typing.Protocol, DataclassInstance):
2228
@classmethod
23-
def from_str(cls, cursor: str):
29+
def from_str(cls, cursor: str) -> typing.Self:
2430
return decode_cursor_dataclass(cursor, cls)
2531

2632
@property
@@ -52,7 +58,7 @@ class OffsetCursor(Cursor):
5258
limit: int
5359
total_count: int # use -1 to mean "many more"
5460

55-
MAX_INDEX: ClassVar[int] = 9999
61+
MAX_INDEX: typing.ClassVar[int] = 9999
5662

5763
@property
5864
def next_cursor_str(self) -> str | None:

addon_toolkit/declarator.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
TypeVar,
88
)
99

10+
from addon_toolkit.typing import DataclassInstance
11+
1012

1113
DecoratorTarget = TypeVar("DecoratorTarget")
12-
DeclarationDataclass = TypeVar("DeclarationDataclass")
1314

1415

1516
@dataclasses.dataclass
16-
class Declarator(Generic[DeclarationDataclass]):
17+
class Declarator(Generic[DataclassInstance]):
1718
"""Declarator: add declarative metadata in python using decorators and dataclasses
1819
1920
define a dataclass with fields you want declared in your decorator, plus a field
@@ -48,15 +49,15 @@ class Declarator(Generic[DeclarationDataclass]):
4849
TwoPartGreetingDeclaration(a='kia', b='ora', on=<function _kia_ora at 0x...>)
4950
"""
5051

51-
declaration_dataclass: type[DeclarationDataclass]
52+
declaration_dataclass: type[DataclassInstance]
5253
field_for_target: str
5354
static_kwargs: dict[str, Any] | None = None
5455

5556
# private storage linking a decorated class or function to data gleaned from its decorator
56-
__declarations_by_target: weakref.WeakKeyDictionary[
57-
object, DeclarationDataclass
58-
] = dataclasses.field(
59-
default_factory=weakref.WeakKeyDictionary,
57+
__declarations_by_target: weakref.WeakKeyDictionary[object, DataclassInstance] = (
58+
dataclasses.field(
59+
default_factory=weakref.WeakKeyDictionary,
60+
)
6061
)
6162

6263
def __post_init__(self) -> None:
@@ -69,7 +70,7 @@ def __post_init__(self) -> None:
6970
), f'expected field "{self.field_for_target}" on dataclass "{self.declaration_dataclass}"'
7071

7172
def __call__(
72-
self, **declaration_dataclass_kwargs
73+
self, **declaration_dataclass_kwargs: Any
7374
) -> Callable[[DecoratorTarget], DecoratorTarget]:
7475
"""for using a Declarator as a decorator"""
7576

@@ -79,13 +80,13 @@ def _decorator(decorator_target: DecoratorTarget) -> DecoratorTarget:
7980

8081
return _decorator
8182

82-
def with_kwargs(self, **static_kwargs) -> "Declarator[DeclarationDataclass]":
83+
def with_kwargs(self, **static_kwargs: Any) -> "Declarator[DataclassInstance]":
8384
"""convenience for decorators that differ only by static field values"""
8485
# note: shared __declarations_by_target
8586
return dataclasses.replace(self, static_kwargs=static_kwargs)
8687

8788
def set_declaration(
88-
self, declaration_target: DecoratorTarget, **declaration_dataclass_kwargs
89+
self, declaration_target: DecoratorTarget, **declaration_dataclass_kwargs: Any
8990
) -> None:
9091
"""create a declaration associated with the target
9192
@@ -98,14 +99,14 @@ def set_declaration(
9899
**{self.field_for_target: declaration_target},
99100
)
100101

101-
def get_declaration(self, target) -> DeclarationDataclass:
102+
def get_declaration(self, target: DecoratorTarget) -> DataclassInstance:
102103
try:
103104
return self.__declarations_by_target[target]
104105
except KeyError:
105106
raise ValueError(f"no declaration found for {target}")
106107

107108

108-
class ClassDeclarator(Declarator[DeclarationDataclass]):
109+
class ClassDeclarator(Declarator[DataclassInstance]):
109110
"""add declarative metadata to python classes using decorators
110111
111112
(same as Declarator but with additional methods that only make
@@ -157,13 +158,15 @@ class ClassDeclarator(Declarator[DeclarationDataclass]):
157158
SemanticVersionDeclaration(major=4, minor=2, patch=9, subj=<class 'addon_toolkit.declarator.MyLongLivedBaseClass'>)
158159
"""
159160

160-
def get_declaration_for_class_or_instance(self, type_or_object: type | object):
161+
def get_declaration_for_class_or_instance(
162+
self, type_or_object: type | object
163+
) -> DataclassInstance:
161164
_cls = (
162165
type_or_object if isinstance(type_or_object, type) else type(type_or_object)
163166
)
164167
return self.get_declaration_for_class(_cls)
165168

166-
def get_declaration_for_class(self, cls: type):
169+
def get_declaration_for_class(self, cls: type) -> DataclassInstance:
167170
for _cls in cls.__mro__:
168171
try:
169172
return self.get_declaration(_cls)

addon_toolkit/imp.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import dataclasses
22
import enum
33
import inspect
4-
from typing import (
5-
Iterable,
6-
Iterator,
7-
)
4+
import typing
85

96
from asgiref.sync import (
107
async_to_sync,
@@ -37,16 +34,16 @@ class AddonImp:
3734
imp_number: int
3835
addon_protocol: AddonProtocolDeclaration = dataclasses.field(init=False)
3936

40-
def __post_init__(self, addon_protocol_cls):
37+
def __post_init__(self, addon_protocol_cls: type) -> None:
4138
object.__setattr__( # using __setattr__ to bypass dataclass frozenness
4239
self,
4340
"addon_protocol",
4441
addon_protocol.get_declaration(addon_protocol_cls),
4542
)
4643

4744
def get_operation_imps(
48-
self, *, capabilities: Iterable[enum.Enum] = ()
49-
) -> Iterator["AddonOperationImp"]:
45+
self, *, capabilities: typing.Iterable[enum.Enum] = ()
46+
) -> typing.Iterator["AddonOperationImp"]:
5047
for _declaration in self.addon_protocol.get_operation_declarations(
5148
capabilities=capabilities
5249
):
@@ -74,26 +71,26 @@ class AddonOperationImp:
7471
addon_imp: AddonImp
7572
declaration: AddonOperationDeclaration
7673

77-
def __post_init__(self):
74+
def __post_init__(self) -> None:
7875
_protocol_fn = getattr(
7976
self.addon_imp.addon_protocol.protocol_cls, self.declaration.name
8077
)
8178
try:
8279
_imp_fn = self.imp_function
83-
except AttributeError:
80+
except Exception:
8481
_imp_fn = _protocol_fn
8582
if _imp_fn is _protocol_fn:
8683
raise NotImplementedError( # TODO: helpful exception type
8784
f"operation '{self.declaration}' not implemented by {self.addon_imp}"
8885
)
8986

9087
@property
91-
def imp_function(self):
88+
def imp_function(self) -> typing.Any: # TODO: less typing.Any
9289
return getattr(self.addon_imp.imp_cls, self.declaration.name)
9390

9491
async def invoke_thru_addon(
9592
self, addon_instance: object, json_kwargs: JsonableDict
96-
):
93+
) -> typing.Any: # TODO: less typing.Any
9794
_method = self._get_instance_method(addon_instance)
9895
_kwargs = kwargs_from_json(self.declaration.call_signature, json_kwargs)
9996
if not inspect.iscoroutinefunction(_method):
@@ -104,7 +101,7 @@ async def invoke_thru_addon(
104101

105102
invoke_thru_addon__blocking = async_to_sync(invoke_thru_addon)
106103

107-
def _get_instance_method(self, addon_instance: object):
104+
def _get_instance_method(
105+
self, addon_instance: object
106+
) -> typing.Any: # TODO: less typing.Any
108107
return getattr(addon_instance, self.declaration.name)
109-
110-
# TODO: async def async_call_with_json_kwargs(self, addon_instance: object, json_kwargs: dict):

addon_toolkit/iri_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ def __init__(self, key_value_pairs: KeyValuePairs | None = None):
6363
_headerslist = list(key_value_pairs)
6464
super().__init__(_headerslist)
6565

66-
def add(self, key: str, value: str, **mediatype_params):
66+
def add(self, key: str, value: str) -> None:
6767
"""add a key-value pair (allowing other values to exist)
6868
6969
alias of `wsgiref.headers.Headers.add_header`
7070
"""
71-
super().add_header(key, value, **mediatype_params)
71+
super().add_header(key, value)
7272

73-
def add_many(self, pairs: Iterable[tuple[str, str]]):
73+
def add_many(self, pairs: Iterable[tuple[str, str]]) -> None:
7474
for _key, _value in pairs:
7575
self.add(_key, _value)
7676

0 commit comments

Comments
 (0)