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
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:57 +0000
1"""Writer adapter for pom.xml files."""
3import logging
4import xml.etree.ElementTree as ET
5from pathlib import Path
6from typing import Any, Dict, List, Optional, Union
8from somesy.core.models import Entity, Person
9from somesy.core.writer import FieldKeyMapping, ProjectMetadataWriter
11from . import POM_ROOT_ATRS, POM_URL
12from .xmlproxy import XMLProxy
14logger = logging.getLogger("somesy")
17class POM(ProjectMetadataWriter):
18 """Java Maven pom.xml parser and saver."""
20 # TODO: write a wrapper for ElementTree that behaves like a dict
21 # TODO: set up correct field name mappings
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.
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 )
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)
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)
61 def _validate(self) -> None:
62 """Validate the POM file."""
63 logger.info("Cannot validate POM file, skipping validation.")
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)
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
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
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
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
138 return Entity(
139 name=name,
140 email=email,
141 website=url,
142 contribution_types=contr,
143 )
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
151 @keywords.setter
152 def keywords(self, keywords: List[str]) -> None:
153 """Set the keywords of the project."""
154 pass
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]
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)
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]
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)
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 []
190 @maintainers.setter
191 def maintainers(self, maintainers: List[Person]) -> None:
192 """Set the maintainers of the project."""
193 pass
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
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 )
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
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 )
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
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 )
239 def sync(self, metadata) -> None:
240 """Sync codemeta.json with project metadata.
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)