Skip to content

API Reference

Update or create files atomically

with_tempfile.write_text(fpath, contents, *, encoding=None)

Atomically write the specified text to the specified file.

Source code in python/src/with_tempfile/impl.py
def write_text(fpath: pathutil.PathLike, contents: str, *, encoding: str | None = None) -> None:
    """Atomically write the specified text to the specified file."""
    with temputil.NamedTemporaryTextFile(
        dir=str(pathutil.as_path(fpath).parent),
        encoding=encoding,
    ) as tempf:
        print(contents, end="", file=tempf, flush=True)
        tempf.path.rename(fpath)
        tempf.unset_delete()

with_tempfile.append_text(fpath, contents, *, encoding=None)

If the file exists, atomically append text, otherwise write the new contents.

Source code in python/src/with_tempfile/impl.py
def append_text(
    fpath: pathutil.PathLike,
    contents: str,
    *,
    encoding: str | Iterable[str] | None = None,
) -> None:
    """If the file exists, atomically append text, otherwise write the new contents."""
    try:
        current, actual_encoding = pathutil.read_text(pathutil.as_path(fpath), encoding=encoding)
    except FileNotFoundError:
        current, actual_encoding = "", pathutil.encoding_tuple(encoding)[0]

    write_text(fpath, f"{current}{contents}", encoding=actual_encoding)

Path manipulation helpers

with_tempfile.pathutil

Helpers for manipulating pathlib.Path objects and strings.

This module's main purpose is to define the PathLike type that may be used for function arguments that may be of any type suitable for passing to the system Python file handling routines. It is somewhat similar to the os.PathLike class, but it has a different set of allowed classes, e.g. it allows strings.

PathLike = pathlib.Path | str module-attribute

Something that may be used as a path.

as_path(path)

Convert a pathlike object to a path as needed.

Source code in python/src/with_tempfile/pathutil.py
def as_path(path: PathLike) -> pathlib.Path:
    """Convert a pathlike object to a path as needed."""
    if isinstance(path, pathlib.Path):
        return path

    return pathlib.Path(path)

encoding_tuple(encoding)

Split the first of the requested encoding names from the rest.

Source code in python/src/with_tempfile/pathutil.py
def encoding_tuple(encoding: str | Iterable[str] | None) -> tuple[str, Iterator[str]]:
    """Split the first of the requested encoding names from the rest."""
    if encoding is None:
        return "UTF-8", iter(())

    if isinstance(encoding, str):
        return encoding, iter(())

    it: Final = iter(encoding)
    first: Final = next(it)
    return first, it

read_text(path, *, encoding)

Read a file's contents, try to decode it using the specified encodings in order.

The return value is a tuple containing the full text read from the file and the name of the encoding used to decode it.

The encoding parameter may be either None, a string, or an iterable of strings. In the latter case the specified encodings are tried in order; the first one that successfully decodes the file's contents is returned.

Source code in python/src/with_tempfile/pathutil.py
def read_text(path: pathlib.Path, *, encoding: str | Iterable[str] | None) -> tuple[str, str]:
    """Read a file's contents, try to decode it using the specified encodings in order.

    The return value is a tuple containing the full text read from the file and
    the name of the encoding used to decode it.

    The `encoding` parameter may be either None, a string, or an iterable of strings.
    In the latter case the specified encodings are tried in order; the first one that
    successfully decodes the file's contents is returned.
    """
    first, rest = encoding_tuple(encoding)
    raw: Final = path.read_bytes()
    try:
        return raw.decode(first), first
    except ValueError:
        for enc in rest:
            try:
                return raw.decode(enc), enc
            except ValueError:
                pass

        # None of the encodings worked, return the error from the first attempt
        raise

Temporary file handling

with_tempfile.temputil

Helpers that combine temporary files and pathlib routines.

This module defines the NamedTemporaryTextFile function and the TemporaryDirectory class that parallel the corresponding classes from the system tempfile module, but also have the additional path and resolved_path fields. The goal is to make it easier to use pathlib.Path objects everywhere, while still using the system temporary file routines.

TemporaryDirectory

Bases: TempDir

A temporary directory with a ready-made pathlib.Path path.

Note that the __enter__() method (and consequently with TemporaryDirectory(...) as tempd:) returns a pathlib.Path object instead of a string!

