diff --git a/.changelog/_unreleased.toml b/.changelog/_unreleased.toml index 10605ebd..000e7d74 100644 --- a/.changelog/_unreleased.toml +++ b/.changelog/_unreleased.toml @@ -51,3 +51,9 @@ id = "4d8e2872-d32f-4747-b2d6-aa5c1cb4ec75" type = "fix" description = "Rename deprecated tool.poetry.dev-dependencies section" author = "scott@stevenson.io" + +[[entries]] +id = "e3edc5a7-39ec-4e05-89c8-d06bf8cb0ee8" +type = "feature" +description = "Enabling `UvPyprojectHandler` to properly bump versions for path dependencies" +author = "alexandre.ghelfi@helsing.ai" diff --git a/examples/uv-project-relative-import/.kraken.py b/examples/uv-project-relative-import/.kraken.py new file mode 100644 index 00000000..2d48ba52 --- /dev/null +++ b/examples/uv-project-relative-import/.kraken.py @@ -0,0 +1,3 @@ +from kraken.std import python + +python.install() diff --git a/examples/uv-project-relative-import/pyproject.toml b/examples/uv-project-relative-import/pyproject.toml new file mode 100644 index 00000000..90269950 --- /dev/null +++ b/examples/uv-project-relative-import/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "uv-project-relative-import" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.10" +dependencies = [ + "tqdm>=4.66.5", + "uv-project", +] + +[tool.uv.sources] +uv-project = { path = "../uv-project", editable = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/uv-project-relative-import/src/uv_project_relative_import/__init__.py b/examples/uv-project-relative-import/src/uv_project_relative_import/__init__.py new file mode 100644 index 00000000..b52abc8b --- /dev/null +++ b/examples/uv-project-relative-import/src/uv_project_relative_import/__init__.py @@ -0,0 +1,5 @@ +from uv_project import hello + + +def main() -> None: + hello() diff --git a/examples/uv-project-relative-import/uv.lock b/examples/uv-project-relative-import/uv.lock new file mode 100644 index 00000000..bf7ecdf6 --- /dev/null +++ b/examples/uv-project-relative-import/uv.lock @@ -0,0 +1,52 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, +] + +[[package]] +name = "uv-project" +version = "0.1.0" +source = { editable = "../uv-project" } +dependencies = [ + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [{ name = "tqdm", specifier = ">=4.66.5" }] + +[package.metadata.requires-dev] +dev = [{ name = "types-tqdm", specifier = ">=4.66.0.20240417" }] + +[[package]] +name = "uv-project-relative-import" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "tqdm" }, + { name = "uv-project" }, +] + +[package.metadata] +requires-dist = [ + { name = "tqdm", specifier = ">=4.66.5" }, + { name = "uv-project", editable = "../uv-project" }, +] diff --git a/kraken-build/src/kraken/std/python/buildsystem/uv.py b/kraken-build/src/kraken/std/python/buildsystem/uv.py index 474b03c3..2fdbbc85 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/uv.py +++ b/kraken-build/src/kraken/std/python/buildsystem/uv.py @@ -196,6 +196,50 @@ def get_packages(self) -> list[PyprojectHandler.Package]: package_name = self.raw["project"]["name"] return [self.Package(include=package_name.replace("-", "_").replace(".", "_"))] + def _get_sources(self) -> dict[str, dict[str, Any]]: + return self.raw.get("tool", {}).get("uv", {}).get("sources", {}) # type: ignore [no-any-return] + + def _get_dependencies(self) -> list[str]: + """Fetches dependencies following [PEP631](https://peps.python.org/pep-0631/) format.""" + return self.raw.get("project", {}).get("dependencies", []) # type: ignore [no-any-return] + + def _get_dependency_groups(self) -> dict[str, list[str]]: + return self.raw.get("project", {}).get("dependency-groups", {}) # type: ignore [no-any-return] + + def _get_optional_dependencies(self) -> dict[str, list[str]]: + return self.raw.get("project", {}).get("optional-dependencies", {}) # type: ignore [no-any-return] + + def set_path_dependencies_to_version(self, version: str) -> None: + """ + Walks through the `[project.dependencies]`, `[project.dependency-groups]` + and `[project.optional-dependencies]` groups to replace all path and workspace sources + with proper index dependencies using the specified `version` string. + + Based on [PEP631](https://peps.python.org/pep-0631/) for dependencies and optional-dependencies, + and [PEP735](https://peps.python.org/pep-0735/) for dependency-groups. + """ + + sources = self._get_sources() + dependencies = self._get_dependencies() + dependency_groups = self._get_dependency_groups() + optional_dependencies = self._get_optional_dependencies() + sources_to_rm: list[str] = [] + for source, params in sources.items(): + # TODO(Ghelfi): Check if entry with `path` is within the current project + if "workspace" in params or "path" in params: + sources_to_rm.append(source) + if (index := dependencies.index(source)) is not None: + dependencies[index] = f"{source}=={version}" + for key, deps in dependency_groups.items(): + if (index := deps.index(source)) is not None: + dependency_groups[key][index] = f"{source}=={version}" + for key, deps in optional_dependencies.items(): + if (index := deps.index(source)) is not None: + optional_dependencies[key][index] = f"{source}=={version}" + + for elem in sources_to_rm: + sources.pop(elem) + class UvPythonBuildSystem(PythonBuildSystem): """ diff --git a/kraken-build/tests/kraken_std/integration/python/test_python.py b/kraken-build/tests/kraken_std/integration/python/test_python.py index 34499586..9c54f076 100644 --- a/kraken-build/tests/kraken_std/integration/python/test_python.py +++ b/kraken-build/tests/kraken_std/integration/python/test_python.py @@ -182,6 +182,34 @@ def test__python_project_upgrade_python_version_string( assert build_as_version == tomli.loads(conf_file.read().decode("UTF-8"))["tool"]["poetry"]["version"] +@unittest.mock.patch.dict(os.environ, {}) +def test__python_project__upgrade_relative_import_version( + kraken_ctx: Context, + kraken_project: Project, +) -> None: + tempdir = kraken_project.directory + + build_as_version = "0.1.1" + project_name = "uv-project-relative-import" + original_dir = example_dir(project_name) + project_dist = kraken_project.build_directory / "python-dist" + + # Copy the projects to the temporary directory. + shutil.copytree(original_dir, tempdir, dirs_exist_ok=True) + python.build(as_version=build_as_version, project=kraken_project) + kraken_ctx.execute([":build"]) + + # Check if generated files are named following proper version. + formatted_project_name = project_name.replace("-", "_") + assert Path(project_dist / f"{formatted_project_name}-{build_as_version}.tar.gz").is_file() + assert Path(project_dist / f"{formatted_project_name}-{build_as_version}-py3-none-any.whl").is_file() + with tarfile.open(project_dist / f"{formatted_project_name}-{build_as_version}.tar.gz", "r:gz") as tar: + # Check if generated files store proper version. + metadata_file = tar.extractfile(f"{formatted_project_name}-{build_as_version}/PKG-INFO") + assert metadata_file is not None, ".tar.gz file does not contain an 'PKG-INFO'" + assert f"Requires-Dist: uv-project=={build_as_version}" in metadata_file.read().decode("UTF-8") + + M = TypeVar("M", PdmPyprojectHandler, PoetryPyprojectHandler)