noio 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- noio-0.1/.github/workflows/build.yml +63 -0
- noio-0.1/.github/workflows/publish.yml +29 -0
- noio-0.1/.gitignore +21 -0
- noio-0.1/AGENTS.md +1 -0
- noio-0.1/CHANGELOG.md +20 -0
- noio-0.1/CONTRIBUTING.md +44 -0
- noio-0.1/LICENSE +21 -0
- noio-0.1/PKG-INFO +77 -0
- noio-0.1/README.md +67 -0
- noio-0.1/noio/__init__.py +3 -0
- noio-0.1/noio/_lock.py +49 -0
- noio-0.1/noio/_lock_test.py +358 -0
- noio-0.1/noio/conftest.py +160 -0
- noio-0.1/noio/gevent_test.py +35 -0
- noio-0.1/noio/py.typed +0 -0
- noio-0.1/noio/routine.md +35 -0
- noio-0.1/noio/routine.py +108 -0
- noio-0.1/noio/routine_test.py +571 -0
- noio-0.1/pyproject.toml +47 -0
- noio-0.1/pyrightconfig.json +5 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_call:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
timeout-minutes: 5
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python: ["3.15", "3.15t"]
|
|
18
|
+
env:
|
|
19
|
+
UV_PYTHON: ${{ matrix.python }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7.0.1
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v10.3.0
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install "${{ matrix.python }}"
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --all-groups
|
|
28
|
+
- name: Check formatting
|
|
29
|
+
run: uv run ruff format --check
|
|
30
|
+
- name: Run ruff linting
|
|
31
|
+
run: uv run ruff check
|
|
32
|
+
- name: Run basedpyright
|
|
33
|
+
run: uv run basedpyright
|
|
34
|
+
- name: Verify free threading
|
|
35
|
+
if: matrix.python == '3.15t'
|
|
36
|
+
run: uv run python -c "import noio, sys; assert not sys._is_gil_enabled()"
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: uv run pytest --cov --cov-report=term-missing
|
|
39
|
+
timeout-minutes: 2
|
|
40
|
+
- name: Verify built package
|
|
41
|
+
run: |
|
|
42
|
+
uv build
|
|
43
|
+
uv run --isolated --no-project --with dist/noio-*.whl python -I -c "import noio"
|
|
44
|
+
- name: Verify release version
|
|
45
|
+
if: github.event_name == 'release'
|
|
46
|
+
env:
|
|
47
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
48
|
+
run: |
|
|
49
|
+
uv run python - <<'PY'
|
|
50
|
+
import os
|
|
51
|
+
import tomllib
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
55
|
+
assert os.environ["RELEASE_TAG"] == version, "Release tag must match package version"
|
|
56
|
+
PY
|
|
57
|
+
- name: Upload distributions
|
|
58
|
+
if: github.event_name == 'release' && matrix.python == '3.15'
|
|
59
|
+
uses: actions/upload-artifact@v7.0.2
|
|
60
|
+
with:
|
|
61
|
+
name: distributions
|
|
62
|
+
path: dist/
|
|
63
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
uses: ./.github/workflows/build.yml
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
needs: build
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 5
|
|
18
|
+
environment:
|
|
19
|
+
name: pypi
|
|
20
|
+
url: https://pypi.org/project/noio/
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/download-artifact@v8.0.2
|
|
25
|
+
with:
|
|
26
|
+
name: distributions
|
|
27
|
+
path: dist/
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@v1.14.2
|
noio-0.1/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.coverage*
|
|
9
|
+
htmlcov/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv
|
|
13
|
+
|
|
14
|
+
# uv
|
|
15
|
+
uv.lock
|
|
16
|
+
.python-version
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.ropeproject/
|
|
19
|
+
|
|
20
|
+
# MkDocs
|
|
21
|
+
site/
|
noio-0.1/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
README.md
|
noio-0.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
All notable changes to this project will be documented in this file.
|
|
5
|
+
|
|
6
|
+
The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
|
|
7
|
+
|
|
8
|
+
[0.1] - 2026-10-17
|
|
9
|
+
-----------------
|
|
10
|
+
|
|
11
|
+
Initial experimental release.
|
|
12
|
+
Requires Python 3.15 or later.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `@noio.routine` to support ordinary calls and awaited `.a()` calls
|
|
17
|
+
from a single coroutine implementation.
|
|
18
|
+
- A guard against ordinary routine calls inside routine implementations.
|
|
19
|
+
|
|
20
|
+
[0.1]: https://github.com/ryanhiebert/noio/releases/tag/0.1
|
noio-0.1/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Contributing
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
Choose APIs that best serve noio's goals.
|
|
5
|
+
Compatibility with existing libraries is valuable,
|
|
6
|
+
but we are free to make different choices when they improve the API.
|
|
7
|
+
Trio has a history of thoughtful API design
|
|
8
|
+
and is a useful reference when evaluating those choices.
|
|
9
|
+
Consider the reasoning behind its decisions,
|
|
10
|
+
without treating them as requirements for noio.
|
|
11
|
+
|
|
12
|
+
Do not attempt to retain backward compatibility between versions,
|
|
13
|
+
even for features that have already been released.
|
|
14
|
+
Document incompatible changes under Breaking Changes in the changelog.
|
|
15
|
+
|
|
16
|
+
Small releases are welcome as long as the included features are ready for people to use,
|
|
17
|
+
and we have reasonable confidence that any breaking changes are documented in the changelog.
|
|
18
|
+
|
|
19
|
+
Before adding a dependency, check that its license is compatible with
|
|
20
|
+
distributing noio under the MIT license.
|
|
21
|
+
Check any attribution or license notice requirements as well.
|
|
22
|
+
|
|
23
|
+
Design primitives so the same instance can be shared across supported runtimes,
|
|
24
|
+
including runtimes running concurrently in different threads.
|
|
25
|
+
Where applicable, test interaction between runtimes,
|
|
26
|
+
not just each runtime independently.
|
|
27
|
+
Treat compatibility across supported runtimes as the default.
|
|
28
|
+
Individual primitive docs only need to describe compatibility limitations.
|
|
29
|
+
|
|
30
|
+
Do not rely on any kind of monkeypatching being in place.
|
|
31
|
+
When monkeypatching is predictable, such as with gevent,
|
|
32
|
+
make sure the implementation will work whether it is patched or not.
|
|
33
|
+
|
|
34
|
+
Every bug fix must include a regression test
|
|
35
|
+
that fails without the fix and passes with it.
|
|
36
|
+
|
|
37
|
+
Record user-facing changes in `CHANGELOG.md` under an Unreleased section,
|
|
38
|
+
creating that section when needed.
|
|
39
|
+
At release time, turn it into a dated version entry.
|
|
40
|
+
|
|
41
|
+
Use semantic line breaks in Markdown prose.
|
|
42
|
+
Start each sentence on a new line,
|
|
43
|
+
and break longer sentences at natural clause or phrase boundaries
|
|
44
|
+
rather than wrapping to a fixed width.
|
noio-0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Hiebert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
noio-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: noio
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Experimental coroutine libraries for ordinary-function applications
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.15
|
|
8
|
+
Requires-Dist: aiologic>=0.17.1
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
noio
|
|
12
|
+
====
|
|
13
|
+
|
|
14
|
+
There are many opinions on the right way to do IO.
|
|
15
|
+
There are so many libraries.
|
|
16
|
+
Some use regular functions, while some use coroutines.
|
|
17
|
+
Some use threads.
|
|
18
|
+
Some use green threads.
|
|
19
|
+
Some existing abstractions only work with coroutines on single threads.
|
|
20
|
+
You just want people to be able to use your library.
|
|
21
|
+
|
|
22
|
+
`noio` is a minimal IO interface
|
|
23
|
+
that provides primitives that work no matter what IO the user chooses,
|
|
24
|
+
with clear documentation of any compatibility limitations.
|
|
25
|
+
|
|
26
|
+
Getting Started
|
|
27
|
+
---------------
|
|
28
|
+
|
|
29
|
+
`noio` believes that coroutines are an advanced feature
|
|
30
|
+
that should be avoided wherever reasonable.
|
|
31
|
+
That includes getting started writing a library with `noio`.
|
|
32
|
+
When writing a new library, start with regular functions,
|
|
33
|
+
which are able to support threads and green threads.
|
|
34
|
+
It might look something like this:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
def greeting(name: str) -> str:
|
|
38
|
+
return f"Hello, {name}!"
|
|
39
|
+
|
|
40
|
+
assert greeting("world") == "Hello, world!"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When it's important for your function to also support coroutine libraries
|
|
44
|
+
like `asyncio`, `trio` and `anyio`,
|
|
45
|
+
just update it to look like something like this:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
import noio
|
|
50
|
+
|
|
51
|
+
@noio.routine
|
|
52
|
+
async def greeting(name: str) -> str:
|
|
53
|
+
return f"Hello, {name}!"
|
|
54
|
+
|
|
55
|
+
@noio.routine
|
|
56
|
+
async def introduction(name: str) -> str:
|
|
57
|
+
return await greeting.a(name)
|
|
58
|
+
|
|
59
|
+
assert greeting("world") == "Hello, world!"
|
|
60
|
+
assert introduction("world") == "Hello, world!"
|
|
61
|
+
assert asyncio.run(introduction.a("world")) == "Hello, world!"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When existing callers call your function the same way they used to,
|
|
65
|
+
it will work without modification
|
|
66
|
+
because it will drive the coroutine implementation to completion directly.
|
|
67
|
+
Other routines will await a new `.a()` version
|
|
68
|
+
that allows the run loop to handle scheduling.
|
|
69
|
+
|
|
70
|
+
Finding all the places to convert can be tricky,
|
|
71
|
+
so `noio` will raise an error
|
|
72
|
+
if a routine implementation calls another routine with a regular function call.
|
|
73
|
+
|
|
74
|
+
Contributing
|
|
75
|
+
------------
|
|
76
|
+
|
|
77
|
+
Before making changes, read [CONTRIBUTING.md](CONTRIBUTING.md).
|
noio-0.1/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
noio
|
|
2
|
+
====
|
|
3
|
+
|
|
4
|
+
There are many opinions on the right way to do IO.
|
|
5
|
+
There are so many libraries.
|
|
6
|
+
Some use regular functions, while some use coroutines.
|
|
7
|
+
Some use threads.
|
|
8
|
+
Some use green threads.
|
|
9
|
+
Some existing abstractions only work with coroutines on single threads.
|
|
10
|
+
You just want people to be able to use your library.
|
|
11
|
+
|
|
12
|
+
`noio` is a minimal IO interface
|
|
13
|
+
that provides primitives that work no matter what IO the user chooses,
|
|
14
|
+
with clear documentation of any compatibility limitations.
|
|
15
|
+
|
|
16
|
+
Getting Started
|
|
17
|
+
---------------
|
|
18
|
+
|
|
19
|
+
`noio` believes that coroutines are an advanced feature
|
|
20
|
+
that should be avoided wherever reasonable.
|
|
21
|
+
That includes getting started writing a library with `noio`.
|
|
22
|
+
When writing a new library, start with regular functions,
|
|
23
|
+
which are able to support threads and green threads.
|
|
24
|
+
It might look something like this:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
def greeting(name: str) -> str:
|
|
28
|
+
return f"Hello, {name}!"
|
|
29
|
+
|
|
30
|
+
assert greeting("world") == "Hello, world!"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When it's important for your function to also support coroutine libraries
|
|
34
|
+
like `asyncio`, `trio` and `anyio`,
|
|
35
|
+
just update it to look like something like this:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
import noio
|
|
40
|
+
|
|
41
|
+
@noio.routine
|
|
42
|
+
async def greeting(name: str) -> str:
|
|
43
|
+
return f"Hello, {name}!"
|
|
44
|
+
|
|
45
|
+
@noio.routine
|
|
46
|
+
async def introduction(name: str) -> str:
|
|
47
|
+
return await greeting.a(name)
|
|
48
|
+
|
|
49
|
+
assert greeting("world") == "Hello, world!"
|
|
50
|
+
assert introduction("world") == "Hello, world!"
|
|
51
|
+
assert asyncio.run(introduction.a("world")) == "Hello, world!"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When existing callers call your function the same way they used to,
|
|
55
|
+
it will work without modification
|
|
56
|
+
because it will drive the coroutine implementation to completion directly.
|
|
57
|
+
Other routines will await a new `.a()` version
|
|
58
|
+
that allows the run loop to handle scheduling.
|
|
59
|
+
|
|
60
|
+
Finding all the places to convert can be tricky,
|
|
61
|
+
so `noio` will raise an error
|
|
62
|
+
if a routine implementation calls another routine with a regular function call.
|
|
63
|
+
|
|
64
|
+
Contributing
|
|
65
|
+
------------
|
|
66
|
+
|
|
67
|
+
Before making changes, read [CONTRIBUTING.md](CONTRIBUTING.md).
|
noio-0.1/noio/_lock.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
import aiologic
|
|
5
|
+
from aiologic.lowlevel import current_async_library
|
|
6
|
+
|
|
7
|
+
from .routine import _sync_call
|
|
8
|
+
from .routine import routine
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Lock:
|
|
12
|
+
"""An owned, non-reentrant lock shared by threads, greenlets, and async tasks."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.__lock = aiologic.Lock()
|
|
16
|
+
|
|
17
|
+
@routine
|
|
18
|
+
async def __enter__(self) -> Self:
|
|
19
|
+
"""Wait for ownership; reacquiring raises RuntimeError."""
|
|
20
|
+
if _sync_call.get():
|
|
21
|
+
self.__lock.green_acquire()
|
|
22
|
+
else:
|
|
23
|
+
await self.__lock.async_acquire()
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
@routine
|
|
27
|
+
async def __exit__(
|
|
28
|
+
self,
|
|
29
|
+
exc_type: type[BaseException] | None,
|
|
30
|
+
exc_value: BaseException | None,
|
|
31
|
+
traceback: TracebackType | None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Release from the owning thread, greenlet, or async task."""
|
|
34
|
+
# An ordinary acquisition inside an async function still has green ownership.
|
|
35
|
+
if self.__lock.green_owned() or current_async_library(failsafe=True) is None:
|
|
36
|
+
self.__lock.green_release()
|
|
37
|
+
else:
|
|
38
|
+
self.__lock.async_release()
|
|
39
|
+
|
|
40
|
+
async def __aenter__(self) -> Self:
|
|
41
|
+
return await self.__enter__.a()
|
|
42
|
+
|
|
43
|
+
async def __aexit__(
|
|
44
|
+
self,
|
|
45
|
+
exc_type: type[BaseException] | None,
|
|
46
|
+
exc_value: BaseException | None,
|
|
47
|
+
traceback: TracebackType | None,
|
|
48
|
+
) -> None:
|
|
49
|
+
await self.__exit__.a(exc_type, exc_value, traceback)
|