From 879361513bdd1c14ca4e223657517fe85b6085b0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 9 Aug 2024 13:21:19 -0400 Subject: [PATCH] DOC: Document code contributors on website (#12774) --- .circleci/config.yml | 6 +- .git-blame-ignore-revs | 21 +- .github/workflows/credit.yml | 43 +++ .gitignore | 2 +- .mailmap | 107 +++++-- .pre-commit-config.yaml | 13 +- doc/.gitignore | 1 + doc/Makefile | 5 +- doc/_templates/sidebar-quicklinks.html | 1 + doc/changes/devel/12774.other.rst | 2 + doc/conf.py | 3 + doc/credit.rst | 12 + doc/sphinxext/prs/12779.json | 19 ++ doc/sphinxext/update_credit_rst.py | 425 +++++++++++++++++++++++++ ignore_words.txt | 1 + pyproject.toml | 3 + tools/check_mne_location.py | 1 + tools/dev/check_steering_committee.py | 5 +- tools/dev/ensure_headers.py | 1 + tools/dev/gen_css_for_mne.py | 3 +- tools/dev/generate_pyi_files.py | 1 + tools/dev/update_credit_json.py | 88 +++++ tools/generate_codemeta.py | 3 +- 23 files changed, 706 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/credit.yml create mode 100644 doc/.gitignore create mode 100644 doc/changes/devel/12774.other.rst create mode 100644 doc/credit.rst create mode 100644 doc/sphinxext/prs/12779.json create mode 100644 doc/sphinxext/update_credit_rst.py create mode 100644 tools/dev/update_credit_json.py diff --git a/.circleci/config.yml b/.circleci/config.yml index d4c387b3be0..644fd8b31b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -433,13 +433,9 @@ jobs: mne sys_info -pd - run: name: make linkcheck + no_output_timeout: 40m command: | make -C doc linkcheck - - run: - name: make linkcheck-grep - when: always - command: | - make -C doc linkcheck-grep - store_artifacts: path: doc/_build/linkcheck destination: linkcheck diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d4f5921e70c..1199ffc4fcd 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,7 +1,14 @@ -e81ec528a42ac687f3d961ed5cf8e25f236925b0 # black -12395f9d9cf6ea3c72b225b62e052dd0d17d9889 # YAML indentation -d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 # isort -e7dd1588013179013a50d3f6b8e8f9ae0a185783 # ruff format -e39995d9be6fc831c7a4a59f09b7a7c0a41ae315 # percent formatting -940ac9553ce42c15b4c16ecd013824ca3ea7244a # whitespace -1c5b39ff1d99bbcb2fc0e0071a989b3f3845ff30 # ruff UP028 +# PR number should follow the commit number so that our code credit +# can parse this file correctly: +d71e497dcf6f98e19eb81e82e641404a71d2d663 # 1420, split up viz.py +203a96cbba2732d2e349a8f96065e74bbfd2a53b # 5862, split utils.py +ff349f356edb04e1b5f0db13deda8d1a20aca351 # 6767, move around manual parts +31a83063557fbd54d898f00f9527ffc547888395 # 10407, alphabetize docdict +e81ec528a42ac687f3d961ed5cf8e25f236925b0 # 11667, black +12395f9d9cf6ea3c72b225b62e052dd0d17d9889 # 11868, YAML indentation +d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 # 12097, isort +e7dd1588013179013a50d3f6b8e8f9ae0a185783 # 12261, ruff format +940ac9553ce42c15b4c16ecd013824ca3ea7244a # 12533, whitespace +e39995d9be6fc831c7a4a59f09b7a7c0a41ae315 # 12588, percent formatting +1c5b39ff1d99bbcb2fc0e0071a989b3f3845ff30 # 12603, ruff UP028 +b8b168088cb474f27833f5f9db9d60abe00dca83 # 12779, PR JSONs \ No newline at end of file diff --git a/.github/workflows/credit.yml b/.github/workflows/credit.yml new file mode 100644 index 00000000000..7c63b4dffd5 --- /dev/null +++ b/.github/workflows/credit.yml @@ -0,0 +1,43 @@ +name: Contributor credit + +on: # yamllint disable-line rule:truthy + # Scheduled actions only run on the main repo branch, which is exactly what we want + schedule: + # TODO: After making sure it works in `main` for a while, switch to monthly + # - cron: '0 0 1 * *' # first day of the month at midnight + - cron: '0 0 * * *' # every day at midnight + +permissions: + pull-requests: write + +jobs: + update_credit: + name: Update + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install pygithub -e . + - run: git checkout -b credit + - run: python tools/dev/update_credit_json.py + - run: python tools/dev/update_credit_rst.py + - run: git add -f doc/sphinxext/prs/*.json + - run: | + git diff && git status --porcelain + if [[ $(git status --porcelain) ]]; then + echo "dirty=true" >> $GITHUB_OUTPUT + fi + id: status + - name: Create PR + run: | + set -xeo pipefail + git config --global user.email "114827586+autofix-ci[bot]@users.noreply.github.com" + git config --global user.name "autofix-ci[bot]" + git commit -am "MAINT: Update code credit" + gh pr create -B main -H credit --title "MAINT: Update code credit" --body "Created by \"${{ github.workflow }}\" GitHub action." --label "no-changelog-entry-needed" + if: steps.status.outputs.dirty == 'true' diff --git a/.gitignore b/.gitignore index 79d03aac44f..d66fbef96de 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,7 @@ cover .venv/ venv/ -*.json +/*.json !codemeta.json .hypothesis/ .ruff_cache/ diff --git a/.mailmap b/.mailmap index d71df509cc2..accf48d96e5 100644 --- a/.mailmap +++ b/.mailmap @@ -2,8 +2,8 @@ Adam Li Adam Li Adam Li Adam Li Alan Leggitt leggitta Alessandro Tonin Lychfindel <58313635+Lychfindel@users.noreply.github.com> -Alex Rockhill Alex Alex Rockhill Alex +Alex Rockhill Alex Alex Rockhill Alex Rockhill Alex Rockhill Alex Rockhill Alexander Rudiuk Alexander Rudiuk @@ -14,19 +14,23 @@ Alexandre Gramfort Alexandre Gramfort Alexandre Gramfort Alexandre Gramfort Alexandre Gramfort Ana Radanovic anaradanovic <79697247+anaradanovic@users.noreply.github.com> +Andres Rodriguez Andrew Dykstra Andrew Quinn AJQuinn +Andy Gilbert <7andy121@gmail.com> Andrew Gilbert Andy Gilbert <7andy121@gmail.com> Andrew Gilbert -Anna Padee <44297909+apadee@users.noreply.github.com> apadee <44297909+apadee@users.noreply.github.com> +Anna Padee apadee <44297909+apadee@users.noreply.github.com> Anne-Sophie Dubarry annesodub Archit Singhal <43236121+architsinghal-mriirs@users.noreply.github.com> archit singhal Arne Pelzer aplzr <7202498+aplzr@users.noreply.github.com> Arne Pelzer pzr -Ashley Drew <33734402+ashdrew@users.noreply.github.com> ashdrew <33734402+ashdrew@users.noreply.github.com> +Ashley Drew ashdrew <33734402+ashdrew@users.noreply.github.com> Asish Panda kaichogami Basile Pinsard Brad Buran Brad Buran Britta Westner britta-wstnr +btkcodedev +buildqa Burkhard Maess Burkhard Maess Carina Forster Carina Carlos de la Torre carlos @@ -43,15 +47,18 @@ Christina Zhao ChristinaZhao Christoph Dinh Christoph Dinh Christopher J. Bailey Chris Bailey Claire Braboszcz claire-braboszcz +Clemens Brunner Clément Moutard -Cora Kim <41998428+kimcoco@users.noreply.github.com> kimcoco <41998428+kimcoco@users.noreply.github.com> +Cora Kim kimcoco <41998428+kimcoco@users.noreply.github.com> Cristóbal Moënne-Loccoz Cristóbal Dan G. Wakeman Daniel G. Wakeman Dan G. Wakeman Daniel Wakeman Dan G. Wakeman dgwakeman Dan G. Wakeman dgwakeman -Daniel Carlström Schad Daniel C Schad +Daniel C Schad Daniel C Schad +Daniel C Schad Daniel Carlström Schad Daniel McCloy Daniel McCloy +Daniel McCloy Daniel McCloy Daniel McCloy drammock Daniel Strohmeier Daniel Strohmeier Daniel Strohmeier joewalter @@ -70,6 +77,7 @@ Dmitrii Altukhov dmalt Dominik Krzemiński dokato Dominik Welke dominikwelke <33089761+dominikwelke@users.noreply.github.com> Dominik Welke dominikwelke +Dominik Wetzel Dominik Wetzel Eberhard Eich ebeich Eduard Ort Eduard Ort Eduard Ort eort @@ -77,7 +85,7 @@ Eduard Ort examplename Ellen Lau ellenlau Emily Stephen Emily P. Stephen Emily Stephen emilyps14 -Enrico Varano <69973551+enricovara@users.noreply.github.com> enricovara <69973551+enricovara@users.noreply.github.com> +Enrico Varano enricovara <69973551+enricovara@users.noreply.github.com> Enzo Altamiranda enzo Eric Larson Eric Larson Eric Larson Eric Larson @@ -93,7 +101,7 @@ Erkka Heinila Teekuningas Etienne de Montalivet Evgenii Kalenkovich kalenkovich Evgeny Goldstein <84768107+evgenygoldstein@users.noreply.github.com> evgenygoldstein <84768107+evgenygoldstein@users.noreply.github.com> -Ezequiel Mikulan <39155887+ezemikulan@users.noreply.github.com> ezemikulan <39155887+ezemikulan@users.noreply.github.com> +Ezequiel Mikulan ezemikulan <39155887+ezemikulan@users.noreply.github.com> Fahimeh Mamashli <33672431+fmamashli@users.noreply.github.com> fmamashli <33672431+fmamashli@users.noreply.github.com> Fede Raimondo Fede Fede Raimondo Fede Raimondo @@ -104,21 +112,23 @@ Fede Raimondo Federico Raimondo Federico Zamberlan <44038765+fzamberlan@users.noreply.github.com> Felix Klotzsche eioe Felix Klotzsche eioe -Félix Raimundo Felix Raimundo Frederik D. Weber Frederik-D-Weber Fu-Te Wong foucault Fu-Te Wong zuxfoucault +Félix Raimundo Felix Raimundo Gansheng Tan <49130176+GanshengT@users.noreply.github.com> Gansheng TAN <49130176+GanshengT@users.noreply.github.com> Gennadiy Belonosov <7503709+Genuster@users.noreply.github.com> Gennadiy <7503709+Genuster@users.noreply.github.com> Giorgio Marinato neurogima <76406896+neurogima@users.noreply.github.com> +Giulio Gabrieli Guillaume Dumas deep-introspection Guillaume Dumas Guillaume Dumas +Hakimeh Aslsardroud Hamid Maymandi <46011104+HamidMandi@users.noreply.github.com> Hamid <46011104+HamidMandi@users.noreply.github.com> -Hasrat Ali Arzoo <56307533+hasrat17@users.noreply.github.com> hasrat17 <56307533+hasrat17@users.noreply.github.com> +Hasrat Ali Arzoo hasrat17 <56307533+hasrat17@users.noreply.github.com> Hongjiang Ye YE Hongjiang Hubert Banville hubertjb -Hüseyin Orkun Elmas Hüseyin Hyonyoung Shin <55095699+mcvain@users.noreply.github.com> mcvain <55095699+mcvain@users.noreply.github.com> +Hüseyin Orkun Elmas Hüseyin Ingoo Lee dlsrnsi Ivo de Jong ivopascal Jaakko Leppakangas Jaakko Leppakangas @@ -126,15 +136,17 @@ Jaakko Leppakangas jaeilepp Jaakko Leppakangas jaeilepp Jair Montoya jmontoyam Jan Ebert janEbert +Jan Sedivy Jan Sosulski jsosulski Jean-Baptiste Schiratti Jean-Baptiste SCHIRATTI Jean-Remi King Jean-Rémi KING Jean-Remi King kingjr -Jean-Remi King kingjr Jean-Remi King kingjr +Jean-Remi King kingjr Jean-Remi King UMR9752 Jean-Remi King UMR9752 Jeff Stout jstout211 +Jennifer Behnke Jesper Duemose Nielsen jdue Jevri Hanna Jeff Hanna Jevri Hanna Jevri Hanna @@ -151,16 +163,18 @@ Jona Sassenhagen jona-sassenhagen jona-sassenhagen@ Jona Sassenhagen jona.sassenhagen@gmail.com Jona Sassenhagen sassenha +Jonathan Kuziek Jordan Drew <39603454+jadrew43@users.noreply.github.com> jadrew43 <39603454+jadrew43@users.noreply.github.com> Joris Van den Bossche Joris Van den Bossche +Joshua Calder-Travis <38797399+jCalderTravis@users.noreply.github.com> jCalderTravis <38797399+jCalderTravis@users.noreply.github.com> +Joshua J Bear +Joshua Teves Joshua Teves José C. García Alanis Jose Alanis José C. García Alanis Jose C. G. Alanis <12409129+JoseAlanis@users.noreply.github.com> José C. García Alanis José C. G. Alanis <12409129+JoseAlanis@users.noreply.github.com> José C. García Alanis José C. García Alanis <12409129+JoseAlanis@users.noreply.github.com> -Joshua J Bear -Joshua Teves Joshua Teves -Joshua Calder-Travis <38797399+jCalderTravis@users.noreply.github.com> jCalderTravis <38797399+jCalderTravis@users.noreply.github.com> Julius Welzel <52565341+JuliusWelzel@users.noreply.github.com> jwelzel <52565341+JuliusWelzel@users.noreply.github.com> +Justus Schwabedal Kaisu Lankinen <41806798+klankinen@users.noreply.github.com> klankinen <41806798+klankinen@users.noreply.github.com> Kambiz Tabavi Kambiz Tavabi Kambiz Tabavi kambysese @@ -171,11 +185,12 @@ Kostiantyn Maksymenko Maksymenko Kostiantyn LaetitiaG Larry Eisenman lneisenman Lenny Varghese lennyvarghese +Liberty Hamilton Lorenz Esch Lorenz Esch Lorenzo Alfine lorrandal Louis Thibault = Louis Thibault Louis Thibault -Lukas Gemein gemeinl +Lukas Gemein gemeinl Lukáš Hejtmánek hejtmy Mads Jensen mads jensen Mainak Jas Mainak @@ -185,21 +200,26 @@ Mainak Jas Mainak Jas mainakjas Manoj Kumar MechCoder Manu Sutela MJAS1 -Marian Dovgialo Marian Dovgialo -Marian Dovgialo mdovgialo +Marian Dovgialo Marian Dovgialo +Marian Dovgialo Marian Dovgialo +Marian Dovgialo mdovgialo Marijn van Vliet Marijn van Vliet -Mark Alexander Henney Mark -Mark Alexander Henney Mark Henney <120719655+henneysq@users.noreply.github.com> +Mark Henney Mark +Mark Henney Mark Alexander Henney +Mark Henney Mark Henney <120719655+henneysq@users.noreply.github.com> Mark Wronkiewicz wronk Marmaduke Woodman maedoc -Martin Billinger kazemakase -Martin Billinger Martin -Martin Billinger Martin Billinger -Martin Billinger mbillingr +Martin BaBer +Martin Billinger kazemakase +Martin Billinger kazemakase +Martin Billinger Martin Billinger +Martin Billinger mbillingr Martin Luessi martin Martin Luessi martin Martin Luessi mluessi@nmr.mgh.harvard.edu -Martin Schulz Martin Schulz <46245704+marsipu@users.noreply.github.com> +Martin Perez-Guevara +Martin Schulz Martin Schulz <46245704+marsipu@users.noreply.github.com> +Martin Schulz Martin Schulz Martin van Harmelen <1544429+MPvHarmelen@users.noreply.github.com> Martin <1544429+MPvHarmelen@users.noreply.github.com> Mathieu Scheltienne Mathieu Scheltienne <73893616+mscheltienne@users.noreply.github.com> Mathieu Scheltienne Mathieu Scheltienne @@ -209,7 +229,7 @@ Mats van Es Mats monkeyman192 Matteo Anelli Matteo Anelli Matteo Visconti di Oleggio Castello Matteo Visconti dOC -Matthias Dold <62005770+matthiasdold@users.noreply.github.com> matthiasdold <62005770+matthiasdold@users.noreply.github.com> +Matthias Dold matthiasdold <62005770+matthiasdold@users.noreply.github.com> Matthias Eberlein <41163089+MatthiasEb@users.noreply.github.com> MatthiasEb <41163089+MatthiasEb@users.noreply.github.com> Matti Hämäläinen Matti Hamalainen Matti Hämäläinen Matti Hamalainen @@ -229,6 +249,7 @@ Nathalie Gayraud Nathalie Naveen <172697+naveensrinivasan@users.noreply.github.com> Nicolas Barascud nbara Nicolas Barascud Nicolas Barascud <10333715+nbara@users.noreply.github.com> +Nicolas Fourcaud-Trocmé Nicolas Gensollen Gensollen Nicolas Legrand Legrand Nicolas Nicolas Legrand LegrandNico @@ -241,9 +262,11 @@ Nikolas Chalas Nichalas Olaf Hauk Olaf Hauk Olaf Hauk olafhauk Omer Shubi Omer S +Pablo Arias Paul Pasler ppasler Paul Roujansky Paul ROUJANSKY Paul Roujansky paulroujansky +Pavel Navratil Pedro Silva pbnsilva Phillip Alday Phillip Alday Phillip Alday Phillip Alday @@ -253,6 +276,7 @@ Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier Pierre-Antoine Bannier +Ping-Keng Jao nafraw Praveen Sripad prav Praveen Sripad prav Proloy Das pdas6 @@ -261,9 +285,14 @@ Ramonapariciog Apariciogarcia ramonapariciog roraa Reza Nasri Reza Reza Nasri RezaNasri +Riessarius Stargardsky Roan LaPlante aestrivex Rob Luke Robert Luke <748691+rob-luke@users.noreply.github.com> +Rob Luke Robert Luke +Robert Seymour Robin Tibor Schirrmeister robintibor +Roeland Hancock +Romain Derollepot Romain Trachel Romain Trachel Romain Trachel Romain Trachel Romain Trachel trachelr @@ -271,9 +300,12 @@ Roman Goj Ross Maddox rkmaddox Ross Maddox Ross Maddox Ross Maddox unknown -Rotem Falach Falach +Rotem Falach Falach Ryan Law Ryan Law +Ryan Law Ryan M.C. Law +Sammi Chekroud Samuel Deslauriers-Gauthier Samuel Deslauriers-Gauthier +Santeri Ruuskanen Santeri Ruuskanen <66060772+ruuskas@users.noreply.github.com> Sara Sommariva sarasommariva Sebastien Treguer DataFox Sena Er <2799280+sena-neuro@users.noreply.github.com> Sena <2799280+sena-neuro@users.noreply.github.com> @@ -283,32 +315,39 @@ Simon Kern Simon Kern <14980558+skjerns@users.noreply.git Simon Kern skjerns <14980558+skjerns@users.noreply.github.com> Simon Kern skjerns Sondre Foslien sondrfos +Sophie Herbst Steve Matindi stevemats Steven Bierer Steven Bierer <40672003+NeuroLaunch@users.noreply.github.com> Steven M. Gutstein S. M. Gutstein Steven M. Gutstein smgutstein -T. Wang <81429617+twang5@users.noreply.github.com> twang5 <81429617+twang5@users.noreply.github.com> +sviter +T. Wang twang5 <81429617+twang5@users.noreply.github.com> Tanay Gahlot Tanay -Teon Brooks -Teon Brooks -Teon Brooks Teon -Teon Brooks Teon Brooks +Teon L Brooks +Teon L Brooks +Teon L Brooks Teon +Teon L Brooks Teon Brooks Thomas Donoghue Tom Thomas Radman -Timon Merk <38216460+timonmerk@users.noreply.github.com> timonmerk <38216460+timonmerk@users.noreply.github.com> +Timon Merk +Timon Merk Timon Merk <38216460+timonmerk@users.noreply.github.com> +Timon Merk timonmerk <38216460+timonmerk@users.noreply.github.com> Timothy Gates Tim Gates +Timur Sokhin Tod Flak <45362686+todflak@users.noreply.github.com> todflak <45362686+todflak@users.noreply.github.com> Tom Ma myd7349 -Tom Stone tomdstone <77251489+tomdstone@users.noreply.github.com> Tom Stone Stone +Tom Stone tomdstone <77251489+tomdstone@users.noreply.github.com> Tristan Stenner Tristan Stenner Tziona NessAiver TzionaN Valerii Chirkov Valerii <42982039+vagechirkov@users.noreply.github.com> Valerii Chirkov Valerii +Velu Prabhakar Kumaravel Velu Prabhakar Kumaravel Victoria Peterson vpeterson +Will Turner Will Turner +Yiping Zuo Frostime Yousra Bekhti Yoursa BEKHTI Yousra Bekhti Yoursa BEKHTI Yousra Bekhti Yousra BEKHTI Yousra Bekhti yousrabk -Yiping Zuo Frostime Zhi Zhang <850734033@qq.com> ZHANG Zhi <850734033@qq.com> diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f937143c53..ab4423bc465 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,11 +6,12 @@ repos: - id: ruff name: ruff lint mne args: ["--fix"] - files: ^mne/ + files: ^mne/|^tools/ + exclude: vulture_allowlist.py - id: ruff name: ruff lint mne preview args: ["--fix", "--preview", "--select=NPY201"] - files: ^mne/ + files: ^mne/|^tools/ - id: ruff name: ruff lint doc, tutorials, and examples # D103: missing docstring in public function @@ -18,7 +19,7 @@ repos: args: ["--ignore=D103,D400", "--fix"] files: ^doc/|^tutorials/|^examples/ - id: ruff-format - files: ^mne/|^doc/|^tutorials/|^examples/ + files: ^mne/|^doc/|^tutorials/|^examples/|^tools/ # Codespell - repo: https://github.com/codespell-project/codespell @@ -27,7 +28,7 @@ repos: - id: codespell additional_dependencies: - tomli - files: ^mne/|^doc/|^examples/|^tutorials/ + files: ^mne/|^doc/|^examples/|^tutorials/|^tools/ types_or: [python, bib, rst, inc] # yamllint @@ -45,13 +46,15 @@ repos: additional_dependencies: - tomli files: ^doc/.*\.(rst|inc)$ + # Credit is problematic because we generate an include on the fly + exclude: ^doc/credit.rst$ # sorting - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: file-contents-sorter - files: ^doc/changes/names.inc + files: ^doc/changes/names.inc|^.mailmap args: ["--ignore-case"] # The following are too slow to run on local commits, so let's only run on CIs: diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000000..e696138e098 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +/code_credit.inc diff --git a/doc/Makefile b/doc/Makefile index 6569adab0f3..ab8219473b0 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -65,10 +65,7 @@ html_dev-noplot: html-noplot html_dev-front: html-front linkcheck: - @$(SPHINXBUILD) -b linkcheck -D nitpicky=0 -D plot_gallery=0 -D exclude_patterns="cited.rst,whats_new.rst,configure_git.rst,_includes,changes/devel" -d _build/doctrees . _build/linkcheck - -linkcheck-grep: - @! grep -h "^.*:.*: \[\(\(local\)\|\(broken\)\)\]" _build/linkcheck/output.txt + @$(SPHINXBUILD) -b linkcheck -D nitpicky=0 -q -D plot_gallery=0 -D exclude_patterns="cited.rst,whats_new.rst,configure_git.rst,_includes,changes/devel" -d _build/doctrees . _build/linkcheck doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest diff --git a/doc/_templates/sidebar-quicklinks.html b/doc/_templates/sidebar-quicklinks.html index fad7be67401..fa26fc0f298 100644 --- a/doc/_templates/sidebar-quicklinks.html +++ b/doc/_templates/sidebar-quicklinks.html @@ -7,6 +7,7 @@
Version {{ release }}
  • Get help
  • Cite
  • Contribute
  • +
  • Contributors
  • diff --git a/doc/changes/devel/12774.other.rst b/doc/changes/devel/12774.other.rst new file mode 100644 index 00000000000..57172476b54 --- /dev/null +++ b/doc/changes/devel/12774.other.rst @@ -0,0 +1,2 @@ +Code contributions are now measured using PRs and reported on the :ref:`contributors` +page, by `Eric Larson`_. diff --git a/doc/conf.py b/doc/conf.py index 3330146aa28..b2f3cadaed4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -51,6 +51,7 @@ sys.path.append(str(curpath / "sphinxext")) from mne_doc_utils import report_scraper, reset_warnings # noqa: E402 +from update_credit_rst import generate_credit_rst # noqa: E402 # -- Project information ----------------------------------------------------- @@ -616,6 +617,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "https://doi.org/10.1073/", # pnas.org "https://doi.org/10.1093/", # academic.oup.com/sleep/ "https://doi.org/10.1098/", # royalsocietypublishing.org + "https://doi.org/10.1101/", # www.biorxiv.org "https://doi.org/10.1111/", # onlinelibrary.wiley.com/doi/10.1111/psyp "https://doi.org/10.1126/", # www.science.org "https://doi.org/10.1137/", # epubs.siam.org @@ -1655,6 +1657,7 @@ def setup(app): """Set up the Sphinx app.""" app.connect("autodoc-process-docstring", append_attr_meth_examples) # High prio, will happen before SG + app.connect("builder-inited", generate_credit_rst, priority=10) app.connect("builder-inited", report_scraper.set_dirs, priority=20) app.connect("build-finished", make_gallery_redirects) app.connect("build-finished", make_api_redirects) diff --git a/doc/credit.rst b/doc/credit.rst new file mode 100644 index 00000000000..4d9a9957604 --- /dev/null +++ b/doc/credit.rst @@ -0,0 +1,12 @@ +:orphan: + +.. _contributors: + +============ +Contributors +============ + +There are many different ways to contribute to MNE-Python! So far we only list +code contributions below, but plan to add other metrics in the future. + +.. include:: ./code_credit.inc diff --git a/doc/sphinxext/prs/12779.json b/doc/sphinxext/prs/12779.json new file mode 100644 index 00000000000..584d8132188 --- /dev/null +++ b/doc/sphinxext/prs/12779.json @@ -0,0 +1,19 @@ +{ + "merge_commit_sha": "b8b168088cb474f27833f5f9db9d60abe00dca83", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/sphinxext/prs/1.json": { + "a": 15, + "d": 0 + }, + "doc/sphinxext/prs/3732.json": { + "a": 15, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/update_credit_rst.py b/doc/sphinxext/update_credit_rst.py new file mode 100644 index 00000000000..701136206c9 --- /dev/null +++ b/doc/sphinxext/update_credit_rst.py @@ -0,0 +1,425 @@ +"""Create code credit RST file. + +Run ./tools/dev/update_credit_json.py first to get the latest PR JSON files. +""" + +import glob +import json +import pathlib +import re +from collections import defaultdict +from pathlib import Path + +import numpy as np +import sphinx.util.logging + +import mne +from mne.utils import logger, verbose + +sphinx_logger = sphinx.util.logging.getLogger("mne") + +repo_root = Path(__file__).parents[2] +doc_root = repo_root / "doc" +data_dir = doc_root / "sphinxext" + + +def _good_name(name): + single_names = "btkcodedev buildqa sviter".split() + if name is None: + return False + assert isinstance(name, str), type(name) + if not name.strip(): + return False + if " " not in name and name not in single_names: # at least two parts + return False + if "Deleted" in name: # Avoid "Deleted user", can have in our mailmap + return False + return True + + +@verbose +def generate_credit_rst(app=None, *, verbose=False): + """Get the credit RST.""" + # TODO: Maybe someday deduplicate names.inc, GitHub profile names that we pull, and + # our commit history / .mailmap. All three names can mismatch. + sphinx_logger.info("Creating code credit RST inclusion file") + ignores = [ + int(ignore.split("#", maxsplit=1)[1].strip().split()[0][:-1]) + for ignore in (repo_root / ".git-blame-ignore-revs") + .read_text("utf-8") + .splitlines() + if not ignore.strip().startswith("#") and ignore.strip() + ] + ignores = {str(ig): [] for ig in ignores} + + # Use mailmap to help translate emails to names + mailmap = dict() + # mapping from email to name + name_map: dict[str, str] = dict() + for line in (repo_root / ".mailmap").read_text("utf-8").splitlines(): + name = re.match("^([^<]+) <([^<>]+)>", line.strip()).group(1) + assert _good_name(name), repr(name) + emails = list(re.findall("<([^<>]+)>", line.strip())) + assert len(emails) > 0 + new = emails[0] + if new in name_map: + assert name_map[new] == name + else: + name_map[new] = name + if len(emails) == 1: + continue + for old in emails[1:]: + if old in mailmap: + assert new == mailmap[old] # can be different names + else: + mailmap[old] = new + if old in name_map: + assert name_map[old] == name + else: + name_map[old] = name + + unknown_emails: set[str] = set() + + # dict with (name, commit) keys, values are int change counts + # ("commits" is really "PRs" for Python mode) + commits: dict[tuple[str], int] = defaultdict(lambda: 0) + + # dict with filename keys, values are dicts with name keys and +/- ndarrays + stats: dict[str, dict[str, np.ndarray]] = defaultdict( + lambda: defaultdict( + lambda: np.zeros(2, int), + ), + ) + + bad_commits = set() + + for fname in sorted(glob.glob(str(data_dir / "prs" / "*.json"))): + commit = Path(fname).stem # PR number is in the filename + data = json.loads(Path(fname).read_text("utf-8")) + del fname + assert data != {} + authors = data["authors"] + for author in authors: + if ( + author["e"] is not None + and author["e"] not in name_map + and _good_name(author["n"]) + ): + name_map[author["e"]] = author["n"] + for file, counts in data["changes"].items(): + if commit in ignores: + ignores[commit].append([file, commit]) + continue + p, m = counts["a"], counts["d"] + used_authors = set() + for author in authors: + if author["e"] is not None: + if author["e"] not in name_map: + unknown_emails.add( + f'{author["e"].ljust(29)} ' + "https://github.com/mne-tools/mne-python/pull/" + f"{commit}/files" + ) + continue + name = name_map[author["e"]] + else: + name = author["n"] + if name is None: + bad_commits.add(commit) + continue + if name in used_authors: + continue + assert name.strip(), repr(name) + used_authors.add(name) + # treat moves and permission changes like a single-line change + if p == m == 0: + p = 1 + commits[(name, commit)] += p + m + stats[file][name] += [p, m] + if bad_commits: + raise RuntimeError( + "Run:\nrm " + + " ".join(f"{bad}.json" for bad in sorted(bad_commits, key=int)) + ) + + # Check for duplicate names based on last name. + # Below are surnames where we have more than one distinct contributor: + name_counts = dict( + Das=2, + Drew=2, + Li=2, + Peterson=2, + Wong=2, + Zhang=2, + ) + last_map = defaultdict(lambda: set()) + for these_stats in stats.values(): + for name in these_stats: + last = name.split()[-1] + last_map[last].add(name) + bad_names = dict() + for last, names in last_map.items(): + if len(names) > name_counts.get(last, 1): + bad_names[last] = sorted(names) + if bad_names: + raise RuntimeError("Unexpected duplicate names found:\n" + "\n".join(bad_names)) + + unknown_emails = set( + email + for email in unknown_emails + if "autofix-ci[bot]" not in email + and "pre-commit-ci[bot]" not in email + and "dependabot[bot]" not in email + ) + assert len(unknown_emails) == 0, "Unknown emails\n" + "\n".join( + sorted(unknown_emails) + ) + + logger.info("Biggest included commits/PRs:") + commits = dict( + (k, commits[k]) + for k in sorted(commits, key=lambda k_: commits[k_], reverse=True) + ) + for ni, name in enumerate(commits, 1): + if ni > 10: + break + logger.info(f"{str(name[1]).ljust(5)} @ {commits[name]:5d} by {name[0]}") + + logger.info("\nIgnored commits:") + # Report the ignores + for commit in ignores: # should have found one of each + logger.info(f"ignored {len(ignores[commit]):3d} files for {commit}") + assert len(ignores[commit]) >= 1, (ignores[commit], commit) + globs = dict() + + # This is the mapping from changed filename globs to module names on the website. + # We need to include aliases for old stuff. Anything we want to exclude we put in + # "null" with a higher priority (i.e., in dict first): + link_overrides = dict() # overrides for links + for key in """ + *.qrc *.png *.svg *.ico *.elc *.sfp *.lout *.lay *.csd *.txt + mne/_version.py mne/externals/* */__init__.py* */resources.py paper.bib + mne/html/*.css mne/html/*.js mne/io/bti/tests/data/* */SHA1SUMS *__init__py + AUTHORS.rst CITATION.cff CONTRIBUTING.rst codemeta.json mne/tests/*.* jr-tools + */whats_new.rst */latest.inc */devel.rst */changelog.rst */manual/* doc/*.json + logo/LICENSE doc/credit.rst + """.strip().split(): + globs[key] = "null" + # Now onto the actual module organization + root_path = pathlib.Path(mne.__file__).parent + mod_file_map = dict() + for file in root_path.iterdir(): + rel = file.relative_to(root_path).with_suffix("") + mod = f"mne.{rel}" + if file.is_dir(): + globs[f"mne/{rel}/*.*"] = mod + globs[f"mne/{rel}.*"] = mod + elif file.is_file() and file.suffix == ".py": + key = f"mne/{rel}.py" + if file.stem == "conftest": + globs[key] = "maintenance" + globs["conftest.py"] = "maintenance" + else: + globs[key] = mod + mod_file_map[mod] = key + globs["mne/artifacts/*.py"] = "mne.preprocessing" + for key in """ + pick.py constants.py info.py fiff/*.* _fiff/*.* raw.py testing.py _hdf5.py + compensator.py + """.strip().split(): + globs[f"mne/{key}"] = "mne.io" + for key in ("mne/transforms/*.py", "mne/_freesurfer.py"): + globs[key] = "mne.transforms" + globs["mne/mixed_norm/*.py"] = "mne.inverse_sparse" + globs["mne/__main__.py"] = "mne.commands" + globs["bin/*"] = "mne.commands" + globs["mne/morph_map.py"] = "mne.surface" + globs["mne/baseline.py"] = "mne.epochs" + for key in """ + parallel.py rank.py misc.py data/*.* defaults.py fixes.py icons/*.* icons.* + """.strip().split(): + globs[f"mne/{key}"] = "mne.utils" + for key in ("mne/_ola.py", "mne/cuda.py"): + globs[key] = "mne.filter" + for key in """ + *digitization/*.py layouts/*.py montages/*.py selection.py + """.strip().split(): + globs[f"mne/{key}"] = "mne.channels" + globs["mne/sparse_learning/*.py"] = "mne.inverse_sparse" + globs["mne/csp.py"] = "mne.preprocessing" + globs["mne/bem_surfaces.py"] = "mne.bem" + globs["mne/coreg/*.py"] = "mne.coreg" + globs["mne/inverse.py"] = "mne.minimum_norm" + globs["mne/stc.py"] = "mne.source_estimate" + globs["mne/surfer.py"] = "mne.viz" + globs["mne/tfr.py"] = "mne.time_frequency" + globs["mne/connectivity/*.py"] = "mne-connectivity (moved)" + link_overrides["mne-connectivity (moved)"] = "mne-tools/mne-connectivity" + globs["mne/realtime/*.py"] = "mne-realtime (moved)" + link_overrides["mne-realtime (moved)"] = "mne-tools/mne-realtime" + globs["mne/html_templates/*.*"] = "mne.report" + globs[".circleci/*"] = "maintenance" + link_overrides["maintenance"] = "mne-tools/mne-python" + globs["tools/*"] = "maintenance" + globs["doc/*"] = "doc" + for key in ("*.py", "*.rst"): + for mod in ("examples", "tutorials", "doc"): + globs[f"{mod}/{key}"] = mod + for key in """ + *.yml *.md setup.* MANIFEST.in Makefile README.rst flow_diagram.py *.toml + debian/* logo/*.py *.git* .pre-commit-config.yaml .mailmap .coveragerc make/* + """.strip().split(): + globs[key] = "maintenance" + + mod_stats = defaultdict(lambda: defaultdict(lambda: np.zeros(2, int))) + other_files = set() + total_lines = np.zeros(2, int) + for fname, counts in stats.items(): + for pattern, mod in globs.items(): + if glob.fnmatch.fnmatch(fname, pattern): + break + else: + other_files.add(fname) + mod = "other" + for e, pm in counts.items(): + if mod == "mne._fiff": + raise RuntimeError + # sanity check a bit + if mod != "null" and (".png" in fname or "/manual/" in fname): + raise RuntimeError(f"Unexpected {mod} {fname}") + mod_stats[mod][e] += pm + mod_stats["mne"][e] += pm + total_lines += pm + mod_stats.pop("null") # stuff we shouldn't give credit for + mod_stats = dict( + (k, mod_stats[k]) + for k in sorted( + mod_stats, + key=lambda x: ( + not x.startswith("mne"), + x == "maintenance", + x.replace("-", "."), + ), + ) + ) # sort modules alphabetically + other_files = sorted(other_files) + if len(other_files): + raise RuntimeError( + f"{len(other_files)} misc file(s) found:\n" + "\n".join(other_files) + ) + logger.info(f"\nTotal line change count: {list(total_lines)}") + + # sphinx-design badges that we use for contributors + BADGE_KINDS = ["bdg-info-line", "bdg"] + content = f"""\ +.. THIS FILE IS AUTO-GENERATED BY {Path(__file__).stem} AND WILL BE OVERWRITTEN + +.. raw:: html + + + +.. _code_credit: + +Code credit +=========== + +Below are lists of code contributors to MNE-Python. The numbers in parentheses are the +number of lines changed in our code history. + +- :{BADGE_KINDS[0]}:`This badge` is used for the top 10% of contributors. +- :{BADGE_KINDS[1]}:`This badge` is used for the remaining 90% of contributors. + +Entire codebase +--------------- + +""" + for mi, (mod, counts) in enumerate(mod_stats.items()): + if mi == 0: + assert mod == "mne", mod + indent = " " * 3 + elif mi == 1: + indent = " " * 6 + content += """ + +By submodule +------------ + +Contributors often have domain-specific expertise, so we've broken down the +contributions by submodule as well below. + +.. grid:: 1 2 3 3 + :gutter: 1 + +""" + # if there are 10 this is 100, if there are 100 this is 100 + these_stats = dict((k, v.sum()) for k, v in counts.items()) + these_stats = dict( + (k, these_stats[k]) + for k in sorted(these_stats, key=lambda x: these_stats[x], reverse=True) + ) + if mod in link_overrides: + link = f"https://github.com/{link_overrides[mod]}" + else: + kind = "blame" if mod in mod_file_map else "tree" + link_mod = mod_file_map.get(mod, mod.replace(".", "/")) + link = f"https://github.com/mne-tools/mne-python/{kind}/main/{link_mod}" + assert "moved" not in link, (mod, link) + # Use badges because they flow nicely, inside a grid to make it more compact + stat_lines = [] + for ki, (k, v) in enumerate(these_stats.items()): + # Round to two digits, e.g. 12340 -> 12000, 12560 -> 13000 + v_round = int(float(f"{v:.2g}")) + assert v_round > 0, f"Got zero lines changed for {k} in {mod}: {v_round}" + # And then write as a max-3-char human-readable abbreviation like + # 123, 1.2k, 123k, 12m, etc. + for prefix in ("", "k", "m", "g"): + if v_round >= 1000: + v_round = v_round / 1000 + else: + if v_round >= 10 or prefix == "": # keep single digit as 1 not 1.0 + v_round = f"{int(round(v_round))}" + else: + v_round = f"{v_round:.1f}" + v_round += prefix + break + else: + raise RuntimeError(f"Too many digits in {v}") + idx = 0 if ki < (len(these_stats) - 1) // 10 + 1 else 1 + if "[bot]" in k or "Lumberbot" in k: + continue + stat_lines.append(f":{BADGE_KINDS[idx]}:`{k} ({v_round})`") + stat_lines = f"\n{indent}".join(stat_lines) + if mi == 0: + content += f""" + +.. card:: {mod} + :class-card: overflow-auto + :link: https://github.com/mne-tools/mne-python/graphs/contributors + +{indent}{stat_lines} + +""" + else: + content += f""" + + .. grid-item-card:: {mod} + :class-card: overflow-auto + :link: {link} + +{indent}{stat_lines} + +""" + (doc_root / "code_credit.inc").write_text(content, encoding="utf-8") + + +if __name__ == "__main__": + generate_credit_rst(verbose=True) diff --git a/ignore_words.txt b/ignore_words.txt index cc0edd4fcc4..a9f983cbdae 100644 --- a/ignore_words.txt +++ b/ignore_words.txt @@ -39,3 +39,4 @@ aas vor connec sme +tim diff --git a/pyproject.toml b/pyproject.toml index c0fabebc5c0..420987f8fcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -274,6 +274,8 @@ skips = ["*/test_*.py"] # assert statements are good practice with pytest report_level = "WARNING" ignore_roles = [ "attr", + "bdg-primary-line", + "bdg-info-line", "class", "doc", "eq", @@ -298,6 +300,7 @@ ignore_directives = [ "automodule", "autosummary", "bibliography", + "card", "cssclass", "currentmodule", "dropdown", diff --git a/tools/check_mne_location.py b/tools/check_mne_location.py index 8dccf9df091..f4975810731 100755 --- a/tools/check_mne_location.py +++ b/tools/check_mne_location.py @@ -3,6 +3,7 @@ # Copyright the MNE-Python contributors. from pathlib import Path + import mne want_mne_dir = Path(__file__).parents[1] / "mne" diff --git a/tools/dev/check_steering_committee.py b/tools/dev/check_steering_committee.py index 419e9fd4164..19f4b74a958 100755 --- a/tools/dev/check_steering_committee.py +++ b/tools/dev/check_steering_committee.py @@ -5,10 +5,11 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from collections import Counter import os import pprint -from datetime import timezone, datetime, timedelta +from collections import Counter +from datetime import datetime, timedelta, timezone + from github import Auth, Github from github.Commit import Commit from tqdm import tqdm diff --git a/tools/dev/ensure_headers.py b/tools/dev/ensure_headers.py index 435376ace37..dd2f8a98042 100644 --- a/tools/dev/ensure_headers.py +++ b/tools/dev/ensure_headers.py @@ -30,6 +30,7 @@ def get_paths_from_tree(root, level=0): + """Get paths from a GitPython tree.""" for entry in root: if entry.type == "tree": yield from get_paths_from_tree(entry, level + 1) diff --git a/tools/dev/gen_css_for_mne.py b/tools/dev/gen_css_for_mne.py index ca7210c8918..54354f9762e 100644 --- a/tools/dev/gen_css_for_mne.py +++ b/tools/dev/gen_css_for_mne.py @@ -16,11 +16,12 @@ # Copyright the MNE-Python contributors. import base64 -import mne from pathlib import Path import rcssmin +import mne + base_dir = Path(mne.__file__).parent / "report" / "js_and_css" / "bootstrap-icons" css_path_in = base_dir / "bootstrap-icons.css" css_path_out = base_dir / "bootstrap-icons.mne.css" diff --git a/tools/dev/generate_pyi_files.py b/tools/dev/generate_pyi_files.py index 97deb34f837..98cdb6c08ce 100644 --- a/tools/dev/generate_pyi_files.py +++ b/tools/dev/generate_pyi_files.py @@ -7,6 +7,7 @@ import ast_comments as ast import black + import mne diff --git a/tools/dev/update_credit_json.py b/tools/dev/update_credit_json.py new file mode 100644 index 00000000000..fc3d0b2787a --- /dev/null +++ b/tools/dev/update_credit_json.py @@ -0,0 +1,88 @@ +"""Collect credit information for PRs. + +The initial run takes a long time (hours!) due to GitHub rate limits, even with +a personal GITHUB_TOKEN. +""" + +import json +import os +import re +from pathlib import Path + +from github import Auth, Github +from tqdm import tqdm + +auth = Auth.Token(os.environ["GITHUB_TOKEN"]) +g = Github(auth=auth, per_page=100) +out_path = Path(__file__).parents[2] / "doc" / "sphinxext" / "prs" +out_path.mkdir(exist_ok=True) +oldest_pr = 6915 # can update this when the oldest open PR changes to speed things up + +# JSON formatting +json_kwargs = dict(indent=2, ensure_ascii=False, sort_keys=False) +# If the above arguments are changed, existing JSON should also be reformatted with +# something like: +# for fname in sorted(glob.glob("doc/sphinxext/prs/*.json")): +# fname = Path(fname).resolve(strict=True) +# fname.write_text(json.dumps(json.loads(fname.read_text("utf-8")), **json_kwargs), "utf-8") # noqa: E501 + +repo = g.get_repo("mne-tools/mne-python") +co_re = re.compile("Co-authored-by: ([^<>]+) <([^()>]+)>") +# We go in descending order of updates and `break` when we encounter a PR we have +# already committed a file for. +pulls_iter = repo.get_pulls(state="closed", sort="created", direction="desc") +iter_ = tqdm(pulls_iter, unit="pr", desc="Traversing") +last = 0 +n_added = 0 +for pull in iter_: + fname_out = out_path / f"{pull.number}.json" + if pull.number < oldest_pr: + iter_.close() + print( + f"After checking {iter_.n + 1} and adding {n_added} PR(s), " + f"found PR number less than oldest existing file {fname_out}, stopping" + ) + break + if fname_out.is_file(): + continue + + # PR diff credit + if not pull.merged: + continue + out = dict() + # One option is to do a git diff between pull.base and pull.head, + # but let's see if we can stay pythonic + out["merge_commit_sha"] = pull.merge_commit_sha + # Prefer the GitHub username information because it should be most up to date + name, email = pull.user.name, pull.user.email + if name is None and email is None: + # no usable GitHub user information, pull it from the first commit + author = pull.get_commits()[0].commit.author + name, email = author.name, author.email + out["authors"] = [dict(n=name, e=email)] + # For PR 54 for example this is empty for some reason! + if out["merge_commit_sha"]: + try: + merge_commit = repo.get_commit(out["merge_commit_sha"]) + except Exception: + pass # this happens on a lot of old PRs for some reason + else: + msg = merge_commit.commit.message.replace("\r", "") + for n, e in co_re.findall(msg): + # sometimes commit messages like for 9754 contain all + # commit messages and include some repeated co-authorship messages + if n not in {a["n"] for a in out["authors"]}: + out["authors"].append(dict(n=n, e=e)) + out["changes"] = dict() + for file in pull.get_files(): + out["changes"][file.filename] = { + k[0]: getattr(file, k) for k in ("additions", "deletions") + } + n_added += 1 + fname_out.write_text(json.dumps(out, **json_kwargs), encoding="utf-8") + + # TODO: Should add: + # pull.get_comments() + # pull.get_review_comments() + +g.close() diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py index a1c1fac77b4..150807a8f15 100644 --- a/tools/generate_codemeta.py +++ b/tools/generate_codemeta.py @@ -1,11 +1,12 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. import subprocess -import tomllib from argparse import ArgumentParser from datetime import date from pathlib import Path +import tomllib + parser = ArgumentParser(description="Generate codemeta.json and CITATION.cff") parser.add_argument("release_version", type=str) release_version = parser.parse_args().release_version