Repository: scipy/oldest-supported-numpy (language: python)

Issue:
Drop support for EOL pythons?
I think we should drop the markers for 3.5 and probably 3.6, and just raise `python_requires` appropriately. Those on old python versions will still get functioning `oldest-supported-numpy` packages, and almost by definition, those old language versions will not receive any new architecture support. However, they still have a maintenance cost, as seen in cases like #71, so I think we should drop them.

Python 3.7 is also [EOL](https://peps.python.org/pep-0537/) this month, but it's a borderline case.


Discussion / hints:
Python 3.5 has also already been dropped from `test_dependencies.py`...

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index b5e001c..153f0e1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -42,4 +42,4 @@ jobs:
         run: pip install -r tests/requirements.txt
 
       - name: Test valid numpy installed
-        run: pytest -k test_valid_numpy_is_installed
+        run: pytest tests/test_installation.py -k test_valid_numpy_is_installed
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c18dd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,1 @@
+__pycache__/
diff --git a/setup.cfg b/setup.cfg
index bdbf081..3bb7d16 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,37 +12,29 @@ version = 2022.11.19
 # SciPy package, which is released under a 3-clause BSD license:
 # https://github.com/scipy/scipy/blob/master/LICENSE.txt
 
-# Important: please don't update `python_requires` or remove specs for Python
-#            versions that are no longer supported. This may break installing
-#            older versions of packages that depend on this meta-package!
 [options]
-python_requires = >=3.5
+python_requires = >=3.7
 install_requires =
     # NOTE: Any platform-related assumptions made here are encoded in the
     #       tests for this package.
 
     # AIX requires fixes contained in numpy 1.16
-    numpy==1.16.0; python_version=='3.5' and platform_system=='AIX'
-    numpy==1.16.0; python_version=='3.6' and platform_system=='AIX'
-    numpy==1.16.0; python_version=='3.7' and platform_system=='AIX'
+    numpy==1.16.0; python_version=='3.7' and platform_system=='AIX' and platform_machine!='loongarch64' and platform_python_implementation != 'PyPy'
 
     # IBM i requires fixes contained in numpy 1.23.3. Only Python 3.6 and 3.9
     # are supported on IBM i system, so we're ignoring other Python versions here
     # for simplicity (see gh-66).
-    numpy==1.23.3; python_version=='3.9' and platform_system=='OS400'
+    numpy==1.23.3; python_version=='3.9' and platform_system=='OS400' and platform_machine!='loongarch64'
 
     # numpy 1.19 was the first minor release to provide aarch64 wheels, but
-    # wheels require fixes contained in numpy 1.19.2 (For Python 3.5: support
-    # was dropped in 1.19 so numpy 1.18.5 can be built from source instead).
-    numpy==1.18.5; python_version=='3.5' and platform_machine=='aarch64' and platform_python_implementation != 'PyPy'
-    numpy==1.19.2; python_version=='3.6' and platform_machine=='aarch64' and platform_python_implementation != 'PyPy'
-    numpy==1.19.2; python_version=='3.7' and platform_machine=='aarch64' and platform_python_implementation != 'PyPy'
+    # wheels require fixes contained in numpy 1.19.2
+    numpy==1.19.2; python_version=='3.7' and platform_machine=='aarch64' and platform_system!='AIX' and platform_python_implementation != 'PyPy'
     numpy==1.19.2; python_version=='3.8' and platform_machine=='aarch64' and platform_python_implementation != 'PyPy'
 
     # arm64 on Darwin supports Python 3.8 and above requires numpy>=1.21.0
     # (first version with arm64 wheels available)
-    numpy==1.21.0; python_version=='3.7' and platform_machine=='arm64' and platform_system=='Darwin'
-    numpy==1.21.0; python_version=='3.8' and platform_machine=='arm64' and platform_system=='Darwin'
+    numpy==1.21.0; python_version=='3.7' and platform_machine=='arm64' and platform_system=='Darwin' and platform_python_implementation!='PyPy'
+    numpy==1.21.0; python_version=='3.8' and platform_machine=='arm64' and platform_system=='Darwin' and platform_python_implementation!='PyPy'
     numpy==1.21.0; python_version=='3.9' and platform_machine=='arm64' and platform_system=='Darwin'
 
     # Python 3.8 on s390x requires at least 1.17.5, see gh-29
@@ -51,28 +43,31 @@ install_requires =
     # loongarch64 requires numpy>=1.22.0
     # Note that 1.22.0 broke support for using the Python limited C API
     # (https://github.com/numpy/numpy/pull/20818), so we use 1.22.2 instead
-    numpy==1.22.2; platform_machine=='loongarch64' and python_version<'3.11'
+    numpy==1.22.2; platform_machine=='loongarch64' and python_version>='3.8' and python_version<'3.11'
+
+    # win-arm64; in the absence of information to the contrary, we want to use the default pins below;
+    # however, these are excluded due to `platform_machine not in 'arm64|...'`, so it's easier to
+    # formulate a separate requirement just for win-arm64
+    numpy==1.14.5; python_version=='3.7' and platform_machine=='arm64' and platform_system=='Windows' and platform_python_implementation != 'PyPy'
+    numpy==1.17.3; python_version=='3.8' and platform_machine=='arm64' and platform_system=='Windows' and platform_python_implementation != 'PyPy'
+    numpy==1.19.3; python_version=='3.9' and platform_machine=='arm64' and platform_system=='Windows'
 
     # default numpy requirements
-    numpy==1.13.3; python_version=='3.5' and platform_machine not in 'aarch64|loongarch64' and platform_system!='AIX'
-    numpy==1.13.3; python_version=='3.6' and platform_machine not in 'aarch64|loongarch64' and platform_system!='AIX' and platform_python_implementation != 'PyPy'
     numpy==1.14.5; python_version=='3.7' and platform_machine not in 'arm64|aarch64|loongarch64' and platform_system!='AIX' and platform_python_implementation != 'PyPy'
     numpy==1.17.3; python_version=='3.8' and platform_machine not in 'arm64|aarch64|s390x|loongarch64' and platform_python_implementation != 'PyPy'
-    numpy==1.19.3; python_version=='3.9' and platform_system not in 'OS400' and platform_machine not in 'arm64|loongarch64' and platform_python_implementation != 'PyPy'
+    numpy==1.19.3; python_version=='3.9' and platform_system not in 'OS400' and platform_machine not in 'arm64|loongarch64'
     # Note that 1.21.3 was the first version with a complete set of 3.10 wheels,
     # however macOS was broken and it's safe to build against 1.21.6 on all platforms (see gh-28 and gh-45)
-    numpy==1.21.6; python_version=='3.10' and platform_machine!='loongarch64' and platform_python_implementation != 'PyPy'
-    numpy==1.23.2; python_version=='3.11' and platform_python_implementation != 'PyPy'
+    numpy==1.21.6; python_version=='3.10' and platform_machine!='loongarch64'
+    numpy==1.23.2; python_version=='3.11'
 
     # PyPy requirements
     numpy==1.19.0; python_version=='3.6' and platform_machine!='loongarch64' and platform_python_implementation=='PyPy'
     numpy==1.20.0; python_version=='3.7' and platform_machine!='loongarch64' and platform_python_implementation=='PyPy'
     numpy==1.22.2; python_version=='3.8' and platform_machine!='loongarch64' and platform_python_implementation=='PyPy'
-    numpy==1.25.0; python_version=='3.9' and platform_machine!='loongarch64' and platform_python_implementation=='PyPy'
 
     # For Python versions which aren't yet officially supported,
     # we specify an unpinned Numpy which allows source distributions
     # to be used and allows wheels to be used as soon as they
     # become available.
     numpy; python_version>='3.12'
-    numpy; python_version>='3.10' and platform_python_implementation=='PyPy'


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..2b90bb6
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,21 @@
+import configparser
+from pathlib import Path
+from typing import List
+
+import pytest
+from packaging.requirements import Requirement
+
+SETUP_CFG_FILE = Path(__file__).parent.parent / "setup.cfg"
+
+
+@pytest.fixture(scope="session")
+def cfg_requirements() -> List[Requirement]:
+    """A fixture for getting the requirements of from setup.cfg."""
+    parser = configparser.ConfigParser()
+    parser.read(SETUP_CFG_FILE)
+
+    return [
+        Requirement(line)
+        for line in parser.get("options", "install_requires").splitlines()
+        if line
+    ]
diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py
index e5e2ddc..d8797e8 100644
--- a/tests/test_dependencies.py
+++ b/tests/test_dependencies.py
@@ -1,56 +1,61 @@
 """Tests to ensure that the dependency declarations are sane.
 """
 
-import configparser
 import pprint
-from functools import lru_cache
-from pathlib import Path
-from typing import List
 
 import pytest
 from packaging.requirements import Requirement
 
-SETUP_CFG_FILE = Path(__file__).parent.parent / "setup.cfg"
-
 
 def is_pinned(requirement: Requirement) -> bool:
     return "==" in str(requirement.specifier)
 
 
-@lru_cache(maxsize=None)
-def get_package_dependencies() -> List[Requirement]:
-    """A cached reader for getting dependencies of this package."""
-    parser = configparser.ConfigParser()
-    parser.read(SETUP_CFG_FILE)
-
-    return [
-        Requirement(line)
-        for line in parser.get("options", "install_requires").splitlines()
-        if line
-    ]
-
-
 # The ordering of these markers is important, and is used in test names.
 # The tests, when run, look like: PyPy-3.6-Linux-aarch64` (bottom-first)
-@pytest.mark.parametrize("platform_machine", ["x86", "x86_64", "aarch64", "s390x", "arm64", "loongarch64"])
+@pytest.mark.parametrize("platform_machine", ["x86", "x86_64", "aarch64", "ppc64le", "s390x", "arm64", "loongarch64"])
 @pytest.mark.parametrize("platform_system", ["Linux", "Windows", "Darwin", "AIX", "OS400"])
-@pytest.mark.parametrize("python_version", ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
+@pytest.mark.parametrize("python_version", ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
 @pytest.mark.parametrize("platform_python_implementation", ["CPython", "PyPy"])
 def test_has_at_most_one_pinned_dependency(
     platform_machine,
     platform_system,
     python_version,
     platform_python_implementation,
+    cfg_requirements,
 ):
     # These are known to be platforms that are not valid / possible at this time.
-    if platform_system in ("AIX", "OS400"):
-        if platform_machine in ["aarch64", "loongarch64"]:
-            pytest.skip(f"{platform_system} and {platform_machine} are mutually exclusive.")
-        if platform_python_implementation == "PyPy":
-            pytest.skip(f"{platform_system} and PyPy are mutually exclusive.")
+    # due the the sheer variety, the default assumption is that a given combination
+    # is invalid, allowing us to specify valid cases more easily below
+    valid = False
+    match (platform_system, platform_machine):
+        case ["Linux", "arm64"]:
+            valid = False  # express "everything but arm64"; called aarch64 on linux
+        case ["Linux", _]:
+            valid = True   # otherwise, Linux is everywhere
+        case ["Darwin", ("x86_64" | "arm64")]:
+            valid = True
+        case ["Windows", ("x86" | "x86_64" | "arm64")]:
+            valid = True
+        # TODO: verify architectures for AIX/OS400
+        case [("AIX" | "OS400"), ("x86" | "x86_64" | "s390x")]:
+            valid = True
+
+    # currently linux-{64, aarch64, ppc64le}, osx-64, win-64; no support for arm64 yet
+    pypy_pairs = [
+        ("Linux", "x86_64"),
+        ("Linux", "aarch64"),
+        ("Linux", "ppc64le"),
+        ("Darwin", "x86_64"),
+        ("Windows", "x86_64"),
+    ]
+    if (platform_python_implementation == "PyPy"
+            and (platform_system, platform_machine) not in pypy_pairs):
+        valid = False
 
-    if platform_python_implementation == "PyPy" and (platform_machine not in ["x86_64", "aarch64"]):
-        pytest.skip(f"PyPy is not supported on {platform_machine}.")
+    if platform_system == "OS400" and python_version != "3.9":
+        # IBMi only supported on CPython 3.9, see gh-66
+        valid = False
 
     environment = {
         "python_version": python_version,
@@ -60,7 +65,7 @@ def test_has_at_most_one_pinned_dependency(
     }
 
     filtered_requirements = []
-    for req in get_package_dependencies():
+    for req in cfg_requirements:
         assert req.marker
         if not req.marker.evaluate(environment):
             continue
@@ -69,20 +74,46 @@ def test_has_at_most_one_pinned_dependency(
 
         filtered_requirements.append(req)
 
-    assert (
-        len(filtered_requirements) <= 1
-    ), f"Expected no more than one pin.\n{pprint.pformat(environment)}"
-
-
-def test_valid_numpy_is_installed():
-    filtered_requirements = []
-    for req in get_package_dependencies():
-        if req.marker.evaluate():
-            filtered_requirements.append(req)
-
-    assert (len(filtered_requirements) == 1), "Expected exactly one pin."
-
-    item, = filtered_requirements[0].specifier
-
-    import numpy
-    assert item.version == numpy.__version__
+    # since we cannot run the installation tests on all platforms,
+    # we formulate the conditions when we expect a pin
+    expect_pin = False
+    match (platform_system, platform_machine):
+        case [("Linux" | "Darwin" | "Windows"), "x86_64"]:
+            expect_pin = True  # baseline
+        case [("Linux" | "Windows"), "x86"]:
+            # 32 bit wheels on Linux only until numpy 1.21, but should still be compatible
+            expect_pin = True
+        case ["Linux", "aarch64"]:
+            expect_pin = True  # as of 1.19.2
+        case ["Darwin", "arm64"]:
+            expect_pin = True  # as of 1.21
+        # no official wheels, but register minimum compatibility
+        case ["Linux", "s390x"]:
+            expect_pin = True  # as of 1.17.5
+        case ["Linux", "loongarch64"]:
+            expect_pin = (python_version != "3.7")  # as of 1.22
+        case ["AIX", ("x86" | "x86_64" | "s390x")]:
+            expect_pin = True  # as of 1.16
+        case ["OS400", ("x86" | "x86_64" | "s390x")]:
+            # only supported on CPython 3.9, see above
+            expect_pin = True  # as of 1.23.3
+        # if there is no information to the contrary, we expect the default pins
+        case ["Linux", "ppc64le"]:
+            expect_pin = True
+        case ["Windows", "arm64"]:
+            expect_pin = True
+
+    # for valid combinations, we test more strictly: expect exactly zero or one pins
+    if valid:
+        # we only expect a pin for released python versions
+        expect_pin = False if (python_version == "3.12") else expect_pin
+        log_msg = "Expected " + ("exactly one pin" if expect_pin else "no pins")
+        assert (
+            len(filtered_requirements) == int(expect_pin)
+        ), f"{log_msg}.\n{pprint.pformat(environment)}"
+    else:
+        # on invalid platform / interpreter combinations, test
+        # that at least we do not produce more than one pin
+        assert (
+            len(filtered_requirements) <= 1
+        ),  f"Expected no more than one pin.\n{pprint.pformat(environment)}"
diff --git a/tests/test_installation.py b/tests/test_installation.py
new file mode 100644
index 0000000..633538f
--- /dev/null
+++ b/tests/test_installation.py
@@ -0,0 +1,16 @@
+"""Test that version that gets installed (where we can run CI)
+matches the expectation defined in setup.cfg"""
+
+
+def test_valid_numpy_is_installed(cfg_requirements):
+    filtered_requirements = []
+    for req in cfg_requirements:
+        if req.marker.evaluate():
+            filtered_requirements.append(req)
+
+    assert (len(filtered_requirements) == 1), "Expected exactly one pin."
+
+    item, = filtered_requirements[0].specifier
+
+    import numpy
+    assert item.version == numpy.__version__