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

85 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-30 09:42 +0000

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

2import logging 

3from collections import OrderedDict 

4from pathlib import Path 

5from typing import Any, List, Optional 

6 

7from rich.pretty import pretty_repr 

8 

9from somesy.core.models import Person, ProjectMetadata 

10from somesy.core.writer import FieldKeyMapping, ProjectMetadataWriter 

11from somesy.json_wrapper import json 

12 

13logger = logging.getLogger("somesy") 

14 

15 

16class CodeMeta(ProjectMetadataWriter): 

17 """Codemeta.json parser and saver.""" 

18 

19 def __init__( 

20 self, 

21 path: Path, 

22 ): 

23 """Codemeta.json parser. 

24 

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

26 """ 

27 mappings: FieldKeyMapping = { 

28 "repository": ["codeRepository"], 

29 "homepage": ["softwareHelp"], 

30 "documentation": ["buildInstructions"], 

31 "keywords": ["keywords"], 

32 "authors": ["author"], 

33 "maintainers": ["maintainer"], 

34 "contributors": ["contributor"], 

35 } 

36 # delete the file if it exists 

37 if path.is_file(): 

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

39 path.unlink() 

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

41 

42 @property 

43 def authors(self): 

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

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

46 

47 @authors.setter 

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

49 """Set the authors of the project.""" 

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

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

52 

53 @property 

54 def contributors(self): 

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

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

57 

58 @contributors.setter 

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

60 """Set the contributors of the project.""" 

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

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

63 

64 def _load(self) -> None: 

65 """Load codemeta.json file.""" 

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

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

68 

69 def _validate(self) -> None: 

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

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

72 

73 logger.debug( 

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

75 ) 

76 

77 def _init_new_file(self) -> None: 

78 data = { 

79 "@context": [ 

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

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

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

83 "https://schema.org", 

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

85 ], 

86 "@type": "SoftwareSourceCode", 

87 "author": [], 

88 } 

89 # dump to file 

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

91 json.dump(data, f) 

92 

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

94 """Save the codemeta.json file.""" 

95 path = path or self.path 

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

97 

98 # copy the _data 

99 data = self._data.copy() 

100 

101 # set license 

102 if "license" in data: 

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

104 

105 # if softwareHelp is set, set url to softwareHelp 

106 if "softwareHelp" in data: 

107 data["url"] = data["softwareHelp"] 

108 

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

110 # codemeta.json indentation is 2 spaces 

111 json.dump(data, f) 

112 

113 @staticmethod 

114 def _from_person(person: Person): 

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

116 person_dict = { 

117 "@type": "Person", 

118 } 

119 if person.given_names: 

120 person_dict["givenName"] = person.given_names 

121 if person.family_names: 

122 person_dict["familyName"] = person.family_names 

123 if person.email: 

124 person_dict["email"] = person.email 

125 if person.orcid: 

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

127 if person.address: 

128 person_dict["address"] = person.address 

129 if person.affiliation: 

130 person_dict["affiliation"] = person.affiliation 

131 return person_dict 

132 

133 @staticmethod 

134 def _to_person(person) -> Person: 

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

136 person_obj = {} 

137 if "givenName" in person: 

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

139 if "familyName" in person: 

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

141 if "email" in person: 

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

143 if "@id" in person: 

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

145 if "address" in person: 

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

147 

148 return Person(**person_obj) 

149 

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

151 """Override the _sync_person_list function from ProjectMetadataWriter. 

152 

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

154 

155 Args: 

156 old (List[Any]): _description_ 

157 new (List[Person]): _description_ 

158 

159 Returns: 

160 List[Any]: _description_ 

161 """ 

162 return new 

163 

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

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

166 

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

168 """ 

169 super().sync(metadata) 

170 self.contributors = self._sync_person_list( 

171 self.contributors, metadata.contributors() 

172 )