Coverage for src/somesy/pom_xml/writer.py: 89%

134 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-04 11:35 +0000

1"""Writer adapter for pom.xml files.""" 

2 

3import logging 

4import xml.etree.ElementTree as ET 

5from pathlib import Path 

6from typing import Any 

7 

8from somesy.core.models import Entity, Person 

9from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter 

10 

11from . import POM_ROOT_ATRS, POM_URL 

12from .xmlproxy import XMLProxy 

13 

14logger = logging.getLogger("somesy") 

15 

16 

17class POM(ProjectMetadataWriter): 

18 """Java Maven pom.xml parser and saver.""" 

19 

20 # TODO: write a wrapper for ElementTree that behaves like a dict 

21 # TODO: set up correct field name mappings 

22 

23 def __init__( 

24 self, 

25 path: Path, 

26 create_if_not_exists: bool = True, 

27 pass_validation: bool | None = False, 

28 ): 

29 """Java Maven pom.xml parser. 

30 

31 See [somesy.core.writer.ProjectMetadataWriter.__init__][]. 

32 """ 

33 mappings: FieldKeyMapping = { 

34 # "year": ["inceptionYear"], # not supported by somesy + does not really change 

35 # "project_slug": ["artifactId"], # not supported by somesy for sync 

36 "license": ["licenses", "license"], 

37 "homepage": ["url"], 

38 "repository": ["scm"], 

39 "documentation": ["distributionManagement", "site"], 

40 "authors": ["developers", "developer"], 

41 "contributors": ["contributors", "contributor"], 

42 } 

43 super().__init__( 

44 path, 

45 create_if_not_exists=create_if_not_exists, 

46 direct_mappings=mappings, 

47 pass_validation=pass_validation, 

48 ) 

49 

50 def _init_new_file(self): 

51 """Initialize new pom.xml file.""" 

52 pom = XMLProxy(ET.Element("project", POM_ROOT_ATRS)) 

53 pom["properties"] = {"info.versionScheme": "semver-spec"} 

54 pom.write(self.path) 

55 

56 def _load(self): 

57 """Load the POM file.""" 

58 ET.register_namespace("", POM_URL) # register POM as default xml namespace 

59 self._data = XMLProxy.parse(self.path, default_namespace=POM_URL) 

60 

61 def _validate(self) -> None: 

62 """Validate the POM file.""" 

63 logger.info("Cannot validate POM file, skipping validation.") 

64 

65 def save(self, path: Path | None = None) -> None: 

66 """Save the POM DOM to a file.""" 

67 self._data.write(path or self.path, default_namespace=None) 

68 

69 def _get_property( 

70 self, 

71 key: str | list[str] | IgnoreKey, 

72 *, 

73 only_first: bool = False, 

74 remove: bool = False, 

75 ) -> Any | None: 

76 """Get (a) property by key.""" 

77 if isinstance(key, IgnoreKey): 

78 return None 

79 elem = super()._get_property(key, only_first=only_first, remove=remove) 

80 if elem is not None: 

81 if isinstance(elem, list): 

82 return [e.to_jsonlike() for e in elem] 

83 else: 

84 return elem.to_jsonlike() 

85 return None 

86 

87 @staticmethod 

88 def _from_person(person: Entity | Person): 

89 """Convert person object to dict for POM XML person format.""" 

90 ret: dict[str, Any] = {} 

91 if isinstance(person, Person): 

92 person_id = person.to_name_email_string() 

93 if person.orcid: 

94 person_id = str(person.orcid) 

95 ret["url"] = str(person.orcid) 

96 else: 

97 person_id = person.to_name_email_string() 

98 if person.website: 

99 person_id = str(person.website) 

100 ret["url"] = person.website 

101 ret["id"] = person_id 

102 ret["name"] = person.full_name 

103 if person.email: 

104 ret["email"] = person.email 

105 if person.contribution_types: 

106 ret["roles"] = {"role": [c.value for c in person.contribution_types]} 

107 return ret 

108 

109 @staticmethod 

110 def _to_person(person_obj: dict) -> Entity | Person: 

111 """Parse POM XML person to a somesy Person.""" 

112 if " " in person_obj["name"]: 

113 names = person_obj["name"].split() 

114 gnames = " ".join(names[:-1]) 

115 fname = names[-1] 

116 email = person_obj.get("email") 

117 url = person_obj.get("url") 

118 maybe_orcid = url if url and "orcid.org" in url else None 

119 if roles := person_obj.get("roles"): 

