Skip to content

stepup.core.path

Specialized path operations.

apply_affixes(path, leading, trailing)

Apply leading ./ and trailing / slashes to a path.

Parameters:

  • path (StrPath) –

    The path to which the affixes will be applied.

  • leading (str) –

    The leading ./ to apply or "".

  • trailing (str) –

    The trailing slash to apply or "".

Raises:

  • PathError

    If the path already has a leading or trailing slash and the corresponding affix is not empty.

  • PathError

    If leading is neither "" nor "./".

  • PathError

    If trailing is neither "" nor "/".

Source code in stepup/core/path.py
def apply_affixes(path: StrPath, leading: str, trailing: str) -> Path:
    """Apply leading `./` and trailing `/` slashes to a path.

    Parameters
    ----------
    path
        The path to which the affixes will be applied.
    leading
        The leading `./` to apply or `""`.
    trailing
        The trailing slash to apply or `""`.

    Raises
    ------
    PathError
        If the path already has a leading or trailing slash
        and the corresponding affix is not empty.
    PathError
        If `leading` is neither `""` nor `"./"`.
    PathError
        If `trailing` is neither `""` nor `"/"`.
    """
    path = coerce_str(path)
    if leading != "":
        if leading != "./":
            raise PathError(f"Leading affix must be one of '' or './', got '{leading}'")
        if path.startswith(("/", "./")):
            raise PathError(f"Path already has a leading slash: {path}")
        path = leading + path
    if trailing != "":
        if trailing != "/":
            raise PathError(f"Trailing affix must be '' or '/', got '{trailing}'")
        if path.endswith("/"):
            raise PathError(f"Path already has a trailing slash: {path}")
        path = path + trailing
    return coerce_path(path)

coerce_path(arg)

Convert a path-like argument to a path.Path instance.

Source code in stepup/core/path.py
def coerce_path(arg: StrPath) -> Path:
    """Convert a path-like argument to a `path.Path` instance."""
    return Path(os.fspath(arg))

coerce_paths(args)

Convert a path-like argument or flat iterable to path.Path instances.

Source code in stepup/core/path.py
def coerce_paths(args: StrPath | Iterable[StrPath]) -> list[Path]:
    """Convert a path-like argument or flat iterable to `path.Path` instances."""
    if isinstance(args, (str, os.PathLike)):
        args = [args]
    return [Path(os.fspath(arg)) for arg in args]

coerce_paths2(args)

Convert an iterable of paths or path sub-iterables, flattening one level of nesting.

Source code in stepup/core/path.py
def coerce_paths2(args: Iterable[StrPath | Iterable[StrPath]]) -> list[Path]:
    """Convert an iterable of paths or path sub-iterables, flattening one level of nesting."""
    result = []
    for arg in args:
        if isinstance(arg, (str, os.PathLike)):
            result.append(Path(os.fspath(arg)))
        else:
            result.extend(Path(os.fspath(a)) for a in arg)
    return result

coerce_str(arg)

Convert a path-like argument via os.fspath.

Source code in stepup/core/path.py
def coerce_str(arg: StrPath) -> str:
    """Convert a path-like argument via `os.fspath`."""
    return os.fspath(arg)

dir_range_upper(parent)

Compute the exclusive upper bound for a range of paths that are all under the given parent.

SQL queries can define the range of all paths under parent with the following WHERE clause:

WHERE path >= :parent AND path < :upper_bound

where :parent is the parent path and :upper_bound is the result of this function.

Raises:

  • ValueError

    If parent does not end with a trailing slash.

Source code in stepup/core/path.py
def dir_range_upper(parent: str) -> str:
    """Compute the exclusive upper bound for a range of paths that are all under the given parent.

    SQL queries can define the range of all paths under parent with the following WHERE clause:

    ```sql
    WHERE path >= :parent AND path < :upper_bound
    ```

    where `:parent` is the parent path and `:upper_bound` is the result of this function.

    Raises
    ------
    ValueError
        If `parent` does not end with a trailing slash.
    """
    # `parent` must end in `/` (`0x2F`).
    # Since `0x2F + 1 == 0x30 == "0"`,
    # replacing the trailing slash with `"0"` gives the smallest string
    # that sorts (byte-wise) just past every label starting with `parent`,
    # so a SQL query can use it as a bound parameter instead of concatenating strings per row.
    if not parent.endswith("/"):
        raise ValueError("Trailing slash expected to compute path range upper bound")
    return parent[:-1] + "0"

format_local_executable(executable)

Format a relative path to a local executable for execution in a shell.

