Skip to content

Commit a99f509

Browse files
authored
Merge pull request #459 from TotallyNotRobots/add-3.10
Add 3.10 to testing
2 parents f5feb97 + 1f5a17f commit a99f509

File tree

13 files changed

+134
-87
lines changed

13 files changed

+134
-87
lines changed

.devcontainer/devcontainer.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/python
3+
{
4+
"name": "Python 3",
5+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6+
"image": "mcr.microsoft.com/devcontainers/python:0-3.9",
7+
"features": {
8+
"ghcr.io/devcontainers-contrib/features/zsh-plugins:0": {
9+
"plugins": "ssh-agent",
10+
"omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions"
11+
}
12+
},
13+
14+
// Features to add to the dev container. More info: https://containers.dev/features.
15+
// "features": {},
16+
17+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
18+
// "forwardPorts": [],
19+
20+
// Use 'postCreateCommand' to run commands after the container is created.
21+
// "postCreateCommand": "pip3 install -Ur requirements-dev.txt",
22+
"postStartCommand": "pip3 install -Ur requirements-dev.txt",
23+
// Configure tool-specific properties.
24+
// "customizations": {},
25+
26+
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
27+
"remoteUser": "root"
28+
}

.github/workflows/pythonapp.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ jobs:
1818
- '3.7'
1919
- '3.8'
2020
- '3.9'
21-
runs-on: ubuntu-latest
21+
- '3.10'
22+
runs-on: ubuntu-22.04
2223
steps:
2324
- uses: actions/checkout@v3
2425
with:

.pre-commit-config.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ repos:
2222
- id: check-docstring-first
2323
- id: check-executables-have-shebangs
2424
- id: check-json
25+
exclude: '.devcontainer/.*'
2526
- id: pretty-format-json
27+
exclude: '.devcontainer/.*'
2628
args:
2729
- --indent
2830
- '4'
@@ -33,11 +35,11 @@ repos:
3335
args:
3436
- --remove
3537
- repo: https://github.com/psf/black
36-
rev: 2ddea293a88919650266472186620a98a4a8bb37 # frozen: 22.12.0
38+
rev: b0d1fba7ac3be53c71fb0d3211d911e629f8aecb # frozen: 23.1.0
3739
hooks:
3840
- id: black
3941
- repo: https://github.com/pycqa/isort
40-
rev: 4e97b170469b7c8ef29afe944ebfb057791457aa # frozen: 5.11.4
42+
rev: dbf82f2dd09ae41d9355bcd7ab69187a19e6bf2f # frozen: 5.12.0
4143
hooks:
4244
- id: isort
4345
- repo: https://github.com/pre-commit/pygrep-hooks

cloudbot/__main__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import logging
23
import os
34
import signal
@@ -9,7 +10,7 @@
910
from cloudbot.util import async_util
1011

1112

12-
def main():
13+
async def async_main():
1314
# store the original working directory, for use when restarting
1415
original_wd = Path().resolve()
1516

@@ -55,7 +56,7 @@ def exit_gracefully(signum, frame):
5556
# start the bot
5657

5758
# CloudBot.run() will return True if it should restart, False otherwise
58-
restart = _bot.run()
59+
restart = await _bot.run()
5960

6061
# the bot has stopped, do we want to restart?
6162
if restart:
@@ -84,5 +85,9 @@ def exit_gracefully(signum, frame):
8485
logging.shutdown()
8586

8687

88+
def main():
89+
asyncio.run(async_main())
90+
91+
8792
if __name__ == "__main__":
8893
main()

cloudbot/bot.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import collections
33
import gc
4+
import importlib
45
import logging
56
import re
67
import time
@@ -10,13 +11,12 @@
1011
from pathlib import Path
1112
from typing import Any, Dict, Optional, Type
1213

13-
from sqlalchemy import Table, create_engine, inspect
14+
from sqlalchemy import Table, create_engine
15+
from sqlalchemy import inspect as sa_inspect
1416
from sqlalchemy.engine import Engine
1517
from sqlalchemy.orm import Session, scoped_session, sessionmaker
16-
from venusian import Scanner
1718
from watchdog.observers import Observer
1819

19-
from cloudbot import clients
2020
from cloudbot.client import Client
2121
from cloudbot.config import Config
2222
from cloudbot.event import CommandEvent, Event, EventType, RegexEvent
@@ -186,22 +186,21 @@ def data_dir(self) -> str:
186186
)
187187
return str(self.data_path)
188188

