diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8022123..9773586 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,9 +28,9 @@ jobs: V: ${{ matrix.python-version }} run: "uv run --group tox tox -e py$(echo $V | tr -d . | sed 's/^py//')" - - name: "Lint" + - name: "Check code" if: matrix.python-version == '3.13' && runner.os == 'Linux' - run: "uv run --group tox tox -e lint" + run: "uv run --group tox tox -e check" - name: "Upload coverage data" uses: "actions/upload-artifact@v4" diff --git a/CHANGELOG.md b/CHANGELOG.md index b327ffb..97bd7e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 25.1.0 (UNRELEASED) +- Call the `asyncio.to_thread` int the `wrap` decorator. + [#213](https://github.com/Tinche/aiofiles/pull/213) - Switch to [uv](https://docs.astral.sh/uv/) + add Python v3.14 support. ([#219](https://github.com/Tinche/aiofiles/pull/219)) - Add `ruff` formatter and linter. diff --git a/Justfile b/Justfile index ccae1b8..d0aae5e 100644 --- a/Justfile +++ b/Justfile @@ -1,19 +1,37 @@ +src_dir := "src" tests_dir := "tests" -code_dirs := "src" + " " + tests_dir -run_prefix := if env_var_or_default("VIRTUAL_ENV", "") == "" { "uv run " } else { "" } +code_dirs := src_dir + " " + tests_dir +# https://just.systems/man/en/functions.html#environment-variables +run := if env("VIRTUAL_ENV", "") == "" { "uv run " } else { "" } + +# list available rules +default: + just --list + +# build the project +build: + uv build + +# install dependencies +sync: + uv sync --group lint --group test --group tox + +# check the code check: - {{ run_prefix }}ruff format --check {{ code_dirs }} - {{ run_prefix }}ruff check {{ code_dirs }} + {{ run }}ruff format --check {{ code_dirs }} + {{ run }}ruff check {{ code_dirs }} + {{ run }}mypy {{ src_dir }} # lint only the source code +# run coverage coverage: - {{ run_prefix }}coverage run -m pytest {{ tests_dir }} - -format: - {{ run_prefix }}ruff format {{ code_dirs }} + {{ run }}coverage run -m pytest {{ tests_dir }} -lint: format - {{ run_prefix }}ruff check --fix {{ code_dirs }} +# lint the code (including formatting) +lint *files=".": + {{ run }}ruff format {{ files }} + {{ run }}ruff check --fix {{ files }} -test: - {{ run_prefix }}pytest -x --ff {{ tests_dir }} \ No newline at end of file +# run the tests +test *args: + {{ run }}pytest {{ args }} diff --git a/README.md b/README.md index 7b402bc..5dfa631 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ and delegate to an executor: - `flush` - `isatty` - `read` -- `readall` - `read1` +- `readall` - `readinto` - `readline` - `readlines` @@ -91,36 +91,39 @@ In case of failure, one of the usual exceptions will be raised. The `aiofiles.os` module contains executor-enabled coroutine versions of several useful `os` functions that deal with files: -- `stat` -- `statvfs` +- `access` +- `getcwd` +- `link` +- `listdir` +- `makedirs` +- `mkdir` +- `path`: + - `path.abspath` + - `path.exists` + - `path.getatime` + - `path.getctime` + - `path.getmtime` + - `path.getsize` + - `path.isdir` + - `path.isfile` + - `path.islink` + - `path.ismount` + - `path.samefile` + - `path.sameopenfile` +- `readlink` +- `remove` +- `removedirs` - `sendfile` - `rename` - `renames` - `replace` -- `remove` -- `unlink` -- `mkdir` -- `makedirs` - `rmdir` -- `removedirs` -- `link` -- `symlink` -- `readlink` -- `listdir` - `scandir` -- `access` -- `getcwd` -- `path.abspath` -- `path.exists` -- `path.isfile` -- `path.isdir` -- `path.islink` -- `path.ismount` -- `path.getsize` -- `path.getatime` -- `path.getctime` -- `path.samefile` -- `path.sameopenfile` +- `sendfile` +- `stat` +- `statvfs` +- `symlink` +- `unlink` ### Tempfile diff --git a/pyproject.toml b/pyproject.toml index 3efac5f..6bd05ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,11 @@ source = [ [tool.pytest.ini_options] minversion = "8.3" +addopts = [ + "-x", + "--ff", +] +testpaths = ["tests"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" @@ -112,6 +117,7 @@ select = [ "PTH", # flake8-use-pathlib (PTH) "PYI", # flake8-pyi (PYI) "Q", # flake8-quotes (Q) + "RUF", # Ruff-specific rules (RUF) "RET", # flake8-return (RET) "RSE", # flake8-raise (RSE) "S", # flake8-bandit (S) @@ -131,7 +137,8 @@ ignore = [ ] fixable = [ "COM", - "I" + "I", + "RUF022", # https://docs.astral.sh/ruff/rules/unsorted-dunder-all/ ] [tool.ruff.lint.per-file-ignores] diff --git a/src/aiofiles/__init__.py b/src/aiofiles/__init__.py index 5f62158..e62baa4 100644 --- a/src/aiofiles/__init__.py +++ b/src/aiofiles/__init__.py @@ -13,11 +13,11 @@ __all__ = [ "open", - "tempfile", - "stdin", - "stdout", "stderr", + "stderr_bytes", + "stdin", "stdin_bytes", + "stdout", "stdout_bytes", - "stderr_bytes", + "tempfile", ] diff --git a/src/aiofiles/base.py b/src/aiofiles/base.py index ef1f81d..37cbfcf 100644 --- a/src/aiofiles/base.py +++ b/src/aiofiles/base.py @@ -1,18 +1,27 @@ -from asyncio import get_running_loop -from collections.abc import Awaitable +from asyncio import get_running_loop, to_thread +from collections.abc import Awaitable, Callable, Coroutine from contextlib import AbstractAsyncContextManager -from functools import partial, wraps +from functools import wraps -def wrap(func): +def wrap(func: Callable) -> Callable: + """Converts the routine `func` into a coroutine. + + The returned coroutine function runs the decorated function + in a separate thread. + + Args: + func: A routine (regular function). + + Returns: + A coroutine function. + """ + @wraps(func) - async def run(*args, loop=None, executor=None, **kwargs): - if loop is None: - loop = get_running_loop() - pfunc = partial(func, *args, **kwargs) - return await loop.run_in_executor(executor, pfunc) + async def _wrapper(*args, **kwargs) -> Coroutine: + return await to_thread(func, *args, **kwargs) - return run + return _wrapper class AsyncBase: diff --git a/src/aiofiles/os.py b/src/aiofiles/os.py index 153d65d..6c35415 100644 --- a/src/aiofiles/os.py +++ b/src/aiofiles/os.py @@ -6,24 +6,23 @@ from .base import wrap __all__ = [ + "access", + "getcwd", + "listdir", + "makedirs", + "mkdir", "path", - "stat", + "readlink", + "remove", + "removedirs", "rename", "renames", "replace", - "remove", - "unlink", - "mkdir", - "makedirs", "rmdir", - "removedirs", - "symlink", - "readlink", - "listdir", "scandir", - "access", - "wrap", - "getcwd", + "stat", + "symlink", + "unlink", ] access = wrap(os.access) diff --git a/src/aiofiles/ospath.py b/src/aiofiles/ospath.py index f47f150..c4beb64 100644 --- a/src/aiofiles/ospath.py +++ b/src/aiofiles/ospath.py @@ -6,11 +6,11 @@ __all__ = [ "abspath", + "exists", "getatime", "getctime", "getmtime", "getsize", - "exists", "isdir", "isfile", "islink", diff --git a/src/aiofiles/tempfile/__init__.py b/src/aiofiles/tempfile/__init__.py index b1c32c8..dd72d5b 100644 --- a/src/aiofiles/tempfile/__init__.py +++ b/src/aiofiles/tempfile/__init__.py @@ -15,9 +15,9 @@ __all__ = [ "NamedTemporaryFile", - "TemporaryFile", "SpooledTemporaryFile", "TemporaryDirectory", + "TemporaryFile", ] diff --git a/src/aiofiles/threadpool/__init__.py b/src/aiofiles/threadpool/__init__.py index 8054034..2541ea6 100644 --- a/src/aiofiles/threadpool/__init__.py +++ b/src/aiofiles/threadpool/__init__.py @@ -25,12 +25,12 @@ __all__ = ( "open", - "stdin", - "stdout", "stderr", + "stderr_bytes", + "stdin", "stdin_bytes", + "stdout", "stdout_bytes", - "stderr_bytes", ) diff --git a/tests/threadpool/test_open.py b/tests/threadpool/test_open.py index ec1ef63..5b2de25 100644 --- a/tests/threadpool/test_open.py +++ b/tests/threadpool/test_open.py @@ -46,7 +46,7 @@ async def test_file_async_context_aexit(): async def test_filetask_async_context_aexit(): async def _process_test_file(file_ctx, sleep_time: float = 1.0): - nonlocal file_ref + nonlocal file_ref # type: ignore async with file_ctx as fp: file_ref = file_ctx._obj await asyncio.sleep(sleep_time) diff --git a/tox.ini b/tox.ini index 9257f0f..5768ba5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,10 @@ requires = tox>=4.26 min_version = 4.26 -env_list = py39, py31{0,1,2,3,4}, pypy39, lint +env_list = py39, py31{0,1,2,3,4}, pypy39, check no_package = false -[testenv:lint] +[testenv:check] skip_install = true basepython = python3.13 allowlist_externals = just