Repository: PlasmaPy/PlasmaPy

Issue:
Define author list in only one place
Right now we have authorship recorded in two locations:

 - `docs/about/credits.rst`
 - `codemeta.json`

We might also potentially create `.zenodo.json` again (#1340) for a third place.  The [CodeMeta](https://codemeta.github.io/) file has become more out-of-date than the author list in the docs.  

It's a lot of work to maintain multiple author lists, which is often work done around the time of releases.  Automating this (in conjunction with #1601) would help streamline the release process and reduce the amount of work that needs to be done manually.

A possibility would be to automagically generate the author list in `docs/credits/about.rst` from the information contained within `codemeta.json`.  I think there might be crosswalk tools out there between `codemeta.json` and `.zenodo.json` files too, and eventually (hopefully) Zenodo will start loading in data from CodeMeta files too.  If we do this, then `codemeta.json` would become the primary location for where authors are defined (which would mean changing #1601).  



Discussion:

Resolving patch:
diff --git a/docs/cff_to_rst.py b/docs/cff_to_rst.py
new file mode 100644
--- /dev/null
+++ b/docs/cff_to_rst.py
@@ -0,0 +1,112 @@
+"""
+Convert author information from :file:`CITATION.cff` into a
+reStructuredText-formatted list.
+"""
+import pathlib
+import yaml
+
+from typing import Union
+
+
+def parse_cff(filename: str) -> dict[str, Union[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`.
+
+    Raises
+    ------
+    `yaml.YAMLError`
+        If :file:`CITATION.cff` cannot be parsed.
+    """
+    with pathlib.Path(filename).open() as stream:
+        try:
+            return yaml.safe_load(stream)
+        except yaml.YAMLError as e:
+            print(e)
+
+
+def sorting_key(author: dict[str, str]) -> str:
+    """
+    Provide sorting key for an author.
+
+    Parameters
+    ----------
+    author : `dict`
+        A dictionary containing author information.
+
+    Returns
+    -------
+    str
+        The key for sorting authors.
+    """
+    if "family-names" in author:
+        return author["family-names"]
+    elif "given-names" in author:
+        return author["given-names"]
+    elif "alias" in author:
+        return author["alias"]
+    else:
+        return ""
+
+
+def generate_reST(authors: list[dict[str, str]]) -> str:
+    """
+    Generate a reStructuredText formatted list of authors.
+
+    Parameters
+    ----------
+    authors : list of dict
+        A list of dictionaries, each containing information about an
+        author.
+
+    Returns
+    -------
+    str
+        The reStructuredText formatted list of authors.
+    """
+    authors = sorted(authors, key=sorting_key)
+    authors_reST = ""
+    for author in authors:
+        if "given-names" in author and "family-names" in author:
+            name = f'{author["given-names"]} {author["family-names"]}'
+        elif "alias" in author:
+            name = f'{author["alias"]}'
+        else:
+            continue
+
+        if "orcid" in author and "alias" in author:
+            orcid = author["orcid"].removeprefix("https://orcid.org/")
+            authors_reST += f'- :user:`{name} <{author["alias"]}>` (:orcid:`{orcid}`)\n'
+        elif "alias" in author:
+            authors_reST += f'- :user:`{name} <{author["alias"]}>`\n'
+        else:
+            authors_reST += f"- :user:`{name}`\n"
+    return authors_reST
+
+
+def main(cff_file="../CITATION.cff", rst_file="about/_authors.rst"):
+    """
+    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_reST = generate_reST(cff_data["authors"])
+    with pathlib.Path(rst_file).open("w") as file:
+        file.write(authors_reST)
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -9,12 +9,20 @@
 import os
 import sys
 
+# isort: off
+sys.path.insert(0, os.path.abspath(".."))  # noqa: PTH100
+sys.path.insert(0, os.path.abspath("."))  # noqa: PTH100
+# isort: on
+
+import cff_to_rst
+
 from datetime import datetime
 from pkg_resources import parse_version
 from sphinx.application import Sphinx
 
-sys.path.insert(0, os.path.abspath(".."))  # noqa: PTH100
-sys.path.insert(0, os.path.abspath("."))  # noqa: PTH100
+# Generate author list from CITATION.cff
+
+cff_to_rst.main()
 
 from plasmapy import __version__ as release  # noqa: E402
 


Test patch: