diff --git a/.coveragerc b/.coveragerc index 164d77ca..b16a2ae1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,18 +1,18 @@ [run] -source = astropy_helpers +source = extension_helpers omit = - astropy_helpers/*/setup_package.py - astropy_helpers/tests/* - astropy_helpers/conftest.py + extension_helpers/*/setup_package.py + extension_helpers/tests/* + extension_helpers/conftest.py [report] omit = - astropy_helpers/*/setup_package.py - astropy_helpers/tests/* - astropy_helpers/conftest.py + extension_helpers/*/setup_package.py + extension_helpers/tests/* + extension_helpers/conftest.py exclude_lines = # Have to re-enable the standard pragma diff --git a/.gitignore b/.gitignore index 91fdf99b..33e60aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,8 @@ __pycache__ # Other generated files MANIFEST -astropy_helpers/version.py -astropy_helpers/cython_version.py +extension_helpers/version.py +extension_helpers/cython_version.py # Sphinx _build diff --git a/.travis.yml b/.travis.yml index 9808d6bb..d01d358b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -127,7 +127,7 @@ before_script: script: # First run tests in the repository - - pytest --cov astropy_helpers astropy_helpers + - pytest --cov extension_helpers extension_helpers # In conftest.py we produce a .coverage.subprocess that contains coverage # statistics for sub-processes, so we combine it with the main one here. @@ -142,9 +142,9 @@ script: - cd dist - tar xvzf *.tar.gz - rm *.tar.gz - - cd astropy-helpers* - - cp -RP ../../astropy_helpers/tests astropy_helpers/tests - - pytest astropy_helpers + - cd extension-helpers* + - cp -RP ../../extension_helpers/tests extension_helpers/tests + - pytest extension_helpers after_success: - codecov diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index 8840e6f3..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,650 +0,0 @@ -astropy-helpers Changelog -************************* - -4.0 (unreleased) ------------------- - -- Changed minimum required Python version to 3.6. [#498] - -- Changed minimum required Sphinx version to 1.7. [#498] - -- Added a --parallel option for build_docs. [#498] - - -3.2.2 (unreleased) ------------------- - -- Correctly handle main package directory inside namespace package. [#486] - - -3.2.1 (2019-06-13) ------------------- - -- Reverting issuing deprecation warning for the ``build_sphinx`` command. [#482] - -- Make sure that all data files get included in tar file releases. [#485] - - -3.2 (2019-05-29) ----------------- - -- Make sure that ``[options.package_data]`` in setup.cfg is taken into account - when collecting package data. [#453] - -- Simplified the code for the custom build_ext command. [#446] - -- Avoid importing the astropy package when trying to get the test command - when testing astropy itself. [#450] - -- Avoid importing whole package when trying to get version information. Note - that this has also introduced a small API change - ``cython_version`` and - ``compiler`` can no longer be imported from the ``package.version`` module - generated by astropy-helpers. Instead, you can import these from - ``package.cython_version`` and ``package.compiler_version`` respectively. [#442] - -- Make it possible to call ``generate_version_py`` and ``register_commands`` - without any arguments, which causes information to be read in from the - ``setup.cfg`` file. [#440] - -- Simplified setup.py and moved most of the configuration to setup.cfg. [#445] - -- Add a new ``astropy_helpers.setup_helpers.setup`` function that does all - the default boilerplate in typical ``setup.py`` files that use - astropy-helpers. [#443] - -- Remove ``deprecated``, ``deprecated_attribute``, and ``minversion`` from - ``astropy_helpers.utils``. [#447] - -- Updated minimum required version of setuptools to 30.3.0. [#440] - -- Remove functionality to adjust compilers if a broken compiler is detected. - This is not useful anymore as only a single compiler was previously patched - (now unlikely to be used) and this was only to fix a compilation issue in the - core astropy package. [#421] - -- ``sphinx-astropy`` is now a required dependency to build the docs, the - machinery to install it as eggs have been removed. [#474] - - -3.1.1 (2019-02-22) ------------------- - -- Moved documentation from README to Sphinx. [#444] - -- Fixed broken OpenMP detection when building with ``-coverage``. [#434] - - -3.1 (2018-12-04) ----------------- - -- Added extensive documentation about astropy-helpers to the README.rst file. [#416] - -- Fixed the compatibility of the build_docs command with Sphinx 1.8 and above. [#413] - -- Removing deprecated test_helpers.py file. [#369] - -- Removing ez_setup.py file and requiring setuptools 1.0 or later. [#384] - -- Remove all sphinx components from ``astropy-helpers``. These are now replaced - by the ``sphinx-astropy`` package in conjunction with the ``astropy-theme-sphinx``, - ``sphinx-automodapi``, and ``numpydoc`` packages. [#368] - -- openmp_helpers.py: Make add_openmp_flags_if_available() work for clang. - The necessary include, library, and runtime paths now get added to the C test code - used to determine if openmp works. - Autogenerator utility added ``openmp_enabled.is_openmp_enabled()`` - which can be called post build to determine state of OpenMP support. - [#382] - -- Add version_info tuple to autogenerated version.py. Allows for simple - version checking, i.e. version_info > (2,0,1). [#385] - - -3.0.2 (2018-06-01) ------------------- - -- Nothing changed. - - -3.0.1 (2018-02-22) ------------------- - -- Nothing changed. - - -3.0 (2018-02-09) ----------------- - -- Removing Python 2 support, including 2to3. Packages wishing to keep Python - 2 support should NOT update to this version. [#340] - -- Removing deprecated _test_compat making astropy a hard dependency for - packages wishing to use the astropy tests machinery. [#314] - -- Removing unused 'register' command since packages should be uploaded - with twine and get registered automatically. [#332] - - -2.0.11 (unreleased) -------------------- - -- Fixed an issue that caused pytest to crash if it tried to collect - tests. [#488] - - -2.0.10 (2019-05-29) -------------------- - -- Removed ``tocdepthfix`` sphinx extension that worked around a big in - Sphinx that has been long fixed. [#475] - -- Allow Python dev versions to pass the python version check. [#476] - -- Updated bundled version of sphinx-automodapi to v0.11. [#478] - - -2.0.9 (2019-02-22) ------------------- - -- Updated bundled version of sphinx-automodapi to v0.10. [#439] - -- Updated bundled sphinx extensions version to sphinx-astropy v1.1.1. [#454] - -- Include package name in error message for Python version in - ``ah_bootstrap.py``. [#441] - - -2.0.8 (2018-12-04) ------------------- - -- Fixed compatibility with Sphinx 1.8+. [#428] - -- Fixed error that occurs when installing a package in an environment where - ``numpy`` is not already installed. [#404] - -- Updated bundled version of sphinx-automodapi to v0.9. [#422] - -- Updated bundled version of numpydoc to v0.8.0. [#423] - - -2.0.7 (2018-06-01) ------------------- - -- Removing ez_setup.py file and requiring setuptools 1.0 or later. [#384] - - -2.0.6 (2018-02-24) ------------------- - -- Avoid deprecation warning due to ``exclude=`` keyword in ``setup.py``. [#379] - - -2.0.5 (2018-02-22) ------------------- - -- Fix segmentation faults that occurred when the astropy-helpers submodule - was first initialized in packages that also contained Cython code. [#375] - - -2.0.4 (2018-02-09) ------------------- - -- Support dotted package names as namespace packages in generate_version_py. - [#370] - -- Fix compatibility with setuptools 36.x and above. [#372] - -- Fix false negative in add_openmp_flags_if_available when measuring code - coverage with gcc. [#374] - - -2.0.3 (2018-01-20) ------------------- - -- Make sure that astropy-helpers 3.x.x is not downloaded on Python 2. [#362, #363] - -- The bundled version of sphinx-automodapi has been updated to v0.7. [#365] - -- Add --auto-use and --no-auto-use command-line flags to match the - ``auto_use`` configuration option, and add an alias - ``--use-system-astropy-helpers`` for ``--no-auto-use``. [#366] - - -2.0.2 (2017-10-13) ------------------- - -- Added new helper function add_openmp_flags_if_available that can add - OpenMP compilation flags to a C/Cython extension if needed. [#346] - -- Update numpydoc to v0.7. [#343] - -- The function ``get_git_devstr`` now returns ``'0'`` instead of ``None`` when - no git repository is present. This allows generation of development version - strings that are in a format that ``setuptools`` expects (e.g. "1.1.3.dev0" - instead of "1.1.3.dev"). [#330] - -- It is now possible to override generated timestamps to make builds - reproducible by setting the ``SOURCE_DATE_EPOCH`` environment variable [#341] - -- Mark Sphinx extensions as parallel-safe. [#344] - -- Switch to using mathjax instead of imgmath for local builds. [#342] - -- Deprecate ``exclude`` parameter of various functions in setup_helpers since - it could not work as intended. Add new function ``add_exclude_packages`` to - provide intended behavior. [#331] - -- Allow custom Sphinx doctest extension to recognize and process standard - doctest directives ``testsetup`` and ``doctest``. [#335] - - -2.0.1 (2017-07-28) ------------------- - -- Fix compatibility with Sphinx <1.5. [#326] - - -2.0 (2017-07-06) ----------------- - -- Add support for package that lies in a subdirectory. [#249] - -- Removing ``compat.subprocess``. [#298] - -- Python 3.3 is no longer supported. [#300] - -- The 'automodapi' Sphinx extension (and associated dependencies) has now - been moved to a standalone package which can be found at - https://github.com/astropy/sphinx-automodapi - this is now bundled in - astropy-helpers under astropy_helpers.extern.automodapi for - convenience. Version shipped with astropy-helpers is v0.6. - [#278, #303, #309, #323] - -- The ``numpydoc`` Sphinx extension has now been moved to - ``astropy_helpers.extern``. [#278] - -- Fix ``build_docs`` error catching, so it doesn't hide Sphinx errors. [#292] - -- Fix compatibility with Sphinx 1.6. [#318] - -- Updating ez_setup.py to the last version before it's removal. [#321] - - -1.3.1 (2017-03-18) ------------------- - -- Fixed the missing button to hide output in documentation code - blocks. [#287] - -- Fixed bug when ``build_docs`` when running with the clean (-l) option. [#289] - -- Add alternative location for various intersphinx inventories to fall back - to. [#293] - - -1.3 (2016-12-16) ----------------- - -- ``build_sphinx`` has been deprecated in favor of the ``build_docs`` command. - [#246] - -- Force the use of Cython's old ``build_ext`` command. A new ``build_ext`` - command was added in Cython 0.25, but it does not work with astropy-helpers - currently. [#261] - - -1.2 (2016-06-18) ----------------- - -- Added sphinx configuration value ``automodsumm_inherited_members``. - If ``True`` this will include members that are inherited from a base - class in the generated API docs. Defaults to ``False`` which matches - the previous behavior. [#215] - -- Fixed ``build_sphinx`` to recognize builds that succeeded but have output - *after* the "build succeeded." statement. This only applies when - ``--warnings-returncode`` is given (which is primarily relevant for Travis - documentation builds). [#223] - -- Fixed ``build_sphinx`` the sphinx extensions to not output a spurious warning - for sphinx versions > 1.4. [#229] - -- Add Python version dependent local sphinx inventories that contain - otherwise missing references. [#216] - -- ``astropy_helpers`` now require Sphinx 1.3 or later. [#226] - - -1.1.2 (2016-03-9) ------------------ - -- The CSS for the sphinx documentation was altered to prevent some text overflow - problems. [#217] - - -1.1.1 (2015-12-23) ------------------- - -- Fixed crash in build with ``AttributeError: cython_create_listing`` with - older versions of setuptools. [#209, #210] - - -1.1 (2015-12-10) ----------------- - -- The original ``AstropyTest`` class in ``astropy_helpers``, which implements - the ``setup.py test`` command, is deprecated in favor of moving the - implementation of that command closer to the actual Astropy test runner in - ``astropy.tests``. Now a dummy ``test`` command is provided solely for - informing users that they need ``astropy`` installed to run the tests - (however, the previous, now deprecated implementation is still provided and - continues to work with older versions of Astropy). See the related issue for - more details. [#184] - -- Added a useful new utility function to ``astropy_helpers.utils`` called - ``find_data_files``. This is similar to the ``find_packages`` function in - setuptools in that it can be used to search a package for data files - (matching a pattern) that can be passed to the ``package_data`` argument for - ``setup()``. See the docstring to ``astropy_helpers.utils.find_data_files`` - for more details. [#42] - -- The ``astropy_helpers`` module now sets the global ``_ASTROPY_SETUP_`` - flag upon import (from within a ``setup.py``) script, so it's not necessary - to have this in the ``setup.py`` script explicitly. If in doubt though, - there's no harm in setting it twice. Putting it in ``astropy_helpers`` - just ensures that any other imports that occur during build will have this - flag set. [#191] - -- It is now possible to use Cython as a ``setup_requires`` build requirement, - and still build Cython extensions even if Cython wasn't available at the - beginning of the build processes (that is, is automatically downloaded via - setuptools' processing of ``setup_requires``). [#185] - -- Moves the ``adjust_compiler`` check into the ``build_ext`` command itself, - so it's only used when actually building extension modules. This also - deprecates the stand-alone ``adjust_compiler`` function. [#76] - -- When running the ``build_sphinx`` / ``build_docs`` command with the ``-w`` - option, the output from Sphinx is streamed as it runs instead of silently - buffering until the doc build is complete. [#197] - -1.0.7 (unreleased) ------------------- - -- Fix missing import in ``astropy_helpers/utils.py``. [#196] - -1.0.6 (2015-12-04) ------------------- - -- Fixed bug where running ``./setup.py build_sphinx`` could return successfully - even when the build was not successful (and should have returned a non-zero - error code). [#199] - - -1.0.5 (2015-10-02) ------------------- - -- Fixed a regression in the ``./setup.py test`` command that was introduced in - v1.0.4. - - -1.0.4 (2015-10-02) ------------------- - -- Fixed issue with the sphinx documentation css where the line numbers for code - blocks were not aligned with the code. [#179, #180] - -- Fixed crash that could occur when trying to build Cython extension modules - when Cython isn't installed. Normally this still results in a failed build, - but was supposed to provide a useful error message rather than crash - outright (this was a regression introduced in v1.0.3). [#181] - -- Fixed a crash that could occur on Python 3 when a working C compiler isn't - found. [#182] - -- Quieted warnings about deprecated Numpy API in Cython extensions, when - building Cython extensions against Numpy >= 1.7. [#183, #186] - -- Improved support for py.test >= 2.7--running the ``./setup.py test`` command - now copies all doc pages into the temporary test directory as well, so that - all test files have a "common root directory". [#189, #190] - - -1.0.3 (2015-07-22) ------------------- - -- Added workaround for sphinx-doc/sphinx#1843, a but in Sphinx which - prevented descriptor classes with a custom metaclass from being documented - correctly. [#158] - -- Added an alias for the ``./setup.py build_sphinx`` command as - ``./setup.py build_docs`` which, to a new contributor, should hopefully be - less cryptic. [#161] - -- The fonts in graphviz diagrams now match the font of the HTML content. [#169] - -- When the documentation is built on readthedocs.org, MathJax will be - used for math rendering. When built elsewhere, the "pngmath" - extension is still used for math rendering. [#170] - -- Fix crash when importing astropy_helpers when running with ``python -OO`` - [#171] - -- The ``build`` and ``build_ext`` stages now correctly recognize the presence - of C++ files in Cython extensions (previously only vanilla C worked). [#173] - - -1.0.2 (2015-04-02) ------------------- - -- Various fixes enabling the astropy-helpers Sphinx build command and - Sphinx extensions to work with Sphinx 1.3. [#148] - -- More improvement to the ability to handle multiple versions of - astropy-helpers being imported in the same Python interpreter session - in the (somewhat rare) case of nested installs. [#147] - -- To better support high resolution displays, use SVG for the astropy - logo and linkout image, falling back to PNGs for browsers that - support it. [#150, #151] - -- Improve ``setup_helpers.get_compiler_version`` to work with more compilers, - and to return more info. This will help fix builds of Astropy on less - common compilers, like Sun C. [#153] - -1.0.1 (2015-03-04) ------------------- - -- Released in concert with v0.4.8 to address the same issues. - -0.4.8 (2015-03-04) ------------------- - -- Improved the ``ah_bootstrap`` script's ability to override existing - installations of astropy-helpers with new versions in the context of - installing multiple packages simultaneously within the same Python - interpreter (e.g. when one package has in its ``setup_requires`` another - package that uses a different version of astropy-helpers. [#144] - -- Added a workaround to an issue in matplotlib that can, in rare cases, lead - to a crash when installing packages that import matplotlib at build time. - [#144] - -1.0 (2015-02-17) ----------------- - -- Added new pre-/post-command hook points for ``setup.py`` commands. Now any - package can define code to run before and/or after any ``setup.py`` command - without having to manually subclass that command by adding - ``pre__hook`` and ``post__hook`` callables to - the package's ``setup_package.py`` module. See the PR for more details. - [#112] - -- The following objects in the ``astropy_helpers.setup_helpers`` module have - been relocated: - - - ``get_dummy_distribution``, ``get_distutils_*``, ``get_compiler_option``, - ``add_command_option``, ``is_distutils_display_option`` -> - ``astropy_helpers.distutils_helpers`` - - - ``should_build_with_cython``, ``generate_build_ext_command`` -> - ``astropy_helpers.commands.build_ext`` - - - ``AstropyBuildPy`` -> ``astropy_helpers.commands.build_py`` - - - ``AstropyBuildSphinx`` -> ``astropy_helpers.commands.build_sphinx`` - - - ``AstropyInstall`` -> ``astropy_helpers.commands.install`` - - - ``AstropyInstallLib`` -> ``astropy_helpers.commands.install_lib`` - - - ``AstropyRegister`` -> ``astropy_helpers.commands.register`` - - - ``get_pkg_version_module`` -> ``astropy_helpers.version_helpers`` - - - ``write_if_different``, ``import_file``, ``get_numpy_include_path`` -> - ``astropy_helpers.utils`` - - All of these are "soft" deprecations in the sense that they are still - importable from ``astropy_helpers.setup_helpers`` for now, and there is - no (easy) way to produce deprecation warnings when importing these objects - from ``setup_helpers`` rather than directly from the modules they are - defined in. But please consider updating any imports to these objects. - [#110] - -- Use of the ``astropy.sphinx.ext.astropyautosummary`` extension is deprecated - for use with Sphinx < 1.2. Instead it should suffice to remove this - extension for the ``extensions`` list in your ``conf.py`` and add the stock - ``sphinx.ext.autosummary`` instead. [#131] - - -0.4.7 (2015-02-17) ------------------- - -- Fixed incorrect/missing git hash being added to the generated ``version.py`` - when creating a release. [#141] - - -0.4.6 (2015-02-16) ------------------- - -- Fixed problems related to the automatically generated _compiler - module not being created properly. [#139] - - -0.4.5 (2015-02-11) ------------------- - -- Fixed an issue where ah_bootstrap.py could blow up when astropy_helper's - version number is 1.0. - -- Added a workaround for documentation of properties in the rare case - where the class's metaclass has a property of the same name. [#130] - -- Fixed an issue on Python 3 where importing a package using astropy-helper's - generated version.py module would crash when the current working directory - is an empty git repository. [#114, #137] - -- Fixed an issue where the "revision count" appended to .dev versions by - the generated version.py did not accurately reflect the revision count for - the package it belongs to, and could be invalid if the current working - directory is an unrelated git repository. [#107, #137] - -- Likewise, fixed a confusing warning message that could occur in the same - circumstances as the above issue. [#121, #137] - - -0.4.4 (2014-12-31) ------------------- - -- More improvements for building the documentation using Python 3.x. [#100] - -- Additional minor fixes to Python 3 support. [#115] - -- Updates to support new test features in Astropy [#92, #106] - - -0.4.3 (2014-10-22) ------------------- - -- The generated ``version.py`` file now preserves the git hash of installed - copies of the package as well as when building a source distribution. That - is, the git hash of the changeset that was installed/released is preserved. - [#87] - -- In smart resolver add resolution for class links when they exist in the - intersphinx inventory, but not the mapping of the current package - (e.g. when an affiliated package uses an astropy core class of which - "actual" and "documented" location differs) [#88] - -- Fixed a bug that could occur when running ``setup.py`` for the first time - in a repository that uses astropy-helpers as a submodule: - ``AttributeError: 'NoneType' object has no attribute 'mkdtemp'`` [#89] - -- Fixed a bug where optional arguments to the ``doctest-skip`` Sphinx - directive were sometimes being left in the generated documentation output. - [#90] - -- Improved support for building the documentation using Python 3.x. [#96] - -- Avoid error message if .git directory is not present. [#91] - - -0.4.2 (2014-08-09) ------------------- - -- Fixed some CSS issues in generated API docs. [#69] - -- Fixed the warning message that could be displayed when generating a - version number with some older versions of git. [#77] - -- Fixed automodsumm to work with new versions of Sphinx (>= 1.2.2). [#80] - - -0.4.1 (2014-08-08) ------------------- - -- Fixed git revision count on systems with git versions older than v1.7.2. - [#70] - -- Fixed display of warning text when running a git command fails (previously - the output of stderr was not being decoded properly). [#70] - -- The ``--offline`` flag to ``setup.py`` understood by ``ah_bootstrap.py`` - now also prevents git from going online to fetch submodule updates. [#67] - -- The Sphinx extension for converting issue numbers to links in the changelog - now supports working on arbitrary pages via a new ``conf.py`` setting: - ``changelog_links_docpattern``. By default it affects the ``changelog`` - and ``whatsnew`` pages in one's Sphinx docs. [#61] - -- Fixed crash that could result from users with missing/misconfigured - locale settings. [#58] - -- The font used for code examples in the docs is now the - system-defined ``monospace`` font, rather than ``Minaco``, which is - not available on all platforms. [#50] - - -0.4 (2014-07-15) ----------------- - -- Initial release of astropy-helpers. See `APE4 - `_ for - details of the motivation and design of this package. - -- The ``astropy_helpers`` package replaces the following modules in the - ``astropy`` package: - - - ``astropy.setup_helpers`` -> ``astropy_helpers.setup_helpers`` - - - ``astropy.version_helpers`` -> ``astropy_helpers.version_helpers`` - - - ``astropy.sphinx`` - > ``astropy_helpers.sphinx`` - - These modules should be considered deprecated in ``astropy``, and any new, - non-critical changes to those modules will be made in ``astropy_helpers`` - instead. Affiliated packages wishing to make use those modules (as in the - Astropy package-template) should use the versions from ``astropy_helpers`` - instead, and include the ``ah_bootstrap.py`` script in their project, for - bootstrapping the ``astropy_helpers`` package in their setup.py script. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de6973cf..ee04364a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,20 +1,20 @@ -Contributing to astropy-helpers +Contributing to extension-helpers =============================== -The guidelines for contributing to ``astropy-helpers`` are generally the same +The guidelines for contributing to ``extension-helpers`` are generally the same as the [contributing guidelines for the astropy core package](http://github.com/astropy/astropy/blob/master/CONTRIBUTING.md). -Basically, report relevant issues in the ``astropy-helpers`` issue tracker, and +Basically, report relevant issues in the ``extension-helpers`` issue tracker, and we welcome pull requests that broadly follow the [Astropy coding guidelines](http://docs.astropy.org/en/latest/development/codeguide.html). The key subtlety lies in understanding the relationship between ``astropy`` and -``astropy-helpers``. This package contains the build, installation, and +``extension-helpers``. This package contains the build, installation, and documentation tools used by astropy. It also includes support for the ``setup.py test`` command, though Astropy is still required for this to function (it does not currently include the full Astropy test runner). So issues or improvements to that functionality should be addressed in this package. Any other aspect of the [astropy core package](http://github.com/astropy/astropy) (or any other package that uses -``astropy-helpers``) should be addressed in the github repository for that +``extension-helpers``) should be addressed in the github repository for that package. diff --git a/MANIFEST.in b/MANIFEST.in index 53e6e7f4..6b36bd26 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,4 +7,4 @@ include ah_bootstrap.py exclude *.pyc *.o prune build -prune astropy_helpers/tests +prune extension_helpers/tests diff --git a/README.rst b/README.rst index 8cac08a5..57c5d0bf 100644 --- a/README.rst +++ b/README.rst @@ -1,28 +1,18 @@ -astropy-helpers -=============== +extension-helpers +================= -.. image:: https://travis-ci.org/astropy/astropy-helpers.svg - :target: https://travis-ci.org/astropy/astropy-helpers +.. image:: https://travis-ci.org/astropy/extension-helpers.svg + :target: https://travis-ci.org/astropy/extension-helpers .. image:: https://ci.appveyor.com/api/projects/status/rt9161t9mhx02xp7/branch/master?svg=true - :target: https://ci.appveyor.com/project/Astropy/astropy-helpers + :target: https://ci.appveyor.com/project/Astropy/extension-helpers -.. image:: https://codecov.io/gh/astropy/astropy-helpers/branch/master/graph/badge.svg - :target: https://codecov.io/gh/astropy/astropy-helpers +.. image:: https://codecov.io/gh/astropy/extension-helpers/branch/master/graph/badge.svg + :target: https://codecov.io/gh/astropy/extension-helpers -The **astropy-helpers** package includes many build, installation, and -documentation-related tools used by the Astropy project, but packaged separately -for use by other projects that wish to leverage this work. The motivation behind -this package and details of its implementation are in the accepted -`Astropy Proposal for Enhancement (APE) 4 `_. +The **extension-helpers** package includes convenience helpers to assist with +building Python packages with compiled C/Cython extensions. It is developed by +the Astropy project but is intended to be general and usable by any Python +package. -Astropy-helpers is not a traditional package in the sense that it is not -intended to be installed directly by users or developers. Instead, it is meant -to be accessed when the ``setup.py`` command is run - see the -"`Using astropy-helpers in a package `_" -section in the documentation for how to do this. -For a real-life example of how to implement astropy-helpers in a -project, see the ``setup.py`` and ``setup.cfg`` files of the -`Affiliated package template `_. - -For more information, see the documentation at http://astropy-helpers.readthedocs.io +For more information, see the documentation at http://extension-helpers.readthedocs.io diff --git a/ah_bootstrap.py b/ah_bootstrap.py deleted file mode 100644 index 67ca92be..00000000 --- a/ah_bootstrap.py +++ /dev/null @@ -1,1009 +0,0 @@ -""" -This bootstrap module contains code for ensuring that the astropy_helpers -package will be importable by the time the setup.py script runs. It also -includes some workarounds to ensure that a recent-enough version of setuptools -is being used for the installation. - -This module should be the first thing imported in the setup.py of distributions -that make use of the utilities in astropy_helpers. If the distribution ships -with its own copy of astropy_helpers, this module will first attempt to import -from the shipped copy. However, it will also check PyPI to see if there are -any bug-fix releases on top of the current version that may be useful to get -past platform-specific bugs that have been fixed. When running setup.py, use -the ``--offline`` command-line option to disable the auto-upgrade checks. - -When this module is imported or otherwise executed it automatically calls a -main function that attempts to read the project's setup.cfg file, which it -checks for a configuration section called ``[ah_bootstrap]`` the presences of -that section, and options therein, determine the next step taken: If it -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 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 -the bootstrap script when ``auto_use = True``. - -See https://github.com/astropy/astropy-helpers for more details, and for the -latest version of this module. -""" - -import contextlib -import errno -import io -import locale -import os -import re -import subprocess as sp -import sys - -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 - -_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: - 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 - -try: - 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 -# https://github.com/astropy/astropy-helpers/issues/89; if we don't import this -# module now, it will get cleaned up after `run_setup` is called, but that will -# later cause the TemporaryDirectory class defined in it to stop working when -# used later on by setuptools -try: - import setuptools.py31compat # noqa -except ImportError: - pass - - -# matplotlib can cause problems if it is imported from within a call of -# run_setup(), because in some circumstances it will try to write to the user's -# home directory, resulting in a SandboxViolation. See -# https://github.com/matplotlib/matplotlib/pull/4165 -# Making sure matplotlib, if it is available, is imported early in the setup -# process can mitigate this (note importing matplotlib.pyplot has the same -# issue) -try: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot -except: - # Ignore if this fails for *any* reason* - pass - - -# End compatibility imports... - - -class _Bootstrapper(object): - """ - Bootstrapper implementation. See ``use_astropy_helpers`` for parameter - documentation. - """ - - def __init__(self, path=None, index_url=None, use_git=None, offline=None, - download_if_needed=None, auto_upgrade=None): - - if path is None: - path = PACKAGE_NAME - - if not (isinstance(path, _str_types) or path is False): - raise TypeError('path must be a string or False') - - if not isinstance(path, str): - fs_encoding = sys.getfilesystemencoding() - path = path.decode(fs_encoding) # path to unicode - - self.path = path - - # Set other option attributes, using defaults where necessary - self.index_url = index_url if index_url is not None else INDEX_URL - self.offline = offline if offline is not None else OFFLINE - - # If offline=True, override download and auto-upgrade - if self.offline: - download_if_needed = False - auto_upgrade = False - - self.download = (download_if_needed - if download_if_needed is not None - else DOWNLOAD_IF_NEEDED) - self.auto_upgrade = (auto_upgrade - if auto_upgrade is not None else AUTO_UPGRADE) - - # If this is a release then the .git directory will not exist so we - # should not use git. - git_dir_exists = os.path.exists(os.path.join(os.path.dirname(__file__), '.git')) - if use_git is None and not git_dir_exists: - use_git = False - - self.use_git = use_git if use_git is not None else USE_GIT - # Declared as False by default--later we check if astropy-helpers can be - # upgraded from PyPI, but only if not using a source distribution (as in - # the case of import from a git submodule) - self.is_submodule = False - - @classmethod - def main(cls, argv=None): - if argv is None: - argv = sys.argv - - config = cls.parse_config() - config.update(cls.parse_command_line(argv)) - - auto_use = config.pop('auto_use', False) - bootstrapper = cls(**config) - - if auto_use: - # Run the bootstrapper, otherwise the setup.py is using the old - # use_astropy_helpers() interface, in which case it will run the - # bootstrapper manually after reconfiguring it. - bootstrapper.run() - - return bootstrapper - - @classmethod - def parse_config(cls): - - if not SETUP_CFG.has_section('ah_bootstrap'): - return {} - - config = {} - - for option, type_ in CFG_OPTIONS: - if not SETUP_CFG.has_option('ah_bootstrap', option): - continue - - if type_ is bool: - value = SETUP_CFG.getboolean('ah_bootstrap', option) - else: - value = SETUP_CFG.get('ah_bootstrap', option) - - config[option] = value - - return config - - @classmethod - def parse_command_line(cls, argv=None): - if argv is None: - argv = sys.argv - - config = {} - - # For now we just pop recognized ah_bootstrap options out of the - # arg list. This is imperfect; in the unlikely case that a setup.py - # custom command or even custom Distribution class defines an argument - # of the same name then we will break that. However there's a catch22 - # here that we can't just do full argument parsing right here, because - # we don't yet know *how* to parse all possible command-line arguments. - if '--no-git' in argv: - config['use_git'] = False - argv.remove('--no-git') - - if '--offline' in argv: - 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): - strategies = ['local_directory', 'local_file', 'index'] - dist = None - - # First, remove any previously imported versions of astropy_helpers; - # this is necessary for nested installs where one package's installer - # is installing another package via setuptools.sandbox.run_setup, as in - # the case of setup_requires - for key in list(sys.modules): - try: - if key == PACKAGE_NAME or key.startswith(PACKAGE_NAME + '.'): - del sys.modules[key] - except AttributeError: - # Sometimes mysterious non-string things can turn up in - # sys.modules - continue - - # Check to see if the path is a submodule - self.is_submodule = self._check_submodule() - - for strategy in strategies: - method = getattr(self, 'get_{0}_dist'.format(strategy)) - dist = method() - if dist is not None: - break - else: - raise _AHBootstrapSystemExit( - "No source found for the {0!r} package; {0} must be " - "available and importable as a prerequisite to building " - "or installing this package.".format(PACKAGE_NAME)) - - # This is a bit hacky, but if astropy_helpers was loaded from a - # directory/submodule its Distribution object gets a "precedence" of - # "DEVELOP_DIST". However, in other cases it gets a precedence of - # "EGG_DIST". However, when activing the distribution it will only be - # placed early on sys.path if it is treated as an EGG_DIST, so always - # do that - dist = dist.clone(precedence=pkg_resources.EGG_DIST) - - # Otherwise we found a version of astropy-helpers, so we're done - # Just active the found distribution on sys.path--if we did a - # download this usually happens automatically but it doesn't hurt to - # do it again - # Note: Adding the dist to the global working set also activates it - # (makes it importable on sys.path) by default. - - try: - pkg_resources.working_set.add(dist, replace=True) - except TypeError: - # Some (much) older versions of setuptools do not have the - # replace=True option here. These versions are old enough that all - # bets may be off anyways, but it's easy enough to work around just - # in case... - if dist.key in pkg_resources.working_set.by_key: - del pkg_resources.working_set.by_key[dist.key] - pkg_resources.working_set.add(dist) - - @property - def config(self): - """ - A `dict` containing the options this `_Bootstrapper` was configured - with. - """ - - return dict((optname, getattr(self, optname)) - for optname, _ in CFG_OPTIONS if hasattr(self, optname)) - - def get_local_directory_dist(self): - """ - Handle importing a vendored package from a subdirectory of the source - distribution. - """ - - if not os.path.isdir(self.path): - return - - log.info('Attempting to import astropy_helpers from {0} {1!r}'.format( - 'submodule' if self.is_submodule else 'directory', - self.path)) - - dist = self._directory_import() - - if dist is None: - log.warn( - 'The requested path {0!r} for importing {1} does not ' - 'exist, or does not contain a copy of the {1} ' - 'package.'.format(self.path, PACKAGE_NAME)) - elif self.auto_upgrade and not self.is_submodule: - # A version of astropy-helpers was found on the available path, but - # check to see if a bugfix release is available on PyPI - upgrade = self._do_upgrade(dist) - if upgrade is not None: - dist = upgrade - - return dist - - def get_local_file_dist(self): - """ - Handle importing from a source archive; this also uses setup_requires - but points easy_install directly to the source archive. - """ - - if not os.path.isfile(self.path): - return - - log.info('Attempting to unpack and import astropy_helpers from ' - '{0!r}'.format(self.path)) - - try: - dist = self._do_download(find_links=[self.path]) - except Exception as e: - if DEBUG: - raise - - log.warn( - 'Failed to import {0} from the specified archive {1!r}: ' - '{2}'.format(PACKAGE_NAME, self.path, str(e))) - dist = None - - if dist is not None and self.auto_upgrade: - # A version of astropy-helpers was found on the available path, but - # check to see if a bugfix release is available on PyPI - upgrade = self._do_upgrade(dist) - if upgrade is not None: - dist = upgrade - - return dist - - def get_index_dist(self): - if not self.download: - log.warn('Downloading {0!r} disabled.'.format(DIST_NAME)) - return None - - log.warn( - "Downloading {0!r}; run setup.py with the --offline option to " - "force offline installation.".format(DIST_NAME)) - - try: - dist = self._do_download() - except Exception as e: - if DEBUG: - raise - log.warn( - 'Failed to download and/or install {0!r} from {1!r}:\n' - '{2}'.format(DIST_NAME, self.index_url, str(e))) - dist = None - - # No need to run auto-upgrade here since we've already presumably - # gotten the most up-to-date version from the package index - return dist - - def _directory_import(self): - """ - Import astropy_helpers from the given path, which will be added to - sys.path. - - Must return True if the import succeeded, and False otherwise. - """ - - # Return True on success, False on failure but download is allowed, and - # otherwise raise SystemExit - path = os.path.abspath(self.path) - - # Use an empty WorkingSet rather than the man - # pkg_resources.working_set, since on older versions of setuptools this - # will invoke a VersionConflict when trying to install an upgrade - ws = pkg_resources.WorkingSet([]) - ws.add_entry(path) - dist = ws.by_key.get(DIST_NAME) - - if dist is None: - # We didn't find an egg-info/dist-info in the given path, but if a - # setup.py exists we can generate it - setup_py = os.path.join(path, 'setup.py') - if os.path.isfile(setup_py): - # 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... - return dist - - return dist - - def _do_download(self, version='', find_links=None): - if find_links: - allow_hosts = '' - index_url = None - else: - allow_hosts = None - index_url = self.index_url - - # Annoyingly, setuptools will not handle other arguments to - # Distribution (such as options) before handling setup_requires, so it - # is not straightforward to programmatically augment the arguments which - # are passed to easy_install - class _Distribution(Distribution): - def get_option_dict(self, command_name): - opts = Distribution.get_option_dict(self, command_name) - if command_name == 'easy_install': - if find_links is not None: - opts['find_links'] = ('setup script', find_links) - if index_url is not None: - opts['index_url'] = ('setup script', index_url) - if allow_hosts is not None: - opts['allow_hosts'] = ('setup script', allow_hosts) - return opts - - if version: - req = '{0}=={1}'.format(DIST_NAME, version) - else: - 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: - - 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 - return pkg_resources.working_set.by_key.get(DIST_NAME) - except Exception as e: - if DEBUG: - raise - - msg = 'Error retrieving {0} from {1}:\n{2}' - if find_links: - source = find_links[0] - elif index_url != INDEX_URL: - source = index_url - else: - source = 'PyPI' - - raise Exception(msg.format(DIST_NAME, source, repr(e))) - - def _do_upgrade(self, dist): - # Build up a requirement for a higher bugfix release but a lower minor - # release (so API compatibility is guaranteed) - next_version = _next_version(dist.parsed_version) - - req = pkg_resources.Requirement.parse( - '{0}>{1},<{2}'.format(DIST_NAME, dist.version, next_version)) - - package_index = PackageIndex(index_url=self.index_url) - - upgrade = package_index.obtain(req) - - if upgrade is not None: - return self._do_download(version=upgrade.version) - - def _check_submodule(self): - """ - Check if the given path is a git submodule. - - See the docstrings for ``_check_submodule_using_git`` and - ``_check_submodule_no_git`` for further details. - """ - - if (self.path is None or - (os.path.exists(self.path) and not os.path.isdir(self.path))): - return False - - if self.use_git: - return self._check_submodule_using_git() - else: - return self._check_submodule_no_git() - - def _check_submodule_using_git(self): - """ - Check if the given path is a git submodule. If so, attempt to initialize - and/or update the submodule if needed. - - This function makes calls to the ``git`` command in subprocesses. The - ``_check_submodule_no_git`` option uses pure Python to check if the given - path looks like a git submodule, but it cannot perform updates. - """ - - cmd = ['git', 'submodule', 'status', '--', self.path] - - try: - log.info('Running `{0}`; use the --no-git option to disable git ' - 'commands'.format(' '.join(cmd))) - returncode, stdout, stderr = run_cmd(cmd) - except _CommandNotFound: - # The git command simply wasn't found; this is most likely the - # case on user systems that don't have git and are simply - # trying to install the package from PyPI or a source - # distribution. Silently ignore this case and simply don't try - # to use submodules - return False - - stderr = stderr.strip() - - if returncode != 0 and stderr: - # Unfortunately the return code alone cannot be relied on, as - # earlier versions of git returned 0 even if the requested submodule - # does not exist - - # This is a warning that occurs in perl (from running git submodule) - # which only occurs with a malformatted locale setting which can - # happen sometimes on OSX. See again - # https://github.com/astropy/astropy/issues/2749 - perl_warning = ('perl: warning: Falling back to the standard locale ' - '("C").') - if not stderr.strip().endswith(perl_warning): - # Some other unknown error condition occurred - log.warn('git submodule command failed ' - 'unexpectedly:\n{0}'.format(stderr)) - return False - - # Output of `git submodule status` is as follows: - # - # 1: Status indicator: '-' for submodule is uninitialized, '+' if - # submodule is initialized but is not at the commit currently indicated - # in .gitmodules (and thus needs to be updated), or 'U' if the - # submodule is in an unstable state (i.e. has merge conflicts) - # - # 2. SHA-1 hash of the current commit of the submodule (we don't really - # need this information but it's useful for checking that the output is - # correct) - # - # 3. The output of `git describe` for the submodule's current commit - # hash (this includes for example what branches the commit is on) but - # only if the submodule is initialized. We ignore this information for - # now - _git_submodule_status_re = re.compile( - r'^(?P[+-U ])(?P[0-9a-f]{40}) ' - r'(?P\S+)( .*)?$') - - # The stdout should only contain one line--the status of the - # requested submodule - m = _git_submodule_status_re.match(stdout) - if m: - # Yes, the path *is* a git submodule - self._update_submodule(m.group('submodule'), m.group('status')) - return True - else: - log.warn( - 'Unexpected output from `git submodule status`:\n{0}\n' - 'Will attempt import from {1!r} regardless.'.format( - stdout, self.path)) - return False - - def _check_submodule_no_git(self): - """ - Like ``_check_submodule_using_git``, but simply parses the .gitmodules file - to determine if the supplied path is a git submodule, and does not exec any - subprocesses. - - This can only determine if a path is a submodule--it does not perform - updates, etc. This function may need to be updated if the format of the - .gitmodules file is changed between git versions. - """ - - gitmodules_path = os.path.abspath('.gitmodules') - - if not os.path.isfile(gitmodules_path): - return False - - # This is a minimal reader for gitconfig-style files. It handles a few of - # the quirks that make gitconfig files incompatible with ConfigParser-style - # files, but does not support the full gitconfig syntax (just enough - # needed to read a .gitmodules file). - gitmodules_fileobj = io.StringIO() - - # Must use io.open for cross-Python-compatible behavior wrt unicode - with io.open(gitmodules_path) as f: - for line in f: - # gitconfig files are more flexible with leading whitespace; just - # go ahead and remove it - line = line.lstrip() - - # comments can start with either # or ; - if line and line[0] in (':', ';'): - continue - - gitmodules_fileobj.write(line) - - gitmodules_fileobj.seek(0) - - cfg = RawConfigParser() - - try: - cfg.readfp(gitmodules_fileobj) - except Exception as exc: - log.warn('Malformatted .gitmodules file: {0}\n' - '{1} cannot be assumed to be a git submodule.'.format( - exc, self.path)) - return False - - for section in cfg.sections(): - if not cfg.has_option(section, 'path'): - continue - - submodule_path = cfg.get(section, 'path').rstrip(os.sep) - - if submodule_path == self.path.rstrip(os.sep): - return True - - return False - - def _update_submodule(self, submodule, status): - if status == ' ': - # The submodule is up to date; no action necessary - return - elif status == '-': - if self.offline: - raise _AHBootstrapSystemExit( - "Cannot initialize the {0} submodule in --offline mode; " - "this requires being able to clone the submodule from an " - "online repository.".format(submodule)) - cmd = ['update', '--init'] - action = 'Initializing' - elif status == '+': - cmd = ['update'] - action = 'Updating' - if self.offline: - cmd.append('--no-fetch') - elif status == 'U': - 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)) - else: - log.warn('Unknown status {0!r} for git submodule {1!r}. Will ' - 'attempt to use the submodule as-is, but try to ensure ' - 'that the submodule is in a clean state and contains no ' - 'conflicts or errors.\n{2}'.format(status, submodule, - _err_help_msg)) - return - - err_msg = None - cmd = ['git', 'submodule'] + cmd + ['--', submodule] - log.warn('{0} {1} submodule with: `{2}`'.format( - action, submodule, ' '.join(cmd))) - - try: - log.info('Running `{0}`; use the --no-git option to disable git ' - 'commands'.format(' '.join(cmd))) - returncode, stdout, stderr = run_cmd(cmd) - except OSError as e: - err_msg = str(e) - else: - if returncode != 0: - err_msg = stderr - - if err_msg is not None: - log.warn('An unexpected error occurred updating the git submodule ' - '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, - _err_help_msg)) - -class _CommandNotFound(OSError): - """ - An exception raised when a command run with run_cmd is not found on the - system. - """ - - -def run_cmd(cmd): - """ - Run a command in a subprocess, given as a list of command-line - arguments. - - Returns a ``(returncode, stdout, stderr)`` tuple. - """ - - try: - p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) - # XXX: May block if either stdout or stderr fill their buffers; - # however for the commands this is currently used for that is - # unlikely (they should have very brief output) - stdout, stderr = p.communicate() - except OSError as e: - if DEBUG: - raise - - if e.errno == errno.ENOENT: - msg = 'Command not found: `{0}`'.format(' '.join(cmd)) - raise _CommandNotFound(msg, cmd) - else: - raise _AHBootstrapSystemExit( - 'An unexpected error occurred when running the ' - '`{0}` command:\n{1}'.format(' '.join(cmd), str(e))) - - - # Can fail of the default locale is not configured properly. See - # https://github.com/astropy/astropy/issues/2749. For the purposes under - # consideration 'latin1' is an acceptable fallback. - try: - stdio_encoding = locale.getdefaultlocale()[1] or 'latin1' - except ValueError: - # Due to an OSX oddity locale.getdefaultlocale() can also crash - # depending on the user's locale/language settings. See: - # http://bugs.python.org/issue18378 - stdio_encoding = 'latin1' - - # Unlikely to fail at this point but even then let's be flexible - if not isinstance(stdout, str): - stdout = stdout.decode(stdio_encoding, 'replace') - if not isinstance(stderr, str): - stderr = stderr.decode(stdio_encoding, 'replace') - - return (p.returncode, stdout, stderr) - - -def _next_version(version): - """ - Given a parsed version from pkg_resources.parse_version, returns a new - version string with the next minor version. - - Examples - ======== - >>> _next_version(pkg_resources.parse_version('1.2.3')) - '1.3.0' - """ - - if hasattr(version, 'base_version'): - # New version parsing from setuptools >= 8.0 - if version.base_version: - parts = version.base_version.split('.') - else: - parts = [] - else: - parts = [] - for part in version: - if part.startswith('*'): - break - parts.append(part) - - parts = [int(p) for p in parts] - - if len(parts) < 3: - parts += [0] * (3 - len(parts)) - - major, minor, micro = parts[:3] - - return '{0}.{1}.{2}'.format(major, minor + 1, 0) - - -class _DummyFile(object): - """A noop writeable object.""" - - errors = '' # Required for Python 3.x - encoding = 'utf-8' - - def write(self, s): - pass - - def flush(self): - pass - - -@contextlib.contextmanager -def _verbose(): - yield - -@contextlib.contextmanager -def _silence(): - """A context manager that silences sys.stdout and sys.stderr.""" - - old_stdout = sys.stdout - old_stderr = sys.stderr - sys.stdout = _DummyFile() - sys.stderr = _DummyFile() - exception_occurred = False - try: - yield - except: - exception_occurred = True - # Go ahead and clean up so that exception handling can work normally - sys.stdout = old_stdout - sys.stderr = old_stderr - raise - - if not exception_occurred: - sys.stdout = old_stdout - sys.stderr = old_stderr - - -class _AHBootstrapSystemExit(SystemExit): - def __init__(self, *args): - if not args: - msg = 'An unknown problem occurred bootstrapping astropy_helpers.' - else: - msg = args[0] - - msg += '\n' + _err_help_msg - - super(_AHBootstrapSystemExit, self).__init__(msg, *args[1:]) - - -BOOTSTRAPPER = _Bootstrapper.main() - - -def use_astropy_helpers(**kwargs): - """ - Ensure that the `astropy_helpers` module is available and is importable. - This supports automatic submodule initialization if astropy_helpers is - included in a project as a git submodule, or will download it from PyPI if - necessary. - - Parameters - ---------- - - path : str or None, optional - A filesystem path relative to the root of the project's source code - that should be added to `sys.path` so that `astropy_helpers` can be - imported from that path. - - If the path is a git submodule it will automatically be initialized - and/or updated. - - The path may also be to a ``.tar.gz`` archive of the astropy_helpers - source distribution. In this case the archive is automatically - unpacked and made temporarily available on `sys.path` as a ``.egg`` - archive. - - If `None` skip straight to downloading. - - download_if_needed : bool, optional - If the provided filesystem path is not found an attempt will be made to - download astropy_helpers from PyPI. It will then be made temporarily - available on `sys.path` as a ``.egg`` archive (using the - ``setup_requires`` feature of setuptools. If the ``--offline`` option - is given at the command line the value of this argument is overridden - to `False`. - - index_url : str, optional - If provided, use a different URL for the Python package index than the - main PyPI server. - - use_git : bool, optional - If `False` no git commands will be used--this effectively disables - support for git submodules. If the ``--no-git`` option is given at the - command line the value of this argument is overridden to `False`. - - auto_upgrade : bool, optional - By default, when installing a package from a non-development source - distribution ah_boostrap will try to automatically check for patch - releases to astropy-helpers on PyPI and use the patched version over - any bundled versions. Setting this to `False` will disable that - functionality. If the ``--offline`` option is given at the command line - the value of this argument is overridden to `False`. - - offline : bool, optional - If `False` disable all actions that require an internet connection, - including downloading packages from the package index and fetching - updates to any git submodule. Defaults to `True`. - """ - - global BOOTSTRAPPER - - config = BOOTSTRAPPER.config - config.update(**kwargs) - - # Create a new bootstrapper with the updated configuration and run it - BOOTSTRAPPER = _Bootstrapper(**config) - BOOTSTRAPPER.run() diff --git a/astropy_helpers/__init__.py b/astropy_helpers/__init__.py deleted file mode 100644 index a995ae5f..00000000 --- a/astropy_helpers/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -try: - from .version import version as __version__ - from .version import githash as __githash__ -except ImportError: - __version__ = '' - __githash__ = '' - - -# If we've made it as far as importing astropy_helpers, we don't need -# ah_bootstrap in sys.modules anymore. Getting rid of it is actually necessary -# if the package we're installing has a setup_requires of another package that -# uses astropy_helpers (and possibly a different version at that) -# See https://github.com/astropy/astropy/issues/3541 -import sys -if 'ah_bootstrap' in sys.modules: - del sys.modules['ah_bootstrap'] - - -# Note, this is repeated from ah_bootstrap.py, but is here too in case this -# astropy-helpers was upgraded to from an older version that did not have this -# check in its ah_bootstrap. -# matplotlib can cause problems if it is imported from within a call of -# run_setup(), because in some circumstances it will try to write to the user's -# home directory, resulting in a SandboxViolation. See -# https://github.com/matplotlib/matplotlib/pull/4165 -# Making sure matplotlib, if it is available, is imported early in the setup -# process can mitigate this (note importing matplotlib.pyplot has the same -# issue) -try: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot -except: - # Ignore if this fails for *any* reason* - pass - - -import os -# Ensure that all module-level code in astropy or other packages know that -# we're in setup mode: -if ('__main__' in sys.modules and - hasattr(sys.modules['__main__'], '__file__')): - filename = os.path.basename(sys.modules['__main__'].__file__) - - if filename.rstrip('co') == 'setup.py': - import builtins - builtins._ASTROPY_SETUP_ = True - - del filename diff --git a/astropy_helpers/commands/__init__.py b/astropy_helpers/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/astropy_helpers/commands/_dummy.py b/astropy_helpers/commands/_dummy.py deleted file mode 100644 index 63762463..00000000 --- a/astropy_helpers/commands/_dummy.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Provides a base class for a 'dummy' setup.py command that has no functionality -(probably due to a missing requirement). This dummy command can raise an -exception when it is run, explaining to the user what dependencies must be met -to use this command. - -The reason this is at all tricky is that we want the command to be able to -provide this message even when the user passes arguments to the command. If we -don't know ahead of time what arguments the command can take, this is -difficult, because distutils does not allow unknown arguments to be passed to a -setup.py command. This hacks around that restriction to provide a useful error -message even when a user passes arguments to the dummy implementation of a -command. - -Use this like: - - try: - from some_dependency import SetupCommand - except ImportError: - from ._dummy import _DummyCommand - - class SetupCommand(_DummyCommand): - description = \ - 'Implementation of SetupCommand from some_dependency; ' - 'some_dependency must be installed to run this command' - - # This is the message that will be raised when a user tries to - # run this command--define it as a class attribute. - error_msg = \ - "The 'setup_command' command requires the some_dependency " - "package to be installed and importable." -""" - -import sys -from setuptools import Command -from distutils.errors import DistutilsArgError -from textwrap import dedent - - -class _DummyCommandMeta(type): - """ - Causes an exception to be raised on accessing attributes of a command class - so that if ``./setup.py command_name`` is run with additional command-line - options we can provide a useful error message instead of the default that - tells users the options are unrecognized. - """ - - def __init__(cls, name, bases, members): - if bases == (Command, object): - # This is the _DummyCommand base class, presumably - return - - if not hasattr(cls, 'description'): - raise TypeError( - "_DummyCommand subclass must have a 'description' " - "attribute.") - - if not hasattr(cls, 'error_msg'): - raise TypeError( - "_DummyCommand subclass must have an 'error_msg' " - "attribute.") - - def __getattribute__(cls, attr): - if attr in ('description', 'error_msg') or attr.startswith('_'): - # Allow cls.description to work so that `./setup.py - # --help-commands` still works - return super(_DummyCommandMeta, cls).__getattribute__(attr) - - raise DistutilsArgError(cls.error_msg) - - -class _DummyCommand(Command, object, metaclass=_DummyCommandMeta): - pass diff --git a/astropy_helpers/commands/build_ext.py b/astropy_helpers/commands/build_ext.py deleted file mode 100644 index fbc2c346..00000000 --- a/astropy_helpers/commands/build_ext.py +++ /dev/null @@ -1,206 +0,0 @@ -import errno -import os -import shutil - -from distutils.core import Extension -from distutils.ccompiler import get_default_compiler -from distutils.command.build_ext import build_ext as DistutilsBuildExt - -from ..distutils_helpers import get_main_package_directory -from ..utils import get_numpy_include_path, import_file - -__all__ = ['AstropyHelpersBuildExt'] - - -def should_build_with_cython(previous_cython_version, is_release): - """ - Returns the previously used Cython version (or 'unknown' if not - previously built) if Cython should be used to build extension modules from - pyx files. - """ - - # Only build with Cython if, of course, Cython is installed, we're in a - # development version (i.e. not release) or the Cython-generated source - # files haven't been created yet (cython_version == 'unknown'). The latter - # case can happen even when release is True if checking out a release tag - # from the repository - have_cython = False - try: - from Cython import __version__ as cython_version # noqa - have_cython = True - except ImportError: - pass - - if have_cython and (not is_release or previous_cython_version == 'unknown'): - return cython_version - else: - return False - - -class AstropyHelpersBuildExt(DistutilsBuildExt): - """ - A custom 'build_ext' command that allows for manipulating some of the C - extension options at build time. - """ - - _uses_cython = False - _force_rebuild = False - - def __new__(cls, value, **kwargs): - - # NOTE: we need to wait until AstropyHelpersBuildExt is initialized to - # import setuptools.command.build_ext because when that package is - # imported, setuptools tries to import Cython - and if it's not found - # it will affect the rest of the build process. This is an issue because - # if we import that module at the top of this one, setup_requires won't - # have been honored yet, so Cython may not yet be available - and if we - # import build_ext too soon, it will think Cython is not available even - # if it is then intalled when setup_requires is processed. To get around - # this we dynamically create a new class that inherits from the - # setuptools build_ext, and by this point setup_requires has been - # processed. - - from setuptools.command.build_ext import build_ext as SetuptoolsBuildExt - - class FinalBuildExt(AstropyHelpersBuildExt, SetuptoolsBuildExt): - pass - - new_type = type(cls.__name__, (FinalBuildExt,), dict(cls.__dict__)) - obj = SetuptoolsBuildExt.__new__(new_type) - obj.__init__(value) - - return obj - - def finalize_options(self): - - # First let's find the package folder, then we can check if the - # version and cython_version are accessible - self.package_dir = get_main_package_directory(self.distribution) - - version = import_file(os.path.join(self.package_dir, 'version.py'), - name='version').version - self.is_release = 'dev' not in version - - try: - self.previous_cython_version = import_file(os.path.join(self.package_dir, - 'cython_version.py'), - name='cython_version').cython_version - except (FileNotFoundError, ImportError): - self.previous_cython_version = 'unknown' - - self._uses_cython = should_build_with_cython(self.previous_cython_version, self.is_release) - - # Add a copy of the _compiler.so module as well, but only if there - # are in fact C modules to compile (otherwise there's no reason to - # include a record of the compiler used). Note that self.extensions - # may not be set yet, but self.distribution.ext_modules is where any - # extension modules passed to setup() can be found - extensions = self.distribution.ext_modules - if extensions: - build_py = self.get_finalized_command('build_py') - package_dir = build_py.get_package_dir(self.package_dir) - src_path = os.path.relpath( - os.path.join(os.path.dirname(__file__), 'src')) - shutil.copy(os.path.join(src_path, 'compiler.c'), - os.path.join(package_dir, '_compiler.c')) - ext = Extension(self.package_dir + '.compiler_version', - [os.path.join(package_dir, '_compiler.c')]) - extensions.insert(0, ext) - - super().finalize_options() - - # If we are using Cython, then make sure we re-build if the version - # of Cython that is installed is different from the version last - # used to generate the C files. - if self._uses_cython and self._uses_cython != self.previous_cython_version: - self._force_rebuild = True - - # Regardless of the value of the '--force' option, force a rebuild - # if the debug flag changed from the last build - if self._force_rebuild: - self.force = True - - def run(self): - - # For extensions that require 'numpy' in their include dirs, - # replace 'numpy' with the actual paths - np_include = None - for extension in self.extensions: - if 'numpy' in extension.include_dirs: - if np_include is None: - np_include = get_numpy_include_path() - idx = extension.include_dirs.index('numpy') - extension.include_dirs.insert(idx, np_include) - extension.include_dirs.remove('numpy') - - self._check_cython_sources(extension) - - # Note that setuptools automatically uses Cython to discover and - # build extensions if available, so we don't have to explicitly call - # e.g. cythonize. - - super().run() - - # Update cython_version.py if building with Cython - - if self._uses_cython and self._uses_cython != self.previous_cython_version: - build_py = self.get_finalized_command('build_py') - package_dir = build_py.get_package_dir(self.package_dir) - cython_py = os.path.join(package_dir, 'cython_version.py') - with open(cython_py, 'w') as f: - f.write('# Generated file; do not modify\n') - f.write('cython_version = {0!r}\n'.format(self._uses_cython)) - - if os.path.isdir(self.build_lib): - # The build/lib directory may not exist if the build_py - # command was not previously run, which may sometimes be - # the case - self.copy_file(cython_py, - os.path.join(self.build_lib, cython_py), - preserve_mode=False) - - def _check_cython_sources(self, extension): - """ - Where relevant, make sure that the .c files associated with .pyx - modules are present (if building without Cython installed). - """ - - # Determine the compiler we'll be using - if self.compiler is None: - compiler = get_default_compiler() - else: - compiler = self.compiler - - # Replace .pyx with C-equivalents, unless c files are missing - for jdx, src in enumerate(extension.sources): - base, ext = os.path.splitext(src) - pyxfn = base + '.pyx' - cfn = base + '.c' - cppfn = base + '.cpp' - - if not os.path.isfile(pyxfn): - continue - - if self._uses_cython: - extension.sources[jdx] = pyxfn - else: - if os.path.isfile(cfn): - extension.sources[jdx] = cfn - elif os.path.isfile(cppfn): - extension.sources[jdx] = cppfn - else: - msg = ( - 'Could not find C/C++ file {0}.(c/cpp) for Cython ' - 'file {1} when building extension {2}. Cython ' - 'must be installed to build from a git ' - 'checkout.'.format(base, pyxfn, extension.name)) - raise IOError(errno.ENOENT, msg, cfn) - - # Cython (at least as of 0.29.2) uses deprecated Numpy API features - # the use of which produces a few warnings when compiling. - # These additional flags should squelch those warnings. - # TODO: Feel free to remove this if/when a Cython update - # removes use of the deprecated Numpy API - if compiler == 'unix': - extension.extra_compile_args.extend([ - '-Wp,-w', '-Wno-unused-function']) diff --git a/astropy_helpers/commands/build_sphinx.py b/astropy_helpers/commands/build_sphinx.py deleted file mode 100644 index 8beb1dcc..00000000 --- a/astropy_helpers/commands/build_sphinx.py +++ /dev/null @@ -1,235 +0,0 @@ - -import os -import pkgutil -import re -import shutil -import subprocess -import sys -from distutils.version import LooseVersion - -from distutils import log - -from sphinx.setup_command import BuildDoc as SphinxBuildDoc - -SUBPROCESS_TEMPLATE = """ -import os -import sys - -{build_main} - -os.chdir({srcdir!r}) - -{sys_path_inserts} - -for builder in {builders!r}: - retcode = build_main(argv={argv!r} + ['-b', builder, '.', os.path.join({output_dir!r}, builder)]) - if retcode != 0: - sys.exit(retcode) -""" - - -def ensure_sphinx_astropy_installed(): - """ - Make sure that sphinx-astropy is available. - """ - - try: - from sphinx_astropy import __version__ as sphinx_astropy_version # noqa - except ImportError: - sphinx_astropy_version = None - - if (sphinx_astropy_version is None - or LooseVersion(sphinx_astropy_version) < LooseVersion('1.2')): - raise ImportError("sphinx-astropy 1.2 or later needs to be installed to build " - "the documentation.") - - -class AstropyBuildDocs(SphinxBuildDoc): - """ - A version of the ``build_docs`` command that uses the version of Astropy - that is built by the setup ``build`` command, rather than whatever is - installed on the system. To build docs against the installed version, run - ``make html`` in the ``astropy/docs`` directory. - """ - - description = 'Build Sphinx documentation for Astropy environment' - user_options = SphinxBuildDoc.user_options[:] - user_options.append( - ('warnings-returncode', 'w', - 'Parses the sphinx output and sets the return code to 1 if there ' - 'are any warnings. Note that this will cause the sphinx log to ' - 'only update when it completes, rather than continuously as is ' - 'normally the case.')) - user_options.append( - ('clean-docs', 'l', - 'Completely clean previous builds, including ' - 'automodapi-generated files before building new ones')) - user_options.append( - ('no-intersphinx', 'n', - 'Skip intersphinx, even if conf.py says to use it')) - user_options.append( - ('open-docs-in-browser', 'o', - 'Open the docs in a browser (using the webbrowser module) if the ' - 'build finishes successfully.')) - user_options.append( - ('parallel=', 'j', - 'Build the docs in parallel on the specified number of ' - 'processes. If "auto", all the cores on the machine will be ' - 'used.')) - - boolean_options = SphinxBuildDoc.boolean_options[:] - boolean_options.append('warnings-returncode') - boolean_options.append('clean-docs') - boolean_options.append('no-intersphinx') - boolean_options.append('open-docs-in-browser') - - _self_iden_rex = re.compile(r"self\.([^\d\W][\w]+)", re.UNICODE) - - def initialize_options(self): - SphinxBuildDoc.initialize_options(self) - self.clean_docs = False - self.no_intersphinx = False - self.open_docs_in_browser = False - self.warnings_returncode = False - self.traceback = False - self.parallel = None - - def finalize_options(self): - - # This has to happen before we call the parent class's finalize_options - if self.build_dir is None: - self.build_dir = 'docs/_build' - - SphinxBuildDoc.finalize_options(self) - - # Clear out previous sphinx builds, if requested - if self.clean_docs: - - dirstorm = [os.path.join(self.source_dir, 'api'), - os.path.join(self.source_dir, 'generated')] - - dirstorm.append(self.build_dir) - - for d in dirstorm: - if os.path.isdir(d): - log.info('Cleaning directory ' + d) - shutil.rmtree(d) - else: - log.info('Not cleaning directory ' + d + ' because ' - 'not present or not a directory') - - def run(self): - - # TODO: Break this method up into a few more subroutines and - # document them better - import webbrowser - - from urllib.request import pathname2url - - # This is used at the very end of `run` to decide if sys.exit should - # be called. If it's None, it won't be. - retcode = None - - # Now make sure Astropy is built and determine where it was built - build_cmd = self.reinitialize_command('build') - build_cmd.inplace = 0 - self.run_command('build') - build_cmd = self.get_finalized_command('build') - build_cmd_path = os.path.abspath(build_cmd.build_lib) - - ah_importer = pkgutil.get_importer('astropy_helpers') - if ah_importer is None: - ah_path = '.' - else: - ah_path = os.path.abspath(ah_importer.path) - - build_main = 'from sphinx.cmd.build import build_main' - - # We need to make sure sphinx-astropy is installed - ensure_sphinx_astropy_installed() - - sys_path_inserts = [build_cmd_path, ah_path] - sys_path_inserts = os.linesep.join(['sys.path.insert(0, {0!r})'.format(path) for path in sys_path_inserts]) - - argv = [] - - if self.warnings_returncode: - argv.append('-W') - - if self.no_intersphinx: - argv.extend(['-D', 'disable_intersphinx=1']) - - # We now need to adjust the flags based on the parent class's options - - if self.fresh_env: - argv.append('-E') - - if self.all_files: - argv.append('-a') - - if getattr(self, 'pdb', False): - argv.append('-P') - - if getattr(self, 'nitpicky', False): - argv.append('-n') - - if self.traceback: - argv.append('-T') - - # The default verbosity level is 1, so in that case we just don't add a flag - if self.verbose == 0: - argv.append('-q') - elif self.verbose > 1: - argv.append('-v') - - if self.parallel is not None: - argv.append(f'-j={self.parallel}') - - if isinstance(self.builder, str): - builders = [self.builder] - else: - builders = self.builder - - subproccode = SUBPROCESS_TEMPLATE.format(build_main=build_main, - srcdir=self.source_dir, - sys_path_inserts=sys_path_inserts, - builders=builders, - argv=argv, - output_dir=os.path.abspath(self.build_dir)) - - log.debug('Starting subprocess of {0} with python code:\n{1}\n' - '[CODE END])'.format(sys.executable, subproccode)) - - proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE) - proc.communicate(subproccode.encode('utf-8')) - if proc.returncode != 0: - retcode = proc.returncode - - if retcode is None: - if self.open_docs_in_browser: - if self.builder == 'html': - absdir = os.path.abspath(self.builder_target_dir) - index_path = os.path.join(absdir, 'index.html') - fileurl = 'file://' + pathname2url(index_path) - webbrowser.open(fileurl) - else: - log.warn('open-docs-in-browser option was given, but ' - 'the builder is not html! Ignoring.') - - # Here we explicitly check proc.returncode since we only want to output - # this for cases where the return code really wasn't 0. - if proc.returncode: - log.warn('Sphinx Documentation subprocess failed with return ' - 'code ' + str(proc.returncode)) - - if retcode is not None: - # this is potentially dangerous in that there might be something - # after the call to `setup` in `setup.py`, and exiting here will - # prevent that from running. But there's no other apparent way - # to signal what the return code should be. - sys.exit(retcode) - - -class AstropyBuildSphinx(AstropyBuildDocs): # pragma: no cover - def run(self): - AstropyBuildDocs.run(self) diff --git a/astropy_helpers/commands/test.py b/astropy_helpers/commands/test.py deleted file mode 100644 index 07b72ef7..00000000 --- a/astropy_helpers/commands/test.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Different implementations of the ``./setup.py test`` command depending on -what's locally available. - -If Astropy v1.1 or later is available it should be possible to import -AstropyTest from ``astropy.tests.command``. Otherwise there is a skeleton -implementation that allows users to at least discover the ``./setup.py test`` -command and learn that they need Astropy to run it. -""" - -import os -from ..utils import import_file - -# Previously these except statements caught only ImportErrors, but there are -# some other obscure exceptional conditions that can occur when importing -# astropy.tests (at least on older versions) that can cause these imports to -# fail - -try: - - # If we are testing astropy itself, we need to use import_file to avoid - # actually importing astropy (just the file we need). - command_file = os.path.join('astropy', 'tests', 'command.py') - if os.path.exists(command_file): - AstropyTest = import_file(command_file, 'astropy_tests_command').AstropyTest - else: - import astropy # noqa - from astropy.tests.command import AstropyTest - -except Exception: - - # No astropy at all--provide the dummy implementation - from ._dummy import _DummyCommand - - class AstropyTest(_DummyCommand): - command_name = 'test' - description = 'Run the tests for this package' - error_msg = ( - "The 'test' command requires the astropy package to be " - "installed and importable.") diff --git a/astropy_helpers/distutils_helpers.py b/astropy_helpers/distutils_helpers.py deleted file mode 100644 index cda1864c..00000000 --- a/astropy_helpers/distutils_helpers.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -This module contains various utilities for introspecting the distutils -module and the setup process. - -Some of these utilities require the -`astropy_helpers.setup_helpers.register_commands` function to be called first, -as it will affect introspection of setuptools command-line arguments. Other -utilities in this module do not have that restriction. -""" - -import os -import sys - -from distutils import ccompiler, log -from distutils.dist import Distribution -from distutils.errors import DistutilsError - -from .utils import silence - - -# This function, and any functions that call it, require the setup in -# `astropy_helpers.setup_helpers.register_commands` to be run first. -def get_dummy_distribution(): - """ - Returns a distutils Distribution object used to instrument the setup - environment before calling the actual setup() function. - """ - - from .setup_helpers import _module_state - - if _module_state['registered_commands'] is None: - raise RuntimeError( - 'astropy_helpers.setup_helpers.register_commands() must be ' - 'called before using ' - 'astropy_helpers.setup_helpers.get_dummy_distribution()') - - # Pre-parse the Distutils command-line options and config files to if - # the option is set. - dist = Distribution({'script_name': os.path.basename(sys.argv[0]), - 'script_args': sys.argv[1:]}) - dist.cmdclass.update(_module_state['registered_commands']) - - with silence(): - try: - dist.parse_config_files() - dist.parse_command_line() - except (DistutilsError, AttributeError, SystemExit): - # Let distutils handle DistutilsErrors itself AttributeErrors can - # get raise for ./setup.py --help SystemExit can be raised if a - # display option was used, for example - pass - - return dist - - -def get_main_package_directory(distribution): - """ - Given a Distribution object, return the main package directory. - """ - return min(distribution.packages, key=len).replace('.', os.sep) - -def get_distutils_option(option, commands): - """ Returns the value of the given distutils option. - - Parameters - ---------- - option : str - The name of the option - - commands : list of str - The list of commands on which this option is available - - Returns - ------- - val : str or None - the value of the given distutils option. If the option is not set, - returns None. - """ - - dist = get_dummy_distribution() - - for cmd in commands: - cmd_opts = dist.command_options.get(cmd) - if cmd_opts is not None and option in cmd_opts: - return cmd_opts[option][1] - else: - return None - - -def get_distutils_build_option(option): - """ Returns the value of the given distutils build option. - - Parameters - ---------- - option : str - The name of the option - - Returns - ------- - val : str or None - The value of the given distutils build option. If the option - is not set, returns None. - """ - return get_distutils_option(option, ['build', 'build_ext', 'build_clib']) - - -def get_distutils_install_option(option): - """ Returns the value of the given distutils install option. - - Parameters - ---------- - option : str - The name of the option - - Returns - ------- - val : str or None - The value of the given distutils build option. If the option - is not set, returns None. - """ - return get_distutils_option(option, ['install']) - - -def get_distutils_build_or_install_option(option): - """ Returns the value of the given distutils build or install option. - - Parameters - ---------- - option : str - The name of the option - - Returns - ------- - val : str or None - The value of the given distutils build or install option. If the - option is not set, returns None. - """ - return get_distutils_option(option, ['build', 'build_ext', 'build_clib', - 'install']) - - -def get_compiler_option(): - """ Determines the compiler that will be used to build extension modules. - - Returns - ------- - compiler : str - The compiler option specified for the build, build_ext, or build_clib - command; or the default compiler for the platform if none was - specified. - - """ - - compiler = get_distutils_build_option('compiler') - if compiler is None: - return ccompiler.get_default_compiler() - - return compiler - - -def add_command_option(command, name, doc, is_bool=False): - """ - Add a custom option to a setup command. - - Issues a warning if the option already exists on that command. - - Parameters - ---------- - command : str - The name of the command as given on the command line - - name : str - The name of the build option - - doc : str - A short description of the option, for the `--help` message - - is_bool : bool, optional - When `True`, the option is a boolean option and doesn't - require an associated value. - """ - - dist = get_dummy_distribution() - cmdcls = dist.get_command_class(command) - - if (hasattr(cmdcls, '_astropy_helpers_options') and - name in cmdcls._astropy_helpers_options): - return - - attr = name.replace('-', '_') - - if hasattr(cmdcls, attr): - raise RuntimeError( - '{0!r} already has a {1!r} class attribute, barring {2!r} from ' - 'being usable as a custom option name.'.format(cmdcls, attr, name)) - - for idx, cmd in enumerate(cmdcls.user_options): - if cmd[0] == name: - log.warn('Overriding existing {0!r} option ' - '{1!r}'.format(command, name)) - del cmdcls.user_options[idx] - if name in cmdcls.boolean_options: - cmdcls.boolean_options.remove(name) - break - - cmdcls.user_options.append((name, None, doc)) - - if is_bool: - cmdcls.boolean_options.append(name) - - # Distutils' command parsing requires that a command object have an - # attribute with the same name as the option (with '-' replaced with '_') - # in order for that option to be recognized as valid - setattr(cmdcls, attr, None) - - # This caches the options added through add_command_option so that if it is - # run multiple times in the same interpreter repeated adds are ignored - # (this way we can still raise a RuntimeError if a custom option overrides - # a built-in option) - if not hasattr(cmdcls, '_astropy_helpers_options'): - cmdcls._astropy_helpers_options = set([name]) - else: - cmdcls._astropy_helpers_options.add(name) - - -def get_distutils_display_options(): - """ Returns a set of all the distutils display options in their long and - short forms. These are the setup.py arguments such as --name or --version - which print the project's metadata and then exit. - - Returns - ------- - opts : set - The long and short form display option arguments, including the - or -- - """ - - short_display_opts = set('-' + o[1] for o in Distribution.display_options - if o[1]) - long_display_opts = set('--' + o[0] for o in Distribution.display_options) - - # Include -h and --help which are not explicitly listed in - # Distribution.display_options (as they are handled by optparse) - short_display_opts.add('-h') - long_display_opts.add('--help') - - # This isn't the greatest approach to hardcode these commands. - # However, there doesn't seem to be a good way to determine - # whether build *will be* run as part of the command at this - # phase. - display_commands = set([ - 'clean', 'register', 'setopt', 'saveopts', 'egg_info', - 'alias']) - - return short_display_opts.union(long_display_opts.union(display_commands)) - - -def is_distutils_display_option(): - """ Returns True if sys.argv contains any of the distutils display options - such as --version or --name. - """ - - display_options = get_distutils_display_options() - return bool(set(sys.argv[1:]).intersection(display_options)) diff --git a/astropy_helpers/git_helpers.py b/astropy_helpers/git_helpers.py deleted file mode 100644 index bab45f6e..00000000 --- a/astropy_helpers/git_helpers.py +++ /dev/null @@ -1,195 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -""" -Utilities for retrieving revision information from a project's git repository. -""" - -# Do not remove the following comment; it is used by -# astropy_helpers.version_helpers to determine the beginning of the code in -# this module - -# BEGIN - -import locale -import os -import subprocess -import warnings - -__all__ = ['get_git_devstr'] - - -def _decode_stdio(stream): - try: - stdio_encoding = locale.getdefaultlocale()[1] or 'utf-8' - except ValueError: - stdio_encoding = 'utf-8' - - try: - text = stream.decode(stdio_encoding) - except UnicodeDecodeError: - # Final fallback - text = stream.decode('latin1') - - return text - - -def update_git_devstr(version, path=None): - """ - Updates the git revision string if and only if the path is being imported - directly from a git working copy. This ensures that the revision number in - the version string is accurate. - """ - - try: - # Quick way to determine if we're in git or not - returns '' if not - devstr = get_git_devstr(sha=True, show_warning=False, path=path) - except OSError: - return version - - if not devstr: - # Probably not in git so just pass silently - return version - - if 'dev' in version: # update to the current git revision - version_base = version.split('.dev', 1)[0] - devstr = get_git_devstr(sha=False, show_warning=False, path=path) - - return version_base + '.dev' + devstr - else: - # otherwise it's already the true/release version - return version - - -def get_git_devstr(sha=False, show_warning=True, path=None): - """ - Determines the number of revisions in this repository. - - Parameters - ---------- - sha : bool - If True, the full SHA1 hash will be returned. Otherwise, the total - count of commits in the repository will be used as a "revision - number". - - show_warning : bool - If True, issue a warning if git returns an error code, otherwise errors - pass silently. - - path : str or None - If a string, specifies the directory to look in to find the git - repository. If `None`, the current working directory is used, and must - be the root of the git repository. - If given a filename it uses the directory containing that file. - - Returns - ------- - devversion : str - Either a string with the revision number (if `sha` is False), the - SHA1 hash of the current commit (if `sha` is True), or an empty string - if git version info could not be identified. - - """ - - if path is None: - path = os.getcwd() - - if not os.path.isdir(path): - path = os.path.abspath(os.path.dirname(path)) - - if sha: - # Faster for getting just the hash of HEAD - cmd = ['rev-parse', 'HEAD'] - else: - cmd = ['rev-list', '--count', 'HEAD'] - - def run_git(cmd): - try: - p = subprocess.Popen(['git'] + cmd, cwd=path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE) - stdout, stderr = p.communicate() - except OSError as e: - if show_warning: - warnings.warn('Error running git: ' + str(e)) - return (None, b'', b'') - - if p.returncode == 128: - if show_warning: - warnings.warn('No git repository present at {0!r}! Using ' - 'default dev version.'.format(path)) - return (p.returncode, b'', b'') - if p.returncode == 129: - if show_warning: - warnings.warn('Your git looks old (does it support {0}?); ' - 'consider upgrading to v1.7.2 or ' - 'later.'.format(cmd[0])) - return (p.returncode, stdout, stderr) - elif p.returncode != 0: - if show_warning: - warnings.warn('Git failed while determining revision ' - 'count: {0}'.format(_decode_stdio(stderr))) - return (p.returncode, stdout, stderr) - - return p.returncode, stdout, stderr - - returncode, stdout, stderr = run_git(cmd) - - if not sha and returncode == 128: - # git returns 128 if the command is not run from within a git - # repository tree. In this case, a warning is produced above but we - # return the default dev version of '0'. - return '0' - elif not sha and returncode == 129: - # git returns 129 if a command option failed to parse; in - # particular this could happen in git versions older than 1.7.2 - # where the --count option is not supported - # Also use --abbrev-commit and --abbrev=0 to display the minimum - # number of characters needed per-commit (rather than the full hash) - cmd = ['rev-list', '--abbrev-commit', '--abbrev=0', 'HEAD'] - returncode, stdout, stderr = run_git(cmd) - # Fall back on the old method of getting all revisions and counting - # the lines - if returncode == 0: - return str(stdout.count(b'\n')) - else: - return '' - elif sha: - return _decode_stdio(stdout)[:40] - else: - return _decode_stdio(stdout).strip() - - -# This function is tested but it is only ever executed within a subprocess when -# creating a fake package, so it doesn't get picked up by coverage metrics. -def _get_repo_path(pathname, levels=None): # pragma: no cover - """ - Given a file or directory name, determine the root of the git repository - this path is under. If given, this won't look any higher than ``levels`` - (that is, if ``levels=0`` then the given path must be the root of the git - repository and is returned if so. - - Returns `None` if the given path could not be determined to belong to a git - repo. - """ - - if os.path.isfile(pathname): - current_dir = os.path.abspath(os.path.dirname(pathname)) - elif os.path.isdir(pathname): - current_dir = os.path.abspath(pathname) - else: - return None - - current_level = 0 - - while levels is None or current_level <= levels: - if os.path.exists(os.path.join(current_dir, '.git')): - return current_dir - - current_level += 1 - if current_dir == os.path.dirname(current_dir): - break - - current_dir = os.path.dirname(current_dir) - - return None diff --git a/astropy_helpers/setup_helpers.py b/astropy_helpers/setup_helpers.py deleted file mode 100644 index 5ae47a8a..00000000 --- a/astropy_helpers/setup_helpers.py +++ /dev/null @@ -1,783 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst -""" -This module contains a number of utilities for use during -setup/build/packaging that are useful to astropy as a whole. -""" - -import collections -import os -import re -import subprocess -import sys -import traceback -import warnings -from configparser import ConfigParser -import builtins - -from distutils import log -from distutils.errors import DistutilsOptionError, DistutilsModuleError -from distutils.core import Extension -from distutils.core import Command -from distutils.command.sdist import sdist as DistutilsSdist - -from setuptools import setup as setuptools_setup -from setuptools.config import read_configuration -from setuptools import find_packages as _find_packages - -from .distutils_helpers import (add_command_option, get_compiler_option, - get_dummy_distribution, get_distutils_build_option, - get_distutils_build_or_install_option) -from .version_helpers import get_pkg_version_module, generate_version_py -from .utils import (walk_skip_hidden, import_file, extends_doc, - resolve_name, AstropyDeprecationWarning) - -from .commands.build_ext import AstropyHelpersBuildExt -from .commands.test import AstropyTest - -# These imports are not used in this module, but are included for backwards -# compat with older versions of this module -from .utils import get_numpy_include_path, write_if_different # noqa - -__all__ = ['register_commands', 'get_package_info'] - -_module_state = {'registered_commands': None, - 'have_sphinx': False, - 'package_cache': None, - 'exclude_packages': set(), - 'excludes_too_late': False} - -try: - import sphinx # noqa - _module_state['have_sphinx'] = True -except ValueError as e: - # This can occur deep in the bowels of Sphinx's imports by way of docutils - # and an occurrence of this bug: http://bugs.python.org/issue18378 - # In this case sphinx is effectively unusable - if 'unknown locale' in e.args[0]: - log.warn( - "Possible misconfiguration of one of the environment variables " - "LC_ALL, LC_CTYPES, LANG, or LANGUAGE. For an example of how to " - "configure your system's language environment on OSX see " - "http://blog.remibergsma.com/2012/07/10/" - "setting-locales-correctly-on-mac-osx-terminal-application/") -except ImportError: - pass -except SyntaxError: - # occurs if markupsafe is recent version, which doesn't support Python 3.2 - pass - - -def setup(**kwargs): - """ - A wrapper around setuptools' setup() function that automatically sets up - custom commands, generates a version file, and customizes the setup process - via the ``setup_package.py`` files. - """ - - # DEPRECATED: store the package name in a built-in variable so it's easy - # to get from other parts of the setup infrastructure. We should phase this - # out in packages that use it - the cookiecutter template should now be - # able to put the right package name where needed. - conf = read_configuration('setup.cfg') - builtins._ASTROPY_PACKAGE_NAME_ = conf['metadata']['name'] - - # Create a dictionary with setup command overrides. Note that this gets - # information about the package (name and version) from the setup.cfg file. - cmdclass = register_commands() - - # Freeze build information in version.py. Note that this gets information - # about the package (name and version) from the setup.cfg file. - version = generate_version_py() - - # Get configuration information from all of the various subpackages. - # See the docstring for setup_helpers.update_package_files for more - # details. - package_info = get_package_info() - package_info['cmdclass'] = cmdclass - package_info['version'] = version - - # Override using any specified keyword arguments - package_info.update(kwargs) - - setuptools_setup(**package_info) - - -def adjust_compiler(package): - warnings.warn( - 'The adjust_compiler function in setup.py is ' - 'deprecated and can be removed from your setup.py.', - AstropyDeprecationWarning) - - -def get_debug_option(packagename): - """ Determines if the build is in debug mode. - - Returns - ------- - debug : bool - True if the current build was started with the debug option, False - otherwise. - - """ - - try: - current_debug = get_pkg_version_module(packagename, - fromlist=['debug'])[0] - except (ImportError, AttributeError): - current_debug = None - - # Only modify the debug flag if one of the build commands was explicitly - # run (i.e. not as a sub-command of something else) - dist = get_dummy_distribution() - if any(cmd in dist.commands for cmd in ['build', 'build_ext']): - debug = bool(get_distutils_build_option('debug')) - else: - debug = bool(current_debug) - - if current_debug is not None and current_debug != debug: - build_ext_cmd = dist.get_command_class('build_ext') - build_ext_cmd._force_rebuild = True - - return debug - - -def add_exclude_packages(excludes): - - if _module_state['excludes_too_late']: - raise RuntimeError( - "add_package_excludes must be called before all other setup helper " - "functions in order to properly handle excluded packages") - - _module_state['exclude_packages'].update(set(excludes)) - - -def register_commands(package=None, version=None, release=None, srcdir='.'): - """ - This function generates a dictionary containing customized commands that - can then be passed to the ``cmdclass`` argument in ``setup()``. - """ - - if package is not None: - warnings.warn('The package argument to generate_version_py has ' - 'been deprecated and will be removed in future. Specify ' - 'the package name in setup.cfg instead', AstropyDeprecationWarning) - - if version is not None: - warnings.warn('The version argument to generate_version_py has ' - 'been deprecated and will be removed in future. Specify ' - 'the version number in setup.cfg instead', AstropyDeprecationWarning) - - if release is not None: - warnings.warn('The release argument to generate_version_py has ' - 'been deprecated and will be removed in future. We now ' - 'use the presence of the "dev" string in the version to ' - 'determine whether this is a release', AstropyDeprecationWarning) - - # We use ConfigParser instead of read_configuration here because the latter - # only reads in keys recognized by setuptools, but we need to access - # package_name below. - conf = ConfigParser() - conf.read('setup.cfg') - - if conf.has_option('metadata', 'name'): - package = conf.get('metadata', 'name') - elif conf.has_option('metadata', 'package_name'): - # The package-template used package_name instead of name for a while - warnings.warn('Specifying the package name using the "package_name" ' - 'option in setup.cfg is deprecated - use the "name" ' - 'option instead.', AstropyDeprecationWarning) - package = conf.get('metadata', 'package_name') - elif package is not None: # deprecated - pass - else: - sys.stderr.write('ERROR: Could not read package name from setup.cfg\n') - sys.exit(1) - - if _module_state['registered_commands'] is not None: - return _module_state['registered_commands'] - - if _module_state['have_sphinx']: - try: - from .commands.build_sphinx import (AstropyBuildSphinx, - AstropyBuildDocs) - except ImportError: - AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx - else: - AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx - - _module_state['registered_commands'] = registered_commands = { - 'test': generate_test_command(package), - - # Use distutils' sdist because it respects package_data. - # setuptools/distributes sdist requires duplication of information in - # MANIFEST.in - 'sdist': DistutilsSdist, - - 'build_ext': AstropyHelpersBuildExt, - 'build_sphinx': AstropyBuildSphinx, - 'build_docs': AstropyBuildDocs - } - - # Need to override the __name__ here so that the commandline options are - # presented as being related to the "build" command, for example; normally - # this wouldn't be necessary since commands also have a command_name - # attribute, but there is a bug in distutils' help display code that it - # uses __name__ instead of command_name. Yay distutils! - for name, cls in registered_commands.items(): - cls.__name__ = name - - # Add a few custom options; more of these can be added by specific packages - # later - for option in [ - ('use-system-libraries', - "Use system libraries whenever possible", True)]: - add_command_option('build', *option) - add_command_option('install', *option) - - add_command_hooks(registered_commands, srcdir=srcdir) - - return registered_commands - - -def add_command_hooks(commands, srcdir='.'): - """ - Look through setup_package.py modules for functions with names like - ``pre__hook`` and ``post__hook`` where - ```` is the name of a ``setup.py`` command (e.g. build_ext). - - If either hook is present this adds a wrapped version of that command to - the passed in ``commands`` `dict`. ``commands`` may be pre-populated with - other custom distutils command classes that should be wrapped if there are - hooks for them (e.g. `AstropyBuildPy`). - """ - - hook_re = re.compile(r'^(pre|post)_(.+)_hook$') - - # Distutils commands have a method of the same name, but it is not a - # *classmethod* (which probably didn't exist when distutils was first - # written) - def get_command_name(cmdcls): - if hasattr(cmdcls, 'command_name'): - return cmdcls.command_name - else: - return cmdcls.__name__ - - packages = find_packages(srcdir) - dist = get_dummy_distribution() - - hooks = collections.defaultdict(dict) - - for setuppkg in iter_setup_packages(srcdir, packages): - for name, obj in vars(setuppkg).items(): - match = hook_re.match(name) - if not match: - continue - - hook_type = match.group(1) - cmd_name = match.group(2) - - if hook_type not in hooks[cmd_name]: - hooks[cmd_name][hook_type] = [] - - hooks[cmd_name][hook_type].append((setuppkg.__name__, obj)) - - for cmd_name, cmd_hooks in hooks.items(): - commands[cmd_name] = generate_hooked_command( - cmd_name, dist.get_command_class(cmd_name), cmd_hooks) - - -def generate_hooked_command(cmd_name, cmd_cls, hooks): - """ - Returns a generated subclass of ``cmd_cls`` that runs the pre- and - post-command hooks for that command before and after the ``cmd_cls.run`` - method. - """ - - def run(self, orig_run=cmd_cls.run): - self.run_command_hooks('pre_hooks') - orig_run(self) - self.run_command_hooks('post_hooks') - - return type(cmd_name, (cmd_cls, object), - {'run': run, 'run_command_hooks': run_command_hooks, - 'pre_hooks': hooks.get('pre', []), - 'post_hooks': hooks.get('post', [])}) - - -def run_command_hooks(cmd_obj, hook_kind): - """Run hooks registered for that command and phase. - - *cmd_obj* is a finalized command object; *hook_kind* is either - 'pre_hook' or 'post_hook'. - """ - - hooks = getattr(cmd_obj, hook_kind, None) - - if not hooks: - return - - for modname, hook in hooks: - if isinstance(hook, str): - try: - hook_obj = resolve_name(hook) - except ImportError as exc: - raise DistutilsModuleError( - 'cannot find hook {0}: {1}'.format(hook, exc)) - else: - hook_obj = hook - - if not callable(hook_obj): - raise DistutilsOptionError('hook {0!r} is not callable' % hook) - - log.info('running {0} from {1} for {2} command'.format( - hook_kind.rstrip('s'), modname, cmd_obj.get_command_name())) - - try: - hook_obj(cmd_obj) - except Exception: - log.error('{0} command hook {1} raised an exception: %s\n'.format( - hook_obj.__name__, cmd_obj.get_command_name())) - log.error(traceback.format_exc()) - sys.exit(1) - - -def generate_test_command(package_name): - """ - Creates a custom 'test' command for the given package which sets the - command's ``package_name`` class attribute to the name of the package being - tested. - """ - - return type(package_name.title() + 'Test', (AstropyTest,), - {'package_name': package_name}) - - -def update_package_files(srcdir, extensions, package_data, packagenames, - package_dirs): - """ - This function is deprecated and maintained for backward compatibility - with affiliated packages. Affiliated packages should update their - setup.py to use `get_package_info` instead. - """ - - info = get_package_info(srcdir) - extensions.extend(info['ext_modules']) - package_data.update(info['package_data']) - packagenames = list(set(packagenames + info['packages'])) - package_dirs.update(info['package_dir']) - - -def get_package_info(srcdir='.', exclude=()): - """ - Collates all of the information for building all subpackages - and returns a dictionary of keyword arguments that can - be passed directly to `distutils.setup`. - - The purpose of this function is to allow subpackages to update the - arguments to the package's ``setup()`` function in its setup.py - script, rather than having to specify all extensions/package data - directly in the ``setup.py``. See Astropy's own - ``setup.py`` for example usage and the Astropy development docs - for more details. - - This function obtains that information by iterating through all - packages in ``srcdir`` and locating a ``setup_package.py`` module. - This module can contain the following functions: - ``get_extensions()``, ``get_package_data()``, - ``get_build_options()``, and ``get_external_libraries()``. - - Each of those functions take no arguments. - - - ``get_extensions`` returns a list of - `distutils.extension.Extension` objects. - - - ``get_package_data()`` returns a dict formatted as required by - the ``package_data`` argument to ``setup()``. - - - ``get_build_options()`` returns a list of tuples describing the - extra build options to add. - - - ``get_external_libraries()`` returns - a list of libraries that can optionally be built using external - dependencies. - """ - ext_modules = [] - packages = [] - package_dir = {} - - # Read in existing package data, and add to it below - setup_cfg = os.path.join(srcdir, 'setup.cfg') - if os.path.exists(setup_cfg): - conf = read_configuration(setup_cfg) - if 'options' in conf and 'package_data' in conf['options']: - package_data = conf['options']['package_data'] - else: - package_data = {} - else: - package_data = {} - - if exclude: - warnings.warn( - "Use of the exclude parameter is no longer supported since it does " - "not work as expected. Use add_exclude_packages instead. Note that " - "it must be called prior to any other calls from setup helpers.", - AstropyDeprecationWarning) - - # Use the find_packages tool to locate all packages and modules - packages = find_packages(srcdir, exclude=exclude) - - # Update package_dir if the package lies in a subdirectory - if srcdir != '.': - package_dir[''] = srcdir - - # For each of the setup_package.py modules, extract any - # information that is needed to install them. The build options - # are extracted first, so that their values will be available in - # subsequent calls to `get_extensions`, etc. - for setuppkg in iter_setup_packages(srcdir, packages): - if hasattr(setuppkg, 'get_build_options'): - options = setuppkg.get_build_options() - for option in options: - add_command_option('build', *option) - if hasattr(setuppkg, 'get_external_libraries'): - libraries = setuppkg.get_external_libraries() - for library in libraries: - add_external_library(library) - - for setuppkg in iter_setup_packages(srcdir, packages): - # get_extensions must include any Cython extensions by their .pyx - # filename. - if hasattr(setuppkg, 'get_extensions'): - ext_modules.extend(setuppkg.get_extensions()) - if hasattr(setuppkg, 'get_package_data'): - package_data.update(setuppkg.get_package_data()) - - # Locate any .pyx files not already specified, and add their extensions in. - # The default include dirs include numpy to facilitate numerical work. - ext_modules.extend(get_cython_extensions(srcdir, packages, ext_modules, - ['numpy'])) - - # Now remove extensions that have the special name 'skip_cython', as they - # exist Only to indicate that the cython extensions shouldn't be built - for i, ext in reversed(list(enumerate(ext_modules))): - if ext.name == 'skip_cython': - del ext_modules[i] - - # On Microsoft compilers, we need to pass the '/MANIFEST' - # commandline argument. This was the default on MSVC 9.0, but is - # now required on MSVC 10.0, but it doesn't seem to hurt to add - # it unconditionally. - if get_compiler_option() == 'msvc': - for ext in ext_modules: - ext.extra_link_args.append('/MANIFEST') - - return { - 'ext_modules': ext_modules, - 'packages': packages, - 'package_dir': package_dir, - 'package_data': package_data, - } - - -def iter_setup_packages(srcdir, packages): - """ A generator that finds and imports all of the ``setup_package.py`` - modules in the source packages. - - Returns - ------- - modgen : generator - A generator that yields (modname, mod), where `mod` is the module and - `modname` is the module name for the ``setup_package.py`` modules. - - """ - - for packagename in packages: - package_parts = packagename.split('.') - package_path = os.path.join(srcdir, *package_parts) - setup_package = os.path.relpath( - os.path.join(package_path, 'setup_package.py')) - - if os.path.isfile(setup_package): - module = import_file(setup_package, - name=packagename + '.setup_package') - yield module - - -def iter_pyx_files(package_dir, package_name): - """ - A generator that yields Cython source files (ending in '.pyx') in the - source packages. - - Returns - ------- - pyxgen : generator - A generator that yields (extmod, fullfn) where `extmod` is the - full name of the module that the .pyx file would live in based - on the source directory structure, and `fullfn` is the path to - the .pyx file. - """ - for dirpath, dirnames, filenames in walk_skip_hidden(package_dir): - for fn in filenames: - if fn.endswith('.pyx'): - fullfn = os.path.relpath(os.path.join(dirpath, fn)) - # Package must match file name - extmod = '.'.join([package_name, fn[:-4]]) - yield (extmod, fullfn) - - break # Don't recurse into subdirectories - - -def get_cython_extensions(srcdir, packages, prevextensions=tuple(), - extincludedirs=None): - """ - Looks for Cython files and generates Extensions if needed. - - Parameters - ---------- - srcdir : str - Path to the root of the source directory to search. - prevextensions : list of `~distutils.core.Extension` objects - The extensions that are already defined. Any .pyx files already here - will be ignored. - extincludedirs : list of str or None - Directories to include as the `include_dirs` argument to the generated - `~distutils.core.Extension` objects. - - Returns - ------- - exts : list of `~distutils.core.Extension` objects - The new extensions that are needed to compile all .pyx files (does not - include any already in `prevextensions`). - """ - - # Vanilla setuptools and old versions of distribute include Cython files - # as .c files in the sources, not .pyx, so we cannot simply look for - # existing .pyx sources in the previous sources, but we should also check - # for .c files with the same remaining filename. So we look for .pyx and - # .c files, and we strip the extension. - prevsourcepaths = [] - ext_modules = [] - - for ext in prevextensions: - for s in ext.sources: - if s.endswith(('.pyx', '.c', '.cpp')): - sourcepath = os.path.realpath(os.path.splitext(s)[0]) - prevsourcepaths.append(sourcepath) - - for package_name in packages: - package_parts = package_name.split('.') - package_path = os.path.join(srcdir, *package_parts) - - for extmod, pyxfn in iter_pyx_files(package_path, package_name): - sourcepath = os.path.realpath(os.path.splitext(pyxfn)[0]) - if sourcepath not in prevsourcepaths: - ext_modules.append(Extension(extmod, [pyxfn], - include_dirs=extincludedirs)) - - return ext_modules - - -class DistutilsExtensionArgs(collections.defaultdict): - """ - A special dictionary whose default values are the empty list. - - This is useful for building up a set of arguments for - `distutils.Extension` without worrying whether the entry is - already present. - """ - def __init__(self, *args, **kwargs): - def default_factory(): - return [] - - super(DistutilsExtensionArgs, self).__init__( - default_factory, *args, **kwargs) - - def update(self, other): - for key, val in other.items(): - self[key].extend(val) - - -def pkg_config(packages, default_libraries, executable='pkg-config'): - """ - Uses pkg-config to update a set of distutils Extension arguments - to include the flags necessary to link against the given packages. - - If the pkg-config lookup fails, default_libraries is applied to - libraries. - - Parameters - ---------- - packages : list of str - A list of pkg-config packages to look up. - - default_libraries : list of str - A list of library names to use if the pkg-config lookup fails. - - Returns - ------- - config : dict - A dictionary containing keyword arguments to - `distutils.Extension`. These entries include: - - - ``include_dirs``: A list of include directories - - ``library_dirs``: A list of library directories - - ``libraries``: A list of libraries - - ``define_macros``: A list of macro defines - - ``undef_macros``: A list of macros to undefine - - ``extra_compile_args``: A list of extra arguments to pass to - the compiler - """ - - flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries', - '-D': 'define_macros', '-U': 'undef_macros'} - command = "{0} --libs --cflags {1}".format(executable, ' '.join(packages)), - - result = DistutilsExtensionArgs() - - try: - pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) - output = pipe.communicate()[0].strip() - except subprocess.CalledProcessError as e: - lines = [ - ("{0} failed. This may cause the build to fail below." - .format(executable)), - " command: {0}".format(e.cmd), - " returncode: {0}".format(e.returncode), - " output: {0}".format(e.output) - ] - log.warn('\n'.join(lines)) - result['libraries'].extend(default_libraries) - else: - if pipe.returncode != 0: - lines = [ - "pkg-config could not lookup up package(s) {0}.".format( - ", ".join(packages)), - "This may cause the build to fail below." - ] - log.warn('\n'.join(lines)) - result['libraries'].extend(default_libraries) - else: - for token in output.split(): - # It's not clear what encoding the output of - # pkg-config will come to us in. It will probably be - # some combination of pure ASCII (for the compiler - # flags) and the filesystem encoding (for any argument - # that includes directories or filenames), but this is - # just conjecture, as the pkg-config documentation - # doesn't seem to address it. - arg = token[:2].decode('ascii') - value = token[2:].decode(sys.getfilesystemencoding()) - if arg in flag_map: - if arg == '-D': - value = tuple(value.split('=', 1)) - result[flag_map[arg]].append(value) - else: - result['extra_compile_args'].append(value) - - return result - - -def add_external_library(library): - """ - Add a build option for selecting the internal or system copy of a library. - - Parameters - ---------- - library : str - The name of the library. If the library is `foo`, the build - option will be called `--use-system-foo`. - """ - - for command in ['build', 'build_ext', 'install']: - add_command_option(command, str('use-system-' + library), - 'Use the system {0} library'.format(library), - is_bool=True) - - -def use_system_library(library): - """ - Returns `True` if the build configuration indicates that the given - library should use the system copy of the library rather than the - internal one. - - For the given library `foo`, this will be `True` if - `--use-system-foo` or `--use-system-libraries` was provided at the - commandline or in `setup.cfg`. - - Parameters - ---------- - library : str - The name of the library - - Returns - ------- - use_system : bool - `True` if the build should use the system copy of the library. - """ - return ( - get_distutils_build_or_install_option('use_system_{0}'.format(library)) or - get_distutils_build_or_install_option('use_system_libraries')) - - -@extends_doc(_find_packages) -def find_packages(where='.', exclude=(), invalidate_cache=False): - """ - This version of ``find_packages`` caches previous results to speed up - subsequent calls. Use ``invalide_cache=True`` to ignore cached results - from previous ``find_packages`` calls, and repeat the package search. - """ - - if exclude: - warnings.warn( - "Use of the exclude parameter is no longer supported since it does " - "not work as expected. Use add_exclude_packages instead. Note that " - "it must be called prior to any other calls from setup helpers.", - AstropyDeprecationWarning) - - # Calling add_exclude_packages after this point will have no effect - _module_state['excludes_too_late'] = True - - if not invalidate_cache and _module_state['package_cache'] is not None: - return _module_state['package_cache'] - - packages = _find_packages( - where=where, exclude=list(_module_state['exclude_packages'])) - _module_state['package_cache'] = packages - - return packages - - -class FakeBuildSphinx(Command): - """ - A dummy build_sphinx command that is called if Sphinx is not - installed and displays a relevant error message - """ - - # user options inherited from sphinx.setup_command.BuildDoc - user_options = [ - ('fresh-env', 'E', ''), - ('all-files', 'a', ''), - ('source-dir=', 's', ''), - ('build-dir=', None, ''), - ('config-dir=', 'c', ''), - ('builder=', 'b', ''), - ('project=', None, ''), - ('version=', None, ''), - ('release=', None, ''), - ('today=', None, ''), - ('link-index', 'i', '')] - - # user options appended in astropy.setup_helpers.AstropyBuildSphinx - user_options.append(('warnings-returncode', 'w', '')) - user_options.append(('clean-docs', 'l', '')) - user_options.append(('no-intersphinx', 'n', '')) - user_options.append(('open-docs-in-browser', 'o', '')) - - def initialize_options(self): - try: - raise RuntimeError("Sphinx and its dependencies must be installed " - "for build_docs.") - except: - log.error('error: Sphinx and its dependencies must be installed ' - 'for build_docs.') - sys.exit(1) diff --git a/astropy_helpers/sphinx/__init__.py b/astropy_helpers/sphinx/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/astropy_helpers/sphinx/conf.py b/astropy_helpers/sphinx/conf.py deleted file mode 100644 index 97c7afdf..00000000 --- a/astropy_helpers/sphinx/conf.py +++ /dev/null @@ -1,5 +0,0 @@ -import warnings - -from sphinx_astropy.conf import * - -warnings.warn("Note that astropy_helpers.sphinx.conf is deprecated - use sphinx_astropy.conf instead") diff --git a/astropy_helpers/tests/test_ah_bootstrap.py b/astropy_helpers/tests/test_ah_bootstrap.py deleted file mode 100644 index dca0a2f8..00000000 --- a/astropy_helpers/tests/test_ah_bootstrap.py +++ /dev/null @@ -1,442 +0,0 @@ -# -*- coding: utf-8 -*- - -import glob -import os -import json -import textwrap - -from distutils.version import LooseVersion - -import setuptools - -import pytest - -from . import reset_setup_helpers, reset_distutils_log # noqa -from . import run_cmd, run_setup, testpackage, create_testpackage -from ..utils import silence - - -TEST_SETUP_PY = """\ -#!/usr/bin/env python - -import os -import sys - -# This import is not the real run of ah_bootstrap for the purposes of the test, -# so we need to preserve the command-line arguments otherwise these get eaten -# up by this import -args = sys.argv[:] -import ah_bootstrap -sys.argv = args - -{extra} - -# reset the name of the package installed by ah_boostrap to -# _astropy_helpers_test_--this will prevent any confusion by pkg_resources with -# any already installed packages named astropy_helpers -# We also disable auto-upgrade by default -ah_bootstrap.DIST_NAME = 'astropy-helpers-test' -ah_bootstrap.PACKAGE_NAME = '_astropy_helpers_test_' -ah_bootstrap.AUTO_UPGRADE = False -ah_bootstrap.DOWNLOAD_IF_NEEDED = False -try: - ah_bootstrap.BOOTSTRAPPER = ah_bootstrap._Bootstrapper.main() - ah_bootstrap.use_astropy_helpers({args}) -finally: - ah_bootstrap.DIST_NAME = 'astropy-helpers' - ah_bootstrap.PACKAGE_NAME = 'astropy_helpers' - ah_bootstrap.AUTO_UPGRADE = True - ah_bootstrap.DOWNLOAD_IF_NEEDED = True - -# Kind of a hacky way to do this, but this assertion is specifically -# for test_check_submodule_no_git -# TODO: Rework the tests in this module so that it's easier to test specific -# behaviors of ah_bootstrap for each test -assert '--no-git' not in sys.argv - -import _astropy_helpers_test_ -filename = os.path.abspath(_astropy_helpers_test_.__file__) -filename = filename.replace('.pyc', '.py') # More consistent this way - -# We print out variables that are needed in tests below in JSON -import json -data = {{}} -data['filename'] = filename -data['ah_bootstrap.BOOTSTRAPPER.use_git'] = ah_bootstrap.BOOTSTRAPPER.use_git -print(json.dumps(data)) -""" - -AH_BOOTSTRAP_FILE = os.path.join(os.path.dirname(__file__), '..', '..', 'ah_bootstrap.py') - -with open(AH_BOOTSTRAP_FILE) as f: - AH_BOOTSTRAP = f.read() - -# The behavior checked in some of the tests depends on the version of -# setuptools -try: - # We need to use LooseVersion here instead of StrictVersion since developer - # versions of setuptools ('35.0.2.post20170530') don't satisfy the - # StrictVersion criteria even though they satisfy PEP440 - SETUPTOOLS_VERSION = LooseVersion(setuptools.__version__).version -except: - # Broken setuptools? ¯\_(ツ)_/¯ - SETUPTOOLS_VERSION = (0, 0, 0) - - -def test_bootstrap_from_submodule(tmpdir, testpackage, capsys): - """ - Tests importing _astropy_helpers_test_ from a submodule in a git - repository. This tests actually performing a fresh clone of the repository - without the submodule initialized, and that importing astropy_helpers in - that context works transparently after calling - `ah_boostrap.use_astropy_helpers`. - """ - - orig_repo = tmpdir.mkdir('orig') - - with orig_repo.as_cwd(): - - run_cmd('git', ['init']) - - orig_repo.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - run_cmd('git', ['add', 'ah_bootstrap.py']) - - # Write a test setup.py that uses ah_bootstrap; it also ensures that - # any previous reference to astropy_helpers is first wiped from - # sys.modules - orig_repo.join('setup.py').write(TEST_SETUP_PY.format(args='', extra='')) - run_cmd('git', ['add', 'setup.py']) - - # Add our own clone of the astropy_helpers repo as a submodule named - # astropy_helpers - run_cmd('git', ['submodule', 'add', str(testpackage), - '_astropy_helpers_test_']) - - run_cmd('git', ['commit', '-m', 'test repository']) - - os.chdir(str(tmpdir)) - - # Creates a clone of our test repo in the directory 'clone' - run_cmd('git', ['clone', 'orig', 'clone']) - - os.chdir('clone') - - run_setup('setup.py', []) - - stdout, stderr = capsys.readouterr() - path = json.loads(stdout.strip())['filename'] - - # Ensure that the astropy_helpers used by the setup.py is the one that - # was imported from git submodule - a = os.path.normcase(path) - b = os.path.normcase(str(tmpdir.join('clone', '_astropy_helpers_test_', - '_astropy_helpers_test_', - '__init__.py'))) - assert a == b - - -def test_bootstrap_from_submodule_no_locale(tmpdir, testpackage, capsys, - monkeypatch): - """ - Regression test for https://github.com/astropy/astropy/issues/2749 - - Runs test_bootstrap_from_submodule but with missing locale/language - settings. - """ - - for varname in ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'): - monkeypatch.delenv(varname, raising=False) - - test_bootstrap_from_submodule(tmpdir, testpackage, capsys) - - -def test_bootstrap_from_submodule_bad_locale(tmpdir, testpackage, capsys, - monkeypatch): - """ - Additional regression test for - https://github.com/astropy/astropy/issues/2749 - """ - - for varname in ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'): - monkeypatch.delenv(varname, raising=False) - - # Test also with bad LC_CTYPE a la http://bugs.python.org/issue18378 - monkeypatch.setenv('LC_CTYPE', 'UTF-8') - - test_bootstrap_from_submodule(tmpdir, testpackage, capsys) - - -UPDATE_ERROR_PATCH = """ -class UpgradeError(Exception): - pass - -def _do_upgrade(*args, **kwargs): - raise UpgradeError() - -ah_bootstrap._Bootstrapper._do_upgrade = _do_upgrade -""" - - -def test_check_submodule_no_git(capsys, tmpdir, testpackage): - """ - Tests that when importing astropy_helpers from a submodule, it is still - recognized as a submodule even when using the --no-git option. - - In particular this ensures that the auto-upgrade feature is not activated. - """ - - orig_repo = tmpdir.mkdir('orig') - - with orig_repo.as_cwd(): - - orig_repo.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - - run_cmd('git', ['init']) - - # Write a test setup.py that uses ah_bootstrap; it also ensures that - # any previous reference to astropy_helpers is first wiped from - # sys.modules - args = 'auto_upgrade=True' - orig_repo.join('setup.py').write(TEST_SETUP_PY.format(args=args, extra=UPDATE_ERROR_PATCH)) - run_cmd('git', ['add', 'setup.py']) - - # Add our own clone of the astropy_helpers repo as a submodule named - # astropy_helpers - run_cmd('git', ['submodule', 'add', str(testpackage), - '_astropy_helpers_test_']) - - run_cmd('git', ['commit', '-m', 'test repository']) - - run_setup('setup.py', ['--no-git']) - - stdout, stderr = capsys.readouterr() - - use_git = bool(json.loads(stdout.strip())['ah_bootstrap.BOOTSTRAPPER.use_git']) - - if 'UpgradeError' in stderr: - pytest.fail('Attempted to run auto-upgrade despite importing ' - '_astropy_helpers_test_ from a git submodule') - - # Ensure that the no-git option was in fact set - assert not use_git - - -def test_bootstrap_from_directory(tmpdir, testpackage, capsys): - """ - Tests simply bundling a copy of the astropy_helpers source code in its - entirety bundled directly in the source package and not in an archive. - """ - - source = tmpdir.mkdir('source') - testpackage.copy(source.join('_astropy_helpers_test_')) - - with source.as_cwd(): - - source.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - - source.join('setup.py').write(TEST_SETUP_PY.format(args='', extra='')) - run_setup('setup.py', []) - stdout, stderr = capsys.readouterr() - path = json.loads(stdout.strip())['filename'] - - # Ensure that the astropy_helpers used by the setup.py is the one that - # was imported from git submodule - a = os.path.normcase(path) - b = os.path.normcase(str(source.join('_astropy_helpers_test_', - '_astropy_helpers_test_', - '__init__.py'))) - assert a == b - - -def test_bootstrap_from_archive(tmpdir, testpackage, capsys): - """ - Tests importing _astropy_helpers_test_ from a .tar.gz source archive - shipped alongside the package that uses it. - """ - - orig_repo = tmpdir.mkdir('orig') - - # Make a source distribution of the test package - with silence(): - run_setup(str(testpackage.join('setup.py')), - ['sdist', '--dist-dir=dist', '--formats=gztar']) - - dist_dir = testpackage.join('dist') - for dist_file in dist_dir.visit('*.tar.gz'): - dist_file.copy(orig_repo) - - with orig_repo.as_cwd(): - - orig_repo.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - - # Write a test setup.py that uses ah_bootstrap; it also ensures that - # any previous reference to astropy_helpers is first wiped from - # sys.modules - args = 'path={0!r}'.format(os.path.basename(str(dist_file))) - orig_repo.join('setup.py').write(TEST_SETUP_PY.format(args=args, extra='')) - - run_setup('setup.py', []) - - stdout, stderr = capsys.readouterr() - path = json.loads(stdout.strip())['filename'] - - # Installation from the .tar.gz should have resulted in a .egg - # directory that the _astropy_helpers_test_ package was imported from - eggs = _get_local_eggs() - assert eggs - egg = orig_repo.join(eggs[0]) - assert os.path.isdir(str(egg)) - - a = os.path.normcase(path) - b = os.path.normcase(str(egg.join('_astropy_helpers_test_', - '__init__.py'))) - - assert a == b - - -def test_download_if_needed(tmpdir, testpackage, capsys): - """ - Tests the case where astropy_helpers was not actually included in a - package, or is otherwise missing, and we need to "download" it. - - This does not test actually downloading from the internet--this is normally - done through setuptools' easy_install command which can also install from a - source archive. From the point of view of ah_boostrap the two actions are - equivalent, so we can just as easily simulate this by providing a setup.cfg - giving the path to a source archive to "download" (as though it were a - URL). - """ - - source = tmpdir.mkdir('source') - - # Ensure ah_bootstrap is imported from the local directory - import ah_bootstrap # noqa - - # Make a source distribution of the test package - with silence(): - run_setup(str(testpackage.join('setup.py')), - ['sdist', '--dist-dir=dist', '--formats=gztar']) - - dist_dir = testpackage.join('dist') - - with source.as_cwd(): - - source.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - - source.join('setup.py').write(TEST_SETUP_PY.format( - args='download_if_needed=True', extra='')) - source.join('setup.cfg').write(textwrap.dedent("""\ - [easy_install] - find_links = {find_links} - """.format(find_links=str(dist_dir)))) - - run_setup('setup.py', []) - - stdout, stderr = capsys.readouterr() - path = json.loads(stdout.strip())['filename'] - - # easy_install should have worked by 'installing' astropy_helpers as a - # .egg in the current directory - eggs = _get_local_eggs() - assert eggs - egg = source.join(eggs[0]) - assert os.path.isdir(str(egg)) - - a = os.path.normcase(path) - b = os.path.normcase(str(egg.join('_astropy_helpers_test_', - '__init__.py'))) - assert a == b - - -EXTRA_PACKAGE_INDEX = """ -from setuptools.package_index import PackageIndex - -class FakePackageIndex(PackageIndex): - def __init__(self, *args, **kwargs): - PackageIndex.__init__(self, *args, **kwargs) - self.to_scan = {dists} - - def find_packages(self, requirement): - # no-op - pass - -ah_bootstrap.PackageIndex = FakePackageIndex -""" - - -def test_upgrade(tmpdir, capsys): - orig_dir = create_testpackage(tmpdir.mkdir('orig')) - - # Make a test package that uses _astropy_helpers_test_ - source = tmpdir.mkdir('source') - dist_dir = source.mkdir('dists') - orig_dir.copy(source.join('_astropy_helpers_test_')) - - with source.as_cwd(): - - source.join('ah_bootstrap.py').write(AH_BOOTSTRAP) - - setup_py = TEST_SETUP_PY.format(args='auto_upgrade=True', extra='') - source.join('setup.py').write(setup_py) - - # This will be used to later to fake downloading the upgrade package - source.join('setup.cfg').write(textwrap.dedent("""\ - [easy_install] - find_links = {find_links} - """.format(find_links=str(dist_dir)))) - - # Make additional "upgrade" versions of the _astropy_helpers_test_ - # package--one of them is version 0.2 and the other is version 0.1.1. The - # auto-upgrade should ignore version 0.2 but use version 0.1.1. - upgrade_dir_1 = create_testpackage(tmpdir.mkdir('upgrade_1'), version='0.2') - upgrade_dir_2 = create_testpackage(tmpdir.mkdir('upgrade_2'), version='0.1.1') - - dists = [] - # For each upgrade package go ahead and build a source distribution of it - # and copy that source distribution to a dist directory we'll use later to - # simulate a 'download' - for upgrade_dir in [upgrade_dir_1, upgrade_dir_2]: - with silence(): - run_setup(str(upgrade_dir.join('setup.py')), - ['sdist', '--dist-dir=dist', '--formats=gztar']) - dists.append(str(upgrade_dir.join('dist'))) - for dist_file in upgrade_dir.visit('*.tar.gz'): - dist_file.copy(source.join('dists')) - - with source.as_cwd(): - - setup_py = TEST_SETUP_PY.format(args='auto_upgrade=True', - extra=EXTRA_PACKAGE_INDEX.format(dists=dists)) - source.join('setup.py').write(setup_py) - - # Now run the source setup.py; this test is similar to - # test_download_if_needed, but we explicitly check that the correct - # *version* of _astropy_helpers_test_ was used - run_setup('setup.py', []) - - stdout, stderr = capsys.readouterr() - path = json.loads(stdout.strip())['filename'] - eggs = _get_local_eggs() - assert eggs - - egg = source.join(eggs[0]) - assert os.path.isdir(str(egg)) - a = os.path.normcase(path) - b = os.path.normcase(str(egg.join('_astropy_helpers_test_', - '__init__.py'))) - assert a == b - assert 'astropy_helpers_test-0.1.1-' in str(egg) - - -def _get_local_eggs(path='.'): - """ - Helper utility used by some tests to get the list of egg archive files - in a local directory. - """ - - if SETUPTOOLS_VERSION[0] >= 7: - eggs = glob.glob(os.path.join(path, '.eggs', '*.egg')) - else: - eggs = glob.glob('*.egg') - - return eggs diff --git a/astropy_helpers/tests/test_git_helpers.py b/astropy_helpers/tests/test_git_helpers.py deleted file mode 100644 index 6b826fc9..00000000 --- a/astropy_helpers/tests/test_git_helpers.py +++ /dev/null @@ -1,300 +0,0 @@ -import glob -import imp -import os -import pkgutil -import re -import sys -import tarfile - -import pytest -from warnings import catch_warnings - -from . import reset_setup_helpers, reset_distutils_log # noqa -from . import run_cmd, run_setup, cleanup_import -from astropy_helpers.git_helpers import get_git_devstr - -_DEV_VERSION_RE = re.compile(r'\d+\.\d+(?:\.\d+)?\.dev(\d+)') - -ASTROPY_HELPERS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) - -TEST_VERSION_SETUP_PY_OLDSTYLE = """\ -#!/usr/bin/env python - -import sys -from setuptools import setup - -NAME = 'apyhtest_eva' -VERSION = {version!r} -RELEASE = 'dev' not in VERSION - -sys.path.insert(0, r'{astropy_helpers_path}') - -from astropy_helpers.git_helpers import get_git_devstr -from astropy_helpers.version_helpers import generate_version_py - -if not RELEASE: - VERSION += get_git_devstr(False) - -generate_version_py(NAME, VERSION, RELEASE, False, uses_git=not RELEASE) - -setup(name=NAME, version=VERSION, packages=['apyhtest_eva']) -""" - -TEST_VERSION_SETUP_CFG = """\ -[metadata] -name = apyhtest_eva -version = {version} -""" - -TEST_VERSION_SETUP_PY_NEWSTYLE = """\ -#!/usr/bin/env python - -import sys -sys.path.insert(0, r'{astropy_helpers_path}') - -from astropy_helpers.setup_helpers import setup -setup() -""" - - -TEST_VERSION_INIT = """\ -try: - from .version import version as __version__ - from .version import githash as __githash__ -except ImportError: - __version__ = __githash__ = '' -""" - - -@pytest.fixture(params=["oldstyle", "newstyle"]) -def version_test_package(tmpdir, request): - - # We test both the old-style syntax of deermining VERSION, RELEASE, etc. - # inside the setup.py, and the new style of getting these from the setup.cfg - # file. - - def make_test_package_oldstyle(version='42.42.dev'): - test_package = tmpdir.mkdir('test_package') - test_package.join('setup.py').write( - TEST_VERSION_SETUP_PY_OLDSTYLE.format(version=version, - astropy_helpers_path=ASTROPY_HELPERS_PATH)) - test_package.mkdir('apyhtest_eva').join('__init__.py').write(TEST_VERSION_INIT) - with test_package.as_cwd(): - run_cmd('git', ['init']) - run_cmd('git', ['add', '--all']) - run_cmd('git', ['commit', '-m', 'test package']) - - if '' in sys.path: - sys.path.remove('') - - sys.path.insert(0, '') - - def finalize(): - cleanup_import('apyhtest_eva') - - request.addfinalizer(finalize) - - return test_package - - def make_test_package_newstyle(version='42.42.dev'): - test_package = tmpdir.mkdir('test_package') - test_package.join('setup.cfg').write( - TEST_VERSION_SETUP_CFG.format(version=version)) - - test_package.join('setup.py').write( - TEST_VERSION_SETUP_PY_NEWSTYLE.format(astropy_helpers_path=ASTROPY_HELPERS_PATH)) - - test_package.mkdir('apyhtest_eva').join('__init__.py').write(TEST_VERSION_INIT) - with test_package.as_cwd(): - run_cmd('git', ['init']) - run_cmd('git', ['add', '--all']) - run_cmd('git', ['commit', '-m', 'test package']) - - if '' in sys.path: - sys.path.remove('') - - sys.path.insert(0, '') - - def finalize(): - cleanup_import('apyhtest_eva') - - request.addfinalizer(finalize) - - return test_package - - if request.param == 'oldstyle': - return make_test_package_oldstyle - else: - return make_test_package_newstyle - - -def test_update_git_devstr(version_test_package, capsys): - """Tests that the commit number in the package's version string updates - after git commits even without re-running setup.py. - """ - - # We have to call version_test_package to actually create the package - test_pkg = version_test_package() - - with test_pkg.as_cwd(): - run_setup('setup.py', ['--version']) - - stdout, stderr = capsys.readouterr() - version = stdout.strip() - - m = _DEV_VERSION_RE.match(version) - assert m, ( - "Stdout did not match the version string pattern:" - "\n\n{0}\n\nStderr:\n\n{1}".format(stdout, stderr)) - revcount = int(m.group(1)) - - import apyhtest_eva - assert apyhtest_eva.__version__ == version - - # Make a silly git commit - with open('.test', 'w'): - pass - - run_cmd('git', ['add', '.test']) - run_cmd('git', ['commit', '-m', 'test']) - - import apyhtest_eva.version - imp.reload(apyhtest_eva.version) - - # Previously this checked packagename.__version__, but in order for that to - # be updated we also have to re-import _astropy_init which could be tricky. - # Checking directly that the packagename.version module was updated is - # sufficient: - m = _DEV_VERSION_RE.match(apyhtest_eva.version.version) - assert m - assert int(m.group(1)) == revcount + 1 - - # This doesn't test astropy_helpers.get_helpers.update_git_devstr directly - # since a copy of that function is made in packagename.version (so that it - # can work without astropy_helpers installed). In order to get test - # coverage on the actual astropy_helpers copy of that function just call it - # directly and compare to the value in packagename - from astropy_helpers.git_helpers import update_git_devstr - - newversion = update_git_devstr(version, path=str(test_pkg)) - assert newversion == apyhtest_eva.version.version - - -def test_version_update_in_other_repos(version_test_package, tmpdir): - """ - Regression test for https://github.com/astropy/astropy-helpers/issues/114 - and for https://github.com/astropy/astropy-helpers/issues/107 - """ - - test_pkg = version_test_package() - - with test_pkg.as_cwd(): - run_setup('setup.py', ['build']) - - # Add the path to the test package to sys.path for now - sys.path.insert(0, str(test_pkg)) - try: - import apyhtest_eva - m = _DEV_VERSION_RE.match(apyhtest_eva.__version__) - assert m - correct_revcount = int(m.group(1)) - - with tmpdir.as_cwd(): - testrepo = tmpdir.mkdir('testrepo') - testrepo.chdir() - # Create an empty git repo - run_cmd('git', ['init']) - - import apyhtest_eva.version - imp.reload(apyhtest_eva.version) - m = _DEV_VERSION_RE.match(apyhtest_eva.version.version) - assert m - assert int(m.group(1)) == correct_revcount - correct_revcount = int(m.group(1)) - - # Add several commits--more than the revcount for the apyhtest_eva package - for idx in range(correct_revcount + 5): - test_filename = '.test' + str(idx) - testrepo.ensure(test_filename) - run_cmd('git', ['add', test_filename]) - run_cmd('git', ['commit', '-m', 'A message']) - - import apyhtest_eva.version - imp.reload(apyhtest_eva.version) - m = _DEV_VERSION_RE.match(apyhtest_eva.version.version) - assert m - assert int(m.group(1)) == correct_revcount - correct_revcount = int(m.group(1)) - finally: - sys.path.remove(str(test_pkg)) - - -@pytest.mark.parametrize('version', ['1.0.dev', '1.0']) -def test_installed_git_version(version_test_package, version, tmpdir, capsys): - """ - Test for https://github.com/astropy/astropy-helpers/issues/87 - - Ensures that packages installed with astropy_helpers have a correct copy - of the git hash of the installed commit. - """ - - # To test this, it should suffice to build a source dist, unpack it - # somewhere outside the git repository, and then do a build and import - # from the build directory--no need to "install" as such - - test_pkg = version_test_package(version) - - with test_pkg.as_cwd(): - run_setup('setup.py', ['build']) - - try: - import apyhtest_eva - githash = apyhtest_eva.__githash__ - assert githash and isinstance(githash, str) - # Ensure that it does in fact look like a git hash and not some - # other arbitrary string - assert re.match(r'[0-9a-f]{40}', githash) - finally: - cleanup_import('apyhtest_eva') - - run_setup('setup.py', ['sdist', '--dist-dir=dist', '--formats=gztar']) - - tgzs = glob.glob(os.path.join('dist', '*.tar.gz')) - assert len(tgzs) == 1 - - tgz = test_pkg.join(tgzs[0]) - - build_dir = tmpdir.mkdir('build_dir') - tf = tarfile.open(str(tgz), mode='r:gz') - tf.extractall(str(build_dir)) - - with build_dir.as_cwd(): - pkg_dir = glob.glob('apyhtest_eva-*')[0] - os.chdir(pkg_dir) - - with catch_warnings(record=True) as w: - run_setup('setup.py', ['build']) - - try: - import apyhtest_eva - loader = pkgutil.get_loader('apyhtest_eva') - # Ensure we are importing the 'packagename' that was just unpacked - # into the build_dir - assert loader.get_filename().startswith(str(build_dir)) - assert apyhtest_eva.__githash__ == githash - finally: - cleanup_import('apyhtest_eva') - - -def test_get_git_devstr(tmpdir): - dirpath = str(tmpdir) - warn_msg = "No git repository present at" - # Verify as much as possible, but avoid dealing with paths on windows - if not sys.platform.startswith('win'): - warn_msg += " '{}'".format(dirpath) - - with catch_warnings(record=True) as w: - devstr = get_git_devstr(path=dirpath) - assert devstr == '0' - assert len(w) == 1 - assert str(w[0].message).startswith(warn_msg) diff --git a/astropy_helpers/tests/test_setup_helpers.py b/astropy_helpers/tests/test_setup_helpers.py deleted file mode 100644 index ab2ae9c0..00000000 --- a/astropy_helpers/tests/test_setup_helpers.py +++ /dev/null @@ -1,435 +0,0 @@ -import os -import sys -import importlib - -import pytest - -from textwrap import dedent - -from ..setup_helpers import get_package_info, register_commands - -from . import reset_setup_helpers, reset_distutils_log # noqa -from . import run_setup, cleanup_import - -ASTROPY_HELPERS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) # noqa - - -def teardown_module(module): - # Remove file generated by test_generate_openmp_enabled_py but - # somehow needed in test_cython_autoextensions - tmpfile = 'openmp_enabled.py' - if os.path.exists(tmpfile): - os.remove(tmpfile) - - -def _extension_test_package(tmpdir, request, extension_type='c', - include_numpy=False): - """Creates a simple test package with an extension module.""" - - test_pkg = tmpdir.mkdir('test_pkg') - test_pkg.mkdir('apyhtest_eva').ensure('__init__.py') - - # TODO: It might be later worth making this particular test package into a - # reusable fixture for other build_ext tests - - if extension_type in ('c', 'both'): - # A minimal C extension for testing - test_pkg.join('apyhtest_eva', 'unit01.c').write(dedent("""\ - #include - - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "unit01", - NULL, - -1, - NULL - }; - PyMODINIT_FUNC - PyInit_unit01(void) { - return PyModule_Create(&moduledef); - } - """)) - - if extension_type in ('pyx', 'both'): - # A minimal Cython extension for testing - test_pkg.join('apyhtest_eva', 'unit02.pyx').write(dedent("""\ - print("Hello cruel angel.") - """)) - - if extension_type == 'c': - extensions = ['unit01.c'] - elif extension_type == 'pyx': - extensions = ['unit02.pyx'] - elif extension_type == 'both': - extensions = ['unit01.c', 'unit02.pyx'] - - include_dirs = ['numpy'] if include_numpy else [] - - extensions_list = [ - "Extension('apyhtest_eva.{0}', [join('apyhtest_eva', '{1}')], include_dirs={2})".format( - os.path.splitext(extension)[0], extension, include_dirs) - for extension in extensions] - - test_pkg.join('apyhtest_eva', 'setup_package.py').write(dedent("""\ - from setuptools import Extension - from os.path import join - def get_extensions(): - return [{0}] - """.format(', '.join(extensions_list)))) - - test_pkg.join('setup.py').write(dedent("""\ - import sys - from os.path import join - from setuptools import setup - sys.path.insert(0, r'{astropy_helpers_path}') - from astropy_helpers.setup_helpers import register_commands - from astropy_helpers.setup_helpers import get_package_info - from astropy_helpers.version_helpers import generate_version_py - - if '--no-cython' in sys.argv: - from astropy_helpers.commands import build_ext - build_ext.should_build_with_cython = lambda *args: False - sys.argv.remove('--no-cython') - - NAME = 'apyhtest_eva' - VERSION = '0.1' - RELEASE = True - - cmdclassd = register_commands(NAME, VERSION, RELEASE) - generate_version_py(NAME, VERSION, RELEASE, False, False) - package_info = get_package_info() - - setup( - name=NAME, - version=VERSION, - cmdclass=cmdclassd, - **package_info - ) - """.format(astropy_helpers_path=ASTROPY_HELPERS_PATH))) - - if '' in sys.path: - sys.path.remove('') - - sys.path.insert(0, '') - - def finalize(): - cleanup_import('apyhtest_eva') - - request.addfinalizer(finalize) - - return test_pkg - - -@pytest.fixture -def extension_test_package(tmpdir, request): - return _extension_test_package(tmpdir, request, extension_type='both') - - -@pytest.fixture -def c_extension_test_package(tmpdir, request): - # Check whether numpy is installed in the test environment - has_numpy = bool(importlib.util.find_spec('numpy')) - return _extension_test_package(tmpdir, request, extension_type='c', - include_numpy=has_numpy) - - -@pytest.fixture -def pyx_extension_test_package(tmpdir, request): - return _extension_test_package(tmpdir, request, extension_type='pyx') - - -def test_cython_autoextensions(tmpdir): - """ - Regression test for https://github.com/astropy/astropy-helpers/pull/19 - - Ensures that Cython extensions in sub-packages are discovered and built - only once. - """ - - # Make a simple test package - test_pkg = tmpdir.mkdir('test_pkg') - test_pkg.mkdir('yoda').mkdir('luke') - test_pkg.ensure('yoda', '__init__.py') - test_pkg.ensure('yoda', 'luke', '__init__.py') - test_pkg.join('yoda', 'luke', 'dagobah.pyx').write( - """def testfunc(): pass""") - - # Required, currently, for get_package_info to work - register_commands('yoda', '0.0', False, srcdir=str(test_pkg)) - package_info = get_package_info(str(test_pkg)) - - assert len(package_info['ext_modules']) == 1 - assert package_info['ext_modules'][0].name == 'yoda.luke.dagobah' - - -def test_compiler_module(capsys, c_extension_test_package): - """ - Test ensuring that the compiler module is built and installed for packages - that have extension modules. - """ - - test_pkg = c_extension_test_package - install_temp = test_pkg.mkdir('install_temp') - - with test_pkg.as_cwd(): - # This is one of the simplest ways to install just a package into a - # test directory - run_setup('setup.py', - ['install', - '--single-version-externally-managed', - '--install-lib={0}'.format(install_temp), - '--record={0}'.format(install_temp.join('record.txt'))]) - - stdout, stderr = capsys.readouterr() - assert "No git repository present at" in stderr - - with install_temp.as_cwd(): - import apyhtest_eva - # Make sure we imported the apyhtest_eva package from the correct place - dirname = os.path.abspath(os.path.dirname(apyhtest_eva.__file__)) - assert dirname == str(install_temp.join('apyhtest_eva')) - - import apyhtest_eva.compiler_version - assert apyhtest_eva.compiler_version != 'unknown' - - -def test_no_cython_buildext(capsys, c_extension_test_package, monkeypatch): - """ - Regression test for https://github.com/astropy/astropy-helpers/pull/35 - - This tests the custom build_ext command installed by astropy_helpers when - used with a project that has no Cython extensions (but does have one or - more normal C extensions). - """ - - test_pkg = c_extension_test_package - - with test_pkg.as_cwd(): - - run_setup('setup.py', ['build_ext', '--inplace', '--no-cython']) - - stdout, stderr = capsys.readouterr() - assert "No git repository present at" in stderr - - sys.path.insert(0, str(test_pkg)) - - try: - import apyhtest_eva.unit01 - dirname = os.path.abspath(os.path.dirname(apyhtest_eva.unit01.__file__)) - assert dirname == str(test_pkg.join('apyhtest_eva')) - finally: - sys.path.remove(str(test_pkg)) - - -def test_missing_cython_c_files(capsys, pyx_extension_test_package, - monkeypatch): - """ - Regression test for https://github.com/astropy/astropy-helpers/pull/181 - - Test failure mode when building a package that has Cython modules, but - where Cython is not installed and the generated C files are missing. - """ - - test_pkg = pyx_extension_test_package - - with test_pkg.as_cwd(): - - with pytest.raises(SystemExit): - run_setup('setup.py', ['build_ext', '--inplace', '--no-cython']) - - stdout, stderr = capsys.readouterr() - assert "No git repository present at" in stderr - - msg = ('Could not find C/C++ file {0}' - '.(c/cpp)'.format('apyhtest_eva/unit02'.replace('/', os.sep))) - - assert msg in stderr - - -@pytest.mark.parametrize('mode', ['cli', 'cli-w', 'cli-sphinx', 'cli-l', 'cli-parallel']) -def test_build_docs(capsys, tmpdir, mode): - """ - Test for build_docs - """ - - test_pkg = tmpdir.mkdir('test_pkg') - - test_pkg.mkdir('mypackage') - - test_pkg.join('mypackage').join('__init__.py').write(dedent("""\ - def test_function(): - pass - - class A(): - pass - - class B(A): - pass - """)) - - test_pkg.mkdir('docs') - - docs_dir = test_pkg.join('docs') - docs_dir.join('conf.py').write(dedent("""\ - import warnings - with warnings.catch_warnings(): # ignore matplotlib warning - warnings.simplefilter("ignore") - from sphinx_astropy.conf import * - exclude_patterns.append('_templates') - suppress_warnings = ['app.add_directive', 'app.add_node', 'app.add_role'] - """)) # noqa - - docs_dir.join('index.rst').write(dedent("""\ - .. automodapi:: mypackage - :no-inheritance-diagram: - """)) - - # For this test we try out the new way of calling register_commands without - # arugments, instead getting the information from setup.cfg. - test_pkg.join('setup.cfg').write(dedent(""" - [metadata] - name = mypackage - version = 0.1 - """)) - - test_pkg.join('setup.py').write(dedent("""\ - import sys - sys.path.insert(0, r'{astropy_helpers_path}') - from astropy_helpers.setup_helpers import setup - setup() - """.format(astropy_helpers_path=ASTROPY_HELPERS_PATH))) - - with test_pkg.as_cwd(): - - if mode == 'cli': - run_setup('setup.py', ['build_docs']) - elif mode == 'cli-w': - run_setup('setup.py', ['build_docs', '-w']) - elif mode == 'cli-l': - run_setup('setup.py', ['build_docs', '-l']) - elif mode == 'cli-sphinx': - run_setup('setup.py', ['build_sphinx']) - elif mode == 'cli-parallel': - run_setup('setup.py', ['build_docs', '--parallel=2']) - - assert os.path.exists(docs_dir.join('_build', 'html', 'index.html').strpath) - - -def test_command_hooks(tmpdir, capsys): - """A basic test for pre- and post-command hooks.""" - - test_pkg = tmpdir.mkdir('test_pkg') - test_pkg.mkdir('_welltall_') - test_pkg.join('_welltall_', '__init__.py').ensure() - - # Create a setup_package module with a couple of command hooks in it - test_pkg.join('_welltall_', 'setup_package.py').write(dedent("""\ - def pre_build_hook(cmd_obj): - print('Hello build!') - - def post_build_hook(cmd_obj): - print('Goodbye build!') - - """)) - - # A simple setup.py for the test package--running register_commands should - # discover and enable the command hooks - test_pkg.join('setup.py').write(dedent("""\ - import sys - from os.path import join - from setuptools import setup, Extension - sys.path.insert(0, r'{astropy_helpers_path}') - from astropy_helpers.setup_helpers import register_commands, get_package_info - - NAME = '_welltall_' - VERSION = '0.1' - RELEASE = True - - cmdclassd = register_commands(NAME, VERSION, RELEASE) - - setup( - name=NAME, - version=VERSION, - cmdclass=cmdclassd - ) - """.format(astropy_helpers_path=ASTROPY_HELPERS_PATH))) - - with test_pkg.as_cwd(): - try: - run_setup('setup.py', ['build']) - finally: - cleanup_import('_welltall_') - - stdout, stderr = capsys.readouterr() - want = dedent("""\ - running build - running pre_hook from _welltall_.setup_package for build command - Hello build! - running post_hook from _welltall_.setup_package for build command - Goodbye build! - """).strip() - - assert want in stdout.replace('\r\n', '\n').replace('\r', '\n') - - -def test_invalid_package_exclusion(tmpdir, capsys): - - module_name = 'foobar' - setup_header = dedent("""\ - import sys - from os.path import join - from setuptools import setup, Extension - sys.path.insert(0, r'{astropy_helpers_path}') - from astropy_helpers.setup_helpers import register_commands, \\ - get_package_info, add_exclude_packages - - NAME = {module_name!r} - VERSION = '0.1' - RELEASE = True - - """.format(module_name=module_name, - astropy_helpers_path=ASTROPY_HELPERS_PATH)) - - setup_footer = dedent("""\ - setup( - name=NAME, - version=VERSION, - cmdclass=cmdclassd, - **package_info - ) - """) - - # Test error when using add_package_excludes out of order - error_commands = dedent("""\ - cmdclassd = register_commands(NAME, VERSION, RELEASE) - package_info = get_package_info() - add_exclude_packages(['tests*']) - - """) - - error_pkg = tmpdir.mkdir('error_pkg') - error_pkg.join('setup.py').write( - setup_header + error_commands + setup_footer) - - with error_pkg.as_cwd(): - with pytest.raises(SystemExit): - run_setup('setup.py', ['build']) - - stdout, stderr = capsys.readouterr() - assert "RuntimeError" in stderr - - # Test warning when using deprecated exclude parameter - warn_commands = dedent("""\ - cmdclassd = register_commands(NAME, VERSION, RELEASE) - package_info = get_package_info(exclude=['test*']) - - """) - - warn_pkg = tmpdir.mkdir('warn_pkg') - warn_pkg.join('setup.py').write( - setup_header + warn_commands + setup_footer) - - with warn_pkg.as_cwd(): - run_setup('setup.py', ['build']) - stdout, stderr = capsys.readouterr() - assert 'AstropyDeprecationWarning' in stderr diff --git a/astropy_helpers/tests/test_utils.py b/astropy_helpers/tests/test_utils.py deleted file mode 100644 index ad76e4f5..00000000 --- a/astropy_helpers/tests/test_utils.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from ..utils import find_data_files - - -def test_find_data_files(tmpdir): - - data = tmpdir.mkdir('data') - sub1 = data.mkdir('sub1') - sub2 = data.mkdir('sub2') - sub3 = sub1.mkdir('sub3') - - for directory in (data, sub1, sub2, sub3): - filename = directory.join('data.dat').strpath - with open(filename, 'w') as f: - f.write('test') - - filenames = find_data_files(data.strpath, '**/*.dat') - - filenames = sorted(os.path.relpath(x, data.strpath) for x in filenames) - - assert filenames[0] == os.path.join('data.dat') - assert filenames[1] == os.path.join('sub1', 'data.dat') - assert filenames[2] == os.path.join('sub1', 'sub3', 'data.dat') - assert filenames[3] == os.path.join('sub2', 'data.dat') diff --git a/astropy_helpers/version_helpers.py b/astropy_helpers/version_helpers.py deleted file mode 100644 index 636f9501..00000000 --- a/astropy_helpers/version_helpers.py +++ /dev/null @@ -1,364 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -""" -Utilities for generating the version string for Astropy (or an affiliated -package) and the version.py module, which contains version info for the -package. - -Within the generated astropy.version module, the `major`, `minor`, and `bugfix` -variables hold the respective parts of the version number (bugfix is '0' if -absent). The `release` variable is True if this is a release, and False if this -is a development version of astropy. For the actual version string, use:: - - from astropy.version import version - -or:: - - from astropy import __version__ - -""" - -import datetime -import os -import pkgutil -import sys -import time -import warnings - -from distutils import log -from configparser import ConfigParser - -import pkg_resources - -from . import git_helpers -from .distutils_helpers import is_distutils_display_option -from .git_helpers import get_git_devstr -from .utils import AstropyDeprecationWarning, import_file - -__all__ = ['generate_version_py'] - - -def _version_split(version): - """ - Split a version string into major, minor, and bugfix numbers. If any of - those numbers are missing the default is zero. Any pre/post release - modifiers are ignored. - - Examples - ======== - >>> _version_split('1.2.3') - (1, 2, 3) - >>> _version_split('1.2') - (1, 2, 0) - >>> _version_split('1.2rc1') - (1, 2, 0) - >>> _version_split('1') - (1, 0, 0) - >>> _version_split('') - (0, 0, 0) - """ - - parsed_version = pkg_resources.parse_version(version) - - if hasattr(parsed_version, 'base_version'): - # New version parsing for setuptools >= 8.0 - if parsed_version.base_version: - parts = [int(part) - for part in parsed_version.base_version.split('.')] - else: - parts = [] - else: - parts = [] - for part in parsed_version: - if part.startswith('*'): - # Ignore any .dev, a, b, rc, etc. - break - parts.append(int(part)) - - if len(parts) < 3: - parts += [0] * (3 - len(parts)) - - # In principle a version could have more parts (like 1.2.3.4) but we only - # support .. - return tuple(parts[:3]) - - -# This is used by setup.py to create a new version.py - see that file for -# details. Note that the imports have to be absolute, since this is also used -# by affiliated packages. -_FROZEN_VERSION_PY_TEMPLATE = """ -# Autogenerated by {packagetitle}'s setup.py on {timestamp!s} UTC -import datetime - -{header} - -major = {major} -minor = {minor} -bugfix = {bugfix} - -version_info = (major, minor, bugfix) - -release = {rel} -timestamp = {timestamp!r} -debug = {debug} - -astropy_helpers_version = "{ahver}" -"""[1:] - - -_FROZEN_VERSION_PY_WITH_GIT_HEADER = """ -{git_helpers} - - -_packagename = "{packagename}" -_last_generated_version = "{verstr}" -_last_githash = "{githash}" - -# Determine where the source code for this module -# lives. If __file__ is not a filesystem path then -# it is assumed not to live in a git repo at all. -if _get_repo_path(__file__, levels=len(_packagename.split('.'))): - version = update_git_devstr(_last_generated_version, path=__file__) - githash = get_git_devstr(sha=True, show_warning=False, - path=__file__) or _last_githash -else: - # The file does not appear to live in a git repo so don't bother - # invoking git - version = _last_generated_version - githash = _last_githash -"""[1:] - - -_FROZEN_VERSION_PY_STATIC_HEADER = """ -version = "{verstr}" -githash = "{githash}" -"""[1:] - - -def _get_version_py_str(packagename, version, githash, release, debug, - uses_git=True): - try: - from astropy_helpers import __version__ as ahver - except ImportError: - ahver = "unknown" - - epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) - timestamp = datetime.datetime.utcfromtimestamp(epoch) - major, minor, bugfix = _version_split(version) - - if packagename.lower() == 'astropy': - packagetitle = 'Astropy' - else: - packagetitle = 'Astropy-affiliated package ' + packagename - - header = '' - - if uses_git: - header = _generate_git_header(packagename, version, githash) - elif not githash: - # _generate_git_header will already generate a new git has for us, but - # for creating a new version.py for a release (even if uses_git=False) - # we still need to get the githash to include in the version.py - # See https://github.com/astropy/astropy-helpers/issues/141 - githash = git_helpers.get_git_devstr(sha=True, show_warning=True) - - if not header: # If _generate_git_header fails it returns an empty string - header = _FROZEN_VERSION_PY_STATIC_HEADER.format(verstr=version, - githash=githash) - - return _FROZEN_VERSION_PY_TEMPLATE.format(packagetitle=packagetitle, - timestamp=timestamp, - header=header, - major=major, - minor=minor, - bugfix=bugfix, - ahver=ahver, - rel=release, debug=debug) - - -def _generate_git_header(packagename, version, githash): - """ - Generates a header to the version.py module that includes utilities for - probing the git repository for updates (to the current git hash, etc.) - These utilities should only be available in development versions, and not - in release builds. - - If this fails for any reason an empty string is returned. - """ - - loader = pkgutil.get_loader(git_helpers) - source = loader.get_source(git_helpers.__name__) or '' - source_lines = source.splitlines() - if not source_lines: - log.warn('Cannot get source code for astropy_helpers.git_helpers; ' - 'git support disabled.') - return '' - - idx = 0 - for idx, line in enumerate(source_lines): - if line.startswith('# BEGIN'): - break - git_helpers_py = '\n'.join(source_lines[idx + 1:]) - - verstr = version - - new_githash = git_helpers.get_git_devstr(sha=True, show_warning=False) - - if new_githash: - githash = new_githash - - return _FROZEN_VERSION_PY_WITH_GIT_HEADER.format( - git_helpers=git_helpers_py, packagename=packagename, - verstr=verstr, githash=githash) - - -def generate_version_py(packagename=None, version=None, release=None, debug=None, - uses_git=None, srcdir='.'): - """ - Generate a version.py file in the package with version information, and - update developer version strings. - - This function should normally be called without any arguments. In this case - the package name and version is read in from the ``setup.cfg`` file (from - the ``name`` or ``package_name`` entry and the ``version`` entry in the - ``[metadata]`` section). - - If the version is a developer version (of the form ``3.2.dev``), the - version string will automatically be expanded to include a sequential - number as a suffix (e.g. ``3.2.dev13312``), and the updated version string - will be returned by this function. - - Based on this updated version string, a ``version.py`` file will be - generated inside the package, containing the version string as well as more - detailed information (for example the major, minor, and bugfix version - numbers, a ``release`` flag indicating whether the current version is a - stable or developer version, and so on. - """ - - if packagename is not None: - warnings.warn('The packagename argument to generate_version_py has ' - 'been deprecated and will be removed in future. Specify ' - 'the package name in setup.cfg instead', AstropyDeprecationWarning) - - if version is not None: - warnings.warn('The version argument to generate_version_py has ' - 'been deprecated and will be removed in future. Specify ' - 'the version number in setup.cfg instead', AstropyDeprecationWarning) - - if release is not None: - warnings.warn('The release argument to generate_version_py has ' - 'been deprecated and will be removed in future. We now ' - 'use the presence of the "dev" string in the version to ' - 'determine whether this is a release', AstropyDeprecationWarning) - - # We use ConfigParser instead of read_configuration here because the latter - # only reads in keys recognized by setuptools, but we need to access - # package_name below. - conf = ConfigParser() - conf.read('setup.cfg') - - if conf.has_option('metadata', 'name'): - packagename = conf.get('metadata', 'name') - elif conf.has_option('metadata', 'package_name'): - # The package-template used package_name instead of name for a while - warnings.warn('Specifying the package name using the "package_name" ' - 'option in setup.cfg is deprecated - use the "name" ' - 'option instead.', AstropyDeprecationWarning) - packagename = conf.get('metadata', 'package_name') - elif packagename is not None: # deprecated - pass - else: - sys.stderr.write('ERROR: Could not read package name from setup.cfg\n') - sys.exit(1) - - if conf.has_option('metadata', 'version'): - version = conf.get('metadata', 'version') - add_git_devstr = True - elif version is not None: # deprecated - add_git_devstr = False - else: - sys.stderr.write('ERROR: Could not read package version from setup.cfg\n') - sys.exit(1) - - if release is None: - release = 'dev' not in version - - if not release and add_git_devstr: - version += get_git_devstr(False) - - if uses_git is None: - uses_git = not release - - # In some cases, packages have a - but this is a _ in the module. Since we - # are only interested in the module here, we replace - by _ - packagename = packagename.replace('-', '_') - - try: - version_module = get_pkg_version_module(packagename) - - try: - last_generated_version = version_module._last_generated_version - except AttributeError: - last_generated_version = version_module.version - - try: - last_githash = version_module._last_githash - except AttributeError: - last_githash = version_module.githash - - current_release = version_module.release - current_debug = version_module.debug - except ImportError: - version_module = None - last_generated_version = None - last_githash = None - current_release = None - current_debug = None - - if release is None: - # Keep whatever the current value is, if it exists - release = bool(current_release) - - if debug is None: - # Likewise, keep whatever the current value is, if it exists - debug = bool(current_debug) - - package_srcdir = os.path.join(srcdir, *packagename.split('.')) - version_py = os.path.join(package_srcdir, 'version.py') - - if (last_generated_version != version or current_release != release or - current_debug != debug): - if '-q' not in sys.argv and '--quiet' not in sys.argv: - log.set_threshold(log.INFO) - - if is_distutils_display_option(): - # Always silence unnecessary log messages when display options are - # being used - log.set_threshold(log.WARN) - - log.info('Freezing version number to {0}'.format(version_py)) - - with open(version_py, 'w') as f: - # This overwrites the actual version.py - f.write(_get_version_py_str(packagename, version, last_githash, - release, debug, uses_git=uses_git)) - - return version - - -def get_pkg_version_module(packagename, fromlist=None): - """Returns the package's .version module generated by - `astropy_helpers.version_helpers.generate_version_py`. Raises an - ImportError if the version module is not found. - - If ``fromlist`` is an iterable, return a tuple of the members of the - version module corresponding to the member names given in ``fromlist``. - Raises an `AttributeError` if any of these module members are not found. - """ - - version = import_file(os.path.join(packagename, 'version.py'), name='version') - - if fromlist: - return tuple(getattr(version, member) for member in fromlist) - else: - return version diff --git a/docs/api.rst b/docs/api.rst index 29a6db09..af39056a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,14 +1,8 @@ API Documentation ================= -.. automodapi:: astropy_helpers.setup_helpers +.. automodapi:: extension_helpers.setup_helpers :no-main-docstr: -.. automodapi:: astropy_helpers.version_helpers - :no-main-docstr: - -.. automodapi:: astropy_helpers.openmp_helpers - :no-main-docstr: - -.. automodapi:: astropy_helpers.git_helpers +.. automodapi:: extension_helpers.openmp_helpers :no-main-docstr: diff --git a/docs/basic.rst b/docs/basic.rst index 7164747c..1ea6f2a1 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -1,273 +1,30 @@ -Basic functionality +Defining extensions =================== -The big-picture purpose of astropy-helpers is to provide customization to Python's -packaging infrastructure process in ways that the Astropy Project has found to -help simplifying the developing and releasing packages. This is primarily -built around ``setup.py`` commands, as outlined below, as well as code to help -manage version numbers and better control the build process of larger packages. +The main functionality in extension-helpers is the +:func:`~extension_helpers.setup_helpers.get_extensions` function which can be +used to collect package extensions. Defining functions is then done in two ways: -Custom setup.py commands ------------------------- +* For simple Cython extensions, :func:`~extension_helpers.setup_helpers.get_extensions` + will automatically generate extension modules with no further work. -The main part of astropy-helpers is to provide customized setuptools commands. -For example, in a package that uses astropy-helpers, the following command -will be available:: +* For other extensions, you can create ``setup_package.py`` files anywhere + in your package, and these files can then include a ``get_extensions`` + function that returns a list of ``distutils.core.Extension`` objects. - python setup.py build_docs +In the second case, the idea is that for large packages, extensions can be defined +in the relevant sub-packages rather than having to all be listed in the main +``setup.py`` file. For packages with only a couple of extensions, using +extension-helpers is not really necessary since you can just define these directly +in ``setup.py``. -and this command is implemented in astropy-helpers. To use the custom commands -described here, you can either use the simplified method of opting in to -astropy-helpers described in :ref:`setup_all`, or if you want more control, use -the :func:`~astropy_helpers.setup_helpers.register_commands` function by -adding:: - - from astropy_helpers.setup_helpers import register_commands - -to your ``setup.py`` file, then doing:: - - # Create a dictionary with setup command overrides. Note that this gets - # information about the package (name and version) from the setup.cfg file. - cmdclassd = register_commands() - -This function requires that the package name and full version are set in the -``setup.cfg`` file in the ``[metadata]`` section, e.g.:: - - [metadata] - name = mypackage - version = 0.4.dev - -Then, pass ``cmdclassd`` to the ``setup`` function in ``setup.py``:: - - setup(..., - cmdclass=cmdclassd) - -The commands we provide or customize are: - -python setup.py test -^^^^^^^^^^^^^^^^^^^^ - -This command will automatically build the package, install it to a temporary -directory, and run the tests using `pytest `_ on this -installed version. Note that the bulk of this command is actually defined -in ``astropy.tests.command.AstropyTest``, because that allows the test -machinery to operate outside a setuptools command. This, here we -simply define the custom -setuptools command. - -python setup.py sdist -^^^^^^^^^^^^^^^^^^^^^ - -We redefine ``sdist`` to use the version from distutils rather than from -setuptools, as the setuptools version requires duplication of information -in ``MANIFEST.in``. - -python setup.py build_docs -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This command will automatically build the package, then run sphinx to build -the documentation. This makes development much easier because it ensures -sphinx extensions that use the package's code to make documentation are -actually using the in-development version of the code. Sphinx itself -provides a custom setuptools command, which we -expand with the following options: - -* ``-w``: set the return code to 1 if there are any warnings during the build - process. - -* ``-l``: completely clean previous builds, including files generated by - the sphinx-automodapi package (which creates API pages for different - functions/classes). - -* ``-n``: disable the intersphinx option. - -* ``-o``: open the documentation in a browser if a build finishes successfully. - -In addition, ``build_docs`` will automatically download and temporarily install -sphinx-astropy (which is a meta-package that -provides standardized configuration and documentation dependencies for astropy -packages) if it isn't already installed. Temporary installation means that the -package will be installed into an ``.eggs`` directory in the current working -directory, and it will only be available for the duration of the call to -``build_docs``. - -python setup.py build_ext -^^^^^^^^^^^^^^^^^^^^^^^^^ - -This is also used when running ``build`` or ``install``. We add several features -compared to the default ``build_ext`` command: - -* For packages with C/Cython extensions, we create a - ``packagename.compiler_version`` submodule and a - ``packagename.cython_version`` submodule that contain information about the - compilers used. - -* Packages that need to build C extensions using the Numpy C API, we allow - those packages to define the include path as ``'numpy'`` as opposed to having - to import Numpy and call ``get_include``. The goal is to solve the issue that - if one has to import Numpy to define extensions, then Numpy has to be - installed/available before the package is installed, which means that one - needs to install Numpy in a separate installation step. - -* We detect broken compilers and replace them with other compilers on-the-fly - unless the compiler is explicitly specified with the ``CC`` environment - variable. - -* If Cython is not installed, then we automatically check for generated C files - (which are normally present in the stable releases) and give a nice error - if these are not found. - -Version helpers ---------------- - -Another piece of functionality we provide in astropy-helpers is the ability -to generate a ``packagename.version`` module that includes functions that -automatically set the version string for developer versions, to e.g. -``3.2.dev22213`` so that each developer version has a unique number (although -note that branches an equal number of commits away from the master branch will -share the same version number). - -In addition, this module contains variables such as ``major``, ``minor``, and -``bugfix``, as well as ``version_info`` (a tuple of the previous three values), -a ``release`` flag that indicates whether we are using a stable release, and -several other complementary variables. To use the version helpers, you can -either use the simplified method of opting in to astropy-helpers described in -:ref:`setup_all`, or if you want more control, use the -:func:`~astropy_helpers.version_helpers.generate_version_py`, import:: - - from astropy_helpers.version_helpers import generate_version_py - -in your ``setup.py`` file, and call:: - - # Freeze build information in version.py. Note that this gets information - # about the package (name and version) from the setup.cfg file. - version = generate_version_py() - -The ``version`` variable will be set to the version number of your package -including any developer suffix. Note that this requires that the package name -and version are set in the ``setup.cfg`` file in the ``[metadata]`` section, -e.g.:: - - [metadata] - name = mypackage - version = 0.4.dev - -Then, pass ``version`` to the ``setup`` function in ``setup.py``:: - - setup(..., - version=version) - -Note that if you want to be able to generate developer versions such as -``3.2.dev22213`` without having to use the ``generate_version_py`` machinery, -you can instead just import :func:`~astropy_helpers.git_helpers.get_git_devstr`:: - - from astropy_helpers.git_helpers import get_git_devstr - -and you will then be able to use e.g.:: - - version += get_git_devstr() - -to add the developer suffix to the version string. - -Collecting package information ------------------------------- - -The ``setup`` function from setuptools can take a number of options that indicate -for example what extensions to build, and what package data to include. However, -for large packages this can become cumbersome. We therefore provide a mechanism -for defining extensions and package data inside individual sub-packages. To do -this, you can create ``setup_package.py`` files anywhere in your package, and -these files can include one or more of the following functions: - -* ``get_package_data``: - This function, if defined, should return a dictionary mapping the name of - the subpackage(s) that need package data to a list of data file paths - (possibly including wildcards) relative to the path of the package's source - code. e.g. if the source distribution has a needed data file - ``astropy/wcs/tests/data/3d_cd.hdr``, this function should return - ``{'astropy.wcs.tests':['data/3d_cd.hdr']}``. See the ``package_data`` - option of the :func:`distutils.core.setup` function. - - It is recommended that all such data be in a directory named ``data`` inside - the package within which it is supposed to be used. This package data - should be accessed via the ``astropy.utils.data.get_pkg_data_filename`` and - ``astropy.utils.data.get_pkg_data_fileobj`` functions. - -* ``get_extensions``: - This provides information for building C or Cython extensions. If defined, - it should return a list of ``distutils.core.Extension`` objects. - -* ``get_build_options``: - This function allows a package to add extra build options. It - should return a list of tuples, where each element has: - - - *name*: The name of the option as it would appear on the - commandline or in the ``setup.cfg`` file. - - - *doc*: A short doc string for the option, displayed by - ``setup.py build --help``. - - - *is_bool* (optional): When `True`, the option is a boolean - option and doesn't have an associated value. - - Once an option has been added, its value can be looked up using - ``astropy_helpers.setup_helpers.get_distutils_build_option``. - -* ``get_external_libraries``: - This function declares that the package uses libraries that are - included in the astropy distribution that may also be distributed - elsewhere on the users system. It should return a list of library - names. For each library, a new build option is created, - ``'--use-system-X'`` which allows the user to request to use the - system's copy of the library. The package would typically call - ``astropy_helpers.setup_helpers.use_system_library`` from its - ``get_extensions`` function to determine if the package should use - the system library or the included one. - -With these files in place, you can either use the simplified method of opting in -to astropy-helpers described in :ref:`setup_all`, or if you want more control, -use theyou can then make use of the -:func:`~astropy_helpers.setup_helpers.get_package_info` function in your -``setup.py`` file with:: - - from astropy_helpers.setup_helpers import get_package_info +To use this, you should modify your ``setup.py`` file to use +:func:`~extension_helpers.setup_helpers.get_extensions` as follows:: + from extension_helpers.setup_helpers import get_extensions ... + setup(..., ext_modules=get_extensions()) - package_info = get_package_info() - - ... - - setup(..., **package_info) - - -.. _setup_all: - -Opting in to all basic functionality ------------------------------------- - -If you are happy to opt in to all the functionality described on this page, you -can make use of the :func:`~astropy_helpers.setup_helpers.setup` function from -:mod:`astropy_helpers.setup_helpers` which wraps the function of the same name -from setuptools and automatically runs -:func:`~astropy_helpers.setup_helpers.register_commands`, -:func:`~astropy_helpers.version_helpers.generate_version_py`, and -:func:`~astropy_helpers.setup_helpers.get_package_info`. If you want to do this, -make sure the package name and version number are defined in ``setup.cfg``:: - - [metadata] - name = mypackage - version = 0.4.dev - -then use the :func:`~astropy_helpers.setup_helpers.setup` function from -astropy-helpers in your ``setup.py`` file as follows:: - - import ah_bootstrap - from astropy_helpers.setup_helpers import setup - setup() - -We recommend that you also include a comment along the following lines in your -``setup.py`` file:: - - # The configuration for the package, including the name, version, and other - # information are set in the setup.cfg file. +Note that if you use this, extension-helpers will also we create a +``packagename.compiler_version`` submodule that contain information about the +compilers used. diff --git a/docs/conf.py b/docs/conf.py index 5459c60c..fd919e95 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -project = 'astropy-helpers' +project = 'extension-helpers' copyright = '2014, The Astropy Developers' author = 'The Astropy Developers' # We need to get the version number from the package import sys # noqa sys.path.insert(0, '..') -import astropy_helpers # noqa -version = astropy_helpers.__version__ -release = astropy_helpers.__version__ +import extension_helpers # noqa +version = extension_helpers.__version__ +release = extension_helpers.__version__ extensions = [ 'sphinx.ext.autodoc', @@ -46,7 +46,7 @@ html_theme = 'bootstrap-astropy' html_theme_options = { - 'logotext1': 'astropy', # white, semi-bold + 'logotext1': 'extension', # white, semi-bold 'logotext2': '-helpers', # orange, light 'logotext3': ':docs' # white, light } diff --git a/docs/developers.rst b/docs/developers.rst deleted file mode 100644 index 8fe3067b..00000000 --- a/docs/developers.rst +++ /dev/null @@ -1,29 +0,0 @@ -Notes for astropy-helpers contributors -====================================== - -Note about versions -------------------- - -As described in `APE4 -`_, the version -numbers for astropy-helpers follow the corresponding major/minor version of the -`astropy core package `_, but with an independent -sequence of micro (bugfix) version numbers. Hence, the initial release is 0.4, -in parallel with Astropy v0.4, which will be the first version of Astropy to -use astropy-helpers. - -Trying out changes ------------------- - -If you contribute a change to astropy-helpers and want to try it out with a -package that already uses astropy-helpers, install astropy-helpers from your -branch of the repository in editable mode:: - - pip install -e . - -Then go to your package and add the ``--use-system-astropy-helpers`` for any -``setup.py`` command you want to check, e.g.:: - - python setup.py build_docs --use-system-astropy-helpers - -This will cause the installed version to be used instead of any local submodule. diff --git a/docs/index.rst b/docs/index.rst index 7eb899cb..b3c7ca8d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,36 +1,18 @@ -About ------ +The **extension-helpers** package includes convenience helpers to assist with +building Python packages with compiled C/Cython extensions. It is developed by +the Astropy project but is intended to be general and usable by any Python +package. -The **astropy-helpers** package includes -many build, installation, and documentation-related tools used by the Astropy -project, but packaged separately for use by other projects that wish to -leverage this work. The motivation behind this package and details of its -implementation are in the accepted -`Astropy Proposal for Enhancement (APE) 4 `_. +This is not a traditional package in the sense that it is not intended to be +installed directly by users or developers. Instead, it is meant to be accessed +when the ``setup.py`` command is run and should be defined as a build-time +dependency in ``pyproject.toml`` files. -Astropy-helpers is not a traditional package in the sense that it -is not intended to be installed directly by users or developers. Instead, it -is meant to be accessed when the ``setup.py`` command is run - see :doc:`using` -for how to do this. - -For a real-life example of how to implement astropy-helpers in a project, -see the ``setup.py`` and ``setup.cfg`` files of the -`Affiliated package template `_. - -.. note:: astropy-helpers v3.x requires Python 3.5 or later. If you wish to - maintain Python 2 support for your package that uses astropy-helpers, - then do not upgrade astropy-helpers to v3.0 or later. We will still - provide Python 2.7 compatible v2.0.x releases until the end of 2019. - -User/developer guide --------------------- .. toctree:: :maxdepth: 1 - basic.rst - advanced.rst using.rst - updating.rst + basic.rst + openmp.rst known_issues.rst - developers.rst api.rst diff --git a/docs/advanced.rst b/docs/openmp.rst similarity index 68% rename from docs/advanced.rst rename to docs/openmp.rst index c001c3e6..937138df 100644 --- a/docs/advanced.rst +++ b/docs/openmp.rst @@ -1,17 +1,14 @@ -Advanced functionality -====================== - OpenMP helpers --------------- +============== We provide a helper function -:func:`~astropy_helpers.openmp_helpers.add_openmp_flags_if_available` that can +:func:`~extension_helpers.openmp_helpers.add_openmp_flags_if_available` that can be used to automatically add OpenMP flags for C/Cython extensions, based on whether OpenMP is available and produces executable code. To use this, edit the ``setup_package.py`` file where you define a C extension, import the helper function:: - from astropy_helpers.openmp_helpers import add_openmp_flags_if_available + from extension_helpers.openmp_helpers import add_openmp_flags_if_available then once you have defined the extension and before returning it, use it as:: diff --git a/docs/updating.rst b/docs/updating.rst deleted file mode 100644 index 9fee0855..00000000 --- a/docs/updating.rst +++ /dev/null @@ -1,28 +0,0 @@ -Updating astropy-helpers in a package -===================================== - -Automatic update ----------------- - -If you would like the Astropy team to automatically open pull requests to update -astropy-helpers in your package, then see the instructions `here -`_. - -Manual update -------------- - -To instead update astropy-helpers manually, go inside the submodule and do:: - - cd astropy_helpers - git fetch origin - -Then checkout the version you want to use, e.g.:: - - git checkout v3.0.3 - -Go back up to the root of the repository and update the ``ah_bootstap.py`` file -too, then add your changes:: - - cp astropy_helpers/ah_bootstrap.py . - git add astropy_helpers ah_bootstrap.py - git commit ... diff --git a/docs/using.rst b/docs/using.rst index 02601ec2..612fb77c 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -1,53 +1,18 @@ -Using astropy-helpers in a package -================================== +Using extension-helpers in a package +==================================== -astropy-helpers includes a special "bootstrap" module called ``ah_bootstrap.py`` -which is intended to be used by a project's setup.py in order to ensure that the -astropy-helpers package is available for build/installation. +To use extension-helpers in your package, you will need to make sure your +package uses a ``pyproject.toml`` file as described in `PEP 518 +`_. +You can then add extension-helpers to the build-time dependencies in your +``pyproject.toml`` file:: -The easiest way to get set up with astropy-helpers in a new package is to use -the `package-template `_ -that we provide. This template is specifically designed for use with the helpers, -so using it avoids some of the tedium of setting up the helpers. + [build-system] + requires = ["setuptools", "wheel", "extension-helpers"] -However, we now go through the steps of adding astropy-helpers -as a submodule to a package in case you wish to do so manually. First, add -astropy-helpers as a submodule at the root of your repository:: +If you have Cython extensions, you will need to make sure ``cython`` is included +in the above list too. - git submodule add git://github.com/astropy/astropy-helpers astropy_helpers - -Then go inside the submodule and check out a stable version of astropy-helpers. -You can see the available versions by running:: - - $ cd astropy_helpers - $ git tag - ... - v2.0.6 - v2.0.7 - ... - v3.0.1 - v3.0.2 - -If you want to support Python 2, pick the latest v2.0.x version (in the above -case ``v2.0.7``) and if you don't need to support Python 2, just pick the latest -stable version (in the above case ``v3.0.2``). Check out this version with e.g.:: - - $ git checkout v3.0.2 - -Then go back up to the root of your repository and copy the ``ah_bootstrap.py`` -file from the submodule to the root of your repository:: - - $ cd .. - $ cp astropy_helpers/ah_bootstrap.py . - -Finally, add:: - - import ah_bootstrap - -at the top of your ``setup.py`` file. This will ensure that ``astropy_helpers`` -is now available to use in your ``setup.py`` file. Finally, add then commit your -changes:: - - git add astropy_helpers ah_bootstrap.py setup.py - git commit -m "Added astropy-helpers" +With extension-helpers set up in this way, you will then be able to use the +functionality described in the remainder of this user guide. diff --git a/extension_helpers/__init__.py b/extension_helpers/__init__.py new file mode 100644 index 00000000..1d592541 --- /dev/null +++ b/extension_helpers/__init__.py @@ -0,0 +1,22 @@ +try: + from .version import version as __version__ + from .version import githash as __githash__ +except ImportError: + __version__ = '' + __githash__ = '' + + +import os +import sys + +# Ensure that all module-level code in astropy or other packages know that +# we're in setup mode: +if ('__main__' in sys.modules and + hasattr(sys.modules['__main__'], '__file__')): + filename = os.path.basename(sys.modules['__main__'].__file__) + + if filename.rstrip('co') == 'setup.py': + import builtins + builtins._ASTROPY_SETUP_ = True + + del filename diff --git a/astropy_helpers/conftest.py b/extension_helpers/conftest.py similarity index 79% rename from astropy_helpers/conftest.py rename to extension_helpers/conftest.py index e3c3f969..203d51ed 100644 --- a/astropy_helpers/conftest.py +++ b/extension_helpers/conftest.py @@ -1,8 +1,8 @@ -# This file contains settings for pytest that are specific to astropy-helpers. +# This file contains settings for pytest that are specific to extension-helpers. # Since we run many of the tests in sub-processes, we need to collect coverage # data inside each subprocess and then combine it into a single .coverage file. # To do this we set up a list which run_setup appends coverage objects to. -# This is not intended to be used by packages other than astropy-helpers. +# This is not intended to be used by packages other than extension-helpers. import os import glob @@ -30,12 +30,12 @@ def pytest_unconfigure(config): # We create an empty coverage data object combined_cdata = CoverageData() - # Add all files from astropy_helpers to make sure we compute the total + # Add all files from extension_helpers to make sure we compute the total # coverage, not just the coverage of the files that have non-zero # coverage. lines = {} - for filename in glob.glob(os.path.join('astropy_helpers', '**', '*.py'), recursive=True): + for filename in glob.glob(os.path.join('extension_helpers', '**', '*.py'), recursive=True): lines[os.path.abspath(filename)] = [] for cdata in SUBPROCESS_COVERAGE: @@ -45,7 +45,7 @@ def pytest_unconfigure(config): # exist. for filename in cdata.measured_files(): try: - pos = filename.rindex('astropy_helpers') + pos = filename.rindex('extension_helpers') except ValueError: continue short_filename = filename[pos:] diff --git a/extension_helpers/distutils_helpers.py b/extension_helpers/distutils_helpers.py new file mode 100644 index 00000000..aa067f18 --- /dev/null +++ b/extension_helpers/distutils_helpers.py @@ -0,0 +1,113 @@ +""" +This module contains various utilities for introspecting the distutils +module and the setup process. + +Some of these utilities require the +`extension_helpers.setup_helpers.register_commands` function to be called first, +as it will affect introspection of setuptools command-line arguments. Other +utilities in this module do not have that restriction. +""" + +import os +import sys + +from distutils import ccompiler +from distutils.dist import Distribution +from distutils.errors import DistutilsError + +from .utils import silence + + +def get_dummy_distribution(): + """ + Returns a distutils Distribution object used to instrument the setup + environment before calling the actual setup() function. + """ + + # Pre-parse the Distutils command-line options and config files to if + # the option is set. + dist = Distribution({'script_name': os.path.basename(sys.argv[0]), + 'script_args': sys.argv[1:]}) + + with silence(): + try: + dist.parse_config_files() + dist.parse_command_line() + except (DistutilsError, AttributeError, SystemExit): + # Let distutils handle DistutilsErrors itself AttributeErrors can + # get raise for ./setup.py --help SystemExit can be raised if a + # display option was used, for example + pass + + return dist + + +def get_main_package_directory(distribution): + """ + Given a Distribution object, return the main package directory. + """ + return min(distribution.packages, key=len).replace('.', os.sep) + + +def get_distutils_option(option, commands): + """ Returns the value of the given distutils option. + + Parameters + ---------- + option : str + The name of the option + + commands : list of str + The list of commands on which this option is available + + Returns + ------- + val : str or None + the value of the given distutils option. If the option is not set, + returns None. + """ + + dist = get_dummy_distribution() + + for cmd in commands: + cmd_opts = dist.command_options.get(cmd) + if cmd_opts is not None and option in cmd_opts: + return cmd_opts[option][1] + else: + return None + + +def get_distutils_build_option(option): + """ Returns the value of the given distutils build option. + + Parameters + ---------- + option : str + The name of the option + + Returns + ------- + val : str or None + The value of the given distutils build option. If the option + is not set, returns None. + """ + return get_distutils_option(option, ['build', 'build_ext', 'build_clib']) + + +def get_compiler_option(): + """ Determines the compiler that will be used to build extension modules. + + Returns + ------- + compiler : str + The compiler option specified for the build, build_ext, or build_clib + command; or the default compiler for the platform if none was + specified. + + """ + + compiler = get_distutils_build_option('compiler') + if compiler is None: + return ccompiler.get_default_compiler() + + return compiler diff --git a/astropy_helpers/openmp_helpers.py b/extension_helpers/openmp_helpers.py similarity index 99% rename from astropy_helpers/openmp_helpers.py rename to extension_helpers/openmp_helpers.py index 6122194c..a6efecb3 100644 --- a/astropy_helpers/openmp_helpers.py +++ b/extension_helpers/openmp_helpers.py @@ -3,7 +3,7 @@ # add_openmp_flags_if_available function in a setup_package.py file where you # are defining your extensions: # -# from astropy_helpers.openmp_helpers import add_openmp_flags_if_available +# from extension_helpers.openmp_helpers import add_openmp_flags_if_available # # then call it with a single extension as the only argument: # diff --git a/extension_helpers/setup_helpers.py b/extension_helpers/setup_helpers.py new file mode 100644 index 00000000..6a4449e0 --- /dev/null +++ b/extension_helpers/setup_helpers.py @@ -0,0 +1,288 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +This module contains a number of utilities for use during +setup/build/packaging that are useful to astropy as a whole. +""" + +import collections +import os +import subprocess +import sys +import shutil + +from distutils import log +from distutils.core import Extension + +from setuptools import find_packages +from setuptools.config import read_configuration + +from .distutils_helpers import get_compiler_option +from .utils import walk_skip_hidden, import_file + +__all__ = ['get_extensions'] + + +def get_extensions(srcdir='.'): + """ + Collates all of the information for building all subpackages + and returns a dictionary of keyword arguments that can + be passed directly to `distutils.setup`. + + The purpose of this function is to allow subpackages to update the + arguments to the package's ``setup()`` function in its setup.py + script, rather than having to specify all extensions/package data + directly in the ``setup.py``. See Astropy's own + ``setup.py`` for example usage and the Astropy development docs + for more details. + + This function obtains that information by iterating through all + packages in ``srcdir`` and locating a ``setup_package.py`` module. + This module can contain the ``get_extensions()`` function which returns + a list of `distutils.extension.Extension` objects. + + """ + ext_modules = [] + packages = [] + package_dir = {} + + # Use the find_packages tool to locate all packages and modules + packages = find_packages(srcdir) + + # Update package_dir if the package lies in a subdirectory + if srcdir != '.': + package_dir[''] = srcdir + + for setuppkg in iter_setup_packages(srcdir, packages): + # get_extensions must include any Cython extensions by their .pyx + # filename. + if hasattr(setuppkg, 'get_extensions'): + ext_modules.extend(setuppkg.get_extensions()) + + # Locate any .pyx files not already specified, and add their extensions in. + # The default include dirs include numpy to facilitate numerical work. + import numpy + ext_modules.extend(get_cython_extensions(srcdir, packages, ext_modules, + [numpy.get_include()])) + + # Now remove extensions that have the special name 'skip_cython', as they + # exist Only to indicate that the cython extensions shouldn't be built + for i, ext in reversed(list(enumerate(ext_modules))): + if ext.name == 'skip_cython': + del ext_modules[i] + + # On Microsoft compilers, we need to pass the '/MANIFEST' + # commandline argument. This was the default on MSVC 9.0, but is + # now required on MSVC 10.0, but it doesn't seem to hurt to add + # it unconditionally. + if get_compiler_option() == 'msvc': + for ext in ext_modules: + ext.extra_link_args.append('/MANIFEST') + + if len(ext_modules) > 0: + main_package_dir = min(packages, key=len) + src_path = os.path.relpath(os.path.join(os.path.dirname(__file__), 'src')) + shutil.copy(os.path.join(src_path, 'compiler.c'), + os.path.join(srcdir, main_package_dir, '_compiler.c')) + ext = Extension(main_package_dir + '.compiler_version', + [os.path.join(main_package_dir, '_compiler.c')]) + ext_modules.append(ext) + + return ext_modules + + +def iter_setup_packages(srcdir, packages): + """ A generator that finds and imports all of the ``setup_package.py`` + modules in the source packages. + + Returns + ------- + modgen : generator + A generator that yields (modname, mod), where `mod` is the module and + `modname` is the module name for the ``setup_package.py`` modules. + + """ + + for packagename in packages: + package_parts = packagename.split('.') + package_path = os.path.join(srcdir, *package_parts) + setup_package = os.path.relpath( + os.path.join(package_path, 'setup_package.py')) + + if os.path.isfile(setup_package): + module = import_file(setup_package, + name=packagename + '.setup_package') + yield module + + +def iter_pyx_files(package_dir, package_name): + """ + A generator that yields Cython source files (ending in '.pyx') in the + source packages. + + Returns + ------- + pyxgen : generator + A generator that yields (extmod, fullfn) where `extmod` is the + full name of the module that the .pyx file would live in based + on the source directory structure, and `fullfn` is the path to + the .pyx file. + """ + for dirpath, dirnames, filenames in walk_skip_hidden(package_dir): + for fn in filenames: + if fn.endswith('.pyx'): + fullfn = os.path.relpath(os.path.join(dirpath, fn)) + # Package must match file name + extmod = '.'.join([package_name, fn[:-4]]) + yield (extmod, fullfn) + + break # Don't recurse into subdirectories + + +def get_cython_extensions(srcdir, packages, prevextensions=tuple(), + extincludedirs=None): + """ + Looks for Cython files and generates Extensions if needed. + + Parameters + ---------- + srcdir : str + Path to the root of the source directory to search. + prevextensions : list of `~distutils.core.Extension` objects + The extensions that are already defined. Any .pyx files already here + will be ignored. + extincludedirs : list of str or None + Directories to include as the `include_dirs` argument to the generated + `~distutils.core.Extension` objects. + + Returns + ------- + exts : list of `~distutils.core.Extension` objects + The new extensions that are needed to compile all .pyx files (does not + include any already in `prevextensions`). + """ + + # Vanilla setuptools and old versions of distribute include Cython files + # as .c files in the sources, not .pyx, so we cannot simply look for + # existing .pyx sources in the previous sources, but we should also check + # for .c files with the same remaining filename. So we look for .pyx and + # .c files, and we strip the extension. + prevsourcepaths = [] + ext_modules = [] + + for ext in prevextensions: + for s in ext.sources: + if s.endswith(('.pyx', '.c', '.cpp')): + sourcepath = os.path.realpath(os.path.splitext(s)[0]) + prevsourcepaths.append(sourcepath) + + for package_name in packages: + package_parts = package_name.split('.') + package_path = os.path.join(srcdir, *package_parts) + + for extmod, pyxfn in iter_pyx_files(package_path, package_name): + sourcepath = os.path.realpath(os.path.splitext(pyxfn)[0]) + if sourcepath not in prevsourcepaths: + ext_modules.append(Extension(extmod, [pyxfn], + include_dirs=extincludedirs)) + + return ext_modules + + +class DistutilsExtensionArgs(collections.defaultdict): + """ + A special dictionary whose default values are the empty list. + + This is useful for building up a set of arguments for + `distutils.Extension` without worrying whether the entry is + already present. + """ + def __init__(self, *args, **kwargs): + def default_factory(): + return [] + + super(DistutilsExtensionArgs, self).__init__( + default_factory, *args, **kwargs) + + def update(self, other): + for key, val in other.items(): + self[key].extend(val) + + +def pkg_config(packages, default_libraries, executable='pkg-config'): + """ + Uses pkg-config to update a set of distutils Extension arguments + to include the flags necessary to link against the given packages. + + If the pkg-config lookup fails, default_libraries is applied to + libraries. + + Parameters + ---------- + packages : list of str + A list of pkg-config packages to look up. + + default_libraries : list of str + A list of library names to use if the pkg-config lookup fails. + + Returns + ------- + config : dict + A dictionary containing keyword arguments to + `distutils.Extension`. These entries include: + + - ``include_dirs``: A list of include directories + - ``library_dirs``: A list of library directories + - ``libraries``: A list of libraries + - ``define_macros``: A list of macro defines + - ``undef_macros``: A list of macros to undefine + - ``extra_compile_args``: A list of extra arguments to pass to + the compiler + """ + + flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries', + '-D': 'define_macros', '-U': 'undef_macros'} + command = "{0} --libs --cflags {1}".format(executable, ' '.join(packages)), + + result = DistutilsExtensionArgs() + + try: + pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + output = pipe.communicate()[0].strip() + except subprocess.CalledProcessError as e: + lines = [ + ("{0} failed. This may cause the build to fail below." + .format(executable)), + " command: {0}".format(e.cmd), + " returncode: {0}".format(e.returncode), + " output: {0}".format(e.output) + ] + log.warn('\n'.join(lines)) + result['libraries'].extend(default_libraries) + else: + if pipe.returncode != 0: + lines = [ + "pkg-config could not lookup up package(s) {0}.".format( + ", ".join(packages)), + "This may cause the build to fail below." + ] + log.warn('\n'.join(lines)) + result['libraries'].extend(default_libraries) + else: + for token in output.split(): + # It's not clear what encoding the output of + # pkg-config will come to us in. It will probably be + # some combination of pure ASCII (for the compiler + # flags) and the filesystem encoding (for any argument + # that includes directories or filenames), but this is + # just conjecture, as the pkg-config documentation + # doesn't seem to address it. + arg = token[:2].decode('ascii') + value = token[2:].decode(sys.getfilesystemencoding()) + if arg in flag_map: + if arg == '-D': + value = tuple(value.split('=', 1)) + result[flag_map[arg]].append(value) + else: + result['extra_compile_args'].append(value) + + return result diff --git a/astropy_helpers/commands/src/compiler.c b/extension_helpers/src/compiler.c similarity index 100% rename from astropy_helpers/commands/src/compiler.c rename to extension_helpers/src/compiler.c diff --git a/astropy_helpers/tests/__init__.py b/extension_helpers/tests/__init__.py similarity index 81% rename from astropy_helpers/tests/__init__.py rename to extension_helpers/tests/__init__.py index e70b2902..5f7ae647 100644 --- a/astropy_helpers/tests/__init__.py +++ b/extension_helpers/tests/__init__.py @@ -83,25 +83,6 @@ def run_setup(setup_script, args): raise SystemExit(p.returncode) -@pytest.fixture(scope='function', autouse=True) -def reset_setup_helpers(request): - """ - Saves and restores the global state of the astropy_helpers.setup_helpers - module between tests. - """ - - mod = __import__('astropy_helpers.setup_helpers', fromlist=['']) - - old_state = mod._module_state.copy() - - def finalizer(old_state=old_state): - mod = sys.modules.get('astropy_helpers.setup_helpers') - if mod is not None: - mod._module_state.update(old_state) - - request.addfinalizer(finalizer) - - @pytest.fixture(scope='function', autouse=True) def reset_distutils_log(): """ @@ -119,11 +100,11 @@ def reset_distutils_log(): from setuptools import setup -NAME = 'astropy-helpers-test' +NAME = 'extension-helpers-test' VERSION = {version!r} setup(name=NAME, version=VERSION, - packages=['_astropy_helpers_test_'], + packages=['_extension_helpers_test_'], zip_safe=False) """ @@ -133,8 +114,8 @@ def create_testpackage(tmpdir, version='0.1'): source = tmpdir.mkdir('testpkg') with source.as_cwd(): - source.mkdir('_astropy_helpers_test_') - init = source.join('_astropy_helpers_test_', '__init__.py') + source.mkdir('_extension_helpers_test_') + init = source.join('_extension_helpers_test_', '__init__.py') init.write('__version__ = {0!r}'.format(version)) setup_py = TEST_PACKAGE_SETUP_PY.format(version=version) source.join('setup.py').write(setup_py) @@ -150,10 +131,10 @@ def create_testpackage(tmpdir, version='0.1'): @pytest.fixture def testpackage(tmpdir, version='0.1'): """ - This fixture creates a simplified package called _astropy_helpers_test_ + This fixture creates a simplified package called _extension_helpers_test_ used primarily for testing ah_boostrap, but without using the - astropy_helpers package directly and getting it confused with the - astropy_helpers package already under test. + extension_helpers package directly and getting it confused with the + extension_helpers package already under test. """ return create_testpackage(tmpdir, version=version) @@ -166,7 +147,7 @@ def cleanup_import(package_name): if not isinstance(k, str): # Some things will actually do this =_= continue - elif k.startswith('astropy_helpers.tests'): + elif k.startswith('extension_helpers.tests'): # Don't delete imported test modules or else the tests will break, # badly continue diff --git a/astropy_helpers/tests/test_openmp_helpers.py b/extension_helpers/tests/test_openmp_helpers.py similarity index 82% rename from astropy_helpers/tests/test_openmp_helpers.py rename to extension_helpers/tests/test_openmp_helpers.py index 28bfb702..e1c7d564 100644 --- a/astropy_helpers/tests/test_openmp_helpers.py +++ b/extension_helpers/tests/test_openmp_helpers.py @@ -6,7 +6,6 @@ from distutils.core import Extension from ..openmp_helpers import add_openmp_flags_if_available, generate_openmp_enabled_py -from ..setup_helpers import _module_state, register_commands IS_TRAVIS_LINUX = os.environ.get('TRAVIS_OS_NAME', None) == 'linux' IS_TRAVIS_OSX = os.environ.get('TRAVIS_OS_NAME', None) == 'osx' @@ -16,21 +15,8 @@ _state = None - -def setup_function(function): - global state - state = deepcopy(_module_state) - - -def teardown_function(function): - _module_state.clear() - _module_state.update(state) - - def test_add_openmp_flags_if_available(): - register_commands('openmp_testing', '0.0', False) - using_openmp = add_openmp_flags_if_available(Extension('test', [])) # Make sure that on Travis (Linux) and AppVeyor OpenMP does get used (for @@ -45,8 +31,6 @@ def test_add_openmp_flags_if_available(): def test_generate_openmp_enabled_py(): - register_commands('openmp_autogeneration_testing', '0.0', False) - # Test file generation generate_openmp_enabled_py('') assert os.path.isfile('openmp_enabled.py') diff --git a/extension_helpers/tests/test_setup_helpers.py b/extension_helpers/tests/test_setup_helpers.py new file mode 100644 index 00000000..0ae79e12 --- /dev/null +++ b/extension_helpers/tests/test_setup_helpers.py @@ -0,0 +1,174 @@ +import os +import sys +import importlib + +import pytest + +from textwrap import dedent + +from ..setup_helpers import get_extensions + +from . import reset_distutils_log # noqa +from . import run_setup, cleanup_import + +extension_helpers_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) # noqa + + +def teardown_module(module): + # Remove file generated by test_generate_openmp_enabled_py but + # somehow needed in test_cython_autoextensions + tmpfile = 'openmp_enabled.py' + if os.path.exists(tmpfile): + os.remove(tmpfile) + + +def _extension_test_package(tmpdir, request, extension_type='c', + include_numpy=False): + """Creates a simple test package with an extension module.""" + + test_pkg = tmpdir.mkdir('test_pkg') + test_pkg.mkdir('apyhtest_eva').ensure('__init__.py') + + # TODO: It might be later worth making this particular test package into a + # reusable fixture for other build_ext tests + + if extension_type in ('c', 'both'): + # A minimal C extension for testing + test_pkg.join('apyhtest_eva', 'unit01.c').write(dedent("""\ + #include + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "unit01", + NULL, + -1, + NULL + }; + PyMODINIT_FUNC + PyInit_unit01(void) { + return PyModule_Create(&moduledef); + } + """)) + + if extension_type in ('pyx', 'both'): + # A minimal Cython extension for testing + test_pkg.join('apyhtest_eva', 'unit02.pyx').write(dedent("""\ + print("Hello cruel angel.") + """)) + + if extension_type == 'c': + extensions = ['unit01.c'] + elif extension_type == 'pyx': + extensions = ['unit02.pyx'] + elif extension_type == 'both': + extensions = ['unit01.c', 'unit02.pyx'] + + include_dirs = ['numpy'] if include_numpy else [] + + extensions_list = [ + "Extension('apyhtest_eva.{0}', [join('apyhtest_eva', '{1}')], include_dirs={2})".format( + os.path.splitext(extension)[0], extension, include_dirs) + for extension in extensions] + + test_pkg.join('apyhtest_eva', 'setup_package.py').write(dedent("""\ + from setuptools import Extension + from os.path import join + def get_extensions(): + return [{0}] + """.format(', '.join(extensions_list)))) + + test_pkg.join('setup.py').write(dedent("""\ + import sys + from os.path import join + from setuptools import setup, find_packages + sys.path.insert(0, r'{extension_helpers_path}') + from extension_helpers.setup_helpers import get_extensions + + setup( + name='apyhtest_eva', + version='0.1', + packages=find_packages(), + ext_modules=get_extensions() + ) + """.format(extension_helpers_path=extension_helpers_PATH))) + + if '' in sys.path: + sys.path.remove('') + + sys.path.insert(0, '') + + def finalize(): + cleanup_import('apyhtest_eva') + + request.addfinalizer(finalize) + + return test_pkg + + +@pytest.fixture +def extension_test_package(tmpdir, request): + return _extension_test_package(tmpdir, request, extension_type='both') + + +@pytest.fixture +def c_extension_test_package(tmpdir, request): + # Check whether numpy is installed in the test environment + has_numpy = bool(importlib.util.find_spec('numpy')) + return _extension_test_package(tmpdir, request, extension_type='c', + include_numpy=has_numpy) + + +@pytest.fixture +def pyx_extension_test_package(tmpdir, request): + return _extension_test_package(tmpdir, request, extension_type='pyx') + + +def test_cython_autoextensions(tmpdir): + """ + Regression test for https://github.com/astropy/extension-helpers/pull/19 + + Ensures that Cython extensions in sub-packages are discovered and built + only once. + """ + + # Make a simple test package + test_pkg = tmpdir.mkdir('test_pkg') + test_pkg.mkdir('yoda').mkdir('luke') + test_pkg.ensure('yoda', '__init__.py') + test_pkg.ensure('yoda', 'luke', '__init__.py') + test_pkg.join('yoda', 'luke', 'dagobah.pyx').write( + """def testfunc(): pass""") + + # Required, currently, for get_extensions to work + ext_modules = get_extensions(str(test_pkg)) + + assert len(ext_modules) == 2 + assert ext_modules[0].name == 'yoda.luke.dagobah' + + +def test_compiler_module(capsys, c_extension_test_package): + """ + Test ensuring that the compiler module is built and installed for packages + that have extension modules. + """ + + test_pkg = c_extension_test_package + install_temp = test_pkg.mkdir('install_temp') + + with test_pkg.as_cwd(): + # This is one of the simplest ways to install just a package into a + # test directory + run_setup('setup.py', + ['install', + '--single-version-externally-managed', + '--install-lib={0}'.format(install_temp), + '--record={0}'.format(install_temp.join('record.txt'))]) + + with install_temp.as_cwd(): + import apyhtest_eva + # Make sure we imported the apyhtest_eva package from the correct place + dirname = os.path.abspath(os.path.dirname(apyhtest_eva.__file__)) + assert dirname == str(install_temp.join('apyhtest_eva')) + + import apyhtest_eva.compiler_version + assert apyhtest_eva.compiler_version != 'unknown' diff --git a/astropy_helpers/utils.py b/extension_helpers/utils.py similarity index 63% rename from astropy_helpers/utils.py rename to extension_helpers/utils.py index 115c9153..c575144a 100644 --- a/astropy_helpers/utils.py +++ b/extension_helpers/utils.py @@ -1,7 +1,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import contextlib -import imp import os import sys import glob @@ -42,29 +41,6 @@ def _get_platlib_dir(cmd): return os.path.join(cmd.build_base, 'lib' + plat_specifier) -def get_numpy_include_path(): - """ - Gets the path to the numpy headers. - """ - # We need to go through this nonsense in case setuptools - # downloaded and installed Numpy for us as part of the build or - # install, since Numpy may still think it's in "setup mode", when - # in fact we're ready to use it to build astropy now. - - import builtins - if hasattr(builtins, '__NUMPY_SETUP__'): - del builtins.__NUMPY_SETUP__ - import imp - import numpy - imp.reload(numpy) - - try: - numpy_include = numpy.get_include() - except AttributeError: - numpy_include = numpy.get_numpy_include() - return numpy_include - - class _DummyFile(object): """A noop writeable object.""" @@ -217,94 +193,7 @@ def import_file(filename, name=None): if not os.path.exists(filename): raise ImportError('Could not import file {0}'.format(filename)) - if import_machinery: - loader = import_machinery.SourceFileLoader(name, filename) - mod = loader.load_module() - else: - with open(filename, mode) as fd: - mod = imp.load_module(name, fd, filename, ('.py', mode, 1)) + loader = import_machinery.SourceFileLoader(name, filename) + mod = loader.load_module() return mod - - -def resolve_name(name): - """Resolve a name like ``module.object`` to an object and return it. - - Raise `ImportError` if the module or name is not found. - """ - - parts = name.split('.') - cursor = len(parts) - 1 - module_name = parts[:cursor] - attr_name = parts[-1] - - while cursor > 0: - try: - ret = __import__('.'.join(module_name), fromlist=[attr_name]) - break - except ImportError: - if cursor == 0: - raise - cursor -= 1 - module_name = parts[:cursor] - attr_name = parts[cursor] - ret = '' - - for part in parts[cursor:]: - try: - ret = getattr(ret, part) - except AttributeError: - raise ImportError(name) - - return ret - - -def extends_doc(extended_func): - """ - A function decorator for use when wrapping an existing function but adding - additional functionality. This copies the docstring from the original - function, and appends to it (along with a newline) the docstring of the - wrapper function. - - Examples - -------- - - >>> def foo(): - ... '''Hello.''' - ... - >>> @extends_doc(foo) - ... def bar(): - ... '''Goodbye.''' - ... - >>> print(bar.__doc__) - Hello. - - Goodbye. - - """ - - def decorator(func): - if not (extended_func.__doc__ is None or func.__doc__ is None): - func.__doc__ = '\n\n'.join([extended_func.__doc__.rstrip('\n'), - func.__doc__.lstrip('\n')]) - return func - - return decorator - - -def find_data_files(package, pattern): - """ - Include files matching ``pattern`` inside ``package``. - - Parameters - ---------- - package : str - The package inside which to look for data files - pattern : str - Pattern (glob-style) to match for the data files (e.g. ``*.dat``). - This supports the``**``recursive syntax. For example, ``**/*.fits`` - matches all files ending with ``.fits`` recursively. Only one - instance of ``**`` can be included in the pattern. - """ - - return glob.glob(os.path.join(package, pattern), recursive=True) diff --git a/setup.cfg b/setup.cfg index 564ea4c2..a4645819 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] -name = astropy-helpers +name = extension-helpers version = 4.0.dev -provides = astropy_helpers +provides = extension_helpers author = The Astropy Developers author_email = astropy.team@gmail.com license = BSD 3-Clause License @@ -27,7 +27,7 @@ python_requires = >=3.6 packages = find: [options.package_data] -astropy_helpers.commands = src/compiler.c +extension_helpers = src/compiler.c [options.extras_require] docs = sphinx-astropy @@ -35,5 +35,5 @@ docs = sphinx-astropy [tool:pytest] norecursedirs = .tox - astropy_helpers/tests/package_template + extension_helpers/tests/package_template python_functions = test_ diff --git a/setup.py b/setup.py index cdb0bae9..eb28f732 100755 --- a/setup.py +++ b/setup.py @@ -11,10 +11,7 @@ from setuptools import setup if LooseVersion(setuptools.__version__) < '30.3': - sys.stderr.write("ERROR: setuptools 30.3 or later is required by astropy-helpers\n") + sys.stderr.write("ERROR: setuptools 30.3 or later is required by extension-helpers\n") sys.exit(1) -from astropy_helpers.version_helpers import generate_version_py # noqa -version = generate_version_py() - -setup(version=version) +setup()