Coverage for src / gitversioned / plugins / hatchling_plugin.py: 80%
71 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:55 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:55 +0000
1"""
2Hatchling version source plugin for GitVersioned.
4This module provides the Hatchling plugin interface to dynamically resolve project
5versions from Git state. It bridges Hatch's versioning configuration with GitVersioned's
6core version resolution and file generation engine.
7"""
9from __future__ import annotations
11from pathlib import Path
12from typing import Any, ClassVar
14from hatchling.metadata.core import ProjectMetadata
15from hatchling.plugin import hookimpl
16from hatchling.version.source.plugin.interface import VersionSourceInterface
17from loguru import logger
19from gitversioned.logging import LoggingSettings, configure_logger
20from gitversioned.settings import Settings
21from gitversioned.utils import BuildEnvironment, GitRepository
22from gitversioned.versioning import resolve_and_generate_version
24__all__ = [
25 "GitVersionedVersionSource",
26 "hatch_register_version_source",
27]
30class GitVersionedVersionSource(VersionSourceInterface):
31 """
32 Hatchling version source interface for GitVersioned.
34 This class provides the implementation for the Hatchling version source plugin
35 interface, allowing projects using Hatchling to dynamically resolve their versions
36 via GitVersioned. It handles version resolution, manual version setting, and project
37 metadata extraction.
39 .. code-block:: python
41 source = GitVersionedVersionSource(root_dir, config)
42 version_data = source.get_version_data()
44 :cvar PLUGIN_NAME: The registered name of the plugin within the Hatchling ecosystem
45 """
47 PLUGIN_NAME: ClassVar[str] = "gitversioned" # type: ignore[misc]
49 def get_version_data(self) -> dict[str, str]:
50 """
51 Computes the project version from Git state.
53 Resolves the version using the Git repository, build environment, and combined
54 configuration context, optionally generating a version file if configured.
56 .. code-block:: python
58 data = source.get_version_data()
59 version = data["version"]
61 :return: A dictionary containing the resolved version string under
62 the 'version' key
63 :raises ValueError: If the version resolution process fails
64 """
65 configure_logger(LoggingSettings(enabled=True))
66 logger.debug("GitVersionedVersionSource.get_version_data called")
68 config = Settings(**self.get_settings_kwargs())
69 repo = GitRepository(config.project_root)
70 build_env = BuildEnvironment(project_root=config.project_root)
71 version, output_path = resolve_and_generate_version(
72 settings=config,
73 repository=repo,
74 environment=build_env,
75 )
77 logger.info(
78 f"gitversioned computed version {version} and wrote it to {output_path}"
79 )
81 return {"version": str(version)}
83 def set_version(
84 self,
85 version: str,
86 version_data: dict[str, Any], # noqa: ARG002
87 ) -> None:
88 """
89 Handler for manual version setting via the Hatch CLI.
91 This method updates the configured version source file with the explicitly
92 provided version, making it the new persistent version source.
94 .. code-block:: python
96 source.set_version("1.2.3", {})
98 :param version: The raw version string passed by the user
99 :param version_data: Additional version data context from Hatchling
100 """
101 _ = (version_data,) # to avoid lint errors for unused parameters
102 logger.debug(
103 f"GitVersionedVersionSource.set_version called with version='{version}'"
104 )
106 config = Settings(**self.get_settings_kwargs())
107 if config.version_source_file:
108 version_source_path = config.project_root / config.version_source_file
109 version_source_path.write_text(f"version={version}\n", encoding="utf-8")
111 logger.info(f"gitversioned set version {version} in {version_source_path}")
112 else:
113 logger.warning("version_source_file is not set; skipping manual update")
115 def get_settings_kwargs(self) -> dict[str, Any]:
116 """
117 Extracts and prepares the configuration settings for GitVersioned.
119 Gathers the project root, package name, source root, and plugin configuration
120 from the Hatchling environment to construct the GitVersioned settings.
122 .. code-block:: python
124 kwargs = source.get_settings_kwargs()
125 settings = Settings(**kwargs)
127 :return: A dictionary of keyword arguments for configuring GitVersioned
128 """
129 project_root = self.get_project_root()
130 package_name = self.get_package_name()
131 src_root = self.get_src_root()
133 kwargs = {
134 "package_name": package_name,
135 "project_root": project_root,
136 "src_root": src_root,
137 "build_is_editable": False,
138 }
140 plugin_config = self.config.copy()
141 plugin_config.pop("project_root", None)
142 plugin_config.pop("src_root", None)
144 kwargs.update(plugin_config)
146 return kwargs
148 def get_project_root(self) -> Path:
149 """
150 Resolves the absolute path to the project root directory.
152 .. code-block:: python
154 root = source.get_project_root()
156 :return: The resolved absolute path to the project root
157 """
158 return Path(self.root).resolve()
160 def get_package_name(self) -> str:
161 """
162 Retrieves the normalized package name from project metadata.
164 Extracts the project name from the Hatchling metadata and normalizes it
165 by replacing hyphens with underscores.
167 .. code-block:: python
169 name = source.get_package_name()
171 :return: The normalized package name
172 """
173 root = self.get_project_root()
174 metadata: Any = ProjectMetadata(str(root), None)
175 return metadata.name.replace("-", "_")
177 def get_src_root(self) -> Path:
178 """
179 Determines the source root directory for the project.
181 Resolves the source directory by checking explicit plugin configuration,
182 Hatchling build targets, or falling back to standard repository layouts
183 like 'src/package_name' or 'package_name'.
185 .. code-block:: python
187 src_root = source.get_src_root()
189 :return: The resolved path to the source root directory
190 """
191 root = self.get_project_root()
193 if "src_root" in self.config:
194 return Path(root) / str(self.config["src_root"])
196 metadata: Any = ProjectMetadata(str(root), None)
197 hatch_config = (
198 metadata.config.get("tool", {})
199 .get("hatch", {})
200 .get("build", {})
201 .get("targets", {})
202 .get("wheel", {})
203 )
205 packages = hatch_config.get("packages", None)
206 if packages and isinstance(packages, list):
207 return root / packages[0]
209 sources = hatch_config.get("sources", None)
210 if sources and isinstance(sources, dict):
211 return root / list(sources.keys())[0]
213 package_name = self.get_package_name()
215 src_path = root / "src" / package_name
216 if src_path.exists():
217 return src_path
219 pkg_path = root / package_name
220 if pkg_path.exists():
221 return pkg_path
223 return root
226@hookimpl
227def hatch_register_version_source() -> type[VersionSourceInterface]:
228 """
229 Register the GitVersioned source plugin with Hatchling.
231 Provides the entry point for Hatchling to discover and load the
232 GitVersionedVersionSource plugin implementation.
234 .. code-block:: python
236 plugin_class = hatch_register_version_source()
238 :return: The class representing the plugin interface
239 """
240 return GitVersionedVersionSource