Repository: brainglobe/brainglobe-utils (language: python)

Issue:
Add citation tool
Docs PR is against website : https://github.com/brainglobe/brainglobe.github.io/pull/146

https://github.com/brainglobe/BrainGlobe/issues/61 gets closer to being resolved.

Would recommend reading the docs PR before the source code :sweat_smile:

## TLDR; on the command line you can now ask BrainGlobe to cite itself:

```bash
$ cite-brainglobe brainglobe atlasapi
@article{bg-atlasapi,
    authors = "Federico Claudi and Luigi Petrucco and Adam Tyson and Tiago Branco and Troy Margrie and Ruben Portugues",
    title = "BrainGlobe Atlas API: a common interface for neuroanatomical atlases",
    journal = "Journal of Open Source Software",
    year = "2020",
    volume = "5",
    month = "10",
    doi = "10.21105/joss.02668",
}

@software{brainglobe-meta,
    authors = "BrainGlobe Developers and Wiliam Michael Graham",
    title = "BrainGlobe",
    url = "https://brainglobe.info/",
    year = "2024",
    abstract = "The BrainGlobe Initiative exists to facilitate the development of interoperable Python-based tools for computational neuroanatomy.",
    license = "BSD-3-Clause",
}
```

The output can be redirected to a file if the user so chooses. Other CLI options:

```bash
$ cite-brainglobe --help
usage: cite-brainglobe [-h] [-l] [-s] [-w] [-o OUTPUT_FILE] [-f FORMAT] [tools ...]

Citation generation for BrainGlobe tools.

positional arguments:
  tools                 BrainGlobe tools to be cited.

options:
  -h, --help            show this help message and exit
  -l, --list            List citable BrainGlobe tools, and formats, then exit.
  -s, --software-citations
                        Explicitly cite software source code over academic papers.
  -w, --warn-unused     Print out when citation information is omitted by the chosen citation
                        format.
  -o OUTPUT_FILE, --output-file OUTPUT_FILE
                        Output file to write citations to.
  -f FORMAT, --format FORMAT
                        Citation format to write. Will be overwritten by the inferred format if
                        the output file argument is also provided. Valid formats can be listed
                        with the -l, --list option.
```

## What's it doing?

The `brainglobe_utils.citation` module fetches `CITATION.cff`s from each repository that we tell it about, parses this information into the requested format, then spits it out.
Currently supports:

- Text, or "just a sentence for citing" as per https://github.com/brainglobe/BrainGlobe/issues/61
- BibTex format

The `text` field supports the custom "citation-sentence" field that we want to use for BrainGlobe tools. Annoyingly, we can't just query the github api to do this forwards (the part that writes outputs is not public yet) nor can we use other existing tools like cffconvert (because they complain if we have extra fields that are otherwise harmless).

## How will it be maintained?

So long as each of our repositories has a `CITATION.cff` (or otherwise equivalent file), and the `citation` module is informed of this, details will be fetched correctly even after updates to the tools themselves.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index e16642c..19fa144 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,3 +117,4 @@ venv.bak/
 .idea/
 .vs/
 *.~lock.*
+.vscode/
diff --git a/pyproject.toml b/pyproject.toml
index 72ece85..98ce8ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,36 +1,36 @@
 [project]
 name = "brainglobe-utils"
-authors = [{name = "Adam Tyson", email= "code@adamltyson.com"}]
+authors = [{ name = "Adam Tyson", email = "code@adamltyson.com" }]
 description = "Shared general purpose tools for the BrainGlobe project"
 readme = "README.md"
 requires-python = ">=3.9.0"
 dynamic = ["version"]
 
 dependencies = [
-    "natsort",
-    "pandas",
-    "psutil",
-    "configobj",
-    "tqdm",
-    "PyYAML",
-    "scipy",
-    "numpy",
-    "slurmio",
-    "tifffile",
-    "imio",
-    "bg-atlasapi",
+  "natsort",
+  "pandas",
+  "psutil",
+  "configobj",
+  "tqdm",
+  "PyYAML",
+  "scipy",
+  "numpy",
+  "slurmio",
+  "tifffile",
+  "imio",
+  "bg-atlasapi",
 ]
 
-license = {text = "MIT"}
+license = { text = "MIT" }
 
 classifiers = [
-    "Development Status :: 2 - Pre-Alpha",
-    "Programming Language :: Python",
-    "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.9",
-    "Programming Language :: Python :: 3.10",
-    "Programming Language :: Python :: 3.11",
-    "Operating System :: OS Independent",
+  "Development Status :: 2 - Pre-Alpha",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Operating System :: OS Independent",
 ]
 
 [project.urls]
