├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── async_class.py ├── pylava.ini ├── setup.cfg ├── setup.py ├── tests ├── test_async_class.py ├── test_async_object.py └── test_task_store.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */env*/* 5 | */tests/* 6 | */.*/* 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{py,yml}] 10 | indent_style = space 11 | 12 | [*.py] 13 | indent_size = 4 14 | 15 | [docs/**.py] 16 | max_line_length = 80 17 | 18 | [*.rst] 19 | indent_size = 4 20 | indent_style = space 21 | 22 | [Makefile] 23 | indent_style = tab 24 | 25 | [*.yml] 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | include: 20 | - python: '3.9' 21 | toxenv: mypy 22 | os: ubuntu-latest 23 | 24 | - python: '3.9' 25 | toxenv: pylava 26 | os: ubuntu-latest 27 | 28 | - python: '3.9' 29 | toxenv: checkdoc 30 | os: ubuntu-latest 31 | 32 | - python: '3.6' 33 | toxenv: py36 34 | os: ubuntu-latest 35 | - python: '3.7' 36 | toxenv: py37 37 | os: ubuntu-latest 38 | - python: '3.8' 39 | toxenv: py38 40 | os: ubuntu-latest 41 | - python: '3.9' 42 | toxenv: py39 43 | os: ubuntu-latest 44 | 45 | - python: '3.6' 46 | toxenv: py36 47 | os: macos-latest 48 | - python: '3.7' 49 | toxenv: py37 50 | os: macos-latest 51 | - python: '3.8' 52 | toxenv: py38 53 | os: macos-latest 54 | - python: '3.9' 55 | toxenv: py39 56 | os: macos-latest 57 | 58 | - python: '3.6' 59 | toxenv: py36 60 | os: windows-latest 61 | - python: '3.7' 62 | toxenv: py37 63 | os: windows-latest 64 | - python: '3.8' 65 | toxenv: py38 66 | os: windows-latest 67 | - python: '3.9' 68 | toxenv: py39 69 | os: windows-latest 70 | 71 | steps: 72 | - uses: actions/checkout@v2 73 | - name: Set up Python ${{ matrix.python }} 74 | uses: actions/setup-python@v1 75 | with: 76 | python-version: ${{ matrix.python }} 77 | 78 | - name: Install tox 79 | env: 80 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 81 | TOXENV: ${{ matrix.toxenv }} 82 | run: pip install tox wheel 83 | 84 | - name: Tests 85 | env: 86 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 87 | FORCE_COLOR: yes 88 | TOXENV: ${{ matrix.toxenv }} 89 | run: tox 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | [Ss]cripts 12 | pyvenv.cfg 13 | .venv 14 | pip-selfcheck.json 15 | ### IPythonNotebook template 16 | # Temporary data 17 | .ipynb_checkpoints/ 18 | ### Python template 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *,cover 64 | .hypothesis/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | docs/source/apidoc 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # IPython Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # dotenv 98 | .env 99 | 100 | # virtualenv 101 | venv/ 102 | ENV/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | ### JetBrains template 110 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 111 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 112 | 113 | # User-specific stuff: 114 | .idea/ 115 | 116 | ## File-based project format: 117 | *.iws 118 | 119 | ## Plugin-specific files: 120 | 121 | # IntelliJ 122 | /out/ 123 | 124 | # mpeltonen/sbt-idea plugin 125 | .idea_modules/ 126 | 127 | # JIRA plugin 128 | atlassian-ide-plugin.xml 129 | 130 | # Crashlytics plugin (for Android Studio and IntelliJ) 131 | com_crashlytics_export_strings.xml 132 | crashlytics.properties 133 | crashlytics-build.properties 134 | fabric.properties 135 | 136 | /htmlcov 137 | /temp 138 | .DS_Store 139 | 140 | /*/version.py 141 | 142 | .pytest_cache 143 | 144 | # Created by https://www.gitignore.io/api/vim 145 | # Edit at https://www.gitignore.io/?templates=vim 146 | 147 | ### Vim ### 148 | # Swap 149 | [._]*.s[a-v][a-z] 150 | [._]*.sw[a-p] 151 | [._]s[a-rt-v][a-z] 152 | [._]ss[a-gi-z] 153 | [._]sw[a-p] 154 | 155 | # Session 156 | Session.vim 157 | 158 | # Temporary 159 | .netrwhist 160 | *~ 161 | # Auto-generated tag files 162 | tags 163 | # Persistent undo 164 | [._]*.un~ 165 | 166 | # End of https://www.gitignore.io/api/vim 167 | 168 | *.ipynb 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2021` `Dmitry Orlov ` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude .* 2 | 3 | include README.rst 4 | include *.pyi 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://coveralls.io/repos/github/mosquito/aiormq/badge.svg?branch=master 2 | :target: https://coveralls.io/github/mosquito/async-class?branch=master 3 | :alt: Coveralls 4 | 5 | .. image:: https://img.shields.io/pypi/l/async-class 6 | :target: https://pypi.org/project/async-class 7 | :alt: License 8 | 9 | .. image:: https://github.com/mosquito/async-class/workflows/tests/badge.svg 10 | :target: https://github.com/mosquito/async-class/actions?query=workflow%3Atests 11 | :alt: Build status 12 | 13 | .. image:: https://img.shields.io/pypi/wheel/async-class 14 | :target: https://pypi.python.org/pypi/async-class/ 15 | :alt: Wheel 16 | 17 | .. image:: https://img.shields.io/pypi/v/async-class 18 | :target: https://pypi.org/project/async-class 19 | :alt: Latest version 20 | 21 | 22 | async-class 23 | =========== 24 | 25 | Adding abillity to write classes with awaitable initialization function. 26 | 27 | .. contents:: Table of contents 28 | 29 | Usage example 30 | ============= 31 | 32 | .. code:: python 33 | :name: test_simple 34 | 35 | import asyncio 36 | from async_class import AsyncClass, AsyncObject, task, link 37 | 38 | 39 | class MyAsyncClass(AsyncClass): 40 | async def __ainit__(self): 41 | # Do async staff here 42 | pass 43 | 44 | 45 | class MainClass(AsyncObject): 46 | async def __ainit__(self): 47 | # Do async staff here 48 | pass 49 | 50 | async def __adel__(self): 51 | """ This method will be called when object will be closed """ 52 | pass 53 | 54 | 55 | class RelatedClass(AsyncObject): 56 | async def __ainit__(self, parent: MainClass): 57 | link(self, parent) 58 | 59 | 60 | async def main(): 61 | instance = await MyAsyncClass() 62 | print(instance) 63 | 64 | main_instance = await MainClass() 65 | related_instance = await RelatedClass(main_instance) 66 | 67 | assert not main_instance.is_closed 68 | assert not related_instance.is_closed 69 | 70 | await main_instance.close() 71 | assert main_instance.is_closed 72 | 73 | # will be closed because linked to closed main_instance 74 | assert related_instance.is_closed 75 | 76 | asyncio.run(main()) 77 | 78 | 79 | Documentation 80 | ============= 81 | 82 | Async objects might be created when no one event loop has been running. 83 | ``self.loop`` property is lazily evaluated. 84 | 85 | Module provides useful abstractions for writing async code. 86 | 87 | Objects inherited from ``AsyncClass`` might have their own ``__init__`` 88 | method, but it strictly not recommend. 89 | 90 | Class ``AsyncClass`` 91 | -------------------- 92 | 93 | Is a base wrapper with metaclass has no ``TaskStore`` instance and 94 | additional methods like ``self.create_task`` and ``self.create_future``. 95 | 96 | This class just solves the initialization problem: 97 | 98 | .. code:: python 99 | :name: test_async_class 100 | 101 | import asyncio 102 | from async_class import AsyncClass 103 | 104 | 105 | class MyAsyncClass(AsyncClass): 106 | async def __ainit__(self): 107 | future = self.loop.create_future() 108 | self.loop.call_soon(future.set_result, True) 109 | await future 110 | 111 | 112 | async def main(): 113 | instance = await MyAsyncClass() 114 | print(instance) 115 | 116 | asyncio.run(main()) 117 | 118 | 119 | Class ``AsyncObject`` 120 | ------------------------- 121 | 122 | Base class with task store instance and helpers for simple task 123 | management. 124 | 125 | .. code:: python 126 | :name: test_async_object 127 | 128 | import asyncio 129 | from async_class import AsyncObject 130 | 131 | 132 | class MyClass(AsyncObject): 133 | async def __ainit__(self): 134 | self.task = self.create_task(asyncio.sleep(3600)) 135 | 136 | 137 | async def main(): 138 | obj = await MyClass() 139 | 140 | assert not obj.task.done() 141 | 142 | await obj.close() 143 | 144 | assert obj.task.done() 145 | 146 | 147 | asyncio.run(main()) 148 | 149 | 150 | Class ``TaskStore`` 151 | ------------------- 152 | 153 | ``TaskStore`` is a task management helper. One instance has 154 | ``create_task()`` and ``create_future()`` methods and all created 155 | entities will be destroyed when ``TaskStore`` will be closed via 156 | ``close()`` method. 157 | 158 | Also, a task store might create a linked copy of the self, which will be 159 | closed when the parent instance will be closed. 160 | 161 | .. code:: python 162 | :name: test_tasK_store 163 | 164 | import asyncio 165 | from async_class import TaskStore 166 | 167 | 168 | async def main(): 169 | store = TaskStore(asyncio.get_event_loop()) 170 | 171 | task1 = store.create_task(asyncio.sleep(3600)) 172 | 173 | child_store = store.get_child() 174 | task2 = child_store.create_task(asyncio.sleep(3600)) 175 | 176 | await store.close() 177 | 178 | assert task1.done() and task2.done() 179 | 180 | 181 | asyncio.run(main()) 182 | -------------------------------------------------------------------------------- /async_class.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | import logging 4 | from contextlib import suppress 5 | from functools import wraps 6 | from typing import ( 7 | Any, Awaitable, Callable, Coroutine, Dict, Generator, List, MutableSet, 8 | NoReturn, Optional, Set, Tuple, TypeVar, Union, 9 | ) 10 | from weakref import WeakSet 11 | 12 | 13 | get_running_loop = getattr(asyncio, "get_running_loop", asyncio.get_event_loop) 14 | log = logging.getLogger(__name__) 15 | CloseCallbacksType = Callable[[], Union[Any, Coroutine]] 16 | 17 | 18 | class TaskStore: 19 | def __init__(self, loop: asyncio.AbstractEventLoop): 20 | self.tasks: MutableSet[asyncio.Task] = WeakSet() 21 | self.futures: MutableSet[asyncio.Future] = WeakSet() 22 | self.children: MutableSet[TaskStore] = WeakSet() 23 | self.close_callbacks: Set[CloseCallbacksType] = set() 24 | self.__loop = loop 25 | self.__closing: asyncio.Future = self.__loop.create_future() 26 | 27 | def get_child(self) -> "TaskStore": 28 | store = self.__class__(self.__loop) 29 | self.children.add(store) 30 | return store 31 | 32 | def add_close_callback(self, func: CloseCallbacksType) -> None: 33 | self.close_callbacks.add(func) 34 | 35 | def create_task(self, *args: Any, **kwargs: Any) -> asyncio.Task: 36 | task = self.__loop.create_task(*args, **kwargs) 37 | self.tasks.add(task) 38 | task.add_done_callback(self.tasks.remove) 39 | return task 40 | 41 | def create_future(self) -> asyncio.Future: 42 | future = self.__loop.create_future() 43 | self.futures.add(future) 44 | future.add_done_callback(self.futures.remove) 45 | return future 46 | 47 | @property 48 | def is_closed(self) -> bool: 49 | return self.__closing.done() 50 | 51 | async def close(self, exc: Optional[Exception] = None) -> None: 52 | if self.__closing.done(): 53 | return 54 | 55 | if exc is None: 56 | self.__closing.set_result(True) 57 | else: 58 | self.__closing.set_exception(exc) 59 | 60 | for future in self.futures: 61 | if future.done(): 62 | continue 63 | 64 | future.set_exception( 65 | exc or asyncio.CancelledError("Object %r closed" % self), 66 | ) 67 | 68 | tasks: List[Union[asyncio.Future, Coroutine]] = [] 69 | 70 | for func in self.close_callbacks: 71 | try: 72 | result = func() 73 | except BaseException: 74 | log.exception("Error in close callback %r", func) 75 | continue 76 | 77 | if ( 78 | asyncio.iscoroutine(result) or 79 | isinstance(result, asyncio.Future) 80 | ): 81 | tasks.append(result) 82 | 83 | for task in self.tasks: 84 | if task.done(): 85 | continue 86 | 87 | task.cancel() 88 | tasks.append(task) 89 | 90 | for store in self.children: 91 | tasks.append(store.close()) 92 | 93 | await asyncio.gather(*tasks, return_exceptions=True) 94 | 95 | 96 | class AsyncClassMeta(abc.ABCMeta): 97 | def __new__( 98 | cls, 99 | clsname: str, 100 | bases: Tuple[type, ...], 101 | namespace: Dict[str, Any], 102 | ) -> "AsyncClassMeta": 103 | instance = super(AsyncClassMeta, cls).__new__( 104 | cls, clsname, bases, namespace, 105 | ) 106 | 107 | if not asyncio.iscoroutinefunction(instance.__ainit__): # type: ignore 108 | raise TypeError("__ainit__ must be coroutine") 109 | 110 | return instance 111 | 112 | 113 | ArgsType = Any 114 | KwargsType = Any 115 | 116 | 117 | class AsyncClass(metaclass=AsyncClassMeta): 118 | __slots__ = ("_args", "_kwargs") 119 | _args: ArgsType 120 | _kwargs: KwargsType 121 | 122 | def __new__(cls, *args: Any, **kwargs: Any) -> "AsyncClass": 123 | self = super().__new__(cls) 124 | self._args = args 125 | self._kwargs = kwargs 126 | return self 127 | 128 | @property 129 | def loop(self) -> asyncio.AbstractEventLoop: 130 | return get_running_loop() 131 | 132 | def __await__(self) -> Generator[None, Any, "AsyncClass"]: 133 | yield from self.__ainit__(*self._args, **self._kwargs).__await__() 134 | return self 135 | 136 | async def __ainit__(self, *args: Any, **kwargs: Any) -> NoReturn: 137 | pass 138 | 139 | 140 | # noinspection PyAttributeOutsideInit 141 | class AsyncObject(AsyncClass): 142 | def __init__(self, *args: ArgsType, **kwargs: KwargsType): 143 | self.__closed = False 144 | self._async_class_task_store: TaskStore 145 | 146 | @property 147 | def __tasks__(self) -> TaskStore: 148 | return self._async_class_task_store 149 | 150 | @property 151 | def is_closed(self) -> bool: 152 | return self.__closed 153 | 154 | def create_task( 155 | self, *args: ArgsType, **kwargs: KwargsType 156 | ) -> asyncio.Task: 157 | return self.__tasks__.create_task(*args, **kwargs) 158 | 159 | def create_future(self) -> asyncio.Future: 160 | return self.__tasks__.create_future() 161 | 162 | async def __adel__(self) -> None: 163 | pass 164 | 165 | def __init_subclass__(cls, **kwargs: KwargsType): 166 | if getattr(cls, "__await__") is not AsyncObject.__await__: 167 | raise TypeError("__await__ redeclaration is forbidden") 168 | 169 | def __await__(self) -> Generator[Any, None, "AsyncObject"]: 170 | if not hasattr(self, "_async_class_task_store"): 171 | self._async_class_task_store = TaskStore(self.loop) 172 | 173 | yield from self.create_task( 174 | self.__ainit__(*self._args, **self._kwargs), 175 | ).__await__() 176 | return self 177 | 178 | def __del__(self) -> None: 179 | if self.__closed: 180 | return 181 | 182 | with suppress(BaseException): 183 | self.loop.create_task(self.close()) 184 | 185 | async def close(self, exc: Optional[Exception] = None) -> None: 186 | if self.__closed: 187 | return 188 | 189 | tasks: List[Union[asyncio.Future, Coroutine]] = [] 190 | 191 | if hasattr(self, "_async_class_task_store"): 192 | tasks.append(self.__adel__()) 193 | tasks.append(self.__tasks__.close(exc)) 194 | self.__closed = True 195 | 196 | if not tasks: 197 | return 198 | 199 | await asyncio.gather(*tasks, return_exceptions=True) 200 | 201 | 202 | T = TypeVar("T") 203 | 204 | 205 | def task( 206 | func: Callable[..., Coroutine[Any, None, T]], 207 | ) -> Callable[..., Awaitable[T]]: 208 | @wraps(func) 209 | def wrap( 210 | self: AsyncObject, 211 | *args: ArgsType, 212 | **kwargs: KwargsType 213 | ) -> Awaitable[T]: 214 | # noinspection PyCallingNonCallable 215 | return self.create_task(func(self, *args, **kwargs)) 216 | 217 | return wrap 218 | 219 | 220 | def link(who: AsyncObject, where: AsyncObject) -> None: 221 | who._async_class_task_store = where.__tasks__.get_child() 222 | who.__tasks__.add_close_callback(who.close) 223 | 224 | 225 | __all__ = ( 226 | "AsyncClass", 227 | "AsyncObject", 228 | "CloseCallbacksType", 229 | "TaskStore", 230 | "link", 231 | "task", 232 | ) 233 | -------------------------------------------------------------------------------- /pylava.ini: -------------------------------------------------------------------------------- 1 | [pylava] 2 | ignore=C901,E252 3 | skip = *env*,.tox*,*build*,.* 4 | 5 | [pylava:pycodestyle] 6 | max_line_length = 80 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | disallow_any_generics = False 4 | disallow_incomplete_defs = True 5 | disallow_subclassing_any = True 6 | disallow_untyped_calls = True 7 | disallow_untyped_decorators = True 8 | disallow_untyped_defs = True 9 | follow_imports = silent 10 | no_implicit_reexport = True 11 | strict_optional = True 12 | warn_redundant_casts = True 13 | warn_unused_configs = True 14 | warn_unused_ignores = True 15 | 16 | [mypy-tests.*] 17 | ignore_errors = True 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="async-class", 6 | version="0.5.0", 7 | description="Write classes with async def __ainit__", 8 | long_description=open("README.rst").read(), 9 | license="MIT", 10 | packages=["."], 11 | project_urls={"Source": "https://github.com/mosquito/async-class"}, 12 | classifiers=[ 13 | "License :: OSI Approved :: MIT License", 14 | "Topic :: Internet", 15 | "Topic :: Software Development", 16 | "Topic :: Software Development :: Libraries", 17 | "Intended Audience :: Developers", 18 | "Natural Language :: English", 19 | "Operating System :: MacOS", 20 | "Operating System :: POSIX", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: Implementation :: CPython", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_async_class.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from async_class import AsyncClass 4 | 5 | 6 | class GlobalInitializedClass(AsyncClass): 7 | pass 8 | 9 | 10 | global_initialized_instance = GlobalInitializedClass() 11 | 12 | 13 | async def test_global_initialized_instance(loop): 14 | await global_initialized_instance 15 | 16 | 17 | async def test_simple(): 18 | await AsyncClass() 19 | 20 | 21 | async def test_simple_class(): 22 | class Sample(AsyncClass): 23 | event = asyncio.Event() 24 | 25 | async def __ainit__(self): 26 | loop = asyncio.get_event_loop() 27 | loop.call_soon(self.event.set) 28 | await self.event.wait() 29 | 30 | instance = await Sample() 31 | 32 | assert instance.__class__ == Sample 33 | 34 | assert Sample.event.is_set() 35 | 36 | 37 | async def test_simple_inheritance(): 38 | class Sample(AsyncClass): 39 | event = asyncio.Event() 40 | 41 | async def __ainit__(self): 42 | loop = asyncio.get_event_loop() 43 | loop.call_soon(self.event.set) 44 | await self.event.wait() 45 | 46 | class MySample(Sample): 47 | async def __ainit__(self): 48 | await super().__ainit__() 49 | 50 | instance = await MySample() 51 | 52 | assert instance.__class__ == MySample 53 | assert instance.__class__ != Sample 54 | 55 | assert Sample.event.is_set() 56 | assert MySample.event.is_set() 57 | 58 | 59 | async def test_simple_with_init(): 60 | class Sample(AsyncClass): 61 | event = asyncio.Event() 62 | 63 | def __init__(self): 64 | self.value = 3 65 | 66 | async def __ainit__(self): 67 | loop = asyncio.get_event_loop() 68 | loop.call_soon(self.event.set) 69 | 70 | await self.event.wait() 71 | 72 | instance = await Sample() 73 | 74 | assert instance.__class__ == Sample 75 | 76 | assert Sample.event.is_set() 77 | assert instance.value == 3 78 | 79 | 80 | async def test_simple_with_init_inheritance(): 81 | class Sample(AsyncClass): 82 | event = asyncio.Event() 83 | 84 | def __init__(self): 85 | self.value = 3 86 | 87 | async def __ainit__(self): 88 | loop = asyncio.get_event_loop() 89 | loop.call_soon(self.event.set) 90 | 91 | await self.event.wait() 92 | 93 | class MySample(Sample): 94 | pass 95 | 96 | instance = await MySample() 97 | 98 | assert instance.__class__ == MySample 99 | 100 | assert Sample.event.is_set() 101 | assert MySample.event.is_set() 102 | assert instance.value == 3 103 | -------------------------------------------------------------------------------- /tests/test_async_object.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import gc 3 | 4 | import pytest 5 | 6 | from async_class import AsyncObject, TaskStore, link, task 7 | 8 | 9 | class GlobalInitializedClass(AsyncObject): 10 | pass 11 | 12 | 13 | global_initialized_instance = GlobalInitializedClass() 14 | 15 | 16 | async def test_global_initialized_instance(loop): 17 | await global_initialized_instance 18 | assert not global_initialized_instance.is_closed 19 | 20 | 21 | async def test_simple(): 22 | await AsyncObject() 23 | 24 | 25 | async def test_simple_class(): 26 | class Simple(AsyncObject): 27 | event = asyncio.Event() 28 | 29 | async def __ainit__(self): 30 | self.loop.call_soon(self.event.set) 31 | await self.event.wait() 32 | 33 | instance = await Simple() 34 | 35 | assert instance.__class__ == Simple 36 | 37 | assert Simple.event.is_set() 38 | 39 | 40 | async def test_simple_inheritance(): 41 | class Simple(AsyncObject): 42 | event = asyncio.Event() 43 | 44 | async def __ainit__(self): 45 | self.loop.call_soon(self.event.set) 46 | await self.event.wait() 47 | 48 | def __del__(self): 49 | return super().__del__() 50 | 51 | class MySimple(Simple): 52 | pass 53 | 54 | instance = await MySimple() 55 | 56 | assert instance.__class__ == MySimple 57 | assert instance.__class__ != Simple 58 | 59 | assert Simple.event.is_set() 60 | assert MySimple.event.is_set() 61 | 62 | 63 | async def test_simple_with_init(): 64 | class Simple(AsyncObject): 65 | event = asyncio.Event() 66 | 67 | def __init__(self): 68 | super().__init__() 69 | self.value = 3 70 | 71 | async def __ainit__(self): 72 | self.loop.call_soon(self.event.set) 73 | await self.event.wait() 74 | 75 | instance = await Simple() 76 | 77 | assert instance.__class__ == Simple 78 | 79 | assert Simple.event.is_set() 80 | assert instance.value == 3 81 | 82 | 83 | async def test_simple_with_init_inheritance(): 84 | class Simple(AsyncObject): 85 | event = asyncio.Event() 86 | 87 | def __init__(self): 88 | super().__init__() 89 | self.value = 3 90 | 91 | async def __ainit__(self): 92 | self.loop.call_soon(self.event.set) 93 | await self.event.wait() 94 | 95 | class MySimple(Simple): 96 | pass 97 | 98 | instance = await MySimple() 99 | 100 | assert instance.__class__ == MySimple 101 | 102 | assert Simple.event.is_set() 103 | assert MySimple.event.is_set() 104 | assert instance.value == 3 105 | 106 | 107 | async def test_non_corotine_ainit(): 108 | with pytest.raises(TypeError): 109 | 110 | class _(AsyncObject): 111 | def __ainit__(self): 112 | pass 113 | 114 | 115 | async def test_async_class_task_store(): 116 | class Sample(AsyncObject): 117 | async def __ainit__(self): 118 | self.future = self.create_future() 119 | self.task = self.create_task(asyncio.sleep(3600)) 120 | 121 | obj = await Sample() 122 | 123 | assert obj.__tasks__ 124 | assert isinstance(obj.__tasks__, TaskStore) 125 | 126 | assert not obj.future.done() 127 | assert not obj.task.done() 128 | 129 | await obj.close() 130 | 131 | assert obj.future.done() 132 | assert obj.task.done() 133 | 134 | assert obj.is_closed 135 | await obj.close() 136 | 137 | del obj 138 | 139 | 140 | async def test_async_class_inherit_from(): 141 | class Parent(AsyncObject): 142 | pass 143 | 144 | class Child(Parent): 145 | async def __ainit__(self, parent: Parent): 146 | link(self, parent) 147 | 148 | parent = await Parent() 149 | child = await Child(parent) 150 | assert not child.is_closed 151 | await parent.close(asyncio.CancelledError) 152 | assert parent.is_closed 153 | assert parent.__tasks__.is_closed 154 | assert child.__tasks__.is_closed 155 | assert child.is_closed 156 | 157 | 158 | async def test_await_redeclaration(): 159 | with pytest.raises(TypeError): 160 | 161 | class _(AsyncObject): 162 | def __await__(self): 163 | pass 164 | 165 | 166 | async def test_close_uninitialized(loop): 167 | future = asyncio.Future() 168 | 169 | class Sample(AsyncObject): 170 | async def __ainit__(self, *args, **kwargs): 171 | await future 172 | 173 | instance = Sample() 174 | 175 | task: asyncio.Task = loop.create_task(instance.__await__()) 176 | await asyncio.sleep(0.1) 177 | 178 | await instance.close() 179 | 180 | assert task.done() 181 | with pytest.raises(asyncio.CancelledError): 182 | await task 183 | 184 | assert future.done() 185 | with pytest.raises(asyncio.CancelledError): 186 | await future 187 | 188 | 189 | def callback_regular(): 190 | pass 191 | 192 | 193 | def callback_with_raise(): 194 | return 1 / 0 195 | 196 | 197 | @pytest.mark.parametrize("callback", [callback_regular, callback_with_raise]) 198 | async def test_close_callabacks(callback): 199 | class Sample(AsyncObject): 200 | pass 201 | 202 | instance = await Sample() 203 | event = asyncio.Event() 204 | instance.__tasks__.add_close_callback(event.set) 205 | instance.__tasks__.add_close_callback(callback) 206 | await instance.close() 207 | assert event.is_set() 208 | 209 | 210 | async def test_del(): 211 | class Sample(AsyncObject): 212 | pass 213 | 214 | instance = await Sample() 215 | event = asyncio.Event() 216 | instance.__tasks__.add_close_callback(event.set) 217 | del instance 218 | await event.wait() 219 | 220 | 221 | async def test_del_child(): 222 | class Parent(AsyncObject): 223 | pass 224 | 225 | class Child(Parent): 226 | async def __ainit__(self, parent: Parent): 227 | link(self, parent) 228 | 229 | parent = await Parent() 230 | 231 | parent_event = asyncio.Event() 232 | parent.__tasks__.add_close_callback(parent_event.set) 233 | 234 | child = await Child(parent) 235 | 236 | child_event = asyncio.Event() 237 | child.__tasks__.add_close_callback(child_event.set) 238 | 239 | del child 240 | 241 | for generation in range(3): 242 | gc.collect(generation) 243 | 244 | await child_event.wait() 245 | assert not parent_event.is_set() 246 | 247 | 248 | async def test_link_init(): 249 | class Parent(AsyncObject): 250 | pass 251 | 252 | class Child(Parent): 253 | def __init__(self, parent: Parent): 254 | super().__init__() 255 | link(self, parent) 256 | 257 | parent = await Parent() 258 | 259 | parent_event = asyncio.Event() 260 | parent.__tasks__.add_close_callback(parent_event.set) 261 | 262 | child = await Child(parent) 263 | 264 | child_event = asyncio.Event() 265 | child.__tasks__.add_close_callback(child_event.set) 266 | 267 | del child 268 | 269 | for generation in range(3): 270 | gc.collect(generation) 271 | 272 | await child_event.wait() 273 | assert not parent_event.is_set() 274 | 275 | 276 | async def test_close_non_initialized(): 277 | class Sample(AsyncObject): 278 | pass 279 | 280 | sample = Sample() 281 | await sample.close() 282 | 283 | 284 | async def test_task_decorator(): 285 | class Sample(AsyncObject): 286 | @task 287 | async def sleep(self, *args): 288 | return await asyncio.sleep(*args) 289 | 290 | sample = await Sample() 291 | result = sample.sleep(0) 292 | 293 | assert isinstance(result, asyncio.Task) 294 | result.cancel() 295 | 296 | with pytest.raises(asyncio.CancelledError): 297 | await result 298 | 299 | await sample.sleep(0) 300 | -------------------------------------------------------------------------------- /tests/test_task_store.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from async_class import TaskStore 4 | 5 | 6 | async def test_store_close(loop): 7 | store = TaskStore(loop) 8 | task1 = store.create_task(asyncio.sleep(3600)) 9 | future1 = store.create_future() 10 | 11 | child_store = store.get_child() 12 | task2 = child_store.create_task(asyncio.sleep(3600)) 13 | future2 = child_store.create_future() 14 | 15 | # Bad future 16 | future3 = loop.create_future() 17 | child_store.futures.add(future3) 18 | 19 | async def awaiter(f): 20 | return await f 21 | 22 | # Bad task 23 | task3 = loop.create_task(awaiter(future3)) 24 | child_store.tasks.add(task3) 25 | 26 | future3.set_result(True) 27 | 28 | await store.close() 29 | assert store.is_closed 30 | 31 | for f in (future1, future2): 32 | assert isinstance(f.exception(), asyncio.CancelledError) 33 | 34 | futures = (task1, task2, future1, future2) 35 | 36 | assert all(f.done() for f in futures) 37 | 38 | assert await future3 39 | assert await task3 40 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pylava,mypy,checkdoc,py3{6,7,8,9} 3 | 4 | [testenv] 5 | passenv = COVERALLS_* TEST_* FORCE_COLOR 6 | usedevelop = true 7 | 8 | deps = 9 | aiomisc 10 | pytest 11 | coveralls 12 | pytest-cov 13 | pytest-rst 14 | 15 | commands= 16 | pytest -v --cov=async_class --cov-report=term-missing --doctest-modules tests README.rst 17 | - coveralls 18 | 19 | [testenv:py36] 20 | commands= 21 | pytest -v --cov=async_class --cov-report=term-missing --doctest-modules tests 22 | - coveralls 23 | 24 | [testenv:pylava] 25 | deps = 26 | pylava 27 | 28 | commands= 29 | pylava -o pylava.ini . 30 | 31 | 32 | [testenv:mypy] 33 | basepython=python3.9 34 | usedevelop = true 35 | 36 | deps = 37 | mypy 38 | 39 | commands = 40 | mypy --color-output --install-types --non-interactive async_class.py 41 | 42 | [testenv:checkdoc] 43 | skip_install=true 44 | deps = 45 | collective.checkdocs 46 | pygments 47 | 48 | commands = 49 | python setup.py checkdocs 50 | --------------------------------------------------------------------------------