Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-aiofile for openSUSE:Factory checked in at 2026-08-06 16:21:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-aiofile (Old) and /work/SRC/openSUSE:Factory/.python-aiofile.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-aiofile" Thu Aug 6 16:21:54 2026 rev:2 rq:1369634 version:3.12.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-aiofile/python-aiofile.changes 2026-06-19 17:39:23.451262778 +0200 +++ /work/SRC/openSUSE:Factory/.python-aiofile.new.16738/python-aiofile.changes 2026-08-06 16:23:57.128106015 +0200 @@ -1,0 +2,10 @@ +Wed Aug 5 05:37:06 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to 3.12.3: + * Fix a double close(), fix TextFileWrapper.read() reading past the + requested length, and add a "closed" attribute + * Require caio ~= 0.12.0, so the dependency floor moves to 0.12.0 + * Remaining changes are a throughput benchmark and README updates +- Package the licence file, which was not installed before + +------------------------------------------------------------------- Old: ---- aiofile-3.11.1.tar.gz New: ---- aiofile-3.12.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-aiofile.spec ++++++ --- /var/tmp/diff_new_pack.f2zZsP/_old 2026-08-06 16:23:58.456152396 +0200 +++ /var/tmp/diff_new_pack.f2zZsP/_new 2026-08-06 16:23:58.460152535 +0200 @@ -17,19 +17,19 @@ Name: python-aiofile -Version: 3.11.1 +Version: 3.12.3 Release: 0 Summary: Asynchronous file operations interface for Python License: Apache-2.0 URL: https://github.com/mosquito/aiofile Source: https://files.pythonhosted.org/packages/source/a/aiofile/aiofile-%{version}.tar.gz -BuildRequires: %{python_module caio >= 0.9.0} +BuildRequires: %{python_module caio >= 0.12.0} BuildRequires: %{python_module hatchling} BuildRequires: %{python_module pip} BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-caio >= 0.9.0 +Requires: python-caio >= 0.12.0 BuildArch: noarch %python_subpackages @@ -54,6 +54,7 @@ %files %{python_files} %doc README.md +%license LICENCE %{python_sitelib}/aiofile %{python_sitelib}/aiofile-%{version}.dist-info ++++++ aiofile-3.11.1.tar.gz -> aiofile-3.12.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/PKG-INFO new/aiofile-3.12.3/PKG-INFO --- old/aiofile-3.11.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: aiofile -Version: 3.11.1 +Version: 3.12.3 Summary: Asynchronous file operations. Project-URL: Homepage, https://github.com/mosquito/aiofile Author-email: Dmitry Orlov <[email protected]> @@ -29,7 +29,7 @@ Classifier: Topic :: System Classifier: Topic :: System :: Operating System Requires-Python: >=3.11 -Requires-Dist: caio~=0.9.0 +Requires-Dist: caio~=0.12.0 Description-Content-Type: text/markdown # AIOFile @@ -509,3 +509,56 @@ asyncio.run(main()) ``` + +## Benchmark + +stdlib, `aiofile` (once per `caio` backend, each in its own subprocess), `aiofiles`, +`aiomisc.io` — linear/random block order, read/write, buffered/`O_DIRECT`, +sequential/concurrent, fixed op count per cell (never a partial/cancelled +measurement). Tool, methodology and raw data: [`benchmark/`](benchmark/); a +third report at 4096B lives in `benchmark/results/`. + + + +Linear write, one Linux/ext4 run, 65536B blocks, 2000 ops/cell, per-op +latency in ms (p50 / p95, ±stdev): + +| participant | x1 | x8 | +|-----------------------|------------------------|------------------------| +| stdlib (buffered) | 0.081 / 0.128 (±0.030) | 0.167 / 0.298 (±0.157) | +| stdlib (O_DIRECT) | 0.148 / 0.398 (±0.097) | 0.266 / 0.650 (±0.192) | +| aiofile (linux_uring) | 0.050 / 0.108 (±0.022) | 0.040 / 0.248 (±0.293) | +| aiofile (linux_aio) | 0.009 / 0.024 (±0.020) | 0.040 / 0.089 (±0.020) | +| aiofile (thread_aio) | 0.150 / 0.461 (±0.128) | 0.664 / 2.930 (±0.943) | +| aiofile (python_aio) | 0.109 / 0.173 (±0.042) | 0.189 / 0.314 (±0.072) | +| aiofiles | 0.155 / 0.257 (±0.175) | 1.166 / 2.238 (±0.570) | +| aiomisc | 0.156 / 0.256 (±0.051) | 1.118 / 1.607 (±1.750) | + +`linux_aio` posts the lowest, tightest per-op latency at both concurrency +levels here -- `io_submit()` is a lean, purpose-built syscall, while +`linux_uring`'s ring/SQPOLL machinery is built to amortize *over batched +submissions*, not to minimize one op's latency, so it only pulls ahead +once there's a batch to amortize over. `aiofiles`/`aiomisc` latency +*grows* at x8 (one seek cursor, serialized by a lock) instead of +shrinking like the positional-I/O libraries. + + + +Linear write, 256B blocks, 2000 ops/cell, per-op latency in ms (p50 / p95, ±stdev): + +| participant | x1 | x8 | +|-----------------------|-------------------------------|------------------------| +| stdlib (buffered) | 0.066 / 0.104 (±0.017) | 0.152 / 0.299 (±0.125) | +| stdlib (O_DIRECT) | n/a (256B isn't 4096-aligned) | n/a | +| aiofile (linux_uring) | 0.086 / 0.159 (±0.043) | 0.043 / 0.070 (±0.021) | +| aiofile (linux_aio) | 0.005 / 0.008 (±0.004) | 0.017 / 0.027 (±0.008) | +| aiofile (thread_aio) | 0.128 / 0.290 (±0.090) | 0.464 / 1.503 (±0.470) | +| aiofile (python_aio) | 0.113 / 0.201 (±0.037) | 0.179 / 0.354 (±0.124) | +| aiofiles | 0.136 / 0.169 (±0.030) | 1.002 / 1.516 (±0.372) | +| aiomisc | 0.144 / 0.223 (±0.040) | 1.046 / 1.999 (±0.415) | + +Same shape at a tiny block size: `linux_aio` still has the lowest latency +by a wide margin (sub-10μs median at x1), `linux_uring` still needs +concurrency to close the gap (and still doesn't fully close it), and the +lock-serialized libraries still get *worse*, not better, under +concurrency. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/README.md new/aiofile-3.12.3/README.md --- old/aiofile-3.11.1/README.md 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/README.md 2020-02-02 01:00:00.000000000 +0100 @@ -475,3 +475,56 @@ asyncio.run(main()) ``` + +## Benchmark + +stdlib, `aiofile` (once per `caio` backend, each in its own subprocess), `aiofiles`, +`aiomisc.io` — linear/random block order, read/write, buffered/`O_DIRECT`, +sequential/concurrent, fixed op count per cell (never a partial/cancelled +measurement). Tool, methodology and raw data: [`benchmark/`](benchmark/); a +third report at 4096B lives in `benchmark/results/`. + + + +Linear write, one Linux/ext4 run, 65536B blocks, 2000 ops/cell, per-op +latency in ms (p50 / p95, ±stdev): + +| participant | x1 | x8 | +|-----------------------|------------------------|------------------------| +| stdlib (buffered) | 0.081 / 0.128 (±0.030) | 0.167 / 0.298 (±0.157) | +| stdlib (O_DIRECT) | 0.148 / 0.398 (±0.097) | 0.266 / 0.650 (±0.192) | +| aiofile (linux_uring) | 0.050 / 0.108 (±0.022) | 0.040 / 0.248 (±0.293) | +| aiofile (linux_aio) | 0.009 / 0.024 (±0.020) | 0.040 / 0.089 (±0.020) | +| aiofile (thread_aio) | 0.150 / 0.461 (±0.128) | 0.664 / 2.930 (±0.943) | +| aiofile (python_aio) | 0.109 / 0.173 (±0.042) | 0.189 / 0.314 (±0.072) | +| aiofiles | 0.155 / 0.257 (±0.175) | 1.166 / 2.238 (±0.570) | +| aiomisc | 0.156 / 0.256 (±0.051) | 1.118 / 1.607 (±1.750) | + +`linux_aio` posts the lowest, tightest per-op latency at both concurrency +levels here -- `io_submit()` is a lean, purpose-built syscall, while +`linux_uring`'s ring/SQPOLL machinery is built to amortize *over batched +submissions*, not to minimize one op's latency, so it only pulls ahead +once there's a batch to amortize over. `aiofiles`/`aiomisc` latency +*grows* at x8 (one seek cursor, serialized by a lock) instead of +shrinking like the positional-I/O libraries. + + + +Linear write, 256B blocks, 2000 ops/cell, per-op latency in ms (p50 / p95, ±stdev): + +| participant | x1 | x8 | +|-----------------------|-------------------------------|------------------------| +| stdlib (buffered) | 0.066 / 0.104 (±0.017) | 0.152 / 0.299 (±0.125) | +| stdlib (O_DIRECT) | n/a (256B isn't 4096-aligned) | n/a | +| aiofile (linux_uring) | 0.086 / 0.159 (±0.043) | 0.043 / 0.070 (±0.021) | +| aiofile (linux_aio) | 0.005 / 0.008 (±0.004) | 0.017 / 0.027 (±0.008) | +| aiofile (thread_aio) | 0.128 / 0.290 (±0.090) | 0.464 / 1.503 (±0.470) | +| aiofile (python_aio) | 0.113 / 0.201 (±0.037) | 0.179 / 0.354 (±0.124) | +| aiofiles | 0.136 / 0.169 (±0.030) | 1.002 / 1.516 (±0.372) | +| aiomisc | 0.144 / 0.223 (±0.040) | 1.046 / 1.999 (±0.415) | + +Same shape at a tiny block size: `linux_aio` still has the lowest latency +by a wide margin (sub-10μs median at x1), `linux_uring` still needs +concurrency to close the gap (and still doesn't fully close it), and the +lock-serialized libraries still get *worse*, not better, under +concurrency. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/aiofile/aio.py new/aiofile-3.12.3/aiofile/aio.py --- old/aiofile-3.11.1/aiofile/aio.py 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/aiofile/aio.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,7 +2,7 @@ import os from collections import namedtuple from concurrent.futures import Executor -from functools import partial +from functools import partial, wraps from os import strerror from pathlib import Path from typing import ( @@ -169,6 +169,10 @@ def encoding(self) -> str: return self._encoding + @property + def closed(self) -> bool: + return self._file_obj is None or self._file_obj.closed + async def open(self) -> Optional[int]: if self._file_obj is not None: if self._file_obj.closed: @@ -198,7 +202,11 @@ return self async def close(self) -> None: - if self._file_obj is None or not self._file_obj_owner: + if ( + self._file_obj is None + or not self._file_obj_owner + or self._file_obj.closed + ): return async with self._clone_lock: @@ -313,28 +321,61 @@ ) -ContextStoreType = Dict[asyncio.AbstractEventLoop, caio.AsyncioContext] +# Keyed by id(loop) so the keys themselves do not retain event loops. Values +# still retain their loops through caio contexts, so create_context() also +# installs synchronous cleanup into loop.close(). This is important for +# short-lived loops: waiting for garbage collection would keep native AIO +# resources (and io_uring SQPOLL threads) alive between loop instances. +ContextStoreType = Dict[int, caio.AsyncioContext] DEFAULT_CONTEXT_STORE: ContextStoreType = {} +def _release_context(loop_id: int) -> None: + context = DEFAULT_CONTEXT_STORE.pop(loop_id, None) + if context is not None: + context.close() + + +def _install_context_cleanup( + loop: asyncio.AbstractEventLoop, +) -> None: + original_close = loop.close + loop_id = id(loop) + + @wraps(original_close) + def close() -> None: + if loop.is_running(): + original_close() + return + + try: + _release_context(loop_id) + finally: + original_close() + + try: + setattr(loop, "close", close) + except (AttributeError, TypeError): + # Some third-party event loops do not allow instance attributes. + # The finalizer below still provides interpreter-shutdown cleanup. + pass + + def create_context( max_requests: int = caio.AsyncioContext.MAX_REQUESTS_DEFAULT, ) -> caio.AsyncioContext: loop = asyncio.get_event_loop() context = caio.AsyncioContext(max_requests, loop=loop) - def finalizer() -> None: - context.close() - DEFAULT_CONTEXT_STORE.pop(loop, None) - - finalize(loop, finalizer) - DEFAULT_CONTEXT_STORE[loop] = context + _install_context_cleanup(loop) + finalize(loop, _release_context, id(loop)) + DEFAULT_CONTEXT_STORE[id(loop)] = context return context def get_default_context() -> caio.AsyncioContext: loop = asyncio.get_event_loop() - context = DEFAULT_CONTEXT_STORE.get(loop) + context = DEFAULT_CONTEXT_STORE.get(id(loop)) if context is not None: return context diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/aiofile/utils.py new/aiofile-3.12.3/aiofile/utils.py --- old/aiofile-3.11.1/aiofile/utils.py 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/aiofile/utils.py 2020-02-02 01:00:00.000000000 +0100 @@ -190,6 +190,10 @@ def tell(self) -> int: return self._offset + @property + def closed(self) -> bool: + return self.file.closed + async def flush(self, sync_metadata: bool = False) -> None: if sync_metadata: await self.file.fsync() @@ -272,7 +276,6 @@ self.encoding = self.file.encoding async def __read(self, length: int) -> str: - chunk_size = 0 offset = self._offset chunk = "" while length < 0 or length > len(chunk): @@ -286,9 +289,10 @@ chunk += part offset += part_offset - if chunk_size > length > 0: + if 0 < length < len(chunk): + extra = chunk[length:] chunk = chunk[:length] - offset = length + offset -= len(extra.encode(self.encoding)) self._offset = offset return chunk diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/pyproject.toml new/aiofile-3.12.3/pyproject.toml --- old/aiofile-3.11.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ [project] name = "aiofile" -version = "3.11.1" +version = "3.12.3" description = "Asynchronous file operations." license = "Apache-2.0" authors = [ @@ -32,7 +32,7 @@ ] requires-python = ">=3.11" dependencies = [ - "caio~=0.9.0", + "caio~=0.12.0", ] [project.urls] @@ -40,7 +40,7 @@ [dependency-groups] dev = [ - "markdown-pytest>=0.3.2,<0.4", + "markdown-pytest>=0.6.5,<0.7", "pytest>=8.4", "aiomisc-pytest>=2.0.0,<3", "pytest-cov>=5.0.0,<6", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/tests/conftest.py new/aiofile-3.12.3/tests/conftest.py --- old/aiofile-3.11.1/tests/conftest.py 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/tests/conftest.py 2020-02-02 01:00:00.000000000 +0100 @@ -18,10 +18,16 @@ except ImportError: linux_aio_asyncio = None # type: ignore +try: + from caio import linux_uring_asyncio +except ImportError: + linux_uring_asyncio = None # type: ignore + IMPLEMENTATIONS: List[Union[ModuleType, None]] = list( filter( None, [ + linux_uring_asyncio, linux_aio_asyncio, thread_aio_asyncio, python_aio_asyncio, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiofile-3.11.1/tests/test_aio.py new/aiofile-3.12.3/tests/test_aio.py --- old/aiofile-3.11.1/tests/test_aio.py 2020-02-02 01:00:00.000000000 +0100 +++ new/aiofile-3.12.3/tests/test_aio.py 2020-02-02 01:00:00.000000000 +0100 @@ -14,6 +14,7 @@ import pytest from aiofile import AIOFile, clone +from aiofile.aio import DEFAULT_CONTEXT_STORE, create_context from aiofile.utils import ( BinaryFileWrapper, LineReader, Reader, TextFileWrapper, Writer, ) @@ -35,6 +36,23 @@ return path +def test_default_context_closed_with_event_loop(monkeypatch): + loop = asyncio.new_event_loop() + context = Mock() + + monkeypatch.setattr(asyncio, "get_event_loop", lambda: loop) + monkeypatch.setattr(caio, "AsyncioContext", Mock(return_value=context)) + + assert create_context() is context + assert DEFAULT_CONTEXT_STORE[id(loop)] is context + + loop.close() + + context.close.assert_called_once_with() + assert id(loop) not in DEFAULT_CONTEXT_STORE + assert loop.is_closed() + + @pytest.fixture def uuid(tmp_path): return str(uuid4()) @@ -217,6 +235,31 @@ await file.open() +async def test_double_close(aio_file_maker, temp_file): + file = aio_file_maker(temp_file, "w") + await file.open() + await file.close() + await file.close() + + +async def test_closed_attribute(aio_file_maker, temp_file): + file = aio_file_maker(temp_file, "w") + assert file.closed + await file.open() + assert not file.closed + await file.close() + assert file.closed + + +async def test_closed_attribute_wrapper(async_open, temp_file): + fp = async_open(temp_file, "w") + assert fp.closed + await fp + assert not fp.closed + await fp.close() + assert fp.closed + + async def test_parallel_open(aio_file_maker, temp_file): file = aio_file_maker(temp_file, "r") try: @@ -437,6 +480,24 @@ assert fp.tell() == 4 +async def test_text_io_wrapper_read_length_multibyte(aio_file_maker, temp_file): + data = ( + "世界人権宣言前文人類社会のすべての構成員の固有の尊厳と平等で" + "譲ることのできない権利とを承認する" + ) + + async with aio_file_maker(temp_file, "w+") as afp: + await afp.write(data) + + collected = "" + async with TextFileWrapper(aio_file_maker(temp_file, "r")) as fp: + while chunk := await fp.read(12): + assert len(chunk) <= 12 + collected += chunk + + assert collected == data + + async def test_binary_io_wrapper(aio_file_maker, temp_file): async with aio_file_maker(temp_file, "wb+") as afp: data = b"\x01\x02\x03"