189-
def run(self):
189+
async def run(self):
190190
"""
191191
Starts CloudBot.
192192
This will load plugins, connect to IRC, and process input.
193193
:return: True if CloudBot should be restarted, False otherwise
194194
"""
195195
self.loop.set_default_executor(self.executor)
196196
# Initializes the bot, plugins and connections
197-
self.loop.run_until_complete(self._init_routine())
197+
await self._init_routine()
198198
# Wait till the bot stops. The stopped_future will be set to True to restart, False otherwise
199199
logger.debug("Init done")
200-
restart = self.loop.run_until_complete(self.stopped_future)
200+
restart = await self.stopped_future
201201
logger.debug("Waiting for plugin unload")
202-
self.loop.run_until_complete(self.plugin_manager.unload_all())
202+
await self.plugin_manager.unload_all()
203203
logger.debug("Unload complete")
204-
self.loop.close()
205204
return restart
206205

207206
def get_client(self, name: str) -> Type[Client]:
@@ -316,8 +315,21 @@ def load_clients(self):
316315
"""
317316
Load all clients from the "clients" directory
318317
"""
319-
scanner = Scanner(bot=self)
320-
scanner.scan(clients, categories=["cloudbot.client"])
318+
for file in (Path(__file__).parent / "clients").rglob("*.py"):
319+
if file.name.startswith("_"):
320+
continue
321+
322+
mod = importlib.import_module("cloudbot.clients." + file.stem)
323+
for _, obj in vars(mod).items():
324+
if not isinstance(obj, type):
325+
continue
326+
327+
try:
328+
_type = obj._cloudbot_client # type: ignore
329+
except AttributeError:
330+
continue
331+
332+
self.register_client(_type, obj)
321333

322334
async def process(self, event):
323335
run_before_tasks = []
@@ -467,7 +479,7 @@ def migrate_db(self) -> None:
467479
old_session: Session = scoped_session(sessionmaker(bind=engine))()
468480
new_session: Session = database.Session()
469481
table: Table
470-
inspector = inspect(engine)
482+
inspector = sa_inspect(engine)
471483
for table in database.metadata.tables.values():
472484
logger.info("Migrating table %s", table.name)
473485
if not inspector.has_table(table.name):

cloudbot/client.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import random
55
from typing import Any, Dict
66

7-
import venusian
8-
97
from cloudbot.permissions import PermissionManager
108
from cloudbot.util import async_util
119

@@ -14,10 +12,7 @@
1412

1513
def client(_type):
1614
def _decorate(cls):
17-
def callback_cb(context, name, obj):
18-
context.bot.register_client(_type, cls)
19-
20-
venusian.attach(cls, callback_cb, category="cloudbot.client")
15+
cls._cloudbot_client = _type
2116
return cls
2217

2318
return _decorate

cloudbot/util/async_util.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import asyncio
6-
import sys
76
from asyncio import AbstractEventLoop
87
from asyncio.tasks import Task
98
from functools import partial
@@ -56,19 +55,10 @@ def run_coroutine_threadsafe(coro, loop):
5655
if not asyncio.iscoroutine(coro):
5756
raise TypeError("A coroutine object is required")
5857

59-
if sys.version_info < (3, 5, 1):
60-
loop.call_soon_threadsafe(partial(wrap_future, coro, loop=loop))
61-
else:
62-
asyncio.run_coroutine_threadsafe(coro, loop)
63-
64-
65-
def create_future(loop=None) -> asyncio.Future:
66-
if loop is None:
67-
loop = asyncio.get_event_loop()
58+
asyncio.run_coroutine_threadsafe(coro, loop)
6859

69-
if sys.version_info < (3, 5, 2):
70-
return asyncio.Future(loop=loop)
7160

61+
def create_future(loop) -> asyncio.Future:
7262
return loop.create_future()
7363

7464

cloudbot/util/mapping.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import weakref
22
from collections import defaultdict
3-
from typing import Generic, Mapping, MutableMapping, Type, TypeVar, cast
3+
from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Union, cast
44

55
__all__ = (
66
"KeyFoldDict",
@@ -10,57 +10,74 @@
1010
)
1111

1212

13-
K = TypeVar("K", bound=str)
13+
K_contra = TypeVar("K_contra", bound=str, contravariant=True)
1414
V = TypeVar("V")
15+
T = TypeVar("T")
1516

1617

