Added some exclude paths to gitignore

This commit is contained in:
nocturn9x 2020-11-17 10:54:18 +01:00
parent e29eaf3862
commit 70646a4767
17 changed files with 1920 additions and 1932 deletions

265
.gitignore vendored
View File

@ -1,131 +1,134 @@
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/ pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # 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. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
.python-version .python-version
pyenv.cfg
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # pipenv
# However, in case of collaboration, if having platform-specific dependencies or dependencies # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# having no cross-platform support, pipenv may install dependencies that don't work, or not # However, in case of collaboration, if having platform-specific dependencies or dependencies
# install all needed dependencies. # having no cross-platform support, pipenv may install dependencies that don't work, or not
#Pipfile.lock # install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule # Celery stuff
celerybeat.pid celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py # SageMath parsed files
*.sage.py
# Environments
.env # Environments
.venv .env
env/ bin
venv/ lib
ENV/ .venv
env.bak/ env/
venv.bak/ venv/
ENV/
# Spyder project settings env.bak/
.spyderproject venv.bak/
.spyproject
# Spyder project settings
# Rope project settings .spyderproject
.ropeproject .spyproject
# mkdocs documentation # Rope project settings
/site .ropeproject
# mypy # mkdocs documentation
.mypy_cache/ /site
.dmypy.json
dmypy.json # mypy
.mypy_cache/
# Pyre type checker .dmypy.json
.pyre/ dmypy.json
.*.swp # Pyre type checker
.pyre/
.*.swp

374
LICENSE
View File

