-
Notifications
You must be signed in to change notification settings - Fork 340
/
Copy path_author_list_from_cff.py
205 lines (158 loc) · 5.29 KB
/
_author_list_from_cff.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
Convert author information from :file:`CITATION.cff` into a
reStructuredText-formatted list.
"""
import pathlib
import yaml
from unidecode import unidecode
# If a contributor changed their contributor name and we don't know
# the new name to update in in CITATION.cff, add their old username to
# the following set to prevent generating a broken link that would get
# detected when doing a `make linkcheck`.
obsolete_github_usernames = {}
def parse_cff(filename: str) -> dict[str, str | list[dict[str, str]]]:
"""
Parse a :file:`CITATION.cff` file into a dictionary.
Parameters
----------
filename : `str`
The path to :file:`CITATION.cff`.
Returns
-------
`dict`
A dictionary containing the parsed data from :file:`CITATION.cff`.
"""
with pathlib.Path(filename).open(encoding="utf-8") as stream:
return yaml.safe_load(stream)
def author_sorting_key(author: dict[str, str]) -> str:
"""
Provide sorting key for an author.
Parameters
----------
author : `dict` of `str` to `str`
A dictionary containing author information, which should be an
item in the list returned by `parse_cff`.
Returns
-------
str
The key for sorting authors.
"""
if "family-names" in author:
key = author["family-names"].lower()
elif "given-names" in author:
key = author["given-names"].lower()
elif "alias" in author:
key = author["alias"].lower()
else:
msg = (
"Need to specify 'family-names', 'given-names', and/or "
f"'alias' in CITATION.cff for author: {author}"
)
raise RuntimeError(msg)
return unidecode(key)
def get_author_name(author: dict[str, str]) -> str:
"""
Return the full name of the author if available, and otherwise
return the author's alias.
Parameters
----------
author : `dict` of `str` to `str`
A dictionary containing author information, which should be an
item in the list returned by `parse_cff`.
Returns
-------
str
The author name (if available) or the author alias.
"""
given_names = author.get("given-names", "")
family_names = author.get("family-names", "")
alias = author.get("alias", "")
if given_names or family_names:
return f"{given_names} {family_names}".strip()
if alias:
return alias
msg = (
"Invalid author information. Need at least one of "
"'alias', 'family-names', or 'given-names' in "
"CITATION.cff. Author information: {author}"
)
raise RuntimeError(msg)
def begin_author_line(author: dict[str, str]) -> str:
"""
Return the beginning of the author line, using the ``:user:`` role
if the alias is available.
Parameters
----------
author : `dict` of `str` to `str`
A dictionary containing author information, which should be an
item in the list returned by `parse_cff`.
Returns
-------
str
The beginning of the author line.
"""
name = get_author_name(author)
alias = author.get("alias")
if not alias or alias in obsolete_github_usernames:
return f"- {name}"
if alias == name:
return f"- :user:`{alias}`"
return f"- :user:`{name} <{alias}>`"
def get_orcid(author: dict[str, str]) -> str:
"""
Return the string that returns a string containing ORCID information
using the ``:orcid:`` role.
Parameters
----------
author : `dict` of `str` to `str`
A dictionary containing author information, which should be an
item in the list returned by `parse_cff`.
Returns
-------
str
The ORCID information using the ``:orcid:`` role.
"""
orcid = author.get("orcid", "").removeprefix("https://orcid.org/")
return f" (:orcid:`{orcid}`)" if orcid else ""
def generate_rst_author_list(authors: list[dict[str, str]]) -> str:
"""
Generate a reStructuredText formatted list of authors.
Parameters
----------
authors : `list` of `dict` from `str` to `str`
A list of dictionaries containing information about an author,
as read in by `parse_cff`.
Returns
-------
str
The reStructuredText formatted list of authors.
"""
authors = sorted(authors, key=author_sorting_key)
author_lines = [begin_author_line(author) + get_orcid(author) for author in authors]
return "\n".join(author_lines)
def generate_rst_file(
cff_file="../CITATION.cff", rst_file="about/_authors.rst", verbose=False
):
"""
Parse :file:`CITATION.cff` file and generate a reStructuredText
formatted list of authors.
Parameters
----------
cff_file : str, default: ``"../CITATION.cff"``
The path to :file:`CITATION.cff`.
rst_file : str, default: ``about/_authors.rst``
The path to the output :file:`.rst` file.
"""
cff_data = parse_cff(cff_file)
authors_rst = generate_rst_author_list(cff_data["authors"])
if verbose:
print(authors_rst) # noqa: T201
with pathlib.Path(rst_file).open("w", encoding="utf-8") as file:
file.write(authors_rst)
if __name__ == "__main__":
"""
To test the functionality in this file, run:
.. code-block: bash
python _author_list_from_cff.py
"""
generate_rst_file(verbose=True)