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
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +0000
1"""Writer adapter for pom.xml files."""
3import logging
4import xml.etree.ElementTree as ET
5from pathlib import Path
6from typing import Any
8from somesy.core.models import Entity, Person
9from somesy.core.writer import FieldKeyMapping, IgnoreKey, 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: bool | None = 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: Path | None = 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: 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
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
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
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
140 return Entity(
141 name=name,
142 email=email,
143 website=url,
144 contribution_types=contr,
145 )
147 # no search keywords supported in POM
148 @property
149 def keywords(self) -> list[str] | None:
150 """Return the keywords of the project."""
152 @keywords.setter
153 def keywords(self, keywords: list[str]) -> None:
154 """Set the keywords of the project."""
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[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)
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[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 | Entity]) -> None:
192 """Set the maintainers of the project."""
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
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 )
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
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 )
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
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 )
243 def sync(self, metadata) -> None:
244 """Sync codemeta.json with project metadata.
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)