From beb828511c1d91b604be31303c8564752b0f4064 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Tue, 11 Feb 2025 00:10:18 +0900 Subject: [PATCH 01/25] feat: Add async support for temporary file handling. Closes #344 --- src/anyio/__init__.py | 10 ++ src/anyio/_core/_tempfile.py | 280 +++++++++++++++++++++++++++++++++++ tests/test_tempfile.py | 185 +++++++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 src/anyio/_core/_tempfile.py create mode 100644 tests/test_tempfile.py diff --git a/src/anyio/__init__.py b/src/anyio/__init__.py index 09831259..9b93b457 100644 --- a/src/anyio/__init__.py +++ b/src/anyio/__init__.py @@ -61,6 +61,16 @@ from ._core._tasks import current_effective_deadline as current_effective_deadline from ._core._tasks import fail_after as fail_after from ._core._tasks import move_on_after as move_on_after +from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile +from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile +from ._core._tempfile import TemporaryDirectory as TemporaryDirectory +from ._core._tempfile import TemporaryFile as TemporaryFile +from ._core._tempfile import gettempdir as gettempdir +from ._core._tempfile import gettempdirb as gettempdirb +from ._core._tempfile import gettempprefix as gettempprefix +from ._core._tempfile import gettempprefixb as gettempprefixb +from ._core._tempfile import mkdtemp as mkdtemp +from ._core._tempfile import mkstemp as mkstemp from ._core._testing import TaskInfo as TaskInfo from ._core._testing import get_current_task as get_current_task from ._core._testing import get_running_tasks as get_running_tasks diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py new file mode 100644 index 00000000..57fbd07b --- /dev/null +++ b/src/anyio/_core/_tempfile.py @@ -0,0 +1,280 @@ +from __future__ import annotations + +import sys +import tempfile +from functools import partial +from os import PathLike +from types import TracebackType +from typing import Any, AnyStr, Callable, Generic, cast + +from .. import AsyncFile, to_thread + + +class TemporaryFile(Generic[AnyStr]): + def __init__( + self, + mode: str = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + errors: str | None = None, + ) -> None: + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.errors = errors + + self._async_file: AsyncFile | None = None + + async def __aenter__(self) -> AsyncFile: + fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile( + self.mode, + self.buffering, + self.encoding, + self.newline, + self.suffix, + self.prefix, + self.dir, + errors=self.errors, + ) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._async_file is not None: + await self._async_file.aclose() + self._async_file = None + + +class NamedTemporaryFile(Generic[AnyStr]): + def __init__( + self, + mode: str = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: (str | bytes) | None = None, + prefix: (str | bytes) | None = None, + dir: (str | bytes | PathLike[str]) | None = None, + delete: bool = True, + *, + errors: str | None = None, + delete_on_close: bool = True, + ) -> None: + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix = suffix + self.prefix = prefix + self.dir = dir + self.delete = delete + self.errors = errors + self.delete_on_close = delete_on_close + + self._async_file: AsyncFile | None = None + + async def __aenter__(self) -> AsyncFile[AnyStr]: + params: dict[str, Any] = { + "mode": self.mode, + "buffering": self.buffering, + "encoding": self.encoding, + "newline": self.newline, + "suffix": self.suffix, + "prefix": self.prefix, + "dir": self.dir, + "delete": self.delete, + "errors": self.errors, + } + if sys.version_info >= (3, 12): + params["delete_on_close"] = self.delete_on_close + + fp = await to_thread.run_sync(lambda: tempfile.NamedTemporaryFile(**params)) + + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._async_file is not None: + await self._async_file.aclose() + self._async_file = None + + +class SpooledTemporaryFile(Generic[AnyStr]): + def __init__( + self, + max_size: int = 0, + mode: str = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + errors: str | None = None, + ) -> None: + self.max_size = max_size + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.errors = errors + + self._async_file: AsyncFile | None = None + + async def __aenter__(self) -> SpooledTemporaryFile: + fp = await to_thread.run_sync( + partial( + tempfile.SpooledTemporaryFile, + self.max_size, + self.mode, + self.buffering, + self.encoding, + self.newline, + self.suffix, + self.prefix, + self.dir, + errors=self.errors, + ) + ) + self._async_file = AsyncFile(fp) + return self + + async def rollover(self) -> None: + if self._async_file is None: + raise RuntimeError("Internal file is not initialized.") + await to_thread.run_sync(cast(Callable[[], None], self._async_file.rollover)) + + @property + def closed(self) -> bool: + if self._async_file is not None: + try: + return bool(self._async_file.closed) + except AttributeError: + f = getattr(self._async_file, "_f", None) + if f is None: + return True + return bool(f.closed) + return True + + def __getattr__(self, attr: str) -> Any: + if self._async_file is not None: + return getattr(self._async_file, attr) + raise AttributeError(f"{self.__class__.__name__} has no attribute {attr}") + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._async_file is not None: + await self._async_file.aclose() + self._async_file = None + + +class TemporaryDirectory(Generic[AnyStr]): + def __init__( + self, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + ignore_cleanup_errors: bool = False, + delete: bool = True, + ) -> None: + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.ignore_cleanup_errors = ignore_cleanup_errors + self.delete = delete + + self._tempdir: tempfile.TemporaryDirectory | None = None + + async def __aenter__(self) -> str: + params: dict[str, Any] = { + "suffix": self.suffix, + "prefix": self.prefix, + "dir": self.dir, + } + if sys.version_info >= (3, 10): + params["ignore_cleanup_errors"] = self.ignore_cleanup_errors + if sys.version_info >= (3, 12): + params["delete"] = self.delete + + self._tempdir = await to_thread.run_sync( + lambda: tempfile.TemporaryDirectory(**params) + ) + return await to_thread.run_sync(self._tempdir.__enter__) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._tempdir is not None: + await to_thread.run_sync( + self._tempdir.__exit__, exc_type, exc_value, traceback + ) + + async def cleanup(self) -> None: + if self._tempdir is not None: + await to_thread.run_sync(self._tempdir.cleanup) + + +async def mkstemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + text: bool = False, +) -> tuple[int, str | bytes]: + return await to_thread.run_sync(lambda: tempfile.mkstemp(suffix, prefix, dir, text)) + + +async def mkdtemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, +) -> str | bytes: + return await to_thread.run_sync(lambda: tempfile.mkdtemp(suffix, prefix, dir)) + + +async def gettempprefix() -> str: + return await to_thread.run_sync(tempfile.gettempprefix) + + +async def gettempprefixb() -> bytes: + return await to_thread.run_sync(tempfile.gettempprefixb) + + +async def gettempdir() -> str: + return await to_thread.run_sync(tempfile.gettempdir) + + +async def gettempdirb() -> bytes: + return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py new file mode 100644 index 00000000..14aedf32 --- /dev/null +++ b/tests/test_tempfile.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import os +import pathlib +import shutil +import tempfile + +import pytest + +from anyio import ( + NamedTemporaryFile, + SpooledTemporaryFile, + TemporaryDirectory, + TemporaryFile, + gettempdir, + gettempdirb, + gettempprefix, + gettempprefixb, + mkdtemp, + mkstemp, + to_thread, +) + +pytestmark = pytest.mark.anyio + + +@pytest.mark.anyio +async def test_temporary_file() -> None: + data = b"temporary file data" + async with TemporaryFile[bytes]() as af: + await af.write(data) + await af.seek(0) + result = await af.read() + assert result == data + assert af.closed + + +@pytest.mark.anyio +async def test_named_temporary_file() -> None: + data = b"named temporary file data" + async with NamedTemporaryFile[bytes]() as af: + filename: str = str(af.name) + assert os.path.exists(filename) + await af.write(data) + await af.seek(0) + result = await af.read() + assert result == data + assert not os.path.exists(filename) + + +@pytest.mark.anyio +async def test_spooled_temporary_file_io_and_rollover() -> None: + data = b"spooled temporary file data" * 3 + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + await stf.write(data) + await stf.seek(0) + result = await stf.read() + assert result == data + pos = await stf.tell() + assert isinstance(pos, int) + await stf.rollover() + assert not stf.closed + assert stf.closed + + +@pytest.mark.anyio +async def test_spooled_temporary_file_error_conditions() -> None: + stf = SpooledTemporaryFile[bytes]() + with pytest.raises(RuntimeError): + await stf.rollover() + with pytest.raises(AttributeError): + _ = stf.nonexistent_attribute + + +@pytest.mark.anyio +async def test_temporary_directory_context_manager() -> None: + async with TemporaryDirectory() as td: + td_path = pathlib.Path(td) + assert td_path.exists() and td_path.is_dir() + file_path = td_path / "test.txt" + file_path.write_text("temp dir test", encoding="utf-8") + assert file_path.exists() + assert not td_path.exists() + + +@pytest.mark.anyio +async def test_temporary_directory_cleanup_method() -> None: + td = TemporaryDirectory() + td_str = await td.__aenter__() + td_path = pathlib.Path(td_str) + file_path = td_path / "file.txt" + file_path.write_text("cleanup test", encoding="utf-8") + await td.cleanup() + assert not td_path.exists() + + +@pytest.mark.anyio +async def test_mkstemp() -> None: + fd, path_ = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) + assert isinstance(fd, int) + assert isinstance(path_, str) + + def write_file() -> None: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write("mkstemp") + + await to_thread.run_sync(write_file) + + def read_file() -> str: + with open(path_, encoding="utf-8") as f: + return f.read() + + content = await to_thread.run_sync(read_file) + assert content == "mkstemp" + await to_thread.run_sync(lambda: os.remove(path_)) + + +@pytest.mark.anyio +async def test_mkdtemp() -> None: + d = await mkdtemp(prefix="mkdtemp_") + if isinstance(d, bytes): + dp = pathlib.Path(os.fsdecode(d)) + else: + dp = pathlib.Path(d) + assert dp.exists() and dp.is_dir() + await to_thread.run_sync(lambda: shutil.rmtree(dp)) + assert not dp.exists() + + +@pytest.mark.anyio +async def test_gettemp_functions() -> None: + pref = await gettempprefix() + prefb = await gettempprefixb() + tdir = await gettempdir() + tdirb = await gettempdirb() + assert isinstance(pref, str) + assert isinstance(prefb, bytes) + assert isinstance(tdir, str) + assert isinstance(tdirb, bytes) + assert pref == tempfile.gettempprefix() + assert prefb == tempfile.gettempprefixb() + assert tdir == tempfile.gettempdir() + assert tdirb == tempfile.gettempdirb() + + +@pytest.mark.anyio +async def test_named_temporary_file_exception_handling() -> None: + async with NamedTemporaryFile[bytes]() as af: + filename = str(af.name) + assert os.path.exists(filename) + + assert not os.path.exists(filename) + with pytest.raises(ValueError): + await af.write(b"should fail") + + +@pytest.mark.anyio +async def test_temporary_directory_exception_handling() -> None: + async with TemporaryDirectory() as td: + td_path = pathlib.Path(td) + assert td_path.exists() and td_path.is_dir() + + assert not td_path.exists() + with pytest.raises(FileNotFoundError): + (td_path / "nonexistent.txt").write_text("should fail", encoding="utf-8") + + +@pytest.mark.anyio +async def test_spooled_temporary_file_rollover_handling() -> None: + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + await stf.write(b"1234567890") + await stf.rollover() + assert not stf.closed + await stf.write(b"more data") + await stf.seek(0) + result = await stf.read() + assert result == b"1234567890more data" + + +@pytest.mark.anyio +async def test_spooled_temporary_file_closed_state() -> None: + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + assert not stf.closed + + assert stf.closed From 93c9ae4221245a48995dcf3afd60f2f57264ba31 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Wed, 12 Feb 2025 02:38:27 +0900 Subject: [PATCH 02/25] Fix: Apply requested changes from @agronholm - Remove redundant decorators - Adjust type annotations by removing unnecessary parentheses - Import from its correct location - Remove unnecessary async versions of - Add blank lines for better readability after control blocks - Remove redundant type casts and isinstance checks - Group related tests under a test class for better organization - Fix path variable naming consistency - Ensure proper handling of blocking I/O within async functions --- src/anyio/__init__.py | 2 - src/anyio/_core/_tempfile.py | 24 ++-- tests/test_tempfile.py | 269 +++++++++++++++++------------------ 3 files changed, 140 insertions(+), 155 deletions(-) diff --git a/src/anyio/__init__.py b/src/anyio/__init__.py index 9b93b457..578cda6f 100644 --- a/src/anyio/__init__.py +++ b/src/anyio/__init__.py @@ -67,8 +67,6 @@ from ._core._tempfile import TemporaryFile as TemporaryFile from ._core._tempfile import gettempdir as gettempdir from ._core._tempfile import gettempdirb as gettempdirb -from ._core._tempfile import gettempprefix as gettempprefix -from ._core._tempfile import gettempprefixb as gettempprefixb from ._core._tempfile import mkdtemp as mkdtemp from ._core._tempfile import mkstemp as mkstemp from ._core._testing import TaskInfo as TaskInfo diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 57fbd07b..0e04f868 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -3,11 +3,11 @@ import sys import tempfile from functools import partial -from os import PathLike from types import TracebackType from typing import Any, AnyStr, Callable, Generic, cast -from .. import AsyncFile, to_thread +from .. import to_thread +from .._core._fileio import AsyncFile class TemporaryFile(Generic[AnyStr]): @@ -68,9 +68,9 @@ def __init__( buffering: int = -1, encoding: str | None = None, newline: str | None = None, - suffix: (str | bytes) | None = None, - prefix: (str | bytes) | None = None, - dir: (str | bytes | PathLike[str]) | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, delete: bool = True, *, errors: str | None = None, @@ -80,9 +80,9 @@ def __init__( self.buffering = buffering self.encoding = encoding self.newline = newline - self.suffix = suffix - self.prefix = prefix - self.dir = dir + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir self.delete = delete self.errors = errors self.delete_on_close = delete_on_close @@ -264,14 +264,6 @@ async def mkdtemp( return await to_thread.run_sync(lambda: tempfile.mkdtemp(suffix, prefix, dir)) -async def gettempprefix() -> str: - return await to_thread.run_sync(tempfile.gettempprefix) - - -async def gettempprefixb() -> bytes: - return await to_thread.run_sync(tempfile.gettempprefixb) - - async def gettempdir() -> str: return await to_thread.run_sync(tempfile.gettempdir) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 14aedf32..978efab6 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -14,8 +14,6 @@ TemporaryFile, gettempdir, gettempdirb, - gettempprefix, - gettempprefixb, mkdtemp, mkstemp, to_thread, @@ -24,162 +22,159 @@ pytestmark = pytest.mark.anyio -@pytest.mark.anyio -async def test_temporary_file() -> None: - data = b"temporary file data" - async with TemporaryFile[bytes]() as af: - await af.write(data) - await af.seek(0) - result = await af.read() - assert result == data - assert af.closed - - -@pytest.mark.anyio -async def test_named_temporary_file() -> None: - data = b"named temporary file data" - async with NamedTemporaryFile[bytes]() as af: - filename: str = str(af.name) - assert os.path.exists(filename) - await af.write(data) - await af.seek(0) - result = await af.read() - assert result == data - assert not os.path.exists(filename) - - -@pytest.mark.anyio -async def test_spooled_temporary_file_io_and_rollover() -> None: - data = b"spooled temporary file data" * 3 - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - await stf.write(data) - await stf.seek(0) - result = await stf.read() +class TestTemporaryFile: + async def test_temporary_file(self) -> None: + data = b"temporary file data" + async with TemporaryFile[bytes]() as af: + await af.write(data) + await af.seek(0) + result = await af.read() + + assert result == data + + assert af.closed + + +class TestNamedTemporaryFile: + async def test_named_temporary_file(self) -> None: + data = b"named temporary file data" + async with NamedTemporaryFile[bytes]() as af: + filename = str(af.name) + assert os.path.exists(filename) + + await af.write(data) + await af.seek(0) + result = await af.read() + assert result == data - pos = await stf.tell() - assert isinstance(pos, int) - await stf.rollover() - assert not stf.closed - assert stf.closed - - -@pytest.mark.anyio -async def test_spooled_temporary_file_error_conditions() -> None: - stf = SpooledTemporaryFile[bytes]() - with pytest.raises(RuntimeError): - await stf.rollover() - with pytest.raises(AttributeError): - _ = stf.nonexistent_attribute - - -@pytest.mark.anyio -async def test_temporary_directory_context_manager() -> None: - async with TemporaryDirectory() as td: - td_path = pathlib.Path(td) - assert td_path.exists() and td_path.is_dir() - file_path = td_path / "test.txt" - file_path.write_text("temp dir test", encoding="utf-8") - assert file_path.exists() - assert not td_path.exists() - - -@pytest.mark.anyio -async def test_temporary_directory_cleanup_method() -> None: - td = TemporaryDirectory() - td_str = await td.__aenter__() - td_path = pathlib.Path(td_str) - file_path = td_path / "file.txt" - file_path.write_text("cleanup test", encoding="utf-8") - await td.cleanup() - assert not td_path.exists() - - -@pytest.mark.anyio + + assert not os.path.exists(filename) + + async def test_exception_handling(self) -> None: + async with NamedTemporaryFile[bytes]() as af: + filename = str(af.name) + assert os.path.exists(filename) + + assert not os.path.exists(filename) + + with pytest.raises(ValueError): + await af.write(b"should fail") + + +class TestSpooledTemporaryFile: + async def test_io_and_rollover(self) -> None: + data = b"spooled temporary file data" * 3 + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + await stf.write(data) + await stf.seek(0) + result = await stf.read() + + assert result == data + + pos = await stf.tell() + assert isinstance(pos, int) + + await stf.rollover() + assert not stf.closed + + assert stf.closed + + async def test_error_conditions(self) -> None: + stf = SpooledTemporaryFile[bytes]() + + with pytest.raises(RuntimeError): + await stf.rollover() + + with pytest.raises(AttributeError): + _ = stf.nonexistent_attribute + + async def test_rollover_handling(self) -> None: + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + await stf.write(b"1234567890") + await stf.rollover() + assert not stf.closed + + await stf.write(b"more data") + await stf.seek(0) + result = await stf.read() + + assert result == b"1234567890more data" + + async def test_closed_state(self) -> None: + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + assert not stf.closed + + assert stf.closed + + +class TestTemporaryDirectory: + async def test_context_manager(self) -> None: + async with TemporaryDirectory() as td: + td_path = pathlib.Path(td) + assert td_path.exists() and td_path.is_dir() + + file_path = td_path / "test.txt" + file_path.write_text("temp dir test", encoding="utf-8") + assert file_path.exists() + + assert not td_path.exists() + + async def test_cleanup_method(self) -> None: + td = TemporaryDirectory() + td_str = await td.__aenter__() + td_path = pathlib.Path(td_str) + + file_path = td_path / "file.txt" + file_path.write_text("cleanup test", encoding="utf-8") + + await td.cleanup() + assert not td_path.exists() + + async def test_exception_handling(self) -> None: + async with TemporaryDirectory() as td: + td_path = pathlib.Path(td) + assert td_path.exists() and td_path.is_dir() + + assert not td_path.exists() + + with pytest.raises(FileNotFoundError): + (td_path / "nonexistent.txt").write_text("should fail", encoding="utf-8") + + async def test_mkstemp() -> None: - fd, path_ = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) + fd, path = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) assert isinstance(fd, int) - assert isinstance(path_, str) - - def write_file() -> None: - with os.fdopen(fd, "w", encoding="utf-8") as f: - f.write("mkstemp") + assert isinstance(path, str) - await to_thread.run_sync(write_file) + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write("mkstemp") - def read_file() -> str: - with open(path_, encoding="utf-8") as f: - return f.read() + content = await to_thread.run_sync( + lambda: pathlib.Path(path).read_text(encoding="utf-8") + ) - content = await to_thread.run_sync(read_file) assert content == "mkstemp" - await to_thread.run_sync(lambda: os.remove(path_)) + + await to_thread.run_sync(lambda: os.remove(path)) -@pytest.mark.anyio async def test_mkdtemp() -> None: d = await mkdtemp(prefix="mkdtemp_") + if isinstance(d, bytes): dp = pathlib.Path(os.fsdecode(d)) else: dp = pathlib.Path(d) + assert dp.exists() and dp.is_dir() - await to_thread.run_sync(lambda: shutil.rmtree(dp)) + + shutil.rmtree(dp) assert not dp.exists() -@pytest.mark.anyio async def test_gettemp_functions() -> None: - pref = await gettempprefix() - prefb = await gettempprefixb() tdir = await gettempdir() tdirb = await gettempdirb() - assert isinstance(pref, str) - assert isinstance(prefb, bytes) - assert isinstance(tdir, str) - assert isinstance(tdirb, bytes) - assert pref == tempfile.gettempprefix() - assert prefb == tempfile.gettempprefixb() + assert tdir == tempfile.gettempdir() assert tdirb == tempfile.gettempdirb() - - -@pytest.mark.anyio -async def test_named_temporary_file_exception_handling() -> None: - async with NamedTemporaryFile[bytes]() as af: - filename = str(af.name) - assert os.path.exists(filename) - - assert not os.path.exists(filename) - with pytest.raises(ValueError): - await af.write(b"should fail") - - -@pytest.mark.anyio -async def test_temporary_directory_exception_handling() -> None: - async with TemporaryDirectory() as td: - td_path = pathlib.Path(td) - assert td_path.exists() and td_path.is_dir() - - assert not td_path.exists() - with pytest.raises(FileNotFoundError): - (td_path / "nonexistent.txt").write_text("should fail", encoding="utf-8") - - -@pytest.mark.anyio -async def test_spooled_temporary_file_rollover_handling() -> None: - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - await stf.write(b"1234567890") - await stf.rollover() - assert not stf.closed - await stf.write(b"more data") - await stf.seek(0) - result = await stf.read() - assert result == b"1234567890more data" - - -@pytest.mark.anyio -async def test_spooled_temporary_file_closed_state() -> None: - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - assert not stf.closed - - assert stf.closed From 684176b368bc650dbb161f13aba6ffa3f5c0f469 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Wed, 12 Feb 2025 05:08:08 +0900 Subject: [PATCH 03/25] fix: add blank lines after control blocks, fix Pyright errors, and ignore ASYNC230 - Insert a blank line after the end of every control block to separate it from the following code (per review instructions). - Explicitly specify return types and adjust type annotations to resolve Pyright errors. - Maintain blocking I/O inside to_thread.run_sync() as per ASYNC230 compliance for runtime code. - Suppress ASYNC230 warnings in the test suite by adding per-file ignores in pyproject.toml. --- pyproject.toml | 3 +++ src/anyio/_core/_tempfile.py | 10 +++++++--- tests/test_tempfile.py | 10 +++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf54908a..3abe8d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,9 @@ extend-select = [ [tool.ruff.lint.isort] "required-imports" = ["from __future__ import annotations"] +[tool.ruff.lint.per-file-ignores] +"tests/test_tempfile.py" = ["ASYNC230"] + [tool.mypy] python_version = "3.13" strict = true diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 0e04f868..26b185c9 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -34,7 +34,7 @@ def __init__( self._async_file: AsyncFile | None = None - async def __aenter__(self) -> AsyncFile: + async def __aenter__(self) -> AsyncFile[AnyStr]: fp = await to_thread.run_sync( lambda: tempfile.TemporaryFile( self.mode, @@ -105,7 +105,6 @@ async def __aenter__(self) -> AsyncFile[AnyStr]: params["delete_on_close"] = self.delete_on_close fp = await to_thread.run_sync(lambda: tempfile.NamedTemporaryFile(**params)) - self._async_file = AsyncFile(fp) return self._async_file @@ -146,7 +145,9 @@ def __init__( self._async_file: AsyncFile | None = None - async def __aenter__(self) -> SpooledTemporaryFile: + async def __aenter__( + self: SpooledTemporaryFile[AnyStr], + ) -> SpooledTemporaryFile[AnyStr]: fp = await to_thread.run_sync( partial( tempfile.SpooledTemporaryFile, @@ -167,6 +168,7 @@ async def __aenter__(self) -> SpooledTemporaryFile: async def rollover(self) -> None: if self._async_file is None: raise RuntimeError("Internal file is not initialized.") + await to_thread.run_sync(cast(Callable[[], None], self._async_file.rollover)) @property @@ -184,6 +186,7 @@ def closed(self) -> bool: def __getattr__(self, attr: str) -> Any: if self._async_file is not None: return getattr(self._async_file, attr) + raise AttributeError(f"{self.__class__.__name__} has no attribute {attr}") async def __aexit__( @@ -223,6 +226,7 @@ async def __aenter__(self) -> str: } if sys.version_info >= (3, 10): params["ignore_cleanup_errors"] = self.ignore_cleanup_errors + if sys.version_info >= (3, 12): params["delete"] = self.delete diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 978efab6..bd02963d 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -16,7 +16,6 @@ gettempdirb, mkdtemp, mkstemp, - to_thread, ) pytestmark = pytest.mark.anyio @@ -31,7 +30,6 @@ async def test_temporary_file(self) -> None: result = await af.read() assert result == data - assert af.closed @@ -47,7 +45,6 @@ async def test_named_temporary_file(self) -> None: result = await af.read() assert result == data - assert not os.path.exists(filename) async def test_exception_handling(self) -> None: @@ -149,13 +146,12 @@ async def test_mkstemp() -> None: with os.fdopen(fd, "w", encoding="utf-8") as f: f.write("mkstemp") - content = await to_thread.run_sync( - lambda: pathlib.Path(path).read_text(encoding="utf-8") - ) + with open(path, encoding="utf-8") as f: + content = f.read() assert content == "mkstemp" - await to_thread.run_sync(lambda: os.remove(path)) + os.remove(path) async def test_mkdtemp() -> None: From 2c89b610b2b3857b548a723c33095e8b3961c1df Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Thu, 13 Feb 2025 01:56:43 +0900 Subject: [PATCH 04/25] fix(tempfile): apply review feedback and improve async tempfile - Optimized to avoid unnecessary thread switching for small files - Fixed blank lines after control blocks for better readability - Resolved Pyright type errors - Updated async tempfile documentation based on review feedback --- docs/index.rst | 1 + docs/tempfile.rst | 119 +++++++++++++++++++++++++++++++++++ src/anyio/_core/_tempfile.py | 48 ++++++++++---- tests/test_tempfile.py | 41 ++++++++++++ 4 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 docs/tempfile.rst diff --git a/docs/index.rst b/docs/index.rst index c6d234ec..43f315d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,6 +20,7 @@ The manual subprocesses subinterpreters fileio + tempfile signals testing api diff --git a/docs/tempfile.rst b/docs/tempfile.rst new file mode 100644 index 00000000..8b788109 --- /dev/null +++ b/docs/tempfile.rst @@ -0,0 +1,119 @@ +Asynchronous Temporary File and Directory +========================================= + +.. py:currentmodule:: anyio + +This module provides asynchronous wrappers for handling temporary files and directories +using the :mod:`tempfile` module. The asynchronous methods execute blocking operations in worker threads. + +Temporary File +-------------- + +:class:`TemporaryFile` creates a temporary file that is automatically deleted upon closure. + +**Example:** + +.. code-block:: python + + from anyio import TemporaryFile, run + + async def main(): + async with TemporaryFile(mode="w+") as f: + await f.write("Temporary file content") + await f.seek(0) + print(await f.read()) # Output: Temporary file content + + run(main) + +Named Temporary File +-------------------- + +:class:`NamedTemporaryFile` works similarly to :class:`TemporaryFile`, but the file has a visible name in the filesystem. + +**Example:** + +.. code-block:: python + + from anyio import NamedTemporaryFile, run + + async def main(): + async with NamedTemporaryFile(mode="w+", delete=True) as f: + print(f"Temporary file name: {f.name}") + await f.write("Named temp file content") + await f.seek(0) + print(await f.read()) + + run(main) + +Spooled Temporary File +---------------------- + +:class:`SpooledTemporaryFile` is useful when temporary data is small and should be kept in memory rather than written to disk. + +**Example:** + +.. code-block:: python + + from anyio import SpooledTemporaryFile, run + + async def main(): + async with SpooledTemporaryFile(max_size=1024, mode="w+") as f: + await f.write("Spooled temp file content") + await f.seek(0) + print(await f.read()) + + run(main) + +Temporary Directory +------------------- + +The :class:`TemporaryDirectory` provides an asynchronous way to create temporary directories. + +**Example:** + +.. code-block:: python + + from anyio import TemporaryDirectory, run + + async def main(): + async with TemporaryDirectory() as temp_dir: + print(f"Temporary directory path: {temp_dir}") + + run(main) + +Low-Level Temporary File and Directory Creation +----------------------------------------------- + +For more control, the module provides lower-level functions: + +- :func:`mkstemp` - Creates a temporary file and returns a tuple of file descriptor and path. +- :func:`mkdtemp` - Creates a temporary directory and returns the directory path. +- :func:`gettempdir` - Returns the path of the default temporary directory. +- :func:`gettempdirb` - Returns the path of the default temporary directory in bytes. + +**Example:** + +.. code-block:: python + + from anyio import mkstemp, mkdtemp, gettempdir, run + import os + + async def main(): + fd, path = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) + print(f"Created temp file: {path}") + + temp_dir = await mkdtemp(prefix="mkdtemp_") + print(f"Created temp dir: {temp_dir}") + + print(f"Default temp dir: {await gettempdir()}") + + os.remove(path) + + run(main) + +.. note:: + Using these functions requires manual cleanup of the created files and directories. + +.. seealso:: + + - Python Standard Library: :mod:`tempfile` (`official documentation `_) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 26b185c9..8d3e2e2c 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -142,8 +142,8 @@ def __init__( self.prefix: AnyStr | None = prefix self.dir: AnyStr | None = dir self.errors = errors - self._async_file: AsyncFile | None = None + self._fp: Any = None async def __aenter__( self: SpooledTemporaryFile[AnyStr], @@ -162,26 +162,22 @@ async def __aenter__( errors=self.errors, ) ) + self._fp = fp self._async_file = AsyncFile(fp) return self async def rollover(self) -> None: - if self._async_file is None: - raise RuntimeError("Internal file is not initialized.") + if self._fp is None: + raise RuntimeError("Underlying file is not initialized.") - await to_thread.run_sync(cast(Callable[[], None], self._async_file.rollover)) + await to_thread.run_sync(cast(Callable[[], None], self._fp.rollover)) @property def closed(self) -> bool: - if self._async_file is not None: - try: - return bool(self._async_file.closed) - except AttributeError: - f = getattr(self._async_file, "_f", None) - if f is None: - return True - return bool(f.closed) - return True + if self._fp is None: + return True + + return bool(self._fp.closed) def __getattr__(self, attr: str) -> Any: if self._async_file is not None: @@ -189,6 +185,32 @@ def __getattr__(self, attr: str) -> Any: raise AttributeError(f"{self.__class__.__name__} has no attribute {attr}") + async def write(self, s: Any) -> int: + if self._fp is None: + raise RuntimeError("Underlying file is not initialized.") + + if not getattr(self._fp, "_rolled", True): + result = self._fp.write(s) + if self._fp._max_size and self._fp.tell() > self._fp._max_size: + self._fp.rollover() + + return result + else: + return await to_thread.run_sync(self._fp.write, s) + + async def writelines(self, lines: Any) -> None: + if self._fp is None: + raise RuntimeError("Underlying file is not initialized.") + + if not getattr(self._fp, "_rolled", True): + result = self._fp.writelines(lines) + if self._fp._max_size and self._fp.tell() > self._fp._max_size: + self._fp.rollover() + + return result + else: + return await to_thread.run_sync(self._fp.writelines, lines) + async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index bd02963d..eb91d7a6 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -97,6 +97,47 @@ async def test_rollover_handling(self) -> None: assert result == b"1234567890more data" + async def test_write_without_rolled(self) -> None: + async with SpooledTemporaryFile[bytes](max_size=10) as stf: + stf._fp._rolled = False + stf._fp._max_size = 10 + rollover_called = False + original_rollover = stf._fp.rollover + + def fake_rollover() -> None: + nonlocal rollover_called + rollover_called = True + return original_rollover() + + stf._fp.rollover = fake_rollover + n1 = await stf.write(b"12345") + assert n1 == 5 + assert not rollover_called + await stf.write(b"67890X") + assert rollover_called + + async def test_writelines_without_rolled(self) -> None: + async with SpooledTemporaryFile[bytes](max_size=20) as stf: + stf._fp._rolled = False + stf._fp._max_size = 20 + rollover_called = False + original_rollover = stf._fp.rollover + + def fake_rollover() -> None: + nonlocal rollover_called + rollover_called = True + return original_rollover() + + stf._fp.rollover = fake_rollover + await stf.writelines([b"hello", b"world"]) + await stf.seek(0) + + content = await stf.read() + assert content == b"helloworld" + + await stf.writelines([b"1234567890123456"]) + assert rollover_called + async def test_closed_state(self) -> None: async with SpooledTemporaryFile[bytes](max_size=10) as stf: assert not stf.closed From 2e6ea501cee18e14c68faf31d2360eaa70024dee Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sat, 15 Feb 2025 01:37:34 +0900 Subject: [PATCH 05/25] Update src/anyio/_core/_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- src/anyio/_core/_tempfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 8d3e2e2c..deab566e 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -195,8 +195,8 @@ async def write(self, s: Any) -> int: self._fp.rollover() return result - else: - return await to_thread.run_sync(self._fp.write, s) + + return await to_thread.run_sync(self._fp.write, s) async def writelines(self, lines: Any) -> None: if self._fp is None: From db3c2cd7899d72ecb5a6a9b49a8d38af7d1dfd0d Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sat, 15 Feb 2025 01:37:46 +0900 Subject: [PATCH 06/25] Update src/anyio/_core/_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- src/anyio/_core/_tempfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index deab566e..d39ba45d 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -208,8 +208,8 @@ async def writelines(self, lines: Any) -> None: self._fp.rollover() return result - else: - return await to_thread.run_sync(self._fp.writelines, lines) + + return await to_thread.run_sync(self._fp.writelines, lines) async def __aexit__( self, From ad12c8516307143474fba46d779893d5e4597444 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Sat, 15 Feb 2025 06:28:11 +0900 Subject: [PATCH 07/25] tempfile: update type annotations, fix Py3.14 compatibility, and update API docs - Improved type annotations for mkstemp and mkdtemp functions using overloads per Typeshed. - Fixed test failures on Python 3.14 caused by additional attributes in pathlib.Path. - Updated API reference to include new classes and functions. --- src/anyio/_core/_fileio.py | 17 +++++++++++++++++ src/anyio/_core/_tempfile.py | 36 +++++++++++++++++++++++++++++++++++- tests/test_tempfile.py | 9 --------- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/anyio/_core/_fileio.py b/src/anyio/_core/_fileio.py index 3728734c..f93dd22a 100644 --- a/src/anyio/_core/_fileio.py +++ b/src/anyio/_core/_fileio.py @@ -241,6 +241,9 @@ class Path: * :meth:`~pathlib.Path.relative_to` (the ``walk_up`` parameter is only available on Python 3.12 or later) * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) + * ``__open_rb__`` (available on Python 3.14 or later) + * ``__open_wb__`` (available on Python 3.14 or later) + * ``info`` (available on Python 3.14 or later) Any methods that do disk I/O need to be awaited on. These methods are: @@ -378,6 +381,20 @@ def suffixes(self) -> list[str]: def stem(self) -> str: return self._path.stem + if sys.version_info >= (3, 14): + + @property + def __open_rb__(self) -> Any: + return self._path.__open_rb__ + + @property + def __open_wb__(self) -> Any: + return self._path.__open_wb__ + + @property + def info(self) -> Any: + return self._path.info + async def absolute(self) -> Path: path = await to_thread.run_sync(self._path.absolute) return Path(path) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index d39ba45d..c482b6fd 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -4,7 +4,7 @@ import tempfile from functools import partial from types import TracebackType -from typing import Any, AnyStr, Callable, Generic, cast +from typing import Any, AnyStr, Callable, Generic, cast, overload from .. import to_thread from .._core._fileio import AsyncFile @@ -273,6 +273,24 @@ async def cleanup(self) -> None: await to_thread.run_sync(self._tempdir.cleanup) +@overload +async def mkstemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + text: bool = False, +) -> tuple[int, str]: ... + + +@overload +async def mkstemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, + text: bool = False, +) -> tuple[int, bytes]: ... + + async def mkstemp( suffix: AnyStr | None = None, prefix: AnyStr | None = None, @@ -282,6 +300,22 @@ async def mkstemp( return await to_thread.run_sync(lambda: tempfile.mkstemp(suffix, prefix, dir, text)) +@overload +async def mkdtemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, +) -> str: ... + + +@overload +async def mkdtemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, +) -> bytes: ... + + async def mkdtemp( suffix: AnyStr | None = None, prefix: AnyStr | None = None, diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index eb91d7a6..0b6b70e4 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -76,15 +76,6 @@ async def test_io_and_rollover(self) -> None: assert stf.closed - async def test_error_conditions(self) -> None: - stf = SpooledTemporaryFile[bytes]() - - with pytest.raises(RuntimeError): - await stf.rollover() - - with pytest.raises(AttributeError): - _ = stf.nonexistent_attribute - async def test_rollover_handling(self) -> None: async with SpooledTemporaryFile[bytes](max_size=10) as stf: await stf.write(b"1234567890") From b7a97221816bf76c3bb966f0d74b958be5bb38b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 15 Feb 2025 16:43:59 +0200 Subject: [PATCH 08/25] Removed redundant py3.14 fixes --- src/anyio/_core/_fileio.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/anyio/_core/_fileio.py b/src/anyio/_core/_fileio.py index 7de0dd0b..350a873a 100644 --- a/src/anyio/_core/_fileio.py +++ b/src/anyio/_core/_fileio.py @@ -242,9 +242,6 @@ class Path: * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available on Python 3.12 or later) * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) - * ``__open_rb__`` (available on Python 3.14 or later) - * ``__open_wb__`` (available on Python 3.14 or later) - * ``info`` (available on Python 3.14 or later) Any methods that do disk I/O need to be awaited on. These methods are: @@ -382,20 +379,6 @@ def suffixes(self) -> list[str]: def stem(self) -> str: return self._path.stem - if sys.version_info >= (3, 14): - - @property - def __open_rb__(self) -> Any: - return self._path.__open_rb__ - - @property - def __open_wb__(self) -> Any: - return self._path.__open_wb__ - - @property - def info(self) -> Any: - return self._path.info - async def absolute(self) -> Path: path = await to_thread.run_sync(self._path.absolute) return Path(path) From 2a1a1cf375f84d4efc8f707c36c3a07a806faa41 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:37:58 +0900 Subject: [PATCH 09/25] Update src/anyio/_core/_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- src/anyio/_core/_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index c482b6fd..1259bf8c 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -297,7 +297,7 @@ async def mkstemp( dir: AnyStr | None = None, text: bool = False, ) -> tuple[int, str | bytes]: - return await to_thread.run_sync(lambda: tempfile.mkstemp(suffix, prefix, dir, text)) + return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) @overload From e608cdcca90dff788a7a8ce15d2ce1eeda49f0f0 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:05 +0900 Subject: [PATCH 10/25] Update src/anyio/_core/_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- src/anyio/_core/_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 1259bf8c..a654db8b 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -321,7 +321,7 @@ async def mkdtemp( prefix: AnyStr | None = None, dir: AnyStr | None = None, ) -> str | bytes: - return await to_thread.run_sync(lambda: tempfile.mkdtemp(suffix, prefix, dir)) + return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) async def gettempdir() -> str: From 4cdadc67a7feff4c2f5674db3acc99d83e6ed25b Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:13 +0900 Subject: [PATCH 11/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 0b6b70e4..27d02ab7 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -194,7 +194,7 @@ async def test_mkdtemp() -> None: else: dp = pathlib.Path(d) - assert dp.exists() and dp.is_dir() + assert dp.is_dir() shutil.rmtree(dp) assert not dp.exists() From bd36d3cee10aa07b42859e56897d0d8a9b3693fe Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:21 +0900 Subject: [PATCH 12/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 27d02ab7..0c2eaf6b 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -197,7 +197,6 @@ async def test_mkdtemp() -> None: assert dp.is_dir() shutil.rmtree(dp) - assert not dp.exists() async def test_gettemp_functions() -> None: From 86719ecad1c66df8102a474681125db3597ef2dc Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:32 +0900 Subject: [PATCH 13/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 0c2eaf6b..3210e6ab 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -42,9 +42,8 @@ async def test_named_temporary_file(self) -> None: await af.write(data) await af.seek(0) - result = await af.read() + assert await af.read() == data - assert result == data assert not os.path.exists(filename) async def test_exception_handling(self) -> None: From c12905760be08501294adc776cc5d0d67c8c4178 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:38 +0900 Subject: [PATCH 14/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 3210e6ab..6ad64835 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -63,9 +63,7 @@ async def test_io_and_rollover(self) -> None: async with SpooledTemporaryFile[bytes](max_size=10) as stf: await stf.write(data) await stf.seek(0) - result = await stf.read() - - assert result == data + assert await stf.read() == data pos = await stf.tell() assert isinstance(pos, int) From 6551774dcf63917652038aa428d2bee20934a12f Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:47 +0900 Subject: [PATCH 15/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 6ad64835..71ddbd82 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -48,7 +48,7 @@ async def test_named_temporary_file(self) -> None: async def test_exception_handling(self) -> None: async with NamedTemporaryFile[bytes]() as af: - filename = str(af.name) + filename = af.name assert os.path.exists(filename) assert not os.path.exists(filename) From f730c722fed61ae6f43e7828ee6e2a1eecd2e4bd Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:38:56 +0900 Subject: [PATCH 16/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 71ddbd82..baf611dc 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -37,7 +37,7 @@ class TestNamedTemporaryFile: async def test_named_temporary_file(self) -> None: data = b"named temporary file data" async with NamedTemporaryFile[bytes]() as af: - filename = str(af.name) + filename = af.name assert os.path.exists(filename) await af.write(data) From 4dc6c7165efd6591c764ae8b59f2b77e0dc06fc5 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:39:08 +0900 Subject: [PATCH 17/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index baf611dc..2cb678ad 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -120,8 +120,7 @@ def fake_rollover() -> None: await stf.writelines([b"hello", b"world"]) await stf.seek(0) - content = await stf.read() - assert content == b"helloworld" + assert await stf.read() == b"helloworld" await stf.writelines([b"1234567890123456"]) assert rollover_called From bd6157217e49ae40ab8e483f3fbbda4b183e2b19 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Sun, 16 Feb 2025 22:39:14 +0900 Subject: [PATCH 18/25] Update tests/test_tempfile.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- tests/test_tempfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index 2cb678ad..b161f719 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -98,8 +98,7 @@ def fake_rollover() -> None: return original_rollover() stf._fp.rollover = fake_rollover - n1 = await stf.write(b"12345") - assert n1 == 5 + assert await stf.write(b"12345") == 5 assert not rollover_called await stf.write(b"67890X") assert rollover_called From 1509c28295c30c4489325c46d427acbc613b3009 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw@naver.com> Date: Mon, 17 Feb 2025 07:58:57 +0900 Subject: [PATCH 19/25] docs: Include TemporaryFile-related classes in API reference --- docs/api.rst | 17 +++++ src/anyio/_core/_tempfile.py | 122 +++++++++++++++++++++++++++++++++++ tests/test_tempfile.py | 53 ++++++++++----- 3 files changed, 177 insertions(+), 15 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5d2a6634..7f159a36 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -82,6 +82,23 @@ Async file I/O .. autoclass:: anyio.AsyncFile .. autoclass:: anyio.Path +Temporary files and directories +------------------------------- + +.. autofunction:: anyio.mkstemp +.. autofunction:: anyio.mkdtemp +.. autofunction:: anyio.gettempdir +.. autofunction:: anyio.gettempdirb + +.. autoclass:: anyio.TemporaryFile + +.. autoclass:: anyio.NamedTemporaryFile + +.. autoclass:: anyio.SpooledTemporaryFile + +.. autoclass:: anyio.TemporaryDirectory + + Streams and stream wrappers --------------------------- diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index a654db8b..f6180e3a 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -11,6 +11,23 @@ class TemporaryFile(Generic[AnyStr]): + """ + An asynchronous temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager interface to a temporary file. + The file is created using Python's standard `tempfile.TemporaryFile` function in a + background thread, and is wrapped as an asynchronous file using `AsyncFile`. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in text mode. + :param newline: Controls how universal newlines mode works (only applicable in text mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + def __init__( self, mode: str = "w+b", @@ -62,6 +79,25 @@ async def __aexit__( class NamedTemporaryFile(Generic[AnyStr]): + """ + An asynchronous named temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager for a temporary file with a + visible name in the file system. It uses Python's standard `tempfile.NamedTemporaryFile` + function and wraps the file object with `AsyncFile` for asynchronous operations. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in text mode. + :param newline: Controls how universal newlines mode works (only applicable in text mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param delete: Whether to delete the file when it is closed. + :param errors: The error handling scheme used for encoding/decoding errors. + :param delete_on_close: (Python 3.12+) Whether to delete the file on close. + """ + def __init__( self, mode: str = "w+b", @@ -120,6 +156,24 @@ async def __aexit__( class SpooledTemporaryFile(Generic[AnyStr]): + """ + An asynchronous spooled temporary file that starts in memory and is spooled to disk. + + This class provides an asynchronous interface to a spooled temporary file using + Python's standard `tempfile.SpooledTemporaryFile`. It supports asynchronous write + operations and provides a method to force a rollover to disk. + + :param max_size: Maximum size in bytes before the file is rolled over to disk. + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file (text mode only). + :param newline: Controls how universal newlines mode works (text mode only). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + def __init__( self, max_size: int = 0, @@ -186,6 +240,16 @@ def __getattr__(self, attr: str) -> Any: raise AttributeError(f"{self.__class__.__name__} has no attribute {attr}") async def write(self, s: Any) -> int: + """ + Asynchronously write data to the spooled temporary file. + + If the file has not yet been rolled over, the data is written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param s: The data to write. + :return: The number of bytes written. + :raises RuntimeError: If the underlying file is not initialized. + """ if self._fp is None: raise RuntimeError("Underlying file is not initialized.") @@ -199,6 +263,15 @@ async def write(self, s: Any) -> int: return await to_thread.run_sync(self._fp.write, s) async def writelines(self, lines: Any) -> None: + """ + Asynchronously write a list of lines to the spooled temporary file. + + If the file has not yet been rolled over, the lines are written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param lines: An iterable of lines to write. + :raises RuntimeError: If the underlying file is not initialized. + """ if self._fp is None: raise RuntimeError("Underlying file is not initialized.") @@ -223,6 +296,20 @@ async def __aexit__( class TemporaryDirectory(Generic[AnyStr]): + """ + An asynchronous temporary directory that is created and cleaned up automatically. + + This class provides an asynchronous context manager for creating a temporary directory. + It wraps Python's standard `tempfile.TemporaryDirectory` to perform directory creation + and cleanup operations in a background thread. + + :param suffix: Suffix to be added to the temporary directory name. + :param prefix: Prefix to be added to the temporary directory name. + :param dir: The parent directory where the temporary directory is created. + :param ignore_cleanup_errors: Whether to ignore errors during cleanup (Python 3.10+). + :param delete: Whether to delete the directory upon closing (Python 3.12+). + """ + def __init__( self, suffix: AnyStr | None = None, @@ -297,6 +384,17 @@ async def mkstemp( dir: AnyStr | None = None, text: bool = False, ) -> tuple[int, str | bytes]: + """ + Asynchronously create a temporary file and return an OS-level handle and the file name. + + This function wraps `tempfile.mkstemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the file name. + :param prefix: Prefix to be added to the file name. + :param dir: Directory in which the temporary file is created. + :param text: Whether the file is opened in text mode. + :return: A tuple containing the file descriptor and the file name. + """ return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) @@ -321,12 +419,36 @@ async def mkdtemp( prefix: AnyStr | None = None, dir: AnyStr | None = None, ) -> str | bytes: + """ + Asynchronously create a temporary directory and return its path. + + This function wraps `tempfile.mkdtemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the directory name. + :param prefix: Prefix to be added to the directory name. + :param dir: Parent directory where the temporary directory is created. + :return: The path of the created temporary directory. + """ return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) async def gettempdir() -> str: + """ + Asynchronously return the name of the directory used for temporary files. + + This function wraps `tempfile.gettempdir` and executes it in a background thread. + + :return: The path of the temporary directory as a string. + """ return await to_thread.run_sync(tempfile.gettempdir) async def gettempdirb() -> bytes: + """ + Asynchronously return the name of the directory used for temporary files in bytes. + + This function wraps `tempfile.gettempdirb` and executes it in a background thread. + + :return: The path of the temporary directory as bytes. + """ return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index b161f719..f80801c5 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -4,6 +4,7 @@ import pathlib import shutil import tempfile +from typing import AnyStr import pytest @@ -38,20 +39,20 @@ async def test_named_temporary_file(self) -> None: data = b"named temporary file data" async with NamedTemporaryFile[bytes]() as af: filename = af.name - assert os.path.exists(filename) + assert os.path.exists(filename) # type: ignore[arg-type] await af.write(data) await af.seek(0) assert await af.read() == data - assert not os.path.exists(filename) + assert not os.path.exists(filename) # type: ignore[arg-type] async def test_exception_handling(self) -> None: async with NamedTemporaryFile[bytes]() as af: filename = af.name - assert os.path.exists(filename) + assert os.path.exists(filename) # type: ignore[arg-type] - assert not os.path.exists(filename) + assert not os.path.exists(filename) # type: ignore[arg-type] with pytest.raises(ValueError): await af.write(b"should fail") @@ -165,24 +166,46 @@ async def test_exception_handling(self) -> None: (td_path / "nonexistent.txt").write_text("should fail", encoding="utf-8") -async def test_mkstemp() -> None: - fd, path = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) - assert isinstance(fd, int) - assert isinstance(path, str) +@pytest.mark.parametrize( + "suffix, prefix, text, content", + [ + (".txt", "mkstemp_", True, "mkstemp"), + (b".txt", b"mkstemp_", False, b"mkstemp"), + ], +) +async def test_mkstemp( + suffix: AnyStr, + prefix: AnyStr, + text: bool, + content: AnyStr, +) -> None: + fd, path = await mkstemp(suffix=suffix, prefix=prefix, text=text) - with os.fdopen(fd, "w", encoding="utf-8") as f: - f.write("mkstemp") + assert isinstance(fd, int) + if text: + assert isinstance(path, str) + else: + assert isinstance(path, bytes) - with open(path, encoding="utf-8") as f: - content = f.read() + if text: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(content) + with open(path, encoding="utf-8") as f: + read_content = f.read() + else: + with os.fdopen(fd, "wb") as f: + f.write(content) + with open(os.fsdecode(path), "rb") as f: + read_content = f.read() - assert content == "mkstemp" + assert read_content == content os.remove(path) -async def test_mkdtemp() -> None: - d = await mkdtemp(prefix="mkdtemp_") +@pytest.mark.parametrize("prefix", [b"mkdtemp_", "mkdtemp_"]) +async def test_mkdtemp(prefix: AnyStr) -> None: + d = await mkdtemp(prefix=prefix) if isinstance(d, bytes): dp = pathlib.Path(os.fsdecode(d)) From 78bb1d17c8b93921e9b28654d17f5396d6e3ca29 Mon Sep 17 00:00:00 2001 From: 11kkw <11kkw17@gmail.com> Date: Mon, 17 Feb 2025 08:14:30 +0900 Subject: [PATCH 20/25] api.rst update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Grönholm --- docs/api.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7f159a36..79cfff41 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -91,14 +91,10 @@ Temporary files and directories .. autofunction:: anyio.gettempdirb .. autoclass:: anyio.TemporaryFile - .. autoclass:: anyio.NamedTemporaryFile - .. autoclass:: anyio.SpooledTemporaryFile - .. autoclass:: anyio.TemporaryDirectory - Streams and stream wrappers --------------------------- From 77e7259400f4956323e632181ead57a0e68294b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Feb 2025 19:52:40 +0200 Subject: [PATCH 21/25] Reimplemented SpooledTemporaryFile and improved type annotations --- src/anyio/_core/_tempfile.py | 418 +++++++++++++++++++++++------------ 1 file changed, 282 insertions(+), 136 deletions(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index f6180e3a..bb3e16bb 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -1,14 +1,25 @@ from __future__ import annotations +import os import sys import tempfile -from functools import partial +from collections.abc import Iterable +from io import BytesIO, TextIOWrapper from types import TracebackType -from typing import Any, AnyStr, Callable, Generic, cast, overload +from typing import ( + TYPE_CHECKING, + Any, + AnyStr, + Generic, + overload, +) from .. import to_thread from .._core._fileio import AsyncFile +if TYPE_CHECKING: + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer + class TemporaryFile(Generic[AnyStr]): """ @@ -20,23 +31,54 @@ class TemporaryFile(Generic[AnyStr]): :param mode: The mode in which the file is opened. Defaults to "w+b". :param buffering: The buffering policy (-1 means the default buffering). - :param encoding: The encoding used to decode or encode the file. Only applicable in text mode. - :param newline: Controls how universal newlines mode works (only applicable in text mode). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). :param suffix: The suffix for the temporary file name. :param prefix: The prefix for the temporary file name. :param dir: The directory in which the temporary file is created. :param errors: The error handling scheme used for encoding/decoding errors. """ + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: TemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: TemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + def __init__( self, - mode: str = "w+b", + mode: OpenTextMode | OpenBinaryMode = "w+b", buffering: int = -1, encoding: str | None = None, newline: str | None = None, - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, *, errors: str | None = None, ) -> None: @@ -44,13 +86,11 @@ def __init__( self.buffering = buffering self.encoding = encoding self.newline = newline - self.suffix: AnyStr | None = suffix - self.prefix: AnyStr | None = prefix - self.dir: AnyStr | None = dir + self.suffix: str | None = suffix + self.prefix: str | None = prefix + self.dir: str | None = dir self.errors = errors - self._async_file: AsyncFile | None = None - async def __aenter__(self) -> AsyncFile[AnyStr]: fp = await to_thread.run_sync( lambda: tempfile.TemporaryFile( @@ -73,9 +113,7 @@ async def __aexit__( exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - if self._async_file is not None: - await self._async_file.aclose() - self._async_file = None + await self._async_file.aclose() class NamedTemporaryFile(Generic[AnyStr]): @@ -83,13 +121,16 @@ class NamedTemporaryFile(Generic[AnyStr]): An asynchronous named temporary file that is automatically created and cleaned up. This class provides an asynchronous context manager for a temporary file with a - visible name in the file system. It uses Python's standard `tempfile.NamedTemporaryFile` - function and wraps the file object with `AsyncFile` for asynchronous operations. + visible name in the file system. It uses Python's standard + :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with + :class:`AsyncFile` for asynchronous operations. :param mode: The mode in which the file is opened. Defaults to "w+b". :param buffering: The buffering policy (-1 means the default buffering). - :param encoding: The encoding used to decode or encode the file. Only applicable in text mode. - :param newline: Controls how universal newlines mode works (only applicable in text mode). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). :param suffix: The suffix for the temporary file name. :param prefix: The prefix for the temporary file name. :param dir: The directory in which the temporary file is created. @@ -98,49 +139,71 @@ class NamedTemporaryFile(Generic[AnyStr]): :param delete_on_close: (Python 3.12+) Whether to delete the file on close. """ + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: NamedTemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + @overload + def __init__( + self: NamedTemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + def __init__( self, - mode: str = "w+b", + mode: OpenBinaryMode | OpenTextMode = "w+b", buffering: int = -1, encoding: str | None = None, newline: str | None = None, - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, delete: bool = True, *, errors: str | None = None, delete_on_close: bool = True, ) -> None: - self.mode = mode - self.buffering = buffering - self.encoding = encoding - self.newline = newline - self.suffix: AnyStr | None = suffix - self.prefix: AnyStr | None = prefix - self.dir: AnyStr | None = dir - self.delete = delete - self.errors = errors - self.delete_on_close = delete_on_close - - self._async_file: AsyncFile | None = None - - async def __aenter__(self) -> AsyncFile[AnyStr]: - params: dict[str, Any] = { - "mode": self.mode, - "buffering": self.buffering, - "encoding": self.encoding, - "newline": self.newline, - "suffix": self.suffix, - "prefix": self.prefix, - "dir": self.dir, - "delete": self.delete, - "errors": self.errors, + self._params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "delete": delete, + "errors": errors, } if sys.version_info >= (3, 12): - params["delete_on_close"] = self.delete_on_close + self._params["delete_on_close"] = delete_on_close - fp = await to_thread.run_sync(lambda: tempfile.NamedTemporaryFile(**params)) + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.NamedTemporaryFile(**self._params) + ) self._async_file = AsyncFile(fp) return self._async_file @@ -150,18 +213,16 @@ async def __aexit__( exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - if self._async_file is not None: - await self._async_file.aclose() - self._async_file = None + await self._async_file.aclose() -class SpooledTemporaryFile(Generic[AnyStr]): +class SpooledTemporaryFile(AsyncFile[AnyStr]): """ An asynchronous spooled temporary file that starts in memory and is spooled to disk. - This class provides an asynchronous interface to a spooled temporary file using - Python's standard `tempfile.SpooledTemporaryFile`. It supports asynchronous write - operations and provides a method to force a rollover to disk. + This class provides an asynchronous interface to a spooled temporary file, much like + Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous + write operations and provides a method to force a rollover to disk. :param max_size: Maximum size in bytes before the file is rolled over to disk. :param mode: The mode in which the file is opened. Defaults to "w+b". @@ -174,72 +235,164 @@ class SpooledTemporaryFile(Generic[AnyStr]): :param errors: The error handling scheme used for encoding/decoding errors. """ + _rolled: bool = False + + @overload + def __init__( + self: SpooledTemporaryFile[bytes], + max_size: int = ..., + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: SpooledTemporaryFile[str], + max_size: int = ..., + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + def __init__( self, max_size: int = 0, - mode: str = "w+b", + mode: OpenBinaryMode | OpenTextMode = "w+b", buffering: int = -1, encoding: str | None = None, newline: str | None = None, - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, *, errors: str | None = None, ) -> None: - self.max_size = max_size - self.mode = mode - self.buffering = buffering - self.encoding = encoding - self.newline = newline - self.suffix: AnyStr | None = suffix - self.prefix: AnyStr | None = prefix - self.dir: AnyStr | None = dir - self.errors = errors - self._async_file: AsyncFile | None = None - self._fp: Any = None - - async def __aenter__( - self: SpooledTemporaryFile[AnyStr], - ) -> SpooledTemporaryFile[AnyStr]: - fp = await to_thread.run_sync( - partial( - tempfile.SpooledTemporaryFile, - self.max_size, - self.mode, - self.buffering, - self.encoding, - self.newline, - self.suffix, - self.prefix, - self.dir, - errors=self.errors, + self._tempfile_params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "errors": errors, + } + self._max_size = max_size + if "b" in mode: + super().__init__(BytesIO()) + else: + super().__init__( + TextIOWrapper( + BytesIO(), + encoding=encoding, + errors=errors, + newline=newline, + write_through=True, + ) ) - ) - self._fp = fp - self._async_file = AsyncFile(fp) - return self - async def rollover(self) -> None: - if self._fp is None: - raise RuntimeError("Underlying file is not initialized.") + async def aclose(self) -> None: + if not self._rolled: + self._fp.close() + return + + await super().aclose() + + async def _check(self) -> None: + if self._rolled or self._fp.tell() < self._max_size: + return - await to_thread.run_sync(cast(Callable[[], None], self._fp.rollover)) + await self.rollover() + + async def rollover(self) -> None: + if self._rolled: + return + + self._rolled = True + buffer = self._fp + buffer.seek(0) + self._fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile(**self._tempfile_params) + ) + await self.write(buffer.read()) + buffer.close() @property def closed(self) -> bool: - if self._fp is None: - return True + return self._fp.closed + + async def read(self, size: int = -1) -> AnyStr: + if not self._rolled: + return self._fp.read(size) + + return await super().read(size) + + async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: + if not self._rolled: + return self._fp.read1(size) - return bool(self._fp.closed) + return await super().read1(size) - def __getattr__(self, attr: str) -> Any: - if self._async_file is not None: - return getattr(self._async_file, attr) + async def readline(self) -> AnyStr: + if not self._rolled: + return self._fp.readline() - raise AttributeError(f"{self.__class__.__name__} has no attribute {attr}") + return await super().readline() - async def write(self, s: Any) -> int: + async def readlines(self) -> list[AnyStr]: + if not self._rolled: + return self._fp.readlines() + + return await super().readlines() + + async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + self._fp.readinto(b) + + return await super().readinto(b) + + async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + self._fp.readinto(b) + + return await super().readinto1(b) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + if not self._rolled: + return self._fp.seek(offset, whence) + + return await super().seek(offset, whence) + + async def tell(self) -> int: + if not self._rolled: + return self._fp.tell() + + return await super().tell() + + async def truncate(self, size: int | None = None) -> int: + if not self._rolled: + return self._fp.truncate(size) + + return await super().truncate(size) + + @overload + async def write(self: SpooledTemporaryFile[bytes], s: ReadableBuffer) -> int: ... + @overload + async def write(self: SpooledTemporaryFile[str], s: str) -> int: ... + + async def write(self, s: str | ReadableBuffer) -> int: """ Asynchronously write data to the spooled temporary file. @@ -249,20 +402,25 @@ async def write(self, s: Any) -> int: :param s: The data to write. :return: The number of bytes written. :raises RuntimeError: If the underlying file is not initialized. - """ - if self._fp is None: - raise RuntimeError("Underlying file is not initialized.") - if not getattr(self._fp, "_rolled", True): + """ + if not self._rolled: result = self._fp.write(s) - if self._fp._max_size and self._fp.tell() > self._fp._max_size: - self._fp.rollover() - + await self._check() return result - return await to_thread.run_sync(self._fp.write, s) + return await super().write(s) - async def writelines(self, lines: Any) -> None: + @overload + async def writelines( + self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + @overload + async def writelines( + self: SpooledTemporaryFile[str], lines: Iterable[str] + ) -> None: ... + + async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None: """ Asynchronously write a list of lines to the spooled temporary file. @@ -271,42 +429,29 @@ async def writelines(self, lines: Any) -> None: :param lines: An iterable of lines to write. :raises RuntimeError: If the underlying file is not initialized. - """ - if self._fp is None: - raise RuntimeError("Underlying file is not initialized.") - if not getattr(self._fp, "_rolled", True): + """ + if not self._rolled: result = self._fp.writelines(lines) - if self._fp._max_size and self._fp.tell() > self._fp._max_size: - self._fp.rollover() - + await self._check() return result - return await to_thread.run_sync(self._fp.writelines, lines) - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if self._async_file is not None: - await self._async_file.aclose() - self._async_file = None + return await super().writelines(lines) class TemporaryDirectory(Generic[AnyStr]): """ An asynchronous temporary directory that is created and cleaned up automatically. - This class provides an asynchronous context manager for creating a temporary directory. - It wraps Python's standard `tempfile.TemporaryDirectory` to perform directory creation - and cleanup operations in a background thread. + This class provides an asynchronous context manager for creating a temporary + directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to + perform directory creation and cleanup operations in a background thread. :param suffix: Suffix to be added to the temporary directory name. :param prefix: Prefix to be added to the temporary directory name. :param dir: The parent directory where the temporary directory is created. - :param ignore_cleanup_errors: Whether to ignore errors during cleanup (Python 3.10+). + :param ignore_cleanup_errors: Whether to ignore errors during cleanup + (Python 3.10+). :param delete: Whether to delete the directory upon closing (Python 3.12+). """ @@ -385,7 +530,8 @@ async def mkstemp( text: bool = False, ) -> tuple[int, str | bytes]: """ - Asynchronously create a temporary file and return an OS-level handle and the file name. + Asynchronously create a temporary file and return an OS-level handle and the file + name. This function wraps `tempfile.mkstemp` and executes it in a background thread. From 058ab8a23d971a5fcec1b768368995d48f3dee75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Feb 2025 19:53:55 +0200 Subject: [PATCH 22/25] Updated tests --- tests/test_tempfile.py | 72 ++++++++++++------------------------------ 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index f80801c5..ca853853 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -59,74 +59,44 @@ async def test_exception_handling(self) -> None: class TestSpooledTemporaryFile: - async def test_io_and_rollover(self) -> None: - data = b"spooled temporary file data" * 3 - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - await stf.write(data) - await stf.seek(0) - assert await stf.read() == data - - pos = await stf.tell() - assert isinstance(pos, int) - - await stf.rollover() - assert not stf.closed + async def test_writewithout_rolled(self) -> None: + rollover_called = False - assert stf.closed - - async def test_rollover_handling(self) -> None: - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - await stf.write(b"1234567890") - await stf.rollover() - assert not stf.closed + async def fake_rollover() -> None: + nonlocal rollover_called + rollover_called = True + await original_rollover() - await stf.write(b"more data") - await stf.seek(0) - result = await stf.read() - - assert result == b"1234567890more data" - - async def test_write_without_rolled(self) -> None: - async with SpooledTemporaryFile[bytes](max_size=10) as stf: - stf._fp._rolled = False - stf._fp._max_size = 10 - rollover_called = False - original_rollover = stf._fp.rollover - - def fake_rollover() -> None: - nonlocal rollover_called - rollover_called = True - return original_rollover() - - stf._fp.rollover = fake_rollover + async with SpooledTemporaryFile(max_size=10) as stf: + original_rollover = stf.rollover + stf.rollover = fake_rollover assert await stf.write(b"12345") == 5 assert not rollover_called + await stf.write(b"67890X") assert rollover_called - async def test_writelines_without_rolled(self) -> None: - async with SpooledTemporaryFile[bytes](max_size=20) as stf: - stf._fp._rolled = False - stf._fp._max_size = 20 - rollover_called = False - original_rollover = stf._fp.rollover + async def test_writelines(self) -> None: + rollover_called = False - def fake_rollover() -> None: - nonlocal rollover_called - rollover_called = True - return original_rollover() + async def fake_rollover() -> None: + nonlocal rollover_called + rollover_called = True + await original_rollover() - stf._fp.rollover = fake_rollover + async with SpooledTemporaryFile(max_size=20) as stf: + original_rollover = stf.rollover + stf.rollover = fake_rollover await stf.writelines([b"hello", b"world"]) + assert not rollover_called await stf.seek(0) assert await stf.read() == b"helloworld" - await stf.writelines([b"1234567890123456"]) assert rollover_called async def test_closed_state(self) -> None: - async with SpooledTemporaryFile[bytes](max_size=10) as stf: + async with SpooledTemporaryFile(max_size=10) as stf: assert not stf.closed assert stf.closed From e88499ebc7df93821e8f117796c816b5f444afca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 1 Mar 2025 19:01:15 +0200 Subject: [PATCH 23/25] Reduced the number of mypy errors --- src/anyio/_core/_tempfile.py | 6 +++--- tests/test_tempfile.py | 27 ++++++++++++++------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index bb3e16bb..42b7e89c 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -339,7 +339,7 @@ async def read(self, size: int = -1) -> AnyStr: return await super().read(size) - async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: + async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: if not self._rolled: return self._fp.read1(size) @@ -357,13 +357,13 @@ async def readlines(self) -> list[AnyStr]: return await super().readlines() - async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: if not self._rolled: self._fp.readinto(b) return await super().readinto(b) - async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: if not self._rolled: self._fp.readinto(b) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index ca853853..9d836090 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -5,6 +5,7 @@ import shutil import tempfile from typing import AnyStr +from unittest.mock import patch import pytest @@ -69,12 +70,12 @@ async def fake_rollover() -> None: async with SpooledTemporaryFile(max_size=10) as stf: original_rollover = stf.rollover - stf.rollover = fake_rollover - assert await stf.write(b"12345") == 5 - assert not rollover_called + with patch.object(stf, "rollover", fake_rollover): + assert await stf.write(b"12345") == 5 + assert not rollover_called - await stf.write(b"67890X") - assert rollover_called + await stf.write(b"67890X") + assert rollover_called async def test_writelines(self) -> None: rollover_called = False @@ -86,14 +87,14 @@ async def fake_rollover() -> None: async with SpooledTemporaryFile(max_size=20) as stf: original_rollover = stf.rollover - stf.rollover = fake_rollover - await stf.writelines([b"hello", b"world"]) - assert not rollover_called - await stf.seek(0) - - assert await stf.read() == b"helloworld" - await stf.writelines([b"1234567890123456"]) - assert rollover_called + with patch.object(stf, "rollover", fake_rollover): + await stf.writelines([b"hello", b"world"]) + assert not rollover_called + await stf.seek(0) + + assert await stf.read() == b"helloworld" + await stf.writelines([b"1234567890123456"]) + assert rollover_called async def test_closed_state(self) -> None: async with SpooledTemporaryFile(max_size=10) as stf: From e57dd1d81628f96b6706a7459e3da03b7ba7bc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 2 Mar 2025 00:45:37 +0200 Subject: [PATCH 24/25] Silenced the remaining mypy errors --- src/anyio/_core/_tempfile.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 42b7e89c..103e1b5f 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -291,10 +291,10 @@ def __init__( } self._max_size = max_size if "b" in mode: - super().__init__(BytesIO()) + super().__init__(BytesIO()) # type: ignore[arg-type] else: super().__init__( - TextIOWrapper( + TextIOWrapper( # type: ignore[arg-type] BytesIO(), encoding=encoding, errors=errors, @@ -337,7 +337,7 @@ async def read(self, size: int = -1) -> AnyStr: if not self._rolled: return self._fp.read(size) - return await super().read(size) + return await super().read(size) # type: ignore[return-value] async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: if not self._rolled: @@ -349,13 +349,13 @@ async def readline(self) -> AnyStr: if not self._rolled: return self._fp.readline() - return await super().readline() + return await super().readline() # type: ignore[return-value] async def readlines(self) -> list[AnyStr]: if not self._rolled: return self._fp.readlines() - return await super().readlines() + return await super().readlines() # type: ignore[return-value] async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: if not self._rolled: @@ -388,11 +388,11 @@ async def truncate(self, size: int | None = None) -> int: return await super().truncate(size) @overload - async def write(self: SpooledTemporaryFile[bytes], s: ReadableBuffer) -> int: ... + async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ... @overload - async def write(self: SpooledTemporaryFile[str], s: str) -> int: ... + async def write(self: SpooledTemporaryFile[str], b: str) -> int: ... - async def write(self, s: str | ReadableBuffer) -> int: + async def write(self, b: ReadableBuffer | str) -> int: """ Asynchronously write data to the spooled temporary file. @@ -405,11 +405,11 @@ async def write(self, s: str | ReadableBuffer) -> int: """ if not self._rolled: - result = self._fp.write(s) + result = self._fp.write(b) await self._check() return result - return await super().write(s) + return await super().write(b) # type: ignore[misc] @overload async def writelines( @@ -436,7 +436,7 @@ async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> N await self._check() return result - return await super().writelines(lines) + return await super().writelines(lines) # type: ignore[misc] class TemporaryDirectory(Generic[AnyStr]): From 7d8f4b7a460d2ea4518e4265aa4b1a06d164deb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 2 Mar 2025 23:10:33 +0200 Subject: [PATCH 25/25] Added some finishing touches --- src/anyio/_core/_tempfile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/anyio/_core/_tempfile.py b/src/anyio/_core/_tempfile.py index 103e1b5f..26d70eca 100644 --- a/src/anyio/_core/_tempfile.py +++ b/src/anyio/_core/_tempfile.py @@ -16,6 +16,7 @@ from .. import to_thread from .._core._fileio import AsyncFile +from ..lowlevel import checkpoint_if_cancelled if TYPE_CHECKING: from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer @@ -335,54 +336,63 @@ def closed(self) -> bool: async def read(self, size: int = -1) -> AnyStr: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.read(size) return await super().read(size) # type: ignore[return-value] async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.read1(size) return await super().read1(size) async def readline(self) -> AnyStr: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.readline() return await super().readline() # type: ignore[return-value] async def readlines(self) -> list[AnyStr]: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.readlines() return await super().readlines() # type: ignore[return-value] async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: if not self._rolled: + await checkpoint_if_cancelled() self._fp.readinto(b) return await super().readinto(b) async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: if not self._rolled: + await checkpoint_if_cancelled() self._fp.readinto(b) return await super().readinto1(b) async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.seek(offset, whence) return await super().seek(offset, whence) async def tell(self) -> int: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.tell() return await super().tell() async def truncate(self, size: int | None = None) -> int: if not self._rolled: + await checkpoint_if_cancelled() return self._fp.truncate(size) return await super().truncate(size) @@ -405,6 +415,7 @@ async def write(self, b: ReadableBuffer | str) -> int: """ if not self._rolled: + await checkpoint_if_cancelled() result = self._fp.write(b) await self._check() return result @@ -432,6 +443,7 @@ async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> N """ if not self._rolled: + await checkpoint_if_cancelled() result = self._fp.writelines(lines) await self._check() return result @@ -540,6 +552,7 @@ async def mkstemp( :param dir: Directory in which the temporary file is created. :param text: Whether the file is opened in text mode. :return: A tuple containing the file descriptor and the file name. + """ return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) @@ -574,6 +587,7 @@ async def mkdtemp( :param prefix: Prefix to be added to the directory name. :param dir: Parent directory where the temporary directory is created. :return: The path of the created temporary directory. + """ return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) @@ -585,6 +599,7 @@ async def gettempdir() -> str: This function wraps `tempfile.gettempdir` and executes it in a background thread. :return: The path of the temporary directory as a string. + """ return await to_thread.run_sync(tempfile.gettempdir) @@ -596,5 +611,6 @@ async def gettempdirb() -> bytes: This function wraps `tempfile.gettempdirb` and executes it in a background thread. :return: The path of the temporary directory as bytes. + """ return await to_thread.run_sync(tempfile.gettempdirb)