Format: text/x-python
⬇ Download File (19.2 KB)
   1  from __future__ import annotations
   2  
   3  import os
   4  import sys
   5  import tempfile
   6  from collections.abc import Iterable
   7  from io import BytesIO, TextIOWrapper
   8  from types import TracebackType
   9  from typing import (
  10      TYPE_CHECKING,
  11      Any,
  12      AnyStr,
  13      Generic,
  14      overload,
  15  )
  16  
  17  from .. import to_thread
  18  from .._core._fileio import AsyncFile
  19  from ..lowlevel import checkpoint_if_cancelled
  20  
  21  if TYPE_CHECKING:
  22      from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
  23  
  24  
  25  class TemporaryFile(Generic[AnyStr]):
  26      """
  27      An asynchronous temporary file that is automatically created and cleaned up.
  28  
  29      This class provides an asynchronous context manager interface to a temporary file.
  30      The file is created using Python's standard `tempfile.TemporaryFile` function in a
  31      background thread, and is wrapped as an asynchronous file using `AsyncFile`.
  32  
  33      :param mode: The mode in which the file is opened. Defaults to "w+b".
  34      :param buffering: The buffering policy (-1 means the default buffering).
  35      :param encoding: The encoding used to decode or encode the file. Only applicable in
  36          text mode.
  37      :param newline: Controls how universal newlines mode works (only applicable in text
  38          mode).
  39      :param suffix: The suffix for the temporary file name.
  40      :param prefix: The prefix for the temporary file name.
  41      :param dir: The directory in which the temporary file is created.
  42      :param errors: The error handling scheme used for encoding/decoding errors.
  43      """
  44  
  45      _async_file: AsyncFile[AnyStr]
  46  
  47      @overload
  48      def __init__(
  49          self: TemporaryFile[bytes],
  50          mode: OpenBinaryMode = ...,
  51          buffering: int = ...,
  52          encoding: str | None = ...,
  53          newline: str | None = ...,
  54          suffix: str | None = ...,
  55          prefix: str | None = ...,
  56          dir: str | None = ...,
  57          *,
  58          errors: str | None = ...,
  59      ): ...
  60      @overload
  61      def __init__(
  62          self: TemporaryFile[str],
  63          mode: OpenTextMode,
  64          buffering: int = ...,
  65          encoding: str | None = ...,
  66          newline: str | None = ...,
  67          suffix: str | None = ...,
  68          prefix: str | None = ...,
  69          dir: str | None = ...,
  70          *,
  71          errors: str | None = ...,
  72      ): ...
  73  
  74      def __init__(
  75          self,
  76          mode: OpenTextMode | OpenBinaryMode = "w+b",
  77          buffering: int = -1,
  78          encoding: str | None = None,
  79          newline: str | None = None,
  80          suffix: str | None = None,
  81          prefix: str | None = None,
  82          dir: str | None = None,
  83          *,
  84          errors: str | None = None,
  85      ) -> None:
  86          self.mode = mode
  87          self.buffering = buffering
  88          self.encoding = encoding
  89          self.newline = newline
  90          self.suffix: str | None = suffix
  91          self.prefix: str | None = prefix
  92          self.dir: str | None = dir
  93          self.errors = errors
  94  
  95      async def __aenter__(self) -> AsyncFile[AnyStr]:
  96          fp = await to_thread.run_sync(
  97              lambda: tempfile.TemporaryFile(
  98                  self.mode,
  99                  self.buffering,
 100                  self.encoding,
 101                  self.newline,
 102                  self.suffix,
 103                  self.prefix,
 104                  self.dir,
 105                  errors=self.errors,
 106              )
 107          )
 108          self._async_file = AsyncFile(fp)
 109          return self._async_file
 110  
 111      async def __aexit__(
 112          self,
 113          exc_type: type[BaseException] | None,
 114          exc_value: BaseException | None,
 115          traceback: TracebackType | None,
 116      ) -> None:
 117          await self._async_file.aclose()
 118  
 119  
 120  class NamedTemporaryFile(Generic[AnyStr]):
 121      """
 122      An asynchronous named temporary file that is automatically created and cleaned up.
 123  
 124      This class provides an asynchronous context manager for a temporary file with a
 125      visible name in the file system. It uses Python's standard
 126      :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with
 127      :class:`AsyncFile` for asynchronous operations.
 128  
 129      :param mode: The mode in which the file is opened. Defaults to "w+b".
 130      :param buffering: The buffering policy (-1 means the default buffering).
 131      :param encoding: The encoding used to decode or encode the file. Only applicable in
 132          text mode.
 133      :param newline: Controls how universal newlines mode works (only applicable in text
 134          mode).
 135      :param suffix: The suffix for the temporary file name.
 136      :param prefix: The prefix for the temporary file name.
 137      :param dir: The directory in which the temporary file is created.
 138      :param delete: Whether to delete the file when it is closed.
 139      :param errors: The error handling scheme used for encoding/decoding errors.
 140      :param delete_on_close: (Python 3.12+) Whether to delete the file on close.
 141      """
 142  
 143      _async_file: AsyncFile[AnyStr]
 144  
 145      @overload
 146      def __init__(
 147          self: NamedTemporaryFile[bytes],
 148          mode: OpenBinaryMode = ...,
 149          buffering: int = ...,
 150          encoding: str | None = ...,
 151          newline: str | None = ...,
 152          suffix: str | None = ...,
 153          prefix: str | None = ...,
 154          dir: str | None = ...,
 155          delete: bool = ...,
 156          *,
 157          errors: str | None = ...,
 158          delete_on_close: bool = ...,
 159      ): ...
 160      @overload
 161      def __init__(
 162          self: NamedTemporaryFile[str],
 163          mode: OpenTextMode,
 164          buffering: int = ...,
 165          encoding: str | None = ...,
 166          newline: str | None = ...,
 167          suffix: str | None = ...,
 168          prefix: str | None = ...,
 169          dir: str | None = ...,
 170          delete: bool = ...,
 171          *,
 172          errors: str | None = ...,
 173          delete_on_close: bool = ...,
 174      ): ...
 175  
 176      def __init__(
 177          self,
 178          mode: OpenBinaryMode | OpenTextMode = "w+b",
 179          buffering: int = -1,
 180          encoding: str | None = None,
 181          newline: str | None = None,
 182          suffix: str | None = None,
 183          prefix: str | None = None,
 184          dir: str | None = None,
 185          delete: bool = True,
 186          *,
 187          errors: str | None = None,
 188          delete_on_close: bool = True,
 189      ) -> None:
 190          self._params: dict[str, Any] = {
 191              "mode": mode,
 192              "buffering": buffering,
 193              "encoding": encoding,
 194              "newline": newline,
 195              "suffix": suffix,
 196              "prefix": prefix,
 197              "dir": dir,
 198              "delete": delete,
 199              "errors": errors,
 200          }
 201          if sys.version_info >= (3, 12):
 202              self._params["delete_on_close"] = delete_on_close
 203  
 204      async def __aenter__(self) -> AsyncFile[AnyStr]:
 205          fp = await to_thread.run_sync(
 206              lambda: tempfile.NamedTemporaryFile(**self._params)
 207          )
 208          self._async_file = AsyncFile(fp)
 209          return self._async_file
 210  
 211      async def __aexit__(
 212          self,
 213          exc_type: type[BaseException] | None,
 214          exc_value: BaseException | None,
 215          traceback: TracebackType | None,
 216      ) -> None:
 217          await self._async_file.aclose()
 218  
 219  
 220  class SpooledTemporaryFile(AsyncFile[AnyStr]):
 221      """
 222      An asynchronous spooled temporary file that starts in memory and is spooled to disk.
 223  
 224      This class provides an asynchronous interface to a spooled temporary file, much like
 225      Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous
 226      write operations and provides a method to force a rollover to disk.
 227  
 228      :param max_size: Maximum size in bytes before the file is rolled over to disk.
 229      :param mode: The mode in which the file is opened. Defaults to "w+b".
 230      :param buffering: The buffering policy (-1 means the default buffering).
 231      :param encoding: The encoding used to decode or encode the file (text mode only).
 232      :param newline: Controls how universal newlines mode works (text mode only).
 233      :param suffix: The suffix for the temporary file name.
 234      :param prefix: The prefix for the temporary file name.
 235      :param dir: The directory in which the temporary file is created.
 236      :param errors: The error handling scheme used for encoding/decoding errors.
 237      """
 238  
 239      _rolled: bool = False
 240  
 241      @overload
 242      def __init__(
 243          self: SpooledTemporaryFile[bytes],
 244          max_size: int = ...,
 245          mode: OpenBinaryMode = ...,
 246          buffering: int = ...,
 247          encoding: str | None = ...,
 248          newline: str | None = ...,
 249          suffix: str | None = ...,
 250          prefix: str | None = ...,
 251          dir: str | None = ...,
 252          *,
 253          errors: str | None = ...,
 254      ): ...
 255      @overload
 256      def __init__(
 257          self: SpooledTemporaryFile[str],
 258          max_size: int = ...,
 259          mode: OpenTextMode = ...,
 260          buffering: int = ...,
 261          encoding: str | None = ...,
 262          newline: str | None = ...,
 263          suffix: str | None = ...,
 264          prefix: str | None = ...,
 265          dir: str | None = ...,
 266          *,
 267          errors: str | None = ...,
 268      ): ...
 269  
 270      def __init__(
 271          self,
 272          max_size: int = 0,
 273          mode: OpenBinaryMode | OpenTextMode = "w+b",
 274          buffering: int = -1,
 275          encoding: str | None = None,
 276          newline: str | None = None,
 277          suffix: str | None = None,
 278          prefix: str | None = None,
 279          dir: str | None = None,
 280          *,
 281          errors: str | None = None,
 282      ) -> None:
 283          self._tempfile_params: dict[str, Any] = {
 284              "mode": mode,
 285              "buffering": buffering,
 286              "encoding": encoding,
 287              "newline": newline,
 288              "suffix": suffix,
 289              "prefix": prefix,
 290              "dir": dir,
 291              "errors": errors,
 292          }
 293          self._max_size = max_size
 294          if "b" in mode:
 295              super().__init__(BytesIO())  # type: ignore[arg-type]
 296          else:
 297              super().__init__(
 298                  TextIOWrapper(  # type: ignore[arg-type]
 299                      BytesIO(),
 300                      encoding=encoding,
 301                      errors=errors,
 302                      newline=newline,
 303                      write_through=True,
 304                  )
 305              )
 306  
 307      async def aclose(self) -> None:
 308          if not self._rolled:
 309              self._fp.close()
 310              return
 311  
 312          await super().aclose()
 313  
 314      async def _check(self) -> None:
 315          if self._rolled or self._fp.tell() < self._max_size:
 316              return
 317  
 318          await self.rollover()
 319  
 320      async def rollover(self) -> None:
 321          if self._rolled:
 322              return
 323  
 324          self._rolled = True
 325          buffer = self._fp
 326          buffer.seek(0)
 327          self._fp = await to_thread.run_sync(
 328              lambda: tempfile.TemporaryFile(**self._tempfile_params)
 329          )
 330          await self.write(buffer.read())
 331          buffer.close()
 332  
 333      @property
 334      def closed(self) -> bool:
 335          return self._fp.closed
 336  
 337      async def read(self, size: int = -1) -> AnyStr:
 338          if not self._rolled:
 339              await checkpoint_if_cancelled()
 340              return self._fp.read(size)
 341  
 342          return await super().read(size)  # type: ignore[return-value]
 343  
 344      async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes:
 345          if not self._rolled:
 346              await checkpoint_if_cancelled()
 347              return self._fp.read1(size)
 348  
 349          return await super().read1(size)
 350  
 351      async def readline(self) -> AnyStr:
 352          if not self._rolled:
 353              await checkpoint_if_cancelled()
 354              return self._fp.readline()
 355  
 356          return await super().readline()  # type: ignore[return-value]
 357  
 358      async def readlines(self) -> list[AnyStr]:
 359          if not self._rolled:
 360              await checkpoint_if_cancelled()
 361              return self._fp.readlines()
 362  
 363          return await super().readlines()  # type: ignore[return-value]
 364  
 365      async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
 366          if not self._rolled:
 367              await checkpoint_if_cancelled()
 368              self._fp.readinto(b)
 369  
 370          return await super().readinto(b)
 371  
 372      async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
 373          if not self._rolled:
 374              await checkpoint_if_cancelled()
 375              self._fp.readinto(b)
 376  
 377          return await super().readinto1(b)
 378  
 379      async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
 380          if not self._rolled:
 381              await checkpoint_if_cancelled()
 382              return self._fp.seek(offset, whence)
 383  
 384          return await super().seek(offset, whence)
 385  
 386      async def tell(self) -> int:
 387          if not self._rolled:
 388              await checkpoint_if_cancelled()
 389              return self._fp.tell()
 390  
 391          return await super().tell()
 392  
 393      async def truncate(self, size: int | None = None) -> int:
 394          if not self._rolled:
 395              await checkpoint_if_cancelled()
 396              return self._fp.truncate(size)
 397  
 398          return await super().truncate(size)
 399  
 400      @overload
 401      async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ...
 402      @overload
 403      async def write(self: SpooledTemporaryFile[str], b: str) -> int: ...
 404  
 405      async def write(self, b: ReadableBuffer | str) -> int:
 406          """
 407          Asynchronously write data to the spooled temporary file.
 408  
 409          If the file has not yet been rolled over, the data is written synchronously,
 410          and a rollover is triggered if the size exceeds the maximum size.
 411  
 412          :param s: The data to write.
 413          :return: The number of bytes written.
 414          :raises RuntimeError: If the underlying file is not initialized.
 415  
 416          """
 417          if not self._rolled:
 418              await checkpoint_if_cancelled()
 419              result = self._fp.write(b)
 420              await self._check()
 421              return result
 422  
 423          return await super().write(b)  # type: ignore[misc]
 424  
 425      @overload
 426      async def writelines(
 427          self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer]
 428      ) -> None: ...
 429      @overload
 430      async def writelines(
 431          self: SpooledTemporaryFile[str], lines: Iterable[str]
 432      ) -> None: ...
 433  
 434      async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None:
 435          """
 436          Asynchronously write a list of lines to the spooled temporary file.
 437  
 438          If the file has not yet been rolled over, the lines are written synchronously,
 439          and a rollover is triggered if the size exceeds the maximum size.
 440  
 441          :param lines: An iterable of lines to write.
 442          :raises RuntimeError: If the underlying file is not initialized.
 443  
 444          """
 445          if not self._rolled:
 446              await checkpoint_if_cancelled()
 447              result = self._fp.writelines(lines)
 448              await self._check()
 449              return result
 450  
 451          return await super().writelines(lines)  # type: ignore[misc]
 452  
 453  
 454  class TemporaryDirectory(Generic[AnyStr]):
 455      """
 456      An asynchronous temporary directory that is created and cleaned up automatically.
 457  
 458      This class provides an asynchronous context manager for creating a temporary
 459      directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to
 460      perform directory creation and cleanup operations in a background thread.
 461  
 462      :param suffix: Suffix to be added to the temporary directory name.
 463      :param prefix: Prefix to be added to the temporary directory name.
 464      :param dir: The parent directory where the temporary directory is created.
 465      :param ignore_cleanup_errors: Whether to ignore errors during cleanup
 466          (Python 3.10+).
 467      :param delete: Whether to delete the directory upon closing (Python 3.12+).
 468      """
 469  
 470      def __init__(
 471          self,
 472          suffix: AnyStr | None = None,
 473          prefix: AnyStr | None = None,
 474          dir: AnyStr | None = None,
 475          *,
 476          ignore_cleanup_errors: bool = False,
 477          delete: bool = True,
 478      ) -> None:
 479          self.suffix: AnyStr | None = suffix
 480          self.prefix: AnyStr | None = prefix
 481          self.dir: AnyStr | None = dir
 482          self.ignore_cleanup_errors = ignore_cleanup_errors
 483          self.delete = delete
 484  
 485          self._tempdir: tempfile.TemporaryDirectory | None = None
 486  
 487      async def __aenter__(self) -> str:
 488          params: dict[str, Any] = {
 489              "suffix": self.suffix,
 490              "prefix": self.prefix,
 491              "dir": self.dir,
 492          }
 493          if sys.version_info >= (3, 10):
 494              params["ignore_cleanup_errors"] = self.ignore_cleanup_errors
 495  
 496          if sys.version_info >= (3, 12):
 497              params["delete"] = self.delete
 498  
 499          self._tempdir = await to_thread.run_sync(
 500              lambda: tempfile.TemporaryDirectory(**params)
 501          )
 502          return await to_thread.run_sync(self._tempdir.__enter__)
 503  
 504      async def __aexit__(
 505          self,
 506          exc_type: type[BaseException] | None,
 507          exc_value: BaseException | None,
 508          traceback: TracebackType | None,
 509      ) -> None:
 510          if self._tempdir is not None:
 511              await to_thread.run_sync(
 512                  self._tempdir.__exit__, exc_type, exc_value, traceback
 513              )
 514  
 515      async def cleanup(self) -> None:
 516          if self._tempdir is not None:
 517              await to_thread.run_sync(self._tempdir.cleanup)
 518  
 519  
 520  @overload
 521  async def mkstemp(
 522      suffix: str | None = None,
 523      prefix: str | None = None,
 524      dir: str | None = None,
 525      text: bool = False,
 526  ) -> tuple[int, str]: ...
 527  
 528  
 529  @overload
 530  async def mkstemp(
 531      suffix: bytes | None = None,
 532      prefix: bytes | None = None,
 533      dir: bytes | None = None,
 534      text: bool = False,
 535  ) -> tuple[int, bytes]: ...
 536  
 537  
 538  async def mkstemp(
 539      suffix: AnyStr | None = None,
 540      prefix: AnyStr | None = None,
 541      dir: AnyStr | None = None,
 542      text: bool = False,
 543  ) -> tuple[int, str | bytes]:
 544      """
 545      Asynchronously create a temporary file and return an OS-level handle and the file
 546      name.
 547  
 548      This function wraps `tempfile.mkstemp` and executes it in a background thread.
 549  
 550      :param suffix: Suffix to be added to the file name.
 551      :param prefix: Prefix to be added to the file name.
 552      :param dir: Directory in which the temporary file is created.
 553      :param text: Whether the file is opened in text mode.
 554      :return: A tuple containing the file descriptor and the file name.
 555  
 556      """
 557      return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text)
 558  
 559  
 560  @overload
 561  async def mkdtemp(
 562      suffix: str | None = None,
 563      prefix: str | None = None,
 564      dir: str | None = None,
 565  ) -> str: ...
 566  
 567  
 568  @overload
 569  async def mkdtemp(
 570      suffix: bytes | None = None,
 571      prefix: bytes | None = None,
 572      dir: bytes | None = None,
 573  ) -> bytes: ...
 574  
 575  
 576  async def mkdtemp(
 577      suffix: AnyStr | None = None,
 578      prefix: AnyStr | None = None,
 579      dir: AnyStr | None = None,
 580  ) -> str | bytes:
 581      """
 582      Asynchronously create a temporary directory and return its path.
 583  
 584      This function wraps `tempfile.mkdtemp` and executes it in a background thread.
 585  
 586      :param suffix: Suffix to be added to the directory name.
 587      :param prefix: Prefix to be added to the directory name.
 588      :param dir: Parent directory where the temporary directory is created.
 589      :return: The path of the created temporary directory.
 590  
 591      """
 592      return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir)
 593  
 594  
 595  async def gettempdir() -> str:
 596      """
 597      Asynchronously return the name of the directory used for temporary files.
 598  
 599      This function wraps `tempfile.gettempdir` and executes it in a background thread.
 600  
 601      :return: The path of the temporary directory as a string.
 602  
 603      """
 604      return await to_thread.run_sync(tempfile.gettempdir)
 605  
 606  
 607  async def gettempdirb() -> bytes:
 608      """
 609      Asynchronously return the name of the directory used for temporary files in bytes.
 610  
 611      This function wraps `tempfile.gettempdirb` and executes it in a background thread.
 612  
 613      :return: The path of the temporary directory as bytes.
 614  
 615      """
 616      return await to_thread.run_sync(tempfile.gettempdirb)
 617