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