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:31 +0000

1"""package.json parser and saver.""" 

2 

3import json 

4import logging 

5from collections import OrderedDict 

6from pathlib import Path 

7from typing import Any 

8 

9from rich.pretty import pretty_repr 

10 

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 

15 

16logger = logging.getLogger("somesy") 

17 

18 

19class PackageJSON(ProjectMetadataWriter): 

20 """package.json parser and saver.""" 

21 

22 def __init__( 

23 self, 

24 path: Path, 

25 pass_validation: bool | None = False, 

26 ): 

27 """package.json parser. 

28 

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 ) 

41 

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] 

52 

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) 

58 

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 [] 

67 

68 maintainers_valid = [] 

69 

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 

77 

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) 

83 

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 [] 

92 

93 contributors_valid = [] 

94 

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 

102 

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) 

108 

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) 

113 

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) 

123 

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}") 

128 

129 write_json(self._data, path) 

130 

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) 

143 

144 if person.email: 

145 response["email"] = person.email 

146 

147 return response 

148 

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) 

157 

158 if isinstance(person_obj, PackageAuthor): 

159 person_obj = person_obj.model_dump(exclude_none=True) 

160 

161 person_dict: dict[str, Any] = person_obj # type: ignore 

162 

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) 

181 

182 def sync(self, metadata: ProjectMetadata) -> None: 

183 """Sync package.json with project metadata. 

184 

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 ) 

191 

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 

202 

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 )