Source code for isaaclab.cloner.path
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Segment-boundary-safe prim-path primitives for the cloner.
A prim path is a sequence of ``/``-delimited segments, not a character string, and the stdlib
string operations cross those boundaries silently: :meth:`str.startswith` reports that
``".../Robot"`` contains ``".../RobotArm"``. This module encodes the boundary semantics once.
Two kinds of prefix appear in the cloner. A *root* is a concrete prefix path
(``"/World/envs/env_0"``); :func:`relative_to`, :func:`under` and :func:`rebase` work against
one. A *template* carries a single ``"{}"`` clone slot standing for one segment
(``"/World/envs/env_{}/Robot"``); :func:`split`, :func:`match` and :func:`relativize` work
against one. Reach them through the package, as ``cloner.path.rebase(...)``.
"""
from __future__ import annotations
import re
from typing import NamedTuple
[docs]
class TemplateMatch(NamedTuple):
"""The ``"{}"`` text a template captured (``"3"``, or a wildcard ``".*"``), and the path below it."""
instance: str
suffix: str
[docs]
def split(template: str) -> tuple[str, str]:
"""Split a clone destination template around its ``"{}"`` clone slot.
The clone slot represents one concrete environment/instance path segment.
Args:
template: Destination path template with exactly one ``"{}"`` for the instance id.
Returns:
The ``(prefix, suffix)`` strings around the clone slot. A trailing slash is
insignificant, so an instance-root template (``".../env_{}"``) yields an empty suffix.
Raises:
ValueError: If ``template`` does not hold exactly one clone slot. A second slot would
survive into the suffix and break the later ``str.format`` that fills the first.
"""
template = template.rstrip("/") or "/"
slots = template.count("{}")
if slots != 1:
raise ValueError(f"Clone destination template must contain exactly one '{{}}', found {slots}: {template!r}.")
prefix, _, suffix = template.partition("{}")
return prefix, suffix
[docs]
def match(path_expr: str, template: str) -> TemplateMatch | None:
"""Match ``path_expr`` against a destination template, capturing the instance slot.
The ``"{}"`` slot matches one path segment's worth of text, whether a concrete id (``3``)
or a wildcard (``.*``). Recovering that text is the only way to tell which instance a
concrete clone path belongs to without slicing the string by hand.
Args:
path_expr: Path or path expression on the clone (destination) side.
template: Destination path template with ``"{}"`` for the instance id.
Returns:
A :class:`TemplateMatch` with the captured instance text and the asset-relative
suffix, or ``None`` when ``path_expr`` is not under the template's instance root.
Example:
>>> match("/World/envs/env_3/Robot/base", "/World/envs/env_{}/Robot")
TemplateMatch(instance='3', suffix='/base')
"""
prefix, template_suffix = split(template)
pattern = re.compile(re.escape(prefix) + r"([^/]+)" + re.escape(template_suffix))
matched = pattern.match(path_expr)
if matched is None:
return None
suffix = path_expr[matched.end() :]
if suffix and not suffix.startswith("/"):
return None
return TemplateMatch(matched.group(1), suffix)
[docs]
def relative_to(path: str, root: str) -> str | None:
"""Strip a concrete ``root`` prefix off ``path`` on a segment boundary.
Unlike slicing or :meth:`str.removeprefix`, this returns ``None`` rather than a
mid-segment remainder when ``path`` is not under ``root``.
Args:
path: Path to make relative.
root: Concrete subtree root. A trailing slash is insignificant, and ``"/"`` is the
root of every path.
Returns:
The suffix below ``root`` (starting with ``/``, or ``""`` when ``path`` equals
``root``), or ``None`` when ``path`` is not under ``root``.
"""
root = root.rstrip("/") or "/"
if path == root:
return ""
# "/" prefixes every path but contributes no segment of its own.
prefix = "" if root == "/" else root
if not path.startswith(prefix):
return None
suffix = path[len(prefix) :]
return suffix if suffix.startswith("/") else None
[docs]
def under(path: str, root: str) -> bool:
"""Return whether ``path`` lies within the subtree rooted at ``root``.
Boundary-correct membership test: unlike :meth:`str.startswith`, it does not match
across a segment boundary (``".../Robot"`` does not contain ``".../RobotArm"``).
Args:
path: Candidate descendant path.
root: Concrete subtree root.
Returns:
``True`` when ``path`` equals ``root`` or is a descendant of it.
"""
return relative_to(path, root) is not None
[docs]
def rebase(path: str, src_root: str, dst_root: str) -> str:
"""Rebase ``path`` from one concrete root prefix onto another on a segment boundary.
Unlike :meth:`str.replace`, this swaps only a boundary-aligned prefix and touches only
the leading occurrence.
Args:
path: Path to rebase.
src_root: Concrete source root prefix.
dst_root: Concrete destination root prefix.
Returns:
The rebased path, or ``path`` unchanged when it is not under ``src_root``.
"""
suffix = relative_to(path, src_root)
if suffix is None:
return path
return (dst_root.rstrip("/") + suffix) or "/"
[docs]
def relativize(path_expr: str, template: str) -> str | None:
"""Return the part of ``path_expr`` below a template's instance root.
The suffix half of :func:`match`, for callers that do not need the captured instance.
Args:
path_expr: Path or path expression on the clone (destination) side.
template: Destination path template with ``"{}"`` for the instance id.
Returns:
The asset-relative suffix (starting with ``/``, or ``""`` when ``path_expr`` is
exactly the template root), or ``None`` when ``path_expr`` is not under the root.
"""
matched = match(path_expr, template)
return None if matched is None else matched.suffix