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
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:57 +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
10from somesy.core.models import Entity, Person, ProjectMetadata
11from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter
14class CFF(ProjectMetadataWriter):
15 """Citation File Format (CFF) parser and saver."""
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.
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)
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 )
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)
56 def _load(self):
57 """Load the CFF file."""
58 with open(self.path) as f:
59 self._data = self._yaml.load(f)
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
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)
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 )
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)
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)
110 # construct (partial) Person while preserving key order from YAML
111 ret.set_key_order(list(person_obj.keys()))
112 return ret