24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147 | class CFF(ProjectMetadataWriter):
"""Citation File Format (CFF) parser and saver."""
def __init__(
self,
path: Path,
create_if_not_exists: bool = True,
pass_validation: bool = False,
):
"""Citation File Format (CFF) parser.
See [somesy.core.writer.ProjectMetadataWriter.__init__][].
"""
self._yaml = YAML()
self._yaml.preserve_quotes = True
self._yaml.indent(mapping=2, sequence=4, offset=2)
self._yaml.width = 120
mappings: FieldKeyMapping = {
"name": ["title"],
"description": ["abstract"],
"homepage": ["url"],
"repository": ["repository-code"],
"documentation": IgnoreKey(),
"maintainers": ["contact"],
}
super().__init__(
path,
create_if_not_exists=create_if_not_exists,
direct_mappings=mappings,
pass_validation=pass_validation,
)
def _init_new_file(self):
"""Initialize new CFF file."""
self._data = {
"cff-version": "1.2.0",
"message": "If you use this software, please cite it using these metadata.",
"type": "software",
}
with open(self.path, "w", encoding="utf-8") as f:
self._yaml.dump(self._data, f)
def _load(self):
"""Load the CFF file."""
with open(self.path, encoding="utf-8") as f:
self._data = self._yaml.load(f)
def _validate(self) -> None:
"""Validate the CFF file."""
if self.pass_validation:
return
try:
# validate the loaded data without a stray `rorid` (repaired on sync)
data = deepcopy(self._data)
_drop_rorid(data)
stream = StringIO()
self._yaml.dump(data, stream)
Citation(stream.getvalue(), src=self.path).validate()
except ValueError as e:
raise ValueError(f"CITATION.cff file is not valid!\n{e}") from e
def save(self, path: Path | None = None) -> None:
"""Save the CFF object to a file."""
path = path or self.path
# if description have new line characters, it should be saved as multiline string
if "abstract" in self._data and "\n" in self._data["abstract"]:
self._data["abstract"] = LiteralScalarString(self._data["abstract"])
self._yaml.dump(self._data, path)
def _sync_authors(self, metadata: ProjectMetadata) -> None:
"""Ensure that publication authors are added all into author list."""
self.authors = self._sync_person_list(
self.authors, metadata.publication_authors()
)
def sync(self, metadata: ProjectMetadata) -> None:
"""Sync CFF metadata."""
_drop_rorid(self._data)
super().sync(metadata)
# CFF 1.2.0 allows a list of SPDX ids
if isinstance(metadata.license, list) and len(metadata.license) > 1:
self.license = [license.value for license in metadata.license]
# an abstract can be longer than a package summary, so prefer it here
if metadata.abstract:
self.description = metadata.abstract
if metadata.doi:
self._data["doi"] = metadata.doi
@staticmethod
def _from_person(person: Person | Entity):
"""Convert project metadata person or entity object to cff dict for person format."""
json_str = person.model_dump_json(
exclude={
"contribution",
"contribution_types",
"contribution_begin",
"contribution_end",
"author",
"publication_author",
"maintainer",
},
by_alias=True, # e.g. family_names -> family-names, etc.
)
ret = json.loads(json_str)
ret.pop("rorid", None) # not a CFF field
return ret
@staticmethod
def _to_person(person_obj) -> Person | Entity:
"""Parse CFF Person to a somesy Person or entity."""
# if the object has key name, it is an entity
if "name" in person_obj:
Entity._aliases()
ret = Entity.make_partial(person_obj)
else:
Person._aliases()
ret = Person.make_partial(person_obj)
# construct (partial) Person while preserving key order from YAML
ret.set_key_order(list(person_obj.keys()))
return ret
|