From 5cd85df130f7284840ee7a07d9399ef435b88ed6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jon=20Haitz=20Legarreta=20Gorro=C3=B1o?=
 <jon.haitz.legarreta@gmail.com>
Date: Fri, 8 Dec 2023 09:47:40 -0500
Subject: [PATCH] ENH: Adopt PEP518 and PEP631

Adopt PEP518 to specify minimum build system requirements for the
package.

Partially comply with PEP631:
- Dependencies are now stored in the `pyproject.toml` file, including
  packages that are not in PyPI (e.g. source code URLs -GitHub, etc.),
  so the related requirements file is removed.

Require `setuptools >= 66` so that the package can be effectively
installed in editable mode when using the minimum required version of
it. Avoids:
```
ERROR:
 Project file:///home/runner/work/whitematteranalysis/whitematteranalysis
 has a 'pyproject.toml' and its build backend is missing the
 'build_editable' hook. Since it does not have a 'setup.py' nor a
 'setup.cfg', it cannot be installed in editable mode. Consider using a
 build backend that supports PEP 660.
```

Require `nibabel > 3.0.0` to avoid:
```
  File "bin/wm_cluster_volumetric_measurements.py", line 7, in <module>
    import nibabel as nib
  File "python3.10/site-packages/nibabel/__init__.py", line 66, in <module>
(...)
AttributeError: module 'numpy' has no attribute 'float'.
`np.float` was a deprecated alias for the builtin `float`.
 To avoid this error in existing code, use `float` by itself. Doing this
 will not modify any behavior and is safe. If you specifically wanted
 the numpy scalar type, use `np.float64` here.
The aliases was originally deprecated in NumPy 1.20; for more details
 and guidance see the original release note at:
https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations. Did you mean: 'cfloat'?
```

Require `statsmodels >= 0.14.0` to avoid

Require `vtk>=9.2.2` for `python_version >= '3.10'` to avoid:
```
ERROR: Could not find a version that satisfies the requirement
 vtk==9.2.0
 (from versions: 9.2.2, 9.2.4, 9.2.5, 9.2.6, 9.3.0rc1, 9.3.0rc2, 9.3.0, 9.3.20230807rc0)
ERROR: No matching distribution found for vtk==9.2.0
```

and `vtk>=9.2.4` for Python 3.11 to avoid:
```
  File "whitematteranalysis/cluster.py", line 16, in <module>
    import vtk
ModuleNotFoundError: No module named 'vtk'
```

Use `float` instad of `np.float` in `tract_measurement.py`. Fixes:
```
AttributeError: module 'numpy' has no attribute 'float'.
`np.float` was a deprecated alias for the builtin `float`. To avoid
this error in existing code, use `float` by itself. Doing this will not
modify any behavior and is safe. If you specifically wanted the numpy
scalar type, use `np.float64` here.
```

Adapt the Sphinx documentation config file to read the necessary
metadata from the `pyproject.toml` file.

Add the `tomli` package to the documentation dependencies so that
`readthedocs` can successfully find it.

Prepend the module location to the script testing `script_runner.run`
calls.

Adopt `tox.ini` for testing automation and standardization.

Adapt the GitHub Actions workflow accordingly.

