Coverage for src/somesy/codemeta/writer.py: 92%

85 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-29 07:50 +0000

1"""codemeta.json creation module.""" 

2 

3import logging 

4from collections import OrderedDict 

5from pathlib import Path 

6from typing import Any, List, Optional 

7 

8from rich.pretty import pretty_repr 

9 

10from somesy.core.models import Person, ProjectMetadata 

11from somesy.core.writer import FieldKeyMapping, ProjectMetadataWriter 

12from somesy.json_wrapper import json 

13 

14logger = logging.getLogger("somesy") 

15 

16 

17class CodeMeta(ProjectMetadataWriter): 

18 """Codemeta.json parser and saver.""" 

19 

20 def __init__( 

21 self, 

22 path: Path, 

23 ): 

24 """Codemeta.json parser. 

25 

26 See [somesy.core.writer.ProjectMetadataWriter.__init__][]. 

27 """ 

28 mappings: FieldKeyMapping = { 

29 "repository": ["codeRepository"], 

30 "homepage": ["softwareHelp"], 

31 "documentation": ["buildInstructions"], 

32 "keywords": ["keywords"], 

33 "authors": ["author"], 

34 "maintainers": ["maintainer"], 

35 "contributors": ["contributor"], 

36 } 

37 # delete the file if it exists 

38 if path.is_file(): 

39 logger.verbose("Deleting existing codemeta.json file.") 

40 path.unlink() 

41 super().__init__(path, create_if_not_exists=True, direct_mappings=mappings) 

42 

43 @property 

44 def authors(self): 

45 """Return the only author of the codemeta.json file as list.""" 

46 return self._get_property(self._get_key("publication_authors")) or [] 

47 

48 @authors.setter 

49 def authors(self, authors: List[Person]) -> None: 

50 """Set the authors of the project.""" 

51 authors = [self._from_person(a) for a in authors] 

52 self._set_property(self._get_key("authors"), authors) 

53 

54 @property 

55 def contributors(self): 

56 """Return the contributors of the codemeta.json file.""" 

57 return self._get_property(self._get_key("contributors")) 

58 

59 @contributors.setter 

60 def contributors(self, contributors: List[Person]) -> None: 

61 """Set the contributors of the project.""" 

62 contributors = [self._from_person(c) for c in contributors] 

63 self._set_property(self._get_key("contributors"), contributors) 

64 

65 def _load(self) -> None: 

66 """Load codemeta.json file.""" 

67 with self.path.open() as f: 

68 self._data = json.load(f, object_pairs_hook=OrderedDict) 

69 

70 def _validate(self) -> None: 

71 """Validate codemeta.json content using pydantic class.""" 

72 config = dict(self._get_property([])) 

73 

74 logger.debug( 

75 f"No validation for codemeta.json files {CodeMeta.__name__}: {pretty_repr(config)}" 

76 ) 

77 

78 def _init_new_file(self) -> None: 

79 data = { 

80 "@context": [ 

81 "https://doi.org/10.5063/schema/codemeta-2.0", 

82 "https://w3id.org/software-iodata", 

83 "https://raw.githubusercontent.com/jantman/repostatus.org/master/badges/latest/ontology.jsonld", 

84 "https://schema.org", 

85 "https://w3id.org/software-types", 

86 ], 

87 "@type": "SoftwareSourceCode", 

88 "author": [], 

89 } 

90 # dump to file 

91 with self.path.open("w+") as f: 

92 json.dump(data, f) 

93 

94 def save(self, path: Optional[Path] = None) -> None: 

95 """Save the codemeta.json file.""" 

96 path = path or self.path 

97 logger.debug(f"Saving codemeta.json to {path}") 

98 

99 # copy the _data 

100 data = self._data.copy() 

101 

102 # set license 

103 if "license" in data: 

104 data["license"] = (f"https://spdx.org/licenses/{data['license']}",) 

105 

106 # if softwareHelp is set, set url to softwareHelp 

107 if "softwareHelp" in data: 

108 data["url"] = data["softwareHelp"] 

109 

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

111 # codemeta.json indentation is 2 spaces 

112 json.dump(data, f) 

113 

114 @staticmethod 

115 def _from_person(person: Person): 

116 """Convert project metadata person object to codemeta.json dict for person format.""" 

117 person_dict = { 

118 "@type": "Person", 

119 } 

120 if person.given_names: 

121 person_dict["givenName"] = person.given_names 

122 if person.family_names: 

123 person_dict["familyName"] = person.family_names 

124 if person.email: 

125 person_dict["email"] = person.email 

126 if person.orcid: 

127 person_dict["@id"] = str(person.orcid) 

128 if person.address: 

129 person_dict["address"] = person.address 

130 if person.affiliation: 

131 person_dict["affiliation"] = person.affiliation 

132 return person_dict 

133 

134 @staticmethod 

135 def _to_person(person) -> Person: 

136 """Convert codemeta.json dict or str for person format to project metadata person object.""" 

137 person_obj = {} 

138 if "givenName" in person: 

139 person_obj["given_names"] = person["givenName"].strip() 

140 if "familyName" in person: 

141 person_obj["family_names"] = person["familyName"].strip() 

142 if "email" in person: 

143 person_obj["email"] = person["email"].strip() 

144 if "@id" in person: 

145 person_obj["orcid"] = person["@id"].strip() 

146 if "address" in person: 

147 person_obj["address"] = person["address"].strip() 

148 

149 return Person(**person_obj) 

150 

151 def _sync_person_list(self, old: List[Any], new: List[Person]) -> List[Any]: 

152 """Override the _sync_person_list function from ProjectMetadataWriter. 

153 

154 This method wont care about existing persons in codemeta.json file. 

155 

156 Args: 

157 old (List[Any]): _description_ 

158 new (List[Person]): _description_ 

159 

160 Returns: 

161 List[Any]: _description_ 

162 

163 """ 

164 return new 

165 

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

167 """Sync codemeta.json with project metadata. 

168 

169 Use existing sync function from ProjectMetadataWriter but update repository and contributors. 

170 """ 

171 super().sync(metadata) 

172 self.contributors = self._sync_person_list( 

173 self.contributors, metadata.contributors() 

174 )