120 contr = roles["role"] 

121 else: 

122 contr = None 

123 

124 return Person( 

125 given_names=gnames, 

126 family_names=fname, 

127 email=email, 

128 orcid=maybe_orcid, 

129 contribution_types=contr, 

130 ) 

131 else: 

132 name = person_obj["name"] 

133 email = person_obj.get("email") 

134 url = person_obj.get("url") 

135 if roles := person_obj.get("roles"): 

136 contr = roles["role"] 

137 else: 

138 contr = None 

139 

140 return Entity( 

141 name=name, 

142 email=email, 

143 website=url, 

144 contribution_types=contr, 

145 ) 

146 

147 # no search keywords supported in POM 

148 @property 

149 def keywords(self) -> list[str] | None: 

150 """Return the keywords of the project.""" 

151 

152 @keywords.setter 

153 def keywords(self, keywords: list[str]) -> None: 

154 """Set the keywords of the project.""" 

155 

156 # authors must be a list 

157 @property 

158 def authors(self): 

159 """Return the authors of the project.""" 

160 authors = self._get_property(self._get_key("authors")) 

161 return authors if isinstance(authors, list) else [authors] 

162 

163 @authors.setter 

164 def authors(self, authors: list[Entity | Person]) -> None: 

165 """Set the authors of the project.""" 

166 author_records = [self._from_person(c) for c in authors] 

167 self._set_property(self._get_key("authors"), author_records) 

168 

169 # contributors must be a list 

170 @property 

171 def contributors(self): 

172 """Return the contributors of the project.""" 

173 contr = self._get_property(self._get_key("contributors")) 

174 if contr is None: 

175 return [] 

176 return contr if isinstance(contr, list) else [contr] 

177 

178 @contributors.setter 

179 def contributors(self, contributors: list[Entity | Person]) -> None: 

180 """Set the contributors of the project.""" 

181 contr = [self._from_person(c) for c in contributors] 

182 self._set_property(self._get_key("contributors"), contr) 

183 

184 # no maintainers supported im POM, only developers and contributors 

185 @property 

186 def maintainers(self): 

187 """Return the maintainers of the project.""" 

188 return [] 

189 

190 @maintainers.setter 

191 def maintainers(self, maintainers: list[Person | Entity]) -> None: 

192 """Set the maintainers of the project.""" 

193 

194 @property 

195 def license(self) -> str | list[str] | None: 

196 """Return the license of the project.""" 

197 licenses = self._get_property(self._get_key("license")) 

198 if licenses is None: 

199 return None 

200 licenses = licenses if isinstance(licenses, list) else [licenses] 

201 names = [license["name"] for license in licenses] 

202 return names[0] if len(names) == 1 else names 

203 

204 @license.setter 

205 def license(self, license: str | list[str] | None) -> None: 

206 """Set the license of the project.""" 

207 licenses = license if isinstance(license, list) else [license] 

208 self._set_property( 

209 self._get_key("license"), 

210 [{"name": license, "distribution": "repo"} for license in licenses], 

211 ) 

212 

213 @property 

214 def repository(self) -> str | dict | None: 

215 """Return the repository url of the project.""" 

216 repo = super().repository 

217 if isinstance(repo, str): 

218 return repo 

219 return repo.get("url") if repo is not None else None 

220 

221 @repository.setter 

222 def repository(self, value: str | dict | None) -> None: 

223 """Set the repository url of the project.""" 

224 self._set_property( 

225 self._get_key("repository"), {"name": "git repository", "url": value} 

226 ) 

227 

228 @property 

229 def documentation(self) -> str | dict | None: 

230 """Return the documentation url of the project.""" 

231 docs = super().documentation 

232 if isinstance(docs, str): 

233 return docs 

234 return docs.get("url") if docs is not None else None 

235 

236 @documentation.setter 

237 def documentation(self, value: str | dict | None) -> None: 

238 """Set the documentation url of the project.""" 

239 self._set_property( 

240 self._get_key("documentation"), {"name": "documentation site", "url": value} 

241 ) 

242 

243 def sync(self, metadata) -> None: 

244 """Sync codemeta.json with project metadata. 

245 

246 Use existing sync function from ProjectMetadataWriter but update repository and contributors. 

247 """ 

248 super().sync(metadata) 

249 licenses = metadata.license 

250 self.license = ( 

251 [license.value for license in licenses] 

252 if isinstance(licenses, list) 

253 else licenses.value 

254 ) 

255 self.contributors = self._sync_person_list(self.contributors, metadata.people)