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

128 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-10 14:57 +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, Dict, List, Optional, Union 

7 

8from somesy.core.models import Entity, Person 

9from somesy.core.writer import FieldKeyMapping, 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: Optional[bool] = 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: Optional[Path] = 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: Union[str, List[str]], 

72 *, 

73 only_first: bool = False, 

74 remove: bool = False, 

75 ) -> Optional[Any]: 

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

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

78 if elem is not None: 

79 if isinstance(elem, list): 

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

81 else: 

82 return elem.to_jsonlike() 

83 return None 

84 

85 @staticmethod 

86 def _from_person(person: Union[Entity, Person]): 

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

88 ret: Dict[str, Any] = {} 

89 if isinstance(person, Person): 

90 person_id = person.to_name_email_string() 

91 if person.orcid: 

92 person_id = str(person.orcid) 

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

94 else: 

95 person_id = person.to_name_email_string() 

96 if person.website: 

97 person_id = str(person.website) 

98 ret["url"] = person.website 

99 ret["id"] = person_id 

100 ret["name"] = person.full_name 

101 if person.email: 

102 ret["email"] = person.email 

103 if person.contribution_types: 

104 ret["roles"] = dict(role=[c.value for c in person.contribution_types]) 

105 return ret 

106 

107 @staticmethod 

108 def _to_person(person_obj: dict) -> Union[Entity, Person]: 

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

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

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

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

113 fname = names[-1] 

114 email = person_obj["email"] 

115 url = person_obj.get("url") 

116 maybe_orcid = url if url.find("orcid.org") >= 0 else None 

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

118 contr = roles["role"] 

119 else: 

120 contr = None 

121 

122 return Person( 

123 given_names=gnames, 

124 family_names=fname, 

125 email=email, 

126 orcid=maybe_orcid, 

127 contribution_types=contr, 

128 ) 

129 else: 

130 name = person_obj["name"] 

131 email = person_obj.get("email") 

132 url = person_obj.get("url") 

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

134 contr = roles["role"] 

135 else: 

136 contr = None 

137 

138 return Entity( 

139 name=name, 

140 email=email, 

141 website=url, 

142 contribution_types=contr, 

143 ) 

144 

145 # no search keywords supported in POM 

146 @property 

147 def keywords(self) -> Optional[List[str]]: 

148 """Return the keywords of the project.""" 

149 pass 

150 

151 @keywords.setter 

152 def keywords(self, keywords: List[str]) -> None: 

153 """Set the keywords of the project.""" 

154 pass 

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[Union[Entity, Person]]) -> None: 

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

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

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

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[Union[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]) -> None: 

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

193 pass 

194 

195 # only one project license supported in somesy (POM can have many) 

196 @property 

197 def license(self) -> Optional[str]: 

198 """Return the license of the project.""" 

199 lic = self._get_property(self._get_key("license"), only_first=True) 

200 return lic.get("name") if lic is not None else None 

201 

202 @license.setter 

203 def license(self, license: Optional[str]) -> None: 

204 """Set the license of the project.""" 

205 self._set_property( 

206 self._get_key("license"), dict(name=license, distribution="repo") 

207 ) 

208 

209 @property 

210 def repository(self) -> Optional[Union[str, dict]]: 

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

212 repo = super().repository 

213 if isinstance(repo, str): 

214 return repo 

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

216 

217 @repository.setter 

218 def repository(self, value: Optional[Union[str, dict]]) -> None: 

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

220 self._set_property( 

221 self._get_key("repository"), dict(name="git repository", url=value) 

222 ) 

223 

224 @property 

225 def documentation(self) -> Optional[Union[str, dict]]: 

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

227 docs = super().documentation 

228 if isinstance(docs, str): 

229 return docs 

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

231 

232 @documentation.setter 

233 def documentation(self, value: Optional[Union[str, dict]]) -> None: 

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

235 self._set_property( 

236 self._get_key("documentation"), dict(name="documentation site", url=value) 

237 ) 

238 

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

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

241 

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

243 """ 

244 super().sync(metadata) 

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