A path without a leading ./ or ../ gets a ./ prefix, so that a shell runs the file at that path instead of searching PATH for its name.

Raises:

  • PathError

    If executable is an absolute path.

Source code in stepup/core/path.py
def format_local_executable(executable: StrPath) -> str:
    """Format a relative path to a local executable for execution in a shell.

    A path without a leading `./` or `../` gets a `./` prefix,
    so that a shell runs the file at that path instead of searching `PATH` for its name.

    Raises
    ------
    PathError
        If `executable` is an absolute path.
    """
    executable = coerce_path(executable)
    if executable.isabs():
        raise PathError(f"Executable is not a relative path: {executable}")
    relative = executable if executable.startswith(("./", "../")) else "." / executable
    return shlex.quote(relative)

get_affixes(path)

Get the leading ./ and trailing / of a path.

Parameters:

  • path (StrPath) –

    The path from which the affixes will be extracted.

Returns:

  • leading

    The leading ./ of the path, or "" if there is none.

  • trailing

    The trailing slash of the path, or "" if there is none.

Notes

For the special case of the path "./", the leading is "" and the trailing is "/".

Source code in stepup/core/path.py
def get_affixes(path: StrPath) -> tuple[str, str]:
    """Get the leading `./` and trailing `/` of a path.

    Parameters
    ----------
    path
        The path from which the affixes will be extracted.

    Returns
    -------
    leading
        The leading `./` of the path, or `""` if there is none.
    trailing
        The trailing slash of the path, or `""` if there is none.

    Notes
    -----
    For the special case of the path `"./"`, the leading is `""` and the trailing is `"/"`.
    """
    path = coerce_str(path)
    trailing = ""
    if path.endswith("/"):
        trailing = "/"
        path = path[:-1]
    leading = "./" if path.startswith("./") else ""
    return leading, trailing

get_stepup_root()

Get the StepUp root directory.

Returns:

  • stepup_root

    The StepUp root directory, which is either the value of ${STEPUP_ROOT}, or the current working directory if the environment variable is not set. The returned path is absolute and normalized.

Source code in stepup/core/path.py
def get_stepup_root() -> Path:
    """Get the StepUp root directory.

    Returns
    -------
    stepup_root
        The StepUp root directory, which is either the value of `${STEPUP_ROOT}`,
        or the current working directory if the environment variable is not set.
        The returned path is absolute and normalized.
    """
    return Path(os.getenv("STEPUP_ROOT", os.getcwd())).absolute()

make_path_out(path_in, dest, ext, other_exts=())

Construct an output path given the input path, a destination and the expected extension.

Parameters:

  • path_in (StrPath) –

    The input path from which the output path can be derived.

  • dest (StrPath | None) –

    An output destination. Either None (only change extension), a destination directory (requires trailing slash) or a file. In all three cases, the output must have extension ext, unless ext is None or the extension is one of other_exts.

  • ext (str | None) –

    The (new) extension of the output, e.g. .pdf. When None, the extension of the input is preserved.

  • other_exts (Iterable[str], default: () ) –

    Other extensions that are allowed for the output.

Returns:

  • path_out

    A properly formatted output path.

Raises:

  • PathError

    If the output path is equal to the input path, or if the output path does not have the expected extension.

Source code in stepup/core/path.py
def make_path_out(
    path_in: StrPath, dest: StrPath | None, ext: str | None, other_exts: Iterable[str] = ()
) -> Path:
    """Construct an output path given the input path, a destination and the expected extension.

    Parameters
    ----------
    path_in
        The input path from which the output path can be derived.
    dest
        An output destination.
        Either `None` (only change extension),
        a destination directory (requires trailing slash) or a file.
        In all three cases, the output must have extension `ext`,
        unless `ext` is `None` or the extension is one of `other_exts`.
    ext
        The (new) extension of the output, e.g. `.pdf`.
        When `None`, the extension of the input is preserved.
    other_exts
        Other extensions that are allowed for the output.

    Returns
    -------
    path_out
        A properly formatted output path.

    Raises
    ------
    PathError
        If the output path is equal to the input path,
        or if the output path does not have the expected extension.
    """
    path_in = coerce_path(path_in)
    if dest is not None:
        dest = coerce_str(dest)
    if dest is None or dest.endswith("/"):
        path_out = path_in
        if ext is not None:
            path_out = Path(path_out.stem + ext)
        if dest is None:
            path_out = path_in.parent / path_out
        else:
            path_out = path_out.basename()
            if dest not in (".", "./"):
                path_out = Path(dest) / path_out
    else:
        path_out = Path(dest)
    if path_out == path_in:
        raise PathError(f"The output path cannot equal the input path: {path_out}")
    if not (ext is None or path_out.suffix == ext or path_out.suffix in other_exts):
        raise PathError(f"The output path does not have extension '{ext}': {path_out}.")
    return path_out

