From ab597c4ec3c30a8da9f497adc87a4e35baa92f6f Mon Sep 17 00:00:00 2001 From: Codemonkey51 <62217716+Codemonkey51@users.noreply.github.com> Date: Mon, 3 Aug 2020 17:50:18 -0700 Subject: [PATCH 01/10] fix liniting fix as much lining as i can --- src/replit/database/__init__.py | 105 +++++++++++++++++++------------- src/replit/database/_async.py | 21 ++++--- 2 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index b8b222b..1d9e82c 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -1,17 +1,16 @@ """Interface with the Replit Database.""" +import asyncio import json -import aiohttp import os from sys import stderr -from typing import Any, Callable, Dict, Tuple, Union +from typing import Any, Callable, Dict, List, Tuple, Union + +import aiohttp from . import _async -import asyncio - asyncio.run = _async.run -import requests JSON_TYPE = Union[str, int, float, bool, type(None), dict, list] @@ -26,12 +25,12 @@ class JSONKey: __slots__ = ("db", "key", "dtype", "get_default", "discard_bad_data") def __init__( - self, - db: Any, - key: str, - dtype: JSON_TYPE, - get_default: Callable = None, - discard_bad_data: bool = False, + self, + db: Any, + key: str, + dtype: JSON_TYPE, + get_default: Callable = None, + discard_bad_data: bool = False, ) -> None: """Initialize the key. @@ -59,8 +58,10 @@ class JSONKey: return self.dtype is Any or isinstance(data, self.dtype) def _type_mismatch_msg(self, data: Any) -> str: - return (f"Type mismatch: Got type {type(data).__name__}," - "expected {self.dtype.__name__}") + return ( + f"Type mismatch: Got type {type(data).__name__}," + "expected {self.dtype.__name__}" + ) def get(self) -> JSON_TYPE: """Get the value of the key. @@ -74,9 +75,7 @@ class JSONKey: try: read = self.db[self.key] except KeyError: - print( - f"Database key {self.key} not set, setting it to default value" - ) + print(f"Database key {self.key} not set, setting it to default value") default = self._default() self.db[self.key] = default return default @@ -87,10 +86,7 @@ class JSONKey: return self._error("Invalid JSON data read", read) if not self._is_valid_type(data): - return self._error( - self._type_mismatch_msg(data), - read, - ) + return self._error(self._type_mismatch_msg(data), read,) return data def _error(self, error: str, read: str) -> JSON_TYPE: @@ -106,7 +102,8 @@ class JSONKey: while True: choice = input( "d to use default, v to view the invalid data, c to insert custom " - "value, ^C to exit: ") + "value, ^C to exit: " + ) if choice.startswith("d"): print("Writing default...") val = self._default() @@ -117,7 +114,8 @@ class JSONKey: elif choice.startswith("c"): toset = input( f"Enter data to write, should be of type {self.dtype.__name__!r}" - " (leave blank to return to menu): ") + " (leave blank to return to menu): " + ) if not toset: continue try: @@ -234,11 +232,11 @@ class ReplitDb(dict): return self.to_dict().items() def jsonkey( - self, - key: str, - dtype: JSON_TYPE, - get_default: Callable = None, - discard_bad_data: bool = False, + self, + key: str, + dtype: JSON_TYPE, + get_default: Callable = None, + discard_bad_data: bool = False, ) -> JSONKey: """Initialize a JSONKey instance. @@ -274,22 +272,47 @@ class ReplitDb(dict): return f"" -class AsyncClient(): - def __init__(self, db_url): +class AsyncClient: + def __init__(self, db_url: str) -> None: + self.db_url = db_url + self.backend = _AsyncBackend(db_url) + + async def view(self, key: str) -> str: + return await self.backend.view(key) + + async def list(self, prefix: str) -> List[str]: + return await self.backend.list(prefix) + + async def delete(self, key: str) -> None: + return await self.backend.delete(key) + + async def set(self, key: str, val: str) -> None: + return await self.backend.set(key, val) + + async def to_dict(self) -> Dict[str, str]: + ret = {} + keys = await self.keys() + for i in keys: + ret[i] = await self.view(i) + return ret + + async def keys(self) -> Tuple[str]: + return tuple(await self.list("")) + + async def values(self) -> Tuple[str]: + return tuple((await self.to_dict()).values()) + + +class _AsyncBackend: + def __init__(self, db_url: str) -> None: self.db_url = db_url - -class _AsyncBackend(): - def __init__(self, db_url): - self.db_url = db_url - - async def set(self, key, val): + async def set(self, key: str, val: str) -> None: async with aiohttp.ClientSession() as session: async with session.post(self.db_url, data={key: val}) as response: response.raise_for_status() - return await response.text() - async def view(self, key): + async def view(self, key: str) -> str: async with aiohttp.ClientSession() as session: async with session.get(self.db_url + "/" + key) as response: if response.status == 404: @@ -297,16 +320,14 @@ class _AsyncBackend(): response.raise_for_status() return await response.text() - async def delete(self, key): + async def delete(self, key: str) -> None: async with aiohttp.ClientSession() as session: async with session.delete(self.db_url + "/" + key) as response: response.raise_for_status() - return await response.text() - async def list(self, prefix): + async def list(self, prefix: str) -> Tuple[str]: async with aiohttp.ClientSession() as session: - async with session.get(self.db_url + "?prefix=" + - prefix) as response: + async with session.get(self.db_url + "?prefix=" + prefix) as response: response.raise_for_status() if not await response.text(): return tuple() diff --git a/src/replit/database/_async.py b/src/replit/database/_async.py index eafc32c..08af0ab 100644 --- a/src/replit/database/_async.py +++ b/src/replit/database/_async.py @@ -1,37 +1,40 @@ -import threading +"""Allows asyncio.run to work in async environment.""" import asyncio import sys +import threading +from typing import Any, Callable, List + asyncio._run = asyncio.run class AsyncThread(threading.Thread): - def __init__(self, res, exc, func): + def __init__(self, res: List[None], exc: List[None], func: Callable) -> None: self.result = res self.exc = exc self.func = func threading.Thread.__init__(self) - def run(self): - def inner(func): + def run(self) -> Any: + def inner(func: Callable) -> Any: try: res = asyncio.run(func) - except Exception as e: + except Exception: self.exc[0] = sys.exc_info() - res = '' + res = "" return res self.result[0] = inner(self.func) -def run(func): - def error(): +def run(func: Callable) -> Any: + def error() -> Any: ret = [None] exc = [None] thread = AsyncThread(ret, exc, func) thread.start() thread.join() exc = exc[0] - if exc != None: + if exc is not None: raise exc[1].with_traceback(exc[2]) sys.exit(1) return ret[0] From b0bce695cd7a284d2df2f83abc502d628912f8a4 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Tue, 4 Aug 2020 13:25:26 -0700 Subject: [PATCH 02/10] Initial code for switch to async --- poetry.lock | 6 +- pyproject.toml | 1 - src/replit/database/__init__.py | 162 +++++++++++--------------------- 3 files changed, 57 insertions(+), 112 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8022d7e..5c6b905 100644 --- a/poetry.lock +++ b/poetry.lock @@ -141,7 +141,7 @@ typed-ast = ">=1.4.0" d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] -category = "main" +category = "dev" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false @@ -493,7 +493,7 @@ python-versions = "*" version = "2020.7.14" [[package]] -category = "main" +category = "dev" description = "Python HTTP for Humans." name = "requests" optional = false @@ -710,7 +710,7 @@ python-versions = "*" version = "3.7.4.2" [[package]] -category = "main" +category = "dev" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false diff --git a/pyproject.toml b/pyproject.toml index 6aeb8da..202e535 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ documentation = "https://replit-python-docs.scoder12.repl.co" [tool.poetry.dependencies] python = "^3.8" -requests = "^2.24.0" typing_extensions = "^3.7.4" flask = "^1.1.2" werkzeug = "^1.0.1" diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 1d9e82c..cb1884d 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -3,14 +3,10 @@ import asyncio import json import os from sys import stderr -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, Tuple, Union import aiohttp -from . import _async - -asyncio.run = _async.run - JSON_TYPE = Union[str, int, float, bool, type(None), dict, list] @@ -63,7 +59,7 @@ class JSONKey: "expected {self.dtype.__name__}" ) - def get(self) -> JSON_TYPE: + async def get(self) -> JSON_TYPE: """Get the value of the key. If an invalid JSON value is read or the type does not match, it will show a @@ -73,32 +69,32 @@ class JSONKey: JSON_TYPE: The value read from the database """ try: - read = self.db[self.key] + read = await self.db.get(self.key) except KeyError: print(f"Database key {self.key} not set, setting it to default value") default = self._default() - self.db[self.key] = default + await self.db.set(self.key, default) return default try: data = json.loads(read) except json.JSONDecodeError: - return self._error("Invalid JSON data read", read) + return await self._error("Invalid JSON data read", read) if not self._is_valid_type(data): - return self._error(self._type_mismatch_msg(data), read,) + return await self._error(self._type_mismatch_msg(data), read,) return data - def _error(self, error: str, read: str) -> JSON_TYPE: + async def _error(self, error: str, read: str) -> JSON_TYPE: print(f"Error reading key {self.key!r}: {error}", file=stderr) if self.discard_bad_data: val = self._default() - self.db[self.key] = json.dumps(val) + await self.db.set(self.key, json.dumps(val)) print(f"Wrote default to key {self.key!r}") return val - return self._should_discard_prompt(error, read) + return await self._should_discard_prompt(error, read) - def _should_discard_prompt(self, error: str, read: str) -> bool: + async def _should_discard_prompt(self, error: str, read: str) -> bool: while True: choice = input( "d to use default, v to view the invalid data, c to insert custom " @@ -107,7 +103,7 @@ class JSONKey: if choice.startswith("d"): print("Writing default...") val = self._default() - self.db[self.key] = val + await self.db.set(self.key, val) return val elif choice.startswith("v"): print(f"Data read from key: {read!r}") @@ -127,11 +123,11 @@ class JSONKey: print(self._type_mismatch_msg(data)) continue - self.db[self.key] = toset + await self.db.set(self.key, toset) print("Wrote data to key") return data - def set(self, data: JSON_TYPE) -> None: + async def set(self, data: JSON_TYPE) -> None: """Set the value of the jsonkey. Args: @@ -142,11 +138,12 @@ class JSONKey: """ if not self._is_valid_type(data): raise TypeError(self._type_mismatch_msg(data)) - self.db[self.key] = json.dumps(data) + + await self.db.set(self.key, json.dumps(data)) -class ReplitDb(dict): - """Interface with the Replit Database.""" +class AsyncClient: + """Async client interface with the Replit Database.""" __slots__ = ("db_url", "sess") @@ -157,9 +154,8 @@ class ReplitDb(dict): db_url (str): Database url to use. """ self.db_url = db_url - self.sess = _AsyncBackend(db_url) - def __getitem__(self, key: str) -> str: + async def get(self, key: str) -> str: """Get the value of an item from the database. Args: @@ -171,39 +167,53 @@ class ReplitDb(dict): Returns: str: The value of the key """ - r = asyncio.run(self.sess.view(key)) - return r + async with aiohttp.ClientSession() as session: + async with session.get(self.db_url + "/" + key) as response: + if response.status == 404: + raise KeyError(key) + response.raise_for_status() + return await response.text() - def __setitem__(self, key: str, value: str) -> None: + async def set(self, key: str, value: str) -> None: """Set a key in the database to value. Args: key (str): The key to set value (str): The value to set it to """ - asyncio.run(self.sess.set(key, value)) + async with aiohttp.ClientSession() as session: + async with session.post(self.db_url, data={key: value}) as response: + response.raise_for_status() - def __delitem__(self, key: str) -> None: + async def delete(self, key: str) -> None: """Delete a key from the database. Args: key (str): The key to delete """ - asyncio.run(self.sess.delete(key)) + async with aiohttp.ClientSession() as session: + async with session.delete(self.db_url + "/" + key) as response: + response.raise_for_status() - def keys(self, prefix: str = "") -> Tuple[str]: - """Return all of the keys in the database. + async def list(self, prefix: str) -> Tuple[str]: + """List keys in the database which start with prefix. Args: - prefix (str): The prefix the keys must start with, - blank means anything. Defaults to "". + prefix (str): The prefix keys must start with, blank not not check. Returns: Tuple[str]: The keys found. """ - return asyncio.run(self.sess.list(prefix)) + async with aiohttp.ClientSession() as session: + async with session.get(self.db_url + "?prefix=" + prefix) as response: + response.raise_for_status() + text = await response.text() + if not text: + return tuple() + else: + return tuple(text.split("\n")) - def to_dict(self, prefix: str = "") -> Dict[str, str]: + async def to_dict(self, prefix: str = "") -> Dict[str, str]: """Dump all data in the database into a dictionary. Args: @@ -213,23 +223,22 @@ class ReplitDb(dict): Returns: Dict[str, str]: All keys in the database. """ - keys = self.keys() - data = {} - for k in keys: - data[k] = self[k] - return data + ret = {} + keys = await self.list(prefix=prefix) + for i in keys: + ret[i] = await self.view(i) + return ret - def values(self) -> Tuple[str]: + async def keys(self) -> Tuple[str]: + return tuple(await self.list("")) + + async def values(self) -> Tuple[str]: """Get every value in the database. Returns: Tuple[str]: The values in the database. """ - data = self.to_dict() - return tuple(data.values()) - - def items(self) -> Tuple[Tuple[str]]: - return self.to_dict().items() + return tuple((await self.to_dict()).values()) def jsonkey( self, @@ -272,69 +281,6 @@ class ReplitDb(dict): return f"" -class AsyncClient: - def __init__(self, db_url: str) -> None: - self.db_url = db_url - self.backend = _AsyncBackend(db_url) - - async def view(self, key: str) -> str: - return await self.backend.view(key) - - async def list(self, prefix: str) -> List[str]: - return await self.backend.list(prefix) - - async def delete(self, key: str) -> None: - return await self.backend.delete(key) - - async def set(self, key: str, val: str) -> None: - return await self.backend.set(key, val) - - async def to_dict(self) -> Dict[str, str]: - ret = {} - keys = await self.keys() - for i in keys: - ret[i] = await self.view(i) - return ret - - async def keys(self) -> Tuple[str]: - return tuple(await self.list("")) - - async def values(self) -> Tuple[str]: - return tuple((await self.to_dict()).values()) - - -class _AsyncBackend: - def __init__(self, db_url: str) -> None: - self.db_url = db_url - - async def set(self, key: str, val: str) -> None: - async with aiohttp.ClientSession() as session: - async with session.post(self.db_url, data={key: val}) as response: - response.raise_for_status() - - async def view(self, key: str) -> str: - async with aiohttp.ClientSession() as session: - async with session.get(self.db_url + "/" + key) as response: - if response.status == 404: - raise KeyError(key) - response.raise_for_status() - return await response.text() - - async def delete(self, key: str) -> None: - async with aiohttp.ClientSession() as session: - async with session.delete(self.db_url + "/" + key) as response: - response.raise_for_status() - - async def list(self, prefix: str) -> Tuple[str]: - async with aiohttp.ClientSession() as session: - async with session.get(self.db_url + "?prefix=" + prefix) as response: - response.raise_for_status() - if not await response.text(): - return tuple() - else: - return tuple((await response.text()).split("\n")) - - db_url = os.environ.get("REPLIT_DB_URL") if db_url: db = ReplitDb(db_url) From 9cf5c102ca2c04e7a61155e75ec4595dc8ad66db Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:30:26 -0700 Subject: [PATCH 03/10] Overhaul async and sync code Add new _async2sync function to copy async code into a sync class --- src/replit/database/__init__.py | 144 +++++++++++++++++++++++++++++--- src/replit/database/_async.py | 47 ----------- 2 files changed, 134 insertions(+), 57 deletions(-) delete mode 100644 src/replit/database/_async.py diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index cb1884d..15248ef 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -1,6 +1,8 @@ """Interface with the Replit Database.""" import asyncio +import functools import json +import inspect import os from sys import stderr from typing import Any, Callable, Dict, Tuple, Union @@ -11,8 +13,8 @@ import aiohttp JSON_TYPE = Union[str, int, float, bool, type(None), dict, list] -class JSONKey: - """Represents a key in the database that holds a JSON value. +class AsyncJSONKey: + """Represents an key in the async database that holds a JSON value. db.jsonkey() will initialize an instance for you, you don't have to do it manually. @@ -142,7 +144,7 @@ class JSONKey: await self.db.set(self.key, json.dumps(data)) -class AsyncClient: +class AsyncReplitDb: """Async client interface with the Replit Database.""" __slots__ = ("db_url", "sess") @@ -230,6 +232,11 @@ class AsyncClient: return ret async def keys(self) -> Tuple[str]: + """Get all keys in the database. + + Returns: + Tuple[str]: The keys in the database. + """ return tuple(await self.list("")) async def values(self) -> Tuple[str]: @@ -240,6 +247,128 @@ class AsyncClient: """ return tuple((await self.to_dict()).values()) + def jsonkey( + self, + key: str, + dtype: JSON_TYPE, + get_default: Callable = None, + discard_bad_data: bool = False, + ) -> AsyncJSONKey: + """Initialize an AsyncJSONKey instance. + + A AsyncJSONKey is used to easily read and set JSON data from the database. + Arguments are the same as AsyncJSONKey constructor. + + Args: + key (str): The key to read + dtype (JSON_TYPE): The datatype the key should be, can be typing.Any. + get_default (Callable): A function that returns the default + value if the key is not set. If it is None (the default) the dtype + argument is used. + discard_bad_data (bool): Don't prompt if bad data is read, overwrite it + with the default. Defaults to False. + + Returns: + AsyncJSONKey: The initialized AsyncJSONKey instance. + """ + return AsyncJSONKey( + db=self, + key=key, + dtype=dtype, + get_default=get_default, + discard_bad_data=discard_bad_data, + ) + + def __repr__(self) -> str: + """A representation of the database. + + Returns: + A string representation of the database object. + """ + return f"" + + +def _async2sync(src_cls: object, res_cls: object) -> None: + class ReprWrapped(object): + def __init__(self, func: Callable) -> None: + self.func = func + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self.func(*args, **kwargs) + + def __repr__(self) -> str: + return ( + f"" + ) + + for attr in dir(src_cls): + if attr in ["__class__", "__dict__"]: + continue + val = getattr(src_cls, attr) + if inspect.iscoroutinefunction(val): + # Convert the async function to sync with asyncio.run + @functools.wraps(val) + def sync_func(*args: Any, **kwargs: Any) -> Any: + print(f"Calling {attr}: {val}") + return asyncio.run(val(*args, **kwargs)) + + setattr(res_cls, attr, ReprWrapped(sync_func)) + elif inspect.isfunction(val): + # Wrap the source function + @functools.wraps(val) + def new_func(*args: Any, **kwargs: Any) -> Any: + return val(*args, **kwargs) + + setattr(res_cls, attr, ReprWrapped(new_func)) + else: + setattr(res_cls, attr, val) + + +class JSONKey: + """Represents an key in the async database that holds a JSON value. + + db.jsonkey() will initialize an instance for you, + you don't have to do it manually. + """ + + pass + + +_async2sync(AsyncJSONKey, JSONKey) + + +class ReplitDb: + """Client interface with the Replit Database.""" + + def __getitem__(self, item: str) -> str: + """Retrieve a key from the database. + + Args: + item (str): The key to retrieve. + + Returns: + str: The value of the key. + """ + return self.get(item) + + def __setitem__(self, item: str, value: str) -> None: + """Set a key in the database. + + Args: + item (str): The key to set. + value (str): The value to set the key to. + """ + self.set(item, value) + + def __delitem__(self, name: str) -> None: + """Delete a key in the database. + + Args: + name (str): The key to delete. + """ + self.delete(name) + def jsonkey( self, key: str, @@ -247,7 +376,7 @@ class AsyncClient: get_default: Callable = None, discard_bad_data: bool = False, ) -> JSONKey: - """Initialize a JSONKey instance. + """Initialize an JSONKey instance. A JSONKey is used to easily read and set JSON data from the database. Arguments are the same as JSONKey constructor. @@ -272,13 +401,8 @@ class AsyncClient: discard_bad_data=discard_bad_data, ) - def __repr__(self) -> str: - """A representation of the database. - Returns: - A string representation of the database object. - """ - return f"" +_async2sync(AsyncReplitDb, ReplitDb) db_url = os.environ.get("REPLIT_DB_URL") diff --git a/src/replit/database/_async.py b/src/replit/database/_async.py deleted file mode 100644 index 08af0ab..0000000 --- a/src/replit/database/_async.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Allows asyncio.run to work in async environment.""" -import asyncio -import sys -import threading -from typing import Any, Callable, List - -asyncio._run = asyncio.run - - -class AsyncThread(threading.Thread): - def __init__(self, res: List[None], exc: List[None], func: Callable) -> None: - self.result = res - self.exc = exc - self.func = func - threading.Thread.__init__(self) - - def run(self) -> Any: - def inner(func: Callable) -> Any: - try: - res = asyncio.run(func) - except Exception: - self.exc[0] = sys.exc_info() - res = "" - return res - - self.result[0] = inner(self.func) - - -def run(func: Callable) -> Any: - def error() -> Any: - ret = [None] - exc = [None] - thread = AsyncThread(ret, exc, func) - thread.start() - thread.join() - exc = exc[0] - if exc is not None: - raise exc[1].with_traceback(exc[2]) - sys.exit(1) - return ret[0] - - try: - return asyncio._run(func) - except RuntimeError: - return error() - except RuntimeWarning: - return error() From b60a027225c1f88df0f1145d47251a8748befdf8 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:44:56 -0700 Subject: [PATCH 04/10] Switch regular string to fstring, reorder imports --- src/replit/database/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 15248ef..bcfe7df 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -1,8 +1,8 @@ """Interface with the Replit Database.""" import asyncio import functools -import json import inspect +import json import os from sys import stderr from typing import Any, Callable, Dict, Tuple, Union @@ -299,7 +299,7 @@ def _async2sync(src_cls: object, res_cls: object) -> None: def __repr__(self) -> str: return ( f"" + f" at {hex(id(self.func))}>" ) for attr in dir(src_cls): From 75f09b90f5f09a12f8b6823bd5072ec97c9f8f80 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 14:12:49 -0700 Subject: [PATCH 05/10] Add async conversions for all coroutines --- src/replit/database/__init__.py | 59 ++++++++++----------------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index bcfe7df..a7f7dc0 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -1,7 +1,6 @@ """Interface with the Replit Database.""" import asyncio import functools -import inspect import json import os from sys import stderr @@ -288,54 +287,25 @@ class AsyncReplitDb: return f"" -def _async2sync(src_cls: object, res_cls: object) -> None: - class ReprWrapped(object): - def __init__(self, func: Callable) -> None: - self.func = func +def _async2sync(coro: Callable) -> None: + @functools.wraps(coro) + def sync_func(self: object, *args: Any, **kwargs: Any) -> Any: + return asyncio.run(coro(self, *args, **kwargs)) - def __call__(self, *args: Any, **kwargs: Any) -> Any: - return self.func(*args, **kwargs) - - def __repr__(self) -> str: - return ( - f"" - ) - - for attr in dir(src_cls): - if attr in ["__class__", "__dict__"]: - continue - val = getattr(src_cls, attr) - if inspect.iscoroutinefunction(val): - # Convert the async function to sync with asyncio.run - @functools.wraps(val) - def sync_func(*args: Any, **kwargs: Any) -> Any: - print(f"Calling {attr}: {val}") - return asyncio.run(val(*args, **kwargs)) - - setattr(res_cls, attr, ReprWrapped(sync_func)) - elif inspect.isfunction(val): - # Wrap the source function - @functools.wraps(val) - def new_func(*args: Any, **kwargs: Any) -> Any: - return val(*args, **kwargs) - - setattr(res_cls, attr, ReprWrapped(new_func)) - else: - setattr(res_cls, attr, val) + return sync_func -class JSONKey: +class JSONKey(AsyncJSONKey): """Represents an key in the async database that holds a JSON value. db.jsonkey() will initialize an instance for you, you don't have to do it manually. """ - pass - - -_async2sync(AsyncJSONKey, JSONKey) + get = _async2sync(AsyncJSONKey.get) + set = _async2sync(AsyncJSONKey.set) + _error = _async2sync(AsyncJSONKey._error) + _should_discard_prompt = _async2sync(AsyncJSONKey._should_discard_prompt) class ReplitDb: @@ -401,8 +371,13 @@ class ReplitDb: discard_bad_data=discard_bad_data, ) - -_async2sync(AsyncReplitDb, ReplitDb) + get = _async2sync(AsyncReplitDb.get) + set = _async2sync(AsyncReplitDb.set) + delete = _async2sync(AsyncReplitDb.delete) + list = _async2sync(AsyncReplitDb.list) + keys = _async2sync(AsyncReplitDb.keys) + to_dict = _async2sync(AsyncReplitDb.to_dict) + values = _async2sync(AsyncReplitDb.values) db_url = os.environ.get("REPLIT_DB_URL") From 69832079d6a6e6104a832f7f2a86450ebf6f98c8 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 15:31:46 -0700 Subject: [PATCH 06/10] Make ReplitDb a subclass of AsyncReplitDb --- src/replit/database/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index a7f7dc0..9b581c2 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -308,7 +308,7 @@ class JSONKey(AsyncJSONKey): _should_discard_prompt = _async2sync(AsyncJSONKey._should_discard_prompt) -class ReplitDb: +class ReplitDb(AsyncReplitDb): """Client interface with the Replit Database.""" def __getitem__(self, item: str) -> str: From 2133d52354d32ca573917682231a5d89ffae3ce4 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 15:43:13 -0700 Subject: [PATCH 07/10] Update poetry --- poetry.lock | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5c6b905..f04b579 100644 --- a/poetry.lock +++ b/poetry.lock @@ -116,9 +116,9 @@ version = "1.6.2" [package.dependencies] GitPython = ">=1.0.1" PyYAML = ">=3.13" -colorama = ">=0.3.9" six = ">=1.10.0" stevedore = ">=1.20.0" +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} [[package]] category = "dev" @@ -167,7 +167,6 @@ version = "7.1.2" [[package]] category = "dev" description = "Cross-platform colored terminal text." -marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" name = "colorama" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -273,7 +272,6 @@ version = "0.18.1" [package.dependencies] pycodestyle = "*" -setuptools = "*" [[package]] category = "dev" @@ -555,12 +553,10 @@ Jinja2 = ">=2.3" Pygments = ">=2.0" alabaster = ">=0.7,<0.8" babel = ">=1.3" -colorama = ">=0.3.5" docutils = ">=0.12" imagesize = "*" packaging = "*" requests = ">=2.5.0" -setuptools = "*" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -568,6 +564,7 @@ sphinxcontrib-htmlhelp = "*" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -747,7 +744,7 @@ idna = ">=2.0" multidict = ">=4.0" [metadata] -content-hash = "e4e37adaadf6628a7aeac64abcb6c944266a6565fe108ff29eb72e816edf6f33" +content-hash = "4044c6cf55441e98661abd6202db3abad444c2e338ab732a4dbc38514a2031b6" python-versions = "^3.8" [metadata.files] From ab79581905e34265dd546c1fd20d915b2e25b991 Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 15:48:37 -0700 Subject: [PATCH 08/10] Use nest_asyncio to fix async to sync function --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + src/replit/database/__init__.py | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index f04b579..310286e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -395,6 +395,14 @@ optional = false python-versions = ">=3.5" version = "4.7.6" +[[package]] +category = "main" +description = "Patch asyncio to allow nested event loops" +name = "nest-asyncio" +optional = false +python-versions = ">=3.5" +version = "1.4.0" + [[package]] category = "dev" description = "Core utilities for Python packages" @@ -744,7 +752,7 @@ idna = ">=2.0" multidict = ">=4.0" [metadata] -content-hash = "4044c6cf55441e98661abd6202db3abad444c2e338ab732a4dbc38514a2031b6" +content-hash = "c92c18a07248bbd0df18fba9327c2387af07b06a9a489123e3276810fb4e89fc" python-versions = "^3.8" [metadata.files] @@ -945,6 +953,10 @@ multidict = [ {file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"}, {file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"}, ] +nest-asyncio = [ + {file = "nest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:ea51120725212ef02e5870dd77fc67ba7343fc945e3b9a7ff93384436e043b6a"}, + {file = "nest_asyncio-1.4.0.tar.gz", hash = "sha256:5773054bbc14579b000236f85bc01ecced7ffd045ec8ca4a9809371ec65a59c8"}, +] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, diff --git a/pyproject.toml b/pyproject.toml index 202e535..a7ba542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ flask = "^1.1.2" werkzeug = "^1.0.1" asks = "^2.4.8" aiohttp = "^3.6.2" +nest_asyncio = "^1.4.0" [tool.poetry.dev-dependencies] flake8 = "^3.8.3" diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 9b581c2..36f1cbd 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -7,6 +7,7 @@ from sys import stderr from typing import Any, Callable, Dict, Tuple, Union import aiohttp +import nest_asyncio JSON_TYPE = Union[str, int, float, bool, type(None), dict, list] @@ -380,6 +381,7 @@ class ReplitDb(AsyncReplitDb): values = _async2sync(AsyncReplitDb.values) +nest_asyncio.apply() db_url = os.environ.get("REPLIT_DB_URL") if db_url: db = ReplitDb(db_url) From 7d6f45be6c3c7ef7043939b3b3dde47e43e47cbb Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 15:51:35 -0700 Subject: [PATCH 09/10] Pass async db to async to synced JSONKey which expects it --- src/replit/database/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 36f1cbd..736247f 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -365,7 +365,7 @@ class ReplitDb(AsyncReplitDb): JSONKey: The initialized JSONKey instance. """ return JSONKey( - db=self, + db=super(), key=key, dtype=dtype, get_default=get_default, From ea8301205e908c40a52790557f885181f94e795d Mon Sep 17 00:00:00 2001 From: Scoder12 <34356756+Scoder12@users.noreply.github.com> Date: Wed, 5 Aug 2020 15:58:16 -0700 Subject: [PATCH 10/10] Don't unasync JSONKey internal methods because they are awaited --- src/replit/database/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 736247f..69174f4 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -305,8 +305,6 @@ class JSONKey(AsyncJSONKey): get = _async2sync(AsyncJSONKey.get) set = _async2sync(AsyncJSONKey.set) - _error = _async2sync(AsyncJSONKey._error) - _should_discard_prompt = _async2sync(AsyncJSONKey._should_discard_prompt) class ReplitDb(AsyncReplitDb):