diff --git a/.gitignore b/.gitignore index 8b30654..1670b69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,131 +1,134 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -.*.swp +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version +pyenv.cfg + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +bin +lib +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.*.swp diff --git a/LICENSE b/LICENSE index 3c4dfe0..71adf4e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,187 +1,187 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. diff --git a/README.md b/README.md index 2ac770a..4ecbb08 100644 --- a/README.md +++ b/README.md @@ -1,380 +1,380 @@ -# giambio - Asynchronous Python made easy (and friendly) - -giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing. -This library implements what is known as a _stackless mode of execution_, or -"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default). - - -_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being -production ready, so be aware that it is likely that you'll find bugs and race conditions - -## Disclaimer - -Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works -and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them! - -Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the -[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a -rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact -that most of the following text is ~~stolen~~ inspired from its documentation) - - -# What the hell is async anyway? - -Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing). -The most common example of this is a network server that needs to handle multiple connections at the same time. -One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they -actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial. -If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization -primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide. -A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)), -and network servers happen to be heavily based on I/O: a blocking operation. -Starting to see where we're heading? - - -## A deeper dive - -Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is -implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details). -For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5: -asynchronous functions, or 'async' for short. - -Async functions are functions defined with `async def` instead of the regular `def`, like so: - -```python -async def async_fun(): # An async function - print("Hello, world!") - -def sync_fun(): # A regular (sync) function - print("Hello, world!") -``` - -First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below: - -```python - -async def async_two(): - print("Hello from async_two!") - -async def async_one(): - print("Hello from async_one!") - await async_two() # This is an async call -``` - -It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async -functions have a unique superpower: they, and no-one else, can call other async functions. - -This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol' -synchronous code; So how do we enter the async context in the first place? - -That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call -asynchronous functions and can therefore initiate the async context. For this -reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_. - -Now that you know all of this, you might be wondering why on earth would one use async functions instead of -regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it? -Take a look at this example below: - -```python -import giambio - -async def foo(): - print("Hello, world!") - -giambio.run(foo) # Prints 'Hello, world!' -``` - -This could as well be written the following way and would produce the same output: - -```python -def foo(): - print("Hello, world!") - -foo() # Prints 'Hello, world!' -``` - -To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness. - -We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio -provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful! -So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code. -As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor): - -__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines -so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async -model is much simpler if we take coroutines out of the game, and everything works just the same. - -```python -import giambio - -async def sleep_double(n): - await giambio.sleep(2 * n) - -giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns -``` - -As it turns out, this function is one that's actually worth making async: because it calls another async function. -Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to -make it async in the first place. - -### Don't forget the `await`! - -As we already learned, async functions can only be called with the `await` keyword, and it would be logical to -think that forgetting to do so would raise an error, but it's actually a little bit trickier than that. - -Take this example here: - -```python -import giambio - -async def sleep_double_broken(n): - print("Taking a nap!") - start = giambio.clock() - giambio.sleep(2 * n) # We forgot the await! - end = giambio.clock() - start - print(f"Slept for {end:.2f} seconds!") - -giambio.run(sleep_double_broken, 2) -``` - -Running this code, will produce an output that looks like this: - -``` -Taking a nap! -Slept 0.00 seconds! -__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited -``` - -Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong: -the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining -that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a -garbage collection cycle occurred or not). -I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that -if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async -function, so try fixing that before trying to figure out what could be the problem if you have a long traceback: -most likely that's just collateral damage caused by the missing keyword. - -If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to -the next section, but for the curios among y'all I might as well explain exactly what happened there. - -When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine' -object - -```python - ->>> giambio.sleep(1) - -``` - -The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering -`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things: -- It creates this weird coroutine object -- Passes that object to `await`, which runs the function - -So basically that's why you always need to put `await` in front of an async function when calling it. - - -## Something actually useful - -Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special -runner function that can start the whole async context, but we didn't really do anything _useful_. -Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't -quite useful is it? - -But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__. -Yep, you read that right. - -To demonstrate this, have a look a this example - - -```python -import giambio - -async def countdown(n: int): - print(f"Counting down from {n}!") - while n > 0: - print(f"Down {n}") - n -= 1 - await giambio.sleep(1) - print("Countdown over") - return 0 - -async def countup(stop: int): - print(f"Counting up to {stop}!") - x = 0 - while x < stop: - print(f"Up {x}") - x += 1 - await giambio.sleep(2) - print("Countup over") - return 1 - -async def main(): - start = giambio.clock() - async with giambio.create_pool() as pool: - pool.spawn(countdown, 10) - pool.spawn(countup, 5) - print("Children spawned, awaiting completion") - print(f"Task execution complete in {giambio.clock() - start:2f} seconds") - -if __name__ == "__main__": - giambio.run(main) - -``` - -There is a lot going on here, and we'll explain every bit of it step by step: - -- First, we imported giambio and defined two async functions: `countup` and `countdown` -- These two functions do exactly what their name suggests, but for the purposes of -this tutorial, `countup` will be running twice as slow as `countdown` (see the call -to `await giambio.sleep(2)`?) -- Here comes the real fun: `async with`? What's going on there? -As it turns out, Python 3.5 didn't just add async functions, but also quite a bit -of related new syntax. One of the things that was added is asynchronous context managers. -You might have already encountered context managers in python, but in case you didn't, -a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()` -at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as` -keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So -context managers are a shorthand for calling functions, and since Python 3.5 added -async functions, we also needed async context managers. While `with foo as sth` calls -`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh? - -__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is -the same though: while `for item in container` calls `container.__next__()` to fetch the -next item, `async for item in container` calls `await container.__anext__()` to do so. -It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good. - -- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio, -there are actually 2 ways to call async functions: one we've already seen (`await fn()`), -while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is -that it will return immediately, without waiting for the async function to finish. So, -now our functions are running in the background. -After we spawn our tasks, we hit the call to `print` and the end of the block, so Python -calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main` -async function in this case) until all children task have exited, and as it turns out, that -is a good thing. -The reason why pools always wait for all children to have finished executing is that it makes -easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks, -exceptions in giambio always behave as expected - - -Ok, so, let's try running this snippet and see what we get: - -``` -Children spawned, awaiting completion -Counting down from 10! -Down 10 -Counting up to 5! -Up 0 -Down 9 -Up 1 -Down 8 -Down 7 -Up 2 -Down 6 -Down 5 -Up 3 -Down 4 -Down 3 -Up 4 -Down 2 -Down 1 -Countup over -Countdown over -Task execution complete in 10.07 seconds -``` - -(Your output might have some lines swapped compared to this) - -You see how `countup` and `countdown` both start and finish -together? Moreover, even though each function slept for about 10 -seconds (therefore 20 seconds total), the program just took 10 -seconds to complete, so our children are really running at the same time. - -If you've ever done thread programming, this will feel like home, and that's good: -that's exactly what we want. But beware! No threads are involved here, giambio is -running in a single thread. That's why we talked about _tasks_ rather than _threads_ -so far. The difference between the two is that you can run a lot of tasks in a single -thread, and that with threads Python can switch which thread is running at any time. -Giambio, on the other hand, can switch tasks only at certain fixed points called -_checkpoints_, more on that later. - -### A sneak peak into the async world - -The basic idea behind libraries like giambio is that they can run a lot of tasks -at the same time by switching back and forth between them at appropriate places. -An example for that could be a web server: while the server is waiting for a response -from a client, we can accept another connection. You don't necessarily need all these -pesky details to use giambio, but it's good to have at least an high-level understanding -of how this all works. - -The peculiarity of asynchronous functions is that they can suspend their execution: that's -what `await` does, it yields back the execution control to giambio, which can then decide -what to do next. - -To understand this better, take a look at this code: - -```python -def countdown(n: int) -> int: - while n: - yield n - n -= 1 - -for x in countdown(5): - print(x) -``` - -In the above snippet, `countdown` is a generator function. Generators are really useful because -they allow to customize iteration. Running that code produces the following output: - -``` -5 -4 -3 -2 -1 -``` - -The trick for this to work is `yield`. -What `yield` does is return back to the caller and suspend itself: In our case, `yield` -returns to the for loop, which calls `countdown` again. So, the generator resumes right -after the `yield`, decrements n, and loops right back to the top for the while loop to -execute again. It's that suspension part that allows the async magic to happen: the whole -`async`/`await` logic overlaps a lot with generator functions. - -Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made -way before Python 3.5 added that nice new syntax. - -So, since only async functions can suspend themselves, the only places where giambio will switch -tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure -that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier -to reason about, because you can know if a function will ever switch, and where will it do so, just -by looking at its source code. That is very different from what threads do: they can (and will) switch -whenever they feel like it. - -Remember when we talked about checkpoints? That's what they are: calls to async functions that allow -giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code, -then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix -for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job, -and it will reschedule the caller almost immediately, because the sleep time is 0. - -### Mix and match? No thanks - -You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a -giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all -your asynchronous code in its event loop, which is what actually runs the tasks. When you call -`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it -to perform a certain thing in a given task, and to communicate your intent to the loop, the -primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand. -Other libraries have other private "languages", so mixing them is not possible: doing so will cause -giambio to get very confused and most likely just explode spectacularly badly - - -TODO: I/O - -## Contributing - -This is a relatively young project and it is looking for collaborators! It's not rocket science, -but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions, -so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x) -or by [E-mail](mailto:hackhab@gmail.com) +# giambio - Asynchronous Python made easy (and friendly) + +giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing. +This library implements what is known as a _stackless mode of execution_, or +"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default). + + +_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being +production ready, so be aware that it is likely that you'll find bugs and race conditions + +## Disclaimer + +Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works +and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them! + +Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the +[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a +rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact +that most of the following text is ~~stolen~~ inspired from its documentation) + + +# What the hell is async anyway? + +Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing). +The most common example of this is a network server that needs to handle multiple connections at the same time. +One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they +actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial. +If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization +primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide. +A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)), +and network servers happen to be heavily based on I/O: a blocking operation. +Starting to see where we're heading? + + +## A deeper dive + +Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is +implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details). +For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5: +asynchronous functions, or 'async' for short. + +Async functions are functions defined with `async def` instead of the regular `def`, like so: + +```python +async def async_fun(): # An async function + print("Hello, world!") + +def sync_fun(): # A regular (sync) function + print("Hello, world!") +``` + +First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below: + +```python + +async def async_two(): + print("Hello from async_two!") + +async def async_one(): + print("Hello from async_one!") + await async_two() # This is an async call +``` + +It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async +functions have a unique superpower: they, and no-one else, can call other async functions. + +This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol' +synchronous code; So how do we enter the async context in the first place? + +That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call +asynchronous functions and can therefore initiate the async context. For this +reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_. + +Now that you know all of this, you might be wondering why on earth would one use async functions instead of +regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it? +Take a look at this example below: + +```python +import giambio + +async def foo(): + print("Hello, world!") + +giambio.run(foo) # Prints 'Hello, world!' +``` + +This could as well be written the following way and would produce the same output: + +```python +def foo(): + print("Hello, world!") + +foo() # Prints 'Hello, world!' +``` + +To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness. + +We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio +provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful! +So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code. +As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor): + +__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines +so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async +model is much simpler if we take coroutines out of the game, and everything works just the same. + +```python +import giambio + +async def sleep_double(n): + await giambio.sleep(2 * n) + +giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns +``` + +As it turns out, this function is one that's actually worth making async: because it calls another async function. +Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to +make it async in the first place. + +### Don't forget the `await`! + +As we already learned, async functions can only be called with the `await` keyword, and it would be logical to +think that forgetting to do so would raise an error, but it's actually a little bit trickier than that. + +Take this example here: + +```python +import giambio + +async def sleep_double_broken(n): + print("Taking a nap!") + start = giambio.clock() + giambio.sleep(2 * n) # We forgot the await! + end = giambio.clock() - start + print(f"Slept for {end:.2f} seconds!") + +giambio.run(sleep_double_broken, 2) +``` + +Running this code, will produce an output that looks like this: + +``` +Taking a nap! +Slept 0.00 seconds! +__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited +``` + +Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong: +the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining +that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a +garbage collection cycle occurred or not). +I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that +if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async +function, so try fixing that before trying to figure out what could be the problem if you have a long traceback: +most likely that's just collateral damage caused by the missing keyword. + +If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to +the next section, but for the curios among y'all I might as well explain exactly what happened there. + +When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine' +object + +```python + +>>> giambio.sleep(1) + +``` + +The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering +`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things: +- It creates this weird coroutine object +- Passes that object to `await`, which runs the function + +So basically that's why you always need to put `await` in front of an async function when calling it. + + +## Something actually useful + +Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special +runner function that can start the whole async context, but we didn't really do anything _useful_. +Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't +quite useful is it? + +But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__. +Yep, you read that right. + +To demonstrate this, have a look a this example + + +```python +import giambio + +async def countdown(n: int): + print(f"Counting down from {n}!") + while n > 0: + print(f"Down {n}") + n -= 1 + await giambio.sleep(1) + print("Countdown over") + return 0 + +async def countup(stop: int): + print(f"Counting up to {stop}!") + x = 0 + while x < stop: + print(f"Up {x}") + x += 1 + await giambio.sleep(2) + print("Countup over") + return 1 + +async def main(): + start = giambio.clock() + async with giambio.create_pool() as pool: + pool.spawn(countdown, 10) + pool.spawn(countup, 5) + print("Children spawned, awaiting completion") + print(f"Task execution complete in {giambio.clock() - start:2f} seconds") + +if __name__ == "__main__": + giambio.run(main) + +``` + +There is a lot going on here, and we'll explain every bit of it step by step: + +- First, we imported giambio and defined two async functions: `countup` and `countdown` +- These two functions do exactly what their name suggests, but for the purposes of +this tutorial, `countup` will be running twice as slow as `countdown` (see the call +to `await giambio.sleep(2)`?) +- Here comes the real fun: `async with`? What's going on there? +As it turns out, Python 3.5 didn't just add async functions, but also quite a bit +of related new syntax. One of the things that was added is asynchronous context managers. +You might have already encountered context managers in python, but in case you didn't, +a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()` +at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as` +keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So +context managers are a shorthand for calling functions, and since Python 3.5 added +async functions, we also needed async context managers. While `with foo as sth` calls +`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh? + +__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is +the same though: while `for item in container` calls `container.__next__()` to fetch the +next item, `async for item in container` calls `await container.__anext__()` to do so. +It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good. + +- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio, +there are actually 2 ways to call async functions: one we've already seen (`await fn()`), +while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is +that it will return immediately, without waiting for the async function to finish. So, +now our functions are running in the background. +After we spawn our tasks, we hit the call to `print` and the end of the block, so Python +calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main` +async function in this case) until all children task have exited, and as it turns out, that +is a good thing. +The reason why pools always wait for all children to have finished executing is that it makes +easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks, +exceptions in giambio always behave as expected + + +Ok, so, let's try running this snippet and see what we get: + +``` +Children spawned, awaiting completion +Counting down from 10! +Down 10 +Counting up to 5! +Up 0 +Down 9 +Up 1 +Down 8 +Down 7 +Up 2 +Down 6 +Down 5 +Up 3 +Down 4 +Down 3 +Up 4 +Down 2 +Down 1 +Countup over +Countdown over +Task execution complete in 10.07 seconds +``` + +(Your output might have some lines swapped compared to this) + +You see how `countup` and `countdown` both start and finish +together? Moreover, even though each function slept for about 10 +seconds (therefore 20 seconds total), the program just took 10 +seconds to complete, so our children are really running at the same time. + +If you've ever done thread programming, this will feel like home, and that's good: +that's exactly what we want. But beware! No threads are involved here, giambio is +running in a single thread. That's why we talked about _tasks_ rather than _threads_ +so far. The difference between the two is that you can run a lot of tasks in a single +thread, and that with threads Python can switch which thread is running at any time. +Giambio, on the other hand, can switch tasks only at certain fixed points called +_checkpoints_, more on that later. + +### A sneak peak into the async world + +The basic idea behind libraries like giambio is that they can run a lot of tasks +at the same time by switching back and forth between them at appropriate places. +An example for that could be a web server: while the server is waiting for a response +from a client, we can accept another connection. You don't necessarily need all these +pesky details to use giambio, but it's good to have at least an high-level understanding +of how this all works. + +The peculiarity of asynchronous functions is that they can suspend their execution: that's +what `await` does, it yields back the execution control to giambio, which can then decide +what to do next. + +To understand this better, take a look at this code: + +```python +def countdown(n: int) -> int: + while n: + yield n + n -= 1 + +for x in countdown(5): + print(x) +``` + +In the above snippet, `countdown` is a generator function. Generators are really useful because +they allow to customize iteration. Running that code produces the following output: + +``` +5 +4 +3 +2 +1 +``` + +The trick for this to work is `yield`. +What `yield` does is return back to the caller and suspend itself: In our case, `yield` +returns to the for loop, which calls `countdown` again. So, the generator resumes right +after the `yield`, decrements n, and loops right back to the top for the while loop to +execute again. It's that suspension part that allows the async magic to happen: the whole +`async`/`await` logic overlaps a lot with generator functions. + +Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made +way before Python 3.5 added that nice new syntax. + +So, since only async functions can suspend themselves, the only places where giambio will switch +tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure +that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier +to reason about, because you can know if a function will ever switch, and where will it do so, just +by looking at its source code. That is very different from what threads do: they can (and will) switch +whenever they feel like it. + +Remember when we talked about checkpoints? That's what they are: calls to async functions that allow +giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code, +then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix +for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job, +and it will reschedule the caller almost immediately, because the sleep time is 0. + +### Mix and match? No thanks + +You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a +giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all +your asynchronous code in its event loop, which is what actually runs the tasks. When you call +`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it +to perform a certain thing in a given task, and to communicate your intent to the loop, the +primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand. +Other libraries have other private "languages", so mixing them is not possible: doing so will cause +giambio to get very confused and most likely just explode spectacularly badly + + +TODO: I/O + +## Contributing + +This is a relatively young project and it is looking for collaborators! It's not rocket science, +but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions, +so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x) +or by [E-mail](mailto:hackhab@gmail.com) diff --git a/giambio/__init__.py b/giambio/__init__.py index 8a33e3d..2ec73b6 100644 --- a/giambio/__init__.py +++ b/giambio/__init__.py @@ -1,38 +1,38 @@ -""" -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -__author__ = "Nocturn9x aka Isgiambyy" -__version__ = (1, 0, 0) - - -from . import exceptions -from .traps import sleep, current_task -from .objects import Event -from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop - - -__all__ = [ - "exceptions", - "sleep", - "Event", - "run", - "clock", - "wrap_socket", - "create_pool", - "get_event_loop", - "current_task", - "new_event_loop" -] +""" +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +__author__ = "Nocturn9x aka Isgiambyy" +__version__ = (1, 0, 0) + + +from . import exceptions +from .traps import sleep, current_task +from .objects import Event +from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop + + +__all__ = [ + "exceptions", + "sleep", + "Event", + "run", + "clock", + "wrap_socket", + "create_pool", + "get_event_loop", + "current_task", + "new_event_loop" +] diff --git a/giambio/context.py b/giambio/context.py index a4f7635..f847550 100644 --- a/giambio/context.py +++ b/giambio/context.py @@ -1,69 +1,70 @@ -""" -Higher-level context managers for async pools - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - - -import types -from .core import AsyncScheduler -from .objects import Task - - -class TaskManager: - """ - An asynchronous context manager for giambio - """ - - def __init__(self, loop: AsyncScheduler) -> None: - """ - Object constructor - """ - - self.loop = loop - self.tasks = [] - - def spawn(self, func: types.FunctionType, *args): - """ - Spawns a child task - """ - - task = Task(func(*args), func.__name__ or str(func)) - task.parent = self.loop.current_task - self.loop.tasks.append(task) - self.tasks.append(task) - - def spawn_after(self, func: types.FunctionType, n: int, *args): - """ - Schedules a task for execution after n seconds - """ - - assert n >= 0, "The time delay can't be negative" - task = Task(func(*args), func.__name__ or str(func)) - task.parent = self.loop.current_task - self.loop.paused.put(task, n) - self.tasks.append(task) - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc, tb): - for task in self.tasks: - try: - await task.join() - except BaseException as e: - self.tasks.remove(task) - for to_cancel in self.tasks: - await to_cancel.cancel() +""" +Higher-level context managers for async pools + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +import types +from .core import AsyncScheduler +from .objects import Task + + +class TaskManager: + """ + An asynchronous context manager for giambio + """ + + def __init__(self, loop: AsyncScheduler) -> None: + """ + Object constructor + """ + + self.loop = loop + self.tasks = [] + + def spawn(self, func: types.FunctionType, *args): + """ + Spawns a child task + """ + + task = Task(func(*args), func.__name__ or str(func)) + task.parent = self.loop.current_task + self.loop.tasks.append(task) + self.tasks.append(task) + + def spawn_after(self, func: types.FunctionType, n: int, *args): + """ + Schedules a task for execution after n seconds + """ + + assert n >= 0, "The time delay can't be negative" + task = Task(func(*args), func.__name__ or str(func)) + task.parent = self.loop.current_task + self.loop.paused.put(task, n) + self.tasks.append(task) + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + for task in self.tasks: + try: + await task.join() + except BaseException: + self.tasks.remove(task) + for to_cancel in self.tasks: + await to_cancel.cancel() + print("oof") \ No newline at end of file diff --git a/giambio/core.py b/giambio/core.py index 36933f6..c3fdf84 100644 --- a/giambio/core.py +++ b/giambio/core.py @@ -1,398 +1,398 @@ -""" -The main runtime environment for giambio - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -# Import libraries and internal resources -import types -import socket -from time import sleep as wait -from timeit import default_timer -from .objects import Task, TimeQueue -from socket import SOL_SOCKET, SO_ERROR -from .traps import want_read, want_write -from collections import deque -from .socket import AsyncSocket, WantWrite, WantRead -from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE -from .exceptions import (InternalError, - CancelledError, - ResourceBusy, - ) - - - -class AsyncScheduler: - """ - An asynchronous scheduler implementation. Tries to mimic the threaded - model in its simplicity, without using actual threads, but rather alternating - across coroutines execution to let more than one thing at a time to proceed - with its calculations. An attempt to fix the threaded model has been made - without making the API unnecessarily complicated. - A few examples are tasks cancellation and exception propagation. - """ - - def __init__(self): - """ - Object constructor - """ - - # Tasks that are ready to run - self.tasks = deque() - # Selector object to perform I/O multiplexing - self.selector = DefaultSelector() - # This will always point to the currently running coroutine (Task object) - self.current_task = None - # Monotonic clock to keep track of elapsed time reliably - self.clock = default_timer - # Tasks that are asleep - self.paused = TimeQueue(self.clock) - # All active Event objects - self.events = set() - # Data to send back to a trap - self.to_send = None - # Have we ever ran? - self.has_ran = False - - def done(self): - """ - Returns True if there is work to do - """ - - if self.selector.get_map() or any([self.paused, - self.tasks, - self.events - ]): - return False - return True - - def shutdown(self): - """ - Shuts down the event loop - """ - - # TODO: See if other teardown is required (massive join()?) - self.selector.close() - - def run(self): - """ - Starts the loop and 'listens' for events until there is work to do, - then exits. This behavior kinda reflects a kernel, as coroutines can - request the loop's functionality only trough some fixed entry points, - which in turn yield and give execution control to the loop itself. - """ - - while True: - try: - if self.done(): - self.shutdown() - break - elif not self.tasks: - if self.paused: - # If there are no actively running tasks - # we try to schedule the asleep ones - self.awake_sleeping() - if self.selector.get_map(): - # The next step is checking for I/O - self.check_io() - if self.events: - # Try to awake event-waiting tasks - self.check_events() - # While there are tasks to run - while self.tasks: - # Sets the currently running task - self.current_task = self.tasks.popleft() - if self.current_task.cancel_pending: - self.do_cancel() - if self.to_send and self.current_task.status != "init": - data = self.to_send - else: - data = None - # Run a single step with the calculation - method, *args = self.current_task.run(data) - self.current_task.status = "run" - self.current_task.steps += 1 - # Data has been sent, reset it to None - if self.to_send and self.current_task != "init": - self.to_send = None - # Sneaky method call, thanks to David Beazley for this ;) - getattr(self, method)(*args) - except AttributeError: # If this happens, that's quite bad! - raise InternalError("Uh oh! Something very bad just happened, did" - " you try to mix primitives from other async libraries?") from None - except CancelledError: - self.current_task.status = "cancelled" - self.current_task.cancelled = True - self.current_task.cancel_pending = False - self.join() # TODO: Investigate if a call to join() is needed - except StopIteration as ret: - # Coroutine ends - self.current_task.status = "end" - self.current_task.result = ret.value - self.current_task.finished = True - self.join() - except BaseException as err: - self.current_task.exc = err - self.current_task.status = "crashed" - self.join() - - def do_cancel(self): - """ - Performs task cancellation by throwing CancelledError inside the current - task in order to stop it from executing. The loop continues to execute - as tasks are independent - """ - - # TODO: Do we need anything else? - self.current_task.throw(CancelledError) - - def get_running(self): - """ - Returns the current task - """ - - self.tasks.append(self.current_task) - self.to_send = self.current_task - - def check_events(self): - """ - Checks for ready or expired events and triggers them - """ - - for event in self.events.copy(): - if event.set: - event.event_caught = True - event.waiters - self.tasks.extend(event.waiters) - self.events.remove(event) - - def awake_sleeping(self): - """ - Checks for and reschedules sleeping tasks - """ - - wait(max(0.0, self.paused[0][0] - self.clock())) - # Sleep until the closest deadline in order not to waste CPU cycles - while self.paused[0][0] < self.clock(): - # Reschedules tasks when their deadline has elapsed - self.tasks.append(self.paused.get()) - if not self.paused: - break - - def check_io(self): - """ - Checks and schedules task to perform I/O - """ - - if self.tasks or self.events: # If there are tasks or events, never wait - timeout = 0.0 - elif self.paused: # If there are asleep tasks, wait until the closest - # deadline - timeout = max(0.0, self.paused[0][0] - self.clock()) - else: - timeout = None # If we _only_ have I/O to do, then wait indefinitely - for key in dict(self.selector.get_map()).values(): - # We make sure we don't reschedule finished tasks - if key.data.finished: - key.data.last_io = () - self.selector.unregister(key.fileobj) - if self.selector.get_map(): # If there is indeed tasks waiting on I/O - io_ready = self.selector.select(timeout) - # Get sockets that are ready and schedule their tasks - for key, _ in io_ready: - self.tasks.append(key.data) # Resource ready? Schedule its task - - def start(self, func: types.FunctionType, *args): - """ - Starts the event loop from a sync context - """ - - entry = Task(func(*args), func.__name__ or str(func)) - self.tasks.append(entry) - self.run() - self.has_ran = True - if entry.exc: - raise entry.exc from None - - def reschedule_joinee(self): - """ - Reschedules the joinee(s) task of the - currently running task, if any - """ - - self.tasks.extend(self.current_task.waiters) - - def join(self): - """ - Handler for the 'join' event, does some magic to tell the scheduler - to wait until the current coroutine ends - """ - - child = self.current_task - child.joined = True - if child.parent: - child.waiters.append(child.parent) - if child.finished: - self.reschedule_joinee() - elif child.exc: - ... # TODO: Handle exceptions - - def sleep(self, seconds: int or float): - """ - Puts the caller to sleep for a given amount of seconds - """ - - if seconds: - self.current_task.status = "sleep" - self.paused.put(self.current_task, seconds) - else: - self.tasks.append(self.current_task) - - # TODO: More generic I/O rather than just sockets - def want_read(self, sock: socket.socket): - """ - Handler for the 'want_read' event, registers the socket inside the - selector to perform I/0 multiplexing - """ - - self.current_task.status = "I/O" - if self.current_task.last_io: - if self.current_task.last_io == ("READ", sock): - # Socket is already scheduled! - return - else: - self.selector.unregister(sock) - self.current_task.last_io = "READ", sock - try: - self.selector.register(sock, EVENT_READ, self.current_task) - except KeyError: - # The socket is already registered doing something else - raise ResourceBusy("The given resource is busy!") from None - - def want_write(self, sock: socket.socket): - """ - Handler for the 'want_write' event, registers the socket inside the - selector to perform I/0 multiplexing - """ - - self.current_task.status = "I/O" - if self.current_task.last_io: - if self.current_task.last_io == ("WRITE", sock): - # Socket is already scheduled! - return - else: - # TODO: Inspect why modify() causes issues - self.selector.unregister(sock) - self.current_task.last_io = "WRITE", sock - try: - self.selector.register(sock, EVENT_WRITE, self.current_task) - except KeyError: - raise ResourceBusy("The given resource is busy!") from None - - def event_set(self, event): - """ - Sets an event - """ - - self.events.add(event) - event.waiters.append(self.current_task) - event.set = True - self.reschedule_joinee() - - def event_wait(self, event): - """ - Pauses the current task on an event - """ - - event.waiters.append(self.current_task) - - - def cancel(self): - """ - Handler for the 'cancel' event, schedules the task to be cancelled later - or does so straight away if it is safe to do so - """ - - if self.current_task.status in ("I/O", "sleep"): - # We cancel right away - self.do_cancel() - else: - self.current_task.cancel_pending = True # Cancellation is deferred - - def wrap_socket(self, sock): - """ - Wraps a standard socket into an AsyncSocket object - """ - - return AsyncSocket(sock, self) - - async def read_sock(self, sock: socket.socket, buffer: int): - """ - Reads from a socket asynchronously, waiting until the resource is - available and returning up to buffer bytes from the socket - """ - - try: - return sock.recv(buffer) - except WantRead: - await want_read(sock) - return sock.recv(buffer) - - async def accept_sock(self, sock: socket.socket): - """ - Accepts a socket connection asynchronously, waiting until the resource - is available and returning the result of the accept() call - """ - - try: - return sock.accept() - except WantRead: - await want_read(sock) - return sock.accept() - - async def sock_sendall(self, sock: socket.socket, data: bytes): - """ - Sends all the passed data, as bytes, trough the socket asynchronously - """ - - while data: - try: - sent_no = sock.send(data) - except WantWrite: - await want_write(sock) - sent_no = sock.send(data) - data = data[sent_no:] - - async def close_sock(self, sock: socket.socket): - """ - Closes the socket asynchronously - """ - - await want_write(sock) - self.selector.unregister(sock) - return sock.close() - - async def connect_sock(self, sock: socket.socket, addr: tuple): - """ - Connects a socket asynchronously - """ - - try: # "Borrowed" from curio - return sock.connect(addr) - except WantWrite: - await want_write(sock) - err = sock.getsockopt(SOL_SOCKET, SO_ERROR) - if err != 0: - raise OSError(err, f"Connect call failed: {addr}") +""" +The main runtime environment for giambio + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Import libraries and internal resources +import types +import socket +from time import sleep as wait +from timeit import default_timer +from .objects import Task, TimeQueue +from socket import SOL_SOCKET, SO_ERROR +from .traps import want_read, want_write +from collections import deque +from .socket import AsyncSocket, WantWrite, WantRead +from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE +from .exceptions import (InternalError, + CancelledError, + ResourceBusy, + ) + + + +class AsyncScheduler: + """ + An asynchronous scheduler implementation. Tries to mimic the threaded + model in its simplicity, without using actual threads, but rather alternating + across coroutines execution to let more than one thing at a time to proceed + with its calculations. An attempt to fix the threaded model has been made + without making the API unnecessarily complicated. + A few examples are tasks cancellation and exception propagation. + """ + + def __init__(self): + """ + Object constructor + """ + + # Tasks that are ready to run + self.tasks = deque() + # Selector object to perform I/O multiplexing + self.selector = DefaultSelector() + # This will always point to the currently running coroutine (Task object) + self.current_task = None + # Monotonic clock to keep track of elapsed time reliably + self.clock = default_timer + # Tasks that are asleep + self.paused = TimeQueue(self.clock) + # All active Event objects + self.events = set() + # Data to send back to a trap + self.to_send = None + # Have we ever ran? + self.has_ran = False + + def done(self): + """ + Returns True if there is work to do + """ + + if self.selector.get_map() or any([self.paused, + self.tasks, + self.events + ]): + return False + return True + + def shutdown(self): + """ + Shuts down the event loop + """ + + # TODO: See if other teardown is required (massive join()?) + self.selector.close() + + def run(self): + """ + Starts the loop and 'listens' for events until there is work to do, + then exits. This behavior kinda reflects a kernel, as coroutines can + request the loop's functionality only trough some fixed entry points, + which in turn yield and give execution control to the loop itself. + """ + + while True: + try: + if self.done(): + self.shutdown() + break + elif not self.tasks: + if self.paused: + # If there are no actively running tasks + # we try to schedule the asleep ones + self.awake_sleeping() + if self.selector.get_map(): + # The next step is checking for I/O + self.check_io() + if self.events: + # Try to awake event-waiting tasks + self.check_events() + # While there are tasks to run + while self.tasks: + # Sets the currently running task + self.current_task = self.tasks.popleft() + if self.current_task.cancel_pending: + self.do_cancel() + if self.to_send and self.current_task.status != "init": + data = self.to_send + else: + data = None + # Run a single step with the calculation + method, *args = self.current_task.run(data) + self.current_task.status = "run" + self.current_task.steps += 1 + # Data has been sent, reset it to None + if self.to_send and self.current_task != "init": + self.to_send = None + # Sneaky method call, thanks to David Beazley for this ;) + getattr(self, method)(*args) + except AttributeError: # If this happens, that's quite bad! + raise InternalError("Uh oh! Something very bad just happened, did" + " you try to mix primitives from other async libraries?") from None + except CancelledError: + self.current_task.status = "cancelled" + self.current_task.cancelled = True + self.current_task.cancel_pending = False + self.join() # TODO: Investigate if a call to join() is needed + except StopIteration as ret: + # Coroutine ends + self.current_task.status = "end" + self.current_task.result = ret.value + self.current_task.finished = True + self.join() + except BaseException as err: + self.current_task.exc = err + self.current_task.status = "crashed" + self.join() + + def do_cancel(self): + """ + Performs task cancellation by throwing CancelledError inside the current + task in order to stop it from executing. The loop continues to execute + as tasks are independent + """ + + # TODO: Do we need anything else? + self.current_task.throw(CancelledError) + + def get_running(self): + """ + Returns the current task + """ + + self.tasks.append(self.current_task) + self.to_send = self.current_task + + def check_events(self): + """ + Checks for ready or expired events and triggers them + """ + + for event in self.events.copy(): + if event.set: + event.event_caught = True + event.waiters + self.tasks.extend(event.waiters) + self.events.remove(event) + + def awake_sleeping(self): + """ + Checks for and reschedules sleeping tasks + """ + + wait(max(0.0, self.paused[0][0] - self.clock())) + # Sleep until the closest deadline in order not to waste CPU cycles + while self.paused[0][0] < self.clock(): + # Reschedules tasks when their deadline has elapsed + self.tasks.append(self.paused.get()) + if not self.paused: + break + + def check_io(self): + """ + Checks and schedules task to perform I/O + """ + + if self.tasks or self.events: # If there are tasks or events, never wait + timeout = 0.0 + elif self.paused: # If there are asleep tasks, wait until the closest + # deadline + timeout = max(0.0, self.paused[0][0] - self.clock()) + else: + timeout = None # If we _only_ have I/O to do, then wait indefinitely + for key in dict(self.selector.get_map()).values(): + # We make sure we don't reschedule finished tasks + if key.data.finished: + key.data.last_io = () + self.selector.unregister(key.fileobj) + if self.selector.get_map(): # If there is indeed tasks waiting on I/O + io_ready = self.selector.select(timeout) + # Get sockets that are ready and schedule their tasks + for key, _ in io_ready: + self.tasks.append(key.data) # Resource ready? Schedule its task + + def start(self, func: types.FunctionType, *args): + """ + Starts the event loop from a sync context + """ + + entry = Task(func(*args), func.__name__ or str(func)) + self.tasks.append(entry) + self.run() + self.has_ran = True + if entry.exc: + raise entry.exc from None + + def reschedule_joinee(self): + """ + Reschedules the joinee(s) of the + currently running task, if any + """ + + self.tasks.extend(self.current_task.waiters) + + def join(self): + """ + Handler for the 'join' event, does some magic to tell the scheduler + to wait until the current coroutine ends + """ + + child = self.current_task + child.joined = True + if child.parent: + child.waiters.append(child.parent) + if child.finished: + self.reschedule_joinee() + elif child.exc: + ... # TODO: Handle exceptions + + def sleep(self, seconds: int or float): + """ + Puts the caller to sleep for a given amount of seconds + """ + + if seconds: + self.current_task.status = "sleep" + self.paused.put(self.current_task, seconds) + else: + self.tasks.append(self.current_task) + + # TODO: More generic I/O rather than just sockets + def want_read(self, sock: socket.socket): + """ + Handler for the 'want_read' event, registers the socket inside the + selector to perform I/0 multiplexing + """ + + self.current_task.status = "I/O" + if self.current_task.last_io: + if self.current_task.last_io == ("READ", sock): + # Socket is already scheduled! + return + else: + self.selector.unregister(sock) + self.current_task.last_io = "READ", sock + try: + self.selector.register(sock, EVENT_READ, self.current_task) + except KeyError: + # The socket is already registered doing something else + raise ResourceBusy("The given resource is busy!") from None + + def want_write(self, sock: socket.socket): + """ + Handler for the 'want_write' event, registers the socket inside the + selector to perform I/0 multiplexing + """ + + self.current_task.status = "I/O" + if self.current_task.last_io: + if self.current_task.last_io == ("WRITE", sock): + # Socket is already scheduled! + return + else: + # TODO: Inspect why modify() causes issues + self.selector.unregister(sock) + self.current_task.last_io = "WRITE", sock + try: + self.selector.register(sock, EVENT_WRITE, self.current_task) + except KeyError: + raise ResourceBusy("The given resource is busy!") from None + + def event_set(self, event): + """ + Sets an event + """ + + self.events.add(event) + event.waiters.append(self.current_task) + event.set = True + self.reschedule_joinee() + + def event_wait(self, event): + """ + Pauses the current task on an event + """ + + event.waiters.append(self.current_task) + + + def cancel(self): + """ + Handler for the 'cancel' event, schedules the task to be cancelled later + or does so straight away if it is safe to do so + """ + + if self.current_task.status in ("I/O", "sleep"): + # We cancel right away + self.do_cancel() + else: + self.current_task.cancel_pending = True # Cancellation is deferred + + def wrap_socket(self, sock): + """ + Wraps a standard socket into an AsyncSocket object + """ + + return AsyncSocket(sock, self) + + async def read_sock(self, sock: socket.socket, buffer: int): + """ + Reads from a socket asynchronously, waiting until the resource is + available and returning up to buffer bytes from the socket + """ + + try: + return sock.recv(buffer) + except WantRead: + await want_read(sock) + return sock.recv(buffer) + + async def accept_sock(self, sock: socket.socket): + """ + Accepts a socket connection asynchronously, waiting until the resource + is available and returning the result of the accept() call + """ + + try: + return sock.accept() + except WantRead: + await want_read(sock) + return sock.accept() + + async def sock_sendall(self, sock: socket.socket, data: bytes): + """ + Sends all the passed data, as bytes, trough the socket asynchronously + """ + + while data: + try: + sent_no = sock.send(data) + except WantWrite: + await want_write(sock) + sent_no = sock.send(data) + data = data[sent_no:] + + async def close_sock(self, sock: socket.socket): + """ + Closes the socket asynchronously + """ + + await want_write(sock) + self.selector.unregister(sock) + return sock.close() + + async def connect_sock(self, sock: socket.socket, addr: tuple): + """ + Connects a socket asynchronously + """ + + try: # "Borrowed" from curio + return sock.connect(addr) + except WantWrite: + await want_write(sock) + err = sock.getsockopt(SOL_SOCKET, SO_ERROR) + if err != 0: + raise OSError(err, f"Connect call failed: {addr}") diff --git a/giambio/exceptions.py b/giambio/exceptions.py index bd531dc..c1b7e3d 100644 --- a/giambio/exceptions.py +++ b/giambio/exceptions.py @@ -1,60 +1,60 @@ -""" -Exceptions for giambio - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - - -class GiambioError(Exception): - """ - Base class for giambio exceptions - """ - - ... - - -class InternalError(GiambioError): - """ - Internal exception - """ - - ... - - -class CancelledError(BaseException): - """ - Exception raised by the giambio.objects.Task.cancel() method - to terminate a child task. This should NOT be catched, or - at least it should be re-raised and never ignored - """ - - ... - - -class ResourceBusy(GiambioError): - """ - Exception that is raised when a resource is accessed by more than - one task at a time - """ - - ... - - -class ResourceClosed(GiambioError): - """ - Raised when I/O is attempted on a closed resource - """ - - ... +""" +Exceptions for giambio + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +class GiambioError(Exception): + """ + Base class for giambio exceptions + """ + + ... + + +class InternalError(GiambioError): + """ + Internal exception + """ + + ... + + +class CancelledError(BaseException): + """ + Exception raised by the giambio.objects.Task.cancel() method + to terminate a child task. This should NOT be catched, or + at least it should be re-raised and never ignored + """ + + ... + + +class ResourceBusy(GiambioError): + """ + Exception that is raised when a resource is accessed by more than + one task at a time + """ + + ... + + +class ResourceClosed(GiambioError): + """ + Raised when I/O is attempted on a closed resource + """ + + ... diff --git a/giambio/objects.py b/giambio/objects.py index 2b95d1f..7c49d80 100644 --- a/giambio/objects.py +++ b/giambio/objects.py @@ -1,158 +1,158 @@ -""" -Various object wrappers and abstraction layers - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import types -from .traps import join, cancel, event_set, event_wait -from heapq import heappop, heappush -from .exceptions import GiambioError -from dataclasses import dataclass, field - - -@dataclass -class Task: - - """ - A simple wrapper around a coroutine object - """ - - coroutine: types.CoroutineType - name: str - cancelled: bool = False # True if the task gets cancelled - exc: BaseException = None - result: object = None - finished: bool = False - status: str = "init" - steps: int = 0 - last_io: tuple = () - parent: object = None - joined: bool= False - cancel_pending: bool = False - waiters: list = field(default_factory=list) - - def run(self, what=None): - """ - Simple abstraction layer over coroutines' ``send`` method - """ - - return self.coroutine.send(what) - - def throw(self, err: Exception): - """ - Simple abstraction layer over coroutines ``throw`` method - """ - - return self.coroutine.throw(err) - - async def join(self): - """ - Joins the task - """ - - res = await join(self) - if self.exc: - raise self.exc - return res - - async def cancel(self): - """ - Cancels the task - """ - - await cancel(self) - - def __del__(self): - self.coroutine.close() - - -class Event: - """ - A class designed similarly to threading.Event - """ - - def __init__(self): - """ - Object constructor - """ - - self.set = False - self.waiters = [] - self.event_caught = False - - async def trigger(self): - """ - Sets the event, waking up all tasks that called - pause() on us - """ - - if self.set: - raise GiambioError("The event has already been set") - await event_set(self) - - async def wait(self): - """ - Waits until the event is set - """ - - await event_wait(self) - - -class TimeQueue: - """ - An abstraction layer over a heap queue based on time. This is where - sleeping tasks will be put when they are not running - """ - - def __init__(self, clock): - """ - Object constructor - """ - - self.clock = clock - self.sequence = 0 - self.container = [] - - def __contains__(self, item): - return item in self.container - - def __iter__(self): - return iter(self.container) - - def __getitem__(self, item): - return self.container.__getitem__(item) - - def __bool__(self): - return bool(self.container) - - def __repr__(self): - return f"TimeQueue({self.container}, clock={self.clock})" - - def put(self, item, amount): - """ - Pushes an item onto the queue with its unique - time amount and ID - """ - - heappush(self.container, (self.clock() + amount, self.sequence, item)) - self.sequence += 1 - - def get(self): - """ - Gets the first task that is meant to run - """ - - return heappop(self.container)[2] +""" +Various object wrappers and abstraction layers + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import types +from .traps import join, cancel, event_set, event_wait +from heapq import heappop, heappush +from .exceptions import GiambioError +from dataclasses import dataclass, field + + +@dataclass +class Task: + + """ + A simple wrapper around a coroutine object + """ + + coroutine: types.CoroutineType + name: str + cancelled: bool = False # True if the task gets cancelled + exc: BaseException = None + result: object = None + finished: bool = False + status: str = "init" + steps: int = 0 + last_io: tuple = () + parent: object = None + joined: bool= False + cancel_pending: bool = False + waiters: list = field(default_factory=list) + + def run(self, what=None): + """ + Simple abstraction layer over coroutines' ``send`` method + """ + + return self.coroutine.send(what) + + def throw(self, err: Exception): + """ + Simple abstraction layer over coroutines ``throw`` method + """ + + return self.coroutine.throw(err) + + async def join(self): + """ + Joins the task + """ + + res = await join(self) + if self.exc: + raise self.exc + return res + + async def cancel(self): + """ + Cancels the task + """ + + await cancel(self) + + def __del__(self): + self.coroutine.close() + + +class Event: + """ + A class designed similarly to threading.Event + """ + + def __init__(self): + """ + Object constructor + """ + + self.set = False + self.waiters = [] + self.event_caught = False + + async def trigger(self): + """ + Sets the event, waking up all tasks that called + pause() on us + """ + + if self.set: + raise GiambioError("The event has already been set") + await event_set(self) + + async def wait(self): + """ + Waits until the event is set + """ + + await event_wait(self) + + +class TimeQueue: + """ + An abstraction layer over a heap queue based on time. This is where + sleeping tasks will be put when they are not running + """ + + def __init__(self, clock): + """ + Object constructor + """ + + self.clock = clock + self.sequence = 0 + self.container = [] + + def __contains__(self, item): + return item in self.container + + def __iter__(self): + return iter(self.container) + + def __getitem__(self, item): + return self.container.__getitem__(item) + + def __bool__(self): + return bool(self.container) + + def __repr__(self): + return f"TimeQueue({self.container}, clock={self.clock})" + + def put(self, item, amount): + """ + Pushes an item onto the queue with its unique + time amount and ID + """ + + heappush(self.container, (self.clock() + amount, self.sequence, item)) + self.sequence += 1 + + def get(self): + """ + Gets the first task that is meant to run + """ + + return heappop(self.container)[2] diff --git a/giambio/run.py b/giambio/run.py index f4c64fe..f6ce946 100644 --- a/giambio/run.py +++ b/giambio/run.py @@ -1,99 +1,99 @@ -""" -Helper methods and public API - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import socket -import threading -from .core import AsyncScheduler -from .exceptions import GiambioError -from .context import TaskManager -from .socket import AsyncSocket -from types import FunctionType, CoroutineType, GeneratorType - - -thread_local = threading.local() - - -def get_event_loop(): - """ - Returns the event loop associated to the current - thread - """ - - try: - return thread_local.loop - except AttributeError: - raise GiambioError("no event loop set") from None - - -def new_event_loop(): - """ - Associates a new event loop to the current thread - and deactivates the old one. This should not be - called explicitly unless you know what you're doing - """ - - try: - loop = thread_local.loop - except AttributeError: - thread_local.loop = AsyncScheduler() - else: - if not loop.done(): - raise GiambioError("cannot set event loop while running") - else: - thread_local.loop = AsyncScheduler() - - -def run(func: FunctionType, *args): - """ - Starts the event loop from a synchronous entry point - """ - - if isinstance(func, (CoroutineType, GeneratorType)): - raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!" - "\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)") - new_event_loop() - thread_local.loop.start(func, *args) - - -def clock(): - """ - Returns the current clock time of the thread-local event - loop - """ - - return thread_local.loop.clock() - - -def wrap_socket(sock: socket.socket) -> AsyncSocket: - """ - Wraps a synchronous socket into a giambio.socket.AsyncSocket - """ - - return thread_local.loop.wrap_socket(sock) - - -def create_pool(): - """ - Creates an async pool - """ - - try: - return TaskManager(thread_local.loop) - except AttributeError: - raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()" - " outside of an async context?") from None +""" +Helper methods and public API + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import socket +import threading +from .core import AsyncScheduler +from .exceptions import GiambioError +from .context import TaskManager +from .socket import AsyncSocket +from types import FunctionType, CoroutineType, GeneratorType + + +thread_local = threading.local() + + +def get_event_loop(): + """ + Returns the event loop associated to the current + thread + """ + + try: + return thread_local.loop + except AttributeError: + raise GiambioError("no event loop set") from None + + +def new_event_loop(): + """ + Associates a new event loop to the current thread + and deactivates the old one. This should not be + called explicitly unless you know what you're doing + """ + + try: + loop = thread_local.loop + except AttributeError: + thread_local.loop = AsyncScheduler() + else: + if not loop.done(): + raise GiambioError("cannot set event loop while running") + else: + thread_local.loop = AsyncScheduler() + + +def run(func: FunctionType, *args): + """ + Starts the event loop from a synchronous entry point + """ + + if isinstance(func, (CoroutineType, GeneratorType)): + raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!" + "\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)") + new_event_loop() + thread_local.loop.start(func, *args) + + +def clock(): + """ + Returns the current clock time of the thread-local event + loop + """ + + return thread_local.loop.clock() + + +def wrap_socket(sock: socket.socket) -> AsyncSocket: + """ + Wraps a synchronous socket into a giambio.socket.AsyncSocket + """ + + return thread_local.loop.wrap_socket(sock) + + +def create_pool(): + """ + Creates an async pool + """ + + try: + return TaskManager(thread_local.loop) + except AttributeError: + raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()" + " outside of an async context?") from None diff --git a/giambio/socket.py b/giambio/socket.py index 6bd47f5..8e5301f 100644 --- a/giambio/socket.py +++ b/giambio/socket.py @@ -1,100 +1,100 @@ -""" -Basic abstraction layer for giambio asynchronous sockets - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - - -import socket -from .exceptions import ResourceClosed -from .traps import sleep - - -# Stolen from curio -try: - from ssl import SSLWantReadError, SSLWantWriteError - WantRead = (BlockingIOError, InterruptedError, SSLWantReadError) - WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError) -except ImportError: - WantRead = (BlockingIOError, InterruptedError) - WantWrite = (BlockingIOError, InterruptedError) - - -class AsyncSocket(object): - """ - Abstraction layer for asynchronous TCP sockets - """ - - def __init__(self, sock: socket.socket, loop): - self.sock = sock - self.loop = loop - self._closed = False - self.sock.setblocking(False) - - async def receive(self, max_size: int): - """ - Receives up to max_size bytes from a socket asynchronously - """ - - if self._closed: - raise ResourceClosed("I/O operation on closed socket") - return await self.loop.read_sock(self.sock, max_size) - - async def accept(self): - """ - Accepts the socket, completing the 3-step TCP handshake asynchronously - """ - - if self._closed: - raise ResourceClosed("I/O operation on closed socket") - to_wrap = await self.loop.accept_sock(self.sock) - return self.loop.wrap_socket(to_wrap[0]), to_wrap[1] - - async def send_all(self, data: bytes): - """ - Sends all data inside the buffer asynchronously until it is empty - """ - - if self._closed: - raise ResourceClosed("I/O operation on closed socket") - return await self.loop.sock_sendall(self.sock, data) - - async def close(self): - """ - Closes the socket asynchronously - """ - - if self._closed: - raise ResourceClosed("I/O operation on closed socket") - await self.loop.close_sock(self.sock) - self._closed = True - - async def connect(self, addr: tuple): - """ - Connects the socket to an endpoint - """ - - if self._closed: - raise ResourceClosed("I/O operation on closed socket") - await self.loop.connect_sock(self.sock, addr) - - async def __aenter__(self): - return self - - async def __aexit__(self, *_): - await self.close() - - def __repr__(self): - return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})" +""" +Basic abstraction layer for giambio asynchronous sockets + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +import socket +from .exceptions import ResourceClosed +from .traps import sleep + + +# Stolen from curio +try: + from ssl import SSLWantReadError, SSLWantWriteError + WantRead = (BlockingIOError, InterruptedError, SSLWantReadError) + WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError) +except ImportError: + WantRead = (BlockingIOError, InterruptedError) + WantWrite = (BlockingIOError, InterruptedError) + + +class AsyncSocket(object): + """ + Abstraction layer for asynchronous TCP sockets + """ + + def __init__(self, sock: socket.socket, loop): + self.sock = sock + self.loop = loop + self._closed = False + self.sock.setblocking(False) + + async def receive(self, max_size: int): + """ + Receives up to max_size bytes from a socket asynchronously + """ + + if self._closed: + raise ResourceClosed("I/O operation on closed socket") + return await self.loop.read_sock(self.sock, max_size) + + async def accept(self): + """ + Accepts the socket, completing the 3-step TCP handshake asynchronously + """ + + if self._closed: + raise ResourceClosed("I/O operation on closed socket") + to_wrap = await self.loop.accept_sock(self.sock) + return self.loop.wrap_socket(to_wrap[0]), to_wrap[1] + + async def send_all(self, data: bytes): + """ + Sends all data inside the buffer asynchronously until it is empty + """ + + if self._closed: + raise ResourceClosed("I/O operation on closed socket") + return await self.loop.sock_sendall(self.sock, data) + + async def close(self): + """ + Closes the socket asynchronously + """ + + if self._closed: + raise ResourceClosed("I/O operation on closed socket") + await self.loop.close_sock(self.sock) + self._closed = True + + async def connect(self, addr: tuple): + """ + Connects the socket to an endpoint + """ + + if self._closed: + raise ResourceClosed("I/O operation on closed socket") + await self.loop.connect_sock(self.sock, addr) + + async def __aenter__(self): + return self + + async def __aexit__(self, *_): + await self.close() + + def __repr__(self): + return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})" diff --git a/giambio/traps.py b/giambio/traps.py index 4d5adaa..b97a72b 100644 --- a/giambio/traps.py +++ b/giambio/traps.py @@ -1,134 +1,134 @@ -""" -Implementation for all giambio traps, which are hooks -into the event loop and allow it to switch tasks. -These coroutines are the one and only way to interact -with the event loop from the user's perspective, and -the entire library is based on them - -Copyright (C) 2020 nocturn9x - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - - -import types - - -@types.coroutine -def create_trap(method, *args): - """ - Creates and yields a trap. This - is the lowest-level method to - interact with the event loop - """ - - data = yield method, *args - return data - - -async def sleep(seconds: int): - """ - Pause the execution of an async function for a given amount of seconds, - without blocking the entire event loop, which keeps watching for other events - - This function is also useful as a sort of checkpoint, because it returns - control to the scheduler, which can then switch to another task. If your code - doesn't have enough calls to async functions (or 'checkpoints') this might - prevent the scheduler from switching tasks properly. If you feel like this - happens in your code, try adding a call to giambio.sleep(0) somewhere. - This will act as a checkpoint without actually pausing the execution - of your function, but it will allow the scheduler to switch tasks - - :param seconds: The amount of seconds to sleep for - :type seconds: int - """ - - assert seconds >= 0, "The time delay can't be negative" - await create_trap("sleep", seconds) - - -async def current_task(): - """ - Gets the currently running task - """ - - return await create_trap("get_running") - - -async def join(task): - """ - Awaits a given task for completion - - :param task: The task to join - :type task: class: Task - """ - - return await create_trap("join") - - -async def cancel(task): - """ - Cancels the given task - - The concept of cancellation is tricky, because there is no real way to 'stop' - a task if not by raising an exception inside it and ignoring whatever it - returns (and also hoping that the task won't cause collateral damage). It - is highly recommended that when you write async code you take into account - that it might be cancelled at any time. You might think to just ignore the - cancellation exception and be done with it, but doing so *will* break your - code, so if you really wanna do that be sure to re-raise it when done! - """ - - await create_trap("cancel") - assert task.cancelled, f"Coroutine ignored CancelledError" - - -async def want_read(stream): - """ - Notifies the event loop that a task wants to read from the given - resource - - :param stream: The resource that needs to be read - """ - - await create_trap("want_read", stream) - - -async def want_write(stream): - """ - Notifies the event loop that a task wants to write on the given - resource - - :param stream: The resource that needs to be written - """ - - await create_trap("want_write", stream) - - -async def event_set(event): - """ - Communicates to the loop that the given event object - must be set. This is important as the loop constantly - checks for active events to deliver them - """ - - await create_trap("event_set", event) - - -async def event_wait(event): - """ - Notifies the event loop that the current task has to wait - for the given event to trigger - """ - - await create_trap("event_wait", event) +""" +Implementation for all giambio traps, which are hooks +into the event loop and allow it to switch tasks. +These coroutines are the one and only way to interact +with the event loop from the user's perspective, and +the entire library is based on them + +Copyright (C) 2020 nocturn9x + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +import types + + +@types.coroutine +def create_trap(method, *args): + """ + Creates and yields a trap. This + is the lowest-level method to + interact with the event loop + """ + + data = yield method, *args + return data + + +async def sleep(seconds: int): + """ + Pause the execution of an async function for a given amount of seconds, + without blocking the entire event loop, which keeps watching for other events + + This function is also useful as a sort of checkpoint, because it returns + control to the scheduler, which can then switch to another task. If your code + doesn't have enough calls to async functions (or 'checkpoints') this might + prevent the scheduler from switching tasks properly. If you feel like this + happens in your code, try adding a call to giambio.sleep(0) somewhere. + This will act as a checkpoint without actually pausing the execution + of your function, but it will allow the scheduler to switch tasks + + :param seconds: The amount of seconds to sleep for + :type seconds: int + """ + + assert seconds >= 0, "The time delay can't be negative" + await create_trap("sleep", seconds) + + +async def current_task(): + """ + Gets the currently running task + """ + + return await create_trap("get_running") + + +async def join(task): + """ + Awaits a given task for completion + + :param task: The task to join + :type task: class: Task + """ + + return await create_trap("join") + + +async def cancel(task): + """ + Cancels the given task + + The concept of cancellation is tricky, because there is no real way to 'stop' + a task if not by raising an exception inside it and ignoring whatever it + returns (and also hoping that the task won't cause collateral damage). It + is highly recommended that when you write async code you take into account + that it might be cancelled at any time. You might think to just ignore the + cancellation exception and be done with it, but doing so *will* break your + code, so if you really wanna do that be sure to re-raise it when done! + """ + + await create_trap("cancel") + assert task.cancelled, f"Coroutine ignored CancelledError" + + +async def want_read(stream): + """ + Notifies the event loop that a task wants to read from the given + resource + + :param stream: The resource that needs to be read + """ + + await create_trap("want_read", stream) + + +async def want_write(stream): + """ + Notifies the event loop that a task wants to write on the given + resource + + :param stream: The resource that needs to be written + """ + + await create_trap("want_write", stream) + + +async def event_set(event): + """ + Communicates to the loop that the given event object + must be set. This is important as the loop constantly + checks for active events to deliver them + """ + + await create_trap("event_set", event) + + +async def event_wait(event): + """ + Notifies the event loop that the current task has to wait + for the given event to trigger + """ + + await create_trap("event_wait", event) diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..7b1c4a3 --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,8 @@ +home = /usr +implementation = CPython +version_info = 3.7.3.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = /usr +base-exec-prefix = /usr +base-executable = /usr/bin/python3 diff --git a/setup.py b/setup.py index 7837f34..4193cd7 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,22 @@ -import setuptools - -with open("README.md", "r") as readme: - long_description = readme.read() - -setuptools.setup( - name="GiambIO", - version="1.0", - author="Nocturn9x aka IsGiambyy", - author_email="hackhab@gmail.com", - description="Asynchronous Python made easy (and friendly)", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/nocturn9x/giambio", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "License :: OSI Approved :: Apache License 2.0", - ], - python_requires=">=3.6", -) +import setuptools + +with open("README.md", "r") as readme: + long_description = readme.read() + +setuptools.setup( + name="GiambIO", + version="1.0", + author="Nocturn9x aka IsGiambyy", + author_email="hackhab@gmail.com", + description="Asynchronous Python made easy (and friendly)", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/nocturn9x/giambio", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache License 2.0", + ], + python_requires=">=3.6", +) diff --git a/tests/count.py b/tests/count.py index 14e9f09..ef74392 100644 --- a/tests/count.py +++ b/tests/count.py @@ -1,42 +1,42 @@ -import giambio - - -# A test for context managers - - -async def countdown(n: int): - print(f"Counting down from {n}!") - while n > 0: - print(f"Down {n}") - n -= 1 - await giambio.sleep(1) -# raise Exception("oh no man") # Uncomment to test propagation - print("Countdown over") - return 0 - - -async def countup(stop: int, step: int = 1): - print(f"Counting up to {stop}!") - x = 0 - while x < stop: - print(f"Up {x}") - x += 1 - await giambio.sleep(step) - print("Countup over") - return 1 - - -async def main(): - start = giambio.clock() - try: - async with giambio.create_pool() as pool: - pool.spawn(countdown, 10) - pool.spawn(countup, 5, 2) - print("Children spawned, awaiting completion") - except Exception as e: - print(f"Got -> {type(e).__name__}: {e}") - print(f"Task execution complete in {giambio.clock() - start:.2f} seconds") - - -if __name__ == "__main__": - giambio.run(main) +import giambio + + +# A test for context managers + + +async def countdown(n: int): + print(f"Counting down from {n}!") + while n > 0: + print(f"Down {n}") + n -= 1 + await giambio.sleep(1) +# raise Exception("oh no man") # Uncomment to test propagation + print("Countdown over") + return 0 + + +async def countup(stop: int, step: int = 1): + print(f"Counting up to {stop}!") + x = 0 + while x < stop: + print(f"Up {x}") + x += 1 + await giambio.sleep(step) + print("Countup over") + return 1 + + +async def main(): + start = giambio.clock() + try: + async with giambio.create_pool() as pool: + pool.spawn(countdown, 10) + pool.spawn(countup, 5, 2) + print("Children spawned, awaiting completion") + except Exception as e: + print(f"Got -> {type(e).__name__}: {e}") + print(f"Task execution complete in {giambio.clock() - start:.2f} seconds") + + +if __name__ == "__main__": + giambio.run(main) diff --git a/tests/events.py b/tests/events.py index c36db9a..65ceb6f 100644 --- a/tests/events.py +++ b/tests/events.py @@ -1,35 +1,35 @@ -import giambio - - -# A test for events - - -async def child(ev: giambio.Event, pause: int): - print("[child] Child is alive! Going to wait until notified") - start_total = giambio.clock() - await ev.wait() - end_pause = giambio.clock() - start_total - print(f"[child] Parent set the event, exiting in {pause} seconds") - start_sleep = giambio.clock() - await giambio.sleep(pause) - end_sleep = giambio.clock() - start_sleep - end_total = giambio.clock() - start_total - print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!") - - -async def parent(pause: int = 1): - async with giambio.create_pool() as pool: - event = giambio.Event() - print("[parent] Spawning child task") - pool.spawn(child, event, pause + 2) - start = giambio.clock() - print(f"[parent] Sleeping {pause} second(s) before setting the event") - await giambio.sleep(pause) - await event.trigger() - print("[parent] Event set, awaiting child") - end = giambio.clock() - start - print(f"[parent] Child exited in {end} seconds") - - -if __name__ == "__main__": - giambio.run(parent, 3) +import giambio + + +# A test for events + + +async def child(ev: giambio.Event, pause: int): + print("[child] Child is alive! Going to wait until notified") + start_total = giambio.clock() + await ev.wait() + end_pause = giambio.clock() - start_total + print(f"[child] Parent set the event, exiting in {pause} seconds") + start_sleep = giambio.clock() + await giambio.sleep(pause) + end_sleep = giambio.clock() - start_sleep + end_total = giambio.clock() - start_total + print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!") + + +async def parent(pause: int = 1): + async with giambio.create_pool() as pool: + event = giambio.Event() + print("[parent] Spawning child task") + pool.spawn(child, event, pause + 2) + start = giambio.clock() + print(f"[parent] Sleeping {pause} second(s) before setting the event") + await giambio.sleep(pause) + await event.trigger() + print("[parent] Event set, awaiting child") + end = giambio.clock() - start + print(f"[parent] Child exited in {end} seconds") + + +if __name__ == "__main__": + giambio.run(parent, 3) diff --git a/tests/join.py b/tests/join.py deleted file mode 100644 index a9624bd..0000000 --- a/tests/join.py +++ /dev/null @@ -1,25 +0,0 @@ -import giambio - - -async def child(sleep: int, ident: int): - start = giambio.clock() # This returns the current time from giambio's perspective - print(f"[child {ident}] Gonna sleep for {sleep} seconds!") - await giambio.sleep(sleep) - end = giambio.clock() - start - print(f"[child {ident}] I woke up! Slept for {end} seconds") - - -async def main(): - print("[parent] Spawning children") - task = giambio.spawn(child, 1, 1) # We spawn a child task - task2 = giambio.spawn(child, 2, 2) # and why not? another one! - start = giambio.clock() - print("[parent] Children spawned, awaiting completion") - await task.join() - await task2.join() - end = giambio.clock() - start - print(f"[parent] Execution terminated in {end} seconds") - - -if __name__ == "__main__": - giambio.run(main) # Start the async context \ No newline at end of file diff --git a/tests/server.py b/tests/server.py index 7f7668a..c80c127 100644 --- a/tests/server.py +++ b/tests/server.py @@ -1,54 +1,55 @@ -import giambio -from giambio.socket import AsyncSocket -import socket -import logging -import sys -import traceback - - -# A test to check for asynchronous I/O - - -async def serve(address: tuple): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(address) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.listen(5) - asock = giambio.wrap_socket(sock) # We make the socket an async socket - logging.info(f"Serving asynchronously at {address[0]}:{address[1]}") - async with giambio.create_pool() as pool: - conn, addr = await asock.accept() - logging.info(f"{addr[0]}:{addr[1]} connected") - pool.spawn(handler, conn, addr) - - -async def handler(sock: AsyncSocket, addr: tuple): - addr = f"{addr[0]}:{addr[1]}" - async with sock: - await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n") - while True: - await sock.send_all(b"-> ") - data = await sock.receive(1024) - if not data: - break - elif data == b"raise\n": - await sock.send_all(b"I'm dead dude\n") - raise TypeError("Oh, no, I'm gonna die!") - to_send_back = data - data = data.decode("utf-8").encode("unicode_escape") - logging.info(f"Got: '{data.decode('utf-8')}' from {addr}") - await sock.send_all(b"Got: " + to_send_back) - logging.info(f"Echoed back '{data.decode('utf-8')}' to {addr}") - logging.info(f"Connection from {addr} closed") - - -if __name__ == "__main__": - port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500 - logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p") - try: - giambio.run(serve, ("localhost", port)) - except (Exception, KeyboardInterrupt) as error: # Exceptions propagate! - if isinstance(error, KeyboardInterrupt): - logging.info("Ctrl+C detected, exiting") - else: - logging.error(f"Exiting due to a {type(error).__name__}: {error}") +import giambio +from giambio.socket import AsyncSocket +import socket +import logging +import sys +import traceback + + +# A test to check for asynchronous I/O + + +async def serve(address: tuple): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(address) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.listen(5) + asock = giambio.wrap_socket(sock) # We make the socket an async socket + logging.info(f"Serving asynchronously at {address[0]}:{address[1]}") + async with giambio.create_pool() as pool: + while True: + conn, addr = await asock.accept() + logging.info(f"{addr[0]}:{addr[1]} connected") + pool.spawn(handler, conn, addr) + print("oof done") + +async def handler(sock: AsyncSocket, addr: tuple): + addr = f"{addr[0]}:{addr[1]}" + async with sock: + await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n") + while True: + await sock.send_all(b"-> ") + data = await sock.receive(1024) + if not data: + break + elif data == b"raise\n": + await sock.send_all(b"I'm dead dude\n") + raise TypeError("Oh, no, I'm gonna die!") + to_send_back = data + data = data.decode("utf-8").encode("unicode_escape") + logging.info(f"Got: '{data.decode('utf-8')}' from {addr}") + await sock.send_all(b"Got: " + to_send_back) + logging.info(f"Echoed back '{data.decode('utf-8')}' to {addr}") + logging.info(f"Connection from {addr} closed") + + +if __name__ == "__main__": + port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500 + logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p") + try: + giambio.run(serve, ("localhost", port)) + except (Exception, KeyboardInterrupt) as error: # Exceptions propagate! + if isinstance(error, KeyboardInterrupt): + logging.info("Ctrl+C detected, exiting") + else: + logging.error(f"Exiting due to a {type(error).__name__}: {error}")