Skip to content

Commit b920bbd

Browse files
committed
Migrate cryptocurrency.py to use pydantic models
1 parent e610191 commit b920bbd

19 files changed

+385
-578
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ config.json
6565
.vagrant/
6666
.mypy_cache/
6767
.DS_Store
68+
*.bak

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ repos:
5555
args:
5656
- "--py38-plus"
5757

58+
- repo: https://github.com/PyCQA/autoflake
59+
rev: 0544741e2b4a22b472d9d93e37d4ea9153820bb1 # frozen: v2.3.1
60+
hooks:
61+
- id: autoflake
62+
63+
5864
- repo: local
5965
hooks:
6066
- id: mypy

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
"evenBetterToml.formatter.allowedBlankLines": 1,
1515
"evenBetterToml.formatter.arrayAutoCollapse": true,
1616
"evenBetterToml.formatter.arrayAutoExpand": false,
17-
"evenBetterToml.formatter.arrayTrailingComma": true
17+
"evenBetterToml.formatter.arrayTrailingComma": true,
18+
"python.analysis.diagnosticMode": "workspace"
1819
}

cloudbot/config.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import time
55
from collections import OrderedDict
66
from pathlib import Path
7+
from typing import Dict, Optional, cast
78

89
logger = logging.getLogger("cloudbot")
910

@@ -15,19 +16,21 @@ def __init__(self, bot, *, filename="config.json"):
1516
self.path = Path(self.filename).resolve()
1617
self.bot = bot
1718

18-
self._api_keys = {}
19+
self._api_keys: Dict[str, Optional[str]] = {}
1920

2021
# populate self with config data
2122
self.load_config()
2223

23-
def get_api_key(self, name, default=None):
24+
def get_api_key(
25+
self, name: str, default: Optional[str] = None
26+
) -> Optional[str]:
2427
try:
2528
return self._api_keys[name]
2629
except LookupError:
2730
self._api_keys[name] = value = self.get("api_keys", {}).get(
2831
name, default
2932
)
30-
return value
33+
return cast(Optional[str], value)
3134

3235
def load_config(self):
3336
"""(re)loads the bot config from the config file"""

cloudbot/event.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import enum
33
import logging
44
from functools import partial
5-
from typing import Any, Iterator, Mapping
5+
from typing import Any, Iterator, Mapping, Optional
66

77
from irclib.parser import Message
88

@@ -245,7 +245,7 @@ def admin_log(self, message, broadcast=False):
245245
if conn and conn.connected:
246246
conn.admin_log(message, console=not broadcast)
247247

248-
def reply(self, *messages, target=None):
248+
def reply(self, *messages: str, target: Optional[str] = None) -> None:
249249
"""sends a message to the current channel/user with a prefix"""
250250
reply_ping = self.conn.config.get("reply_ping", True)
251251
if target is None:

cloudbot/hook.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
import re
55
import warnings
66
from enum import Enum, IntEnum, unique
7+
from typing import (
8+
Any,
9+
Callable,
10+
List,
11+
Optional,
12+
Sequence,
13+
TypeVar,
14+
Union,
15+
overload,
16+
)
17+
18+
from typing_extensions import ParamSpec
719

820
from cloudbot.event import EventType
921
from cloudbot.util import HOOK_ATTR
@@ -186,10 +198,36 @@ def _hook_warn():
186198
)
187199

188200

