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)

with_tempfile.safecat(*, tmpdir, spooldir, contents, encoding='UTF-8', prefix='', suffix='')

Create a file in the temporary directory, move it to a unique name within the spool one.

Source code in python/src/with_tempfile/impl.py
def safecat(
    *,
    tmpdir: pathutil.PathLike,
    spooldir: pathutil.PathLike,
    contents: str,
    encoding: str = "UTF-8",
    prefix: str = "",
    suffix: str = "",
) -> pathlib.Path:
    """Create a file in the temporary directory, move it to a unique name within the spool one."""
    spool: Final = temputil.MaildirSpool(
        tmpdir=pathutil.as_path(tmpdir),
        spooldir=pathutil.as_path(spooldir),
    )
    return spool.safecat(contents=contents, encoding=encoding, prefix=prefix, suffix=suffix)

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.

MaildirSpool dataclass

A Maildir-like directory structure: a temporary directory and a spool one.

Source code in python/src/with_tempfile/temputil.py
@dataclasses.dataclass(frozen=True)
class MaildirSpool:
    """A Maildir-like directory structure: a temporary directory and a spool one."""

    tmpdir: pathlib.Path
    """The path to the temporary directory."""

    spooldir: pathlib.Path
    """The path to the spool directory."""

    @classmethod
    def from_maildir(cls, path: pathlib.Path) -> Self:
        """Use the default Maildir `tmp` and `new` subdirectory names."""
        return cls(tmpdir=path / "tmp", spooldir=path / "new")

    def create_file(
        self,
        *,
        encoding: str = "UTF-8",
        prefix: str = "",
        suffix: str = "",
    ) -> MaildirSpoolFile:
        """Create a temporary file, let the caller write to it."""
        tempf, name = create_named_file(
            dir=str(self.tmpdir),
            encoding=encoding,
            prefix=prefix,
            suffix=suffix,
        )
        return MaildirSpoolFile(
            tempf,
            name,
            delete=True,
            maildir_spool=self,
            maildir_prefix=prefix,
            maildir_suffix=suffix,
        )

    def safecat(
        self,
        *,
        contents: str,
        encoding: str = "UTF-8",
        prefix: str = "",
        suffix: str = "",
    ) -> pathlib.Path:
        """Create a file in the temp directory, move it to a unique name within the spool one."""
        with self.create_file(encoding=encoding, prefix=prefix, suffix=suffix) as tempf:
            print(contents, file=tempf, end="")
            return tempf.rename_maildir_file()
spooldir instance-attribute

The path to the spool directory.

tmpdir instance-attribute

The path to the temporary directory.

create_file(*, encoding='UTF-8', prefix='', suffix='')

Create a temporary file, let the caller write to it.

Source code in python/src/with_tempfile/temputil.py
def create_file(
    self,
    *,
    encoding: str = "UTF-8",
    prefix: str = "",
    suffix: str = "",
) -> MaildirSpoolFile:
    """Create a temporary file, let the caller write to it."""
    tempf, name = create_named_file(
        dir=str(self.tmpdir),
        encoding=encoding,
        prefix=prefix,
        suffix=suffix,
    )
    return MaildirSpoolFile(
        tempf,
        name,
        delete=True,
        maildir_spool=self,
        maildir_prefix=prefix,
        maildir_suffix=suffix,
    )
from_maildir(path) classmethod

Use the default Maildir tmp and new subdirectory names.

Source code in python/src/with_tempfile/temputil.py
@classmethod
def from_maildir(cls, path: pathlib.Path) -> Self:
    """Use the default Maildir `tmp` and `new` subdirectory names."""
    return cls(tmpdir=path / "tmp", spooldir=path / "new")
safecat(*, contents, encoding='UTF-8', prefix='', suffix='')

Create a file in the temp directory, move it to a unique name within the spool one.

Source code in python/src/with_tempfile/temputil.py
def safecat(
    self,
    *,
    contents: str,
    encoding: str = "UTF-8",
    prefix: str = "",
    suffix: str = "",
) -> pathlib.Path:
    """Create a file in the temp directory, move it to a unique name within the spool one."""
    with self.create_file(encoding=encoding, prefix=prefix, suffix=suffix) as tempf:
        print(contents, file=tempf, end="")
        return tempf.rename_maildir_file()

MaildirSpoolFile

Bases: TemporaryTextFile

A temporary file that keeps a reference to the Maildir-style directory it is in.

Source code in python/src/with_tempfile/temputil.py
class MaildirSpoolFile(TemporaryTextFile):
    """A temporary file that keeps a reference to the Maildir-style directory it is in."""

    maildir_spool: MaildirSpool
    """The Maildir-style directory that this file was created as part of."""

    maildir_prefix: str
    """The prefix to use when moving the file to the spool directory."""

    maildir_suffix: str
    """The suffix to use when moving the file to the spool directory."""

    # ruff: ignore[too-many-arguments]
    # all the TemporaryTextFile constructor arguments and then some more
    def __init__(
        self: Self,
        file: IO[str],
        name: str,
        *,
        maildir_spool: MaildirSpool,
        delete: bool = True,
        maildir_prefix: str = "",
        maildir_suffix: str = "",
    ) -> None:
        """Create the temporary file, initialize our new fields."""
        super().__init__(file, name=name, delete=delete)
        self.maildir_spool = maildir_spool
        self.maildir_prefix = maildir_prefix
        self.maildir_suffix = maildir_suffix

    def rename_maildir_file(self) -> pathlib.Path:
        """Rename the temporary file into the spool directory."""
        inum: Final = self.path.stat().st_ino
        res: Final = (
            self.maildir_spool.spooldir / f"{self.maildir_prefix}{inum}{self.maildir_suffix}"
        )
        if res.is_symlink() or res.exists():
            raise RuntimeError(repr((self, inum, res)))

        self.flush()
        self.path.rename(res)
        self.unset_delete()
        return res
