Coverage for src/somesy/cff/writer.py: 96%

52 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-14 13:01 +0000

1"""Citation File Format (CFF) parser and saver.""" 

2 

3import json 

4from pathlib import Path 

5from typing import Optional, Union 

6 

7from cffconvert.cli.create_citation import create_citation 

8from ruamel.yaml import YAML 

9from ruamel.yaml.scalarstring import LiteralScalarString 

10 

11from somesy.core.models import Entity, Person, ProjectMetadata 

12from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter 

13 

14 

15class CFF(ProjectMetadataWriter): 

16 """Citation File Format (CFF) parser and saver.""" 

17 

18 def __init__( 

19 self, 

20 path: Path, 

21 create_if_not_exists: bool = True, 

22 pass_validation: bool = False, 

23 ): 

24 """Citation File Format (CFF) parser. 

25 

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

27 """ 

28 self._yaml = YAML() 

29 self._yaml.preserve_quotes = True 

30 self._yaml.indent(mapping=2, sequence=4, offset=2) 

31 

32 mappings: FieldKeyMapping = { 

33 "name": ["title"], 

34 "description": ["abstract"], 

35 "homepage": ["url"], 

36 "repository": ["repository-code"], 

37 "documentation": IgnoreKey(), 

38 "maintainers": ["contact"], 

39 } 

40 super().__init__( 

41 path, 

42 create_if_not_exists=create_if_not_exists, 

43 direct_mappings=mappings, 

44 pass_validation=pass_validation, 

45 ) 

46 

47 def _init_new_file(self): 

48 """Initialize new CFF file.""" 

49 self._data = { 

50 "cff-version": "1.2.0", 

51 "message": "If you use this software, please cite it using these metadata.", 

52 "type": "software", 

53 } 

54 with open(self.path, "w") as f: 

55 self._yaml.dump(self._data, f) 

56 

57 def _load(self): 

58 """Load the CFF file.""" 

59 with open(self.path) as f: 

60 self._data = self._yaml.load(f) 

61 

62 def _validate(self) -> None: 

63 """Validate the CFF file.""" 

64 if self.pass_validation: 

65 return 

66 try: 

67 citation = create_citation(self.path, None) 

68 citation.validate() 

69 except ValueError as e: 

70 raise ValueError(f"CITATION.cff file is not valid!\n{e}") from e 

71 

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

73 """Save the CFF object to a file.""" 

74 path = path or self.path 

75 

76 # if description have new line characters, it should be saved as multiline string 

77 if "abstract" in self._data: 

78 if "\n" in self._data["abstract"]: 

79 self._data["abstract"] = LiteralScalarString(self._data["abstract"]) 

80 else: 

81 self._data["abstract"] = str(self.description) 

82 

83 self._yaml.dump(self._data, path) 

84 

85 def _sync_authors(self, metadata: ProjectMetadata) -> None: 

86 """Ensure that publication authors are added all into author list.""" 

87 self.authors = self._sync_person_list( 

88 self.authors, metadata.publication_authors() 

89 ) 

90 

91 @staticmethod 

92 def _from_person(person: Union[Person, Entity]): 

93 """Convert project metadata person or entity object to cff dict for person format.""" 

94 json_str = person.model_dump_json( 

95 exclude={ 

96 "contribution", 

97 "contribution_types", 

98 "contribution_begin", 

99 "contribution_end", 

100 "author", 

101 "publication_author", 

102 "maintainer", 

103 }, 

104 by_alias=True, # e.g. family_names -> family-names, etc. 

105 ) 

106 return json.loads(json_str) 

107 

108 @staticmethod 

109 def _to_person(person_obj) -> Union[Person, Entity]: 

110 """Parse CFF Person to a somesy Person or entity.""" 

111 # if the object has key name, it is an entity 

112 if "name" in person_obj: 

113 Entity._aliases() 

114 ret = Entity.make_partial(person_obj) 

115 else: 

116 Person._aliases() 

117 ret = Person.make_partial(person_obj) 

118 

119 # construct (partial) Person while preserving key order from YAML 

120 ret.set_key_order(list(person_obj.keys())) 

121 return ret