rostree 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rostree/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """rostree: visualize ROS 2 package dependencies as a tree (library, TUI, CLI)."""
2
+
3
+ from rostree.api import (
4
+ build_tree,
5
+ get_package_info,
6
+ list_known_packages,
7
+ list_known_packages_by_source,
8
+ scan_workspaces,
9
+ WorkspaceInfo,
10
+ )
11
+
12
+ __all__ = [
13
+ "build_tree",
14
+ "get_package_info",
15
+ "list_known_packages",
16
+ "list_known_packages_by_source",
17
+ "scan_workspaces",
18
+ "WorkspaceInfo",
19
+ ]
20
+ __version__ = "0.1.0"
rostree/api.py ADDED
@@ -0,0 +1,117 @@
1
+ """Public API: use rostree from Python or from other tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from rostree.core.finder import (
8
+ find_package_path,
9
+ list_package_paths,
10
+ list_packages_by_source,
11
+ scan_for_workspaces,
12
+ WorkspaceInfo,
13
+ )
14
+ from rostree.core.parser import parse_package_xml, PackageInfo
15
+ from rostree.core.tree import DependencyNode, build_dependency_tree
16
+
17
+
18
+ def list_known_packages(
19
+ *,
20
+ extra_source_roots: list[Path] | None = None,
21
+ ) -> dict[str, Path]:
22
+ """
23
+ List all ROS 2 packages visible in the current environment.
24
+
25
+ Uses AMENT_PREFIX_PATH, COLCON_PREFIX_PATH, workspace source trees,
26
+ and optional extra_source_roots (user-added paths).
27
+ Returns a mapping from package name to path to its package.xml.
28
+ """
29
+ return list_package_paths(extra_source_roots=extra_source_roots)
30
+
31
+
32
+ def list_known_packages_by_source(
33
+ *,
34
+ extra_source_roots: list[Path] | None = None,
35
+ ) -> dict[str, list[str]]:
36
+ """
37
+ List packages grouped by source (System, Workspace, Other, Source, Added).
38
+
39
+ Lets you distinguish your workspace packages from ROS distro (System),
40
+ third-party (Other), unbuilt source (Source), and user-added (Added).
41
+ Returns dict mapping source_label -> sorted list of package names.
42
+ """
43
+ return list_packages_by_source(extra_source_roots=extra_source_roots)
44
+
45
+
46
+ def get_package_info(
47
+ package_name: str,
48
+ *,
49
+ extra_source_roots: list[Path] | None = None,
50
+ ) -> PackageInfo | None:
51
+ """
52
+ Get metadata and dependencies for a ROS 2 package by name.
53
+
54
+ Finds the package (install or source) and parses its package.xml.
55
+ Returns None if the package is not found or package.xml cannot be parsed.
56
+ """
57
+ path = find_package_path(package_name, extra_source_roots=extra_source_roots)
58
+ if path is None:
59
+ return None
60
+ return parse_package_xml(path)
61
+
62
+
63
+ def build_tree(
64
+ root_package: str,
65
+ *,
66
+ max_depth: int | None = None,
67
+ include_buildtool: bool = False,
68
+ runtime_only: bool = False,
69
+ extra_source_roots: list[Path] | None = None,
70
+ ) -> DependencyNode | None:
71
+ """
72
+ Build a full dependency tree for a ROS 2 package.
73
+
74
+ Args:
75
+ root_package: Name of the root package.
76
+ max_depth: Optional maximum depth; None = unlimited.
77
+ include_buildtool: Whether to include buildtool dependencies.
78
+ runtime_only: If True, only depend and exec_depend (faster, smaller tree).
79
+ extra_source_roots: Optional list of Paths to scan for packages (user-added).
80
+
81
+ Returns:
82
+ Root DependencyNode, or None if root package is not found.
83
+ """
84
+ return build_dependency_tree(
85
+ root_package,
86
+ max_depth=max_depth,
87
+ include_buildtool=include_buildtool,
88
+ runtime_only=runtime_only,
89
+ extra_source_roots=extra_source_roots,
90
+ )
91
+
92
+
93
+ def scan_workspaces(
94
+ roots: list[Path] | None = None,
95
+ *,
96
+ max_depth: int = 4,
97
+ include_home: bool = True,
98
+ include_opt_ros: bool = True,
99
+ ) -> list[WorkspaceInfo]:
100
+ """
101
+ Scan the host machine for ROS 2 workspaces.
102
+
103
+ Args:
104
+ roots: Directories to start scanning from. Defaults to common locations.
105
+ max_depth: How deep to recurse when looking for workspaces.
106
+ include_home: If True and roots is None, include ~/ros*_ws, etc.
107
+ include_opt_ros: If True and roots is None, include /opt/ros/* distros.
108
+
109
+ Returns:
110
+ List of WorkspaceInfo for each discovered workspace.
111
+ """
112
+ return scan_for_workspaces(
113
+ roots=roots,
114
+ max_depth=max_depth,
115
+ include_home=include_home,
116
+ include_opt_ros=include_opt_ros,
117
+ )
rostree/cli.py ADDED
@@ -0,0 +1,288 @@
1
+ """Command-line interface for rostree: scan workspaces, list packages, show dependency trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from rostree.core.finder import (
11
+ list_package_paths,
12
+ list_packages_by_source,
13
+ scan_for_workspaces,
14
+ )
15
+ from rostree.core.tree import build_dependency_tree, DependencyNode
16
+
17
+
18
+ def _print_tree_text(node: DependencyNode, indent: int = 0, prefix: str = "") -> None:
19
+ """Print a dependency tree as indented text."""
20
+ marker = "├── " if prefix else ""
21
+ version = f" ({node.version})" if node.version else ""
22
+ desc = (
23
+ f" - {node.description}"
24
+ if node.description and node.description not in ("(not found)", "(cycle)", "(parse error)")
25
+ else ""
26
+ )
27
+ if node.description in ("(not found)", "(cycle)", "(parse error)"):
28
+ desc = f" [{node.description}]"
29
+ print(f"{prefix}{marker}{node.name}{version}{desc}")
30
+
31
+ children = node.children
32
+ for i, child in enumerate(children):
33
+ is_last = i == len(children) - 1
34
+ child_prefix = prefix + (" " if is_last or not prefix else "│ ")
35
+ _print_tree_text(child, indent + 1, child_prefix if prefix else "")
36
+
37
+
38
+ def cmd_scan(args: argparse.Namespace) -> int:
39
+ """Scan for ROS 2 workspaces on the host."""
40
+ roots = [Path(p) for p in args.paths] if args.paths else None
41
+ workspaces = scan_for_workspaces(
42
+ roots=roots,
43
+ max_depth=args.depth,
44
+ include_home=not args.no_home,
45
+ include_opt_ros=not args.no_system,
46
+ )
47
+
48
+ if args.json:
49
+ print(json.dumps([ws.to_dict() for ws in workspaces], indent=2))
50
+ else:
51
+ if not workspaces:
52
+ print("No ROS 2 workspaces found.")
53
+ return 0
54
+ print(f"Found {len(workspaces)} workspace(s):\n")
55
+ for ws in workspaces:
56
+ status = []
57
+ if ws.has_src:
58
+ status.append("src")
59
+ if ws.has_install:
60
+ status.append("install")
61
+ if ws.has_build:
62
+ status.append("build")
63
+ status_str = ", ".join(status) if status else "empty"
64
+ print(f" {ws.path}")
65
+ print(f" Status: {status_str}")
66
+ print(f" Packages: {len(ws.packages)}")
67
+ if args.verbose and ws.packages:
68
+ for pkg in ws.packages[:20]:
69
+ print(f" - {pkg}")
70
+ if len(ws.packages) > 20:
71
+ print(f" ... and {len(ws.packages) - 20} more")
72
+ print()
73
+ return 0
74
+
75
+
76
+ def cmd_list(args: argparse.Namespace) -> int:
77
+ """List known ROS 2 packages."""
78
+ extra_roots = [Path(p) for p in args.source] if args.source else None
79
+
80
+ if args.by_source:
81
+ by_source = list_packages_by_source(extra_source_roots=extra_roots)
82
+ if args.json:
83
+ print(json.dumps(by_source, indent=2))
84
+ else:
85
+ if not by_source:
86
+ print("No packages found. Is your ROS 2 environment sourced?")
87
+ return 1
88
+ total = sum(len(pkgs) for pkgs in by_source.values())
89
+ print(f"Found {total} package(s) from {len(by_source)} source(s):\n")
90
+ for source, packages in by_source.items():
91
+ print(f" {source} ({len(packages)})")
92
+ if args.verbose:
93
+ for pkg in packages[:50]:
94
+ print(f" - {pkg}")
95
+ if len(packages) > 50:
96
+ print(f" ... and {len(packages) - 50} more")
97
+ print()
98
+ else:
99
+ packages = list_package_paths(extra_source_roots=extra_roots)
100
+ if args.json:
101
+ print(json.dumps({name: str(path) for name, path in packages.items()}, indent=2))
102
+ else:
103
+ if not packages:
104
+ print("No packages found. Is your ROS 2 environment sourced?")
105
+ return 1
106
+ print(f"Found {len(packages)} package(s):\n")
107
+ for name in sorted(packages.keys()):
108
+ if args.verbose:
109
+ print(f" {name}: {packages[name]}")
110
+ else:
111
+ print(f" {name}")
112
+ return 0
113
+
114
+
115
+ def cmd_tree(args: argparse.Namespace) -> int:
116
+ """Show dependency tree for a package."""
117
+ extra_roots = [Path(p) for p in args.source] if args.source else None
118
+
119
+ tree = build_dependency_tree(
120
+ args.package,
121
+ max_depth=args.depth,
122
+ runtime_only=args.runtime,
123
+ extra_source_roots=extra_roots,
124
+ )
125
+
126
+ if tree is None:
127
+ print(f"Package not found: {args.package}", file=sys.stderr)
128
+ return 1
129
+
130
+ if args.json:
131
+ print(json.dumps(tree.to_dict(), indent=2))
132
+ else:
133
+ _print_tree_text(tree)
134
+ return 0
135
+
136
+
137
+ def cmd_tui(args: argparse.Namespace) -> int:
138
+ """Launch the interactive TUI."""
139
+ from rostree.tui.app import DepTreeApp
140
+
141
+ app = DepTreeApp(root_package=args.package if hasattr(args, "package") else None)
142
+ app.run()
143
+ return 0
144
+
145
+
146
+ def main(argv: list[str] | None = None) -> int:
147
+ """Main entry point for the rostree CLI."""
148
+ parser = argparse.ArgumentParser(
149
+ prog="rostree",
150
+ description="Explore ROS 2 package dependencies from the command line.",
151
+ )
152
+ parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
153
+
154
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
155
+
156
+ # rostree scan
157
+ scan_parser = subparsers.add_parser(
158
+ "scan",
159
+ help="Scan for ROS 2 workspaces on the host machine",
160
+ description="Discover ROS 2 workspaces by scanning common locations or specified paths.",
161
+ )
162
+ scan_parser.add_argument(
163
+ "paths",
164
+ nargs="*",
165
+ help="Directories to scan (default: common locations like ~/ros*_ws, /opt/ros/*)",
166
+ )
167
+ scan_parser.add_argument(
168
+ "-d",
169
+ "--depth",
170
+ type=int,
171
+ default=4,
172
+ help="Maximum recursion depth (default: 4)",
173
+ )
174
+ scan_parser.add_argument(
175
+ "--no-home",
176
+ action="store_true",
177
+ help="Don't scan home directory locations",
178
+ )
179
+ scan_parser.add_argument(
180
+ "--no-system",
181
+ action="store_true",
182
+ help="Don't scan /opt/ros system installs",
183
+ )
184
+ scan_parser.add_argument(
185
+ "-v",
186
+ "--verbose",
187
+ action="store_true",
188
+ help="Show packages in each workspace",
189
+ )
190
+ scan_parser.add_argument(
191
+ "--json",
192
+ action="store_true",
193
+ help="Output as JSON",
194
+ )
195
+ scan_parser.set_defaults(func=cmd_scan)
196
+
197
+ # rostree list
198
+ list_parser = subparsers.add_parser(
199
+ "list",
200
+ help="List known ROS 2 packages",
201
+ description="List packages visible in the current ROS 2 environment.",
202
+ )
203
+ list_parser.add_argument(
204
+ "-s",
205
+ "--source",
206
+ action="append",
207
+ metavar="PATH",
208
+ help="Additional source directories to scan (can be repeated)",
209
+ )
210
+ list_parser.add_argument(
211
+ "--by-source",
212
+ action="store_true",
213
+ help="Group packages by source (System, Workspace, etc.)",
214
+ )
215
+ list_parser.add_argument(
216
+ "-v",
217
+ "--verbose",
218
+ action="store_true",
219
+ help="Show package paths",
220
+ )
221
+ list_parser.add_argument(
222
+ "--json",
223
+ action="store_true",
224
+ help="Output as JSON",
225
+ )
226
+ list_parser.set_defaults(func=cmd_list)
227
+
228
+ # rostree tree
229
+ tree_parser = subparsers.add_parser(
230
+ "tree",
231
+ help="Show dependency tree for a package",
232
+ description="Build and display the dependency tree for a ROS 2 package.",
233
+ )
234
+ tree_parser.add_argument(
235
+ "package",
236
+ help="Package name to show dependencies for",
237
+ )
238
+ tree_parser.add_argument(
239
+ "-d",
240
+ "--depth",
241
+ type=int,
242
+ default=None,
243
+ help="Maximum tree depth (default: unlimited)",
244
+ )
245
+ tree_parser.add_argument(
246
+ "-r",
247
+ "--runtime",
248
+ action="store_true",
249
+ help="Show only runtime dependencies (depend, exec_depend)",
250
+ )
251
+ tree_parser.add_argument(
252
+ "-s",
253
+ "--source",
254
+ action="append",
255
+ metavar="PATH",
256
+ help="Additional source directories to scan (can be repeated)",
257
+ )
258
+ tree_parser.add_argument(
259
+ "--json",
260
+ action="store_true",
261
+ help="Output as JSON",
262
+ )
263
+ tree_parser.set_defaults(func=cmd_tree)
264
+
265
+ # rostree tui (default if no command)
266
+ tui_parser = subparsers.add_parser(
267
+ "tui",
268
+ help="Launch the interactive terminal UI",
269
+ description="Start the interactive TUI for browsing packages and dependencies.",
270
+ )
271
+ tui_parser.add_argument(
272
+ "package",
273
+ nargs="?",
274
+ help="Optional: start with this package's tree",
275
+ )
276
+ tui_parser.set_defaults(func=cmd_tui)
277
+
278
+ args = parser.parse_args(argv)
279
+
280
+ # Default to TUI if no command specified
281
+ if args.command is None:
282
+ return cmd_tui(argparse.Namespace(package=None))
283
+
284
+ return args.func(args)
285
+
286
+
287
+ if __name__ == "__main__":
288
+ sys.exit(main())
@@ -0,0 +1,22 @@
1
+ """Core library: package discovery, package.xml parsing, dependency tree building."""
2
+
3
+ from rostree.core.finder import (
4
+ find_package_path,
5
+ list_package_paths,
6
+ list_packages_by_source,
7
+ scan_for_workspaces,
8
+ WorkspaceInfo,
9
+ )
10
+ from rostree.core.parser import parse_package_xml
11
+ from rostree.core.tree import DependencyNode, build_dependency_tree
12
+
13
+ __all__ = [
14
+ "find_package_path",
15
+ "list_package_paths",
16
+ "list_packages_by_source",
17
+ "scan_for_workspaces",
18
+ "WorkspaceInfo",
19
+ "parse_package_xml",
20
+ "DependencyNode",
21
+ "build_dependency_tree",
22
+ ]