From 71c901bcb9881cb22b1fe33406786eb98cf3dd8b Mon Sep 17 00:00:00 2001 From: Doug Burke Date: Mon, 10 Feb 2020 15:12:37 -0500 Subject: [PATCH] Update to v3.2.2 of astropy_helpers It's not clear to me what the best version to use is (i.e. how backwards- and forwards- compatible are the astropy-helper versions)? The latest suggestion is to use tox for testing, but I am not planning to go that far in this set of changes. --- ah_bootstrap.py | 334 ++++++++++++++++++++++++++---------------------- astropy_helpers | 2 +- 2 files changed, 179 insertions(+), 157 deletions(-) diff --git a/ah_bootstrap.py b/ah_bootstrap.py index 0dc5007..67ca92b 100644 --- a/ah_bootstrap.py +++ b/ah_bootstrap.py @@ -19,9 +19,14 @@ contains an option called ``auto_use`` with a value of ``True``, it will automatically call the main function of this module called `use_astropy_helpers` (see that function's docstring for full details). -Otherwise no further action is taken (however, -``ah_bootstrap.use_astropy_helpers`` may be called manually from within the -setup.py script). +Otherwise no further action is taken and by default the system-installed version +of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers`` +may be called manually from within the setup.py script). + +This behavior can also be controlled using the ``--auto-use`` and +``--no-auto-use`` command-line flags. For clarity, an alias for +``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using +the latter if needed. Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same names as the arguments to `use_astropy_helpers`, and can be used to configure @@ -33,7 +38,6 @@ import contextlib import errno -import imp import io import locale import os @@ -41,54 +45,133 @@ import subprocess as sp import sys -try: - from ConfigParser import ConfigParser, RawConfigParser -except ImportError: - from configparser import ConfigParser, RawConfigParser +from distutils import log +from distutils.debug import DEBUG + +from configparser import ConfigParser, RawConfigParser + +import pkg_resources + +from setuptools import Distribution +from setuptools.package_index import PackageIndex + +# This is the minimum Python version required for astropy-helpers +__minimum_python_version__ = (3, 5) + +# TODO: Maybe enable checking for a specific version of astropy_helpers? +DIST_NAME = 'astropy-helpers' +PACKAGE_NAME = 'astropy_helpers' +UPPER_VERSION_EXCLUSIVE = None +# Defaults for other options +DOWNLOAD_IF_NEEDED = True +INDEX_URL = 'https://pypi.python.org/simple' +USE_GIT = True +OFFLINE = False +AUTO_UPGRADE = True + +# A list of all the configuration options and their required types +CFG_OPTIONS = [ + ('auto_use', bool), ('path', str), ('download_if_needed', bool), + ('index_url', str), ('use_git', bool), ('offline', bool), + ('auto_upgrade', bool) +] + +# Start off by parsing the setup.cfg file -if sys.version_info[0] < 3: - _str_types = (str, unicode) - _text_type = unicode - PY3 = False +_err_help_msg = """ +If the problem persists consider installing astropy_helpers manually using pip +(`pip install astropy_helpers`) or by manually downloading the source archive, +extracting it, and installing by running `python setup.py install` from the +root of the extracted source code. +""" + +SETUP_CFG = ConfigParser() + +if os.path.exists('setup.cfg'): + + try: + SETUP_CFG.read('setup.cfg') + except Exception as e: + if DEBUG: + raise + + log.error( + "Error reading setup.cfg: {0!r}\n{1} will not be " + "automatically bootstrapped and package installation may fail." + "\n{2}".format(e, PACKAGE_NAME, _err_help_msg)) + +# We used package_name in the package template for a while instead of name +if SETUP_CFG.has_option('metadata', 'name'): + parent_package = SETUP_CFG.get('metadata', 'name') +elif SETUP_CFG.has_option('metadata', 'package_name'): + parent_package = SETUP_CFG.get('metadata', 'package_name') else: - _str_types = (str, bytes) - _text_type = str - PY3 = True + parent_package = None + +if SETUP_CFG.has_option('options', 'python_requires'): + + python_requires = SETUP_CFG.get('options', 'python_requires') + + # The python_requires key has a syntax that can be parsed by SpecifierSet + # in the packaging package. However, we don't want to have to depend on that + # package, so instead we can use setuptools (which bundles packaging). We + # have to add 'python' to parse it with Requirement. + + from pkg_resources import Requirement + req = Requirement.parse('python' + python_requires) + + # We want the Python version as a string, which we can get from the platform module + import platform + # strip off trailing '+' incase this is a dev install of python + python_version = platform.python_version().strip('+') + # allow pre-releases to count as 'new enough' + if not req.specifier.contains(python_version, True): + if parent_package is None: + message = "ERROR: Python {} is required by this package\n".format(req.specifier) + else: + message = "ERROR: Python {} is required by {}\n".format(req.specifier, parent_package) + sys.stderr.write(message) + sys.exit(1) + +if sys.version_info < __minimum_python_version__: + + if parent_package is None: + message = "ERROR: Python {} or later is required by astropy-helpers\n".format( + __minimum_python_version__) + else: + message = "ERROR: Python {} or later is required by astropy-helpers for {}\n".format( + __minimum_python_version__, parent_package) + + sys.stderr.write(message) + sys.exit(1) + +_str_types = (str, bytes) # What follows are several import statements meant to deal with install-time # issues with either missing or misbehaving pacakges (including making sure # setuptools itself is installed): +# Check that setuptools 30.3 or later is present +from distutils.version import LooseVersion -# Some pre-setuptools checks to ensure that either distribute or setuptools >= -# 0.7 is used (over pre-distribute setuptools) if it is available on the path; -# otherwise the latest setuptools will be downloaded and bootstrapped with -# ``ez_setup.py``. This used to be included in a separate file called -# setuptools_bootstrap.py; but it was combined into ah_bootstrap.py try: - import pkg_resources - _setuptools_req = pkg_resources.Requirement.parse('setuptools>=0.7') - # This may raise a DistributionNotFound in which case no version of - # setuptools or distribute is properly installed - _setuptools = pkg_resources.get_distribution('setuptools') - if _setuptools not in _setuptools_req: - # Older version of setuptools; check if we have distribute; again if - # this results in DistributionNotFound we want to give up - _distribute = pkg_resources.get_distribution('distribute') - if _setuptools != _distribute: - # It's possible on some pathological systems to have an old version - # of setuptools and distribute on sys.path simultaneously; make - # sure distribute is the one that's used - sys.path.insert(1, _distribute.location) - _distribute.activate() - imp.reload(pkg_resources) -except: - # There are several types of exceptions that can occur here; if all else - # fails bootstrap and use the bootstrapped version - from ez_setup import use_setuptools - use_setuptools() + import setuptools + assert LooseVersion(setuptools.__version__) >= LooseVersion('30.3') +except (ImportError, AssertionError): + sys.stderr.write("ERROR: setuptools 30.3 or later is required by astropy-helpers\n") + sys.exit(1) + +# typing as a dependency for 1.6.1+ Sphinx causes issues when imported after +# initializing submodule with ah_boostrap.py +# See discussion and references in +# https://github.com/astropy/astropy-helpers/issues/302 + +try: + import typing # noqa +except ImportError: + pass # Note: The following import is required as a workaround to @@ -97,7 +180,7 @@ # later cause the TemporaryDirectory class defined in it to stop working when # used later on by setuptools try: - import setuptools.py31compat + import setuptools.py31compat # noqa except ImportError: pass @@ -121,36 +204,6 @@ # End compatibility imports... -# In case it didn't successfully import before the ez_setup checks -import pkg_resources - -from setuptools import Distribution -from setuptools.package_index import PackageIndex -from setuptools.sandbox import run_setup - -from distutils import log -from distutils.debug import DEBUG - - -# TODO: Maybe enable checking for a specific version of astropy_helpers? -DIST_NAME = 'astropy-helpers' -PACKAGE_NAME = 'astropy_helpers' - -# Defaults for other options -DOWNLOAD_IF_NEEDED = True -INDEX_URL = 'https://pypi.python.org/simple' -USE_GIT = True -OFFLINE = False -AUTO_UPGRADE = True - -# A list of all the configuration options and their required types -CFG_OPTIONS = [ - ('auto_use', bool), ('path', str), ('download_if_needed', bool), - ('index_url', str), ('use_git', bool), ('offline', bool), - ('auto_upgrade', bool) -] - - class _Bootstrapper(object): """ Bootstrapper implementation. See ``use_astropy_helpers`` for parameter @@ -166,7 +219,7 @@ def __init__(self, path=None, index_url=None, use_git=None, offline=None, if not (isinstance(path, _str_types) or path is False): raise TypeError('path must be a string or False') - if PY3 and not isinstance(path, _text_type): + if not isinstance(path, str): fs_encoding = sys.getfilesystemencoding() path = path.decode(fs_encoding) # path to unicode @@ -220,36 +273,20 @@ def main(cls, argv=None): @classmethod def parse_config(cls): - if not os.path.exists('setup.cfg'): - return {} - - cfg = ConfigParser() - try: - cfg.read('setup.cfg') - except Exception as e: - if DEBUG: - raise - - log.error( - "Error reading setup.cfg: {0!r}\n{1} will not be " - "automatically bootstrapped and package installation may fail." - "\n{2}".format(e, PACKAGE_NAME, _err_help_msg)) - return {} - - if not cfg.has_section('ah_bootstrap'): + if not SETUP_CFG.has_section('ah_bootstrap'): return {} config = {} for option, type_ in CFG_OPTIONS: - if not cfg.has_option('ah_bootstrap', option): + if not SETUP_CFG.has_option('ah_bootstrap', option): continue if type_ is bool: - value = cfg.getboolean('ah_bootstrap', option) + value = SETUP_CFG.getboolean('ah_bootstrap', option) else: - value = cfg.get('ah_bootstrap', option) + value = SETUP_CFG.get('ah_bootstrap', option) config[option] = value @@ -276,6 +313,18 @@ def parse_command_line(cls, argv=None): config['offline'] = True argv.remove('--offline') + if '--auto-use' in argv: + config['auto_use'] = True + argv.remove('--auto-use') + + if '--no-auto-use' in argv: + config['auto_use'] = False + argv.remove('--no-auto-use') + + if '--use-system-astropy-helpers' in argv: + config['auto_use'] = False + argv.remove('--use-system-astropy-helpers') + return config def run(self): @@ -453,9 +502,10 @@ def _directory_import(self): # setup.py exists we can generate it setup_py = os.path.join(path, 'setup.py') if os.path.isfile(setup_py): - with _silence(): - run_setup(os.path.join(path, 'setup.py'), - ['egg_info']) + # We use subprocess instead of run_setup from setuptools to + # avoid segmentation faults - see the following for more details: + # https://github.com/cython/cython/issues/2104 + sp.check_output([sys.executable, 'setup.py', 'egg_info'], cwd=path) for dist in pkg_resources.find_distributions(path, True): # There should be only one... @@ -490,16 +540,32 @@ def get_option_dict(self, command_name): if version: req = '{0}=={1}'.format(DIST_NAME, version) else: - req = DIST_NAME + if UPPER_VERSION_EXCLUSIVE is None: + req = DIST_NAME + else: + req = '{0}<{1}'.format(DIST_NAME, UPPER_VERSION_EXCLUSIVE) attrs = {'setup_requires': [req]} + # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure + # it honours the options set in the [easy_install] section, and we need + # to explicitly fetch the requirement eggs as setup_requires does not + # get honored in recent versions of setuptools: + # https://github.com/pypa/setuptools/issues/1273 + try: - if DEBUG: - _Distribution(attrs=attrs) - else: - with _silence(): - _Distribution(attrs=attrs) + + context = _verbose if DEBUG else _silence + with context(): + dist = _Distribution(attrs=attrs) + try: + dist.parse_config_files(ignore_option_errors=True) + dist.fetch_build_eggs(req) + except TypeError: + # On older versions of setuptools, ignore_option_errors + # doesn't exist, and the above two lines are not needed + # so we can just continue + pass # If the setup_requires succeeded it will have added the new dist to # the main working_set @@ -609,8 +675,8 @@ def _check_submodule_using_git(self): # only if the submodule is initialized. We ignore this information for # now _git_submodule_status_re = re.compile( - '^(?P[+-U ])(?P[0-9a-f]{40}) ' - '(?P\S+)( .*)?$') + r'^(?P[+-U ])(?P[0-9a-f]{40}) ' + r'(?P\S+)( .*)?$') # The stdout should only contain one line--the status of the # requested submodule @@ -702,7 +768,7 @@ def _update_submodule(self, submodule, status): if self.offline: cmd.append('--no-fetch') elif status == 'U': - raise _AHBoostrapSystemExit( + raise _AHBootstrapSystemExit( 'Error: Submodule {0} contains unresolved merge conflicts. ' 'Please complete or abandon any changes in the submodule so that ' 'it is in a usable state, then try again.'.format(submodule)) @@ -763,7 +829,7 @@ def run_cmd(cmd): msg = 'Command not found: `{0}`'.format(' '.join(cmd)) raise _CommandNotFound(msg, cmd) else: - raise _AHBoostrapSystemExit( + raise _AHBootstrapSystemExit( 'An unexpected error occurred when running the ' '`{0}` command:\n{1}'.format(' '.join(cmd), str(e))) @@ -780,9 +846,9 @@ def run_cmd(cmd): stdio_encoding = 'latin1' # Unlikely to fail at this point but even then let's be flexible - if not isinstance(stdout, _text_type): + if not isinstance(stdout, str): stdout = stdout.decode(stdio_encoding, 'replace') - if not isinstance(stderr, _text_type): + if not isinstance(stderr, str): stderr = stderr.decode(stdio_encoding, 'replace') return (p.returncode, stdout, stderr) @@ -835,6 +901,10 @@ def flush(self): pass +@contextlib.contextmanager +def _verbose(): + yield + @contextlib.contextmanager def _silence(): """A context manager that silences sys.stdout and sys.stderr.""" @@ -858,14 +928,6 @@ def _silence(): sys.stderr = old_stderr -_err_help_msg = """ -If the problem persists consider installing astropy_helpers manually using pip -(`pip install astropy_helpers`) or by manually downloading the source archive, -extracting it, and installing by running `python setup.py install` from the -root of the extracted source code. -""" - - class _AHBootstrapSystemExit(SystemExit): def __init__(self, *args): if not args: @@ -878,46 +940,6 @@ def __init__(self, *args): super(_AHBootstrapSystemExit, self).__init__(msg, *args[1:]) -if sys.version_info[:2] < (2, 7): - # In Python 2.6 the distutils log does not log warnings, errors, etc. to - # stderr so we have to wrap it to ensure consistency at least in this - # module - import distutils - - class log(object): - def __getattr__(self, attr): - return getattr(distutils.log, attr) - - def warn(self, msg, *args): - self._log_to_stderr(distutils.log.WARN, msg, *args) - - def error(self, msg): - self._log_to_stderr(distutils.log.ERROR, msg, *args) - - def fatal(self, msg): - self._log_to_stderr(distutils.log.FATAL, msg, *args) - - def log(self, level, msg, *args): - if level in (distutils.log.WARN, distutils.log.ERROR, - distutils.log.FATAL): - self._log_to_stderr(level, msg, *args) - else: - distutils.log.log(level, msg, *args) - - def _log_to_stderr(self, level, msg, *args): - # This is the only truly 'public' way to get the current threshold - # of the log - current_threshold = distutils.log.set_threshold(distutils.log.WARN) - distutils.log.set_threshold(current_threshold) - if level >= current_threshold: - if args: - msg = msg % args - sys.stderr.write('%s\n' % msg) - sys.stderr.flush() - - log = log() - - BOOTSTRAPPER = _Bootstrapper.main() diff --git a/astropy_helpers b/astropy_helpers index d23a53f..ce42e6e 160000 --- a/astropy_helpers +++ b/astropy_helpers @@ -1 +1 @@ -Subproject commit d23a53f46dd1c3703e5eee63dca3f53bd18a4e8b +Subproject commit ce42e6e238c200a4715785ef8c9d233f612d0c75