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

374
LICENSE
View File

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

760
README.md
View File

@ -1,380 +1,380 @@
# giambio - Asynchronous Python made easy (and friendly)
giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing.
This library implements what is known as a _stackless mode of execution_, or
"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default).
_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being
production ready, so be aware that it is likely that you'll find bugs and race conditions
## Disclaimer
Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works
and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them!
Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the
[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a
rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact
that most of the following text is ~~stolen~~ inspired from its documentation)
# What the hell is async anyway?
Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing).
The most common example of this is a network server that needs to handle multiple connections at the same time.
One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they
actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial.
If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization
primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide.
A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)),
and network servers happen to be heavily based on I/O: a blocking operation.
Starting to see where we're heading?
## A deeper dive
Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is
implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details).
For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5:
asynchronous functions, or 'async' for short.
Async functions are functions defined with `async def` instead of the regular `def`, like so:
```python
async def async_fun(): # An async function
print("Hello, world!")
def sync_fun(): # A regular (sync) function
print("Hello, world!")
```
First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below:
```python
async def async_two():
print("Hello from async_two!")
async def async_one():
print("Hello from async_one!")
await async_two() # This is an async call
```
It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async
functions have a unique superpower: they, and no-one else, can call other async functions.
This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol'
synchronous code; So how do we enter the async context in the first place?
That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call
asynchronous functions and can therefore initiate the async context. For this
reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_.
Now that you know all of this, you might be wondering why on earth would one use async functions instead of
regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it?
Take a look at this example below:
```python
import giambio
async def foo():
print("Hello, world!")
giambio.run(foo) # Prints 'Hello, world!'
```
This could as well be written the following way and would produce the same output:
```python
def foo():
print("Hello, world!")
foo() # Prints 'Hello, world!'
```
To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness.
We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio
provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful!
So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code.
As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor):
__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines
so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async
model is much simpler if we take coroutines out of the game, and everything works just the same.
```python
import giambio
async def sleep_double(n):
await giambio.sleep(2 * n)
giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns
```
As it turns out, this function is one that's actually worth making async: because it calls another async function.
Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to
make it async in the first place.
### Don't forget the `await`!
As we already learned, async functions can only be called with the `await` keyword, and it would be logical to
think that forgetting to do so would raise an error, but it's actually a little bit trickier than that.
Take this example here:
```python
import giambio
async def sleep_double_broken(n):
print("Taking a nap!")
start = giambio.clock()
giambio.sleep(2 * n) # We forgot the await!
end = giambio.clock() - start
print(f"Slept for {end:.2f} seconds!")
giambio.run(sleep_double_broken, 2)
```
Running this code, will produce an output that looks like this:
```
Taking a nap!
Slept 0.00 seconds!
__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited
```
Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong:
the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining
that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a
garbage collection cycle occurred or not).
I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that
if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async
function, so try fixing that before trying to figure out what could be the problem if you have a long traceback:
most likely that's just collateral damage caused by the missing keyword.
If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to
the next section, but for the curios among y'all I might as well explain exactly what happened there.
When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine'
object
```python
>>> giambio.sleep(1)
<coroutine object sleep at 0x1069520d0>
```
The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering
`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things:
- It creates this weird coroutine object
- Passes that object to `await`, which runs the function
So basically that's why you always need to put `await` in front of an async function when calling it.
## Something actually useful
Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special
runner function that can start the whole async context, but we didn't really do anything _useful_.
Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't
quite useful is it?
But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__.
Yep, you read that right.
To demonstrate this, have a look a this example
```python
import giambio
async def countdown(n: int):
print(f"Counting down from {n}!")
while n > 0:
print(f"Down {n}")
n -= 1
await giambio.sleep(1)
print("Countdown over")
return 0
async def countup(stop: int):
print(f"Counting up to {stop}!")
x = 0
while x < stop:
print(f"Up {x}")
x += 1
await giambio.sleep(2)
print("Countup over")
return 1
async def main():
start = giambio.clock()
async with giambio.create_pool() as pool:
pool.spawn(countdown, 10)
pool.spawn(countup, 5)
print("Children spawned, awaiting completion")
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
if __name__ == "__main__":
giambio.run(main)
```
There is a lot going on here, and we'll explain every bit of it step by step:
- First, we imported giambio and defined two async functions: `countup` and `countdown`
- These two functions do exactly what their name suggests, but for the purposes of
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
to `await giambio.sleep(2)`?)
- Here comes the real fun: `async with`? What's going on there?
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
of related new syntax. One of the things that was added is asynchronous context managers.
You might have already encountered context managers in python, but in case you didn't,
a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()`
at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as`
keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So
context managers are a shorthand for calling functions, and since Python 3.5 added
async functions, we also needed async context managers. While `with foo as sth` calls
`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh?
__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is
the same though: while `for item in container` calls `container.__next__()` to fetch the
next item, `async for item in container` calls `await container.__anext__()` to do so.
It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good.
- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio,
there are actually 2 ways to call async functions: one we've already seen (`await fn()`),
while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is
that it will return immediately, without waiting for the async function to finish. So,
now our functions are running in the background.
After we spawn our tasks, we hit the call to `print` and the end of the block, so Python
calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main`
async function in this case) until all children task have exited, and as it turns out, that
is a good thing.
The reason why pools always wait for all children to have finished executing is that it makes
easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks,
exceptions in giambio always behave as expected
Ok, so, let's try running this snippet and see what we get:
```
Children spawned, awaiting completion
Counting down from 10!
Down 10
Counting up to 5!
Up 0
Down 9
Up 1
Down 8
Down 7
Up 2
Down 6
Down 5
Up 3
Down 4
Down 3
Up 4
Down 2
Down 1
Countup over
Countdown over
Task execution complete in 10.07 seconds
```
(Your output might have some lines swapped compared to this)
You see how `countup` and `countdown` both start and finish
together? Moreover, even though each function slept for about 10
seconds (therefore 20 seconds total), the program just took 10
seconds to complete, so our children are really running at the same time.
If you've ever done thread programming, this will feel like home, and that's good:
that's exactly what we want. But beware! No threads are involved here, giambio is
running in a single thread. That's why we talked about _tasks_ rather than _threads_
so far. The difference between the two is that you can run a lot of tasks in a single
thread, and that with threads Python can switch which thread is running at any time.
Giambio, on the other hand, can switch tasks only at certain fixed points called
_checkpoints_, more on that later.
### A sneak peak into the async world
The basic idea behind libraries like giambio is that they can run a lot of tasks
at the same time by switching back and forth between them at appropriate places.
An example for that could be a web server: while the server is waiting for a response
from a client, we can accept another connection. You don't necessarily need all these
pesky details to use giambio, but it's good to have at least an high-level understanding
of how this all works.
The peculiarity of asynchronous functions is that they can suspend their execution: that's
what `await` does, it yields back the execution control to giambio, which can then decide
what to do next.
To understand this better, take a look at this code:
```python
def countdown(n: int) -> int:
while n:
yield n
n -= 1
for x in countdown(5):
print(x)
```
In the above snippet, `countdown` is a generator function. Generators are really useful because
they allow to customize iteration. Running that code produces the following output:
```
5
4
3
2
1
```
The trick for this to work is `yield`.
What `yield` does is return back to the caller and suspend itself: In our case, `yield`
returns to the for loop, which calls `countdown` again. So, the generator resumes right
after the `yield`, decrements n, and loops right back to the top for the while loop to
execute again. It's that suspension part that allows the async magic to happen: the whole
`async`/`await` logic overlaps a lot with generator functions.
Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made
way before Python 3.5 added that nice new syntax.
So, since only async functions can suspend themselves, the only places where giambio will switch
tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure
that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier
to reason about, because you can know if a function will ever switch, and where will it do so, just
by looking at its source code. That is very different from what threads do: they can (and will) switch
whenever they feel like it.
Remember when we talked about checkpoints? That's what they are: calls to async functions that allow
giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code,
then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
and it will reschedule the caller almost immediately, because the sleep time is 0.
### Mix and match? No thanks
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
to perform a certain thing in a given task, and to communicate your intent to the loop, the
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
giambio to get very confused and most likely just explode spectacularly badly
TODO: I/O
## Contributing
This is a relatively young project and it is looking for collaborators! It's not rocket science,
but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions,
so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x)
or by [E-mail](mailto:hackhab@gmail.com)
# giambio - Asynchronous Python made easy (and friendly)
giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing.
This library implements what is known as a _stackless mode of execution_, or
"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default).
_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being
production ready, so be aware that it is likely that you'll find bugs and race conditions
## Disclaimer
Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works
and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them!
Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the
[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a
rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact
that most of the following text is ~~stolen~~ inspired from its documentation)
# What the hell is async anyway?
Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing).
The most common example of this is a network server that needs to handle multiple connections at the same time.
One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they
actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial.
If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization
primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide.
A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)),
and network servers happen to be heavily based on I/O: a blocking operation.
Starting to see where we're heading?
## A deeper dive
Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is
implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details).
For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5:
asynchronous functions, or 'async' for short.
Async functions are functions defined with `async def` instead of the regular `def`, like so:
```python
async def async_fun(): # An async function
print("Hello, world!")
def sync_fun(): # A regular (sync) function
print("Hello, world!")
```
First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below:
```python
async def async_two():
print("Hello from async_two!")
async def async_one():
print("Hello from async_one!")
await async_two() # This is an async call
```
It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async
functions have a unique superpower: they, and no-one else, can call other async functions.
This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol'
synchronous code; So how do we enter the async context in the first place?
That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call
asynchronous functions and can therefore initiate the async context. For this
reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_.
Now that you know all of this, you might be wondering why on earth would one use async functions instead of
regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it?
Take a look at this example below:
```python
import giambio
async def foo():
print("Hello, world!")
giambio.run(foo) # Prints 'Hello, world!'
```
This could as well be written the following way and would produce the same output:
```python
def foo():
print("Hello, world!")
foo() # Prints 'Hello, world!'
```
To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness.
We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio
provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful!
So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code.
As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor):
__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines
so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async
model is much simpler if we take coroutines out of the game, and everything works just the same.
```python
import giambio
async def sleep_double(n):
await giambio.sleep(2 * n)
giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns
```
As it turns out, this function is one that's actually worth making async: because it calls another async function.
Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to
make it async in the first place.
### Don't forget the `await`!
As we already learned, async functions can only be called with the `await` keyword, and it would be logical to
think that forgetting to do so would raise an error, but it's actually a little bit trickier than that.
Take this example here:
```python
import giambio
async def sleep_double_broken(n):
print("Taking a nap!")
start = giambio.clock()
giambio.sleep(2 * n) # We forgot the await!
end = giambio.clock() - start
print(f"Slept for {end:.2f} seconds!")
giambio.run(sleep_double_broken, 2)
```
Running this code, will produce an output that looks like this:
```
Taking a nap!
Slept 0.00 seconds!
__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited
```
Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong:
the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining
that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a
garbage collection cycle occurred or not).
I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that
if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async
function, so try fixing that before trying to figure out what could be the problem if you have a long traceback:
most likely that's just collateral damage caused by the missing keyword.
If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to
the next section, but for the curios among y'all I might as well explain exactly what happened there.
When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine'
object
```python
>>> giambio.sleep(1)
<coroutine object sleep at 0x1069520d0>
```
The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering
`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things:
- It creates this weird coroutine object
- Passes that object to `await`, which runs the function
So basically that's why you always need to put `await` in front of an async function when calling it.
## Something actually useful
Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special
runner function that can start the whole async context, but we didn't really do anything _useful_.
Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't
quite useful is it?
But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__.
Yep, you read that right.
To demonstrate this, have a look a this example
```python
import giambio
async def countdown(n: int):
print(f"Counting down from {n}!")
while n > 0:
print(f"Down {n}")
n -= 1
await giambio.sleep(1)
print("Countdown over")
return 0
async def countup(stop: int):
print(f"Counting up to {stop}!")
x = 0
while x < stop:
print(f"Up {x}")
x += 1
await giambio.sleep(2)
print("Countup over")
return 1
async def main():
start = giambio.clock()
async with giambio.create_pool() as pool:
pool.spawn(countdown, 10)
pool.spawn(countup, 5)
print("Children spawned, awaiting completion")
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
if __name__ == "__main__":
giambio.run(main)
```
There is a lot going on here, and we'll explain every bit of it step by step:
- First, we imported giambio and defined two async functions: `countup` and `countdown`
- These two functions do exactly what their name suggests, but for the purposes of
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
to `await giambio.sleep(2)`?)
- Here comes the real fun: `async with`? What's going on there?
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
of related new syntax. One of the things that was added is asynchronous context managers.
You might have already encountered context managers in python, but in case you didn't,
a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()`
at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as`
keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So
context managers are a shorthand for calling functions, and since Python 3.5 added
async functions, we also needed async context managers. While `with foo as sth` calls
`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh?
__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is
the same though: while `for item in container` calls `container.__next__()` to fetch the
next item, `async for item in container` calls `await container.__anext__()` to do so.
It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good.
- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio,
there are actually 2 ways to call async functions: one we've already seen (`await fn()`),
while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is
that it will return immediately, without waiting for the async function to finish. So,
now our functions are running in the background.
After we spawn our tasks, we hit the call to `print` and the end of the block, so Python
calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main`
async function in this case) until all children task have exited, and as it turns out, that
is a good thing.
The reason why pools always wait for all children to have finished executing is that it makes
easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks,
exceptions in giambio always behave as expected
Ok, so, let's try running this snippet and see what we get:
```
Children spawned, awaiting completion
Counting down from 10!
Down 10
Counting up to 5!
Up 0
Down 9
Up 1
Down 8
Down 7
Up 2
Down 6
Down 5
Up 3
Down 4
Down 3
Up 4
Down 2
Down 1
Countup over
Countdown over
Task execution complete in 10.07 seconds
```
(Your output might have some lines swapped compared to this)
You see how `countup` and `countdown` both start and finish
together? Moreover, even though each function slept for about 10
seconds (therefore 20 seconds total), the program just took 10
seconds to complete, so our children are really running at the same time.
If you've ever done thread programming, this will feel like home, and that's good:
that's exactly what we want. But beware! No threads are involved here, giambio is
running in a single thread. That's why we talked about _tasks_ rather than _threads_
so far. The difference between the two is that you can run a lot of tasks in a single
thread, and that with threads Python can switch which thread is running at any time.
Giambio, on the other hand, can switch tasks only at certain fixed points called
_checkpoints_, more on that later.
### A sneak peak into the async world
The basic idea behind libraries like giambio is that they can run a lot of tasks
at the same time by switching back and forth between them at appropriate places.
An example for that could be a web server: while the server is waiting for a response
from a client, we can accept another connection. You don't necessarily need all these
pesky details to use giambio, but it's good to have at least an high-level understanding
of how this all works.
The peculiarity of asynchronous functions is that they can suspend their execution: that's
what `await` does, it yields back the execution control to giambio, which can then decide
what to do next.
To understand this better, take a look at this code:
```python
def countdown(n: int) -> int:
while n:
yield n
n -= 1
for x in countdown(5):
print(x)
```
In the above snippet, `countdown` is a generator function. Generators are really useful because
they allow to customize iteration. Running that code produces the following output:
```
5
4
3
2
1
```
The trick for this to work is `yield`.
What `yield` does is return back to the caller and suspend itself: In our case, `yield`
returns to the for loop, which calls `countdown` again. So, the generator resumes right
after the `yield`, decrements n, and loops right back to the top for the while loop to
execute again. It's that suspension part that allows the async magic to happen: the whole
`async`/`await` logic overlaps a lot with generator functions.
Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made
way before Python 3.5 added that nice new syntax.
So, since only async functions can suspend themselves, the only places where giambio will switch
tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure
that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier
to reason about, because you can know if a function will ever switch, and where will it do so, just
by looking at its source code. That is very different from what threads do: they can (and will) switch
whenever they feel like it.
Remember when we talked about checkpoints? That's what they are: calls to async functions that allow
giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code,
then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
and it will reschedule the caller almost immediately, because the sleep time is 0.
### Mix and match? No thanks
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
to perform a certain thing in a given task, and to communicate your intent to the loop, the
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
giambio to get very confused and most likely just explode spectacularly badly
TODO: I/O
## Contributing
This is a relatively young project and it is looking for collaborators! It's not rocket science,
but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions,
so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x)
or by [E-mail](mailto:hackhab@gmail.com)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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