Documentation:
https://www.python.org/dev/peps/pep-0518/
https://www.python.org/dev/peps/pep-0631/
---
 .github/workflows/test_package.yaml           |  24 ++--
 .../test_harden_transform_with_slicer.py      |   2 +-
 bin/tests/test_wm_append_clusters.py          |   2 +-
 ...wm_append_clusters_to_anatomical_tracts.py |   2 +-
 ...pend_diffusion_measures_across_subjects.py |   2 +-
 ...m_assess_cluster_location_by_hemisphere.py |   2 +-
 bin/tests/test_wm_average_tract_measures.py   |   2 +-
 bin/tests/test_wm_change_nrrd_dir.py          |   2 +-
 bin/tests/test_wm_cluster_from_atlas.py       |   2 +-
 bin/tests/test_wm_cluster_remove_outliers.py  |   2 +-
 ...test_wm_cluster_volumetric_measurements.py |   3 +-
 bin/tests/test_wm_compare_vtks.py             |   2 +-
 bin/tests/test_wm_create_mrml_file.py         |   2 +-
 bin/tests/test_wm_diffusion_measurements.py   |   2 +-
 ..._wm_download_anatomically_curated_atlas.py |   2 +-
 bin/tests/test_wm_harden_transform.py         |   2 +-
 bin/tests/test_wm_preprocess_all.py           |   2 +-
 ...est_wm_quality_control_after_clustering.py |   3 +-
 ...wm_quality_control_cluster_measurements.py |   2 +-
 .../test_wm_quality_control_tract_overlap.py  |   3 +-
 .../test_wm_quality_control_tractography.py   |   3 +-
 .../test_wm_register_multisubject_faster.py   |   3 +-
 bin/tests/test_wm_register_to_atlas_new.py    |   2 +-
 bin/tests/test_wm_remove_data_along_tracts.py |   2 +-
 ...test_wm_separate_clusters_by_hemisphere.py |   2 +-
 bin/tests/test_wm_tract_to_volume.py          |   2 +-
 bin/tests/test_wm_vtp2vtk.py                  |   2 +-
 docs/source/conf.py                           |  41 ++----
 pyproject.toml                                | 124 ++++++++++++++++++
 requirements.txt                              |  15 ---
 requirements_dev.txt                          |   3 -
 setup.py                                      |  49 -------
 tox.ini                                       |  31 +++++
 .../tests/test_wm_assess_cluster_location.py  |   2 +-
 .../tests/test_wm_compute_FA_from_DWIs.py     |   2 +-
 utilities/tests/test_wm_compute_TAP.py        |   2 +-
 ..._compute_bundle_feature_population_math.py |   2 +-
 utilities/tests/test_wm_extract_cluster.py    |   2 +-
 .../test_wm_extract_clusters_by_endpoints.py  |   3 +-
 utilities/tests/test_wm_fix_UKF_trace.py      |   2 +-
 ...m_fix_hemisphere_loc_name_in_tractogram.py |   2 +-
 .../test_wm_flatten_length_distribution.py    |   3 +-
 utilities/tests/test_wm_laterality_all.py     |   3 +-
 .../tests/test_wm_measure_all_clusters.py     |   2 +-
 .../tests/test_wm_measure_endpoint_overlap.py |   3 +-
 .../tests/test_wm_query_atlas_bundle_names.py |   3 +-
 utilities/tests/test_wm_statistics.py         |   2 +-
 .../tests/test_wm_statistics_export_data.py   |   3 +-
 utilities/tests/test_wm_transform_polydata.py |   2 +-
 whitematteranalysis/tract_measurement.py      |   2 +-
 50 files changed, 232 insertions(+), 152 deletions(-)
 create mode 100644 pyproject.toml
 delete mode 100644 requirements.txt
 delete mode 100644 requirements_dev.txt
 delete mode 100755 setup.py
 create mode 100644 tox.ini

diff --git a/.github/workflows/test_package.yaml b/.github/workflows/test_package.yaml
index 7226c9c6..36bdbf75 100644
--- a/.github/workflows/test_package.yaml
+++ b/.github/workflows/test_package.yaml
@@ -28,9 +28,7 @@ jobs:
     - name: Set min. dependencies
       if: matrix.requires == 'minimal'
       run: |
-        python -c "req = open('requirements.txt').read().replace(' >= ', ' == ') ; open('requirements.txt', 'w').write(req)"
-        python -c "req = open('requirements_dev.txt').read().replace(' >= ', ' == ') ; open('requirements_dev.txt', 'w').write(req)"
-        python -c "req = open('setup.py').read().replace(' >= ', ' == ') ; open('setup.py', 'w').write(req)"
+        python -c "req = open('pyproject.toml').read().replace(' >= ', ' == ') ; open('pyproject.toml', 'w').write(req)"
 
     # - name: Cache pip
     #   uses: actions/cache@v2
@@ -45,18 +43,24 @@ jobs:
     - name: Install dependencies
       # if: steps.cache.outputs.cache-hit != 'true'
       run: |
-        python -m pip install --upgrade --user pip setuptools pytest-cov
-        pip install -r requirements_dev.txt
+        # SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL required due to
+        # some dependency listing "scikit-learn" as "sklearn" in its dependencies
+        export SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL=True
+        python -m pip install --upgrade --user pip
+        pip install setuptools tox
+        pip install -e .[test]
         python --version
         pip --version
         pip list
 
     - name: Run tests
       run: |
-        # tox --sitepackages
-        pip install -e .
+        # Test with tox
+        tox --sitepackages
+        # Test with pytest
         python -c 'import whitematteranalysis'
-        coverage run --source whitematteranalysis -m pytest -o junit_family=xunit2 -v --doctest-modules --junitxml=junit/test-results-${{ runner.os }}-${{ matrix.python-version }}.xml
+        coverage run --source whitematteranalysis -m pytest -o junit_family=xunit2 -v --junitxml=junit/test-results-${{ runner.os }}-${{ matrix.python-version }}.xml
+        # coverage run --source whitematteranalysis -m pytest -o junit_family=xunit2 -v --doctest-modules --junitxml=junit/test-results-${{ runner.os }}-${{ matrix.python-version }}.xml
 
     - name: Upload pytest test results
       uses: actions/upload-artifact@master
@@ -74,9 +78,9 @@ jobs:
     - name: Package Setup
     # - name: Run tests with tox
       run: |
-        # pip install build
+        pip install build
         # check-manifest
-        python setup.py install
+        python -m build
         # twine check dist/
         # tox --sitepackages
         # python -m tox
