Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop EOL Python, simplify PyPy situation, improve testing #74

Merged
merged 12 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
h-vetinari marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
41 changes: 18 additions & 23 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
h-vetinari marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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'
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
]
123 changes: 77 additions & 46 deletions tests/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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)}"
16 changes: 16 additions & 0 deletions tests/test_installation.py
Original file line number Diff line number Diff line change
@@ -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__