Coverage for src / gitversioned / plugins / hatchling_plugin.py: 0%

71 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-14 20:57 +0000

1""" 

2Hatchling version source plugin for GitVersioned. 

3 

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""" 

8 

9from __future__ import annotations 

10 

11from pathlib import Path 

12from typing import Any, ClassVar 

13 

14from hatchling.metadata.core import ProjectMetadata 

15from hatchling.plugin import hookimpl 

16from hatchling.version.source.plugin.interface import VersionSourceInterface 

17from loguru import logger 

18 

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 

23 

24__all__ = [ 

25 "GitVersionedVersionSource", 

26 "hatch_register_version_source", 

27] 

28 

29 

30class GitVersionedVersionSource(VersionSourceInterface): 

31 """ 

32 Hatchling version source interface for GitVersioned. 

33 

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. 

38 

39 .. code-block:: python 

40 

41 source = GitVersionedVersionSource(root_dir, config) 

42 version_data = source.get_version_data() 

43 

44 :cvar PLUGIN_NAME: The registered name of the plugin within the Hatchling ecosystem 

45 """ 

46 

47 PLUGIN_NAME: ClassVar[str] = "gitversioned" # type: ignore[misc] 

48 

49 def get_version_data(self) -> dict[str, str]: 

50 """ 

51 Computes the project version from Git state. 

52 

53 Resolves the version using the Git repository, build environment, and combined 

54 configuration context, optionally generating a version file if configured. 

55 

56 .. code-block:: python 

57 

58 data = source.get_version_data() 

59 version = data["version"] 

60 

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") 

67 

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 ) 

76 

77 logger.info( 

78 f"gitversioned computed version {version} and wrote it to {output_path}" 

79 ) 

80 

81 return {"version": str(version)} 

82 

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. 

90 

91 This method updates the configured version source file with the explicitly 

92 provided version, making it the new persistent version source. 

93 

94 .. code-block:: python 

95 

96 source.set_version("1.2.3", {}) 

97 

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 ) 

105 

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") 

110 

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") 

114 

115 def get_settings_kwargs(self) -> dict[str, Any]: 

116 """ 

117 Extracts and prepares the configuration settings for GitVersioned. 

118 

119 Gathers the project root, package name, source root, and plugin configuration 

120 from the Hatchling environment to construct the GitVersioned settings. 

121 

122 .. code-block:: python 

123 

124 kwargs = source.get_settings_kwargs() 

125 settings = Settings(**kwargs) 

126 

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() 

132 

133 kwargs = { 

134 "package_name": package_name, 

135 "project_root": project_root, 

136 "src_root": src_root, 

137 "build_is_editable": False, 

138 } 

139 

140 plugin_config = self.config.copy() 

141 plugin_config.pop("project_root", None) 

142 plugin_config.pop("src_root", None) 

143 

144 kwargs.update(plugin_config) 

145 

146 return kwargs 

147 

148 def get_project_root(self) -> Path: 

149 """ 

150 Resolves the absolute path to the project root directory. 

151 

152 .. code-block:: python 

153 

154 root = source.get_project_root() 

155 

156 :return: The resolved absolute path to the project root 

157 """ 

158 return Path(self.root).resolve() 

159 

160 def get_package_name(self) -> str: 

161 """ 

162 Retrieves the normalized package name from project metadata. 

163 

164 Extracts the project name from the Hatchling metadata and normalizes it 

165 by replacing hyphens with underscores. 

166 

167 .. code-block:: python 

168 

169 name = source.get_package_name() 

170 

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("-", "_") 

176 

177 def get_src_root(self) -> Path: 

178 """ 

179 Determines the source root directory for the project. 

180 

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'. 

184 

185 .. code-block:: python 

186 

187 src_root = source.get_src_root() 

188 

189 :return: The resolved path to the source root directory 

190 """ 

191 root = self.get_project_root() 

192 

193 if "src_root" in self.config: 

194 return Path(root) / str(self.config["src_root"]) 

195 

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 ) 

204 

205 packages = hatch_config.get("packages", None) 

206 if packages and isinstance(packages, list): 

207 return root / packages[0] 

208 

209 sources = hatch_config.get("sources", None) 

210 if sources and isinstance(sources, dict): 

211 return root / list(sources.keys())[0] 

212 

213 package_name = self.get_package_name() 

214 

215 src_path = root / "src" / package_name 

216 if src_path.exists(): 

217 return src_path 

218 

219 pkg_path = root / package_name 

220 if pkg_path.exists(): 

221 return pkg_path 

222 

223 return root 

224 

225 

226@hookimpl 

227def hatch_register_version_source() -> type[VersionSourceInterface]: 

228 """ 

229 Register the GitVersioned source plugin with Hatchling. 

230 

231 Provides the entry point for Hatchling to discover and load the 

232 GitVersionedVersionSource plugin implementation. 

233 

234 .. code-block:: python 

235 

236 plugin_class = hatch_register_version_source() 

237 

238 :return: The class representing the plugin interface 

239 """ 

240 return GitVersionedVersionSource