Skip to content

Commit 060c137

Browse files
feat(utils): resolve_asset_path with @holosoma package alias
Add resolve_asset_path() for turning a config asset string into a concrete path, and generalize the package-path resolution it shares with resolve_data_file_path. The package-path trigger widens from the `holosoma/data` prefix (resolved against files("holosoma.data")) to the `holosoma/` prefix (resolved against files("holosoma")), and an `@holosoma/...` spelling alias is accepted for both. Self-locating inputs (holosoma/, @holosoma/, s3://, absolute paths) pass through; other relative inputs resolve against the package data root. Motivation: the scene-asset spawners (added in following commits) need one backend-agnostic way to resolve an asset path regardless of the working directory or how the package was installed. Widening the prefix to `holosoma/` lets assets outside data/ (and the @-alias form used by the new scene configs) resolve through the same code. Note this also changes resolve_data_file_path's accepted prefix for existing callers — they continue to work because `holosoma/data/...` still matches the broader `holosoma/` prefix. Covered by test_path_resolution.py.
1 parent 342829a commit 060c137

2 files changed

Lines changed: 126 additions & 18 deletions

File tree

src/holosoma/holosoma/utils/path.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ def resolve_data_file_path(file_path: str) -> str:
1616
Resolve a data file path.
1717
1818
Handles multiple path formats:
19-
1. S3 paths: "s3://bucket/path/to/file.npz" -> return as-is
20-
2. Package data paths: "holosoma/data/.../file.npz" -> resolved via importlib.resources
19+
1. S3 paths: "s3://bucket/path/to/file.npz" -> returned as-is
20+
2. Package paths: "holosoma/..." (or the "@holosoma/..." alias) -> resolved
21+
relative to the installed holosoma package via importlib.resources
2122
3. Absolute paths: "/path/to/file.npz" -> returned as-is
2223
4. Relative paths: "./data/file.npz" or "../data/file.npz" -> resolved relative to CWD
2324
@@ -28,28 +29,32 @@ def resolve_data_file_path(file_path: str) -> str:
2829
The resolved absolute path as a string
2930
3031
Examples:
31-
>>> # Package data
32-
>>> path = resolve_data_file_path("holosoma/data/motions/g1_29dof/whole_body_tracking/motion_crawl_slope.npz")
33-
>>> print(path)
34-
/path/to/installed/holosoma/data/motions/g1_29dof/whole_body_tracking/motion_crawl_slope.npz
32+
>>> # Package data (both forms resolve to the same location)
33+
>>> resolve_data_file_path("holosoma/data/robots/g1/g1_29dof.xml")
34+
'/path/to/installed/holosoma/data/robots/g1/g1_29dof.xml'
35+
>>> resolve_data_file_path("@holosoma/data/robots/g1/g1_29dof.xml")
36+
'/path/to/installed/holosoma/data/robots/g1/g1_29dof.xml'
3537
3638
>>> # User's custom file (absolute)
37-
>>> path = resolve_data_file_path("/home/user/my_motions/custom.npz")
38-
>>> print(path)
39-
/home/user/my_motions/custom.npz
40-
41-
>>> # User's custom file (relative)
42-
>>> path = resolve_data_file_path("./my_data/custom.npz")
43-
>>> print(path)
44-
/current/working/dir/my_data/custom.npz
39+
>>> resolve_data_file_path("/home/user/my_motions/custom.npz")
40+
'/home/user/my_motions/custom.npz'
41+
42+
>>> # User's custom file (relative to CWD)
43+
>>> resolve_data_file_path("./my_data/custom.npz")
44+
'/current/working/dir/my_data/custom.npz'
4545
"""
4646
# 1. If it's an S3 path, return as-is
4747
if file_path.startswith("s3://"):
4848
return file_path
49-
# 2. If starts with "holosoma/data", use importlib.resources
50-
if file_path.startswith("holosoma/data"):
51-
suffix = file_path[13:].lstrip("/") # Remove "holosoma/data" and leading slashes
52-
base = files("holosoma.data")
49+
50+
# 2. Package path. "@holosoma/..." is a spelling alias for "holosoma/...";
51+
# normalize it so both forms resolve via the same importlib.resources path
52+
# (robust for namespace/zipped packages, unlike __file__ arithmetic).
53+
if file_path.startswith("@holosoma/"):
54+
file_path = file_path[len("@") :] # "@holosoma/..." -> "holosoma/..."
55+
if file_path.startswith("holosoma/"):
56+
suffix = file_path[len("holosoma") :].lstrip("/") # path within the holosoma package
57+
base = files("holosoma")
5358
return str(base / suffix) if suffix else str(base)
5459

5560
# 3. If it's an absolute path, return as-is
@@ -60,3 +65,18 @@ def resolve_data_file_path(file_path: str) -> str:
6065
# 4. Otherwise, resolve relative path to absolute (relative to CWD)
6166
resolved = path_obj.resolve()
6267
return str(resolved)
68+
69+
70+
def resolve_asset_path(asset_file: str, asset_root: str | None) -> str:
71+
"""Resolve an asset path, optionally rooted under ``asset_root``.
72+
73+
A package path ("holosoma/..." or "@holosoma/...") or an absolute/S3 path is
74+
self-locating and resolved directly, ignoring ``asset_root`` — joining a root
75+
onto it would turn it into a bogus filesystem path. Only a plain relative path is
76+
joined onto ``asset_root`` (when one is given) before resolution. The self-locating
77+
cases are exactly the non-CWD-relative branches of :func:`resolve_data_file_path`.
78+
"""
79+
is_self_locating = asset_file.startswith(("holosoma/", "@holosoma/", "s3://")) or Path(asset_file).is_absolute()
80+
if asset_root and not is_self_locating:
81+
asset_file = f"{asset_root.rstrip('/')}/{asset_file}"
82+
return resolve_data_file_path(asset_file)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Unit tests for asset/data path resolution (pure, no simulator).
2+
3+
resolve_data_file_path / resolve_asset_path are the single path-classification layer that
4+
every backend funnels scene + robot assets through (package "holosoma/..." paths, the
5+
"@holosoma/..." alias, s3://, absolute, and asset_root-relative). Two real bugs this branch
6+
fixed lived exactly here (raw ``Path(asset_root)/...`` joins crashing on package paths), so
7+
each classification branch is locked down below.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import sys
13+
from pathlib import Path
14+
15+
if sys.version_info >= (3, 9):
16+
from importlib.resources import files
17+
else:
18+
from importlib_resources import files # type: ignore[import-not-found]
19+
20+
import pytest
21+
22+
from holosoma.utils.path import resolve_asset_path, resolve_data_file_path
23+
24+
PKG = str(files("holosoma")) # absolute path of the installed holosoma package
25+
26+
27+
# ----- resolve_data_file_path: one assertion per input class -----
28+
29+
30+
def test_s3_path_returned_as_is():
31+
assert resolve_data_file_path("s3://bucket/path/to/box.usd") == "s3://bucket/path/to/box.usd"
32+
33+
34+
def test_package_path_resolves_under_holosoma():
35+
assert (
36+
resolve_data_file_path("holosoma/data/scene_objects/boxes/small_box.urdf")
37+
== f"{PKG}/data/scene_objects/boxes/small_box.urdf"
38+
)
39+
40+
41+
def test_at_holosoma_alias_resolves_identically():
42+
plain = resolve_data_file_path("holosoma/data/scene_objects/boxes/small_box.urdf")
43+
alias = resolve_data_file_path("@holosoma/data/scene_objects/boxes/small_box.urdf")
44+
assert alias == plain == f"{PKG}/data/scene_objects/boxes/small_box.urdf"
45+
46+
47+
def test_package_root_with_trailing_slash_resolves_to_package_dir():
48+
# The "holosoma/..." rule keys on the trailing slash; "holosoma/" -> the package dir.
49+
# (Bare "holosoma" with no slash is NOT a package path — it's treated as relative.)
50+
assert resolve_data_file_path("holosoma/") == PKG
51+
52+
53+
def test_absolute_path_returned_as_is():
54+
assert resolve_data_file_path("/home/user/custom.npz") == "/home/user/custom.npz"
55+
56+
57+
def test_relative_path_resolved_against_cwd():
58+
assert resolve_data_file_path("my_data/custom.npz") == str(Path.cwd() / "my_data/custom.npz")
59+
60+
61+
# ----- resolve_asset_path: asset_root applies ONLY to plain relative paths -----
62+
63+
64+
@pytest.mark.parametrize(
65+
"asset_file",
66+
[
67+
"holosoma/data/scene_objects/boxes/small_box.urdf",
68+
"@holosoma/data/scene_objects/boxes/small_box.urdf",
69+
"s3://bucket/box.usd",
70+
"/abs/box.urdf",
71+
],
72+
)
73+
def test_self_locating_paths_ignore_asset_root(asset_file):
74+
"""Package/alias/s3/absolute paths are self-locating — asset_root must NOT be joined
75+
(joining would produce a bogus filesystem path)."""
76+
assert resolve_asset_path(asset_file, asset_root="/some/root") == resolve_data_file_path(asset_file)
77+
78+
79+
def test_relative_path_joined_onto_asset_root():
80+
assert resolve_asset_path("box.urdf", asset_root="/some/root") == resolve_data_file_path("/some/root/box.urdf")
81+
82+
83+
def test_relative_path_strips_trailing_slash_on_root():
84+
assert resolve_asset_path("box.urdf", asset_root="/some/root/") == resolve_data_file_path("/some/root/box.urdf")
85+
86+
87+
def test_relative_path_without_root_falls_back_to_cwd():
88+
assert resolve_asset_path("box.urdf", asset_root=None) == str(Path.cwd() / "box.urdf")

0 commit comments

Comments
 (0)