diff --git a/bin/tests/test_harden_transform_with_slicer.py b/bin/tests/test_harden_transform_with_slicer.py
index 73cff0dc..6858ae5f 100755
--- a/bin/tests/test_harden_transform_with_slicer.py
+++ b/bin/tests/test_harden_transform_with_slicer.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["harden_transform_with_slicer.py", "--help"])
+    ret = script_runner.run(["bin/harden_transform_with_slicer.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_append_clusters.py b/bin/tests/test_wm_append_clusters.py
index 3331ef9b..8fac56bd 100755
--- a/bin/tests/test_wm_append_clusters.py
+++ b/bin/tests/test_wm_append_clusters.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_append_clusters.py", "--help"])
+    ret = script_runner.run(["bin/wm_append_clusters.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_append_clusters_to_anatomical_tracts.py b/bin/tests/test_wm_append_clusters_to_anatomical_tracts.py
index 52449f43..fcf7d24b 100755
--- a/bin/tests/test_wm_append_clusters_to_anatomical_tracts.py
+++ b/bin/tests/test_wm_append_clusters_to_anatomical_tracts.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_append_clusters_to_anatomical_tracts.py", "--help"])
+        ["bin/wm_append_clusters_to_anatomical_tracts.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_append_diffusion_measures_across_subjects.py b/bin/tests/test_wm_append_diffusion_measures_across_subjects.py
index 76f91597..629f6427 100755
--- a/bin/tests/test_wm_append_diffusion_measures_across_subjects.py
+++ b/bin/tests/test_wm_append_diffusion_measures_across_subjects.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_append_diffusion_measures_across_subjects.py", "--help"])
+        ["bin/wm_append_diffusion_measures_across_subjects.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_assess_cluster_location_by_hemisphere.py b/bin/tests/test_wm_assess_cluster_location_by_hemisphere.py
index b457c031..d8f4205a 100755
--- a/bin/tests/test_wm_assess_cluster_location_by_hemisphere.py
+++ b/bin/tests/test_wm_assess_cluster_location_by_hemisphere.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_assess_cluster_location_by_hemisphere.py", "--help"])
+        ["bin/wm_assess_cluster_location_by_hemisphere.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_average_tract_measures.py b/bin/tests/test_wm_average_tract_measures.py
index 5a4660db..957bcb96 100755
--- a/bin/tests/test_wm_average_tract_measures.py
+++ b/bin/tests/test_wm_average_tract_measures.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_average_tract_measures.py", "--help"])
+    ret = script_runner.run(["bin/wm_average_tract_measures.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_change_nrrd_dir.py b/bin/tests/test_wm_change_nrrd_dir.py
index aeb333e5..42bb0ebe 100755
--- a/bin/tests/test_wm_change_nrrd_dir.py
+++ b/bin/tests/test_wm_change_nrrd_dir.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_change_nrrd_dir.py", "--help"])
+    ret = script_runner.run(["bin/wm_change_nrrd_dir.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_cluster_from_atlas.py b/bin/tests/test_wm_cluster_from_atlas.py
index e7af2b46..4cf21ba3 100755
--- a/bin/tests/test_wm_cluster_from_atlas.py
+++ b/bin/tests/test_wm_cluster_from_atlas.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_cluster_from_atlas.py", "--help"])
+    ret = script_runner.run(["bin/wm_cluster_from_atlas.py", "--help"])
     assert ret.success
 
 
diff --git a/bin/tests/test_wm_cluster_remove_outliers.py b/bin/tests/test_wm_cluster_remove_outliers.py
index 8f2638bf..ead4410e 100644
--- a/bin/tests/test_wm_cluster_remove_outliers.py
+++ b/bin/tests/test_wm_cluster_remove_outliers.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_cluster_remove_outliers.py", "--help"])
+    ret = script_runner.run(["bin/wm_cluster_remove_outliers.py", "--help"])
     assert ret.success
 
 
diff --git a/bin/tests/test_wm_cluster_volumetric_measurements.py b/bin/tests/test_wm_cluster_volumetric_measurements.py
index ed82430c..5ff305d9 100755
--- a/bin/tests/test_wm_cluster_volumetric_measurements.py
+++ b/bin/tests/test_wm_cluster_volumetric_measurements.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_cluster_volumetric_measurements.py", "--help"])
+    ret = script_runner.run(
+        ["bin/wm_cluster_volumetric_measurements.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_compare_vtks.py b/bin/tests/test_wm_compare_vtks.py
index 39ad4806..690ed6d5 100755
--- a/bin/tests/test_wm_compare_vtks.py
+++ b/bin/tests/test_wm_compare_vtks.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_compare_vtks.py", "--help"])
+    ret = script_runner.run(["bin/wm_compare_vtks.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_create_mrml_file.py b/bin/tests/test_wm_create_mrml_file.py
index b7865b64..fa055a8f 100755
--- a/bin/tests/test_wm_create_mrml_file.py
+++ b/bin/tests/test_wm_create_mrml_file.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_create_mrml_file.py", "--help"])
+    ret = script_runner.run(["bin/wm_create_mrml_file.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_diffusion_measurements.py b/bin/tests/test_wm_diffusion_measurements.py
index 0739fe9e..b913f481 100755
--- a/bin/tests/test_wm_diffusion_measurements.py
+++ b/bin/tests/test_wm_diffusion_measurements.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_diffusion_measurements.py", "--help"])
+    ret = script_runner.run(["bin/wm_diffusion_measurements.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_download_anatomically_curated_atlas.py b/bin/tests/test_wm_download_anatomically_curated_atlas.py
index e436de92..7688db7b 100755
--- a/bin/tests/test_wm_download_anatomically_curated_atlas.py
+++ b/bin/tests/test_wm_download_anatomically_curated_atlas.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_download_anatomically_curated_atlas.py", "--help"])
+        ["bin/wm_download_anatomically_curated_atlas.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_harden_transform.py b/bin/tests/test_wm_harden_transform.py
index 65aaa4c8..db163e23 100755
--- a/bin/tests/test_wm_harden_transform.py
+++ b/bin/tests/test_wm_harden_transform.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_harden_transform.py", "--help"])
+    ret = script_runner.run(["bin/wm_harden_transform.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_preprocess_all.py b/bin/tests/test_wm_preprocess_all.py
index e56b4103..ce13534d 100755
--- a/bin/tests/test_wm_preprocess_all.py
+++ b/bin/tests/test_wm_preprocess_all.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_preprocess_all.py", "--help"])
+    ret = script_runner.run(["bin/wm_preprocess_all.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_quality_control_after_clustering.py b/bin/tests/test_wm_quality_control_after_clustering.py
index 24eaa48c..61e4d92b 100755
--- a/bin/tests/test_wm_quality_control_after_clustering.py
+++ b/bin/tests/test_wm_quality_control_after_clustering.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_quality_control_after_clustering.py", "--help"])
+    ret = script_runner.run(
+        ["bin/wm_quality_control_after_clustering.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_quality_control_cluster_measurements.py b/bin/tests/test_wm_quality_control_cluster_measurements.py
index 5a93964f..e2414c47 100755
--- a/bin/tests/test_wm_quality_control_cluster_measurements.py
+++ b/bin/tests/test_wm_quality_control_cluster_measurements.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_quality_control_cluster_measurements.py", "--help"])
+        ["bin/wm_quality_control_cluster_measurements.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_quality_control_tract_overlap.py b/bin/tests/test_wm_quality_control_tract_overlap.py
index cb9e08bc..c45035e8 100755
--- a/bin/tests/test_wm_quality_control_tract_overlap.py
+++ b/bin/tests/test_wm_quality_control_tract_overlap.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_quality_control_tract_overlap.py", "--help"])
+    ret = script_runner.run(
+        ["bin/wm_quality_control_tract_overlap.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_quality_control_tractography.py b/bin/tests/test_wm_quality_control_tractography.py
index 5ec2e7da..f57ef9a1 100755
--- a/bin/tests/test_wm_quality_control_tractography.py
+++ b/bin/tests/test_wm_quality_control_tractography.py
@@ -3,7 +3,8 @@
 
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_quality_control_tractography.py", "--help"])
+    ret = script_runner.run(
+        ["bin/wm_quality_control_tractography.py", "--help"])
     assert ret.success
 
 
diff --git a/bin/tests/test_wm_register_multisubject_faster.py b/bin/tests/test_wm_register_multisubject_faster.py
index ef6e47c5..e76ed835 100755
--- a/bin/tests/test_wm_register_multisubject_faster.py
+++ b/bin/tests/test_wm_register_multisubject_faster.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_register_multisubject_faster.py", "--help"])
+    ret = script_runner.run(
+        ["bin/wm_register_multisubject_faster.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_register_to_atlas_new.py b/bin/tests/test_wm_register_to_atlas_new.py
index 44ca521d..448bcb28 100755
--- a/bin/tests/test_wm_register_to_atlas_new.py
+++ b/bin/tests/test_wm_register_to_atlas_new.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_register_to_atlas_new.py", "--help"])
+    ret = script_runner.run(["bin/wm_register_to_atlas_new.py", "--help"])
     assert ret.success
 
 
diff --git a/bin/tests/test_wm_remove_data_along_tracts.py b/bin/tests/test_wm_remove_data_along_tracts.py
index 03a87a0e..27289bf2 100755
--- a/bin/tests/test_wm_remove_data_along_tracts.py
+++ b/bin/tests/test_wm_remove_data_along_tracts.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_remove_data_along_tracts.py", "--help"])
+    ret = script_runner.run(["bin/wm_remove_data_along_tracts.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_separate_clusters_by_hemisphere.py b/bin/tests/test_wm_separate_clusters_by_hemisphere.py
index f7cf82c4..6556a001 100755
--- a/bin/tests/test_wm_separate_clusters_by_hemisphere.py
+++ b/bin/tests/test_wm_separate_clusters_by_hemisphere.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_separate_clusters_by_hemisphere.py", "--help"])
+    ret = script_runner.run(["bin/wm_separate_clusters_by_hemisphere.py", "--help"])
     assert ret.success
 
 
diff --git a/bin/tests/test_wm_tract_to_volume.py b/bin/tests/test_wm_tract_to_volume.py
index 99dad31b..d548a8e4 100755
--- a/bin/tests/test_wm_tract_to_volume.py
+++ b/bin/tests/test_wm_tract_to_volume.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_tract_to_volume.py", "--help"])
+    ret = script_runner.run(["bin/wm_tract_to_volume.py", "--help"])
     assert ret.success
diff --git a/bin/tests/test_wm_vtp2vtk.py b/bin/tests/test_wm_vtp2vtk.py
index 253ef1b7..e105356e 100755
--- a/bin/tests/test_wm_vtp2vtk.py
+++ b/bin/tests/test_wm_vtp2vtk.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_vtp2vtk.py", "--help"])
+    ret = script_runner.run(["bin/wm_vtp2vtk.py", "--help"])
     assert ret.success
diff --git a/docs/source/conf.py b/docs/source/conf.py
index bbe05b5b..5ce22f47 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -13,53 +13,30 @@
 import os
 import re
 from datetime import datetime
+from importlib.metadata import version as vers
+
+import tomli
 
 # import sys
 
 # sys.path.insert(0, os.path.abspath('.'))
 
-author_setup_str = "    author="
-author_email_setup_str = "    author_email="
-name_setup_str = "    name="
-version_setup_str = "    version="
-
-
 # Load the release info into a dict by explicit execution
-info = {}
-with open(os.path.abspath(os.path.join(
-        os.path.dirname(__file__), "../..", "setup.py")), "r") as f:
-    for line in f:
-        if line.startswith(name_setup_str):
-            project = (
-                re.search(name_setup_str + "(.*),", line).group(1).strip("\'"))
-        elif line.startswith(author_setup_str):
-            _author = (
-                re.search(
-                    author_setup_str + "(.*),",
-                    line).group(1).strip("\'").replace("\\", "")
-            )
-        elif line.startswith(author_email_setup_str):
-            _email = (
-                re.search(
-                    author_email_setup_str + "(.*),",
-                    line).group(1).strip("\'")
-            )
-        elif line.startswith(version_setup_str):
-            _version = (
-                re.search(
-                    version_setup_str + "(.*),",
-                    line).group(1).strip("\'")
-            )
+with open(os.path.join("../..", "pyproject.toml"), "rb") as f:
+    info = tomli.load(f)
 
 # -- Project information -----------------------------------------------------
 
+project = info["project"]["name"]
+_author = info["project"]["authors"][0]["name"]
+_email = info["project"]["authors"][1]["email"]
 copyright = f"2013-{datetime.now().year}, {_author} <{_email}>"
 author = f"{_author}s"
 
+_version = vers(project)
 # The full version, including alpha/beta/rc tags
 release = _version
 
-
 # -- General configuration ---------------------------------------------------
 
 # Add any Sphinx extension module names here, as strings. They can be
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..7ab27e72
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,124 @@
+[build-system]
+requires = [
+    "setuptools >= 66",
+    "wheel",
+    "setuptools_scm >= 6.4",
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+authors = [
+  {name = "O'Donnell lab"}, {email = "odonnell@bwh.harvard.edu"}
+]
+classifiers = [
+    "Development Status :: 3 - Alpha",
+    "Intended Audience :: Science/Research",
+    "Programming Language :: Python :: 3",
+    "Topic :: Scientific/Engineering :: Medical Science Apps.",
+]
+dependencies = [
+    "joblib >= 1.1.0",
+    "matplotlib >= 3.6.0, < 3.8",
+    "nibabel > 3.0.0",
+    "numpy >= 1.20.0, <1.21.0; python_version == '3.8'",
+    "numpy >= 1.21.0; python_version == '3.9' and python_version > '3.9'",
+    #"numpy > 1.21.0",
+    "pandas < 2.1.0",
+    "scipy >= 1.4.0, < 1.11.0; python_version == '3.8'",
+    "scipy >= 1.5.0; python_version == '3.9'",
+    "scipy >= 1.9.0; python_version == '3.10' and python_version > '3.10'",
+    #"statsmodels >= 0.10.0, < 0.13.0; python_version == '3.8'",
+    #"statsmodels >= 0.13.0; python_version >= '3.9'",
+    "statsmodels >= 0.14.0",
+    "vtk >= 9.1.0, < 9.2.0; python_version >= '3.8' and python_version <= '3.9'",
+    "vtk >= 9.2.2; python_version == '3.10'",
+    "vtk >= 9.2.4; python_version >= '3.11'",
+    "xlrd",
+]
+description = "Diffusion MRI fiber clustering and tractography analysis."
+dynamic = ["version"]
+keywords = ["dMRI, neuroimaging, tractography"]
+maintainers = [
+  {name = "O'Donnell lab"}, {email = "odonnell@bwh.harvard.edu"}
+]
+name = "whitematteranalysis"
+readme = "README.md"
+requires-python = ">=3.8"
+
+[project.optional-dependencies]
+test = [
+    "pytest >= 7.2",
+    "pytest-cov",
+    "pytest_console_scripts",
+    "pytest-xdist",
+]
+dev = [
+    "pre-commit",
+]
+doc = [
+    "sphinx",
+    "sphinx-argparse",
+    "sphinx-rtd-theme",
+    "tomli",
+]
+
+[project.scripts]
+harden_transform_with_slicer = "bin.harden_transform_with_slicer:main"
+wm_append_clusters = "bin.wm_append_clusters:main"
+wm_append_clusters_to_anatomical_tracts = "bin.wm_append_clusters_to_anatomical_tracts:main"
+wm_append_diffusion_measures_across_subjects = "bin.wm_append_diffusion_measures_across_subjects:main"
+wm_assess_cluster_location_by_hemisphere = "bin.wm_assess_cluster_location_by_hemisphere:main"
+wm_average_tract_measures = "bin.wm_average_tract_measures:main"
+wm_change_nrrd_dir = "bin.wm_change_nrrd_dir:main"
+wm_cluster_atlas = "bin.wm_cluster_atlas:main"
+wm_cluster_from_atlas = "bin.wm_cluster_from_atlas:main"
+wm_cluster_remove_outliers = "bin.wm_cluster_remove_outliers:main"
+wm_cluster_volumetric_measurements = "bin.wm_cluster_volumetric_measurements:main"
+wm_compare_vtks = "bin.wm_compare_vtks:main"
+wm_create_mrml_file = "bin.wm_create_mrml_file:main"
+wm_diffusion_measurements = "bin.wm_diffusion_measurements:main"
+wm_download_anatomically_curated_atlas = "bin.wm_download_anatomically_curated_atlas:main"
+wm_harden_transform = "bin.wm_harden_transform:main"
+wm_preprocess_all = "bin.wm_preprocess_all:main"
+wm_quality_control_after_clustering = "bin.wm_quality_control_after_clustering:main"
+wm_quality_control_cluster_measurements = "bin.wm_quality_control_cluster_measurements:main"
+wm_quality_control_tractography = "bin.wm_quality_control_tractography:main"
+wm_quality_control_tract_overlap = "bin.wm_quality_control_tract_overlap:main"
+wm_register_multisubject_faster = "bin.wm_register_multisubject_faster:main"
+wm_register_to_atlas_new = "bin.wm_register_to_atlas_new:main"
+wm_remove_data_along_tracts = "bin.wm_remove_data_along_tracts:main"
+wm_separate_clusters_by_hemisphere = "bin.wm_separate_clusters_by_hemisphere:main"
+wm_tract_to_volume = "bin.wm_tract_to_volume:main"
+wm_vtp2vtk = "bin.wm_vtp2vtk:main"
+wm_assess_cluster_location = "utilities.wm_assess_cluster_location:main"
+wm_compute_bundle_feature_population_math = "utilities.wm_compute_bundle_feature_population_math:main"
+wm_compute_FA_from_DWIs = "utilities.wm_compute_FA_from_DWIs:main"
+wm_compute_TAP = "utilities.wm_compute_TAP:main"
+wm_extract_cluster = "utilities.wm_extract_cluster:main"
+wm_extract_clusters_by_endpoints = "utilities.wm_extract_clusters_by_endpoints:main"
+wm_fix_hemisphere_loc_name_in_tractogram = "utilities.wm_fix_hemisphere_loc_name_in_tractogram:main"
+wm_fix_UKF_trace = "utilities.wm_fix_UKF_trace:main"
+wm_flatten_length_distribution = "utilities.wm_flatten_length_distribution:main"
+wm_laterality_all = "utilities.wm_laterality_all:main"
+wm_measure_all_clusters = "utilities.wm_measure_all_clusters:main"
+wm_measure_endpoint_overlap = "utilities.wm_measure_endpoint_overlap:main"
+wm_query_atlas_bundles_names = "utilities.wm_query_atlas_bundles_names:main"
+wm_statistics_export_data = "utilities.wm_statistics_export_data:main"
+wm_statistics = "utilities.wm_statistics:main"
+wm_transform_polydata = "utilities.wm_transform_polydata:main"
+
+[options.extras_require]
+all = [
+    "%(test)s",
+]
+
+[project.urls]
+homepage = "https://whitematteranalysis.readthedocs.io/en/latest/"
+documentation = "https://whitematteranalysis.readthedocs.io/en/latest/"
+repository = "https://github.com/SlicerDMRI/whitematteranalysis"
+
+[tool.setuptools.packages]
+find = {}  # Scanning implicit namespaces is active by default
+
+[tool.setuptools_scm]
+write_to = "whitematteranalysis/_version.py"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index dd2f098e..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-joblib>=1.1.0
-matplotlib>=3.6.0,<3.8
-nibabel>=3.0.0
-numpy>=1.20.0,<1.21.0;python_version=="3.8"
-numpy>=1.21.0;python_version>="3.9"
-pandas<2.1.0
-scipy>=1.4.0,<1.11.0;python_version=="3.8"
-scipy>=1.5.0;python_version=="3.9"
-scipy>=1.9.0;python_version>="3.10"
-setuptools>=44.0.0
-statsmodels>=0.10.0,<0.13.0;python_version=="3.8"
-statsmodels>=0.13.0;python_version>="3.9"
-vtk>=9.1.0,<9.2.0;python_version>="3.8" and python_version<="3.9"
-vtk>=9.2.0;python_version>="3.10"
-xlrd
diff --git a/requirements_dev.txt b/requirements_dev.txt
deleted file mode 100644
index 3926f27b..00000000
--- a/requirements_dev.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pre-commit
-pytest
-pytest_console_scripts
diff --git a/setup.py b/setup.py
deleted file mode 100755
index b0fae485..00000000
--- a/setup.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import glob
-import os
-import sys
-from itertools import chain
-
-from setuptools import find_packages, setup
-
-#    url='http://pypi.python.org/pypi/whitematteranalysis/',
-#    scripts=['bin/test1.py','bin/test2.py'],
-
-# ext_modules work-around: https://stackoverflow.com/a/38057196
-# get_include work-around: https://stackoverflow.com/a/21621689
-
-if sys.platform == 'win32':
-    # force setuptools to use the MSVC compiler environment
-    # otherwise it will check version for VC9, and error out
-    os.environ['MSSdk'] = '1'
-    os.environ['DISTUTILS_USE_SDK'] = '1'
-
-
-with open("requirements.txt") as f:
-    required_dependencies = f.read().splitlines()
-    external_dependencies = []
-    for dependency in required_dependencies:
-        if dependency[0:2] == "-e":
-            repo_name = dependency.split("=")[-1]
-            repo_url = dependency[3:]
-            external_dependencies.append("f{repo_name} @ {repo_url}")
-        else:
-            external_dependencies.append(dependency)
-
-
-setup(
-    name='whitematteranalysis',
-    version='0.3.0',
-    author='Fan Zhang and Lauren O\'Donnell',
-    author_email='odonnell@bwh.harvard.edu',
-    packages=find_packages(exclude=["docs"]),
-    license='LICENSE.txt',
-    description='Processing of whole-brain streamline tractography.',
-    long_description=open('README.md').read(),
-    long_description_content_type="text/markdown",
-    install_requires=external_dependencies,
-    extras_require={'doc': ['sphinx', 'sphinx-argparse', 'sphinx_rtd_theme']},
-    scripts=list(chain.from_iterable([
-        glob.glob("bin/[a-zA-Z]*.py"),
-        glob.glob("utilities/[a-zA-Z]*.py"),
-        ]))
-)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..5ee9938e
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,31 @@
+[tox]
+envlist = {py38,py39,py310,py311}
+isolated_build = True
+
+[testenv]
+commands = python -m pytest
+deps = pytest-cov
+extras = test
+
+[testenv:isort]
+skip_install = True
+deps = pre-commit
+commands = pre-commit run isort --all-files
+
+[testenv:import-lint]
+skip_install = True
+deps = pre-commit
+commands = pre-commit run --hook-stage manual import-linter --all-files
+
+[testenv:package]
+isolated_build = True
+skip_install = True
+deps =
+    # check_manifest
+    wheel
+    # twine
+    build
+commands =
+    # check-manifest
+    python -m build
+    # python -m twine check dist/*
diff --git a/utilities/tests/test_wm_assess_cluster_location.py b/utilities/tests/test_wm_assess_cluster_location.py
index f4768341..8d0f6314 100755
--- a/utilities/tests/test_wm_assess_cluster_location.py
+++ b/utilities/tests/test_wm_assess_cluster_location.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_assess_cluster_location.py", "--help"])
+    ret = script_runner.run(["utilities/wm_assess_cluster_location.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_compute_FA_from_DWIs.py b/utilities/tests/test_wm_compute_FA_from_DWIs.py
index 80511e6f..02d9a0f3 100755
--- a/utilities/tests/test_wm_compute_FA_from_DWIs.py
+++ b/utilities/tests/test_wm_compute_FA_from_DWIs.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_compute_FA_from_DWIs.py", "--help"])
+    ret = script_runner.run(["utilities/wm_compute_FA_from_DWIs.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_compute_TAP.py b/utilities/tests/test_wm_compute_TAP.py
index 86ff0d1c..2eb5c621 100755
--- a/utilities/tests/test_wm_compute_TAP.py
+++ b/utilities/tests/test_wm_compute_TAP.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_compute_TAP.py", "--help"])
+    ret = script_runner.run(["utilities/wm_compute_TAP.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_compute_bundle_feature_population_math.py b/utilities/tests/test_wm_compute_bundle_feature_population_math.py
index 0d9a94b0..f622d807 100644
--- a/utilities/tests/test_wm_compute_bundle_feature_population_math.py
+++ b/utilities/tests/test_wm_compute_bundle_feature_population_math.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_compute_bundle_feature_population_math.py", "--help"])
+        ["utilities/wm_compute_bundle_feature_population_math.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_extract_cluster.py b/utilities/tests/test_wm_extract_cluster.py
index 6cefc1cb..db90bfa9 100755
--- a/utilities/tests/test_wm_extract_cluster.py
+++ b/utilities/tests/test_wm_extract_cluster.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_extract_cluster.py", "--help"])
+    ret = script_runner.run(["utilities/wm_extract_cluster.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_extract_clusters_by_endpoints.py b/utilities/tests/test_wm_extract_clusters_by_endpoints.py
index 5a091468..830b5dc2 100755
--- a/utilities/tests/test_wm_extract_clusters_by_endpoints.py
+++ b/utilities/tests/test_wm_extract_clusters_by_endpoints.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_extract_clusters_by_endpoints.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_extract_clusters_by_endpoints.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_fix_UKF_trace.py b/utilities/tests/test_wm_fix_UKF_trace.py
index dbd50b8b..0ca9b6c6 100755
--- a/utilities/tests/test_wm_fix_UKF_trace.py
+++ b/utilities/tests/test_wm_fix_UKF_trace.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_fix_UKF_trace.py", "--help"])
+    ret = script_runner.run(["utilities/wm_fix_UKF_trace.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_fix_hemisphere_loc_name_in_tractogram.py b/utilities/tests/test_wm_fix_hemisphere_loc_name_in_tractogram.py
index 388e63b0..7b6c43ec 100644
--- a/utilities/tests/test_wm_fix_hemisphere_loc_name_in_tractogram.py
+++ b/utilities/tests/test_wm_fix_hemisphere_loc_name_in_tractogram.py
@@ -3,5 +3,5 @@
 
 def test_help_option(script_runner):
     ret = script_runner.run(
-        ["wm_fix_hemisphere_loc_name_in_tractogram.py", "--help"])
+        ["utilities/wm_fix_hemisphere_loc_name_in_tractogram.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_flatten_length_distribution.py b/utilities/tests/test_wm_flatten_length_distribution.py
index e93f0344..6c45d509 100755
--- a/utilities/tests/test_wm_flatten_length_distribution.py
+++ b/utilities/tests/test_wm_flatten_length_distribution.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_flatten_length_distribution.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_flatten_length_distribution.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_laterality_all.py b/utilities/tests/test_wm_laterality_all.py
index e3d46b01..ee3358de 100755
--- a/utilities/tests/test_wm_laterality_all.py
+++ b/utilities/tests/test_wm_laterality_all.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_laterality_all.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_laterality_all.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_measure_all_clusters.py b/utilities/tests/test_wm_measure_all_clusters.py
index a36ddd1e..94bbaf19 100755
--- a/utilities/tests/test_wm_measure_all_clusters.py
+++ b/utilities/tests/test_wm_measure_all_clusters.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_measure_all_clusters.py", "--help"])
+    ret = script_runner.run(["utilities/wm_measure_all_clusters.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_measure_endpoint_overlap.py b/utilities/tests/test_wm_measure_endpoint_overlap.py
index 55586582..cfe61641 100755
--- a/utilities/tests/test_wm_measure_endpoint_overlap.py
+++ b/utilities/tests/test_wm_measure_endpoint_overlap.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_measure_endpoint_overlap.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_measure_endpoint_overlap.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_query_atlas_bundle_names.py b/utilities/tests/test_wm_query_atlas_bundle_names.py
index 811fedda..f67cbda9 100644
--- a/utilities/tests/test_wm_query_atlas_bundle_names.py
+++ b/utilities/tests/test_wm_query_atlas_bundle_names.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_query_atlas_bundles_names.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_query_atlas_bundles_names.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_statistics.py b/utilities/tests/test_wm_statistics.py
index 119ba234..e37f8365 100755
--- a/utilities/tests/test_wm_statistics.py
+++ b/utilities/tests/test_wm_statistics.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_statistics.py", "--help"])
+    ret = script_runner.run(["utilities/wm_statistics.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_statistics_export_data.py b/utilities/tests/test_wm_statistics_export_data.py
index 13d6e8ea..e8cdaff6 100755
--- a/utilities/tests/test_wm_statistics_export_data.py
+++ b/utilities/tests/test_wm_statistics_export_data.py
@@ -2,5 +2,6 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_statistics_export_data.py", "--help"])
+    ret = script_runner.run(
+        ["utilities/wm_statistics_export_data.py", "--help"])
     assert ret.success
diff --git a/utilities/tests/test_wm_transform_polydata.py b/utilities/tests/test_wm_transform_polydata.py
index c08e87f7..dd9784d9 100755
--- a/utilities/tests/test_wm_transform_polydata.py
+++ b/utilities/tests/test_wm_transform_polydata.py
@@ -2,5 +2,5 @@
 # -*- coding: utf-8 -*-
 
 def test_help_option(script_runner):
-    ret = script_runner.run(["wm_transform_polydata.py", "--help"])
+    ret = script_runner.run(["utilities/wm_transform_polydata.py", "--help"])
     assert ret.success
diff --git a/whitematteranalysis/tract_measurement.py b/whitematteranalysis/tract_measurement.py
index 23099499..9cb3d722 100755
--- a/whitematteranalysis/tract_measurement.py
+++ b/whitematteranalysis/tract_measurement.py
@@ -64,7 +64,7 @@ def load(self):
         self.case_id = os.path.splitext(os.path.split(self.measurement_file)[1])[0]
         self.cluster_path = tmp_matrix[1:, 0]
         self.measurement_header = tmp_matrix[0, 1:]
-        self.measurement_matrix = tmp_matrix[1:, 1:].astype(np.float)
+        self.measurement_matrix = tmp_matrix[1:, 1:].astype(float)
         self.cluster_number = self.measurement_matrix.shape[0]
 
     def check(self):