diff --git a/.github/workflows/docker-master.yaml b/.github/workflows/docker-master.yaml
index 22e31c2..ea0f0ba 100644
--- a/.github/workflows/docker-master.yaml
+++ b/.github/workflows/docker-master.yaml
@@ -2,15 +2,17 @@ name: CI
on:
push:
branches: [master]
+ paths: [rath/**, tests/**]
pull_request:
branches: [master]
+ paths: [rath/**, tests/**]
jobs:
mac_and_windows:
strategy:
fail-fast: false
matrix:
- python-version: [3.7, 3.8, 3.9]
- poetry-version: [1.2]
+ python-version: [3.8, 3.9, "3.10", "3.11"]
+ poetry-version: [1.7]
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -18,44 +20,41 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
+ - uses: tlambert03/setup-qt-libs@v1
- name: Run image
uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Run Poetry Install
run: poetry install
+ - name: Install Qt because Qt sucks with Poetry
+ run: poetry run pip install pyqt5
- name: Run Tests
- run: poetry run pytest --cov --cov-report=xml .
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1
- linux:
+ run: poetry run pytest --cov --cov-report=xml -k "not process"
+ linux_integration:
strategy:
fail-fast: false
matrix:
- python-version: [3.7, 3.8, 3.9]
- poetry-version: [1.2]
+ python-version: [3.8, 3.9, "3.10", "3.11"]
+ poetry-version: [1.7]
os: [ubuntu-latest]
+ env:
+ DISPLAY: ':99.0'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- env:
- DISPLAY: ":99.0"
- run: |
- sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0
- - name: Run image
- uses: abatilo/actions-poetry@v2.0.0
+ - uses: tlambert03/setup-qt-libs@v1
+ - uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Run Poetry Install
run: poetry install
+ - name: Install Qt because Qt sucks with Poetry
+ run: poetry run pip install pyqt5
- name: Run Tests
- env:
- QT_DEBUG_PLUGINS: 1
- DISPLAY: ":99.0"
- run: xvfb-run `which poetry` run pytest --cov --cov-report=xml .
+ run: poetry run pytest --cov --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml
new file mode 100644
index 0000000..231b0b9
--- /dev/null
+++ b/.github/workflows/quality.yaml
@@ -0,0 +1,83 @@
+name: Python Code Quality and Style Check
+on:
+ push:
+ branches: [master]
+ paths: [koil/**, tests/**]
+ pull_request:
+ branches: [master]
+ paths: [koil/**, tests/**]
+jobs:
+ ruff:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11"]
+ poetry-version: [1.7]
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - uses: tlambert03/setup-qt-libs@v1
+ - name: Run image
+ uses: abatilo/actions-poetry@v2.0.0
+ with:
+ poetry-version: ${{ matrix.poetry-version }}
+ - name: Run Poetry Install
+ run: poetry install --all-extras
+ - name: Install Qt because Qt sucks with Poetry
+ run: poetry run pip install pyqt5 pyqtwebengine
+ - name: Run Ruff
+ run: poetry run ruff .
+ mypy:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11"]
+ poetry-version: [1.7]
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - uses: tlambert03/setup-qt-libs@v1
+ - name: Run image
+ uses: abatilo/actions-poetry@v2.0.0
+ with:
+ poetry-version: ${{ matrix.poetry-version }}
+ - name: Run Poetry Install
+ run: poetry install --all-extras
+ - name: Install Qt because Qt sucks with Poetry
+ run: poetry run pip install pyqt5 pyqtwebengine
+ - name: Run Mypy
+ run: poetry run mypy .
+ black:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11"]
+ poetry-version: [1.4]
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - uses: tlambert03/setup-qt-libs@v1
+ - name: Run image
+ uses: abatilo/actions-poetry@v2.0.0
+ with:
+ poetry-version: ${{ matrix.poetry-version }}
+ - name: Run Poetry Install
+ run: poetry install --all-extras
+ - name: Install Qt because Qt sucks with Poetry
+ run: poetry run pip install pyqt5 pyqtwebengine
+ - name: Run black
+ run: poetry run black --check .
+
\ No newline at end of file
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 87983e6..7d012c9 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -17,7 +17,7 @@ jobs:
- name: Run image
uses: abatilo/actions-poetry@v2.0.0
with:
- poetry-version: "1.1.4"
+ poetry-version: "1.4"
- name: Run Poetry Install
run: poetry install
- name: Publish
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f012532..7fb39c7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -35,4 +35,8 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.black-formatter"
+ },
+ "python.formatting.provider": "none",
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 7c24b31..7156483 100644
--- a/README.md
+++ b/README.md
@@ -7,24 +7,28 @@
[](https://pypi.python.org/pypi/koil/)
[](https://pypi.python.org/pypi/koil/)
-### DEVELOPMENT
-
-# Quick Start
-
-Let's discover **Koil in less than 5 minutes**.
### Inspiration
koil is an abstraction layer for threaded asyncio to enable "sensible defaults" for
programmers working with frameworks that are barely compatible with asyncio (originally developped to get around pyqt5)
+### Installation
+
+```bash
+pip install koil
+```
+
+
### Main Concept
Async libraries are amazing, and its an ecosystem rapidly increasing, however in some contexts it still doesn't seem like
the way to go and the burden of learning these concepts might be to high. However you developed a wonderful async api
-that you want to share with the world.
+that you want to share with the world. Of couse because you care about cleaning up your resources you made it a context.
```python
+
+
class AmazingAsyncAPI:
def __init__(self) -> None:
pass
@@ -33,6 +37,11 @@ class AmazingAsyncAPI:
await asyncio.sleep(0.01)
return "the-glory-of-async"
+ async def yielding_sleeper(self):
+ for i in range(0, 20):
+ await asyncio.sleep(0.01)
+ yield i
+
async def __aenter__(self):
# amazing connection logic
return self
@@ -43,21 +52,48 @@ class AmazingAsyncAPI:
```
-However if somebody wants to use this api in sync environment they are in for a good one, as a call to asyncio.run() just wont do the trick.
+However if somebody wants to use this api in sync environment they are in for a good one, as a call to asyncio.run() just won't do the trick.
+And you will have to write a lot of boilerplate code to make it work. And once you start trying to use the yielding_sleeper
+you will be in for a good one.
+
+```python
+
+async def the_annoying_wrapper():
+
+ async with AmazingAsyncAPI() as e:
+ print(await e.sleep()) # easy enough
+
+ # How do I use the output of the yielding
+ # sleeper? Queues? Another thread?
+
+
+asyncio.run(the_annoying_wrapper())
+
+```
+
+Well koil is here to help. Just mark your class with koilable and the functions that you want to be able to call from
+a sync context with unkoilable.
+
```python
from koil import koilable, unkoilable
-@koilable()
+@koilable
class AmazingAsyncAPI:
def __init__(self) -> None:
pass
- @unkoilable()
+ @unkoilable
async def sleep(self):
await asyncio.sleep(0.01)
return "the-glory-of-async"
+ @unkoilable
+ async def yielding_sleeper(self):
+ for i in range(0, 20):
+ await asyncio.sleep(0.01)
+ yield i
+
async def __aenter__(self):
# amazing connection logic
return self
@@ -73,13 +109,27 @@ And now it works. Just use your Api with a normal context manager.
```python
with AmazingAsyncAPI as e:
print(e.sleep())
-```
+
+ for i in e.yielding_sleeper():
+ print(i)
+
+# Context manager is closed and cleaned up
+```>
+
+
+## How does it work?
Koil under the hood spawns a new event loop in another thread, calls functions that are marked with unkoilable
threadsafe in that loop and returns the result, when exiting it shuts down the loop in the other thread.
-If you have multiple context managers or tasks that you would just like to run in another thread, you can
-also create a loop in another thread
+### Other usages
+
+If you have multiple context managers or tasks that you would just like to run in another thread, we do *not*
+spawn a new thread for each of them, but rather use the same thread for all of them. This is to avoid the overhead
+of spawning a new thread for each context manager. On the asyncio side, all tasks will be in the same loop, so
+you can use asyncio primitives to communicate between them.
+
+You can also just use Koil as a threaded event loop, and use the unkoil function to run functions in that loop.
```python
@@ -96,49 +146,110 @@ with Koil(): # creates a threaded loop
```
-Moreover koil also is able to be used with generators
+Importantly, Koil also provides primites to run sync functions (in another thread, and experimentally in another process)
+and await them in the koil loop. This is useful if you have a sync function that you want to use in an async context.
```python
-import asyncio
-from koil import unkoil_gen
+from koil.helpers import run_spawned, iterate_spawned
+import time
-async def task(arg):
- for i in range(0,20)
- await asyncio.sleep(1)
- yield arg
+def sync_function(arg):
+ return arg
+def sync_generator(arg):
+ for i in range(0, arg):
+ time.sleep(1)
+ yield i
-with Koil(): # creates a threaded loop
+async def run_async():
+ x = await run_spawned(sync_function, 1)
+
+ async for i in iterate_spawned(sync_generator, 1):
+ print(i)
- for x in unkoil_gen(task, 1):
- print(x)
+ return x
+
+with Koil():
+ x = unkoil(run_async)
```
-And finally koil is able to create task like objects,
+THis allows you to use async primitives to communicate with sync functions. Note that this is not a good idea
+if you have a lot of sync functions, as the overhead of spawning a new thread for each of them is quite high.
+You can however pass a threadpool to the run_spawned function to avoid this overhead.
-```python
-async def task(arg):
- await asyncio.sleep(2)
- return arg
-with Koil():
+## Task Support
- x = unkoil(task, 1, as_task=True)
+Sometimes you want to run a task in the background and just get the result when you need it. Can't Koil do that?
+Well it could, but we belive this is a bad idea. You should have that in the async world. However you can
+browser our code and use our deprecated functions to do that. We just don't think its a good idea.
- # do other stuff
+## PyQt Support
- if x.done():
- print(x)
+One of the main reasons for koil was to get around the fact that PyQt5 is not asyncio compatible, and all the### Installation
+```bash
+pip install koil
```
-## PyQt Support
-... Documentation coming soon...
+```python
+from koil.qt import create_qt_koil, koilqt, unkoilqt
+
+class KoiledInterferingFutureWidget(QtWidgets.QWidget):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.koil = create_qt_koil(parent=self)
+ # important to set parent, helps with cleanup
+ # once the widget is destroyed. Loop starts automatically
-### Installation
+ self.ado_me = koilqt(self.in_qt_task, autoresolve=True)
+ # We can make qt functions callable from the async loop
+
+ self.loop_runner = unkoilqt(self.ado_stuff_in_loop)
+ self.loop_runner.returned.connect(self.task_finished)
+
+ self.call_task_button = QtWidgets.QPushButton("Call Task")
+ self.greet_label = QtWidgets.QLabel("")
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.call_task_button)
+ layout.addWidget(self.greet_label)
+
+ self.setLayout(layout)
+
+ self.call_task_button.clicked.connect(self.my_coro_task.run)
+
+ def in_qt_task(self, future: QtFuture):
+ """This is a task that is run in the Qt Main Thread"""
+ # We can do stuff in the Qt Thread
+ # We can resolve the future at any time
+ # self.on_going_future = future
+
+ future.resolve("called")
+
+ def task_finished(self):
+ """This is called when the task is finished. Main thread"""
+ self.greet_label.setText("Hello!")
+
+ async def ado_stuff_in_loop(self):
+ """This is a coroutine that is run in the threaded loop"""
+ x = await self.ado_me()
+ self.coroutine_was_run = True
+
+
+def main():
+ app = QtWidgets.QApplication(sys.argv)
+ widget = KoiledInterferingFutureWidget()
+ widget.show()
+ sys.exit(app.exec_())
+
+if __name__ == "__main__":
+ main()
-```bash
-pip install koil
```
+
+
+
+
diff --git a/koil/composition/base.py b/koil/composition/base.py
index a7f96fa..d960381 100644
--- a/koil/composition/base.py
+++ b/koil/composition/base.py
@@ -1,11 +1,7 @@
-import asyncio
-from pydantic import BaseModel, Field, root_validator
-from dataclasses import field
+from pydantic import BaseModel, Field
from koil.decorators import koilable
-from typing import Optional, TypeVar
-from koil.vars import *
-from koil.errors import *
-from koil.koil import *
+from typing import Optional, TypeVar, Any
+from koil.koil import KoilMixin
T = TypeVar("T")
@@ -22,24 +18,6 @@ class PedanticKoil(BaseModel, KoilMixin):
_token = None
_loop = None
- @root_validator()
- @classmethod
- def check_not_running_in_koil(cls, values):
- if current_loop.get() is not None:
- raise ValueError(
- f"You are already running in a Koil Loop. You cannot run a Koil Loop inside another Koil Loop. {current_loop.get()}"
- )
- try:
- asyncio.get_running_loop()
- if not values["sync_in_async"]:
- raise ValueError(
- "Please use async instead. Or set Koil to sync_in_async=True"
- )
- except RuntimeError:
- pass
-
- return values
-
def _repr_html_inline_(self):
return f"
allow sync in async | {self.sync_in_async} |
uvified | {self.uvify} |
"
@@ -50,7 +28,7 @@ class Config:
@koilable(fieldname="koil", add_connectors=True, koil_class=PedanticKoil)
class KoiledModel(BaseModel):
- koil: Optional[PedanticKoil]
+ koil: PedanticKoil = Field(default_factory=PedanticKoil, exclude=True)
def __enter__(self: T) -> T:
...
@@ -82,8 +60,6 @@ class Config:
copy_on_model_validation = "none"
-
-
class Composition(KoiledModel):
async def __aenter__(self: T) -> T:
for key, value in self:
@@ -106,5 +82,3 @@ def _repr_html_(self):
+ "\n".join(["{} |
".format(key) for key, value in self])
+ ""
)
-
-
diff --git a/koil/composition/qt.py b/koil/composition/qt.py
index ddddffb..b5b76f5 100644
--- a/koil/composition/qt.py
+++ b/koil/composition/qt.py
@@ -1,8 +1,7 @@
-from pydantic import Field
from koil.composition.base import PedanticKoil
from typing import Optional
-from koil.qt import QtFuture, QtKoilMixin, QtRunner
+from koil.qt import QtKoilMixin
from qtpy import QtWidgets, QtCore
import logging
@@ -11,19 +10,8 @@
class QtPedanticKoil(PedanticKoil, QtKoilMixin):
- parent: Optional[QtWidgets.QWidget] = Field(exclude=True)
-
- _qobject: QtCore.QObject = None
-
- def create_task(self, coro, *args, **kwargs) -> QtFuture:
- logger.warning(
- """Creating a task within the qt loop is not recommended. As this might lead to deathlocks and other bad things. (Especially when calling qt back from the koil). Try to use a `create_runner`
- and connect your signals to and then call the `run` method instead."""
- )
- return coro(*args, **kwargs, as_task=True).run()
-
- def create_runner(self, coro, *args, **kwargs) -> QtRunner:
- return coro(*args, **kwargs, as_task=True)
+ parent: Optional[QtWidgets.QWidget] = None
+ _qobject: Optional[QtCore.QObject] = None
class Config:
underscore_attrs_are_private = True
diff --git a/koil/context.py b/koil/context.py
deleted file mode 100644
index 2fc7631..0000000
--- a/koil/context.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from contextvars import ContextVar
-
-
-
-
-class Context:
- def __init__(self, value):
- self.__value: ContextVar = value
-
- def set(self, value):
- return self.__value.set(value)
-
- def reset(self, token):
- self.__value.reset(token)
- return
-
- @classmethod
- def __get_validators__(cls):
- # one or more validators may be yielded which will be called in the
- # order to validate the input, deach validator will receive as an input
- # the value returned from the previous validator
- yield cls.validate
-
- @classmethod
- def validate(cls, v):
- if isinstance(v, ContextVar):
- return cls(v)
-
- raise TypeError("Needs to be either a instance of ContextVar or a string")
diff --git a/koil/contrib/__init__.py b/koil/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/koil/contrib/pytest.py b/koil/contrib/pytest.py
new file mode 100644
index 0000000..e69de29
diff --git a/koil/contrib/pytest_qt.py b/koil/contrib/pytest_qt.py
new file mode 100644
index 0000000..44b45ce
--- /dev/null
+++ b/koil/contrib/pytest_qt.py
@@ -0,0 +1,58 @@
+from typing import Callable
+from koil.qt import QtRunner
+from pytestqt.qtbot import QtBot
+
+
+def wait_for_qttask(qtbot: QtBot, task: QtRunner, cause: Callable[[QtBot], None]):
+ """A helper function that waits for a QtRunner task to finish.
+
+ This function will utilize the pytest-qt qtbot fixture to wait for a QtRunner
+ task to finish.
+ It will return the value of the task, or raise an exception if the task errored.
+
+ Parameters
+ ----------
+ qtbot : QtBot
+ The qtbot fixture from pytest-qt
+ task : QtRunner
+ The task to wait for
+ cause : Callable[[QtBot], None]
+ A function that will cause the task to run
+
+ Raises
+ ------
+ Exception
+ The exception that the task raised
+
+ Returns
+ -------
+ Any
+ The value that the task returned
+
+
+
+ """
+ out = ("MAGIC_NOTHINGNESS_WORD",)
+
+ def callback(*value):
+ nonlocal out
+ out = value
+
+ task.returned.connect(callback)
+ task.errored.connect(callback)
+
+ with qtbot.waitSignal(task.returned) as blocker:
+ blocker.connect(task.errored)
+ cause(qtbot)
+
+ if len(out) == 0:
+ raise RuntimeError("Task did not return a value.")
+
+ if isinstance(out[0], Exception):
+ raise out[0]
+ elif out[0] == "MAGIC_NOTHINGNESS_WORD":
+ raise RuntimeError("Task did not return a value.")
+ else:
+ if len(out) == 1:
+ return out[0]
+ return out
diff --git a/koil/decorators.py b/koil/decorators.py
index edf284a..d7aac97 100644
--- a/koil/decorators.py
+++ b/koil/decorators.py
@@ -1,9 +1,11 @@
-from koil.helpers import unkoil
+from koil.helpers import unkoil, unkoil_gen
from koil.koil import Koil
import inspect
from typing import Callable, Type, TypeVar
from koil.vars import current_loop
+from koil.errors import KoilError
import logging
+from .utils import check_is_asyncfunc, check_is_asyncgen
T = TypeVar("T")
@@ -14,16 +16,22 @@
def koilable(
fieldname: str = "__koil",
add_connectors: bool = False,
+ init_koil: bool = True,
koil_class: Type[Koil] = Koil,
**koilparams,
) -> Callable[[Type[T]], Type[T]]:
"""
Decorator to make an async generator koilable.
+ Args:
+ fieldname (str, optional): The name of the field to store the koil instance. Defaults to "__koil".
+ add_connectors (bool, optional): If True, it will add the connectors to the class. Defaults to False.
+ init_koil (bool, optional): If True, it will initialize the koil instance on the field if it is None. Defaults to True.
+ koil_class (Type[Koil], optional): The class of the koil to use. Defaults to Koil.
+
"""
def real_cls_decorator(cls: Type[T]) -> Type[T]:
-
cls.__koilable__ = True
assert hasattr(cls, "__aenter__"), "Class must implement __aenter__"
assert hasattr(cls, "__aenter__"), "Class must implement __aexit__"
@@ -34,25 +42,26 @@ def real_cls_decorator(cls: Type[T]) -> Type[T]:
cls.__aexit__
), "__aexit__ must be a coroutine (awaitable)"
- def ___get_koiled_loop(self):
- return getattr(self, fieldname)
-
def koiled_enter(self, *args, **kwargs):
potential_koiled_loop = current_loop.get()
if potential_koiled_loop is not None:
- assert (
- getattr(self, fieldname, None) is None
- ), f"You cannot enter a koil loop inside another koil loop. Do no set explicitly a Koil in {cls}. Found koiled loop {potential_koiled_loop}"
+ # We are in a koiled loop no need to koil again
+ return unkoil(self.__aenter__, *args, **kwargs)
else:
- if getattr(self, fieldname, None) is None:
- setattr(
- self,
- fieldname,
- koil_class(creating_instance=self, **koilparams),
- )
- getattr(self, fieldname).__enter__()
+ if not hasattr(self, fieldname) or getattr(self, fieldname) is None:
+ if init_koil:
+ setattr(
+ self,
+ fieldname,
+ koil_class(name=f"{repr(self)}", **koilparams),
+ )
+ else:
+ raise KoilError(
+ f"Does not have a koil instance on {fieldname} and init_koil is False"
+ )
- return unkoil(self.__aenter__, *args, **kwargs)
+ getattr(self, fieldname).__enter__()
+ return unkoil(self.__aenter__, *args, **kwargs)
def koiled_exit(self, *args, **kwargs):
unkoil(self.__aexit__, *args, **kwargs)
@@ -73,7 +82,6 @@ async def aenter(self):
cls.aenter = aenter
cls.exit = koiled_exit
cls.enter = koiled_enter
- cls.__get_koiled_loop = ___get_koiled_loop
return cls
@@ -81,7 +89,29 @@ async def aenter(self):
def unkoilable(func):
- def wrapper(*args, **kwargs):
- return unkoil(func, *args, **kwargs)
+ """Decorator to make a function unkoilable
+
+ Args:
+ func (Callable): The function to run in the koil loop
+
+ Raises:
+ TypeError: If the function is not a coroutine
+
+ """
+
+ if not check_is_asyncgen(func) and not check_is_asyncfunc(func):
+ raise TypeError("Function must be a coroutine")
+
+ if check_is_asyncgen(func):
+
+ def wrapper(*args, **kwargs):
+ return unkoil_gen(func, *args, **kwargs)
+
+ return wrapper
+
+ else:
+
+ def wrapper(*args, **kwargs):
+ return unkoil(func, *args, **kwargs)
- return wrapper
+ return wrapper
diff --git a/koil/errors.py b/koil/errors.py
index 50916e4..0eef742 100644
--- a/koil/errors.py
+++ b/koil/errors.py
@@ -6,7 +6,6 @@ class KoilStopIteration(Exception):
"""Wrapper around StopIteration as it interacts badly with Futures"""
-
class ContextError(KoilError):
pass
@@ -16,8 +15,11 @@ class ThreadCancelledError(KoilError):
be raised from within a thread that was spawned by koil."""
+class ProcessCancelledError(KoilError):
+ """Mimics ayncio.CancelledError for process if they accept cancellation. This should only
+ be raised from within a thread that was spawned by koil."""
+
class CancelledError(KoilError):
"""Mimics ayncio.CancelledError for futures that are living in a koiled loop. Catch this
as if you would catch asyncio.CancelledError."""
-
diff --git a/koil/helpers.py b/koil/helpers.py
index 6eaf45f..518c573 100644
--- a/koil/helpers.py
+++ b/koil/helpers.py
@@ -1,6 +1,6 @@
import asyncio
import threading
-
+from .process import KoiledProcess
import janus
from koil.errors import (
KoilError,
@@ -9,12 +9,37 @@
)
from koil.task import KoilFuture, KoilRunner
from koil.utils import run_threaded_with_context
-from koil.vars import *
+from koil.vars import (
+ current_loop,
+ current_cancel_event,
+)
+import contextvars
import time
import logging
+from .process import unkoil_process_gen, unkoil_process_func, is_in_process
+from typing import Callable, TypeVar
+
+try:
+ from typing import ParamSpec
+except ImportError:
+ from typing_extensions import ParamSpec
+
+
+from typing import Coroutine, Any, Union, Awaitable, AsyncIterator, Iterator
+
+P = ParamSpec("P")
+T = TypeVar("T")
+R = TypeVar("R")
+
+def unkoil_gen(
+ iterator: Callable[P, AsyncIterator[R]], *args: P.args, **kwargs: P.kwargs
+) -> Iterator[R]:
+ if is_in_process():
+ for i in unkoil_process_gen(iterator, args, kwargs):
+ yield i
-def unkoil_gen(iterator, *args, **kwargs):
+ return
loop = current_loop.get()
try:
@@ -69,7 +94,17 @@ async def next_on_ait(inside_args):
next_args = yield obj
-def unkoil(coro, *args, **kwargs):
+def unkoil(
+ coro: Union[
+ Callable[P, Coroutine[Any, Any, R]],
+ Callable[P, Awaitable[R]],
+ ],
+ *args: P.args,
+ **kwargs: P.kwargs,
+) -> R:
+ if is_in_process():
+ return unkoil_process_func(coro, args, kwargs)
+
loop = current_loop.get()
try:
loop0 = asyncio.events.get_running_loop()
@@ -122,14 +157,14 @@ async def passed_with_context():
async def run_spawned(
- sync_func,
- *sync_args,
+ sync_func: Callable[P, R],
+ *sync_args: P.args,
executor=None,
pass_context=False,
pass_loop=True,
cancel_timeout=None,
- **sync_kwargs,
-):
+ **sync_kwargs: P.kwargs,
+) -> R:
"""
Spawn a thread with a given sync function and arguments
"""
@@ -198,14 +233,14 @@ def wrapper(sync_args, sync_kwargs, loop, cancel_event, context):
async def iterate_spawned(
- sync_gen,
- *sync_args,
+ sync_gen: Callable[P, Iterator[R]],
+ *sync_args: P.args,
executor=None,
pass_context=False,
pass_loop=True,
cancel_timeout=None,
- **sync_kwargs,
-):
+ **sync_kwargs: P.kwargs,
+) -> R:
"""
Spawn a thread with a given sync function and arguments
"""
@@ -256,7 +291,7 @@ def wrapper(
sync_yield_queue.put(res)
args = sync_next_queue.get()
sync_next_queue.task_done()
- except StopIteration as e:
+ except StopIteration:
raise KoilStopIteration("Thread stopped")
except Exception as e:
logging.info("Exception in generator", exc_info=True)
@@ -278,7 +313,6 @@ def wrapper(
try:
while True:
-
it_task = asyncio.create_task(yield_queue.async_q.get())
finish, unfinished = await asyncio.wait(
@@ -313,6 +347,23 @@ def wrapper(
raise e
+async def run_processed(
+ func: Callable[P, R],
+ *args: P.args,
+ **kwargs: P.kwargs,
+) -> R:
+ async with KoiledProcess() as p:
+ return await p.call(func, *args, **kwargs)
+
+
+async def iterate_processed(
+ func: Callable[P, Iterator[R]], *args: P.args, **kwargs: P.kwargs
+) -> AsyncIterator[R]:
+ async with KoiledProcess() as p:
+ async for i in p.iter(func, *args, **kwargs):
+ yield i
+
+
def create_task(coro, *args, **kwargs) -> KoilFuture:
return KoilRunner(coro, preset_args=args, preset_kwargs=kwargs).run()
diff --git a/koil/koil.py b/koil/koil.py
index 1f8f36f..f8ece02 100644
--- a/koil/koil.py
+++ b/koil/koil.py
@@ -4,10 +4,9 @@
import os
import sys
import threading
-from typing import Any, Optional
from koil.errors import ContextError
-from koil.vars import *
+from koil.vars import current_loop
import time
import logging
@@ -16,13 +15,12 @@
try:
import uvloop
-except:
+except ImportError:
uvloop = None
@contextmanager
def _selector_policy(uvify=True):
-
original_policy = asyncio.get_event_loop_policy()
try:
@@ -43,7 +41,6 @@ def _selector_policy(uvify=True):
def run_threaded_event_loop(loop):
-
try:
loop.run_forever()
finally:
@@ -78,7 +75,7 @@ async def gather():
def get_threaded_loop(name="KoilLoop", uvify=True):
- """Creates a new event loop and run it in a new thread."""
+ """Creates a new event loop and run it in a new threads"""
with _selector_policy(uvify=uvify):
newloop = asyncio.new_event_loop()
@@ -95,9 +92,6 @@ class KoilMixin:
def exit(self):
return self.__exit__(None, None, None)
- async def aexit(self):
- return await self.__aexit__(None, None, None)
-
async def aenter(self):
return await self.__aenter__()
@@ -118,59 +112,54 @@ def __enter__(self):
asyncio.get_running_loop()
if not hasattr(self, "sync_in_async") or self.sync_in_async is False:
raise ContextError(
- "You are running in asyncio event loop already. Using koil makes no sense here, use asyncio instead. If this happens in a context manager, you probably forgot to use the `async with` syntax."
+ f"""You are running in asyncio event loop already.
+ Using koil makes no sense here, use asyncio instead. You can use koil in a sync context by setting `sync_in_async=True` currently it is
+ set to {getattr(self, "sync_in_async", None)}.
+ If this happens in a context manager, you probably forgot to use the `async with` syntax."""
)
except RuntimeError:
pass
- self._loop = current_loop.get()
- assert (
- self._loop is None
- ), f"You are already in a koiled context. You can't nest koiled contexts. Omit creating a new Koil here {self._loop.name}"
- # We are now creating a koiled loop for this context
- self._loop = get_threaded_loop(
- getattr(
- self,
- "name",
- f"KoiledLoop {'governed by' + self.creating_instance.__class__.__name__ if getattr(self, 'creating_instance', None) else ''}",
- ),
- uvify=getattr(self, "uvify", True),
- )
- current_loop.set(self._loop)
+ self._loop = None
+ _loop = current_loop.get()
+ if _loop is None:
+ # We are now creating a koiled loop for this context
+ self._loop = get_threaded_loop(
+ getattr(
+ self,
+ "name",
+ f"KoiledLoop {'governed by' + self.__class__.__name__ if getattr(self, 'creating_instance', None) else ''}",
+ ),
+ uvify=getattr(self, "uvify", True),
+ )
+ current_loop.set(self._loop)
self.running = True
return self
- async def __aloop_close(self):
- loop = asyncio.get_event_loop()
- logger.debug("Causing loop to stop")
- loop.stop()
-
def __exit__(self, *args, **kwargs):
- asyncio.run_coroutine_threadsafe(self.__aloop_close(), self._loop)
+ if self._loop:
+ self._loop.call_soon_threadsafe(self._loop.stop)
- iterations = 0
+ iterations = 0
- while self._loop.is_running():
- time.sleep(0.001)
- iterations += 1
- if iterations == 100:
- logger.warning(
- "Shutting Down takes longer than expected. Probably we are having loose Threads? Keyboard interrupt?"
- )
+ while self._loop.is_running():
+ time.sleep(0.001)
+ iterations += 1
+ if iterations == 100:
+ logger.warning(
+ "Shutting Down takes longer than expected. Probably we are having loose Threads? Keyboard interrupt?"
+ )
- current_loop.set(None)
+ current_loop.set(None)
self.running = False
-
@dataclass
class Koil(KoilMixin):
-
- creating_instance: Optional[Any] = None
"The instance that created this class through entering"
uvify: bool = False
- """Shoul we spawn a new thread for each task?"""
+ """Shoul we run the loop with uvloop?"""
name: str = "KoilLoop"
"""How would you like to name this loop"""
diff --git a/koil/predicates.py b/koil/predicates.py
new file mode 100644
index 0000000..90c69fa
--- /dev/null
+++ b/koil/predicates.py
@@ -0,0 +1,43 @@
+from .vars import current_loop, current_process
+import asyncio
+
+
+def running_in_thread():
+ """
+ Check if the current code is running in a thread
+ """
+
+
+def running_in_same_loop():
+ """
+ Check if the current code is running in the same loop as the main thread
+ """
+ loop = current_loop.get()
+
+ try:
+ loop0 = asyncio.events.get_running_loop()
+
+ if not loop:
+ # No loop found but we are running in a loop, so probably the main loop
+ # without a koiled loop
+ return True
+
+ if loop0 == loop:
+ # We are running in the same loop as the main koiled loop, we can safely
+ return True
+
+ return False
+ except RuntimeError:
+ return False
+
+
+def running_in_process():
+ """
+ Check if the current code is running in a process
+ """
+ return current_process.get() is not None
+
+
+def koiled_loop_is_healthy():
+ loop = current_loop.get()
+ return loop.is_closed() is False and loop.is_running() is True
diff --git a/koil/process.py b/koil/process.py
new file mode 100644
index 0000000..c7f5088
--- /dev/null
+++ b/koil/process.py
@@ -0,0 +1,261 @@
+from .vars import output_queue_context, input_queue_context, in_process_context
+import multiprocessing
+
+from .errors import ProcessCancelledError, KoilError
+from concurrent.futures import ThreadPoolExecutor
+import asyncio
+
+
+try:
+ import cloudpickle
+except ImportError:
+ import pickle as cloudpickle
+
+RETURN = 0
+EXCEPTION = 1
+TRACEBACK = 2
+YIELD = 3
+CANCEL = 4
+ITER = 5
+DONE = 6
+QUIT = 7
+CALL = 8
+
+
+def is_in_process():
+ return in_process_context.get()
+
+
+def matches_suffixes(name, suffixes):
+ for i in suffixes:
+ if name.endswith(i):
+ return True
+ return False
+
+
+def unkoil_process_gen(iterator, args, kwargs):
+ input_queue = input_queue_context.get()
+ out_queue = output_queue_context.get()
+ send_to_queue(input_queue, ITER, (iterator, args, kwargs))
+
+ while True:
+ answer, args_kwargs_or_exception = get_from_queue(out_queue)
+ if answer == YIELD:
+ yield args_kwargs_or_exception
+ elif answer == EXCEPTION:
+ raise args_kwargs_or_exception
+ elif answer == DONE:
+ break
+ elif answer == CANCEL:
+ raise ProcessCancelledError("Cancelled during loop back of generator")
+ else:
+ raise KoilError(f"Unexpected answer: {answer}")
+
+
+def unkoil_process_func(coro, args, kwargs):
+ input_queue = input_queue_context.get()
+ out_queue = output_queue_context.get()
+
+ send_to_queue(input_queue, CALL, (coro, args, kwargs))
+ while True:
+ answer, func_args_kwargs_return_exception = get_from_queue(out_queue)
+ if answer == RETURN:
+ return func_args_kwargs_return_exception
+ elif answer == EXCEPTION:
+ raise func_args_kwargs_return_exception
+ elif answer == CANCEL:
+ raise ProcessCancelledError("Cancelled during loop back")
+ else:
+ raise KoilError(f"Unexpected answer: {answer}")
+
+
+def send_to_queue(queue, task, func_args_kwargs_return_exception):
+ cloudpickle_args = cloudpickle.dumps(func_args_kwargs_return_exception)
+ queue.put((task, cloudpickle_args))
+
+
+def get_from_queue(queue):
+ task, cloudpickle_args = queue.get()
+ if cloudpickle_args is None:
+ func_args_kwargs_return_exception = None
+ else:
+ func_args_kwargs_return_exception = cloudpickle.loads(cloudpickle_args)
+ return task, func_args_kwargs_return_exception
+
+
+def gen_runner(queue, gen, *args, **kwargs):
+ for i in gen(*args, **kwargs):
+ send_to_queue(queue, YIELD, i)
+
+
+def worker(input_queue, output_queue):
+ output_queue_context.set(input_queue)
+ input_queue_context.set(output_queue)
+ in_process_context.set(True)
+ while True:
+ # Wait for a task
+ task, func_args_kwargs_return_exception = get_from_queue(input_queue)
+ if task == CALL:
+ func, args, kwargs = func_args_kwargs_return_exception
+ try:
+ result = func(*args, **kwargs)
+ send_to_queue(output_queue, RETURN, result)
+ except Exception as e:
+ send_to_queue(output_queue, EXCEPTION, e)
+
+ if task == ITER:
+ gen, args, kwargs = func_args_kwargs_return_exception
+ try:
+ gen_runner(output_queue, gen, *args, **kwargs)
+ send_to_queue(output_queue, DONE, None)
+ except Exception as e:
+ send_to_queue(output_queue, EXCEPTION, e)
+
+ if task == CANCEL:
+ exception = func_args_kwargs_return_exception
+ try:
+ raise ProcessCancelledError("from worker") from exception
+ except Exception as e:
+ send_to_queue(output_queue, EXCEPTION, e)
+
+ break
+
+ if task == EXCEPTION:
+ raise func_args_kwargs_return_exception
+
+ if task == QUIT:
+ break
+
+
+class KoiledProcess:
+ """A class that allows to call functions in a separate process."""
+
+ def __init__(self, max_workers=1):
+ """Create a new KoiledProcess.
+
+ Args:
+ max_workers (int, optional): The number of workers for the put to queue calls to use. Defaults to 1.
+ """
+
+ # Create the thread pool (we propably only need one thread?)
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
+ # Create queues for communication
+ self.input_queue = multiprocessing.Queue()
+ self.output_queue = multiprocessing.Queue()
+
+ # Start the worker process
+ self.worker_process = multiprocessing.Process(
+ target=worker, args=(self.input_queue, self.output_queue)
+ )
+
+ self.loop = asyncio.get_event_loop()
+ self.loop.run_in_executor
+ self.started = False
+
+ async def __aenter__(self):
+ self.started = True
+ self.worker_process.start()
+ return self
+
+ async def __aexit__(self, *args, **kwargs):
+ if self.started:
+ self.quit()
+
+ async def get_output(self):
+ return await asyncio.get_event_loop().run_in_executor(
+ self.executor, get_from_queue, self.output_queue
+ )
+
+ async def call(self, func, *args, **kwargs):
+ assert self.started, "You need to start the KoilProcess first"
+ # Send the function and arguments to the worker process
+ try:
+ send_to_queue(self.input_queue, CALL, (func, args, kwargs))
+
+ # Wait for the result and then execute the callback
+ while True:
+ task, func_args_kwargs_return_exception = await self.get_output()
+
+ if task == RETURN:
+ return func_args_kwargs_return_exception
+
+ if task == EXCEPTION:
+ raise func_args_kwargs_return_exception
+
+ if task == CALL:
+ func, args, kwargs = func_args_kwargs_return_exception
+ try:
+ result = await func(*args, **kwargs)
+ send_to_queue(self.input_queue, RETURN, result)
+ except Exception as e:
+ send_to_queue(self.input_queue, EXCEPTION, e)
+
+ if task == ITER:
+ func, args, kwargs = func_args_kwargs_return_exception
+ try:
+ async for result in func(*args, **kwargs):
+ send_to_queue(self.input_queue, YIELD, result)
+
+ send_to_queue(self.input_queue, DONE, None)
+ except Exception as e:
+ send_to_queue(self.input_queue, EXCEPTION, e)
+
+ except asyncio.CancelledError as e:
+ send_to_queue(self.input_queue, CANCEL, e)
+ task, args = await self.get_output()
+ if task == EXCEPTION:
+ if isinstance(args, ProcessCancelledError):
+ raise e
+ else:
+ raise args
+
+ async def iter(self, func, *args, **kwargs):
+ assert self.started, "You need to start the KoilProcess first"
+ # Send the function and arguments to the worker process
+ try:
+ send_to_queue(self.input_queue, ITER, (func, args, kwargs))
+
+ # Wait for the result and then execute the callback
+ while True:
+ task, func_args_kwargs_return_exception = await self.get_output()
+
+ if task == DONE:
+ break
+
+ if task == YIELD:
+ yield func_args_kwargs_return_exception
+
+ if task == CALL:
+ func, args, kwargs = func_args_kwargs_return_exception
+ try:
+ result = await func(*args, **kwargs)
+ send_to_queue(self.input_queue, RETURN, result)
+ except Exception as e:
+ send_to_queue(self.input_queue, EXCEPTION, e)
+
+ if task == EXCEPTION:
+ raise func_args_kwargs_return_exception
+
+ if task == ITER:
+ func, args, kwargs = func_args_kwargs_return_exception
+ try:
+ async for result in func(*args, **kwargs):
+ send_to_queue(self.input_queue, YIELD, result)
+
+ send_to_queue(self.input_queue, DONE, None)
+ except Exception as e:
+ send_to_queue(self.input_queue, EXCEPTION, e)
+
+ except asyncio.CancelledError as e:
+ send_to_queue(self.input_queue, CANCEL, e)
+ task, args = await self.get_output()
+ if task == EXCEPTION:
+ if isinstance(args, ProcessCancelledError):
+ raise e
+ else:
+ raise args
+
+ def quit(self):
+ # Send quit message to worker process
+ send_to_queue(self.input_queue, QUIT, None)
+ self.worker_process.join()
diff --git a/koil/qt.py b/koil/qt.py
index 02264ae..986e356 100644
--- a/koil/qt.py
+++ b/koil/qt.py
@@ -4,11 +4,10 @@
import logging
import threading
from dataclasses import dataclass
-from typing import Callable, Generic, TypeVar
+from typing import Any, Callable, Generic, TypeVar, Awaitable
from qtpy import QtCore, QtWidgets
from typing_extensions import ParamSpec
-from functools import partial
from koil.koil import Koil, KoilMixin
from koil.task import KoilFuture, KoilGeneratorRunner, KoilRunner, KoilYieldFuture
from koil.utils import (
@@ -17,6 +16,10 @@
)
from koil.vars import current_loop
import uuid
+from typing import Protocol
+from .utils import check_is_asyncfunc, check_is_asyncgen, check_is_syncgen
+
+
logger = logging.getLogger(__name__)
@@ -28,13 +31,6 @@ class UnconnectedSignalError(Exception):
pass
-def get_receiver_length(qobject, qsignal, callstring):
- try:
- return qobject.receivers(qsignal)
- except:
- return qobject.receivers(callstring)
-
-
class QtFuture:
def __init__(self):
self.id = uuid.uuid4().hex
@@ -50,9 +46,18 @@ def resolve(self, *args):
if not args:
args = (None,)
ctx = contextvars.copy_context()
+
+ if self.aiofuture.done():
+ logger.warning(f"QtFuture {self} already done. Cannot resolve")
+ return
+
self.loop.call_soon_threadsafe(self.aiofuture.set_result, (ctx,) + args)
def reject(self, exp: Exception):
+ if self.aiofuture.done():
+ logger.warning(f"QtFuture {self} already done. Could not reject")
+ return
+
self.loop.call_soon_threadsafe(self.aiofuture.set_exception, exp)
@@ -113,7 +118,7 @@ def __init__(
super().__init__(*args, **kwargs)
assert not inspect.iscoroutinefunction(
coro
- ), "This should not be a coroutine, but a normal qt slot with the first parameter being a qtfuture"
+ ), f"This should not be a coroutine, but a normal qt slot {'with the first parameter being a qtfuture' if autoresolve is False else ''}"
self.coro = coro
self.called.connect(self.on_called)
self.autoresolve = autoresolve
@@ -165,15 +170,6 @@ def __call__(self, *args):
self.loop.call_soon_threadsafe(self.queue.put_nowait, args)
-class Iterator:
- def __init__(self, queue, timeout=None) -> None:
- self.queue = queue
- self.timeout = timeout
-
- def __anext__(self):
- return self.next()
-
-
class QtSignal(QtCore.QObject, Generic[T, P]):
def __init__(
self,
@@ -257,6 +253,9 @@ def run(self, *args: P.args, **kwargs: P.kwargs):
)
return KoilFuture(future, cancel_event)
+ def __call__(self, *args: Any, **kwds: Any) -> Any:
+ return self.run(*args, **kwds)
+
class QtGeneratorRunner(KoilGeneratorRunner, QtCore.QObject):
errored = QtCore.Signal(Exception)
@@ -326,6 +325,122 @@ def __enter__(self):
return self
+def async_generator_to_qt(func):
+ return QtGeneratorRunner(func)
+
+
+def async_to_qt(func):
+ return QtRunner(func)
+
+
+def qt_to_async(func, autoresolve=False, use_context=True):
+ return QtCoro(func, autoresolve=autoresolve, use_context=use_context).acall
+
+
+def qtgenerator_to_async(func):
+ return QtGenerator(func)
+
+
+class UnkoiledQt(Protocol):
+ errored: QtCore.Signal
+ cancelled: QtCore.Signal()
+ yielded: QtCore.Signal
+ done: QtCore.Signal
+ returned: QtCore.Signal
+
+ def run(self, *args, **kwargs) -> KoilFuture:
+ """ Runs the function in the governing loop and returns a KoilFuture
+
+ This is useful if you want to cancel the function from the outside.
+ The function will be run in the governing loop and the result will be
+ send to the main thread via a QtSignal.
+
+ Args:
+ *args: The arguments for the function
+ **kwargs: The keyword arguments for the function
+
+ Returns:
+ KoilFuture: The future that can be cancelled
+
+ """
+ ...
+
+
+class KoilQt(Protocol):
+ errored: QtCore.Signal
+ cancelled: QtCore.Signal()
+ yielded: QtCore.Signal
+ done: QtCore.Signal
+ returned: QtCore.Signal
+
+ def run(self, *args, **kwargs) -> KoilFuture:
+ """ Runs the function in the governing loop and returns a KoilFuture
+
+ This is useful if you want to cancel the function from the outside.
+ The function will be run in the governing loop and the result will be
+ send to the main thread via a QtSignal.
+
+ Args:
+ *args: The arguments for the function
+ **kwargs: The keyword arguments for the function
+
+ Returns:
+ KoilFuture: The future that can be cancelled
+
+ """
+ ...
+
+
+
+def unkoilqt(func, *args, **kwargs) -> UnkoiledQt:
+ """ Unkoils a function so that it can be run in the main thread
+
+ Args:
+ func (Callable): The function to run in the main thread
+
+
+
+ """
+
+ if not (check_is_asyncgen(func) or check_is_asyncfunc(func)):
+ raise TypeError(f"{func} is not an async function")
+
+ if check_is_asyncgen(func):
+ return async_generator_to_qt(func, *args, **kwargs)
+
+ else:
+ return async_to_qt(func, *args, **kwargs)
+
+
+def koilqt(func, *args, autoresolve=None, **kwargs) -> Callable[..., Awaitable[Any]]:
+ """ Converts a qt mainthread function to be run in the asyncio loop
+
+ Args:
+ func (Callable): The function to run in the main thread (can also
+ be a generator)
+
+ Returns:
+ Callable[..., Awaitable[Any]]: The function that can be run in the
+ asyncio loop
+
+ """
+
+ if check_is_asyncgen(func) or check_is_asyncfunc(func):
+ raise TypeError(f"{func} should NOT be a coroutine function. This is a decorator to convert a function to be callable form the asyncio loop")
+
+ if check_is_syncgen(func):
+ if autoresolve is not None or autoresolve is True:
+ raise TypeError("Cannot autoresolve a generator")
+ return qtgenerator_to_async(func, *args, **kwargs)
+
+ else:
+ if autoresolve is None:
+ autoresolve = True
+ return qt_to_async(func, *args, autoresolve=autoresolve, **kwargs)
+
+
+
+
class WrappedObject(QtCore.QObject):
def __init__(self, *args, koil: Koil = None, **kwargs):
super().__init__(*args, **kwargs)
@@ -357,12 +472,10 @@ def __post_init__(self):
class Config:
arbitrary_types_allowed = True
- def create_task(self, coro, *args, **kwargs) -> QtFuture:
- logger.warning(
- """Creating a task within the qt loop is not recommended. As this might lead to deathlocks and other bad things. (Especially when calling qt back from the koil). Try to use a `create_runner`
- and connect your signals to and then call the `run` method instead."""
- )
- return coro(*args, **kwargs, as_task=True).run()
- def create_runner(self, coro, *args, **kwargs) -> QtRunner:
- return coro(*args, **kwargs, as_task=True)
+
+def create_qt_koil(parent, auto_enter: bool=True) -> QtKoil:
+ koil = QtKoil(parent=parent)
+ if auto_enter:
+ koil.enter()
+ return koil
\ No newline at end of file
diff --git a/koil/task.py b/koil/task.py
index 1e499a7..5c81597 100644
--- a/koil/task.py
+++ b/koil/task.py
@@ -42,7 +42,7 @@ def cancel(self, wait=False):
if wait:
try:
- done = self.future.result()
+ self.future.result()
raise RuntimeError("Task was cancelled but returned a result")
except CancelledError:
return True
@@ -144,7 +144,6 @@ def run(self, *args: P.args, **kwargs: P.kwargs):
assert loop is not None, "No koiled loop found"
assert loop.is_running(), "Loop is not running"
assert not loop.is_closed(), "Loop is closed"
- ait = self.iterator(*self.args, **self.kwargs).__aiter__()
- res = [False, False]
- cancel_event = current_cancel_event.get()
+ self.iterator(*self.args, **self.kwargs).__aiter__()
+ current_cancel_event.get()
raise NotImplementedError("No design decision was taken")
diff --git a/koil/utils.py b/koil/utils.py
index 25b5116..bc2df6a 100644
--- a/koil/utils.py
+++ b/koil/utils.py
@@ -4,7 +4,40 @@
import threading
from typing import Any, Callable
from koil.errors import CancelledError
-import asyncio
+
+import inspect
+
+
+def check_is_asyncgen(func) -> bool:
+ """Checks if a function is an async generator"""
+ if inspect.isasyncgenfunction(func):
+ return True
+
+ return False
+
+
+def check_is_asyncfunc(func) -> bool:
+ """Checks if a function is an async function"""
+ if inspect.iscoroutinefunction(func):
+ return True
+
+ return False
+
+
+def check_is_syncgen(func) -> bool:
+ """Checks if a function is an async generator"""
+ if inspect.isgeneratorfunction(func):
+ return True
+
+ return False
+
+
+def check_is_syncfunc(func) -> bool:
+ """Checks if a function is an async function"""
+ if inspect.isfunction(func):
+ return True
+
+ return False
async def check_event(event: threading.Event):
diff --git a/koil/vars.py b/koil/vars.py
index 66e1916..4a24c82 100644
--- a/koil/vars.py
+++ b/koil/vars.py
@@ -1,13 +1,40 @@
import contextvars
-
+import multiprocessing
from koil.errors import ThreadCancelledError
current_loop = contextvars.ContextVar("current_loop", default=None)
+
+
+# Will only be set in the worker tread
current_cancel_event = contextvars.ContextVar("current_cancel_event", default=None)
+# Will only be set in the worker process
+output_queue_context: contextvars.ContextVar[
+ multiprocessing.Queue
+] = contextvars.ContextVar("out_queue_context")
+input_queue_context: contextvars.ContextVar[
+ multiprocessing.Queue
+] = contextvars.ContextVar("input_queue_context")
+in_process_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
+ "in_process_context", default=False
+)
+
+
def check_cancelled():
cancel_event = current_cancel_event.get()
if cancel_event and cancel_event.is_set():
raise ThreadCancelledError("Task was cancelled")
+
+
+def get_process_queues():
+ """Get the process queues.
+
+ Returns the input and output queues of the current process.
+
+ Returns:
+ multiprocessing.Queue: The input queue
+ multiprocessing.Queue: The output queue
+ """
+ return input_queue_context.get(), output_queue_context.get()
diff --git a/poetry.lock b/poetry.lock
index 545c38b..401508b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,36 +1,42 @@
-[[package]]
-name = "attrs"
-version = "22.1.0"
-description = "Classes Without Boilerplate"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
-tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
[[package]]
name = "autoflake"
-version = "1.7.7"
+version = "1.7.8"
description = "Removes unused imports and unused variables"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"},
+ {file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"},
+]
[package.dependencies]
-pyflakes = ">=1.1.0"
+pyflakes = ">=1.1.0,<3"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[[package]]
name = "black"
-version = "22.10.0"
+version = "22.12.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
+ {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
+ {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
+ {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
+ {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
+ {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
+ {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
+ {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
+ {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
+ {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
+ {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
+ {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
+]
[package.dependencies]
click = ">=8.0.0"
@@ -38,7 +44,6 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
-typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
@@ -49,15 +54,30 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
-version = "8.1.3"
+version = "8.1.6"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
+ {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
+]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "cloudpickle"
+version = "2.2.1"
+description = "Extended pickling support for Python objects"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"},
+ {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"},
+]
[[package]]
name = "colorama"
@@ -66,14 +86,80 @@ description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
[[package]]
name = "coverage"
-version = "6.5.0"
+version = "7.2.7"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+ {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+ {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+ {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+ {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+ {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+ {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+ {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+ {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+ {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+ {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+ {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+ {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+ {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+ {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+ {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
@@ -83,39 +169,30 @@ toml = ["tomli"]
[[package]]
name = "exceptiongroup"
-version = "1.0.4"
+version = "1.1.2"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"},
+ {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"},
+]
[package.extras]
test = ["pytest (>=6)"]
-[[package]]
-name = "importlib-metadata"
-version = "5.0.0"
-description = "Read metadata from Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-perf = ["ipython"]
-testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
-
[[package]]
name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
[[package]]
name = "janus"
@@ -124,59 +201,125 @@ description = "Mixed sync-async queue to interoperate between asyncio tasks and
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "janus-1.0.0-py3-none-any.whl", hash = "sha256:2596ea5482711c1ee3ef2df6c290aaf370a13c55a007826e8f7c32d696d1d00a"},
+ {file = "janus-1.0.0.tar.gz", hash = "sha256:df976f2cdcfb034b147a2d51edfc34ff6bfb12d4e2643d3ad0e10de058cb1612"},
+]
[package.dependencies]
typing-extensions = ">=3.7.4.3"
+[[package]]
+name = "mypy"
+version = "1.8.0"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
+ {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
+ {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
+ {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
+ {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
+ {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
+ {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
+ {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
+ {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
+ {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
+ {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
+ {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
+ {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
+ {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
+ {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
+ {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
+ {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
+ {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
+ {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
+ {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
+ {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
+ {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
+ {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
+ {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
+ {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
+ {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
+ {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
[[package]]
name = "mypy-extensions"
-version = "0.4.3"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
[[package]]
name = "packaging"
-version = "21.3"
+version = "23.1"
description = "Core utilities for Python packages"
category = "main"
optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+]
[[package]]
name = "pathspec"
-version = "0.10.2"
+version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
+ {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+]
[[package]]
name = "platformdirs"
-version = "2.5.4"
+version = "3.9.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"},
+ {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"},
+]
[package.extras]
-docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
-test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
[[package]]
name = "pluggy"
-version = "1.0.0"
+version = "1.2.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+python-versions = ">=3.7"
+files = [
+ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
+ {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
+]
[package.extras]
dev = ["pre-commit", "tox"]
@@ -184,14 +327,52 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pydantic"
-version = "1.10.2"
+version = "1.10.12"
description = "Data validation and settings management using python type hints"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"},
+ {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"},
+ {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"},
+ {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"},
+ {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"},
+ {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"},
+ {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"},
+ {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"},
+ {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"},
+ {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"},
+ {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"},
+ {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"},
+ {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"},
+ {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"},
+ {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"},
+ {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"},
+ {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"},
+ {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"},
+ {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"},
+ {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"},
+ {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"},
+ {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"},
+ {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"},
+ {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"},
+ {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"},
+ {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"},
+ {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"},
+ {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"},
+ {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"},
+ {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"},
+ {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"},
+ {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"},
+ {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"},
+ {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"},
+ {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"},
+ {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"},
+]
[package.dependencies]
-typing-extensions = ">=4.1.0"
+typing-extensions = ">=4.2.0"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
@@ -204,89 +385,64 @@ description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=3.6"
-
-[[package]]
-name = "pyparsing"
-version = "3.0.9"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "main"
-optional = false
-python-versions = ">=3.6.8"
-
-[package.extras]
-diagrams = ["jinja2", "railroad-diagrams"]
-
-[[package]]
-name = "PyQt5"
-version = "5.15.7"
-description = "Python bindings for the Qt cross platform application toolkit"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-PyQt5-Qt5 = ">=5.15.0"
-PyQt5-sip = ">=12.11,<13"
-
-[[package]]
-name = "PyQt5-Qt5"
-version = "5.15.2"
-description = "The subset of a Qt installation needed by PyQt5."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "PyQt5-sip"
-version = "12.11.0"
-description = "The sip module support for PyQt5"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
+files = [
+ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
+ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
+]
[[package]]
name = "pytest"
-version = "7.2.0"
+version = "7.4.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
+ {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
+]
[package.dependencies]
-attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
-version = "0.20.2"
+version = "0.20.3"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"},
+ {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"},
+]
[package.dependencies]
pytest = ">=6.1.0"
-typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]]
name = "pytest-cov"
-version = "4.0.0"
+version = "4.1.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+ {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
@@ -302,6 +458,10 @@ description = "pytest support for PyQt and PySide applications"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pytest-qt-4.2.0.tar.gz", hash = "sha256:00a17b586dd530b6d7a9399923a40489ca4a9a309719011175f55dc6b5dc8f41"},
+ {file = "pytest_qt-4.2.0-py2.py3-none-any.whl", hash = "sha256:a7659960a1ab2af8fc944655a157ff45d714b80ed7a6af96a4b5bb99ecf40a22"},
+]
[package.dependencies]
pytest = ">=3.0.0"
@@ -311,12 +471,16 @@ dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
-name = "QtPy"
-version = "2.3.0"
+name = "qtpy"
+version = "2.3.1"
description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)."
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "QtPy-2.3.1-py3-none-any.whl", hash = "sha256:5193d20e0b16e4d9d3bc2c642d04d9f4e2c892590bd1b9c92bfe38a95d5a2e12"},
+ {file = "QtPy-2.3.1.tar.gz", hash = "sha256:a8c74982d6d172ce124d80cafd39653df78989683f760f2281ba91a6e7b9de8b"},
+]
[package.dependencies]
packaging = "*"
@@ -325,28 +489,55 @@ packaging = "*"
test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"]
[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
+name = "ruff"
+version = "0.1.15"
+description = "An extremely fast Python linter and code formatter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
+ {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
+ {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
+ {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
+ {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
+]
[[package]]
-name = "typed-ast"
-version = "1.5.4"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
[[package]]
name = "typing-extensions"
-version = "4.4.0"
+version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+]
[[package]]
name = "uvloop"
@@ -355,294 +546,7 @@ description = "Fast implementation of asyncio event loop on top of libuv"
category = "main"
optional = true
python-versions = ">=3.7"
-
-[package.extras]
-dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
-docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
-test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"]
-
-[[package]]
-name = "zipp"
-version = "3.10.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
-
-[extras]
-qtpy = []
-uvloop = ["uvloop"]
-
-[metadata]
-lock-version = "1.1"
-python-versions = "^3.7"
-content-hash = "eb35290053d398ee62d072f064beec6082a18c19f2e0093f956163028cca6860"
-
-[metadata.files]
-attrs = [
- {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
- {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
-]
-autoflake = [
- {file = "autoflake-1.7.7-py3-none-any.whl", hash = "sha256:a9b43d08f8e455824e4f7b3f078399f59ba538ba53872f466c09e55c827773ef"},
- {file = "autoflake-1.7.7.tar.gz", hash = "sha256:c8e4fc41aa3eae0f5c94b939e3a3d50923d7a9306786a6cbf4866a077b8f6832"},
-]
-black = [
- {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
- {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
- {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
- {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
- {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
- {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
- {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
- {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
- {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
- {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
- {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
- {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
- {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
- {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
- {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
- {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
- {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
- {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
- {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
- {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
- {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
-]
-click = [
- {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
- {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
-]
-colorama = [
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
-coverage = [
- {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
- {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
- {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
- {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
- {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
- {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
- {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
- {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
- {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
- {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
- {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
- {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
- {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
- {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
- {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
- {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
- {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
- {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
- {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
- {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
-]
-exceptiongroup = [
- {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
- {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
-]
-importlib-metadata = [
- {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
- {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
-]
-iniconfig = [
- {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
- {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
-janus = [
- {file = "janus-1.0.0-py3-none-any.whl", hash = "sha256:2596ea5482711c1ee3ef2df6c290aaf370a13c55a007826e8f7c32d696d1d00a"},
- {file = "janus-1.0.0.tar.gz", hash = "sha256:df976f2cdcfb034b147a2d51edfc34ff6bfb12d4e2643d3ad0e10de058cb1612"},
-]
-mypy-extensions = [
- {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
- {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
-]
-packaging = [
- {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
- {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
-]
-pathspec = [
- {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"},
- {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"},
-]
-platformdirs = [
- {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"},
- {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"},
-]
-pluggy = [
- {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
- {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
-]
-pydantic = [
- {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
- {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
- {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
- {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
- {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
- {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
- {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
- {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
- {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
- {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
- {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
- {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
- {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
- {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
- {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
- {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
- {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
- {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
- {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
- {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
- {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
- {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
- {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
- {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
- {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
- {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
- {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
- {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
- {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
- {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
- {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
- {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
- {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
- {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
- {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
- {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
-]
-pyflakes = [
- {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
- {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
-]
-pyparsing = [
- {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
- {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-PyQt5 = [
- {file = "PyQt5-5.15.7-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:1a793748c60d5aff3850b7abf84d47c1d41edb11231b7d7c16bef602c36be643"},
- {file = "PyQt5-5.15.7-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:e319c9d8639e0729235c1b09c99afdadad96fa3dbd8392ab561b5ab5946ee6ef"},
- {file = "PyQt5-5.15.7-cp37-abi3-win32.whl", hash = "sha256:08694f0a4c7d4f3d36b2311b1920e6283240ad3b7c09b515e08262e195dcdf37"},
- {file = "PyQt5-5.15.7-cp37-abi3-win_amd64.whl", hash = "sha256:232fe5b135a095cbd024cf341d928fc672c963f88e6a52b0c605be8177c2fdb5"},
- {file = "PyQt5-5.15.7.tar.gz", hash = "sha256:755121a52b3a08cb07275c10ebb96576d36e320e572591db16cfdbc558101594"},
-]
-PyQt5-Qt5 = [
- {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"},
- {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"},
- {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"},
- {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
-]
-PyQt5-sip = [
- {file = "PyQt5_sip-12.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f9e312ff8284d6dfebc5366f6f7d103f84eec23a4da0be0482403933e68660"},
- {file = "PyQt5_sip-12.11.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:4031547dfb679be309094bfa79254f5badc5ddbe66b9ad38e319d84a7d612443"},
- {file = "PyQt5_sip-12.11.0-cp310-cp310-win32.whl", hash = "sha256:ad21ca0ee8cae2a41b61fc04949dccfab6fe008749627d94e8c7078cb7a73af1"},
- {file = "PyQt5_sip-12.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3126c84568ab341c12e46ded2230f62a9a78752a70fdab13713f89a71cd44f73"},
- {file = "PyQt5_sip-12.11.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0f77655c62ec91d47c2c99143f248624d44dd2d8a12d016e7c020508ad418aca"},
- {file = "PyQt5_sip-12.11.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ec5e9ef78852e1f96f86d7e15c9215878422b83dde36d44f1539a3062942f19c"},
- {file = "PyQt5_sip-12.11.0-cp37-cp37m-win32.whl", hash = "sha256:d12b81c3a08abf7657a2ebc7d3649852a1f327eb2146ebadf45930486d32e920"},
- {file = "PyQt5_sip-12.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b69a1911f768b489846335e31e49eb34795c6b5a038ca24d894d751e3b0b44da"},
- {file = "PyQt5_sip-12.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:51e377789d59196213eddf458e6927f33ba9d217b614d17d20df16c9a8b2c41c"},
- {file = "PyQt5_sip-12.11.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4e5c1559311515291ea0ab0635529f14536954e3b973a7c7890ab7e4de1c2c23"},
- {file = "PyQt5_sip-12.11.0-cp38-cp38-win32.whl", hash = "sha256:9bca450c5306890cb002fe36bbca18f979dd9e5b810b766dce8e3ce5e66ba795"},
- {file = "PyQt5_sip-12.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:f6b72035da4e8fecbb0bc4a972e30a5674a9ad5608dbddaa517e983782dbf3bf"},
- {file = "PyQt5_sip-12.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9356260d4feb60dbac0ab66f8a791a0d2cda1bf98c9dec8e575904a045fbf7c5"},
- {file = "PyQt5_sip-12.11.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205f3e1b3eea3597d8e878936c1a06e04bd23a59e8b179ee806465d72eea3071"},
- {file = "PyQt5_sip-12.11.0-cp39-cp39-win32.whl", hash = "sha256:686071be054e5be6ca5aaaef7960931d4ba917277e839e2e978c7cbe3f43bb6e"},
- {file = "PyQt5_sip-12.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e"},
- {file = "PyQt5_sip-12.11.0.tar.gz", hash = "sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39"},
-]
-pytest = [
- {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
- {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
-]
-pytest-asyncio = [
- {file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"},
- {file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"},
-]
-pytest-cov = [
- {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
- {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
-]
-pytest-qt = [
- {file = "pytest-qt-4.2.0.tar.gz", hash = "sha256:00a17b586dd530b6d7a9399923a40489ca4a9a309719011175f55dc6b5dc8f41"},
- {file = "pytest_qt-4.2.0-py2.py3-none-any.whl", hash = "sha256:a7659960a1ab2af8fc944655a157ff45d714b80ed7a6af96a4b5bb99ecf40a22"},
-]
-QtPy = [
- {file = "QtPy-2.3.0-py3-none-any.whl", hash = "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408"},
- {file = "QtPy-2.3.0.tar.gz", hash = "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5"},
-]
-tomli = [
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-typed-ast = [
- {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
- {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
- {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
- {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
- {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
- {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
- {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
- {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
- {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
- {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
- {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
- {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
- {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
- {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
- {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
- {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
- {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
- {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
- {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
- {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
- {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
- {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
- {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
- {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
-]
-typing-extensions = [
- {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
- {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
-]
-uvloop = [
+files = [
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
{file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"},
@@ -660,7 +564,18 @@ uvloop = [
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
]
-zipp = [
- {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
- {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
-]
+
+[package.extras]
+dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"]
+
+[extras]
+process = ["cloudpickle"]
+qtpy = []
+uvloop = ["uvloop"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.8"
+content-hash = "2fb22c03a1e3b394784ca9728c5589138ab1eb1fb15fbf84953c7aa95afd70e7"
diff --git a/pyproject.toml b/pyproject.toml
index bf5b860..038d28e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "koil"
-version = "0.2.14"
+version = "0.3.6"
readme = "README.md"
description = "Async for a sync world"
authors = ["jhnnsrs "]
@@ -8,14 +8,16 @@ license = "CC BY-NC 3.0"
packages = [{ include = "koil" }]
[tool.poetry.dependencies]
-python = "^3.7"
+python = "^3.8"
qtpy = { version = ">1", optional = true }
uvloop = { version = "^0.16.0", optional = true }
janus = "^1.0.0"
+cloudpickle = { version = "^2.2.1", optional = true }
[tool.poetry.extras]
qtpy = ["qt"]
uvloop = ["uvloop"]
+process = ["cloudpickle"]
[tool.poetry.group.dev.dependencies]
@@ -24,10 +26,12 @@ black = "^22.10.0"
pytest-cov = "^4.0.0"
pytest-qt = "^4.2.0"
QtPy = "^2.3.0"
-PyQt5 = "^5.15.7"
pytest-asyncio = "^0.20.2"
pydantic = "^1.10.2"
autoflake = "^1.7.7"
+cloudpickle = "^2.2.1"
+mypy = "^1.8.0"
+ruff = "^0.1.15"
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -35,6 +39,9 @@ build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
asyncio_mode = "auto"
+qt_api = "pyqt5"
+markers = ["qt: marks tests that require a running qt application", "process: marks tests that require the process extra"]
+
[[tool.pydoc-markdown.loaders]]
type = "python"
@@ -44,3 +51,8 @@ search_path = ["koil"]
[tool.pydoc-markdown.renderer]
type = "docusaurus"
docs_base_path = "website/docs"
+
+
+[tool.mypy]
+ignore_missing_imports = true
+strict = true
diff --git a/tests/context.py b/tests/context.py
index b8b2526..d49121a 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -1,5 +1,9 @@
import asyncio
from koil.decorators import koilable, unkoilable
+import contextvars
+
+
+t = contextvars.ContextVar("t", default=0)
@koilable()
diff --git a/tests/test_context_complex.py b/tests/test_context_complex.py
index 3fbb0c9..099e216 100644
--- a/tests/test_context_complex.py
+++ b/tests/test_context_complex.py
@@ -32,7 +32,7 @@ class X(object):
def __init__(self, x):
self.x = x
- def sleep_and_call(self, nana):
+ def sleep_and_call(self, nana) -> str:
time.sleep(0.04)
y = self.a(nana, as_task=True).run()
check_cancelled()
@@ -43,7 +43,7 @@ def sleep_and_yield(self, nana):
for i in range(2):
a = self.a("v")
check_cancelled()
- t = yield a
+ yield a
@unkoilable
async def a(self, a):
@@ -51,11 +51,12 @@ async def a(self, a):
@unkoilable
async def t(self):
- return "x" + await run_spawned(self.sleep_and_call, "haha", cancel_timeout=3)
+ f = await run_spawned(self.sleep_and_call, "haha", cancel_timeout=3)
+ return "x" + f
async def g(self):
async for i in iterate_spawned(self.sleep_and_yield, "haha", cancel_timeout=3):
- x = yield i + "33"
+ yield i + "33"
async def __aenter__(self):
return self
@@ -65,7 +66,6 @@ async def __aexit__(self, *args, **kwargs):
async def test_async():
-
async with Koil():
async with X(1) as x:
x = asyncio.create_task(x.t())
@@ -73,18 +73,16 @@ async def test_async():
x.cancel()
try:
x = await x
- except asyncio.CancelledError as e:
+ except asyncio.CancelledError:
pass
def test_x_sync():
-
with X(1) as x:
- l = unkoil_gen(x.g)
- l.send(None)
- l.send(None)
- print("Here")
+ sender = unkoil_gen(x.g)
+ sender.send(None)
+ sender.send(None)
try:
- l.send(None)
+ sender.send(None)
except StopIteration:
pass
diff --git a/tests/test_multilevel.py b/tests/test_multilevel.py
new file mode 100644
index 0000000..ecfcd33
--- /dev/null
+++ b/tests/test_multilevel.py
@@ -0,0 +1,56 @@
+import asyncio
+from koil.composition.base import KoiledModel
+from koil import Koil
+from koil.helpers import unkoil
+import pytest
+
+
+class Kant(KoiledModel):
+ connected: bool = False
+
+ async def __aenter__(self):
+ await asyncio.sleep(0.004)
+
+ self.connected = True
+ return self
+
+ async def __aexit__(self, *args, **kwargs):
+ self.connected = False
+
+
+class Tan(KoiledModel): #
+ x: int = 3
+
+ async def arun(self):
+ await asyncio.sleep(0.02)
+ return self.x
+
+ def run(self):
+ return unkoil(self.arun)
+
+ async def __aenter__(self):
+ await asyncio.sleep(0.002)
+ self.x = 4
+ return self
+
+ async def __aexit__(self, *args, **kwargs):
+ pass
+
+
+def test_multilevel():
+ with Koil():
+ with Tan() as t:
+ assert t.x == 4, "tan.x should be 4 because it was set in enter"
+
+ with Kant() as k:
+ assert k.connected, "kant should be connected"
+
+
+@pytest.mark.asyncio
+async def test_nothing_multilevel():
+ async with Koil():
+ async with Tan() as t:
+ assert t.x == 4, "tan.x should be 4 because it was set in enter"
+
+ async with Kant() as k:
+ assert k.connected, "kant should be connected"
diff --git a/tests/test_processed.py b/tests/test_processed.py
new file mode 100644
index 0000000..474937e
--- /dev/null
+++ b/tests/test_processed.py
@@ -0,0 +1,149 @@
+import asyncio
+
+import pytest
+
+from koil.helpers import unkoil, unkoil_gen, run_processed, iterate_processed
+from .context import AsyncContextManager
+from koil import Koil
+from .context import t
+
+
+async def sleep(ins):
+ await asyncio.sleep(0.001)
+ return ins
+
+
+async def sleep_and_raise(ins):
+ await asyncio.sleep(0.001)
+ raise Exception("This is a sleep and raise exception")
+
+
+async def iterating():
+ yield 1
+ await asyncio.sleep(0.001)
+ yield 2
+ await asyncio.sleep(0.001)
+ yield 3
+
+
+async def iterate_and_raise():
+ yield 1
+ raise Exception("This is an iterate and raise exception")
+
+
+def test_sync_context():
+ with AsyncContextManager() as c:
+ c.aprint()
+
+
+async def test_async_context():
+ async with AsyncContextManager() as c:
+ await c.aprint()
+
+
+def test_sync():
+ with Koil():
+ assert unkoil(sleep, 1) == 1, "Koil realized its async and was okay with that"
+
+
+def process_func(arg: int, number: int):
+ return arg + number
+
+
+def raising_process_func(arg: int, number: int):
+ raise Exception("This is a test exception")
+
+
+def back_calling_func(arg: int, number: int):
+ return unkoil(sleep, arg + number)
+
+
+def back_calling_raising_func(arg: int, number: int):
+ return unkoil(sleep_and_raise, arg + number)
+
+
+def context_vars():
+ return t.get()
+
+
+@pytest.mark.process
+async def test_spawn_process_func():
+ async with Koil():
+ assert (
+ await run_processed(process_func, 1, number=2) == 3
+ ), "Process should run and return 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_exception_func():
+ async with Koil():
+ with pytest.raises(Exception, match="This is a test exception"):
+ assert (
+ await run_processed(raising_process_func, 1, number=2) == 3
+ ), "Process should run and return 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_back_calling_func():
+ async with Koil():
+ assert (
+ await run_processed(back_calling_func, 1, number=2) == 3
+ ), "Process should run and return 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_back_raise_calling_func():
+ async with Koil():
+ with pytest.raises(Exception, match="This is a sleep and raise exception"):
+ assert (
+ await run_processed(back_calling_raising_func, 1, number=2) == 3
+ ), "Process should run and return 3"
+
+
+def process_gen(arg: int, number: int):
+ yield arg + number
+ yield arg + number
+
+
+def raising_process_gen(arg: int, number: int):
+ raise Exception("This is a test exception")
+
+
+def back_calling_gen(arg: int, number: int):
+ for i in unkoil_gen(iterating):
+ yield arg + number
+
+
+def back_calling_raising_gen(arg: int, number: int):
+ for i in unkoil_gen(iterate_and_raise):
+ yield arg + number
+
+
+@pytest.mark.process
+async def test_spawn_process_gen():
+ async with Koil():
+ async for i in iterate_processed(process_gen, 1, number=2):
+ assert i == 3, "Process should run and yield 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_exception_gen():
+ async with Koil():
+ with pytest.raises(Exception, match="This is a test exception"):
+ async for i in iterate_processed(raising_process_gen, 1, number=2):
+ assert i == 3, "Process should run and yield 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_back_calling_gen():
+ async with Koil():
+ async for i in iterate_processed(back_calling_gen, 1, number=2):
+ assert i == 3, "Process should run and yield 3"
+
+
+@pytest.mark.process
+async def test_spawn_process_back_raise_calling_gen():
+ async with Koil():
+ with pytest.raises(Exception, match="This is an iterate and raise exception"):
+ async for i in iterate_processed(back_calling_raising_gen, 1, number=2):
+ assert i == 3, "Process should run and yield 3"
diff --git a/tests/test_qtkoil.py b/tests/test_qtkoil.py
index f2cb40e..bcb7c60 100644
--- a/tests/test_qtkoil.py
+++ b/tests/test_qtkoil.py
@@ -2,6 +2,7 @@
from PyQt5 import QtWidgets, QtCore
from koil.qt import QtCoro, QtFuture, QtGenerator, QtGeneratorRunner, QtKoil, QtRunner
import contextvars
+import pytest
x = contextvars.ContextVar("x")
@@ -139,7 +140,7 @@ def task_finished(self):
self.greet_label.setText("Hello!")
async def call_coro(self):
- x = await self.do_me.acall()
+ await self.do_me.acall()
self.coroutine_was_run = True
@@ -185,11 +186,12 @@ async def call_coro(self):
self.coroutine_finished = True
- x = await self.do_me.acall()
+ await self.do_me.acall()
self.coroutine_was_run = True
print("nana")
+@pytest.mark.qt
def test_koil_qt_no_interference(qtbot):
"""Tests if just adding koil interferes with normal
qtpy widgets.
@@ -206,27 +208,30 @@ def test_koil_qt_no_interference(qtbot):
assert widget.greet_label.text() == "Hello!"
+@pytest.mark.qt
def test_koil_qt_call_task(qtbot):
"""Tests if we can call a task from a koil widget."""
widget = KoiledInterferingWidget()
qtbot.addWidget(widget)
# click in the Greet button and make sure it updates the appropriate label
- with qtbot.waitSignal(widget.sleep_and_resolve_task.returned) as b:
+ with qtbot.waitSignal(widget.sleep_and_resolve_task.returned):
qtbot.mouseClick(widget.call_task_button, QtCore.Qt.LeftButton)
+@pytest.mark.qt
def test_call_gen(qtbot):
"""Tests if we can call a task from a koil widget."""
widget = KoiledInterferingWidget()
qtbot.addWidget(widget)
# click in the Greet button and make sure it updates the appropriate label
- with qtbot.waitSignal(widget.sleep_and_yield_task.yielded, timeout=1000) as b:
+ with qtbot.waitSignal(widget.sleep_and_yield_task.yielded, timeout=1000):
qtbot.mouseClick(widget.call_gen_button, QtCore.Qt.LeftButton)
+@pytest.mark.qt
def test_call_future(qtbot):
"""Tests if we can call a task from a koil widget."""
widget = KoiledInterferingFutureWidget()
@@ -237,10 +242,11 @@ def test_call_future(qtbot):
qtbot.mouseClick(widget.call_task_button, QtCore.Qt.LeftButton)
- assert widget.task_was_run == True
- assert widget.coroutine_was_run == True
+ assert widget.task_was_run is True
+ assert widget.coroutine_was_run is True
+@pytest.mark.qt
def test_call_raise(qtbot):
"""Tests if we can call a task from a koil widget."""
widget = KoiledInterferingWidget()
@@ -254,6 +260,7 @@ def test_call_raise(qtbot):
assert isinstance(b.args[0], Exception)
+@pytest.mark.qt
def test_context(qtbot):
"""Tests if we can call a task from a koil widget."""
widget = KoiledInterferingWidget()
diff --git a/tests/test_qtkoil_modern.py b/tests/test_qtkoil_modern.py
new file mode 100644
index 0000000..87410cc
--- /dev/null
+++ b/tests/test_qtkoil_modern.py
@@ -0,0 +1,278 @@
+import asyncio
+from PyQt5 import QtWidgets, QtCore
+from koil.qt import QtFuture, QtGenerator, QtKoil
+import contextvars
+import pytest
+from koil.qt import unkoilqt, koilqt
+
+x = contextvars.ContextVar("x")
+
+
+async def sleep_and_resolve():
+ await asyncio.sleep(0.1)
+ return 1
+
+
+async def sleep_and_raise():
+ await asyncio.sleep(0.1)
+ raise Exception("Task is done!")
+
+
+async def sleep_and_use_context():
+ await asyncio.sleep(0.1)
+ return x.get() + 1
+
+
+async def sleep_and_yield(times=5):
+ for i in range(times):
+ await asyncio.sleep(0.1)
+ yield i
+
+
+class KoiledWidget(QtWidgets.QWidget):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.koil = QtKoil(parent=self)
+ self.koil.enter()
+
+ self.button_greet = QtWidgets.QPushButton("Greet")
+ self.greet_label = QtWidgets.QLabel("")
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.button_greet)
+ layout.addWidget(self.greet_label)
+
+ self.setLayout(layout)
+
+ self.button_greet.clicked.connect(self.greet)
+
+ def greet(self):
+ self.greet_label.setText("Hello!")
+
+
+class KoiledInterferingWidget(QtWidgets.QWidget):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.koil = QtKoil(parent=self)
+ self.koil.enter()
+
+ self.call_task_button = QtWidgets.QPushButton("Call Task")
+ self.call_gen_button = QtWidgets.QPushButton("Call Generator")
+ self.call_raise_button = QtWidgets.QPushButton("Call Raise")
+ self.call_context_button = QtWidgets.QPushButton("Call Context")
+
+ self.sleep_and_resolve_task = unkoilqt(sleep_and_resolve)
+ self.sleep_and_resolve_task.returned.connect(self.task_finished)
+
+ self.sleep_and_use_context_task = unkoilqt(sleep_and_use_context)
+ self.sleep_and_use_context_task.returned.connect(self.task_finished)
+
+ self.sleep_and_yield_task = unkoilqt(sleep_and_yield)
+ self.sleep_and_yield_task.yielded.connect(self.task_finished)
+
+ self.sleep_and_raise_task = unkoilqt(sleep_and_raise)
+ self.sleep_and_resolve_task.returned.connect(self.task_finished)
+
+ self.greet_label = QtWidgets.QLabel("")
+ self.value = None
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.call_task_button)
+ layout.addWidget(self.call_gen_button)
+ layout.addWidget(self.call_context_button)
+ layout.addWidget(self.greet_label)
+
+ self.setLayout(layout)
+
+ self.call_task_button.clicked.connect(self.call_task)
+ self.call_gen_button.clicked.connect(self.call_gen)
+ self.call_context_button.clicked.connect(self.call_context)
+ self.call_raise_button.clicked.connect(self.call_raise)
+
+ def call_task(self):
+ self.sleep_and_resolve_task.run()
+
+ def call_gen(self):
+ self.sleep_and_yield_task.run()
+
+ def call_context(self):
+ self.sleep_and_use_context_task.run()
+
+ def call_raise(self):
+ self.sleep_and_raise_task.run()
+
+ def task_finished(self, int):
+ self.value = int
+ self.greet_label.setText("Hello!")
+
+
+class KoiledInterferingFutureWidget(QtWidgets.QWidget):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.koil = QtKoil(parent=self)
+ self.koil.enter()
+
+ self.do_me = koilqt(self.in_qt_task, autoresolve=False)
+
+ self.my_coro_task = unkoilqt(self.call_coro)
+ self.my_coro_task.returned.connect(self.task_finished)
+
+ self.task_was_run = False
+ self.coroutine_was_run = False
+
+ self.call_task_button = QtWidgets.QPushButton("Call Task")
+ self.greet_label = QtWidgets.QLabel("")
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.call_task_button)
+ layout.addWidget(self.greet_label)
+
+ self.setLayout(layout)
+
+ self.call_task_button.clicked.connect(self.call_task)
+
+ def in_qt_task(self, future: QtFuture):
+ self.task_was_run = True
+ future.resolve("called")
+
+ def call_task(self):
+ self.my_coro_task.run()
+
+ def task_finished(self):
+ self.greet_label.setText("Hello!")
+
+ async def call_coro(self):
+ await self.do_me()
+ self.coroutine_was_run = True
+
+
+class KoiledGeneratorWidget(QtWidgets.QWidget):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.koil = QtKoil(parent=self)
+ self.koil.enter()
+
+ self.my_coro_task = unkoilqt(self.call_coro)
+ self.my_coro_task.returned.connect(self.task_finished)
+
+ self.task_was_run = False
+ self.coroutine_was_run = False
+ self.coroutine_finished = False
+
+ self.call_task_button = QtWidgets.QPushButton("Call Task")
+ self.greet_label = QtWidgets.QLabel("")
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.call_task_button)
+ layout.addWidget(self.greet_label)
+
+ self.setLayout(layout)
+
+ self.call_task_button.clicked.connect(self.call_task)
+
+ def in_qt_task(self, future: QtFuture):
+ self.task_was_run = True
+ future.resolve("called")
+
+ def call_task(self):
+ self.my_coro_task.run()
+
+ def task_finished(self):
+ self.greet_label.setText("Hello!")
+
+ async def call_coro(self):
+ self.qt_generator = QtGenerator()
+
+ async for x in self.qt_generator:
+ self.coroutine_was_run = True
+
+ self.coroutine_finished = True
+
+ await self.do_me.acall()
+ self.coroutine_was_run = True
+ print("nana")
+
+
+@pytest.mark.qt
+def test_koil_qt_no_interference(qtbot):
+ """Tests if just adding koil interferes with normal
+ qtpy widgets.
+
+ Args:
+ qtbot (_type_): _description_
+ """
+ widget = KoiledWidget()
+ qtbot.addWidget(widget)
+
+ # click in the Greet button and make sure it updates the appropriate label
+ qtbot.mouseClick(widget.button_greet, QtCore.Qt.LeftButton)
+
+ assert widget.greet_label.text() == "Hello!"
+
+
+@pytest.mark.qt
+def test_koil_qt_call_task(qtbot):
+ """Tests if we can call a task from a koil widget."""
+ widget = KoiledInterferingWidget()
+ qtbot.addWidget(widget)
+
+ # click in the Greet button and make sure it updates the appropriate label
+ with qtbot.waitSignal(widget.sleep_and_resolve_task.returned):
+ qtbot.mouseClick(widget.call_task_button, QtCore.Qt.LeftButton)
+
+
+@pytest.mark.qt
+def test_call_gen(qtbot):
+ """Tests if we can call a task from a koil widget."""
+ widget = KoiledInterferingWidget()
+ qtbot.addWidget(widget)
+
+ # click in the Greet button and make sure it updates the appropriate label
+ with qtbot.waitSignal(widget.sleep_and_yield_task.yielded, timeout=1000):
+
+ qtbot.mouseClick(widget.call_gen_button, QtCore.Qt.LeftButton)
+
+
+@pytest.mark.qt
+def test_call_future(qtbot):
+ """Tests if we can call a task from a koil widget."""
+ widget = KoiledInterferingFutureWidget()
+ qtbot.addWidget(widget)
+
+ # click in the Greet button and make sure it updates the appropriate label
+ with qtbot.waitSignal(widget.my_coro_task.returned, timeout=1000):
+
+ qtbot.mouseClick(widget.call_task_button, QtCore.Qt.LeftButton)
+
+ assert widget.task_was_run is True
+ assert widget.coroutine_was_run is True
+
+
+@pytest.mark.qt
+def test_call_raise(qtbot):
+ """Tests if we can call a task from a koil widget."""
+ widget = KoiledInterferingWidget()
+ qtbot.addWidget(widget)
+
+ # click in the Greet button and make sure it updates the appropriate label
+
+ with qtbot.waitSignal(widget.sleep_and_raise_task.errored, timeout=1000) as b:
+ qtbot.mouseClick(widget.call_raise_button, QtCore.Qt.LeftButton)
+
+ assert isinstance(b.args[0], Exception)
+
+
+@pytest.mark.qt
+def test_context(qtbot):
+ """Tests if we can call a task from a koil widget."""
+ widget = KoiledInterferingWidget()
+ qtbot.addWidget(widget)
+
+ x.set(5)
+ # click in the Greet button and make sure it updates the appropriate label
+
+ with qtbot.waitSignal(
+ widget.sleep_and_use_context_task.returned, timeout=1000
+ ) as b:
+ qtbot.mouseClick(widget.call_context_button, QtCore.Qt.LeftButton)
+
+ assert b.args[0] == 6
diff --git a/tests/test_koil.py b/tests/test_threaded.py
similarity index 88%
rename from tests/test_koil.py
rename to tests/test_threaded.py
index 3b76abb..92893d2 100644
--- a/tests/test_koil.py
+++ b/tests/test_threaded.py
@@ -66,17 +66,7 @@ def test_double_context():
print(c.aprint())
-def test_ierating():
-
- with Koil():
-
- x = unkoil_gen(iterating)
- assert next(x) == 1
- assert next(x) == 2
- assert next(x) == 3
-
-
-def test_ierating():
+def test_iterating():
with Koil():