maildir_prefix = maildir_prefix instance-attribute

The prefix to use when moving the file to the spool directory.

maildir_spool = maildir_spool instance-attribute

The Maildir-style directory that this file was created as part of.

maildir_suffix = maildir_suffix instance-attribute

The suffix to use when moving the file to the spool directory.

__init__(file, name, *, maildir_spool, delete=True, maildir_prefix='', maildir_suffix='')

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,
    *,
    maildir_spool: MaildirSpool,
    delete: bool = True,
    maildir_prefix: str = "",
    maildir_suffix: str = "",
) -> None:
    """Create the temporary file, initialize our new fields."""
    super().__init__(file, name=name, delete=delete)
    self.maildir_spool = maildir_spool
    self.maildir_prefix = maildir_prefix
    self.maildir_suffix = maildir_suffix
rename_maildir_file()

Rename the temporary file into the spool directory.

Source code in python/src/with_tempfile/temputil.py
def rename_maildir_file(self) -> pathlib.Path:
    """Rename the temporary file into the spool directory."""
    inum: Final = self.path.stat().st_ino
    res: Final = (
        self.maildir_spool.spooldir / f"{self.maildir_prefix}{inum}{self.maildir_suffix}"
    )
    if res.is_symlink() or res.exists():
        raise RuntimeError(repr((self, inum, res)))

    self.flush()
    self.path.rename(res)
    self.unset_delete()
    return res

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,
            # ruff: ignore[builtin-argument-shadowing]
            # tempfile API
            dir: pathutil.PathLike | None = None,
            # ruff: ignore[boolean-type-hint-positional-argument]
            # ruff: ignore[boolean-default-value-positional-argument]
            # tempfile API
            ignore_cleanup_errors: bool = False,
            *,
            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,
            # ruff: ignore[builtin-argument-shadowing]
            # tempfile API
            dir: pathutil.PathLike | None = None,
            # ruff: ignore[boolean-type-hint-positional-argument]
            # ruff: ignore[boolean-default-value-positional-argument]
            # tempfile API
            ignore_cleanup_errors: bool = False,
        ) -> 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()

    # ty: ignore[invalid-method-override]
    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,
    # ruff: ignore[builtin-argument-shadowing]
    # tempfile API
    dir: pathutil.PathLike | None = None,
    # ruff: ignore[boolean-type-hint-positional-argument]
    # ruff: ignore[boolean-default-value-positional-argument]
    # tempfile API
    ignore_cleanup_errors: bool = False,
) -> 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,
            # ruff: ignore[boolean-type-hint-positional-argument]
            # ruff: ignore[boolean-default-value-positional-argument]
            # tempfile API
            delete: bool = True,
            # ruff: ignore[boolean-type-hint-positional-argument]
            # ruff: ignore[boolean-default-value-positional-argument]
            # tempfile API
            delete_on_close: bool = True,
        ) -> 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,
            # ruff: ignore[boolean-type-hint-positional-argument]
            # ruff: ignore[boolean-default-value-positional-argument]
            # tempfile API
            delete: bool = True,
        ) -> 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:  # ruff: ignore[any-type]  # 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,
    # ruff: ignore[boolean-type-hint-positional-argument]
    # ruff: ignore[boolean-default-value-positional-argument]
    # tempfile API
    delete: bool = True,
) -> 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:  # ruff: ignore[any-type]  # 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(
    *,
    buffering: int = -1,
    dir: str | None = None,  # ruff: ignore[builtin-argument-shadowing]  # 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."""
    tempf, name = create_named_file(
        buffering=buffering,
        dir=dir,
        encoding=encoding,
        prefix=prefix,
        suffix=suffix,
    )
    return TemporaryTextFile(tempf, name, delete=delete)

create_named_file(*, buffering=-1, dir=None, encoding=None, prefix=None, suffix=None)

Create a temporary file with the specified attributes.

Source code in python/src/with_tempfile/temputil.py
def create_named_file(
    *,
    buffering: int = -1,
    dir: str | None = None,  # ruff: ignore[builtin-argument-shadowing]  # tempfile API
    encoding: str | None = None,
    prefix: str | None = None,
    suffix: str | None = None,
) -> tuple[IO[str], str]:
    """Create a temporary file with the specified attributes."""
    fd, name = tempfile.mkstemp(dir=dir, prefix=prefix, suffix=suffix, text=True)
    # ruff: ignore[builtin-open,open-file-with-context-handler]
    # low-level; also, a file descriptor
    tempf: Final = open(
        fd,
        mode="w+",
        buffering=buffering,
        encoding=encoding,
    )
    return tempf, name