@@ -40,11 +40,7 @@ source_code = "https://github.com/brainglobe/brainglobe-utils"
 user_support = "https://github.com/brainglobe/brainglobe-utils/issues"
 
 [project.optional-dependencies]
-napari = [
-  "napari>=0.4.18",
-  "qtpy",
-  "superqt"
-]
+napari = ["napari>=0.4.18", "qtpy", "superqt"]
 
 dev = [
   "qtpy",
@@ -60,16 +56,12 @@ dev = [
   "setuptools_scm",
   "pyqt5",
   "superqt",
-  "scikit-image"
+  "scikit-image",
 ]
 
 
 [build-system]
-requires = [
-    "setuptools>=45",
-    "wheel",
-    "setuptools_scm[toml]>=6.2",
-]
+requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
 build-backend = "setuptools.build_meta"
 
 [tool.setuptools]
@@ -104,7 +96,7 @@ ignore = [
 
 [tool.ruff]
 line-length = 79
-exclude = ["__init__.py","build",".eggs"]
+exclude = ["__init__.py", "build", ".eggs"]
 select = ["I", "E", "F"]
 fix = true
 
@@ -115,3 +107,28 @@ ignore_errors = true
 module = ["imlib.cells.*"]
 ignore_errors = false
 strict = true
+
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+envlist = py{39,310,311}
+isolated_build = True
+
+[gh-actions]
+python =
+    3.9: py39
+    3.10: py310
+    3.11: py311
+
+[testenv]
+extras =
+    dev
+commands =
+    pytest -v --color=yes --cov=brainglobe_utils --cov-report=xml
+passenv =
+    CI
+    GITHUB_ACTIONS
+    DISPLAY
+    XAUTHORITY
+    PYVISTA_OFF_SCREEN
+"""
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 3857869..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,21 +0,0 @@
-[tox]
-envlist = py{39,310,311}
-isolated_build = True
-
-[gh-actions]
-python =
-    3.9: py39
-    3.10: py310
-    3.11: py311
-
-[testenv]
-extras =
-    dev
-commands =
-    pytest -v --color=yes --cov=brainglobe_utils --cov-report=xml
-passenv =
-    CI
-    GITHUB_ACTIONS
-    DISPLAY
-    XAUTHORITY
-    PYVISTA_OFF_SCREEN


Test patch:
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index 286c6a8..ab62ef0 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -3,10 +3,11 @@ name: tests
 on:
   push:
     branches:
-      - '*'
+      - 'main'
     tags:
-      - '*'
+      - 'v*'
   pull_request:
+  workflow_dispatch:
 
 jobs:
   linting:
diff --git a/tests/tests/test_brainmapper/test_analysis.py b/tests/tests/test_brainmapper/test_analysis.py
index d99b473..c7a4231 100644
--- a/tests/tests/test_brainmapper/test_analysis.py
+++ b/tests/tests/test_brainmapper/test_analysis.py
@@ -1,6 +1,6 @@
-import filecmp
 from pathlib import Path
 
+import pandas as pd
 import pytest
 
 from brainglobe_utils.brainmapper.analysis import (
@@ -37,9 +37,17 @@ def structures_with_points():
 
 
 def test_get_region_totals(tmp_path, points, structures_with_points):
-    """Regression test for count_points_per_brain_region for
+    """
+    Regression test for count_points_per_brain_region for
     pandas 1.5.3 -> 2.1.3+.
-    pd.Dataframe.append was deprecated and removed in this time."""
+    pd.DataFrame.append was deprecated and removed in this time.
+
+    2024-01-31: Newer versions of pandas are writing the
+    DataFrame rows in a different order. As such, filecmp is no longer
+    sufficient to check that we are still counting the number of cells
+    correctly - we will need to load the data back in and do a pandas
+    comparison.
+    """
     OUTPUT_DATA_LOC = (
         Path(__file__).parent.parent.parent / "data" / "brainmapper"
     )
@@ -54,4 +62,21 @@ def test_get_region_totals(tmp_path, points, structures_with_points):
         points, structures_with_points, volumes_path, output_path
     )
     assert output_path.exists()
-    assert filecmp.cmp(output_path, expected_output)
+
+    # Read data back in, and sort rows by the structures for comparison.
+    # NOTE: Column 'structure name' should exist, otherwise we've failed
+    # and the test will flag the error appropriately
+    expected_df = (
+        pd.read_csv(expected_output)
+        .sort_values("structure_name")
+        .reset_index(drop=True)
+    )
+    produced_df = (
+        pd.read_csv(output_path)
+        .sort_values("structure_name")
+        .reset_index(drop=True)
+    )
+    assert expected_df.equals(
+        produced_df
+    ), "Produced DataFrame no longer matches per-region "
+    "count from expected DataFrame"