Source code in python/src/with_tempfile/temputil.py
class TemporaryDirectory(TempDir):
    """A temporary directory with a ready-made `pathlib.Path` path.

    Note that the `__enter__()` method (and consequently `with TemporaryDirectory(...) as tempd:`)
    returns a `pathlib.Path` object instead of a string!
    """

    path: pathlib.Path
    """The path to the tempfile as a `pathlib.Path` object."""

    resolved_path: pathlib.Path
    """The resolved path to the tempfile as a `pathlib.Path` object."""

    if sys.version_info >= (3, 12):

        def __init__(
            self,
            suffix: str | None = None,
            prefix: str | None = None,
            dir: pathutil.PathLike | None = None,  # noqa: A002  # tempfile API
            ignore_cleanup_errors: bool = False,  # noqa: FBT001,FBT002  # tempfile API
            *,
            delete: bool = True,
        ) -> None:
            """Create the temporary directory, initialize our new fields."""
            super().__init__(
                suffix=suffix,
                prefix=prefix,
                dir=dir,
                ignore_cleanup_errors=ignore_cleanup_errors,
                delete=delete,
            )
            self._setup_paths()

    else:

        def __init__(
            self,
            suffix: str | None = None,
            prefix: str | None = None,
            dir: pathutil.PathLike | None = None,  # noqa: A002  # tempfile API
            ignore_cleanup_errors: bool = False,  # noqa: FBT001,FBT002  # tempfile API
        ) -> None:
            """Create the temporary directory, initialize our new fields."""
            super().__init__(
                suffix=suffix,
                prefix=prefix,
                dir=dir,
                ignore_cleanup_errors=ignore_cleanup_errors,
            )
            self._setup_paths()

    def _setup_paths(self) -> None:
        """Initialize our new fields."""
        self.path = pathlib.Path(self.name)
        self.resolved_path = self.path.resolve()

    def __enter__(self) -> pathlib.Path:  # type: ignore[override]
        """Enter a context, return the path to the directory.

        Note that this method returns a `pathlib.Path` object instead of a string!
        """
        return self.path
path instance-attribute

The path to the tempfile as a pathlib.Path object.

resolved_path instance-attribute

The resolved path to the tempfile as a pathlib.Path object.

__enter__()

Enter a context, return the path to the directory.

Note that this method returns a pathlib.Path object instead of a string!

Source code in python/src/with_tempfile/temputil.py
def __enter__(self) -> pathlib.Path:  # type: ignore[override]
    """Enter a context, return the path to the directory.

    Note that this method returns a `pathlib.Path` object instead of a string!
    """
    return self.path
__init__(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False)

Create the temporary directory, initialize our new fields.

Source code in python/src/with_tempfile/temputil.py
def __init__(
    self,
    suffix: str | None = None,
    prefix: str | None = None,
    dir: pathutil.PathLike | None = None,  # noqa: A002  # tempfile API
    ignore_cleanup_errors: bool = False,  # noqa: FBT001,FBT002  # tempfile API
) -> None:
    """Create the temporary directory, initialize our new fields."""
    super().__init__(
        suffix=suffix,
        prefix=prefix,
        dir=dir,
        ignore_cleanup_errors=ignore_cleanup_errors,
    )
    self._setup_paths()

TemporaryTextFile

Bases: TempTextWrap

A named temporary file with a ready-made pathlib.Path path.