parent_dir(path)

The directory containing path, with the project root written as ..

A path ending in a separator names a directory, and its parent is then that same directory without the trailing separator. Strip the trailing separator before calling to get the directory one level up instead.

Parameters:

  • path (StrPath) –

    A path relative to the project root.

Returns:

  • parent

    The parent directory, never empty and without a trailing separator.

Source code in stepup/core/path.py
def parent_dir(path: StrPath) -> str:
    """The directory containing `path`, with the project root written as `.`.

    A path ending in a separator names a directory,
    and its parent is then that same directory without the trailing separator.
    Strip the trailing separator before calling
    to get the directory one level up instead.

    Parameters
    ----------
    path
        A path relative to the project root.

    Returns
    -------
    parent
        The parent directory, never empty and without a trailing separator.
    """
    return str(coerce_path(path).parent) or "."

short_path(path)

Shorten a path for display in a message.

A path inside the working directory is made relative to it, with a ./ prefix. Any other path is shown as it is: an absolute path is easier to read than a relative one climbing out of the tree, and a path like ~/.config/stepup.toml already says where it is.

Source code in stepup/core/path.py
def short_path(path: str | Path) -> Path:
    """Shorten a path for display in a message.

    A path inside the working directory is made relative to it, with a `./` prefix.
    Any other path is shown as it is:
    an absolute path is easier to read than a relative one climbing out of the tree,
    and a path like `~/.config/stepup.toml` already says where it is.
    """
    path = Path(path)
    if not path.startswith(os.getcwd() + os.sep):
        return path
    short = path.relpath()
    return short if short.startswith(".") else "./" / short

translate(path, workdir='.')

Normalize the path and, if relative, make it relative to STEPUP_ROOT.

Parameters:

  • path (StrPath) –

    The path to translate. If relative, it is assumed to be relative to workdir.

  • workdir (StrPath, default: '.' ) –

    The working directory. If relative, it is assumed to be relative to HERE.

Returns:

  • translated_path

    A path that can be interpreted in the working directory of the StepUp director.

Source code in stepup/core/path.py
def translate(path: StrPath, workdir: StrPath = ".") -> Path:
    """Normalize the path and, if relative, make it relative to `STEPUP_ROOT`.

    Parameters
    ----------
    path
        The path to translate.
        If relative, it is assumed to be relative to `workdir`.
    workdir
        The working directory.
        If relative, it is assumed to be relative to `HERE`.

    Returns
    -------
    translated_path
        A path that can be interpreted in the working directory of the StepUp director.
    """
    path = coerce_path(path).normpath()
    if not path.isabs():
        workdir = coerce_path(workdir).normpath()
        path = workdir / path
        if not workdir.isabs():
            root = get_stepup_root()
            here = Path(os.getenv("HERE", Path(".").relpath(root)))
            path = (root / here / path).normpath().relpath(root)
    return path

translate_back(path, workdir='.')

If relative, make the path relative to workdir, assuming it is relative to STEPUP_ROOT.

Parameters:

  • path (StrPath) –

    The path to translate. If relative, it is assumed to be relative to STEPUP_ROOT.

  • workdir (StrPath, default: '.' ) –

    The working directory. If relative, it is assumed to be relative to HERE.

Returns:

  • back_translated_path

    A path that can be interpreted in the working directory.

Source code in stepup/core/path.py
def translate_back(path: StrPath, workdir: StrPath = ".") -> Path:
    """If relative, make the path relative to `workdir`, assuming it is relative to `STEPUP_ROOT`.

    Parameters
    ----------
    path
        The path to translate.
        If relative, it is assumed to be relative to `STEPUP_ROOT`.
    workdir
        The working directory.
        If relative, it is assumed to be relative to `HERE`.

    Returns
    -------
    back_translated_path
        A path that can be interpreted in the working directory.
    """
    path = coerce_path(path).normpath()
    workdir = coerce_path(workdir).normpath()
    if path.isabs():
        if workdir.isabs() and path.startswith(workdir):
            path = Path(path).relpath(workdir)
    else:
        root = get_stepup_root()
        here = Path(os.getenv("HERE", Path(".").relpath(root)))
        path = Path(root / path).relpath(root / here / workdir)
    return path