From cf637f8561c11a04aae8d2f81aa277d39dff7432 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Thu, 23 Sep 2021 12:29:37 +0200 Subject: [PATCH] Scheduler (#26) * Add support for using a scheduler * Extend Qt example * Update example to PySide6 * Fix linting * cleanup repo and ci * add poetry.lock * see if this speeds things up * split into modules and lint * add module level docstrings * Add method to register callback to request flush * Remove unnecessary teardown as WeakSets icw GC is not needed * Add index array to make use of bisect in queue while flushing Also fix bug with self.has.remove * Black Co-authored-by: Berend Klein Haneveld --- .github/workflows/ci.yml | 81 ++- .gitignore | 1 - LICENSE | 2 +- examples/observe_qt.py | 81 ++- observ/__init__.py | 426 +-------------- observ/api.py | 37 ++ observ/dep.py | 27 + observ/observables.py | 298 +++++++++++ observ/scheduler.py | 97 ++++ observ/watcher.py | 107 ++++ poetry.lock | 1030 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 23 +- scripts.py | 24 - setup.cfg | 6 +- tests/test_collections.py | 8 +- tests/test_usage.py | 8 +- 16 files changed, 1770 insertions(+), 486 deletions(-) create mode 100644 observ/api.py create mode 100644 observ/dep.py create mode 100644 observ/observables.py create mode 100644 observ/scheduler.py create mode 100644 observ/watcher.py create mode 100644 poetry.lock delete mode 100644 scripts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b52cd3..3805063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,18 @@ on: push: branches: - master + tags: + - 'v*' pull_request: branches: - master jobs: - build: - name: ${{ matrix.name }} + test: + name: Lint and test on ${{ matrix.name }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: include: - name: Linux py36 @@ -23,7 +26,6 @@ jobs: pyversion: '3.8' - name: Linux py39 pyversion: '3.9' - steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.pyversion }} @@ -31,12 +33,69 @@ jobs: with: python-version: ${{ matrix.pyversion }} - name: Install poetry - run: | - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - echo "$HOME/.poetry/bin" >> $GITHUB_PATH + run: pip install "poetry>=1.1.8,<1.2" - name: Install dependencies - run: | - poetry install - - name: Lint (black and flake8) and test - run: | - poetry run test + run: poetry install + - name: Lint + run: poetry run flake8 + - name: Test + run: poetry run pytest --cov=observ --cov-report=term-missing + + build: + name: Build and test wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install poetry + run: pip install "poetry>=1.1.8,<1.2" + - name: Install dependencies + run: poetry install + - name: Build wheel + run: poetry build + - name: Twine check + run: poetry run twine check dist/* + - name: Upload wheel artifact + uses: actions/upload-artifact@v2 + with: + path: dist + name: dist + + publish: + name: Publish to Github and Pypi + runs-on: ubuntu-latest + needs: [test, build] + if: success() && startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v2 + - name: Download wheel artifact + uses: actions/download-artifact@v1.0.0 + with: + name: dist + - name: Get version from git ref + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + - name: Create GH release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.get_version.outputs.VERSION }} + release_name: Release ${{ steps.get_version.outputs.VERSION }} + draft: false + prerelease: false + - name: Upload release assets + # Move back to official action after fix https://github.com/actions/upload-release-asset/issues/4 + uses: AButler/upload-release-assets@v2.0 + with: + release-tag: ${{ steps.get_version.outputs.VERSION }} + files: 'dist/*.tar.gz;dist/*.whl' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.gitignore b/.gitignore index a5eb1f6..633a173 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -poetry.lock *.egg-info/ __pycache__ .vscode diff --git a/LICENSE b/LICENSE index 4d9a1d3..11c09a5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright © 2019-2020 observ authors +Copyright © 2019-2021 observ authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/examples/observe_qt.py b/examples/observe_qt.py index 4205010..a3bb4ef 100644 --- a/examples/observe_qt.py +++ b/examples/observe_qt.py @@ -7,9 +7,19 @@ and updates the label whenever a computed property based on the state changes. """ +from time import sleep -from observ import computed, observe, watch -from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget +from PySide6.QtCore import QObject, QThread, Signal +from PySide6.QtWidgets import ( + QApplication, + QLabel, + QProgressBar, + QPushButton, + QVBoxLayout, + QWidget, +) + +from observ import observe, scheduler, watch class Display(QWidget): @@ -19,23 +29,61 @@ def __init__(self, state, *args, **kwargs): self.state = state self.label = QLabel() + self.progress = QProgressBar() + self.progress.setMinimum(0) + self.progress.setMaximum(100) layout = QVBoxLayout() layout.addWidget(self.label) + layout.addWidget(self.progress) self.setLayout(layout) - @computed def label_text(): if state["clicked"] == 0: return "Please click the button below" return f"Clicked {state['clicked']} times!" + def progress_visible(): + return state["progress"] > 0 + self.watcher = watch(label_text, self.update_label, immediate=True) + self.progress_watch = watch( + lambda: state["progress"], + self.update_progress, + ) + self.progress_visible = watch( + progress_visible, self.update_visibility, immediate=True + ) + + def update_progress(self, old_value, new_value): + # Trigger another watcher during scheduler flush + if new_value == 50: + self.state["clicked"] += 0.5 + self.progress.setValue(new_value) def update_label(self, old_value, new_value): self.label.setText(new_value) + def update_visibility(self, old_value, new_value): + self.progress.setVisible(new_value) + + +class LongJob(QObject): + progress = Signal(int) + result = Signal(int) + finished = Signal() + + def run(self): + self.progress.emit(0) + for i in range(100): + sleep(1 / 100.0) + self.progress.emit(i + 1) + + self.progress.emit(0) + self.result.emit(1) + self.finished.emit() + class Controls(QWidget): def __init__(self, state, *args, **kwargs): @@ -56,7 +104,26 @@ def __init__(self, state, *args, **kwargs): self.reset.clicked.connect(self.on_reset_clicked) def on_button_clicked(self): - self.state["clicked"] += 1 + self.thread = QThread() + self.worker = LongJob() + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.run) + self.worker.finished.connect(self.thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + + self.thread.start() + + def progress(x): + self.state["progress"] = x + + def bump(x): + self.state["clicked"] += x * 0.5 + + self.button.setEnabled(False) + self.thread.finished.connect(lambda: self.button.setEnabled(True)) + self.worker.result.connect(lambda x: bump(x)) + self.worker.progress.connect(lambda x: progress(x)) def on_reset_clicked(self): self.state["clicked"] = 0 @@ -64,10 +131,12 @@ def on_reset_clicked(self): if __name__ == "__main__": # Define some state - state = observe({"clicked": 0}) + state = observe({"clicked": 0, "progress": 0}) app = QApplication([]) + scheduler.register_qt() + # Create layout and pass state to widgets layout = QVBoxLayout() layout.addWidget(Display(state)) @@ -78,4 +147,4 @@ def on_reset_clicked(self): widget.show() widget.setWindowTitle("Clicked?") - app.exec_() + app.exec() diff --git a/observ/__init__.py b/observ/__init__.py index 92cce17..14999ed 100644 --- a/observ/__init__.py +++ b/observ/__init__.py @@ -1,426 +1,4 @@ -__version__ = "0.3.0" +__version__ = "0.4.0" -from collections.abc import Container -from functools import wraps -from itertools import count -import sys -from typing import Any -from weakref import WeakSet - - -class Dep: - stack = [] - - def __init__(self) -> None: - self._subs = WeakSet() - - def add_sub(self, sub: "Watcher") -> None: - self._subs.add(sub) - - def remove_sub(self, sub: "Watcher") -> None: - self._subs.remove(sub) - - def depend(self) -> None: - if self.stack: - self.stack[-1].add_dep(self) - - def notify(self) -> None: - for sub in sorted(self._subs, key=lambda s: s.id): - sub.update() - - -def traverse(obj): - _traverse(obj, set()) - - -def _traverse(obj, seen): - seen.add(id(obj)) - if isinstance(obj, dict): - val_iter = iter(obj.values()) - elif isinstance(obj, (list, tuple, set)): - val_iter = iter(obj) - else: - val_iter = iter(()) - for v in val_iter: - if isinstance(v, Container) and id(v) not in seen: - _traverse(v, seen) - - -_ids = count() - - -class Watcher: - def __init__(self, fn, lazy=True, deep=False, callback=None) -> None: - self.id = next(_ids) - self.fn = fn - self._deps, self._new_deps = WeakSet(), WeakSet() - - self.callback = callback - self.deep = deep - self.lazy = lazy - self.dirty = self.lazy - self.value = None if self.lazy else self.get() - - def update(self) -> None: - self.dirty = True - if not self.lazy: - self.evaluate() - - def evaluate(self) -> None: - if self.dirty: - old_value = self.value - self.value = self.get() - self.dirty = False - if self.callback: - self.callback(old_value, self.value) - - def get(self) -> Any: - Dep.stack.append(self) - try: - value = self.fn() - finally: - if self.deep: - traverse(value) - Dep.stack.pop() - self.cleanup_deps() - return value - - def add_dep(self, dep: Dep) -> None: - if dep not in self._new_deps: - self._new_deps.add(dep) - if dep not in self._deps: - dep.add_sub(self) - - def cleanup_deps(self) -> None: - for dep in self._deps: - if dep not in self._new_deps: - dep.remove_sub(self) - self._deps, self._new_deps = self._new_deps, self._deps - self._new_deps.clear() - - def depend(self) -> None: - """This function is used by other watchers to depend on everything - this watcher depends on.""" - if Dep.stack: - for dep in self._deps: - dep.depend() - - def teardown(self) -> None: - for dep in self._deps: - dep.remove_sub(self) - - def __del__(self) -> None: - self.teardown() - - -def make_observable(cls): - def read(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - if Dep.stack: - self.__dep__.depend() - return fn(self, *args, **kwargs) - - return inner - - def read_key(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - if Dep.stack: - key = args[0] - self.__keydeps__[key].depend() - return fn(self, *args, **kwargs) - - return inner - - def write(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - args = tuple(observe(a) for a in args) - kwargs = {k: observe(v) for k, v in kwargs.items()} - retval = fn(self, *args, **kwargs) - self.__dep__.notify() - return retval - - return inner - - def write_key(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - key = args[0] - is_new = key not in self.__keydeps__ - old_value = cls.__getitem__(self, key) if not is_new else None - args = [key] + [observe(a) for a in args[1:]] - kwargs = {k: observe(v) for k, v in kwargs.items()} - retval = fn(self, *args, **kwargs) - new_value = cls.__getitem__(self, key) - if is_new: - self.__keydeps__[key] = Dep() - if old_value != new_value: - self.__keydeps__[key].notify() - self.__dep__.notify() - return retval - - return inner - - def delete(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - retval = fn(self, *args, **kwargs) - self.__dep__.notify() - for key in self._orphaned_keydeps(): - self.__keydeps__[key].notify() - del self.__keydeps__[key] - return retval - - return inner - - def delete_key(fn): - @wraps(fn) - def inner(self, *args, **kwargs): - retval = fn(self, *args, **kwargs) - # TODO prevent firing if value hasn't actually changed? - key = args[0] - self.__dep__.notify() - self.__keydeps__[key].notify() - del self.__keydeps__[key] - return retval - - return inner - - todo = [ - ("_READERS", read), - ("_KEYREADERS", read_key), - ("_WRITERS", write), - ("_KEYWRITERS", write_key), - ("_DELETERS", delete), - ("_KEYDELETERS", delete_key), - ] - - for category, decorate in todo: - for name in getattr(cls, category, set()): - fn = getattr(cls, name) - setattr(cls, name, decorate(fn)) - - return cls - - -class ObservableDict(dict): - _READERS = { - "values", - "copy", - "items", - "keys", - "__eq__", - "__format__", - "__ge__", - "__gt__", - "__iter__", - "__le__", - "__len__", - "__lt__", - "__ne__", - "__repr__", - "__sizeof__", - "__str__", - } - _KEYREADERS = { - "get", - "__contains__", - "__getitem__", - } - _WRITERS = { - "update", - } - _KEYWRITERS = { - "setdefault", - "__setitem__", - } - _DELETERS = { - "clear", - "popitem", - } - _KEYDELETERS = { - "pop", - "__delitem__", - } - - @wraps(dict.__init__) - def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - self.__dep__ = Dep() - self.__keydeps__ = {key: Dep() for key in dict.keys(self)} - - def _orphaned_keydeps(self): - return set(self.__keydeps__.keys()) - set(dict.keys(self)) - - -if sys.version_info >= (3, 8, 0): - ObservableDict._READERS.add("__reversed__") -if sys.version_info >= (3, 9, 0): - ObservableDict._READERS.add("__or__") - ObservableDict._READERS.add("__ror__") - ObservableDict._WRITERS.add("__ior__") -ObservableDict = make_observable(ObservableDict) - - -@make_observable -class ObservableList(list): - _READERS = { - "count", - "index", - "copy", - "__add__", - "__getitem__", - "__contains__", - "__eq__", - "__ge__", - "__gt__", - "__le__", - "__lt__", - "__mul__", - "__ne__", - "__rmul__", - "__iter__", - "__len__", - "__repr__", - "__str__", - "__format__", - "__reversed__", - "__sizeof__", - } - _WRITERS = { - "append", - "clear", - "extend", - "insert", - "pop", - "remove", - "reverse", - "sort", - "__setitem__", - "__delitem__", - "__iadd__", - "__imul__", - } - - @wraps(list.__init__) - def __init__(self, *args, **kwargs): - list.__init__(self, *args, **kwargs) - self.__dep__ = Dep() - - -@make_observable -class ObservableSet(set): - _READERS = { - "copy", - "difference", - "intersection", - "isdisjoint", - "issubset", - "issuperset", - "symmetric_difference", - "union", - "__and__", - "__contains__", - "__eq__", - "__format__", - "__ge__", - "__gt__", - "__iand__", - "__ior__", - "__isub__", - "__iter__", - "__ixor__", - "__le__", - "__len__", - "__lt__", - "__ne__", - "__or__", - "__rand__", - "__repr__", - "__ror__", - "__rsub__", - "__rxor__", - "__sizeof__", - "__str__", - "__sub__", - "__xor__", - } - _WRITERS = { - "add", - "clear", - "difference_update", - "intersection_update", - "discard", - "pop", - "remove", - "symmetric_difference_update", - "update", - } - - @wraps(set.__init__) - def __init__(self, *args, **kwargs): - set.__init__(self, *args, **kwargs) - self.__dep__ = Dep() - - -def observe(obj, deep=True): - if not isinstance(obj, (dict, list, tuple, set)): - return obj # common case first - elif isinstance(obj, dict): - if not isinstance(obj, ObservableDict): - reactive = ObservableDict(obj) - else: - reactive = obj - if deep: - for k, v in reactive.items(): - reactive[k] = observe(v) - return reactive - elif isinstance(obj, list): - if not isinstance(obj, ObservableList): - reactive = ObservableList(obj) - else: - reactive = obj - if deep: - for i, v in enumerate(reactive): - reactive[i] = observe(v) - return reactive - elif isinstance(obj, tuple): - reactive = obj # tuples are immutable - if deep: - reactive = tuple(observe(v) for v in reactive) - return reactive - elif isinstance(obj, set): - if deep: - return ObservableSet(observe(v) for v in obj) - else: - if not isinstance(obj, ObservableSet): - reactive = ObservableSet(obj) - else: - reactive = obj - return reactive - - -def computed(fn): - watcher = Watcher(fn) - - @wraps(fn) - def getter(): - if watcher.dirty: - watcher.evaluate() - if Dep.stack: - watcher.depend() - return watcher.value - - getter.__watcher__ = watcher - return getter - - -def watch(fn, callback, deep=False, immediate=False): - watcher = Watcher(fn, lazy=False, deep=deep, callback=callback) - if immediate: - watcher.dirty = True - watcher.evaluate() - return watcher +from .api import * diff --git a/observ/api.py b/observ/api.py new file mode 100644 index 0000000..f330578 --- /dev/null +++ b/observ/api.py @@ -0,0 +1,37 @@ +""" +Defines the public API for observ users +""" +from functools import wraps + +from .dep import Dep +from .observables import observe +from .scheduler import scheduler +from .watcher import Watcher + + +__all__ = ("observe", "computed", "watch", "scheduler") + + +def computed(fn): + watcher = Watcher(fn) + + @wraps(fn) + def getter(): + if watcher.dirty: + watcher.evaluate() + if Dep.stack: + watcher.depend() + return watcher.value + + getter.__watcher__ = watcher + return getter + + +def watch(fn, callback, sync=False, deep=False, immediate=False): + watcher = Watcher(fn, sync=sync, lazy=False, deep=deep, callback=callback) + if immediate: + watcher.dirty = True + watcher.evaluate() + if watcher.callback: + watcher.callback(None, watcher.value) + return watcher diff --git a/observ/dep.py b/observ/dep.py new file mode 100644 index 0000000..972548a --- /dev/null +++ b/observ/dep.py @@ -0,0 +1,27 @@ +""" +Deps implement the classic observable pattern, and +are attached to observable datastructures. +""" +from typing import List +from weakref import WeakSet + + +class Dep: + stack: List["Watcher"] = [] # noqa: F821 + + def __init__(self) -> None: + self._subs = WeakSet() + + def add_sub(self, sub: "Watcher") -> None: # noqa: F821 + self._subs.add(sub) + + def remove_sub(self, sub: "Watcher") -> None: # noqa: F821 + self._subs.remove(sub) + + def depend(self) -> None: + if self.stack: + self.stack[-1].add_dep(self) + + def notify(self) -> None: + for sub in sorted(self._subs, key=lambda s: s.id): + sub.update() diff --git a/observ/observables.py b/observ/observables.py new file mode 100644 index 0000000..c47ffed --- /dev/null +++ b/observ/observables.py @@ -0,0 +1,298 @@ +""" +observe converts plain datastructures (dict, list, set) to +proxied versions of those datastructures to make them reactive. +""" +from functools import wraps +import sys + +from .dep import Dep + + +def observe(obj, deep=True): + """Please be aware: this only works on plain data types!""" + if not isinstance(obj, (dict, list, tuple, set)): + return obj # common case first + elif isinstance(obj, dict): + if not isinstance(obj, ObservableDict): + reactive = ObservableDict(obj) + else: + reactive = obj + if deep: + for k, v in reactive.items(): + reactive[k] = observe(v) + return reactive + elif isinstance(obj, list): + if not isinstance(obj, ObservableList): + reactive = ObservableList(obj) + else: + reactive = obj + if deep: + for i, v in enumerate(reactive): + reactive[i] = observe(v) + return reactive + elif isinstance(obj, tuple): + reactive = obj # tuples are immutable + if deep: + reactive = tuple(observe(v) for v in reactive) + return reactive + elif isinstance(obj, set): + if deep: + return ObservableSet(observe(v) for v in obj) + else: + if not isinstance(obj, ObservableSet): + reactive = ObservableSet(obj) + else: + reactive = obj + return reactive + + +def make_observable(cls): + def read(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + if Dep.stack: + self.__dep__.depend() + return fn(self, *args, **kwargs) + + return inner + + def read_key(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + if Dep.stack: + key = args[0] + self.__keydeps__[key].depend() + return fn(self, *args, **kwargs) + + return inner + + def write(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + args = tuple(observe(a) for a in args) + kwargs = {k: observe(v) for k, v in kwargs.items()} + retval = fn(self, *args, **kwargs) + # TODO prevent firing if value hasn't actually changed? + self.__dep__.notify() + return retval + + return inner + + def write_key(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + key = args[0] + is_new = key not in self.__keydeps__ + old_value = cls.__getitem__(self, key) if not is_new else None + args = [key] + [observe(a) for a in args[1:]] + kwargs = {k: observe(v) for k, v in kwargs.items()} + retval = fn(self, *args, **kwargs) + new_value = cls.__getitem__(self, key) + if is_new: + self.__keydeps__[key] = Dep() + if old_value != new_value: + self.__keydeps__[key].notify() + self.__dep__.notify() + return retval + + return inner + + def delete(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + retval = fn(self, *args, **kwargs) + self.__dep__.notify() + for key in self._orphaned_keydeps(): + self.__keydeps__[key].notify() + del self.__keydeps__[key] + return retval + + return inner + + def delete_key(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + retval = fn(self, *args, **kwargs) + key = args[0] + self.__dep__.notify() + self.__keydeps__[key].notify() + del self.__keydeps__[key] + return retval + + return inner + + todo = [ + ("_READERS", read), + ("_KEYREADERS", read_key), + ("_WRITERS", write), + ("_KEYWRITERS", write_key), + ("_DELETERS", delete), + ("_KEYDELETERS", delete_key), + ] + + for category, decorate in todo: + for name in getattr(cls, category, set()): + fn = getattr(cls, name) + setattr(cls, name, decorate(fn)) + + return cls + + +class ObservableDict(dict): + _READERS = { + "values", + "copy", + "items", + "keys", + "__eq__", + "__format__", + "__ge__", + "__gt__", + "__iter__", + "__le__", + "__len__", + "__lt__", + "__ne__", + "__repr__", + "__sizeof__", + "__str__", + } + _KEYREADERS = { + "get", + "__contains__", + "__getitem__", + } + _WRITERS = { + "update", + } + _KEYWRITERS = { + "setdefault", + "__setitem__", + } + _DELETERS = { + "clear", + "popitem", + } + _KEYDELETERS = { + "pop", + "__delitem__", + } + + @wraps(dict.__init__) + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.__dep__ = Dep() + self.__keydeps__ = {key: Dep() for key in dict.keys(self)} + + def _orphaned_keydeps(self): + return set(self.__keydeps__.keys()) - set(dict.keys(self)) + + +if sys.version_info >= (3, 8, 0): + ObservableDict._READERS.add("__reversed__") +if sys.version_info >= (3, 9, 0): + ObservableDict._READERS.add("__or__") + ObservableDict._READERS.add("__ror__") + ObservableDict._WRITERS.add("__ior__") +ObservableDict = make_observable(ObservableDict) + + +@make_observable +class ObservableList(list): + _READERS = { + "count", + "index", + "copy", + "__add__", + "__getitem__", + "__contains__", + "__eq__", + "__ge__", + "__gt__", + "__le__", + "__lt__", + "__mul__", + "__ne__", + "__rmul__", + "__iter__", + "__len__", + "__repr__", + "__str__", + "__format__", + "__reversed__", + "__sizeof__", + } + _WRITERS = { + "append", + "clear", + "extend", + "insert", + "pop", + "remove", + "reverse", + "sort", + "__setitem__", + "__delitem__", + "__iadd__", + "__imul__", + } + + @wraps(list.__init__) + def __init__(self, *args, **kwargs): + list.__init__(self, *args, **kwargs) + self.__dep__ = Dep() + + +@make_observable +class ObservableSet(set): + _READERS = { + "copy", + "difference", + "intersection", + "isdisjoint", + "issubset", + "issuperset", + "symmetric_difference", + "union", + "__and__", + "__contains__", + "__eq__", + "__format__", + "__ge__", + "__gt__", + "__iand__", + "__ior__", + "__isub__", + "__iter__", + "__ixor__", + "__le__", + "__len__", + "__lt__", + "__ne__", + "__or__", + "__rand__", + "__repr__", + "__ror__", + "__rsub__", + "__rxor__", + "__sizeof__", + "__str__", + "__sub__", + "__xor__", + } + _WRITERS = { + "add", + "clear", + "difference_update", + "intersection_update", + "discard", + "pop", + "remove", + "symmetric_difference_update", + "update", + } + + @wraps(set.__init__) + def __init__(self, *args, **kwargs): + set.__init__(self, *args, **kwargs) + self.__dep__ = Dep() diff --git a/observ/scheduler.py b/observ/scheduler.py new file mode 100644 index 0000000..a846964 --- /dev/null +++ b/observ/scheduler.py @@ -0,0 +1,97 @@ +""" +The scheduler queues up and deduplicates re-evaluation of lazy Watchers +and should be integrated in the event loop of your choosing. +""" +from bisect import bisect + + +class Scheduler: + def __init__(self): + self._queue = [] + self._queue_indices = [] + self.flushing = False + self.has = set() + self.circular = {} + self.index = 0 + self.waiting = False + + def register_qt(self): + """ + Utility function for integration with Qt event loop + """ + # Currently only supports PySide6 + from PySide6.QtCore import QTimer + + self.timer = QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(scheduler.flush) + # Set interval to 0 to trigger the timer as soon + # as possible (when Qt is done processing events) + self.timer.setInterval(0) + self.register_flush_request(self.timer.start) + + def register_flush_request(self, request_flush): + """ + Register callback for registering a call to flush + """ + self.request_flush = request_flush + + def flush(self): + """ + Flush the queue to evaluate all queued watchers. + You can call this manually, or register a callback + to request to perform the flush. + """ + if not self._queue: + return + + self.flushing = True + self.waiting = False + self._queue.sort(key=lambda s: s.id) + self._queue_indices.sort() + + while self.index < len(self._queue): + watcher = self._queue[self.index] + self.has.discard(watcher.id) + watcher.run() + + if watcher.id in self.has: + self.circular[watcher.id] = self.circular.get(watcher.id, 0) + 1 + if self.circular[watcher.id] > 100: + # TODO: help user to figure out which watcher + # or function this is about + raise RecursionError("Infinite update loop detected") + + self.index += 1 + + self._queue.clear() + self._queue_indices.clear() + self.flushing = False + self.has.clear() + self.circular.clear() + self.index = 0 + + def queue(self, watcher: "Watcher"): # noqa: F821 + if watcher.id in self.has: + return + + self.has.add(watcher.id) + if not self.flushing: + self._queue.append(watcher) + self._queue_indices.append(watcher.id) + if not self.waiting and self.request_flush: + self.waiting = True + self.request_flush() + else: + # If already flushing, splice the watcher based on its id + # If already past its id, it will be run next immediately. + # Last part of the queue should stay ordered, in order to + # properly make use of bisect and avoid deadlocks + i = bisect(self._queue_indices[self.index + 1 :], watcher.id) + i += self.index + 1 + self._queue.insert(i, watcher) + self._queue_indices.insert(i, watcher.id) + + +# Construct global instance +scheduler = Scheduler() diff --git a/observ/watcher.py b/observ/watcher.py new file mode 100644 index 0000000..221ea08 --- /dev/null +++ b/observ/watcher.py @@ -0,0 +1,107 @@ +""" +watchers perform dependency tracking via functions acting on +observable datastructures, and optionally trigger callback when +a change is detected. +""" +from collections.abc import Container +from itertools import count +from typing import Any +from weakref import WeakSet + +from .dep import Dep +from .scheduler import scheduler + + +def traverse(obj): + _traverse(obj, set()) + + +def _traverse(obj, seen): + seen.add(id(obj)) + if isinstance(obj, dict): + val_iter = iter(obj.values()) + elif isinstance(obj, (list, tuple, set)): + val_iter = iter(obj) + else: + val_iter = iter(()) + for v in val_iter: + if isinstance(v, Container) and id(v) not in seen: + _traverse(v, seen) + + +# Every Watcher gets a unique ID which is used to +# keep track of the order in which subscribers will +# be notified +_ids = count() + + +class Watcher: + def __init__(self, fn, sync=False, lazy=True, deep=False, callback=None) -> None: + """ + sync: Ignore the scheduler + lazy: Only reevalutate when value is requested + deep: Deep watch the watched value + callback: Method to call when value has changed + """ + self.id = next(_ids) + self.fn = fn + self._deps, self._new_deps = WeakSet(), WeakSet() + + self.sync = sync + self.callback = callback + self.deep = deep + self.lazy = lazy + self.dirty = self.lazy + self.value = None if self.lazy else self.get() + + def update(self) -> None: + if self.lazy: + self.dirty = True + elif self.sync: + self.run() + else: + scheduler.queue(self) + + def evaluate(self) -> None: + self.value = self.get() + self.dirty = False + + def run(self) -> None: + """Called by scheduler""" + value = self.get() + if self.deep or isinstance(value, Container) or value != self.value: + old_value = self.value + self.value = value + if self.callback: + self.callback(old_value, self.value) + + def get(self) -> Any: + Dep.stack.append(self) + try: + value = self.fn() + finally: + if self.deep: + traverse(value) + Dep.stack.pop() + self.cleanup_deps() + return value + + def add_dep(self, dep: Dep) -> None: + if dep not in self._new_deps: + self._new_deps.add(dep) + if dep not in self._deps: + dep.add_sub(self) + + def cleanup_deps(self) -> None: + for dep in self._deps: + if dep not in self._new_deps: + dep.remove_sub(self) + self._deps, self._new_deps = self._new_deps, self._deps + self._new_deps.clear() + + def depend(self) -> None: + """This function is used by other watchers to depend on everything + this watcher depends on.""" + if Dep.stack: + for dep in self._deps: + dep.depend() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..31776d8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1030 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "bleach" +version = "4.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.6" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.6" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "cryptography" +version = "3.4.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "dev" +optional = false +python-versions = ">=3.6, <3.7" + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "flake8-black" +version = "0.2.3" +description = "flake8 plugin to call black as a code style validator" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +black = "*" +flake8 = ">=3.0.0" +toml = "*" + +[[package]] +name = "flake8-import-order" +version = "0.18.1" +description = "Flake8 and pylama plugin that checks the ordering of import statements." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = "*" + +[[package]] +name = "flake8-print" +version = "4.0.0" +description = "print statement checker plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +flake8 = ">=3.0" +pycodestyle = "*" +six = "*" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jeepney" +version = "0.7.1" +description = "Low-level, pure Python DBus protocol wrapper." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] +trio = ["trio", "async-generator"] + +[[package]] +name = "keyring" +version = "23.2.1" +description = "Store and access your passwords safely." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = ">=3.6" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pkginfo" +version = "1.7.1" +description = "Query metadatdata from sdists / bdists / installed packages." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +testing = ["nose", "coverage"] + +[[package]] +name = "pluggy" +version = "1.0.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\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pyside6" +version = "6.1.3" +description = "Python bindings for the Qt cross-platform application and UI framework" +category = "dev" +optional = false +python-versions = ">=3.6, <3.10" + +[package.dependencies] +shiboken6 = "6.1.3" + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "readme-renderer" +version = "29.0" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" +six = "*" + +[package.extras] +md = ["cmarkgfm (>=0.5.0,<0.6.0)"] + +[[package]] +name = "regex" +version = "2021.8.28" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "secretstorage" +version = "3.3.1" +description = "Python bindings to FreeDesktop.org Secret Service API" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "shiboken6" +version = "6.1.3" +description = "Python / C++ bindings helper module" +category = "dev" +optional = false +python-versions = ">=3.6, <3.10" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tqdm" +version = "4.62.3" +description = "Fast, Extensible Progress Meter" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +telegram = ["requests"] + +[[package]] +name = "twine" +version = "3.4.2" +description = "Collection of utilities for publishing packages on PyPI" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = ">=0.4.3" +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.4.2" +readme-renderer = ">=21.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +tqdm = ">=4.14" + +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.5.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.6,<3.10" +content-hash = "50794712bb36b65563b8b5d7c7fd5469fedb0f4a7982b72bedde6c59a436e546" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +bleach = [ + {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, + {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +cffi = [ + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, + {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +cryptography = [ + {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, + {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, + {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, + {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, + {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +flake8-black = [ + {file = "flake8-black-0.2.3.tar.gz", hash = "sha256:c199844bc1b559d91195ebe8620216f21ed67f2cc1ff6884294c91a0d2492684"}, + {file = "flake8_black-0.2.3-py3-none-any.whl", hash = "sha256:cc080ba5b3773b69ba102b6617a00cc4ecbad8914109690cfda4d565ea435d96"}, +] +flake8-import-order = [ + {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"}, + {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"}, +] +flake8-print = [ + {file = "flake8-print-4.0.0.tar.gz", hash = "sha256:5afac374b7dc49aac2c36d04b5eb1d746d72e6f5df75a6ecaecd99e9f79c6516"}, + {file = "flake8_print-4.0.0-py3-none-any.whl", hash = "sha256:6c0efce658513169f96d7a24cf136c434dc711eb00ebd0a985eb1120103fe584"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +jeepney = [ + {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, + {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, +] +keyring = [ + {file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"}, + {file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +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.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pkginfo = [ + {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"}, + {file = "pkginfo-1.7.1.tar.gz", hash = "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyside6 = [ + {file = "PySide6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-abi3-macosx_10_14_x86_64.whl", hash = "sha256:619b6bb4a3e5451fe939983cb9f5155f2cded44c8449f60a1d250747a48f00f3"}, + {file = "PySide6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-abi3-manylinux1_x86_64.whl", hash = "sha256:64d69fd2d38b47982c06619f9969b6ae5c632f21f8ff2432e4953b0179a21411"}, + {file = "PySide6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:cbbe1a2bacca737b3000a8a18b0ba389ca93106800b10acb93660c1a5b6bfdec"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] +readme-renderer = [ + {file = "readme_renderer-29.0-py2.py3-none-any.whl", hash = "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c"}, + {file = "readme_renderer-29.0.tar.gz", hash = "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"}, +] +regex = [ + {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, + {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, + {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, + {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, + {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, + {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, + {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, + {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, + {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, + {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, + {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, + {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, + {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, + {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, + {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, + {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +secretstorage = [ + {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, + {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, +] +shiboken6 = [ + {file = "shiboken6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-abi3-macosx_10_14_x86_64.whl", hash = "sha256:7d285cb61dca4d543ea1ccd6f9bad1b555f222232dfc7240ebc7df83e21fb805"}, + {file = "shiboken6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-abi3-manylinux1_x86_64.whl", hash = "sha256:c44d406263c1cbd1cb92efca02790c39b1563a25e888e3e5c7291aabd2363aee"}, + {file = "shiboken6-6.1.3-6.1.3-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:10023b1b6b27318db74a2232e808c8e5691ec1ff39be89d9878fc60e0adbb690"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tqdm = [ + {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, + {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, +] +twine = [ + {file = "twine-3.4.2-py3-none-any.whl", hash = "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218"}, + {file = "twine-3.4.2.tar.gz", hash = "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"}, +] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +zipp = [ + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, +] diff --git a/pyproject.toml b/pyproject.toml index dc021ac..88e7bac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,25 @@ [tool.poetry] name = "observ" -version = "0.3.0" -description = "" +version = "0.4.0" +description = "Reactive state management for Python" authors = ["Korijn van Golen "] license = "MIT" homepage = "https://github.com/Korijn/observ" readme = "README.md" [tool.poetry.dependencies] -python = ">=3.6" +python = ">=3.6,<3.10" [tool.poetry.dev-dependencies] -black = "^19.10b0" -flake8 = "^3.8.3" -flake8-black = "^0.2.0" -flake8-import-order = "^0.18.1" -flake8-print = "^3.1.4" -pytest = "^5.4.3" -pytest-cov = "^2.10.0" -# PyQt5 = "^5.15.4" # needed for PyQt example +black = "*" +flake8 = "*" +flake8-black = "*" +flake8-import-order = "*" +flake8-print = "*" +pytest = "*" +pytest-cov = "*" +PySide6 = "*" +twine = "*" [build-system] requires = ["poetry>=0.12"] diff --git a/scripts.py b/scripts.py deleted file mode 100644 index 837ac1d..0000000 --- a/scripts.py +++ /dev/null @@ -1,24 +0,0 @@ -from subprocess import CalledProcessError, run -import sys - - -def test(args): - try: - run(["flake8"], check=True) - run( - ["pytest", "--cov=observ", "--cov-report=term-missing"] + args, check=True, - ) - except CalledProcessError: - sys.exit(1) - - -def main(): - cmd = sys.argv[0] - if cmd == "test": - test(sys.argv[1:]) - else: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index 8fe7c74..769c63e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,7 @@ max-line-length = 88 extend-ignore = # See https://github.com/PyCQA/pycodestyle/issues/373 E203, -application-import-names = pylinalg -import-order-style = google \ No newline at end of file +application-import-names = observ +import-order-style = google +per-file-ignores = + observ/__init__.py:F401,F403 diff --git a/tests/test_collections.py b/tests/test_collections.py index ffb83b4..8bcc38e 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,6 +1,7 @@ from unittest.mock import Mock -from observ import Dep, ObservableDict, ObservableList, ObservableSet +from observ.dep import Dep +from observ.observables import ObservableDict, ObservableList, ObservableSet COLLECTIONS = { @@ -67,7 +68,10 @@ def test_list_notify(): "remove": (2,), "reverse": (), "sort": (), - "__setitem__": (0, 5,), + "__setitem__": ( + 0, + 5, + ), "__delitem__": (0,), "__iadd__": ([5],), "__imul__": (5,), diff --git a/tests/test_usage.py b/tests/test_usage.py index d2e3eb8..6962a8d 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -48,7 +48,7 @@ def _callback(old_value, new_value): nonlocal called called += 1 - watcher = watch(lambda: a["quuz"], _callback, deep=True, immediate=False) + watcher = watch(lambda: a["quuz"], _callback, sync=True, deep=True, immediate=False) assert not watcher.dirty assert watcher.value == a["quuz"] assert len(watcher._deps) > 1 @@ -75,7 +75,7 @@ def _callback(old_value, new_value): nonlocal called called += 1 - watcher = watch(lambda: a["quuz"], _callback, deep=True, immediate=True) + watcher = watch(lambda: a["quuz"], _callback, sync=True, deep=True, immediate=True) assert not watcher.dirty assert watcher.value == a["quuz"] assert len(watcher._deps) > 1 @@ -100,8 +100,8 @@ def _deep_callback(old_value, new_value): nonlocal deep_called deep_called += 1 - watcher = watch(lambda: a["foo"], _non_deep_callback) - deep_watcher = watch(lambda: a["foo"], _deep_callback, deep=True) + watcher = watch(lambda: a["foo"], _non_deep_callback, sync=True) + deep_watcher = watch(lambda: a["foo"], _deep_callback, sync=True, deep=True) assert not watcher.dirty assert not deep_watcher.dirty assert non_deep_called == 0