1 from __future__ import annotations 2 3 import os 4 import pathlib 5 import sys 6 from collections.abc import ( 7 AsyncIterator, 8 Callable, 9 Iterable, 10 Iterator, 11 Sequence, 12 ) 13 from dataclasses import dataclass 14 from functools import partial 15 from os import PathLike 16 from typing import ( 17 IO, 18 TYPE_CHECKING, 19 Any, 20 AnyStr, 21 ClassVar, 22 Final, 23 Generic, 24 overload, 25 ) 26 27 from .. import to_thread 28 from ..abc import AsyncResource 29 30 if TYPE_CHECKING: 31 from types import ModuleType 32 33 from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer 34 else: 35 ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object 36 37 38 class AsyncFile(AsyncResource, Generic[AnyStr]): 39 """ 40 An asynchronous file object. 41 42 This class wraps a standard file object and provides async friendly versions of the 43 following blocking methods (where available on the original file object): 44 45 * read 46 * read1 47 * readline 48 * readlines 49 * readinto 50 * readinto1 51 * write 52 * writelines 53 * truncate 54 * seek 55 * tell 56 * flush 57 58 All other methods are directly passed through. 59 60 This class supports the asynchronous context manager protocol which closes the 61 underlying file at the end of the context block. 62 63 This class also supports asynchronous iteration:: 64 65 async with await open_file(...) as f: 66 async for line in f: 67 print(line) 68 """ 69 70 def __init__(self, fp: IO[AnyStr]) -> None: 71 self._fp: Any = fp 72 73 def __getattr__(self, name: str) -> object: 74 return getattr(self._fp, name) 75 76 @property 77 def wrapped(self) -> IO[AnyStr]: 78 """The wrapped file object.""" 79 return self._fp 80 81 async def __aiter__(self) -> AsyncIterator[AnyStr]: 82 while True: 83 line = await self.readline() 84 if line: 85 yield line 86 else: 87 break 88 89 async def aclose(self) -> None: 90 return await to_thread.run_sync(self._fp.close) 91 92 async def read(self, size: int = -1) -> AnyStr: 93 return await to_thread.run_sync(self._fp.read, size) 94 95 async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: 96 return await to_thread.run_sync(self._fp.read1, size) 97 98 async def readline(self) -> AnyStr: 99 return await to_thread.run_sync(self._fp.readline) 100 101 async def readlines(self) -> list[AnyStr]: 102 return await to_thread.run_sync(self._fp.readlines) 103 104 async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: 105 return await to_thread.run_sync(self._fp.readinto, b) 106 107 async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: 108 return await to_thread.run_sync(self._fp.readinto1, b) 109 110 @overload 111 async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... 112 113 @overload 114 async def write(self: AsyncFile[str], b: str) -> int: ... 115 116 async def write(self, b: ReadableBuffer | str) -> int: 117 return await to_thread.run_sync(self._fp.write, b) 118 119 @overload 120 async def writelines( 121 self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] 122 ) -> None: ... 123 124 @overload 125 async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... 126 127 async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: 128 return await to_thread.run_sync(self._fp.writelines, lines) 129 130 async def truncate(self, size: int | None = None) -> int: 131 return await to_thread.run_sync(self._fp.truncate, size) 132 133 async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: 134 return await to_thread.run_sync(self._fp.seek, offset, whence) 135 136 async def tell(self) -> int: 137 return await to_thread.run_sync(self._fp.tell) 138 139 async def flush(self) -> None: 140 return await to_thread.run_sync(self._fp.flush) 141 142 143 @overload 144 async def open_file( 145 file: str | PathLike[str] | int, 146 mode: OpenBinaryMode, 147 buffering: int = ..., 148 encoding: str | None = ..., 149 errors: str | None = ..., 150 newline: str | None = ..., 151 closefd: bool = ..., 152 opener: Callable[[str, int], int] | None = ..., 153 ) -> AsyncFile[bytes]: ... 154 155 156 @overload 157 async def open_file( 158 file: str | PathLike[str] | int, 159 mode: OpenTextMode = ..., 160 buffering: int = ..., 161 encoding: str | None = ..., 162 errors: str | None = ..., 163 newline: str | None = ..., 164 closefd: bool = ..., 165 opener: Callable[[str, int], int] | None = ..., 166 ) -> AsyncFile[str]: ... 167 168 169 async def open_file( 170 file: str | PathLike[str] | int, 171 mode: str = "r", 172 buffering: int = -1, 173 encoding: str | None = None, 174 errors: str | None = None, 175 newline: str | None = None, 176 closefd: bool = True, 177 opener: Callable[[str, int], int] | None = None, 178 ) -> AsyncFile[Any]: 179 """ 180 Open a file asynchronously. 181 182 The arguments are exactly the same as for the builtin :func:`open`. 183 184 :return: an asynchronous file object 185 186 """ 187 fp = await to_thread.run_sync( 188 open, file, mode, buffering, encoding, errors, newline, closefd, opener 189 ) 190 return AsyncFile(fp) 191 192 193 def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]: 194 """ 195 Wrap an existing file as an asynchronous file. 196 197 :param file: an existing file-like object 198 :return: an asynchronous file object 199 200 """ 201 return AsyncFile(file) 202 203 204 @dataclass(eq=False) 205 class _PathIterator(AsyncIterator["Path"]): 206 iterator: Iterator[PathLike[str]] 207 208 async def __anext__(self) -> Path: 209 nextval = await to_thread.run_sync( 210 next, self.iterator, None, abandon_on_cancel=True 211 ) 212 if nextval is None: 213 raise StopAsyncIteration from None 214 215 return Path(nextval) 216 217 218 class Path: 219 """ 220 An asynchronous version of :class:`pathlib.Path`. 221 222 This class cannot be substituted for :class:`pathlib.Path` or 223 :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` 224 interface. 225 226 It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for 227 the deprecated :meth:`~pathlib.Path.link_to` method. 228 229 Some methods may be unavailable or have limited functionality, based on the Python 230 version: 231 232 * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) 233 * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) 234 * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) 235 * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later) 236 * :attr:`~pathlib.Path.info` (available on Python 3.14 or later) 237 * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) 238 * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only 239 available on Python 3.13 or later) 240 * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) 241 * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) 242 * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available 243 on Python 3.12 or later) 244 * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) 245 246 Any methods that do disk I/O need to be awaited on. These methods are: 247 248 * :meth:`~pathlib.Path.absolute` 249 * :meth:`~pathlib.Path.chmod` 250 * :meth:`~pathlib.Path.cwd` 251 * :meth:`~pathlib.Path.exists` 252 * :meth:`~pathlib.Path.expanduser` 253 * :meth:`~pathlib.Path.group` 254 * :meth:`~pathlib.Path.hardlink_to` 255 * :meth:`~pathlib.Path.home` 256 * :meth:`~pathlib.Path.is_block_device` 257 * :meth:`~pathlib.Path.is_char_device` 258 * :meth:`~pathlib.Path.is_dir` 259 * :meth:`~pathlib.Path.is_fifo` 260 * :meth:`~pathlib.Path.is_file` 261 * :meth:`~pathlib.Path.is_junction` 262 * :meth:`~pathlib.Path.is_mount` 263 * :meth:`~pathlib.Path.is_socket` 264 * :meth:`~pathlib.Path.is_symlink` 265 * :meth:`~pathlib.Path.lchmod` 266 * :meth:`~pathlib.Path.lstat` 267 * :meth:`~pathlib.Path.mkdir` 268 * :meth:`~pathlib.Path.open` 269 * :meth:`~pathlib.Path.owner` 270 * :meth:`~pathlib.Path.read_bytes` 271 * :meth:`~pathlib.Path.read_text` 272 * :meth:`~pathlib.Path.readlink` 273 * :meth:`~pathlib.Path.rename` 274 * :meth:`~pathlib.Path.replace` 275 * :meth:`~pathlib.Path.resolve` 276 * :meth:`~pathlib.Path.rmdir` 277 * :meth:`~pathlib.Path.samefile` 278 * :meth:`~pathlib.Path.stat` 279 * :meth:`~pathlib.Path.symlink_to` 280 * :meth:`~pathlib.Path.touch` 281 * :meth:`~pathlib.Path.unlink` 282 * :meth:`~pathlib.Path.walk` 283 * :meth:`~pathlib.Path.write_bytes` 284 * :meth:`~pathlib.Path.write_text` 285 286 Additionally, the following methods return an async iterator yielding 287 :class:`~.Path` objects: 288 289 * :meth:`~pathlib.Path.glob` 290 * :meth:`~pathlib.Path.iterdir` 291 * :meth:`~pathlib.Path.rglob` 292 """ 293 294 __slots__ = "_path", "__weakref__" 295 296 __weakref__: Any 297 298 def __init__(self, *args: str | PathLike[str]) -> None: 299 self._path: Final[pathlib.Path] = pathlib.Path(*args) 300 301 def __fspath__(self) -> str: 302 return self._path.__fspath__() 303 304 def __str__(self) -> str: 305 return self._path.__str__() 306 307 def __repr__(self) -> str: 308 return f"{self.__class__.__name__}({self.as_posix()!r})" 309 310 def __bytes__(self) -> bytes: 311 return self._path.__bytes__() 312 313 def __hash__(self) -> int: 314 return self._path.__hash__() 315 316 def __eq__(self, other: object) -> bool: 317 target = other._path if isinstance(other, Path) else other 318 return self._path.__eq__(target) 319 320 def __lt__(self, other: pathlib.PurePath | Path) -> bool: 321 target = other._path if isinstance(other, Path) else other 322 return self._path.__lt__(target) 323 324 def __le__(self, other: pathlib.PurePath | Path) -> bool: 325 target = other._path if isinstance(other, Path) else other 326 return self._path.__le__(target) 327 328 def __gt__(self, other: pathlib.PurePath | Path) -> bool: 329 target = other._path if isinstance(other, Path) else other 330 return self._path.__gt__(target) 331 332 def __ge__(self, other: pathlib.PurePath | Path) -> bool: 333 target = other._path if isinstance(other, Path) else other 334 return self._path.__ge__(target) 335 336 def __truediv__(self, other: str | PathLike[str]) -> Path: 337 return Path(self._path / other) 338 339 def __rtruediv__(self, other: str | PathLike[str]) -> Path: 340 return Path(other) / self 341 342 @property 343 def parts(self) -> tuple[str, ...]: 344 return self._path.parts 345 346 @property 347 def drive(self) -> str: 348 return self._path.drive 349 350 @property 351 def root(self) -> str: 352 return self._path.root 353 354 @property 355 def anchor(self) -> str: 356 return self._path.anchor 357 358 @property 359 def parents(self) -> Sequence[Path]: 360 return tuple(Path(p) for p in self._path.parents) 361 362 @property 363 def parent(self) -> Path: 364 return Path(self._path.parent) 365 366 @property 367 def name(self) -> str: 368 return self._path.name 369 370 @property 371 def suffix(self) -> str: 372 return self._path.suffix 373 374 @property 375 def suffixes(self) -> list[str]: 376 return self._path.suffixes 377 378 @property 379 def stem(self) -> str: 380 return self._path.stem 381 382 async def absolute(self) -> Path: 383 path = await to_thread.run_sync(self._path.absolute) 384 return Path(path) 385 386 def as_posix(self) -> str: 387 return self._path.as_posix() 388 389 def as_uri(self) -> str: 390 return self._path.as_uri() 391 392 if sys.version_info >= (3, 13): 393 parser: ClassVar[ModuleType] = pathlib.Path.parser 394 395 @classmethod 396 def from_uri(cls, uri: str) -> Path: 397 return Path(pathlib.Path.from_uri(uri)) 398 399 def full_match( 400 self, path_pattern: str, *, case_sensitive: bool | None = None 401 ) -> bool: 402 return self._path.full_match(path_pattern, case_sensitive=case_sensitive) 403 404 def match( 405 self, path_pattern: str, *, case_sensitive: bool | None = None 406 ) -> bool: 407 return self._path.match(path_pattern, case_sensitive=case_sensitive) 408 else: 409 410 def match(self, path_pattern: str) -> bool: 411 return self._path.match(path_pattern) 412 413 if sys.version_info >= (3, 14): 414 415 @property 416 def info(self) -> Any: # TODO: add return type annotation when Typeshed gets it 417 return self._path.info 418 419 async def copy( 420 self, 421 target: str | os.PathLike[str], 422 *, 423 follow_symlinks: bool = True, 424 dirs_exist_ok: bool = False, 425 preserve_metadata: bool = False, 426 ) -> Path: 427 func = partial( 428 self._path.copy, 429 follow_symlinks=follow_symlinks, 430 dirs_exist_ok=dirs_exist_ok, 431 preserve_metadata=preserve_metadata, 432 ) 433 return Path(await to_thread.run_sync(func, target)) 434 435 async def copy_into( 436 self, 437 target_dir: str | os.PathLike[str], 438 *, 439 follow_symlinks: bool = True, 440 dirs_exist_ok: bool = False, 441 preserve_metadata: bool = False, 442 ) -> Path: 443 func = partial( 444 self._path.copy_into, 445 follow_symlinks=follow_symlinks, 446 dirs_exist_ok=dirs_exist_ok, 447 preserve_metadata=preserve_metadata, 448 ) 449 return Path(await to_thread.run_sync(func, target_dir)) 450 451 async def move(self, target: str | os.PathLike[str]) -> Path: 452 # Upstream does not handle anyio.Path properly as a PathLike 453 target = pathlib.Path(target) 454 return Path(await to_thread.run_sync(self._path.move, target)) 455 456 async def move_into( 457 self, 458 target_dir: str | os.PathLike[str], 459 ) -> Path: 460 return Path(await to_thread.run_sync(self._path.move_into, target_dir)) 461 462 def is_relative_to(self, other: str | PathLike[str]) -> bool: 463 try: 464 self.relative_to(other) 465 return True 466 except ValueError: 467 return False 468 469 async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: 470 func = partial(os.chmod, follow_symlinks=follow_symlinks) 471 return await to_thread.run_sync(func, self._path, mode) 472 473 @classmethod 474 async def cwd(cls) -> Path: 475 path = await to_thread.run_sync(pathlib.Path.cwd) 476 return cls(path) 477 478 async def exists(self) -> bool: 479 return await to_thread.run_sync(self._path.exists, abandon_on_cancel=True) 480 481 async def expanduser(self) -> Path: 482 return Path( 483 await to_thread.run_sync(self._path.expanduser, abandon_on_cancel=True) 484 ) 485 486 def glob(self, pattern: str) -> AsyncIterator[Path]: 487 gen = self._path.glob(pattern) 488 return _PathIterator(gen) 489 490 async def group(self) -> str: 491 return await to_thread.run_sync(self._path.group, abandon_on_cancel=True) 492 493 async def hardlink_to( 494 self, target: str | bytes | PathLike[str] | PathLike[bytes] 495 ) -> None: 496 if isinstance(target, Path): 497 target = target._path 498 499 await to_thread.run_sync(os.link, target, self) 500 501 @classmethod 502 async def home(cls) -> Path: 503 home_path = await to_thread.run_sync(pathlib.Path.home) 504 return cls(home_path) 505 506 def is_absolute(self) -> bool: 507 return self._path.is_absolute() 508 509 async def is_block_device(self) -> bool: 510 return await to_thread.run_sync( 511 self._path.is_block_device, abandon_on_cancel=True 512 ) 513 514 async def is_char_device(self) -> bool: 515 return await to_thread.run_sync( 516 self._path.is_char_device, abandon_on_cancel=True 517 ) 518 519 async def is_dir(self) -> bool: 520 return await to_thread.run_sync(self._path.is_dir, abandon_on_cancel=True) 521 522 async def is_fifo(self) -> bool: 523 return await to_thread.run_sync(self._path.is_fifo, abandon_on_cancel=True) 524 525 async def is_file(self) -> bool: 526 return await to_thread.run_sync(self._path.is_file, abandon_on_cancel=True) 527 528 if sys.version_info >= (3, 12): 529 530 async def is_junction(self) -> bool: 531 return await to_thread.run_sync(self._path.is_junction) 532 533 async def is_mount(self) -> bool: 534 return await to_thread.run_sync( 535 os.path.ismount, self._path, abandon_on_cancel=True 536 ) 537 538 def is_reserved(self) -> bool: 539 return self._path.is_reserved() 540 541 async def is_socket(self) -> bool: 542 return await to_thread.run_sync(self._path.is_socket, abandon_on_cancel=True) 543 544 async def is_symlink(self) -> bool: 545 return await to_thread.run_sync(self._path.is_symlink, abandon_on_cancel=True) 546 547 async def iterdir(self) -> AsyncIterator[Path]: 548 gen = ( 549 self._path.iterdir() 550 if sys.version_info < (3, 13) 551 else await to_thread.run_sync(self._path.iterdir, abandon_on_cancel=True) 552 ) 553 async for path in _PathIterator(gen): 554 yield path 555 556 def joinpath(self, *args: str | PathLike[str]) -> Path: 557 return Path(self._path.joinpath(*args)) 558 559 async def lchmod(self, mode: int) -> None: 560 await to_thread.run_sync(self._path.lchmod, mode) 561 562 async def lstat(self) -> os.stat_result: 563 return await to_thread.run_sync(self._path.lstat, abandon_on_cancel=True) 564 565 async def mkdir( 566 self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False 567 ) -> None: 568 await to_thread.run_sync(self._path.mkdir, mode, parents, exist_ok) 569 570 @overload 571 async def open( 572 self, 573 mode: OpenBinaryMode, 574 buffering: int = ..., 575 encoding: str | None = ..., 576 errors: str | None = ..., 577 newline: str | None = ..., 578 ) -> AsyncFile[bytes]: ... 579 580 @overload 581 async def open( 582 self, 583 mode: OpenTextMode = ..., 584 buffering: int = ..., 585 encoding: str | None = ..., 586 errors: str | None = ..., 587 newline: str | None = ..., 588 ) -> AsyncFile[str]: ... 589 590 async def open( 591 self, 592 mode: str = "r", 593 buffering: int = -1, 594 encoding: str | None = None, 595 errors: str | None = None, 596 newline: str | None = None, 597 ) -> AsyncFile[Any]: 598 fp = await to_thread.run_sync( 599 self._path.open, mode, buffering, encoding, errors, newline 600 ) 601 return AsyncFile(fp) 602 603 async def owner(self) -> str: 604 return await to_thread.run_sync(self._path.owner, abandon_on_cancel=True) 605 606 async def read_bytes(self) -> bytes: 607 return await to_thread.run_sync(self._path.read_bytes) 608 609 async def read_text( 610 self, encoding: str | None = None, errors: str | None = None 611 ) -> str: 612 return await to_thread.run_sync(self._path.read_text, encoding, errors) 613 614 if sys.version_info >= (3, 12): 615 616 def relative_to( 617 self, *other: str | PathLike[str], walk_up: bool = False 618 ) -> Path: 619 return Path(self._path.relative_to(*other, walk_up=walk_up)) 620 621 else: 622 623 def relative_to(self, *other: str | PathLike[str]) -> Path: 624 return Path(self._path.relative_to(*other)) 625 626 async def readlink(self) -> Path: 627 target = await to_thread.run_sync(os.readlink, self._path) 628 return Path(target) 629 630 async def rename(self, target: str | pathlib.PurePath | Path) -> Path: 631 if isinstance(target, Path): 632 target = target._path 633 634 await to_thread.run_sync(self._path.rename, target) 635 return Path(target) 636 637 async def replace(self, target: str | pathlib.PurePath | Path) -> Path: 638 if isinstance(target, Path): 639 target = target._path 640 641 await to_thread.run_sync(self._path.replace, target) 642 return Path(target) 643 644 async def resolve(self, strict: bool = False) -> Path: 645 func = partial(self._path.resolve, strict=strict) 646 return Path(await to_thread.run_sync(func, abandon_on_cancel=True)) 647 648 def rglob(self, pattern: str) -> AsyncIterator[Path]: 649 gen = self._path.rglob(pattern) 650 return _PathIterator(gen) 651 652 async def rmdir(self) -> None: 653 await to_thread.run_sync(self._path.rmdir) 654 655 async def samefile(self, other_path: str | PathLike[str]) -> bool: 656 if isinstance(other_path, Path): 657 other_path = other_path._path 658 659 return await to_thread.run_sync( 660 self._path.samefile, other_path, abandon_on_cancel=True 661 ) 662 663 async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: 664 func = partial(os.stat, follow_symlinks=follow_symlinks) 665 return await to_thread.run_sync(func, self._path, abandon_on_cancel=True) 666 667 async def symlink_to( 668 self, 669 target: str | bytes | PathLike[str] | PathLike[bytes], 670 target_is_directory: bool = False, 671 ) -> None: 672 if isinstance(target, Path): 673 target = target._path 674 675 await to_thread.run_sync(self._path.symlink_to, target, target_is_directory) 676 677 async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: 678 await to_thread.run_sync(self._path.touch, mode, exist_ok) 679 680 async def unlink(self, missing_ok: bool = False) -> None: 681 try: 682 await to_thread.run_sync(self._path.unlink) 683 except FileNotFoundError: 684 if not missing_ok: 685 raise 686 687 if sys.version_info >= (3, 12): 688 689 async def walk( 690 self, 691 top_down: bool = True, 692 on_error: Callable[[OSError], object] | None = None, 693 follow_symlinks: bool = False, 694 ) -> AsyncIterator[tuple[Path, list[str], list[str]]]: 695 def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: 696 try: 697 return next(gen) 698 except StopIteration: 699 return None 700 701 gen = self._path.walk(top_down, on_error, follow_symlinks) 702 while True: 703 value = await to_thread.run_sync(get_next_value) 704 if value is None: 705 return 706 707 root, dirs, paths = value 708 yield Path(root), dirs, paths 709 710 def with_name(self, name: str) -> Path: 711 return Path(self._path.with_name(name)) 712 713 def with_stem(self, stem: str) -> Path: 714 return Path(self._path.with_name(stem + self._path.suffix)) 715 716 def with_suffix(self, suffix: str) -> Path: 717 return Path(self._path.with_suffix(suffix)) 718 719 def with_segments(self, *pathsegments: str | PathLike[str]) -> Path: 720 return Path(*pathsegments) 721 722 async def write_bytes(self, data: bytes) -> int: 723 return await to_thread.run_sync(self._path.write_bytes, data) 724 725 async def write_text( 726 self, 727 data: str, 728 encoding: str | None = None, 729 errors: str | None = None, 730 newline: str | None = None, 731 ) -> int: 732 # Path.write_text() does not support the "newline" parameter before Python 3.10 733 def sync_write_text() -> int: 734 with self._path.open( 735 "w", encoding=encoding, errors=errors, newline=newline 736 ) as fp: 737 return fp.write(data) 738 739 return await to_thread.run_sync(sync_write_text) 740 741 742 PathLike.register(Path) 743