189-
def command(*args, **kwargs):
201+
_T = TypeVar("_T")
202+
_P = ParamSpec("_P")
203+
_Func = Callable[_P, _T]
204+
205+
206+
@overload
207+
def command(arg: Callable[_P, _T], /) -> Callable[_P, _T]: ...
208+
209+
210+
@overload
211+
def command(
212+
arg: Optional[Union[str, Sequence[str]]] = None,
213+
/,
214+
*args: Union[str, Sequence[str]],
215+
**kwargs: Any,
216+
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ...
217+
218+
219+
def command(
220+
arg: Optional[Union[Callable[_P, _T], str, Sequence[str]]] = None,
221+
/,
222+
*args: Union[str, Sequence[str]],
223+
**kwargs: Any,
224+
) -> Union[Callable[_P, _T], Callable[[Callable[_P, _T]], Callable[_P, _T]]]:
190225
"""External command decorator. Can be used directly as a decorator, or with args to return a decorator."""
191226

192-
def _command_hook(func, alias_param=None):
227+
def _command_hook(
228+
func: Callable[_P, _T],
229+
alias_param: Optional[Sequence[Union[Sequence[str], str]]] = None,
230+
) -> Callable[_P, _T]:
193231
hook = _get_hook(func, "command")
194232
if hook is None:
195233
hook = _CommandHook(func)
@@ -198,13 +236,17 @@ def _command_hook(func, alias_param=None):
198236
hook.add_hook(alias_param, kwargs)
199237
return func
200238

201-
if len(args) == 1 and callable(args[0]):
239+
if arg is not None and not isinstance(arg, (str, collections.abc.Sequence)):
202240
# this decorator is being used directly
203241
_hook_warn()
204-
return _command_hook(args[0])
242+
return _command_hook(arg)
243+
244+
arg_list: List[Union[str, Sequence[str]]] = list(args)
245+
if arg:
246+
arg_list.insert(0, arg)
205247

206248
# this decorator is being used indirectly, so return a decorator function
207-
return lambda func: _command_hook(func, alias_param=args)
249+
return lambda func: _command_hook(func, alias_param=arg_list)
208250

209251

210252
def irc_raw(triggers_param, **kwargs):
@@ -332,10 +374,22 @@ def _config_hook(func):
332374
return _config_hook
333375

334376

335-
def on_start(param=None, **kwargs):
377+
@overload
378+
def on_start(
379+
**kwargs: Any,
380+
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ...
381+
382+
383+
@overload
384+
def on_start(param: Callable[_P, _T], /) -> Callable[_P, _T]: ...
385+
386+
387+
def on_start(
388+
param: Optional[Callable[_P, _T]] = None, /, **kwargs: Any
389+
) -> Union[Callable[_P, _T], Callable[[Callable[_P, _T]], Callable[_P, _T]]]:
336390
"""External on_start decorator. Can be used directly as a decorator, or with args to return a decorator"""
337391

338-
def _on_start_hook(func):
392+
def _on_start_hook(func: Callable[_P, _T]) -> Callable[_P, _T]:
339393
hook = _get_hook(func, "on_start")
340394
if hook is None:
341395
hook = _Hook(func, "on_start")

cloudbot/util/colors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def get_available_colours():
154154
return ret[:-2]
155155

156156

157-
def parse(string):
157+
def parse(string: str) -> str:
158158
"""
159159
parse: Formats a string, replacing words wrapped in $( ) with actual colours or formatting.
160160
example:

cloudbot/util/func_utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import inspect
2+
from typing import Any, Callable, Mapping, TypeVar
23

34

45
class ParameterError(Exception):
@@ -12,7 +13,14 @@ def __init__(self, name, valid_args):
1213
self.valid_args = list(valid_args)
1314

1415

15-
def call_with_args(func, arg_data):
16+
_T = TypeVar("_T")
17+
18+
19+
def call_with_args(func: Callable[..., _T], arg_data: Mapping[str, Any]) -> _T:
20+
"""
21+
>>> call_with_args(lambda a: a, {'a':1, 'b':2})
22+
1
23+
"""
1624
sig = inspect.signature(func, follow_wrapped=False)
1725
try:
1826
args = [

cloudbot/util/web.py

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@
1717
import logging
1818
import time
1919
from operator import attrgetter
20-
from typing import Dict, Optional, Union
20+
from typing import (
21+
Dict,
22+
Generic,
23+
Iterable,
24+
Iterator,
25+
Optional,
26+
Tuple,
27+
TypeVar,
28+
Union,
29+
)
2130

2231
import requests
2332
from requests import (
@@ -41,76 +50,79 @@
4150

4251
# Public API
4352

53+
_T = TypeVar("_T")
4454

45-
class Registry:
46-
class Item:
47-
def __init__(self, item):
48-
self.item = item
49-
self.working = True
50-
self.last_check = 0.0
51-
self.uses = 0
5255

53-
def failed(self):
54-
self.working = False
55-
self.last_check = time.time()
56+
class RegistryItem(Generic[_T]):
57+
def __init__(self, item: _T) -> None:
58+
self.item = item
59+
self.working = True
60+
self.last_check = 0.0
61+
self.uses = 0
62+
63+
def failed(self) -> None:
64+
self.working = False
65+
self.last_check = time.time()
5666

57-
@property
58-
def should_use(self):
59-
if self.working:
60-
return True
67+
@property
68+
def should_use(self) -> bool:
69+
if self.working:
70+
return True
71+
72+
if (time.time() - self.last_check) > (5 * 60):
73+
# It's been 5 minutes, try again
74+
self.working = True
75+
return True
6176

62-
if (time.time() - self.last_check) > (5 * 60):
63-
# It's been 5 minutes, try again
64-
self.working = True
65-
return True
77+
return False
6678

67-
return False
6879

80+
class Registry(Generic[_T]):
6981
def __init__(self):
70-
self._items: Dict[str, "Registry.Item"] = {}
82+
self._items: Dict[str, RegistryItem[_T]] = {}
7183

72-
def register(self, name, item):
84+
def register(self, name: str, item: _T) -> None:
7385
if name in self._items:
7486
raise ValueError("Attempt to register duplicate item")
7587

76-
self._items[name] = self.Item(item)
88+
self._items[name] = RegistryItem(item)
7789

78-
def get(self, name):
90+
def get(self, name: str) -> Optional[_T]:
7991
val = self._items.get(name)
8092
if val:
8193
return val.item
8294

83-
return val
95+
return None
8496

85-
def get_item(self, name):
97+
def get_item(self, name: str) -> Optional[RegistryItem[_T]]:
8698
return self._items.get(name)
8799

88-
def get_working(self) -> Optional["Item"]:
100+
def get_working(self) -> Optional[RegistryItem[_T]]:
89101
working = [item for item in self._items.values() if item.should_use]
90102

91103
if not working:
92104
return None
93105

94106
return min(working, key=attrgetter("uses"))
95107

96-
def remove(self, name):
108+
def remove(self, name: str) -> None:
97109
del self._items[name]
98110

99-
def items(self):
111+
def items(self) -> Iterable[Tuple[str, RegistryItem[_T]]]:
100112
return self._items.items()
101113

102-
def __iter__(self):
114+
def __iter__(self) -> Iterator[str]:
103115
return iter(self._items)
104116

105-
def __getitem__(self, item):
117+
def __getitem__(self, item: str) -> _T:
106118
return self._items[item].item
107119

108-
def set_working(self):
120+
def set_working(self) -> None:
109121
for item in self._items.values():
110122
item.working = True
111123

112124

113-
def shorten(url, custom=None, key=None, service=DEFAULT_SHORTENER):
125+
def shorten(url: str, custom=None, key=None, service=DEFAULT_SHORTENER):
114126
impl = shorteners[service]
115127
return impl.shorten(url, custom, key)
116128

@@ -140,7 +152,12 @@ class NoPasteException(Exception):
140152
"""No pastebins succeeded"""
141153

142154

143-
def paste(data, ext="txt", service=DEFAULT_PASTEBIN, raise_on_no_paste=False):
155+
def paste(
156+
data: Union[str, bytes],
157+
ext="txt",
158+
service=DEFAULT_PASTEBIN,
159+
raise_on_no_paste=False,
160+
) -> str:
144161
if service:
145162
impl = pastebins.get_item(service)
146163
else:
@@ -218,12 +235,12 @@ class Pastebin:
218235
def __init__(self):
219236
pass
220237

221-
def paste(self, data, ext):
238+
def paste(self, data, ext) -> str:
222239
raise NotImplementedError
223240

224241

225-
shorteners = Registry()
226-
pastebins = Registry()
242+
shorteners = Registry[Shortener]()
243+
pastebins = Registry[Pastebin]()
227244

228245
# Internal Implementations
229246

@@ -346,7 +363,7 @@ def __init__(self, base_url):
346363
super().__init__()
347364
self.url = base_url
348365

349-
def paste(self, data, ext):
366+
def paste(self, data, ext) -> str:
350367
if isinstance(data, str):
351368
encoded = data.encode()
352369
else:

0 commit comments

Comments
 (0)