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

47 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-10 14:57 +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 

9 

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

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

12 

13 

14class CFF(ProjectMetadataWriter): 

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

16 

17 def __init__( 

18 self, 

19 path: Path, 

20 create_if_not_exists: bool = True, 

21 pass_validation: bool = False, 

22 ): 

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

24 

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

26 """ 

27 self._yaml = YAML() 

28 self._yaml.preserve_quotes = True 

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

30 

31 mappings: FieldKeyMapping = { 

32 "name": ["title"], 

33 "description": ["abstract"], 

34 "homepage": ["url"], 

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

36 "documentation": IgnoreKey(), 

37 "maintainers": ["contact"], 

38 } 

39 super().__init__( 

40 path, 

41 create_if_not_exists=create_if_not_exists, 

42 direct_mappings=mappings, 

43 pass_validation=pass_validation, 

44 ) 

45 

46 def _init_new_file(self): 

47 """Initialize new CFF file.""" 

48 self._data = { 

49 "cff-version": "1.2.0", 

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

51 "type": "software", 

52 } 

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

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

55 

56 def _load(self): 

57 """Load the CFF file.""" 

58 with open(self.path) as f: 

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

60 

61 def _validate(self) -> None: 

62 """Validate the CFF file.""" 

63 if self.pass_validation: 

64 return 

65 try: 

66 citation = create_citation(self.path, None) 

67 citation.validate() 

68 except ValueError as e: 

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

70 

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

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

73 path = path or self.path 

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

75 

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

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

78 self.authors = self._sync_person_list( 

79 self.authors, metadata.publication_authors() 

80 ) 

81 

82 @staticmethod 

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

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

85 json_str = person.model_dump_json( 

86 exclude={ 

87 "contribution", 

88 "contribution_types", 

89 "contribution_begin", 

90 "contribution_end", 

91 "author", 

92 "publication_author", 

93 "maintainer", 

94 }, 

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

96 ) 

97 return json.loads(json_str) 

98 

99 @staticmethod 

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

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

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

103 if "name" in person_obj: 

104 Entity._aliases() 

105 ret = Entity.make_partial(person_obj) 

106 else: 

107 Person._aliases() 

108 ret = Person.make_partial(person_obj) 

109 

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

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

112 return ret