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

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

2 

3import logging 

4from collections import OrderedDict 

5from pathlib import Path 

6from typing import Any, Dict, List, Optional, Union 

7 

8from rich.pretty import pretty_repr 

9 

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 

14 

15logger = logging.getLogger("somesy") 

16 

17 

18class PackageJSON(ProjectMetadataWriter): 

19 """package.json parser and saver.""" 

20 

21 def __init__( 

22 self, 

23 path: Path, 

24 pass_validation: Optional[bool] = False, 

25 ): 

26 """package.json parser. 

27 

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 ) 

40 

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

49 

50 return [self._get_property(self._get_key("authors"))] 

51 

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) 

57 

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

66 

67 maintainers_valid = [] 

68 

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 

76 

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) 

82 

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

91 

92 contributors_valid = [] 

93 

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 

101 

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) 

107 

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) 

112 

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) 

122 

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

127 

128 with path.open("w") as f: 

129 # package.json indentation is 2 spaces 

130 json.dump(self._data, f) 

131 

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 

144 

145 if person.email: 

146 response["email"] = person.email 

147 

148 return response 

149 

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) 

158 

159 if isinstance(person, PackageAuthor): 

160 person = person.model_dump(exclude_none=True) 

161 

162 person_dict: dict[str, Any] = person # type: ignore 

163 

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) 

182 

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

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

185 

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) 

190 

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 

201 

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