Coverage for src/somesy/cff/writer.py: 96%
52 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-14 13:02 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-14 13:02 +0000
1"""Citation File Format (CFF) parser and saver."""
3import json
4from pathlib import Path
5from typing import Optional, Union
7from cffconvert.cli.create_citation import create_citation
8from ruamel.yaml import YAML
9from ruamel.yaml.scalarstring import LiteralScalarString
11from somesy.core.models import Entity, Person, ProjectMetadata
12from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter
15class CFF(ProjectMetadataWriter):
16 """Citation File Format (CFF) parser and saver."""
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.
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)
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 )
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)
57 def _load(self):
58 """Load the CFF file."""
59 with open(self.path) as f:
60 self._data = self._yaml.load(f)
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
72 def save(self, path: Optional[Path] = None) -> None:
73 """Save the CFF object to a file."""
74 path = path or self.path
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)
83 self._yaml.dump(self._data, path)
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 )
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)
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)
119 # construct (partial) Person while preserving key order from YAML
120 ret.set_key_order(list(person_obj.keys()))
121 return ret