Repository: idaholab/MontePy (language: python)

Issue:
End of Life for python 3.8 support
Python 3.8 security [end of life](https://peps.python.org/pep-0569/#lifespan) is scheduled for October 2024. MontePy should move past 3.8 by that point.

This should have minimal impact on code, and should only required metadata updates, and removing old tests. 

This does bring up a question of should we jump from almost dead version to almost dead version or keep in the middle of the pack for python support.
Decide how to test against Numpy 1 and 2
Numpy 2.0 came out this June and is the first major release in a long time, which means some backwards compatibility is lost. Numpy 2.0 requires python 3.9. Due to this I saw that for python 3.8 we are testing against Numpy 1.24, and for Python >=3.9 we are testing against numpy 2.0. So right now we are accidentally testing against both major versions. However, this isn't a good spot to be.

So actions IMO:
1. Research what breaks in 2.0
2. Determine if this matters to us.
3. Decide if we need to peg to Numpy 1.xxx or Numpy 2.xxx
4. Decide if we need to do matrix testing against both major versions of numpy.

Thoughts @tjlaboss?


Discussion / hints:
changed due date to November 01, 2024
FYI:

| Version | EOL   |
| ------ | ------ |
|   3.9  | Oct-25 |
| 3.10   |  Oct-26 |
| 3.11   | Oct-27  |
# 1. What breaks in 2.0?

[NumPy 2.0 Migration Guide](https://numpy.org/devdocs/numpy_2_0_migration_guide.html)

[NumPy 2.0 Python API Removals](https://numpy.org/devdocs/release/2.0.0-notes.html#numpy-2-0-python-api-removals)

# 2. Does this matter to us?

No, MontePy's usage of NumPy is basic and should continue to work the same.

### _data_inputs/fill.py_
```python
np.zeros(..., dtype=np.dtype(int))  # not broken
np.empty_like(..., dtype="O")       # not broken
```

### _data_inputs/transform.py_
```python
np.any(...)   # not broken
np.array([])  # not broken
np.array(values)  # not broken
```

There is also `np.ndarray` type hinting and an `np.ndarray` instantiation in the test suite.

# 3. Do we need to peg to Numpy 1.xxx or Numpy 2.xxx?

No, we are able to maintain compatibility with both 1 and 2 at this time.

# 4. Do need to do matrix testing against both major versions of numpy?

Yes, it would be a good idea to have at least one test for each, which will require an update to the CI once Python 3.8 is EOL'd.
Yeah... I'm not entirely sure the best way to test this in a `pyproject` compliant way?
Maybe install depenendencies. Uninstall numpy. Install requested version of numpy?
Some ideas:

 - lazy hacky way: pick a specific `matrix.python-version` and manually set it to NumPy 1.x later on
 - lazy list way: `python-numpy-versions: [["3.9", "1.x.y"], ["3.10", "2.a.b"]]` and install requested version
 - test all version for both NumPy 1 and 2, and install requested version
I was thinking of using GHA to build a 2d matrix. 

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 821d4633..c636a23f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
 
     steps:
       - uses: actions/checkout@v4
@@ -36,15 +36,13 @@ jobs:
       - run: pip install --user dist/*.tar.gz
       - run: pip install --user . montepy[test]
       - run: pip install --user . montepy[doc]
-        if: ${{ matrix.python-version != '3.8'}}
       - run: pip install --user . montepy[format]
       - run: pip install --user . montepy[build]
       - run: pip install --user . montepy[develop]
-        if: ${{ matrix.python-version != '3.8'}}
       - run: pip freeze
       - name: Upload build artifacts
         uses: actions/upload-artifact@v4.3.1
-        if: ${{ matrix.python-version == '3.9' }}
+        if: ${{ matrix.python-version == '3.12'  && matrix.numpy-version == '2.0' }}
         with: 
            name: build
            path: dist/*
@@ -54,7 +52,8 @@ jobs:
     permissions: write-all
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+        numpy-version: ["1.0", "2.0"]
     
     steps:
       - uses: actions/checkout@v4
@@ -62,27 +61,33 @@ jobs:
         uses: actions/setup-python@v5
         with: 
           python-version: ${{ matrix.python-version }}
+      - run: pip install numpy~=${{ matrix.numpy-version }}
+        name: install specific numpy version
       - run: pip install --user . montepy[test]
       - run: pip install --user . montepy[build]
+      - run: pip uninstall -y pytest-profiling
+        name: Uninstall incompatible library
+        if: ${{ matrix.python-version == '3.13' }}
+      - run: pip freeze
       - run: coverage run -m pytest --junitxml=test_report.xml
       - run: coverage report
         if:  ${{ success() || failure() }}
       - run: coverage xml
         if:  ${{ success() || failure() }}
       - name: Upload test report
-        if: ${{ matrix.python-version == '3.9' && (success() || failure() )}}
+        if: ${{ matrix.python-version == '3.12' && matrix.numpy-version == '2.0' && (success() || failure() )}}
         uses: actions/upload-artifact@v4
         with:
           name: test
           path: test_report.xml
       - name: Upload coverage report
-        if: ${{ matrix.python-version == '3.9' && (success() || failure() )}}
+        if: ${{ matrix.python-version == '3.12' && matrix.numpy-version == '2.0' && (success() || failure() )}}
         uses: actions/upload-artifact@v4
         with:
           name: coverage
           path: coverage.xml
       - name: Coveralls GitHub Action
-        if: ${{ matrix.python-version == '3.9' && (success() || failure() )}}
+        if: ${{ matrix.python-version == '3.12' && matrix.numpy-version == '2.0' && (success() || failure() )}}
         uses: coverallsapp/github-action@v2
         with:
           file: coverage.xml
diff --git a/pyproject.toml b/pyproject.toml
index d863fc63..07b2ea5c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ description = "A library for reading, editing, and writing MCNP input files"
 
 # gitlab limits the readme to 4,000 chars.
 readme = "README.md"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 maintainers = [
 	{name = "Micah Gale", email = "mgale@montepy.org"}
 ]
@@ -18,7 +18,6 @@ keywords = ["MCNP", "neutronics", "imcnp", "input file", "monte carlo", "radiati
 license = {file="LICENSE"}
 classifiers = [
        "Development Status :: 4 - Beta",
-       "Programming Language :: Python :: 3 :: Only",
        "Intended Audience :: Science/Research",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
@@ -27,7 +26,12 @@ classifiers = [
        "Topic :: Scientific/Engineering :: Physics",
        "Topic :: Scientific/Engineering",
        "Topic :: Scientific/Engineering :: Human Machine Interfaces",
-
+       "Programming Language :: Python :: 3 :: Only",
+       "Programming Language :: Python :: 3.9",
+       "Programming Language :: Python :: 3.10",
+       "Programming Language :: Python :: 3.11",
+       "Programming Language :: Python :: 3.12",
+       "Programming Language :: Python :: 3.13"
 ]
 dependencies = [
 	"numpy>=1.18",


Test patch:
diff --git a/tests/test_scripts.py b/tests/test_scripts.py
index aa68de76..ea690c1f 100644
--- a/tests/test_scripts.py
+++ b/tests/test_scripts.py
@@ -36,9 +36,10 @@ def run_script(args):
 
     def test_delete_bad(self):
         for in_file in self.files:
-            with open(os.path.join("tests", "inputs", in_file), "rb") as in_fh, open(
-                self.files[in_file]["-d"], "rb"
-            ) as out_fh:
+            with (
+                open(os.path.join("tests", "inputs", in_file), "rb") as in_fh,
+                open(self.files[in_file]["-d"], "rb") as out_fh,
+            ):
                 for in_line, out_line in zip(in_fh, out_fh):
                     try:
                         in_line.decode("ascii")
@@ -52,9 +53,10 @@ def test_delete_bad(self):
 
     def test_whitespace_bad(self):
         for in_file in self.files:
-            with open(os.path.join("tests", "inputs", in_file), "rb") as in_fh, open(
-                self.files[in_file]["-w"], "rb"
-            ) as out_fh:
+            with (
+                open(os.path.join("tests", "inputs", in_file), "rb") as in_fh,
+                open(self.files[in_file]["-w"], "rb") as out_fh,
+            ):
                 for in_line, out_line in zip(in_fh, out_fh):
                     try:
                         in_line.decode("ascii")