Source code in python/src/with_tempfile/temputil.py
class TemporaryTextFile(TempTextWrap):
    """A named temporary file with a ready-made `pathlib.Path` path."""

    path: pathlib.Path
    """The path to the tempfile as a `pathlib.Path` object."""

    resolved_path: pathlib.Path
    """The resolved path to the tempfile as a `pathlib.Path` object."""

    if sys.version_info >= (3, 12):

        def __init__(
            self: Self,
            file: IO[str],
            name: str,
            delete: bool = True,  # noqa: FBT001,FBT002  # tempfile API
            delete_on_close: bool = True,  # noqa: FBT001,FBT002  # tempfile API
        ) -> None:
            """Create the temporary file, initialize our new fields."""
            super().__init__(file, name=name, delete=delete, delete_on_close=delete_on_close)
            self._setup_paths()
    else:

        def __init__(
            self: Self,
            file: IO[str],
            name: str,
            delete: bool = True,  # noqa: FBT001,FBT002  # tempfile API
        ) -> None:
            """Create the temporary file, initialize our new fields."""
            super().__init__(file, name=name, delete=delete)
            self._setup_paths()

    def __repr__(self) -> str:
        """Provide a Python-esque representation."""
        # Python 3.14 may evaluate `f"{self!r}"` in the constructor...
        if not hasattr(self, "path"):
            self._setup_paths()

        return (
            f"{type(self).__name__}(file={self.file!r}, name={self.name!r}, "
            f"path={self.path!r}, resolved_path={self.resolved_path!r}"
        )

    def _setup_paths(self) -> None:
        """Initialize our new fields."""
        self.path = pathlib.Path(self.name)
        self.resolved_path = self.path.resolve()

    def unset_delete(self, *, also_on_close: bool = True) -> None:
        """Unset the "delete on drop" and also possibly the "delete on close" flag.

        The caller took care of the file, e.g. renamed it, removed it, or passed it to
        another consumer to use as-is.
        """

        def handle(obj: Any) -> None:  # noqa: ANN401  # we can handle anything
            """Unset the `delete` and maybe the `delete_on_close` flags if they are there."""
            if hasattr(obj, "delete"):
                obj.delete = False
            if also_on_close and hasattr(obj, "delete_on_close"):
                obj.delete_on_close = False

        handle(self)

        match getattr(self, "_closer", None):
            case None:
                pass

            case closer:
                handle(closer)
path instance-attribute

The path to the tempfile as a pathlib.Path object.

resolved_path instance-attribute

The resolved path to the tempfile as a pathlib.Path object.

__init__(file, name, delete=True)

Create the temporary file, initialize our new fields.

Source code in python/src/with_tempfile/temputil.py
def __init__(
    self: Self,
    file: IO[str],
    name: str,
    delete: bool = True,  # noqa: FBT001,FBT002  # tempfile API
) -> None:
    """Create the temporary file, initialize our new fields."""
    super().__init__(file, name=name, delete=delete)
    self._setup_paths()
__repr__()

Provide a Python-esque representation.

Source code in python/src/with_tempfile/temputil.py
def __repr__(self) -> str:
    """Provide a Python-esque representation."""
    # Python 3.14 may evaluate `f"{self!r}"` in the constructor...
    if not hasattr(self, "path"):
        self._setup_paths()

    return (
        f"{type(self).__name__}(file={self.file!r}, name={self.name!r}, "
        f"path={self.path!r}, resolved_path={self.resolved_path!r}"
    )
unset_delete(*, also_on_close=True)

Unset the "delete on drop" and also possibly the "delete on close" flag.

The caller took care of the file, e.g. renamed it, removed it, or passed it to another consumer to use as-is.

Source code in python/src/with_tempfile/temputil.py
def unset_delete(self, *, also_on_close: bool = True) -> None:
    """Unset the "delete on drop" and also possibly the "delete on close" flag.

    The caller took care of the file, e.g. renamed it, removed it, or passed it to
    another consumer to use as-is.
    """

    def handle(obj: Any) -> None:  # noqa: ANN401  # we can handle anything
        """Unset the `delete` and maybe the `delete_on_close` flags if they are there."""
        if hasattr(obj, "delete"):
            obj.delete = False
        if also_on_close and hasattr(obj, "delete_on_close"):
            obj.delete_on_close = False

    handle(self)

    match getattr(self, "_closer", None):
        case None:
            pass

        case closer:
            handle(closer)

NamedTemporaryTextFile(*, buffering=-1, dir=None, encoding=None, prefix=None, suffix=None, delete=True)

Create a temporary file that holds its path.

Source code in python/src/with_tempfile/temputil.py
def NamedTemporaryTextFile(  # noqa: N802,PLR0913  # tempfile-like API
    *,
    buffering: int = -1,
    dir: str | None = None,  # noqa: A002  # tempfile API
    encoding: str | None = None,
    prefix: str | None = None,
    suffix: str | None = None,
    delete: bool = True,
) -> TemporaryTextFile:
    """Create a temporary file that holds its path."""
    fd, name = tempfile.mkstemp(dir=dir, prefix=prefix, suffix=suffix, text=True)
    tempf: Final = open(  # noqa: PTH123,SIM115  # low-level; also, a file descriptor
        fd,
        mode="w+",
        buffering=buffering,
        encoding=encoding,
    )
    return TemporaryTextFile(tempf, name, delete=delete)