diff options
-rw-r--r-- | numpy/compat/__init__.py | 1 | ||||
-rw-r--r-- | numpy/compat/_pep440.py | 487 | ||||
-rw-r--r-- | numpy/core/setup.py | 3 | ||||
-rw-r--r-- | numpy/core/setup_common.py | 11 | ||||
-rw-r--r-- | numpy/core/tests/test_cython.py | 8 | ||||
-rw-r--r-- | numpy/core/tests/test_scalarmath.py | 10 | ||||
-rw-r--r-- | numpy/random/tests/test_extending.py | 8 | ||||
-rwxr-xr-x | setup.py | 29 | ||||
-rwxr-xr-x | tools/cythonize.py | 29 |
9 files changed, 532 insertions, 54 deletions
diff --git a/numpy/compat/__init__.py b/numpy/compat/__init__.py index afee621b8..ff04f725a 100644 --- a/numpy/compat/__init__.py +++ b/numpy/compat/__init__.py @@ -9,6 +9,7 @@ extensions, which may be included for the following reasons: """ from . import _inspect +from . import _pep440 from . import py3k from ._inspect import getargspec, formatargspec from .py3k import * diff --git a/numpy/compat/_pep440.py b/numpy/compat/_pep440.py new file mode 100644 index 000000000..73d0afb5e --- /dev/null +++ b/numpy/compat/_pep440.py @@ -0,0 +1,487 @@ +"""Utility to compare pep440 compatible version strings. + +The LooseVersion and StrictVersion classes that distutils provides don't +work; they don't recognize anything like alpha/beta/rc/dev versions. +""" + +# Copyright (c) Donald Stufft and individual contributors. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import collections +import itertools +import re + + +__all__ = [ + "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN", +] + + +# BEGIN packaging/_structures.py + + +class Infinity: + def __repr__(self): + return "Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True + + def __neg__(self): + return NegativeInfinity + + +Infinity = Infinity() + + +class NegativeInfinity: + def __repr__(self): + return "-Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return True + + def __le__(self, other): + return True + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return False + + def __ge__(self, other): + return False + + def __neg__(self): + return Infinity + + +# BEGIN packaging/version.py + + +NegativeInfinity = NegativeInfinity() + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +def parse(version): + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion: + + def __hash__(self): + return hash(self._key) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + + def __init__(self, version): + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + return self._version + + def __repr__(self): + return "<LegacyVersion({0})>".format(repr(str(self))) + + @property + def public(self): + return self._version + + @property + def base_version(self): + return self._version + + @property + def local(self): + return None + + @property + def is_prerelease(self): + return False + + @property + def is_postrelease(self): + return False + + +_legacy_version_component_re = re.compile( + r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, +) + +_legacy_version_replacement_map = { + "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", +} + + +def _parse_version_parts(s): + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version): + # We hardcode an epoch of -1 here. A PEP 440 version can only have an epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # its adoption of the packaging library. + parts = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + parts = tuple(parts) + + return epoch, parts + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = r""" + v? + (?: + (?:(?P<epoch>[0-9]+)!)? # epoch + (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment + (?P<pre> # pre-release + [-_\.]? + (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) + [-_\.]? + (?P<pre_n>[0-9]+)? + )? + (?P<post> # post release + (?:-(?P<post_n1>[0-9]+)) + | + (?: + [-_\.]? + (?P<post_l>post|rev|r) + [-_\.]? + (?P<post_n2>[0-9]+)? + ) + )? + (?P<dev> # dev release + [-_\.]? + (?P<dev_l>dev) + [-_\.]? + (?P<dev_n>[0-9]+)? + )? + ) + (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version +""" + + +class Version(_BaseVersion): + + _regex = re.compile( + r"^\s*" + VERSION_PATTERN + r"\s*$", + re.VERBOSE | re.IGNORECASE, + ) + + def __init__(self, version): + # Validate the version and parse it into pieces + match = self._regex.search(version) + if not match: + raise InvalidVersion("Invalid version: '{0}'".format(version)) + + # Store the parsed out pieces of the version + self._version = _Version( + epoch=int(match.group("epoch")) if match.group("epoch") else 0, + release=tuple(int(i) for i in match.group("release").split(".")), + pre=_parse_letter_version( + match.group("pre_l"), + match.group("pre_n"), + ), + post=_parse_letter_version( + match.group("post_l"), + match.group("post_n1") or match.group("post_n2"), + ), + dev=_parse_letter_version( + match.group("dev_l"), + match.group("dev_n"), + ), + local=_parse_local_version(match.group("local")), + ) + + # Generate a key which will be used for sorting + self._key = _cmpkey( + self._version.epoch, + self._version.release, + self._version.pre, + self._version.post, + self._version.dev, + self._version.local, + ) + + def __repr__(self): + return "<Version({0})>".format(repr(str(self))) + + def __str__(self): + parts = [] + + # Epoch + if self._version.epoch != 0: + parts.append("{0}!".format(self._version.epoch)) + + # Release segment + parts.append(".".join(str(x) for x in self._version.release)) + + # Pre-release + if self._version.pre is not None: + parts.append("".join(str(x) for x in self._version.pre)) + + # Post-release + if self._version.post is not None: + parts.append(".post{0}".format(self._version.post[1])) + + # Development release + if self._version.dev is not None: + parts.append(".dev{0}".format(self._version.dev[1])) + + # Local version segment + if self._version.local is not None: + parts.append( + "+{0}".format(".".join(str(x) for x in self._version.local)) + ) + + return "".join(parts) + + @property + def public(self): + return str(self).split("+", 1)[0] + + @property + def base_version(self): + parts = [] + + # Epoch + if self._version.epoch != 0: + parts.append("{0}!".format(self._version.epoch)) + + # Release segment + parts.append(".".join(str(x) for x in self._version.release)) + + return "".join(parts) + + @property + def local(self): + version_string = str(self) + if "+" in version_string: + return version_string.split("+", 1)[1] + + @property + def is_prerelease(self): + return bool(self._version.dev or self._version.pre) + + @property + def is_postrelease(self): + return bool(self._version.post) + + +def _parse_letter_version(letter, number): + if letter: + # We assume there is an implicit 0 in a pre-release if there is + # no numeral associated with it. + if number is None: + number = 0 + + # We normalize any letters to their lower-case form + letter = letter.lower() + + # We consider some words to be alternate spellings of other words and + # in those cases we want to normalize the spellings to our preferred + # spelling. + if letter == "alpha": + letter = "a" + elif letter == "beta": + letter = "b" + elif letter in ["c", "pre", "preview"]: + letter = "rc" + elif letter in ["rev", "r"]: + letter = "post" + + return letter, int(number) + if not letter and number: + # We assume that if we are given a number but not given a letter, + # then this is using the implicit post release syntax (e.g., 1.0-1) + letter = "post" + + return letter, int(number) + + +_local_version_seperators = re.compile(r"[\._-]") + + +def _parse_local_version(local): + """ + Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). + """ + if local is not None: + return tuple( + part.lower() if not part.isdigit() else int(part) + for part in _local_version_seperators.split(local) + ) + + +def _cmpkey(epoch, release, pre, post, dev, local): + # When we compare a release version, we want to compare it with all of the + # trailing zeros removed. So we'll use a reverse the list, drop all the now + # leading zeros until we come to something non-zero, then take the rest, + # re-reverse it back into the correct order, and make it a tuple and use + # that for our sorting key. + release = tuple( + reversed(list( + itertools.dropwhile( + lambda x: x == 0, + reversed(release), + ) + )) + ) + + # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. + # We'll do this by abusing the pre-segment, but we _only_ want to do this + # if there is no pre- or a post-segment. If we have one of those, then + # the normal sorting rules will handle this case correctly. + if pre is None and post is None and dev is not None: + pre = -Infinity + # Versions without a pre-release (except as noted above) should sort after + # those with one. + elif pre is None: + pre = Infinity + + # Versions without a post-segment should sort before those with one. + if post is None: + post = -Infinity + + # Versions without a development segment should sort after those with one. + if dev is None: + dev = Infinity + + if local is None: + # Versions without a local segment should sort before those with one. + local = -Infinity + else: + # Versions with a local segment need that segment parsed to implement + # the sorting rules in PEP440. + # - Alphanumeric segments sort before numeric segments + # - Alphanumeric segments sort lexicographically + # - Numeric segments sort numerically + # - Shorter versions sort before longer versions when the prefixes + # match exactly + local = tuple( + (i, "") if isinstance(i, int) else (-Infinity, i) + for i in local + ) + + return epoch, release, pre, post, dev, local diff --git a/numpy/core/setup.py b/numpy/core/setup.py index 63962ab79..a13480907 100644 --- a/numpy/core/setup.py +++ b/numpy/core/setup.py @@ -422,12 +422,13 @@ def configuration(parent_package='',top_path=None): exec_mod_from_location) from numpy.distutils.system_info import (get_info, blas_opt_info, lapack_opt_info) + from numpy.version import release as is_released config = Configuration('core', parent_package, top_path) local_dir = config.local_path codegen_dir = join(local_dir, 'code_generators') - if is_released(config): + if is_released: warnings.simplefilter('error', MismatchCAPIWarning) # Check whether we have a mismatch between the set C API VERSION and the diff --git a/numpy/core/setup_common.py b/numpy/core/setup_common.py index 772c87c96..20d44f4ec 100644 --- a/numpy/core/setup_common.py +++ b/numpy/core/setup_common.py @@ -50,17 +50,6 @@ C_API_VERSION = 0x0000000f class MismatchCAPIWarning(Warning): pass -def is_released(config): - """Return True if a released version of numpy is detected.""" - from distutils.version import LooseVersion - - v = config.get_version('../_version.py') - if v is None: - raise ValueError("Could not get version") - pv = LooseVersion(vstring=v).version - if len(pv) > 3: - return False - return True def get_api_versions(apiversion, codegen_dir): """ diff --git a/numpy/core/tests/test_cython.py b/numpy/core/tests/test_cython.py index 9896de0ec..a9df9c9b6 100644 --- a/numpy/core/tests/test_cython.py +++ b/numpy/core/tests/test_cython.py @@ -13,14 +13,14 @@ try: except ImportError: cython = None else: - from distutils.version import LooseVersion + from numpy.compat import _pep440 - # Cython 0.29.21 is required for Python 3.9 and there are + # Cython 0.29.24 is required for Python 3.10 and there are # other fixes in the 0.29 series that are needed even for earlier # Python versions. # Note: keep in sync with the one in pyproject.toml - required_version = LooseVersion("0.29.21") - if LooseVersion(cython_version) < required_version: + required_version = "0.29.24" + if _pep440.parse(cython_version) < _pep440.Version(required_version): # too old or wrong cython, skip the test cython = None diff --git a/numpy/core/tests/test_scalarmath.py b/numpy/core/tests/test_scalarmath.py index 8a77eca00..e58767d56 100644 --- a/numpy/core/tests/test_scalarmath.py +++ b/numpy/core/tests/test_scalarmath.py @@ -4,7 +4,7 @@ import warnings import itertools import operator import platform -from distutils.version import LooseVersion as _LooseVersion +from numpy.compat import _pep440 import pytest from hypothesis import given, settings, Verbosity from hypothesis.strategies import sampled_from @@ -684,8 +684,8 @@ class TestAbs: if ( sys.platform == "cygwin" and dtype == np.clongdouble and ( - _LooseVersion(platform.release().split("-")[0]) - < _LooseVersion("3.3.0") + _pep440.parse(platform.release().split("-")[0]) + < _pep440.Version("3.3.0") ) ): pytest.xfail( @@ -698,8 +698,8 @@ class TestAbs: if ( sys.platform == "cygwin" and dtype == np.clongdouble and ( - _LooseVersion(platform.release().split("-")[0]) - < _LooseVersion("3.3.0") + _pep440.parse(platform.release().split("-")[0]) + < _pep440.Version("3.3.0") ) ): pytest.xfail( diff --git a/numpy/random/tests/test_extending.py b/numpy/random/tests/test_extending.py index d362092b5..58adb66c7 100644 --- a/numpy/random/tests/test_extending.py +++ b/numpy/random/tests/test_extending.py @@ -31,13 +31,13 @@ try: except ImportError: cython = None else: - from distutils.version import LooseVersion - # Cython 0.29.21 is required for Python 3.9 and there are + from numpy.compat import _pep440 + # Cython 0.29.24 is required for Python 3.10 and there are # other fixes in the 0.29 series that are needed even for earlier # Python versions. # Note: keep in sync with the one in pyproject.toml - required_version = LooseVersion('0.29.21') - if LooseVersion(cython_version) < required_version: + required_version = '0.29.24' + if _pep440.parse(cython_version) < _pep440.Version(required_version): # too old or wrong cython, skip the test cython = None @@ -207,7 +207,7 @@ def get_build_overrides(): """ from numpy.distutils.command.build_clib import build_clib from numpy.distutils.command.build_ext import build_ext - from distutils.version import LooseVersion + from numpy.compat import _pep440 def _needs_gcc_c99_flag(obj): if obj.compiler.compiler_type != 'unix': @@ -221,7 +221,7 @@ def get_build_overrides(): out = subprocess.run([cc, '-dumpversion'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) # -std=c99 is default from this version on - if LooseVersion(out.stdout) >= LooseVersion('5.0'): + if _pep440.parse(out.stdout) >= _pep440.Version('5.0'): return False return True @@ -242,6 +242,31 @@ def get_build_overrides(): def generate_cython(): + # Check Cython version + from numpy.compat import _pep440 + try: + # try the cython in the installed python first (somewhat related to + # scipy/scipy#2397) + import Cython + from Cython.Compiler.Version import version as cython_version + except ImportError as e: + # The `cython` command need not point to the version installed in the + # Python running this script, so raise an error to avoid the chance of + # using the wrong version of Cython. + msg = 'Cython needs to be installed in Python as a module' + raise OSError(msg) from e + else: + # Note: keep in sync with that in pyproject.toml + # Update for Python 3.10 + required_version = '0.29.24' + + if _pep440.parse(cython_version) < _pep440.Version(required_version): + cython_path = Cython.__file__ + msg = 'Building NumPy requires Cython >= {}, found {} at {}' + msg = msg.format(required_version, cython_version, cython_path) + raise RuntimeError(msg) + + # Process files cwd = os.path.abspath(os.path.dirname(__file__)) print("Cythonizing sources") for d in ('random',): diff --git a/tools/cythonize.py b/tools/cythonize.py index c06962cf9..002b2fad7 100755 --- a/tools/cythonize.py +++ b/tools/cythonize.py @@ -48,33 +48,8 @@ def process_pyx(fromfile, tofile): if tofile.endswith('.cxx'): flags.append('--cplus') - try: - # try the cython in the installed python first (somewhat related to scipy/scipy#2397) - import Cython - from Cython.Compiler.Version import version as cython_version - except ImportError as e: - # The `cython` command need not point to the version installed in the - # Python running this script, so raise an error to avoid the chance of - # using the wrong version of Cython. - msg = 'Cython needs to be installed in Python as a module' - raise OSError(msg) from e - else: - # check the version, and invoke through python - from distutils.version import LooseVersion - - # Cython 0.29.21 is required for Python 3.9 and there are - # other fixes in the 0.29 series that are needed even for earlier - # Python versions. - # Note: keep in sync with that in pyproject.toml - # Update for Python 3.10 - required_version = LooseVersion('0.29.24') - - if LooseVersion(cython_version) < required_version: - cython_path = Cython.__file__ - raise RuntimeError(f'Building {VENDOR} requires Cython >= {required_version}' - f', found {cython_version} at {cython_path}') - subprocess.check_call( - [sys.executable, '-m', 'cython'] + flags + ["-o", tofile, fromfile]) + subprocess.check_call( + [sys.executable, '-m', 'cython'] + flags + ["-o", tofile, fromfile]) def process_tempita_pyx(fromfile, tofile): |