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/`.
+
+![aiofile benchmark, 65536B 
blocks](https://media.githubusercontent.com/media/mosquito/aiofile/master/benchmark/results/aiofile-bench-report-bs65536.png)
+
+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.
+
+![aiofile benchmark, 256B 
blocks](https://media.githubusercontent.com/media/mosquito/aiofile/master/benchmark/results/aiofile-bench-report-bs256.png)
+
+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/`.
+
+![aiofile benchmark, 65536B 
blocks](https://media.githubusercontent.com/media/mosquito/aiofile/master/benchmark/results/aiofile-bench-report-bs65536.png)
+
+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.
+
+![aiofile benchmark, 256B 
blocks](https://media.githubusercontent.com/media/mosquito/aiofile/master/benchmark/results/aiofile-bench-report-bs256.png)
+
+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"

Reply via email to