Coverage for src/somesy/package_json/writer.py: 93%
123 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-03 13:13 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-03 13:13 +0000
1"""package.json parser and saver."""
3import json
4import logging
5from collections import OrderedDict
6from pathlib import Path
7from typing import Any
9from rich.pretty import pretty_repr
11from somesy.core.core import write_json
12from somesy.core.models import Entity, Person, ProjectMetadata
13from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter
14from somesy.package_json.models import PackageAuthor, PackageJsonConfig
16logger = logging.getLogger("somesy")
19class PackageJSON(ProjectMetadataWriter):
20 """package.json parser and saver."""
22 def __init__(
23 self,
24 path: Path,
25 pass_validation: bool | None = False,
26 ):
27 """package.json parser.
29 See [somesy.core.writer.ProjectMetadataWriter.__init__][].
30 """
31 mappings: FieldKeyMapping = {
32 "authors": ["author"],
33 "documentation": IgnoreKey(),
34 }
35 super().__init__(
36 path,
37 create_if_not_exists=False,
38 direct_mappings=mappings,
39 pass_validation=pass_validation,
40 )
42 @property
43 def authors(self):
44 """Return the only author of the package.json file as list."""
45 # check if the author has the correct format
46 author = self._get_property(self._get_key("authors"))
47 if author is None:
48 return []
49 if isinstance(author, str):
50 author = PackageJsonConfig.convert_author(author)
51 return [author]
53 @authors.setter
54 def authors(self, authors: list[Entity | Person]) -> None:
55 """Set the authors of the project."""
56 authors_dict = self._from_person(authors[0])
57 self._set_property(self._get_key("authors"), authors_dict)
59 @property
60 def maintainers(self):
61 """Return the maintainers of the package.json file."""
62 # check if the maintainer has the correct format
63 maintainers = self._get_property(self._get_key("maintainers"))
64 # return empty list if maintainers is None
65 if maintainers is None:
66 return []
68 maintainers_valid = []
70 for maintainer in maintainers:
71 if isinstance(maintainer, str):
72 maintainer = PackageJsonConfig.convert_author(maintainer)
73 if maintainer is None:
74 continue
75 maintainers_valid.append(maintainer)
76 return maintainers_valid
78 @maintainers.setter
79 def maintainers(self, maintainers: list[Entity | Person]) -> None:
80 """Set the maintainers of the project."""
81 maintainers_dict = [self._from_person(m) for m in maintainers]
82 self._set_property(self._get_key("maintainers"), maintainers_dict)
84 @property
85 def contributors(self):
86 """Return the contributors of the package.json file."""
87 # check if the contributor has the correct format
88 contributors = self._get_property(self._get_key("contributors"))
89 # return empty list if contributors is None
90 if contributors is None:
91 return []
93 contributors_valid = []
95 for contributor in contributors:
96 if isinstance(contributor, str):
97 contributor = PackageJsonConfig.convert_author(contributor)
98 if contributor is None:
99 continue
100 contributors_valid.append(contributor)
101 return contributors_valid
103 @contributors.setter
104 def contributors(self, contributors: list[Entity | Person]) -> None:
105 """Set the contributors of the project."""
106 contributors_dict = [self._from_person(c) for c in contributors]
107 self._set_property(self._get_key("contributors"), contributors_dict)
109 def _load(self) -> None:
110 """Load package.json file."""
111 with self.path.open(encoding="utf-8") as f:
112 self._data = json.load(f, object_pairs_hook=OrderedDict)
114 def _validate(self) -> None:
115 """Validate package.json content using pydantic class."""
116 if self.pass_validation:
117 return
118 config = dict(self._get_property([]))
119 logger.debug(
120 f"Validating config using {PackageJsonConfig.__name__}: {pretty_repr(config)}"
121 )
122 PackageJsonConfig(**config)
124 def save(self, path: Path | None = None) -> None:
125 """Save the package.json file."""
126 path = path or self.path
127 logger.debug(f"Saving package.json to {path}")
129 write_json(self._data, path)
131 @staticmethod
132 def _from_person(person: Entity | Person) -> dict:
133 """Convert project metadata person/entity object to package.json dict for person format."""
134 response = {}
135 if isinstance(person, Person):
136 response["name"] = person.full_name
137 if person.orcid:
138 response["url"] = str(person.orcid)
139 else:
140 response["name"] = person.name
141 if person.website:
142 response["url"] = str(person.website)
144 if person.email:
145 response["email"] = person.email
147 return response
149 @staticmethod
150 def _to_person(
151 person_obj: str | dict[str, Any] | PackageAuthor,
152 ) -> Entity | Person:
153 """Convert package.json dict or str for person format to project metadata person object."""
154 if isinstance(person_obj, str):
155 # parse from package.json format
156 person_obj = PackageJsonConfig.convert_author(person_obj)
158 if isinstance(person_obj, PackageAuthor):
159 person_obj = person_obj.model_dump(exclude_none=True)
161 person_dict: dict[str, Any] = person_obj # type: ignore
163 if "name" in person_dict and " " in person_dict["name"]:
164 names = [s.strip() for s in person_dict["name"].split()]
165 person_obj = {
166 "given-names": " ".join(names[:-1]),
167 "family-names": names[-1],
168 }
169 if "email" in person_dict:
170 person_obj["email"] = person_dict["email"].strip()
171 if "url" in person_dict and "orcid.org" in person_dict["url"]:
172 person_obj["orcid"] = person_dict["url"].strip()
173 return Person(**person_obj)
174 else:
175 entity_obj = {"name": person_dict["name"]}
176 if "email" in person_dict:
177 entity_obj["email"] = person_dict["email"].strip()
178 if "url" in person_dict:
179 entity_obj["website"] = person_dict["url"].strip()
180 return Entity(**entity_obj)
182 def sync(self, metadata: ProjectMetadata) -> None:
183 """Sync package.json with project metadata.
185 Use existing sync function from ProjectMetadataWriter but update repository and contributors.
186 """
187 super().sync(metadata)
188 self.contributors = self._sync_person_list(
189 self.contributors, metadata.contributors()
190 )
192 @property
193 def repository(self) -> str | dict | None:
194 """Return the repository url of the project."""
195 if repo := super().repository:
196 if isinstance(repo, str):
197 return repo
198 else:
199 return repo.get("url")
200 else:
201 return None
203 @repository.setter
204 def repository(self, value: str | dict | None) -> None:
205 """Set the repository url of the project."""
206 if value is None:
207 self._set_property(self._get_key("repository"), None)
208 else:
209 self._set_property(
210 self._get_key("repository"), {"type": "git", "url": value}
211 )