@ -1,187 +1,187 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions. 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, "License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document. and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by "Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License. the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all "Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition, control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity. outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity "You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License. exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, "Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation including but not limited to software source code, documentation
source, and configuration files. source, and configuration files.
"Object" form shall mean any form resulting from mechanical "Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation, not limited to compiled object code, generated documentation,
and conversions to other media types. and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or "Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work copyright notice that is included in or attached to the work
(an example is provided in the Appendix below). (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object "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 form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of, separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof. the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including "Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner 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 or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted" the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution." designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity "Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work. subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of 2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual, this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of, copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form. Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of 3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual, this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, (except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work, use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s) Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate granted to You under this License for that Work shall terminate
as of the date such litigation is filed. as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the 4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You modifications, and in Source or Object form, provided that You
meet the following conditions: meet the following conditions:
(a) You must give any other recipients of the Work or (a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices (b) You must cause any modified files to carry prominent notices
stating that You changed the files; and stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works (c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work, attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of excluding those notices that do not pertain to any part of
the Derivative Works; and the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its (d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or, documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed that such additional attribution notices cannot be construed
as modifying the License. as modifying the License.
You may add Your own copyright statement to Your modifications and You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use, for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License. the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, 5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions. this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions. with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade 6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor, names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file. origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or 7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS, Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License. risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, 8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise, whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the 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 (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages. has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing 9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer, the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity, and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify, of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability. of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work. APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]" boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.

760
README.md
View File

@ -1,380 +1,380 @@
# giambio - Asynchronous Python made easy (and friendly) # giambio - Asynchronous Python made easy (and friendly)
giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing. 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 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). "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 _*_: 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 production ready, so be aware that it is likely that you'll find bugs and race conditions
## Disclaimer ## Disclaimer
Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works 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! 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 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 [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 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) that most of the following text is ~~stolen~~ inspired from its documentation)
# What the hell is async anyway? # 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). 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. 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 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. 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 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. 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)), 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. and network servers happen to be heavily based on I/O: a blocking operation.
Starting to see where we're heading? Starting to see where we're heading?
## A deeper dive ## 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 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). 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: 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. asynchronous functions, or 'async' for short.
Async functions are functions defined with `async def` instead of the regular `def`, like so: Async functions are functions defined with `async def` instead of the regular `def`, like so:
```python ```python
async def async_fun(): # An async function async def async_fun(): # An async function
print("Hello, world!") print("Hello, world!")
def sync_fun(): # A regular (sync) function def sync_fun(): # A regular (sync) function
print("Hello, world!") 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: 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 ```python
async def async_two(): async def async_two():
print("Hello from async_two!") print("Hello from async_two!")
async def async_one(): async def async_one():
print("Hello from async_one!") print("Hello from async_one!")
await async_two() # This is an async call 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 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. 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' 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? 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 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 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_. 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 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? 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: Take a look at this example below:
```python ```python
import giambio import giambio
async def foo(): async def foo():
print("Hello, world!") print("Hello, world!")
giambio.run(foo) # Prints 'Hello, world!' giambio.run(foo) # Prints 'Hello, world!'
``` ```
This could as well be written the following way and would produce the same output: This could as well be written the following way and would produce the same output:
```python ```python
def foo(): def foo():
print("Hello, world!") print("Hello, world!")
foo() # Prints '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. 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 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! 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. 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): 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 __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 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. model is much simpler if we take coroutines out of the game, and everything works just the same.
```python ```python
import giambio import giambio
async def sleep_double(n): async def sleep_double(n):
await giambio.sleep(2 * n) await giambio.sleep(2 * n)
giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns 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. 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 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. make it async in the first place.
### Don't forget the `await`! ### 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 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. think that forgetting to do so would raise an error, but it's actually a little bit trickier than that.
Take this example here: Take this example here:
```python ```python
import giambio import giambio
async def sleep_double_broken(n): async def sleep_double_broken(n):
print("Taking a nap!") print("Taking a nap!")
start = giambio.clock() start = giambio.clock()
giambio.sleep(2 * n) # We forgot the await! giambio.sleep(2 * n) # We forgot the await!
end = giambio.clock() - start end = giambio.clock() - start
print(f"Slept for {end:.2f} seconds!") print(f"Slept for {end:.2f} seconds!")
giambio.run(sleep_double_broken, 2) giambio.run(sleep_double_broken, 2)
``` ```
Running this code, will produce an output that looks like this: Running this code, will produce an output that looks like this:
``` ```
Taking a nap! Taking a nap!
Slept 0.00 seconds! Slept 0.00 seconds!
__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited __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: 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 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 that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a
garbage collection cycle occurred or not). 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 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 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: 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. 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 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. 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' When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine'
object object
```python ```python
>>> giambio.sleep(1) >>> giambio.sleep(1)
<coroutine object sleep at 0x1069520d0> <coroutine object sleep at 0x1069520d0>
``` ```
The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering 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: `await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things:
- It creates this weird coroutine object - It creates this weird coroutine object
- Passes that object to `await`, which runs the function - 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. So basically that's why you always need to put `await` in front of an async function when calling it.
## Something actually useful ## Something actually useful
Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special 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_. 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 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? 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__. 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. Yep, you read that right.
To demonstrate this, have a look a this example To demonstrate this, have a look a this example
```python ```python
import giambio import giambio
async def countdown(n: int): async def countdown(n: int):
print(f"Counting down from {n}!") print(f"Counting down from {n}!")
while n > 0: while n > 0:
print(f"Down {n}") print(f"Down {n}")
n -= 1 n -= 1
await giambio.sleep(1) await giambio.sleep(1)
print("Countdown over") print("Countdown over")
return 0 return 0
async def countup(stop: int): async def countup(stop: int):
print(f"Counting up to {stop}!") print(f"Counting up to {stop}!")
x = 0 x = 0
while x < stop: while x < stop:
print(f"Up {x}") print(f"Up {x}")
x += 1 x += 1
await giambio.sleep(2) await giambio.sleep(2)
print("Countup over") print("Countup over")
return 1 return 1
async def main(): async def main():
start = giambio.clock() start = giambio.clock()
async with giambio.create_pool() as pool: async with giambio.create_pool() as pool:
pool.spawn(countdown, 10) pool.spawn(countdown, 10)
pool.spawn(countup, 5) pool.spawn(countup, 5)
print("Children spawned, awaiting completion") print("Children spawned, awaiting completion")
print(f"Task execution complete in {giambio.clock() - start:2f} seconds") print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
if __name__ == "__main__": if __name__ == "__main__":
giambio.run(main) giambio.run(main)
``` ```
There is a lot going on here, and we'll explain every bit of it step by step: 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` - 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 - 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 this tutorial, `countup` will be running twice as slow as `countdown` (see the call
to `await giambio.sleep(2)`?) to `await giambio.sleep(2)`?)
- Here comes the real fun: `async with`? What's going on there? - 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 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. 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, 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__()` 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` 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 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 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 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? `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 __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 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. 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. 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, - 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()`), 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 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, that it will return immediately, without waiting for the async function to finish. So,
now our functions are running in the background. 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 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` 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 async function in this case) until all children task have exited, and as it turns out, that
is a good thing. is a good thing.
The reason why pools always wait for all children to have finished executing is that it makes 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, easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks,
exceptions in giambio always behave as expected exceptions in giambio always behave as expected
Ok, so, let's try running this snippet and see what we get: Ok, so, let's try running this snippet and see what we get:
``` ```
Children spawned, awaiting completion Children spawned, awaiting completion
Counting down from 10! Counting down from 10!
Down 10 Down 10
Counting up to 5! Counting up to 5!
Up 0 Up 0
Down 9 Down 9
Up 1 Up 1
Down 8 Down 8
Down 7 Down 7
Up 2 Up 2
Down 6 Down 6
Down 5 Down 5
Up 3 Up 3
Down 4 Down 4
Down 3 Down 3
Up 4 Up 4
Down 2 Down 2
Down 1 Down 1
Countup over Countup over
Countdown over Countdown over
Task execution complete in 10.07 seconds Task execution complete in 10.07 seconds
``` ```
(Your output might have some lines swapped compared to this) (Your output might have some lines swapped compared to this)
You see how `countup` and `countdown` both start and finish You see how `countup` and `countdown` both start and finish
together? Moreover, even though each function slept for about 10 together? Moreover, even though each function slept for about 10
seconds (therefore 20 seconds total), the program just took 10 seconds (therefore 20 seconds total), the program just took 10
seconds to complete, so our children are really running at the same time. 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: 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 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_ 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 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. 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 Giambio, on the other hand, can switch tasks only at certain fixed points called
_checkpoints_, more on that later. _checkpoints_, more on that later.
### A sneak peak into the async world ### A sneak peak into the async world
The basic idea behind libraries like giambio is that they can run a lot of tasks 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. 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 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 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 pesky details to use giambio, but it's good to have at least an high-level understanding
of how this all works. of how this all works.
The peculiarity of asynchronous functions is that they can suspend their execution: that's 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 `await` does, it yields back the execution control to giambio, which can then decide
what to do next. what to do next.
To understand this better, take a look at this code: To understand this better, take a look at this code:
```python ```python
def countdown(n: int) -> int: def countdown(n: int) -> int:
while n: while n:
yield n yield n
n -= 1 n -= 1
for x in countdown(5): for x in countdown(5):
print(x) print(x)
``` ```
In the above snippet, `countdown` is a generator function. Generators are really useful because 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: they allow to customize iteration. Running that code produces the following output:
``` ```
5 5
4 4
3 3
2 2
1 1
``` ```
The trick for this to work is `yield`. The trick for this to work is `yield`.
What `yield` does is return back to the caller and suspend itself: In our case, `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 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 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 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. `async`/`await` logic overlaps a lot with generator functions.
Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made
way before Python 3.5 added that nice new syntax. 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 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 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 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 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 by looking at its source code. That is very different from what threads do: they can (and will) switch
whenever they feel like it. whenever they feel like it.
Remember when we talked about checkpoints? That's what they are: calls to async functions that allow 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, 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 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, 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. and it will reschedule the caller almost immediately, because the sleep time is 0.
### Mix and match? No thanks ### Mix and match? No thanks
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a 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 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 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 `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 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. 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 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 giambio to get very confused and most likely just explode spectacularly badly
TODO: I/O TODO: I/O
## Contributing ## Contributing
This is a relatively young project and it is looking for collaborators! It's not rocket science, 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, 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) 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) or by [E-mail](mailto:hackhab@gmail.com)

View File

@ -1,38 +1,38 @@
""" """
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
__author__ = "Nocturn9x aka Isgiambyy" __author__ = "Nocturn9x aka Isgiambyy"
__version__ = (1, 0, 0) __version__ = (1, 0, 0)
from . import exceptions from . import exceptions
from .traps import sleep, current_task from .traps import sleep, current_task
from .objects import Event from .objects import Event
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
__all__ = [ __all__ = [
"exceptions", "exceptions",
"sleep", "sleep",
"Event", "Event",
"run", "run",
"clock", "clock",
"wrap_socket", "wrap_socket",
"create_pool", "create_pool",
"get_event_loop", "get_event_loop",
"current_task", "current_task",
"new_event_loop" "new_event_loop"
] ]

View File

@ -1,69 +1,70 @@
""" """
Higher-level context managers for async pools Higher-level context managers for async pools
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import types import types
from .core import AsyncScheduler from .core import AsyncScheduler
from .objects import Task from .objects import Task
class TaskManager: class TaskManager:
""" """
An asynchronous context manager for giambio An asynchronous context manager for giambio
""" """
def __init__(self, loop: AsyncScheduler) -> None: def __init__(self, loop: AsyncScheduler) -> None:
""" """
Object constructor Object constructor
""" """
self.loop = loop self.loop = loop
self.tasks = [] self.tasks = []
def spawn(self, func: types.FunctionType, *args): def spawn(self, func: types.FunctionType, *args):
""" """
Spawns a child task Spawns a child task
""" """
task = Task(func(*args), func.__name__ or str(func)) task = Task(func(*args), func.__name__ or str(func))
task.parent = self.loop.current_task task.parent = self.loop.current_task
self.loop.tasks.append(task) self.loop.tasks.append(task)
self.tasks.append(task) self.tasks.append(task)
def spawn_after(self, func: types.FunctionType, n: int, *args): def spawn_after(self, func: types.FunctionType, n: int, *args):
""" """
Schedules a task for execution after n seconds Schedules a task for execution after n seconds
""" """
assert n >= 0, "The time delay can't be negative" assert n >= 0, "The time delay can't be negative"
task = Task(func(*args), func.__name__ or str(func)) task = Task(func(*args), func.__name__ or str(func))
task.parent = self.loop.current_task task.parent = self.loop.current_task
self.loop.paused.put(task, n) self.loop.paused.put(task, n)
self.tasks.append(task) self.tasks.append(task)
async def __aenter__(self): async def __aenter__(self):
return self return self
async def __aexit__(self, exc_type, exc, tb): async def __aexit__(self, exc_type, exc, tb):
for task in self.tasks: for task in self.tasks:
try: try:
await task.join() await task.join()
except BaseException as e: except BaseException:
self.tasks.remove(task) self.tasks.remove(task)
for to_cancel in self.tasks: for to_cancel in self.tasks:
await to_cancel.cancel() await to_cancel.cancel()
print("oof")

View File

@ -1,398 +1,398 @@
""" """
The main runtime environment for giambio The main runtime environment for giambio
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
# Import libraries and internal resources # Import libraries and internal resources
import types import types
import socket import socket
from time import sleep as wait from time import sleep as wait
from timeit import default_timer from timeit import default_timer
from .objects import Task, TimeQueue from .objects import Task, TimeQueue
from socket import SOL_SOCKET, SO_ERROR from socket import SOL_SOCKET, SO_ERROR
from .traps import want_read, want_write from .traps import want_read, want_write
from collections import deque from collections import deque
from .socket import AsyncSocket, WantWrite, WantRead from .socket import AsyncSocket, WantWrite, WantRead
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
from .exceptions import (InternalError, from .exceptions import (InternalError,
CancelledError, CancelledError,
ResourceBusy, ResourceBusy,
) )
class AsyncScheduler: class AsyncScheduler:
""" """
An asynchronous scheduler implementation. Tries to mimic the threaded An asynchronous scheduler implementation. Tries to mimic the threaded
model in its simplicity, without using actual threads, but rather alternating 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 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 with its calculations. An attempt to fix the threaded model has been made
without making the API unnecessarily complicated. without making the API unnecessarily complicated.
A few examples are tasks cancellation and exception propagation. A few examples are tasks cancellation and exception propagation.
""" """
def __init__(self): def __init__(self):
""" """
Object constructor Object constructor
""" """
# Tasks that are ready to run # Tasks that are ready to run
self.tasks = deque() self.tasks = deque()
# Selector object to perform I/O multiplexing # Selector object to perform I/O multiplexing
self.selector = DefaultSelector() self.selector = DefaultSelector()
# This will always point to the currently running coroutine (Task object) # This will always point to the currently running coroutine (Task object)
self.current_task = None self.current_task = None
# Monotonic clock to keep track of elapsed time reliably # Monotonic clock to keep track of elapsed time reliably
self.clock = default_timer self.clock = default_timer
# Tasks that are asleep # Tasks that are asleep
self.paused = TimeQueue(self.clock) self.paused = TimeQueue(self.clock)
# All active Event objects # All active Event objects
self.events = set() self.events = set()
# Data to send back to a trap # Data to send back to a trap
self.to_send = None self.to_send = None
# Have we ever ran? # Have we ever ran?
self.has_ran = False self.has_ran = False
def done(self): def done(self):
""" """
Returns True if there is work to do Returns True if there is work to do
""" """
if self.selector.get_map() or any([self.paused, if self.selector.get_map() or any([self.paused,
self.tasks, self.tasks,
self.events self.events
]): ]):
return False return False
return True return True
def shutdown(self): def shutdown(self):
""" """
Shuts down the event loop Shuts down the event loop
""" """
# TODO: See if other teardown is required (massive join()?) # TODO: See if other teardown is required (massive join()?)
self.selector.close() self.selector.close()
def run(self): def run(self):
""" """
Starts the loop and 'listens' for events until there is work to do, Starts the loop and 'listens' for events until there is work to do,
then exits. This behavior kinda reflects a kernel, as coroutines can then exits. This behavior kinda reflects a kernel, as coroutines can
request the loop's functionality only trough some fixed entry points, request the loop's functionality only trough some fixed entry points,
which in turn yield and give execution control to the loop itself. which in turn yield and give execution control to the loop itself.
""" """
while True: while True:
try: try:
if self.done(): if self.done():
self.shutdown() self.shutdown()
break break
elif not self.tasks: elif not self.tasks:
if self.paused: if self.paused:
# If there are no actively running tasks # If there are no actively running tasks
# we try to schedule the asleep ones # we try to schedule the asleep ones
self.awake_sleeping() self.awake_sleeping()
if self.selector.get_map(): if self.selector.get_map():
# The next step is checking for I/O # The next step is checking for I/O
self.check_io() self.check_io()
if self.events: if self.events:
# Try to awake event-waiting tasks # Try to awake event-waiting tasks
self.check_events() self.check_events()
# While there are tasks to run # While there are tasks to run
while self.tasks: while self.tasks:
# Sets the currently running task # Sets the currently running task
self.current_task = self.tasks.popleft() self.current_task = self.tasks.popleft()
if self.current_task.cancel_pending: if self.current_task.cancel_pending:
self.do_cancel() self.do_cancel()
if self.to_send and self.current_task.status != "init": if self.to_send and self.current_task.status != "init":
data = self.to_send data = self.to_send
else: else:
data = None data = None
# Run a single step with the calculation # Run a single step with the calculation
method, *args = self.current_task.run(data) method, *args = self.current_task.run(data)
self.current_task.status = "run" self.current_task.status = "run"
self.current_task.steps += 1 self.current_task.steps += 1
# Data has been sent, reset it to None # Data has been sent, reset it to None
if self.to_send and self.current_task != "init": if self.to_send and self.current_task != "init":
self.to_send = None self.to_send = None
# Sneaky method call, thanks to David Beazley for this ;) # Sneaky method call, thanks to David Beazley for this ;)
getattr(self, method)(*args) getattr(self, method)(*args)
except AttributeError: # If this happens, that's quite bad! except AttributeError: # If this happens, that's quite bad!
raise InternalError("Uh oh! Something very bad just happened, did" raise InternalError("Uh oh! Something very bad just happened, did"
" you try to mix primitives from other async libraries?") from None " you try to mix primitives from other async libraries?") from None
except CancelledError: except CancelledError:
self.current_task.status = "cancelled" self.current_task.status = "cancelled"
self.current_task.cancelled = True self.current_task.cancelled = True
self.current_task.cancel_pending = False self.current_task.cancel_pending = False
self.join() # TODO: Investigate if a call to join() is needed self.join() # TODO: Investigate if a call to join() is needed
except StopIteration as ret: except StopIteration as ret:
# Coroutine ends # Coroutine ends
self.current_task.status = "end" self.current_task.status = "end"
self.current_task.result = ret.value self.current_task.result = ret.value
self.current_task.finished = True self.current_task.finished = True
self.join() self.join()
except BaseException as err: except BaseException as err:
self.current_task.exc = err self.current_task.exc = err
self.current_task.status = "crashed" self.current_task.status = "crashed"
self.join() self.join()
def do_cancel(self): def do_cancel(self):
""" """
Performs task cancellation by throwing CancelledError inside the current Performs task cancellation by throwing CancelledError inside the current
task in order to stop it from executing. The loop continues to execute task in order to stop it from executing. The loop continues to execute
as tasks are independent as tasks are independent
""" """
# TODO: Do we need anything else? # TODO: Do we need anything else?
self.current_task.throw(CancelledError) self.current_task.throw(CancelledError)
def get_running(self): def get_running(self):
""" """
Returns the current task Returns the current task
""" """
self.tasks.append(self.current_task) self.tasks.append(self.current_task)
self.to_send = self.current_task self.to_send = self.current_task
def check_events(self): def check_events(self):
""" """
Checks for ready or expired events and triggers them Checks for ready or expired events and triggers them
""" """
for event in self.events.copy(): for event in self.events.copy():
if event.set: if event.set:
event.event_caught = True event.event_caught = True
event.waiters event.waiters
self.tasks.extend(event.waiters) self.tasks.extend(event.waiters)
self.events.remove(event) self.events.remove(event)
def awake_sleeping(self): def awake_sleeping(self):
""" """
Checks for and reschedules sleeping tasks Checks for and reschedules sleeping tasks
""" """
wait(max(0.0, self.paused[0][0] - self.clock())) wait(max(0.0, self.paused[0][0] - self.clock()))
# Sleep until the closest deadline in order not to waste CPU cycles # Sleep until the closest deadline in order not to waste CPU cycles
while self.paused[0][0] < self.clock(): while self.paused[0][0] < self.clock():
# Reschedules tasks when their deadline has elapsed # Reschedules tasks when their deadline has elapsed
self.tasks.append(self.paused.get()) self.tasks.append(self.paused.get())
if not self.paused: if not self.paused:
break break
def check_io(self): def check_io(self):
""" """
Checks and schedules task to perform I/O Checks and schedules task to perform I/O
""" """
if self.tasks or self.events: # If there are tasks or events, never wait if self.tasks or self.events: # If there are tasks or events, never wait
timeout = 0.0 timeout = 0.0
elif self.paused: # If there are asleep tasks, wait until the closest elif self.paused: # If there are asleep tasks, wait until the closest
# deadline # deadline
timeout = max(0.0, self.paused[0][0] - self.clock()) timeout = max(0.0, self.paused[0][0] - self.clock())
else: else:
timeout = None # If we _only_ have I/O to do, then wait indefinitely timeout = None # If we _only_ have I/O to do, then wait indefinitely
for key in dict(self.selector.get_map()).values(): for key in dict(self.selector.get_map()).values():
# We make sure we don't reschedule finished tasks # We make sure we don't reschedule finished tasks
if key.data.finished: if key.data.finished:
key.data.last_io = () key.data.last_io = ()
self.selector.unregister(key.fileobj) self.selector.unregister(key.fileobj)
if self.selector.get_map(): # If there is indeed tasks waiting on I/O if self.selector.get_map(): # If there is indeed tasks waiting on I/O
io_ready = self.selector.select(timeout) io_ready = self.selector.select(timeout)
# Get sockets that are ready and schedule their tasks # Get sockets that are ready and schedule their tasks
for key, _ in io_ready: for key, _ in io_ready:
self.tasks.append(key.data) # Resource ready? Schedule its task self.tasks.append(key.data) # Resource ready? Schedule its task
def start(self, func: types.FunctionType, *args): def start(self, func: types.FunctionType, *args):
""" """
Starts the event loop from a sync context Starts the event loop from a sync context
""" """
entry = Task(func(*args), func.__name__ or str(func)) entry = Task(func(*args), func.__name__ or str(func))
self.tasks.append(entry) self.tasks.append(entry)
self.run() self.run()
self.has_ran = True self.has_ran = True
if entry.exc: if entry.exc:
raise entry.exc from None raise entry.exc from None
def reschedule_joinee(self): def reschedule_joinee(self):
""" """
Reschedules the joinee(s) task of the Reschedules the joinee(s) of the
currently running task, if any currently running task, if any
""" """
self.tasks.extend(self.current_task.waiters) self.tasks.extend(self.current_task.waiters)
def join(self): def join(self):
""" """
Handler for the 'join' event, does some magic to tell the scheduler Handler for the 'join' event, does some magic to tell the scheduler
to wait until the current coroutine ends to wait until the current coroutine ends
""" """
child = self.current_task child = self.current_task
child.joined = True child.joined = True
if child.parent: if child.parent:
child.waiters.append(child.parent) child.waiters.append(child.parent)
if child.finished: if child.finished:
self.reschedule_joinee() self.reschedule_joinee()
elif child.exc: elif child.exc:
... # TODO: Handle exceptions ... # TODO: Handle exceptions
def sleep(self, seconds: int or float): def sleep(self, seconds: int or float):
""" """
Puts the caller to sleep for a given amount of seconds Puts the caller to sleep for a given amount of seconds
""" """
if seconds: if seconds:
self.current_task.status = "sleep" self.current_task.status = "sleep"
self.paused.put(self.current_task, seconds) self.paused.put(self.current_task, seconds)
else: else:
self.tasks.append(self.current_task) self.tasks.append(self.current_task)
# TODO: More generic I/O rather than just sockets # TODO: More generic I/O rather than just sockets
def want_read(self, sock: socket.socket): def want_read(self, sock: socket.socket):
""" """
Handler for the 'want_read' event, registers the socket inside the Handler for the 'want_read' event, registers the socket inside the
selector to perform I/0 multiplexing selector to perform I/0 multiplexing
""" """
self.current_task.status = "I/O" self.current_task.status = "I/O"
if self.current_task.last_io: if self.current_task.last_io:
if self.current_task.last_io == ("READ", sock): if self.current_task.last_io == ("READ", sock):
# Socket is already scheduled! # Socket is already scheduled!
return return
else: else:
self.selector.unregister(sock) self.selector.unregister(sock)
self.current_task.last_io = "READ", sock self.current_task.last_io = "READ", sock
try: try:
self.selector.register(sock, EVENT_READ, self.current_task) self.selector.register(sock, EVENT_READ, self.current_task)
except KeyError: except KeyError:
# The socket is already registered doing something else # The socket is already registered doing something else
raise ResourceBusy("The given resource is busy!") from None raise ResourceBusy("The given resource is busy!") from None
def want_write(self, sock: socket.socket): def want_write(self, sock: socket.socket):
""" """
Handler for the 'want_write' event, registers the socket inside the Handler for the 'want_write' event, registers the socket inside the
selector to perform I/0 multiplexing selector to perform I/0 multiplexing
""" """
self.current_task.status = "I/O" self.current_task.status = "I/O"
if self.current_task.last_io: if self.current_task.last_io:
if self.current_task.last_io == ("WRITE", sock): if self.current_task.last_io == ("WRITE", sock):
# Socket is already scheduled! # Socket is already scheduled!
return return
else: else:
# TODO: Inspect why modify() causes issues # TODO: Inspect why modify() causes issues
self.selector.unregister(sock) self.selector.unregister(sock)
self.current_task.last_io = "WRITE", sock self.current_task.last_io = "WRITE", sock
try: try:
self.selector.register(sock, EVENT_WRITE, self.current_task) self.selector.register(sock, EVENT_WRITE, self.current_task)
except KeyError: except KeyError:
raise ResourceBusy("The given resource is busy!") from None raise ResourceBusy("The given resource is busy!") from None
def event_set(self, event): def event_set(self, event):
""" """
Sets an event Sets an event
""" """
self.events.add(event) self.events.add(event)
event.waiters.append(self.current_task) event.waiters.append(self.current_task)
event.set = True event.set = True
self.reschedule_joinee() self.reschedule_joinee()
def event_wait(self, event): def event_wait(self, event):
""" """
Pauses the current task on an event Pauses the current task on an event
""" """
event.waiters.append(self.current_task) event.waiters.append(self.current_task)
def cancel(self): def cancel(self):
""" """
Handler for the 'cancel' event, schedules the task to be cancelled later Handler for the 'cancel' event, schedules the task to be cancelled later
or does so straight away if it is safe to do so or does so straight away if it is safe to do so
""" """
if self.current_task.status in ("I/O", "sleep"): if self.current_task.status in ("I/O", "sleep"):
# We cancel right away # We cancel right away
self.do_cancel() self.do_cancel()
else: else:
self.current_task.cancel_pending = True # Cancellation is deferred self.current_task.cancel_pending = True # Cancellation is deferred
def wrap_socket(self, sock): def wrap_socket(self, sock):
""" """
Wraps a standard socket into an AsyncSocket object Wraps a standard socket into an AsyncSocket object
""" """
return AsyncSocket(sock, self) return AsyncSocket(sock, self)
async def read_sock(self, sock: socket.socket, buffer: int): async def read_sock(self, sock: socket.socket, buffer: int):
""" """
Reads from a socket asynchronously, waiting until the resource is Reads from a socket asynchronously, waiting until the resource is
available and returning up to buffer bytes from the socket available and returning up to buffer bytes from the socket
""" """
try: try:
return sock.recv(buffer) return sock.recv(buffer)
except WantRead: except WantRead:
await want_read(sock) await want_read(sock)
return sock.recv(buffer) return sock.recv(buffer)
async def accept_sock(self, sock: socket.socket): async def accept_sock(self, sock: socket.socket):
""" """
Accepts a socket connection asynchronously, waiting until the resource Accepts a socket connection asynchronously, waiting until the resource
is available and returning the result of the accept() call is available and returning the result of the accept() call
""" """
try: try:
return sock.accept() return sock.accept()
except WantRead: except WantRead:
await want_read(sock) await want_read(sock)
return sock.accept() return sock.accept()
async def sock_sendall(self, sock: socket.socket, data: bytes): async def sock_sendall(self, sock: socket.socket, data: bytes):
""" """
Sends all the passed data, as bytes, trough the socket asynchronously Sends all the passed data, as bytes, trough the socket asynchronously
""" """
while data: while data:
try: try:
sent_no = sock.send(data) sent_no = sock.send(data)
except WantWrite: except WantWrite:
await want_write(sock) await want_write(sock)
sent_no = sock.send(data) sent_no = sock.send(data)
data = data[sent_no:] data = data[sent_no:]
async def close_sock(self, sock: socket.socket): async def close_sock(self, sock: socket.socket):
""" """
Closes the socket asynchronously Closes the socket asynchronously
""" """
await want_write(sock) await want_write(sock)
self.selector.unregister(sock) self.selector.unregister(sock)
return sock.close() return sock.close()
async def connect_sock(self, sock: socket.socket, addr: tuple): async def connect_sock(self, sock: socket.socket, addr: tuple):
""" """
Connects a socket asynchronously Connects a socket asynchronously
""" """
try: # "Borrowed" from curio try: # "Borrowed" from curio
return sock.connect(addr) return sock.connect(addr)
except WantWrite: except WantWrite:
await want_write(sock) await want_write(sock)
err = sock.getsockopt(SOL_SOCKET, SO_ERROR) err = sock.getsockopt(SOL_SOCKET, SO_ERROR)
if err != 0: if err != 0:
raise OSError(err, f"Connect call failed: {addr}") raise OSError(err, f"Connect call failed: {addr}")

View File

@ -1,60 +1,60 @@
""" """
Exceptions for giambio Exceptions for giambio
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
class GiambioError(Exception): class GiambioError(Exception):
""" """
Base class for giambio exceptions Base class for giambio exceptions
""" """
... ...
class InternalError(GiambioError): class InternalError(GiambioError):
""" """
Internal exception Internal exception
""" """
... ...
class CancelledError(BaseException): class CancelledError(BaseException):
""" """
Exception raised by the giambio.objects.Task.cancel() method Exception raised by the giambio.objects.Task.cancel() method
to terminate a child task. This should NOT be catched, or to terminate a child task. This should NOT be catched, or
at least it should be re-raised and never ignored at least it should be re-raised and never ignored
""" """
... ...
class ResourceBusy(GiambioError): class ResourceBusy(GiambioError):
""" """
Exception that is raised when a resource is accessed by more than Exception that is raised when a resource is accessed by more than
one task at a time one task at a time
""" """
... ...
class ResourceClosed(GiambioError): class ResourceClosed(GiambioError):
""" """
Raised when I/O is attempted on a closed resource Raised when I/O is attempted on a closed resource
""" """
... ...

View File

@ -1,158 +1,158 @@
""" """
Various object wrappers and abstraction layers Various object wrappers and abstraction layers
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import types import types
from .traps import join, cancel, event_set, event_wait from .traps import join, cancel, event_set, event_wait
from heapq import heappop, heappush from heapq import heappop, heappush
from .exceptions import GiambioError from .exceptions import GiambioError
from dataclasses import dataclass, field from dataclasses import dataclass, field
@dataclass @dataclass
class Task: class Task:
""" """
A simple wrapper around a coroutine object A simple wrapper around a coroutine object
""" """
coroutine: types.CoroutineType coroutine: types.CoroutineType
name: str name: str
cancelled: bool = False # True if the task gets cancelled cancelled: bool = False # True if the task gets cancelled
exc: BaseException = None exc: BaseException = None
result: object = None result: object = None
finished: bool = False finished: bool = False
status: str = "init" status: str = "init"
steps: int = 0 steps: int = 0
last_io: tuple = () last_io: tuple = ()
parent: object = None parent: object = None
joined: bool= False joined: bool= False
cancel_pending: bool = False cancel_pending: bool = False
waiters: list = field(default_factory=list) waiters: list = field(default_factory=list)
def run(self, what=None): def run(self, what=None):
""" """
Simple abstraction layer over coroutines' ``send`` method Simple abstraction layer over coroutines' ``send`` method
""" """
return self.coroutine.send(what) return self.coroutine.send(what)
def throw(self, err: Exception): def throw(self, err: Exception):
""" """
Simple abstraction layer over coroutines ``throw`` method Simple abstraction layer over coroutines ``throw`` method
""" """
return self.coroutine.throw(err) return self.coroutine.throw(err)
async def join(self): async def join(self):
""" """
Joins the task Joins the task
""" """
res = await join(self) res = await join(self)
if self.exc: if self.exc:
raise self.exc raise self.exc
return res return res
async def cancel(self): async def cancel(self):
""" """
Cancels the task Cancels the task
""" """
await cancel(self) await cancel(self)
def __del__(self): def __del__(self):
self.coroutine.close() self.coroutine.close()
class Event: class Event:
""" """
A class designed similarly to threading.Event A class designed similarly to threading.Event
""" """
def __init__(self): def __init__(self):
""" """
Object constructor Object constructor
""" """
self.set = False self.set = False
self.waiters = [] self.waiters = []
self.event_caught = False self.event_caught = False
async def trigger(self): async def trigger(self):
""" """
Sets the event, waking up all tasks that called Sets the event, waking up all tasks that called
pause() on us pause() on us
""" """
if self.set: if self.set:
raise GiambioError("The event has already been set") raise GiambioError("The event has already been set")
await event_set(self) await event_set(self)
async def wait(self): async def wait(self):
""" """
Waits until the event is set Waits until the event is set
""" """
await event_wait(self) await event_wait(self)
class TimeQueue: class TimeQueue:
""" """
An abstraction layer over a heap queue based on time. This is where An abstraction layer over a heap queue based on time. This is where
sleeping tasks will be put when they are not running sleeping tasks will be put when they are not running
""" """
def __init__(self, clock): def __init__(self, clock):
""" """
Object constructor Object constructor
""" """
self.clock = clock self.clock = clock
self.sequence = 0 self.sequence = 0
self.container = [] self.container = []
def __contains__(self, item): def __contains__(self, item):
return item in self.container return item in self.container
def __iter__(self): def __iter__(self):
return iter(self.container) return iter(self.container)
def __getitem__(self, item): def __getitem__(self, item):
return self.container.__getitem__(item) return self.container.__getitem__(item)
def __bool__(self): def __bool__(self):
return bool(self.container) return bool(self.container)
def __repr__(self): def __repr__(self):
return f"TimeQueue({self.container}, clock={self.clock})" return f"TimeQueue({self.container}, clock={self.clock})"
def put(self, item, amount): def put(self, item, amount):
""" """
Pushes an item onto the queue with its unique Pushes an item onto the queue with its unique
time amount and ID time amount and ID
""" """
heappush(self.container, (self.clock() + amount, self.sequence, item)) heappush(self.container, (self.clock() + amount, self.sequence, item))
self.sequence += 1 self.sequence += 1
def get(self): def get(self):
""" """
Gets the first task that is meant to run Gets the first task that is meant to run
""" """
return heappop(self.container)[2] return heappop(self.container)[2]

View File

@ -1,99 +1,99 @@
""" """
Helper methods and public API Helper methods and public API
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import socket import socket
import threading import threading
from .core import AsyncScheduler from .core import AsyncScheduler
from .exceptions import GiambioError from .exceptions import GiambioError
from .context import TaskManager from .context import TaskManager
from .socket import AsyncSocket from .socket import AsyncSocket
from types import FunctionType, CoroutineType, GeneratorType from types import FunctionType, CoroutineType, GeneratorType
thread_local = threading.local() thread_local = threading.local()
def get_event_loop(): def get_event_loop():
""" """
Returns the event loop associated to the current Returns the event loop associated to the current
thread thread
""" """
try: try:
return thread_local.loop return thread_local.loop
except AttributeError: except AttributeError:
raise GiambioError("no event loop set") from None raise GiambioError("no event loop set") from None
def new_event_loop(): def new_event_loop():
""" """
Associates a new event loop to the current thread Associates a new event loop to the current thread
and deactivates the old one. This should not be and deactivates the old one. This should not be
called explicitly unless you know what you're doing called explicitly unless you know what you're doing
""" """
try: try:
loop = thread_local.loop loop = thread_local.loop
except AttributeError: except AttributeError:
thread_local.loop = AsyncScheduler() thread_local.loop = AsyncScheduler()
else: else:
if not loop.done(): if not loop.done():
raise GiambioError("cannot set event loop while running") raise GiambioError("cannot set event loop while running")
else: else:
thread_local.loop = AsyncScheduler() thread_local.loop = AsyncScheduler()
def run(func: FunctionType, *args): def run(func: FunctionType, *args):
""" """
Starts the event loop from a synchronous entry point Starts the event loop from a synchronous entry point
""" """
if isinstance(func, (CoroutineType, GeneratorType)): if isinstance(func, (CoroutineType, GeneratorType)):
raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!" 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, ...)") "\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
new_event_loop() new_event_loop()
thread_local.loop.start(func, *args) thread_local.loop.start(func, *args)
def clock(): def clock():
""" """
Returns the current clock time of the thread-local event Returns the current clock time of the thread-local event
loop loop
""" """
return thread_local.loop.clock() return thread_local.loop.clock()
def wrap_socket(sock: socket.socket) -> AsyncSocket: def wrap_socket(sock: socket.socket) -> AsyncSocket:
""" """
Wraps a synchronous socket into a giambio.socket.AsyncSocket Wraps a synchronous socket into a giambio.socket.AsyncSocket
""" """
return thread_local.loop.wrap_socket(sock) return thread_local.loop.wrap_socket(sock)
def create_pool(): def create_pool():
""" """
Creates an async pool Creates an async pool
""" """
try: try:
return TaskManager(thread_local.loop) return TaskManager(thread_local.loop)
except AttributeError: except AttributeError:
raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()" raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()"
" outside of an async context?") from None " outside of an async context?") from None

View File

@ -1,100 +1,100 @@
""" """
Basic abstraction layer for giambio asynchronous sockets Basic abstraction layer for giambio asynchronous sockets
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import socket import socket
from .exceptions import ResourceClosed from .exceptions import ResourceClosed
from .traps import sleep from .traps import sleep
# Stolen from curio # Stolen from curio
try: try:
from ssl import SSLWantReadError, SSLWantWriteError from ssl import SSLWantReadError, SSLWantWriteError
WantRead = (BlockingIOError, InterruptedError, SSLWantReadError) WantRead = (BlockingIOError, InterruptedError, SSLWantReadError)
WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError) WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError)
except ImportError: except ImportError:
WantRead = (BlockingIOError, InterruptedError) WantRead = (BlockingIOError, InterruptedError)
WantWrite = (BlockingIOError, InterruptedError) WantWrite = (BlockingIOError, InterruptedError)
class AsyncSocket(object): class AsyncSocket(object):
""" """
Abstraction layer for asynchronous TCP sockets Abstraction layer for asynchronous TCP sockets
""" """
def __init__(self, sock: socket.socket, loop): def __init__(self, sock: socket.socket, loop):
self.sock = sock self.sock = sock
self.loop = loop self.loop = loop
self._closed = False self._closed = False
self.sock.setblocking(False) self.sock.setblocking(False)
async def receive(self, max_size: int): async def receive(self, max_size: int):
""" """
Receives up to max_size bytes from a socket asynchronously Receives up to max_size bytes from a socket asynchronously
""" """
if self._closed: if self._closed:
raise ResourceClosed("I/O operation on closed socket") raise ResourceClosed("I/O operation on closed socket")
return await self.loop.read_sock(self.sock, max_size) return await self.loop.read_sock(self.sock, max_size)
async def accept(self): async def accept(self):
""" """
Accepts the socket, completing the 3-step TCP handshake asynchronously Accepts the socket, completing the 3-step TCP handshake asynchronously
""" """
if self._closed: if self._closed:
raise ResourceClosed("I/O operation on closed socket") raise ResourceClosed("I/O operation on closed socket")
to_wrap = await self.loop.accept_sock(self.sock) to_wrap = await self.loop.accept_sock(self.sock)
return self.loop.wrap_socket(to_wrap[0]), to_wrap[1] return self.loop.wrap_socket(to_wrap[0]), to_wrap[1]
async def send_all(self, data: bytes): async def send_all(self, data: bytes):
""" """
Sends all data inside the buffer asynchronously until it is empty Sends all data inside the buffer asynchronously until it is empty
""" """
if self._closed: if self._closed:
raise ResourceClosed("I/O operation on closed socket") raise ResourceClosed("I/O operation on closed socket")
return await self.loop.sock_sendall(self.sock, data) return await self.loop.sock_sendall(self.sock, data)
async def close(self): async def close(self):
""" """
Closes the socket asynchronously Closes the socket asynchronously
""" """
if self._closed: if self._closed:
raise ResourceClosed("I/O operation on closed socket") raise ResourceClosed("I/O operation on closed socket")
await self.loop.close_sock(self.sock) await self.loop.close_sock(self.sock)
self._closed = True self._closed = True
async def connect(self, addr: tuple): async def connect(self, addr: tuple):
""" """
Connects the socket to an endpoint Connects the socket to an endpoint
""" """
if self._closed: if self._closed:
raise ResourceClosed("I/O operation on closed socket") raise ResourceClosed("I/O operation on closed socket")
await self.loop.connect_sock(self.sock, addr) await self.loop.connect_sock(self.sock, addr)
async def __aenter__(self): async def __aenter__(self):
return self return self
async def __aexit__(self, *_): async def __aexit__(self, *_):
await self.close() await self.close()
def __repr__(self): def __repr__(self):
return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})" return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})"

View File

@ -1,134 +1,134 @@
""" """
Implementation for all giambio traps, which are hooks Implementation for all giambio traps, which are hooks
into the event loop and allow it to switch tasks. into the event loop and allow it to switch tasks.
These coroutines are the one and only way to interact These coroutines are the one and only way to interact
with the event loop from the user's perspective, and with the event loop from the user's perspective, and
the entire library is based on them the entire library is based on them
Copyright (C) 2020 nocturn9x Copyright (C) 2020 nocturn9x
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import types import types
@types.coroutine @types.coroutine
def create_trap(method, *args): def create_trap(method, *args):
""" """
Creates and yields a trap. This Creates and yields a trap. This
is the lowest-level method to is the lowest-level method to
interact with the event loop interact with the event loop
""" """
data = yield method, *args data = yield method, *args
return data return data
async def sleep(seconds: int): async def sleep(seconds: int):
""" """
Pause the execution of an async function for a given amount of seconds, 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 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 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 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 doesn't have enough calls to async functions (or 'checkpoints') this might
prevent the scheduler from switching tasks properly. If you feel like this 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. happens in your code, try adding a call to giambio.sleep(0) somewhere.
This will act as a checkpoint without actually pausing the execution This will act as a checkpoint without actually pausing the execution
of your function, but it will allow the scheduler to switch tasks of your function, but it will allow the scheduler to switch tasks
:param seconds: The amount of seconds to sleep for :param seconds: The amount of seconds to sleep for
:type seconds: int :type seconds: int
""" """
assert seconds >= 0, "The time delay can't be negative" assert seconds >= 0, "The time delay can't be negative"
await create_trap("sleep", seconds) await create_trap("sleep", seconds)
async def current_task(): async def current_task():
""" """
Gets the currently running task Gets the currently running task
""" """
return await create_trap("get_running") return await create_trap("get_running")
async def join(task): async def join(task):
""" """
Awaits a given task for completion Awaits a given task for completion
:param task: The task to join :param task: The task to join
:type task: class: Task :type task: class: Task
""" """
return await create_trap("join") return await create_trap("join")
async def cancel(task): async def cancel(task):
""" """
Cancels the given task Cancels the given task
The concept of cancellation is tricky, because there is no real way to 'stop' 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 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 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 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 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 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! code, so if you really wanna do that be sure to re-raise it when done!
""" """
await create_trap("cancel") await create_trap("cancel")
assert task.cancelled, f"Coroutine ignored CancelledError" assert task.cancelled, f"Coroutine ignored CancelledError"
async def want_read(stream): async def want_read(stream):
""" """
Notifies the event loop that a task wants to read from the given Notifies the event loop that a task wants to read from the given
resource resource
:param stream: The resource that needs to be read :param stream: The resource that needs to be read
""" """
await create_trap("want_read", stream) await create_trap("want_read", stream)
async def want_write(stream): async def want_write(stream):
""" """
Notifies the event loop that a task wants to write on the given Notifies the event loop that a task wants to write on the given
resource resource
:param stream: The resource that needs to be written :param stream: The resource that needs to be written
""" """
await create_trap("want_write", stream) await create_trap("want_write", stream)
async def event_set(event): async def event_set(event):
""" """
Communicates to the loop that the given event object Communicates to the loop that the given event object
must be set. This is important as the loop constantly must be set. This is important as the loop constantly
checks for active events to deliver them checks for active events to deliver them
""" """
await create_trap("event_set", event) await create_trap("event_set", event)
async def event_wait(event): async def event_wait(event):
""" """
Notifies the event loop that the current task has to wait Notifies the event loop that the current task has to wait
for the given event to trigger for the given event to trigger
""" """
await create_trap("event_wait", event) await create_trap("event_wait", event)

8
pyvenv.cfg Normal file
View File

@ -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

View File

@ -1,22 +1,22 @@
import setuptools import setuptools
with open("README.md", "r") as readme: with open("README.md", "r") as readme:
long_description = readme.read() long_description = readme.read()
setuptools.setup( setuptools.setup(
name="GiambIO", name="GiambIO",
version="1.0", version="1.0",
author="Nocturn9x aka IsGiambyy", author="Nocturn9x aka IsGiambyy",
author_email="hackhab@gmail.com", author_email="hackhab@gmail.com",
description="Asynchronous Python made easy (and friendly)", description="Asynchronous Python made easy (and friendly)",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://github.com/nocturn9x/giambio", url="https://github.com/nocturn9x/giambio",
packages=setuptools.find_packages(), packages=setuptools.find_packages(),
classifiers=[ classifiers=[
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"License :: OSI Approved :: Apache License 2.0", "License :: OSI Approved :: Apache License 2.0",
], ],
python_requires=">=3.6", python_requires=">=3.6",
) )

View File

@ -1,42 +1,42 @@
import giambio import giambio
# A test for context managers # A test for context managers
async def countdown(n: int): async def countdown(n: int):
print(f"Counting down from {n}!") print(f"Counting down from {n}!")
while n > 0: while n > 0:
print(f"Down {n}") print(f"Down {n}")
n -= 1 n -= 1
await giambio.sleep(1) await giambio.sleep(1)
# raise Exception("oh no man") # Uncomment to test propagation # raise Exception("oh no man") # Uncomment to test propagation
print("Countdown over") print("Countdown over")
return 0 return 0
async def countup(stop: int, step: int = 1): async def countup(stop: int, step: int = 1):
print(f"Counting up to {stop}!") print(f"Counting up to {stop}!")
x = 0 x = 0
while x < stop: while x < stop:
print(f"Up {x}") print(f"Up {x}")
x += 1 x += 1
await giambio.sleep(step) await giambio.sleep(step)
print("Countup over") print("Countup over")
return 1 return 1
async def main(): async def main():
start = giambio.clock() start = giambio.clock()
try: try:
async with giambio.create_pool() as pool: async with giambio.create_pool() as pool:
pool.spawn(countdown, 10) pool.spawn(countdown, 10)
pool.spawn(countup, 5, 2) pool.spawn(countup, 5, 2)
print("Children spawned, awaiting completion") print("Children spawned, awaiting completion")
except Exception as e: except Exception as e:
print(f"Got -> {type(e).__name__}: {e}") print(f"Got -> {type(e).__name__}: {e}")
print(f"Task execution complete in {giambio.clock() - start:.2f} seconds") print(f"Task execution complete in {giambio.clock() - start:.2f} seconds")
if __name__ == "__main__": if __name__ == "__main__":
giambio.run(main) giambio.run(main)

View File

@ -1,35 +1,35 @@
import giambio import giambio
# A test for events # A test for events
async def child(ev: giambio.Event, pause: int): async def child(ev: giambio.Event, pause: int):
print("[child] Child is alive! Going to wait until notified") print("[child] Child is alive! Going to wait until notified")
start_total = giambio.clock() start_total = giambio.clock()
await ev.wait() await ev.wait()
end_pause = giambio.clock() - start_total end_pause = giambio.clock() - start_total
print(f"[child] Parent set the event, exiting in {pause} seconds") print(f"[child] Parent set the event, exiting in {pause} seconds")
start_sleep = giambio.clock() start_sleep = giambio.clock()
await giambio.sleep(pause) await giambio.sleep(pause)
end_sleep = giambio.clock() - start_sleep end_sleep = giambio.clock() - start_sleep
end_total = giambio.clock() - start_total end_total = giambio.clock() - start_total
print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!") 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 def parent(pause: int = 1):
async with giambio.create_pool() as pool: async with giambio.create_pool() as pool:
event = giambio.Event() event = giambio.Event()
print("[parent] Spawning child task") print("[parent] Spawning child task")
pool.spawn(child, event, pause + 2) pool.spawn(child, event, pause + 2)
start = giambio.clock() start = giambio.clock()
print(f"[parent] Sleeping {pause} second(s) before setting the event") print(f"[parent] Sleeping {pause} second(s) before setting the event")
await giambio.sleep(pause) await giambio.sleep(pause)
await event.trigger() await event.trigger()
print("[parent] Event set, awaiting child") print("[parent] Event set, awaiting child")
end = giambio.clock() - start end = giambio.clock() - start
print(f"[parent] Child exited in {end} seconds") print(f"[parent] Child exited in {end} seconds")
if __name__ == "__main__": if __name__ == "__main__":
giambio.run(parent, 3) giambio.run(parent, 3)

View File

@ -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

View File

@ -1,54 +1,55 @@
import giambio import giambio
from giambio.socket import AsyncSocket from giambio.socket import AsyncSocket
import socket import socket
import logging import logging
import sys import sys
import traceback import traceback
# A test to check for asynchronous I/O # A test to check for asynchronous I/O
async def serve(address: tuple): async def serve(address: tuple):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(address) sock.bind(address)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.listen(5) sock.listen(5)
asock = giambio.wrap_socket(sock) # We make the socket an async socket asock = giambio.wrap_socket(sock) # We make the socket an async socket
logging.info(f"Serving asynchronously at {address[0]}:{address[1]}") logging.info(f"Serving asynchronously at {address[0]}:{address[1]}")
async with giambio.create_pool() as pool: async with giambio.create_pool() as pool:
conn, addr = await asock.accept() while True:
logging.info(f"{addr[0]}:{addr[1]} connected") conn, addr = await asock.accept()
pool.spawn(handler, conn, addr) 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 def handler(sock: AsyncSocket, addr: tuple):
async with sock: addr = f"{addr[0]}:{addr[1]}"
await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n") async with sock:
while True: await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n")
await sock.send_all(b"-> ") while True:
data = await sock.receive(1024) await sock.send_all(b"-> ")
if not data: data = await sock.receive(1024)
break if not data:
elif data == b"raise\n": break
await sock.send_all(b"I'm dead dude\n") elif data == b"raise\n":
raise TypeError("Oh, no, I'm gonna die!") await sock.send_all(b"I'm dead dude\n")
to_send_back = data raise TypeError("Oh, no, I'm gonna die!")
data = data.decode("utf-8").encode("unicode_escape") to_send_back = data
logging.info(f"Got: '{data.decode('utf-8')}' from {addr}") data = data.decode("utf-8").encode("unicode_escape")
await sock.send_all(b"Got: " + to_send_back) logging.info(f"Got: '{data.decode('utf-8')}' from {addr}")
logging.info(f"Echoed back '{data.decode('utf-8')}' to {addr}") await sock.send_all(b"Got: " + to_send_back)
logging.info(f"Connection from {addr} closed") 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 if __name__ == "__main__":
logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p") port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500
try: logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p")
giambio.run(serve, ("localhost", port)) try:
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate! giambio.run(serve, ("localhost", port))
if isinstance(error, KeyboardInterrupt): except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
logging.info("Ctrl+C detected, exiting") if isinstance(error, KeyboardInterrupt):
else: logging.info("Ctrl+C detected, exiting")
logging.error(f"Exiting due to a {type(error).__name__}: {error}") else:
logging.error(f"Exiting due to a {type(error).__name__}: {error}")