colcon-uv 0.0.1__py2.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.
colcon_uv/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Colcon extension for UV-based Python packages."""
2
+
3
+ __version__ = "0.0.1"
colcon_uv/cli.py ADDED
@@ -0,0 +1,102 @@
1
+ """Command-line interface for colcon-uv."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from colcon_core.plugin_system import satisfies_version
9
+ from colcon_core.verb import VerbExtensionPoint
10
+
11
+ from .dependencies.install import discover_packages, install_dependencies
12
+
13
+
14
+ class UvVerb(VerbExtensionPoint):
15
+ """UV command for colcon."""
16
+
17
+ def __init__(self): # noqa: D107
18
+ super().__init__()
19
+ satisfies_version(VerbExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
20
+
21
+ def add_arguments(self, *, parser): # noqa: D102
22
+ subparsers = parser.add_subparsers(dest="uv_command", help="UV commands")
23
+
24
+ # Install subcommand
25
+ install_parser = subparsers.add_parser(
26
+ "install", help="Install dependencies for UV-based packages"
27
+ )
28
+ install_parser.add_argument(
29
+ "--base-paths",
30
+ nargs="*",
31
+ help="The base paths to recursively crawl for packages",
32
+ )
33
+ install_parser.add_argument(
34
+ "--uv-args", nargs="*", help="Additional arguments to pass to UV"
35
+ )
36
+
37
+ def main(self, *, context): # noqa: D102
38
+ args = context.args
39
+ logger = logging.getLogger("colcon.uv.cli")
40
+
41
+ if not hasattr(args, "uv_command") or args.uv_command != "install":
42
+ logger.error("Usage: colcon uv install [options]")
43
+ return 1
44
+
45
+ # Get base paths
46
+ base_paths = args.base_paths
47
+ if not base_paths:
48
+ base_paths = [Path(".")]
49
+ else:
50
+ base_paths = [Path(p) for p in base_paths]
51
+
52
+ # Discover UV packages using the install.py logic
53
+ try:
54
+ packages = discover_packages(base_paths)
55
+ except SystemExit:
56
+ logger.warning("No UV packages found")
57
+ return 0
58
+
59
+ # Install dependencies for each package using install.py logic
60
+ install_base = Path("install") # Default install base
61
+ merge_install = False # Default to separate installs
62
+
63
+ for package in packages:
64
+ try:
65
+ install_dependencies(package, install_base, merge_install)
66
+ except Exception as e:
67
+ logger.error(
68
+ f"Failed to install dependencies for '{package.name}': {e}"
69
+ )
70
+ return 1
71
+
72
+ return 0
73
+
74
+
75
+ def main():
76
+ """Run the CLI."""
77
+ parser = argparse.ArgumentParser(
78
+ description="Install dependencies for UV-based packages"
79
+ )
80
+ parser.add_argument(
81
+ "--base-paths",
82
+ nargs="*",
83
+ help="The base paths to recursively crawl for packages",
84
+ )
85
+ parser.add_argument(
86
+ "--uv-args", nargs="*", help="Additional arguments to pass to UV"
87
+ )
88
+
89
+ args = parser.parse_args()
90
+
91
+ # Set up logging
92
+ logging.basicConfig(
93
+ level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s"
94
+ )
95
+
96
+ # Run the UV verb
97
+ verb = UvVerb()
98
+ return verb.main(context=argparse.Namespace(args=args))
99
+
100
+
101
+ if __name__ == "__main__":
102
+ sys.exit(main())
File without changes
@@ -0,0 +1,187 @@
1
+ """Install dependencies for UV packages."""
2
+
3
+ import argparse
4
+ import logging
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ import tomli
11
+
12
+ logger = logging.getLogger("colcon.uv.dependencies")
13
+
14
+
15
+ class NotAUvPackageError(Exception):
16
+ """Raised when a directory is not a UV package."""
17
+
18
+ pass
19
+
20
+
21
+ class UvPackage:
22
+ """Represents a UV package."""
23
+
24
+ def __init__(self, path: Path, logger=None):
25
+ """Initialize UV package."""
26
+ self.path = path
27
+ self.logger = logger or logging.getLogger(__name__)
28
+
29
+ self.pyproject_file = path / "pyproject.toml"
30
+ if not self.pyproject_file.exists():
31
+ raise NotAUvPackageError(f"No pyproject.toml found in {path}")
32
+
33
+ # Load pyproject.toml
34
+ with open(self.pyproject_file, "rb") as f:
35
+ self.pyproject_data = tomli.load(f)
36
+
37
+ # Check if it's a UV package
38
+ if (
39
+ "tool" not in self.pyproject_data
40
+ or "colcon-uv-ros" not in self.pyproject_data["tool"]
41
+ ):
42
+ raise NotAUvPackageError(
43
+ f"No [tool.colcon-uv-ros] section found in {self.pyproject_file}"
44
+ )
45
+
46
+ # Get package name
47
+ if (
48
+ "project" in self.pyproject_data
49
+ and "name" in self.pyproject_data["project"]
50
+ ):
51
+ self.name = self.pyproject_data["project"]["name"]
52
+ else:
53
+ self.name = path.name
54
+
55
+
56
+ def main():
57
+ """Main entry point for UV dependency installation."""
58
+ args = _parse_args()
59
+ logging.basicConfig(
60
+ level=logging.DEBUG if args.verbose else logging.INFO,
61
+ format="%(levelname)s:%(name)s: %(message)s",
62
+ )
63
+
64
+ for project in discover_packages(args.base_paths):
65
+ logger.info(f"Installing dependencies for {project.path.name}...")
66
+ install_dependencies(project, args.install_base, args.merge_install)
67
+
68
+ logger.info("Dependencies installed!")
69
+
70
+
71
+ def discover_packages(base_paths: List[Path]) -> List[UvPackage]:
72
+ """Discover UV packages in the given base paths."""
73
+ projects: List[UvPackage] = []
74
+
75
+ potential_packages = []
76
+ for path in base_paths:
77
+ potential_packages += list(path.glob("*"))
78
+
79
+ for path in potential_packages:
80
+ if path.is_dir():
81
+ try:
82
+ project = UvPackage(path)
83
+ except NotAUvPackageError:
84
+ continue
85
+ else:
86
+ projects.append(project)
87
+
88
+ if len(projects) == 0:
89
+ base_paths_str = ", ".join([str(p) for p in base_paths])
90
+ logger.error(
91
+ f"No UV packages were found in the following paths: {base_paths_str}"
92
+ )
93
+ sys.exit(1)
94
+
95
+ return projects
96
+
97
+
98
+ def install_dependencies(
99
+ project: UvPackage, install_base: Path, merge_install: bool
100
+ ) -> None:
101
+ """Install dependencies for a UV package using UV."""
102
+ # Handle both contexts:
103
+ # 1. Direct install: install_base = /install, need to add package name
104
+ # 2. Build task: install_base = /install/package_name, already included
105
+
106
+ if not merge_install:
107
+ # Check if install_base already ends with the package name
108
+ if install_base.name != project.name:
109
+ install_base /= project.name
110
+
111
+ # Create the install directory first
112
+ install_base.mkdir(parents=True, exist_ok=True)
113
+
114
+ # Venv path - this should be /install/PACKAGE_NAME/venv/
115
+ venv_path = install_base / "venv"
116
+
117
+ # Create virtual environment at the target location with system packages access
118
+ # --system-site-packages is needed because ROS 2 packages like rclpy are installed
119
+ # system-wide (not available on PyPI) and our nodes need access to them
120
+ subprocess.run(["uv", "venv", "--system-site-packages", str(venv_path)], check=True)
121
+
122
+ # Install dependencies and the package itself to the target venv
123
+ # Use --python to specify the target venv's python
124
+ python_exe = venv_path / "bin" / "python"
125
+ subprocess.run(
126
+ ["uv", "pip", "install", "--python", str(python_exe), "-e", str(project.path)],
127
+ check=True,
128
+ )
129
+
130
+
131
+ def install_dependencies_from_descriptor(
132
+ pkg_descriptor, install_base: Path, merge_install: bool
133
+ ):
134
+ """Install dependencies from a PackageDescriptor object.
135
+
136
+ This is a convenience function for use by colcon build tasks.
137
+ """
138
+ try:
139
+ uv_package = UvPackage(pkg_descriptor.path)
140
+ install_dependencies(uv_package, install_base, merge_install)
141
+ except NotAUvPackageError as e:
142
+ # Skip packages that aren't UV packages
143
+ logger.debug(f"Skipping non-UV package {pkg_descriptor.name}: {e}")
144
+ return
145
+
146
+
147
+ def _parse_args() -> argparse.Namespace:
148
+ """Parse command line arguments."""
149
+ parser = argparse.ArgumentParser(
150
+ description="Searches for UV packages and installs their dependencies "
151
+ "to a configurable install base"
152
+ )
153
+
154
+ parser.add_argument(
155
+ "--base-paths",
156
+ nargs="+",
157
+ type=Path,
158
+ default=[Path.cwd()],
159
+ help="The paths to start looking for UV projects in. Defaults to the "
160
+ "current directory.",
161
+ )
162
+
163
+ parser.add_argument(
164
+ "--install-base",
165
+ type=Path,
166
+ default=Path("install"),
167
+ help="The base path for all install prefixes (default: install)",
168
+ )
169
+
170
+ parser.add_argument(
171
+ "--merge-install",
172
+ action="store_true",
173
+ help="Merge all install prefixes into a single location",
174
+ )
175
+
176
+ parser.add_argument(
177
+ "-v",
178
+ "--verbose",
179
+ action="store_true",
180
+ help="If provided, debug logs will be printed",
181
+ )
182
+
183
+ return parser.parse_args()
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()
@@ -0,0 +1,5 @@
1
+ """Package augmentation for UV-based Python packages."""
2
+
3
+ from colcon_uv.package_augmentation.uv import UvPackageAugmentation
4
+
5
+ __all__ = ["UvPackageAugmentation"]
@@ -0,0 +1,122 @@
1
+ """Augment packages that use UV for Python dependency management."""
2
+
3
+ import logging
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from colcon_core.package_augmentation import PackageAugmentationExtensionPoint
8
+ from colcon_core.package_augmentation.python import create_dependency_descriptor
9
+ from colcon_core.package_descriptor import PackageDescriptor
10
+ from colcon_core.plugin_system import satisfies_version
11
+
12
+ logger = logging.getLogger("colcon.uv.package_augmentation")
13
+
14
+
15
+ class UvPackageAugmentation(PackageAugmentationExtensionPoint):
16
+ """Augment packages that use UV for Python dependency management."""
17
+
18
+ _TOOL_SECTION = "tool"
19
+ _COLCON_UV_ROS_SECTION = "colcon-uv-ros"
20
+ _DEPENDENCIES_SECTION = "dependencies"
21
+ _DATA_FILES_SECTION = "data-files"
22
+ _DEPEND_LIST = "depend"
23
+ _BUILD_DEPEND_LIST = "build_depend"
24
+ _EXEC_DEPEND_LIST = "exec_depend"
25
+ _TEST_DEPEND_LIST = "test_depend"
26
+ _PACKAGE_BUILD_CATEGORY = "build"
27
+ _PACKAGE_EXEC_CATEGORY = "run"
28
+ _PACKAGE_TEST_CATEGORY = "test"
29
+
30
+ def __init__(self): # noqa: D107
31
+ super().__init__()
32
+ satisfies_version(
33
+ PackageAugmentationExtensionPoint.EXTENSION_POINT_VERSION, "^1.0"
34
+ )
35
+
36
+ def augment_package(
37
+ self, desc: PackageDescriptor, *, additional_argument_names=None
38
+ ): # noqa: D102
39
+ if desc.type != "uv":
40
+ return
41
+
42
+ # Read pyproject.toml to get package metadata
43
+ pyproject_toml = desc.path / "pyproject.toml"
44
+ if not pyproject_toml.exists():
45
+ return
46
+
47
+ try:
48
+ import tomli
49
+
50
+ with open(pyproject_toml, "rb") as f:
51
+ data = tomli.load(f)
52
+ if (
53
+ self._TOOL_SECTION in data
54
+ and self._COLCON_UV_ROS_SECTION in data[self._TOOL_SECTION]
55
+ ):
56
+ uv_config = data[self._TOOL_SECTION][self._COLCON_UV_ROS_SECTION]
57
+
58
+ # Set package name if not already set
59
+ if not desc.name and "name" in uv_config:
60
+ desc.name = uv_config["name"]
61
+
62
+ # Handle dependencies
63
+ if self._DEPENDENCIES_SECTION in uv_config:
64
+ deps = uv_config[self._DEPENDENCIES_SECTION]
65
+
66
+ # Build dependencies
67
+ if self._BUILD_DEPEND_LIST in deps:
68
+ build_deps = set(deps[self._BUILD_DEPEND_LIST])
69
+ else:
70
+ build_deps = set()
71
+
72
+ # Exec dependencies
73
+ if self._EXEC_DEPEND_LIST in deps:
74
+ exec_deps = set(deps[self._EXEC_DEPEND_LIST])
75
+ else:
76
+ exec_deps = set()
77
+
78
+ # Test dependencies
79
+ if self._TEST_DEPEND_LIST in deps:
80
+ test_deps = set(deps[self._TEST_DEPEND_LIST])
81
+ else:
82
+ test_deps = set()
83
+
84
+ # Handle general dependencies (add to both build and exec)
85
+ if self._DEPEND_LIST in deps:
86
+ depends = deps[self._DEPEND_LIST]
87
+ build_deps.update(depends)
88
+ exec_deps.update(depends)
89
+
90
+ # Set dependencies in descriptor
91
+ desc.dependencies[self._PACKAGE_BUILD_CATEGORY] = {
92
+ create_dependency_descriptor(dep) for dep in build_deps
93
+ }
94
+ desc.dependencies[self._PACKAGE_EXEC_CATEGORY] = {
95
+ create_dependency_descriptor(dep) for dep in exec_deps
96
+ }
97
+ desc.dependencies[self._PACKAGE_TEST_CATEGORY] = {
98
+ create_dependency_descriptor(dep) for dep in test_deps
99
+ }
100
+
101
+ # Handle data files
102
+ if self._DATA_FILES_SECTION in uv_config:
103
+ data_files = uv_config[self._DATA_FILES_SECTION]
104
+ for dest_dir, files in data_files.items():
105
+ # Create destination directory if it doesn't exist
106
+ dest_path = Path(dest_dir)
107
+ dest_path.mkdir(parents=True, exist_ok=True)
108
+
109
+ # Copy each file
110
+ for file in files:
111
+ src_path = desc.path / file
112
+ if src_path.is_file():
113
+ shutil.copy2(src_path, dest_path / src_path.name)
114
+ elif src_path.is_dir():
115
+ shutil.copytree(
116
+ src_path,
117
+ dest_path / src_path.name,
118
+ dirs_exist_ok=True,
119
+ )
120
+ except ImportError:
121
+ # If tomli is not available, we'll just use basic metadata
122
+ pass
@@ -0,0 +1,5 @@
1
+ """Package identification for UV-based Python packages."""
2
+
3
+ from colcon_uv.package_identification.uv import UvPackageIdentification
4
+
5
+ __all__ = ["UvPackageIdentification"]
@@ -0,0 +1,53 @@
1
+ """Identify packages that use UV for Python dependency management."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ from colcon_core.package_identification import PackageIdentificationExtensionPoint
7
+ from colcon_core.plugin_system import satisfies_version
8
+
9
+ logger = logging.getLogger("colcon.uv.package_identification")
10
+
11
+
12
+ class UvPackageIdentification(PackageIdentificationExtensionPoint):
13
+ """Identify packages that use UV for Python dependency management."""
14
+
15
+ # High priority to supersede other identifiers like ament_python
16
+ PRIORITY = 200
17
+
18
+ def __init__(self): # noqa: D107
19
+ super().__init__()
20
+ satisfies_version(
21
+ PackageIdentificationExtensionPoint.EXTENSION_POINT_VERSION, "^1.0"
22
+ )
23
+
24
+ def identify(self, desc): # noqa: D102
25
+ if desc.type is not None and desc.type != "uv.python":
26
+ return
27
+
28
+ pyproject_toml = desc.path / "pyproject.toml"
29
+ if not pyproject_toml.exists():
30
+ return
31
+
32
+ # Check if it's a UV package by looking for our custom section
33
+ try:
34
+ import tomli
35
+
36
+ with open(pyproject_toml, "rb") as f:
37
+ data = tomli.load(f)
38
+ if "tool" in data and "colcon-uv-ros" in data["tool"]:
39
+ desc.type = "uv.python"
40
+ # Get name from pyproject.toml if available, otherwise use directory name
41
+ if "project" in data and "name" in data["project"]:
42
+ desc.name = data["project"]["name"]
43
+ else:
44
+ desc.name = os.path.basename(desc.path)
45
+ return
46
+ except ImportError:
47
+ # If tomli is not available, we'll just check for the string
48
+ with open(pyproject_toml) as f:
49
+ content = f.read()
50
+ if "[tool.colcon-uv-ros]" in content:
51
+ desc.type = "uv.python"
52
+ desc.name = os.path.basename(desc.path)
53
+ return
@@ -0,0 +1,6 @@
1
+ """Tasks for UV-based Python packages."""
2
+
3
+ from colcon_uv.task.uv.build import UvBuildTask
4
+ from colcon_uv.task.uv.test import UvTestTask
5
+
6
+ __all__ = ["UvBuildTask", "UvTestTask"]
@@ -0,0 +1,6 @@
1
+ """UV-specific tasks for colcon."""
2
+
3
+ from colcon_uv.task.uv.build import UvBuildTask
4
+ from colcon_uv.task.uv.test import UvTestTask
5
+
6
+ __all__ = ["UvBuildTask", "UvTestTask"]
@@ -0,0 +1,170 @@
1
+ """Build task for UV-based Python packages."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from colcon_core.logging import colcon_logger
7
+ from colcon_core.plugin_system import satisfies_version
8
+ from colcon_core.task import TaskExtensionPoint
9
+
10
+ from colcon_uv.dependencies.install import install_dependencies_from_descriptor
11
+
12
+ logger = colcon_logger.getChild("colcon.uv.task.build")
13
+
14
+
15
+ class UvBuildTask(TaskExtensionPoint):
16
+ """Build task for UV-based Python packages."""
17
+
18
+ def __init__(self): # noqa: D107
19
+ super().__init__()
20
+ satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
21
+
22
+ def add_arguments(self, *, parser): # noqa: D102
23
+ parser.add_argument(
24
+ "--uv-args",
25
+ nargs="*",
26
+ metavar="*",
27
+ type=str.lstrip,
28
+ help="Pass arguments to UV. "
29
+ "Arguments matching other options must be prefixed by a space,\n"
30
+ 'e.g. --uv-args " --help"',
31
+ )
32
+
33
+ async def build(self, *, additional_hooks=None):
34
+ pkg = self.context.pkg
35
+ args = self.context.args
36
+
37
+ # UV sync handles EVERYTHING - deps + package installation
38
+ logger.info("Syncing dependencies and installing package...")
39
+ install_dependencies_from_descriptor(pkg, Path(args.install_base), False)
40
+
41
+ # Handle ROS-specific stuff (data files, environment hooks, etc.)
42
+ return_code = await self._add_data_files()
43
+ if return_code != 0:
44
+ return return_code
45
+
46
+ # Create executable symlinks for ROS 2
47
+ self._create_executable_symlinks()
48
+
49
+ # Add environment hooks for ROS
50
+ self._create_environment_hooks()
51
+
52
+ async def _add_data_files(self) -> int:
53
+ """Install data files based on the [tool.colcon-uv-ros.data-files] table."""
54
+ pkg = self.context.pkg
55
+ args = self.context.args
56
+
57
+ pyproject_toml = pkg.path / "pyproject.toml"
58
+
59
+ try:
60
+ import tomli
61
+
62
+ with open(pyproject_toml, "rb") as f:
63
+ data = tomli.load(f)
64
+
65
+ if "tool" not in data or "colcon-uv-ros" not in data["tool"]:
66
+ return 0
67
+
68
+ uv_config = data["tool"]["colcon-uv-ros"]
69
+ if "data-files" not in uv_config:
70
+ return 0
71
+
72
+ data_files = uv_config["data-files"]
73
+ except (ImportError, FileNotFoundError, KeyError):
74
+ return 0
75
+
76
+ if not isinstance(data_files, dict):
77
+ logger.error("data-files must be a table")
78
+ return 1
79
+
80
+ for destination, sources in data_files.items():
81
+ if not isinstance(sources, list):
82
+ logger.error(f"Field '{destination}' in data-files must be an array")
83
+ return 1
84
+
85
+ dest_path = Path(args.install_base) / destination
86
+ dest_path.mkdir(parents=True, exist_ok=True)
87
+
88
+ for source in sources:
89
+ source_path = pkg.path / Path(source)
90
+ if source_path.exists():
91
+ if source_path.is_dir():
92
+ shutil.copytree(
93
+ source_path,
94
+ dest_path / source_path.name,
95
+ dirs_exist_ok=True,
96
+ )
97
+ else:
98
+ shutil.copy2(source_path, dest_path)
99
+
100
+ return 0
101
+
102
+ def _create_executable_symlinks(self):
103
+ """Create symlinks for executables in the standard ROS 2 location."""
104
+ pkg = self.context.pkg
105
+ args = self.context.args
106
+
107
+ # Read pyproject.toml to get executable entry points
108
+ pyproject_toml = pkg.path / "pyproject.toml"
109
+ if not pyproject_toml.exists():
110
+ return
111
+
112
+ try:
113
+ import tomli
114
+
115
+ with open(pyproject_toml, "rb") as f:
116
+ data = tomli.load(f)
117
+
118
+ if "project" not in data or "scripts" not in data["project"]:
119
+ return
120
+
121
+ scripts = data["project"]["scripts"]
122
+ except (ImportError, FileNotFoundError, KeyError):
123
+ return
124
+
125
+ # Create lib/package_name directory for executables
126
+ lib_dir = Path(args.install_base) / "lib" / pkg.name
127
+ lib_dir.mkdir(parents=True, exist_ok=True)
128
+
129
+ # Create symlinks for each executable
130
+ venv_bin = Path(args.install_base) / "venv" / "bin"
131
+
132
+ for script_name, _entry_point in scripts.items():
133
+ venv_executable = venv_bin / script_name
134
+ ros_executable = lib_dir / script_name
135
+
136
+ if venv_executable.exists():
137
+ # Remove existing symlink if it exists
138
+ if ros_executable.exists() or ros_executable.is_symlink():
139
+ ros_executable.unlink()
140
+
141
+ # Create symlink
142
+ ros_executable.symlink_to(venv_executable)
143
+ logger.info(
144
+ f"Created executable symlink: {ros_executable} -> {venv_executable}"
145
+ )
146
+
147
+ def _create_environment_hooks(self):
148
+ """Create ROS environment hooks."""
149
+ from colcon_core.environment import (
150
+ create_environment_hooks,
151
+ create_environment_scripts,
152
+ )
153
+ from colcon_core.shell import create_environment_hook
154
+
155
+ pkg = self.context.pkg
156
+ args = self.context.args
157
+
158
+ additional_hooks = create_environment_hook(
159
+ "ament_prefix_path",
160
+ Path(args.install_base) / pkg.name,
161
+ pkg.name,
162
+ "AMENT_PREFIX_PATH",
163
+ "",
164
+ mode="prepend",
165
+ )
166
+
167
+ hooks = create_environment_hooks(Path(args.install_base) / pkg.name, pkg.name)
168
+ create_environment_scripts(
169
+ pkg, args, default_hooks=list(hooks), additional_hooks=additional_hooks
170
+ )
@@ -0,0 +1,47 @@
1
+ """Test task for UV-based Python packages."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from colcon_core.logging import colcon_logger
7
+ from colcon_core.plugin_system import satisfies_version
8
+ from colcon_core.task import TaskExtensionPoint
9
+
10
+ logger = colcon_logger.getChild("colcon.uv.task.test")
11
+
12
+
13
+ class UvTestTask(TaskExtensionPoint):
14
+ """Test task for UV-based Python packages."""
15
+
16
+ def __init__(self): # noqa: D107
17
+ super().__init__()
18
+ satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
19
+
20
+ def add_arguments(self, *, parser): # noqa: D102
21
+ parser.add_argument(
22
+ "--uv-args",
23
+ nargs="*",
24
+ metavar="*",
25
+ type=str.lstrip,
26
+ help="Pass arguments to UV. "
27
+ "Arguments matching other options must be prefixed by a space,\n"
28
+ 'e.g. --uv-args " --help"',
29
+ )
30
+
31
+ async def test(self, *, additional_hooks=None): # noqa: D102
32
+ pkg = self.context.pkg
33
+ args = self.context.args
34
+
35
+ logger.info("Testing UV package in '{args.path}'".format_map(locals()))
36
+
37
+ # Get virtual environment path
38
+ venv_path = Path(args.build_base) / "venv"
39
+ if not venv_path.exists():
40
+ logger.error(
41
+ "Virtual environment not found. Please build the package first."
42
+ )
43
+ return
44
+
45
+ # Run tests using pytest
46
+ logger.info("Running tests...")
47
+ subprocess.run([str(venv_path / "bin" / "pytest"), str(pkg.path)], check=True)
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: colcon-uv
3
+ Version: 0.0.1
4
+ Summary: A Colcon extension providing support for Python projects that use UV
5
+ Project-URL: Homepage, https://github.com/nzlz/colcon-uv
6
+ Author-email: Nestor Gonzalez <nestor94gonzalez@gmail.com>
7
+ Maintainer-email: Nestor Gonzalez <nestor94gonzalez@gmail.com>
8
+ License: BSD-3-Clause
9
+ Keywords: colcon
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Plugins
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Requires-Dist: colcon-core~=0.6
17
+ Requires-Dist: tomli>=1.2.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # colcon-uv
21
+
22
+ [![CI](https://github.com/nzlz/colcon-uv/actions/workflows/ci.yml/badge.svg)](https://github.com/nzlz/colcon-uv/actions/workflows/ci.yml)
23
+
24
+ A **colcon extension** for building and testing Python packages that use **[uv](https://github.com/astral-sh/uv)** for dependency management.
25
+
26
+ ## Features
27
+
28
+ - **Fast Dependency Management**: Leverages UV's lightning-fast dependency resolution and installation
29
+ - **Modern Python Packaging**: Support for `pyproject.toml`-based packages following PEP 517/518 standards
30
+ - **ROS Integration**: Seamless integration with colcon build system and ROS package management
31
+ - **Dependency Isolation**: Prevents dependency conflicts between packages
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install colcon-uv
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ ### Data Files
42
+
43
+ Similar to [colcon-poetry-ros](https://github.com/UrbanMachine/colcon-poetry-ros), you can specify data files using the `[tool.colcon-uv-ros.data-files]` section:
44
+
45
+ ```toml
46
+ [tool.colcon-uv-ros.data-files]
47
+ "share/ament_index/resource_index/packages" = ["resource/{package_name}"]
48
+ "share/{package_name}" = ["package.xml", "launch/", "config/"]
49
+ "lib/{package_name}" = ["scripts/"]
50
+ ```
51
+
52
+ **Required entries** for all ROS packages:
53
+
54
+ ```toml
55
+ [tool.colcon-uv-ros.data-files]
56
+ "share/ament_index/resource_index/packages" = ["resource/{package_name}"]
57
+ "share/{package_name}" = ["package.xml"]
58
+ ```
59
+
60
+ ### Package Dependencies
61
+
62
+ Specify package dependencies for build ordering and to use system libraries (fetched from system paths, not installed in virtual environment):
63
+
64
+ ```toml
65
+ [tool.colcon-uv-ros.dependencies]
66
+ depend = ["rclpy", "geometry_msgs"] # System packages (adds to both build_depend and exec_depend)
67
+ build_depend = ["bar_package"] # Build-time only dependency
68
+ exec_depend = ["std_msgs"] # Runtime system library
69
+ test_depend = ["qux_package"] # Test-time only dependency
70
+ ```
71
+
72
+ **Important**: ROS system libraries like `rclpy`, `geometry_msgs`, `std_msgs`, etc. should be listed here so they are resolved from the system installation rather than being installed into the virtual environment.
@@ -0,0 +1,16 @@
1
+ colcon_uv/__init__.py,sha256=8KK-O_XfVuu9Ubj2TcQft9hNogiJQ40U-Iip8vbu_pE,76
2
+ colcon_uv/cli.py,sha256=TX4_FP1FKDpR0pGkiiKc0-OQThQoYtqEs0EK-gpaJiE,3033
3
+ colcon_uv/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ colcon_uv/dependencies/install.py,sha256=v9oNIX8JhQtgus3coxu1oshWps7gupqTVRXSMwD_NMM,5637
5
+ colcon_uv/package_augmentation/__init__.py,sha256=_Z2QV8zXzG_8SfobclzxMrlLr0AvYuoMksEXwJEVl8Y,163
6
+ colcon_uv/package_augmentation/uv.py,sha256=WuE76pJ1OXqrKNh2__KE9NMQMlK6eQg6Cb-6YK6jQT0,5132
7
+ colcon_uv/package_identification/__init__.py,sha256=uLfNAqg-obGtQoLrwyUGtzTxT_dqLe9KsBfl-eTMjIc,171
8
+ colcon_uv/package_identification/uv.py,sha256=H1snv1YG349Jmxz0E8_Kh50UDSVOwngUirkd_9Y-iws,1978
9
+ colcon_uv/task/__init__.py,sha256=A09krWgApqaeh_YPpzURMSloItEz3SCMkd-pDmnozb4,178
10
+ colcon_uv/task/uv/__init__.py,sha256=1htbhWTxp42siw-xHbRrcRQCSuK6zIp6N_nOgYcf1_g,172
11
+ colcon_uv/task/uv/build.py,sha256=GalspkAe4FhuSkMf-cvj8-8_ySVVkbykeuSr3eeZ3_0,5756
12
+ colcon_uv/task/uv/test.py,sha256=1CAiHzJJIgFEOHJKesOV-xJGlsFA7V1iI247-zSENhM,1526
13
+ colcon_uv-0.0.1.dist-info/METADATA,sha256=PWuc7QI1Jlsdo0w5qDpurXKnsiQxGcn3nJRqsRTqL2g,2809
14
+ colcon_uv-0.0.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
15
+ colcon_uv-0.0.1.dist-info/entry_points.txt,sha256=c3r5DG7L2VfygiA4QDIr4qQXkb3nEIwC05hycOY6L84,390
16
+ colcon_uv-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,14 @@
1
+ [colcon_core.package_augmentation]
2
+ uv = colcon_uv.package_augmentation.uv:UvPackageAugmentation
3
+
4
+ [colcon_core.package_identification]
5
+ uv = colcon_uv.package_identification.uv:UvPackageIdentification
6
+
7
+ [colcon_core.task.build]
8
+ uv.python = colcon_uv.task.uv.build:UvBuildTask
9
+
10
+ [colcon_core.task.test]
11
+ uv.python = colcon_uv.task.uv.test:UvTestTask
12
+
13
+ [colcon_core.verb]
14
+ uv = colcon_uv.cli:UvVerb