17-
class KeyFoldMixin(Generic[K, V]):
18+
if TYPE_CHECKING:
19+
from typing import Protocol
20+
21+
class MapBase(Protocol[K_contra, V]):
22+
def __getitem__(self, item: K_contra) -> V:
23+
...
24+
25+
def __delitem__(self, item: K_contra) -> None:
26+
...
27+
28+
def __setitem__(self, item: K_contra, value: V) -> None:
29+
...
30+
31+
def get(self, item: K_contra, default: V = None) -> Optional[V]:
32+
...
33+
34+
def setdefault(
35+
self, key: K_contra, default: Union[V, T] = None
36+
) -> Union[V, T]:
37+
...
38+
39+
def pop(
40+
self, key: K_contra, default: Union[V, T] = None
41+
) -> Union[V, T]:
42+
...
43+
44+
else:
45+
46+
class MapBase(Generic[K_contra, V]):
47+
...
48+
49+
50+
class KeyFoldMixin(MapBase[K_contra, V]):
1851
"""
1952
A mixin for Mapping to allow for case-insensitive keys
2053
"""
2154

22-
@classmethod
23-
def get_class(cls) -> Type[MutableMapping[K, V]]:
24-
raise NotImplementedError
25-
26-
def __getitem__(self, item: K) -> V:
27-
return self.get_class().__getitem__(
28-
cast(Mapping[K, V], self), cast(K, item.casefold())
29-
)
55+
def __getitem__(self, item: K_contra) -> V:
56+
return super().__getitem__(cast(K_contra, item.casefold()))
3057

31-
def __setitem__(self, key: K, value: V) -> None:
32-
return self.get_class().__setitem__(
33-
cast(MutableMapping[K, V], self), cast(K, key.casefold()), value
34-
)
58+
def __setitem__(self, key: K_contra, value: V) -> None:
59+
return super().__setitem__(cast(K_contra, key.casefold()), value)
3560

36-
def __delitem__(self, key: K) -> None:
37-
return self.get_class().__delitem__(
38-
cast(MutableMapping[K, V], self), cast(K, key.casefold())
39-
)
61+
def __delitem__(self, key: K_contra) -> None:
62+
return super().__delitem__(cast(K_contra, key.casefold()))
4063

41-
def pop(self, key: K, *args) -> V:
64+
def pop(self, key: K_contra, *args) -> V:
4265
"""
4366
Wraps `dict.pop`
4467
"""
45-
return self.get_class().pop(
46-
cast(MutableMapping[K, V], self), cast(K, key.casefold()), *args
47-
)
68+
return super().pop(cast(K_contra, key.casefold()), *args)
4869

49-
def get(self, key: K, default=None):
70+
def get(self, key: K_contra, default=None):
5071
"""
5172
Wrap `dict.get`
5273
"""
53-
return self.get_class().get(
54-
cast(Mapping[K, V], self), cast(K, key.casefold()), default
55-
)
74+
return super().get(cast(K_contra, key.casefold()), default)
5675

57-
def setdefault(self, key: K, default=None):
76+
def setdefault(self, key: K_contra, default=None):
5877
"""
5978
Wrap `dict.setdefault`
6079
"""
61-
return self.get_class().setdefault(
62-
cast(MutableMapping[K, V], self), cast(K, key.casefold()), default
63-
)
80+
return super().setdefault(cast(K_contra, key.casefold()), default)
6481

6582
def update(self, *args, **kwargs):
6683
"""
@@ -84,26 +101,14 @@ class KeyFoldDict(KeyFoldMixin, dict):
84101
KeyFolded dict type
85102
"""
86103

87-
@classmethod
88-
def get_class(cls) -> Type[MutableMapping[K, V]]:
89-
return dict
90-
91104

92105
class DefaultKeyFoldDict(KeyFoldMixin, defaultdict):
93106
"""
94107
KeyFolded defaultdict
95108
"""
96109

97-
@classmethod
98-
def get_class(cls) -> Type[MutableMapping[K, V]]:
99-
return defaultdict
100-
101110

102111
class KeyFoldWeakValueDict(KeyFoldMixin, weakref.WeakValueDictionary):
103112
"""
104113
KeyFolded WeakValueDictionary
105114
"""
106-
107-
@classmethod
108-
def get_class(cls) -> Type[MutableMapping[K, V]]:
109-
return weakref.WeakValueDictionary

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mypy == 0.910
44
pre-commit == 2.17.0
55
pylint == 2.11.1
66
pytest == 6.2.5
7-
pytest-asyncio == 0.16.0
7+
pytest-asyncio == 0.20.3
88
pytest-cov == 3.0.0
99
pytest-random-order == 1.0.4
1010
responses == 0.16.0

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ py-irclib == 0.3.0
1717
requests == 2.27.1
1818
tweepy == 3.10.0
1919
urllib3 == 1.26.7
20-
venusian == 3.0.0
2120
watchdog == 2.1.6
2221
yarl == 1.7.2

0 commit comments

Comments
 (0)