pax_global_header 0000666 0000000 0000000 00000000064 15021547125 0014514 g ustar 00root root 0000000 0000000 52 comment=4e5b4b9c7f151f65c53f65eb564faf99fc635e24
astropy-extension-helpers-4e5b4b9/ 0000775 0000000 0000000 00000000000 15021547125 0017307 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/.github/ 0000775 0000000 0000000 00000000000 15021547125 0020647 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/.github/dependabot.yml 0000664 0000000 0000000 00000000303 15021547125 0023473 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: ".github/workflows"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
astropy-extension-helpers-4e5b4b9/.github/release.yml 0000664 0000000 0000000 00000000563 15021547125 0023016 0 ustar 00root root 0000000 0000000 changelog:
exclude:
authors:
- pre-commit-ci
categories:
- title: New Features
labels:
- enhancement
- title: Bug Fixes
labels:
- bug
- title: Infrastructure
labels:
- infrastructure
- title: Documentation
labels:
- Documentation
- title: Other Changes
labels:
- "*"
astropy-extension-helpers-4e5b4b9/.github/workflows/ 0000775 0000000 0000000 00000000000 15021547125 0022704 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/.github/workflows/main.yml 0000664 0000000 0000000 00000002634 15021547125 0024360 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@8c0fde6f7e926df6ed7057255d29afa9c1ad5320 # v1.16.0
with:
posargs: --openmp-expected=True
coverage: codecov
envs: |
# Code style
- linux: style
# Standard tests
- linux: py310-test-oldestdeps
- linux: py311-test
- linux: py312-test
- linux: py313-test
- linux: py313-test-devdeps
- macos: py312-test
posargs: --openmp-expected=False
- macos: py313-test-devdeps
posargs: --openmp-expected=False
- windows: py310-test
- windows: py312-test-devdeps
# Test with more compilers, for the OpenMP helpers
- macos: py313-test-osxclang-conda
runs-on: macos-13
coverage: ''
- linux: py310-test-linuxgcc-conda
coverage: ''
# Test downstream packages
- linux: py313-downstream
publish:
needs: tests
uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@8c0fde6f7e926df6ed7057255d29afa9c1ad5320 # v1.16.0
with:
test_extras: test
test_command: pytest --pyargs extension_helpers
secrets:
pypi_token: ${{ secrets.PYPI_TOKEN }}
astropy-extension-helpers-4e5b4b9/.github/workflows/update-changelog.yml 0000664 0000000 0000000 00000001766 15021547125 0026650 0 ustar 00root root 0000000 0000000 # This workflow takes the GitHub release notes an updates the changelog on the
# main branch with the body of the release notes, thereby keeping a log in
# the git repo of the changes.
name: "Update Changelog"
on:
release:
types: [released]
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
- name: Update Changelog
uses: stefanzweifel/changelog-updater-action@a938690fad7edf25368f37e43a1ed1b34303eb36 # v1.12.0
with:
release-notes: ${{ github.event.release.body }}
latest-version: ${{ github.event.release.name }}
path-to-changelog: CHANGES.md
- name: Commit updated CHANGELOG
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
with:
branch: main
commit_message: Update CHANGELOG
file_pattern: CHANGES.md
astropy-extension-helpers-4e5b4b9/.gitignore 0000664 0000000 0000000 00000001245 15021547125 0021301 0 ustar 00root root 0000000 0000000 # Compiled files
*.py[cod]
*.a
*.o
*.so
*.pyd
__pycache__
# Ignore .c files by default to avoid including generated code. If you want to
# add a non-generated .c extension, use `git add -f filename.c`.
*.c
# Other generated files
MANIFEST
extension_helpers/version.py
extension_helpers/cython_version.py
# Sphinx
_build
_generated
api
# Packages/installer info
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
distribute-*.tar.gz
# Other
.cache
.tox
.*.swp
*~
.project
.pydevproject
.settings
.coverage
.coverage.subprocess
cover
htmlcov
.pytest_cache
# Mac OSX
.DS_Store
# PyCharm
.idea
# Hypothesis
.hypothesis
# vscode
settings.json
astropy-extension-helpers-4e5b4b9/.pre-commit-config.yaml 0000664 0000000 0000000 00000001711 15021547125 0023570 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ".*(data.*|extern.*|licenses.*|.*.fits)$"
- id: trailing-whitespace
exclude: ".*(data.*|extern.*|licenses.*|.*.fits)$"
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/scientific-python/cookie
rev: 2025.05.02
hooks:
- id: sp-repo-review
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
args: ["--write-changes"]
additional_dependencies:
- tomli
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.11.12"
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
ci:
autofix_prs: false
astropy-extension-helpers-4e5b4b9/.readthedocs.yaml 0000664 0000000 0000000 00000000475 15021547125 0022544 0 ustar 00root root 0000000 0000000 version: 2
build:
os: ubuntu-24.04
tools:
python: "3.13"
apt_packages:
- graphviz
sphinx:
builder: html
configuration: docs/conf.py
fail_on_warning: true
python:
install:
- method: pip
extra_requirements:
- docs
path: .
# Don't build any extra formats
formats: []
astropy-extension-helpers-4e5b4b9/CHANGES.md 0000664 0000000 0000000 00000014107 15021547125 0020704 0 ustar 00root root 0000000 0000000 ## v1.3.0 - 2025-05-27
### What's Changed
#### New Features
* Automatically set compiler flags to target PEP 384 Python limited API by @lpsinger in https://github.com/astropy/extension-helpers/pull/26
#### Other Changes
* Bump the actions group across 1 directory with 2 updates by @dependabot in https://github.com/astropy/extension-helpers/pull/92
* MNT: Replace ubuntu-20.04 with ubuntu-22.04 by @pllim in https://github.com/astropy/extension-helpers/pull/100
* Bump the actions group across 1 directory with 2 updates by @dependabot in https://github.com/astropy/extension-helpers/pull/99
* Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0 in /.github/workflows in the actions group by @dependabot in https://github.com/astropy/extension-helpers/pull/104
* Bump minimum version of Python to 3.10 and update versions by @astrofrog in https://github.com/astropy/extension-helpers/pull/106
**Full Changelog**: https://github.com/astropy/extension-helpers/compare/v1.2.0...v1.3.0
## v1.2.0 - 2024-10-16
### What's Changed
#### New Features
* Support pathlib.Path in write_if_different and import_file by @astrofrog in https://github.com/astropy/extension-helpers/pull/84
#### Bug Fixes
* TST: fix pyproject-only test (ensure build-time dependencies are installed) by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/80
#### Other Changes
* TST: drop legacy pytest fixture tmpdir, use tmp_path instead by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/81
* Test downstream with Python 3.12 by @astrofrog in https://github.com/astropy/extension-helpers/pull/86
* MNT: Use hash for Action workflow versions and update if needed by @pllim in https://github.com/astropy/extension-helpers/pull/88
* Bump actions/checkout from 4.2.0 to 4.2.1 in /.github/workflows in the actions group by @dependabot in https://github.com/astropy/extension-helpers/pull/89
**Full Changelog**: https://github.com/astropy/extension-helpers/compare/v1.1.1...v1.2.0
## v1.1.1 - 2023-12-07
### What's Changed
#### Bug Fixes
* get_extensions: use shutil.copyfile to avoid PermissionError by @doronbehar in https://github.com/astropy/extension-helpers/pull/59
* Fix bug that caused extension-helpers to not work correctly if pyproject was the only configuration file present by @astrofrog in https://github.com/astropy/extension-helpers/pull/66
#### Other Changes
* Replace all instances of distutils in docs with setuptools by @lpsinger in https://github.com/astropy/extension-helpers/pull/65
* Fix typos by @lpsinger in https://github.com/astropy/extension-helpers/pull/64
* MNT: handle deprecation warnings seen in tests by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/67
* Add note about pinning extension-helpers by @astrofrog in https://github.com/astropy/extension-helpers/pull/72
* DEP: drop dependency on tomli on Python 3.11 and newer by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/73
* TST: treat warnings as errors by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/74
* MNT: find and replace log.warn -> log.warning (the warn method is deprecated) by @neutrinoceros in https://github.com/astropy/extension-helpers/pull/75
* Infrastructure updates by @astrofrog in https://github.com/astropy/extension-helpers/pull/68
* Bump actions/checkout from 2 to 4 by @dependabot in https://github.com/astropy/extension-helpers/pull/77
* Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/astropy/extension-helpers/pull/76
* Add back support for absolute source paths but deprecate it by @astrofrog in https://github.com/astropy/extension-helpers/pull/70
### New Contributors
* @doronbehar made their first contribution in https://github.com/astropy/extension-helpers/pull/59
* @neutrinoceros made their first contribution in https://github.com/astropy/extension-helpers/pull/67
* @dependabot made their first contribution in https://github.com/astropy/extension-helpers/pull/77
**Full Changelog**: https://github.com/astropy/extension-helpers/compare/v1.1.0...v1.1.1
## v1.1.0 - 2023-07-24
### What's Changed
#### New Features
- Support enabling via `pyproject.toml` by @WilliamJamieson in https://github.com/astropy/extension-helpers/pull/48
#### Bug Fixes
- OpenMP functions should detect the Intel oneAPI compiler by @lpsinger in https://github.com/astropy/extension-helpers/pull/44
#### Infrastructure
- Skip hypothesis tests in downstream testing by @astrofrog in https://github.com/astropy/extension-helpers/pull/39
- Set language for docs by @lpsinger in https://github.com/astropy/extension-helpers/pull/45
- Update python requirements by @WilliamJamieson in https://github.com/astropy/extension-helpers/pull/50
- Add pre-commit configuration by @astrofrog in https://github.com/astropy/extension-helpers/pull/53
- Set testpaths to avoid picking up other tests by @astrofrog in https://github.com/astropy/extension-helpers/pull/54
- Added configuration required to update changelog when doing release through GitHub UI by @astrofrog in https://github.com/astropy/extension-helpers/pull/56
### New Contributors
- @WilliamJamieson made their first contribution in https://github.com/astropy/extension-helpers/pull/50
- @pre-commit-ci made their first contribution in https://github.com/astropy/extension-helpers/pull/55
**Full Changelog**: https://github.com/astropy/extension-helpers/compare/v1.0.0...v1.1.0
## 1.0.0 - 2022-03-16
- Added support for coverage>=5 for the extension-helpers test suite. [#24]
- Removed any direct usage of distutils. [#34]
- Remove support for the undocumented --compiler argument to setup.py. [#36]
- Added support for enabling extension-helpers from setup.cfg. [#33]
## 0.1 - 2019-12-18
- Initial release of extension-helpers, which was forked from astropy-helpers 4.0.
astropy-extension-helpers-4e5b4b9/CONTRIBUTING.md 0000664 0000000 0000000 00000000755 15021547125 0021547 0 ustar 00root root 0000000 0000000 Contributing to extension-helpers
===============================
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/main/CONTRIBUTING.md).
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).
astropy-extension-helpers-4e5b4b9/LICENSE.rst 0000664 0000000 0000000 00000002723 15021547125 0021127 0 ustar 00root root 0000000 0000000 Copyright (c) 2019, Astropy Developers
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the Astropy Team nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
astropy-extension-helpers-4e5b4b9/MANIFEST.in 0000664 0000000 0000000 00000000220 15021547125 0021037 0 ustar 00root root 0000000 0000000 include README.rst
include CHANGES.rst
include LICENSE.rst
recursive-include licenses *
include ah_bootstrap.py
exclude *.pyc *.o
prune build
astropy-extension-helpers-4e5b4b9/README.rst 0000664 0000000 0000000 00000001750 15021547125 0021001 0 ustar 00root root 0000000 0000000 extension-helpers
=================
.. image:: https://github.com/astropy/extension-helpers/actions/workflows/main.yml/badge.svg
:target: https://github.com/astropy/extension-helpers/actions/workflows/main.yml
.. image:: https://codecov.io/gh/astropy/extension-helpers/branch/main/graph/badge.svg
:target: https://codecov.io/gh/astropy/extension-helpers
.. image:: https://readthedocs.org/projects/extension-helpers/badge/?version=latest
:target: https://extension-helpers.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.10286296.svg
:target: https://doi.org/10.5281/zenodo.10286296
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.
For more information, see the documentation at http://extension-helpers.readthedocs.io
astropy-extension-helpers-4e5b4b9/conftest.py 0000664 0000000 0000000 00000000163 15021547125 0021506 0 ustar 00root root 0000000 0000000 def pytest_addoption(parser):
parser.addoption("--openmp-expected", action="store", default=None, help="help")
astropy-extension-helpers-4e5b4b9/docs/ 0000775 0000000 0000000 00000000000 15021547125 0020237 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/docs/Makefile 0000664 0000000 0000000 00000001110 15021547125 0021670 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS = -W
SPHINXBUILD = sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
astropy-extension-helpers-4e5b4b9/docs/api.rst 0000664 0000000 0000000 00000000133 15021547125 0021537 0 ustar 00root root 0000000 0000000 API Documentation
=================
.. automodapi:: extension_helpers
:no-main-docstr:
astropy-extension-helpers-4e5b4b9/docs/conf.py 0000664 0000000 0000000 00000003137 15021547125 0021542 0 ustar 00root root 0000000 0000000 from pkg_resources import get_distribution
project = "extension-helpers"
copyright = "2019, The Astropy Developers"
author = "The Astropy Developers"
# We need to get the version number from the package
version = release = get_distribution("extension-helpers").version
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"sphinx_automodapi.automodapi",
]
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"setuptools": ("https://setuptools.pypa.io/en/latest/", None),
}
# The suffix(es) of source filenames.
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "alabaster"
html_theme_options = {
"description": "A build time package to simplify C/Cython extensions.",
"code_font_family": "'Fira Code', monospace",
"github_user": "astropy",
"github_repo": "extension-helpers",
"sidebar_width": "300px",
}
# Enable nitpicky mode to pick reference issues
default_role = "obj"
nitpicky = True
astropy-extension-helpers-4e5b4b9/docs/index.rst 0000664 0000000 0000000 00000001153 15021547125 0022100 0 ustar 00root root 0000000 0000000 Extension Helpers
=================
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.
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.
.. toctree::
:maxdepth: 1
using.rst
openmp.rst
api.rst
astropy-extension-helpers-4e5b4b9/docs/make.bat 0000664 0000000 0000000 00000001423 15021547125 0021644 0 ustar 00root root 0000000 0000000 @ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd
astropy-extension-helpers-4e5b4b9/docs/openmp.rst 0000664 0000000 0000000 00000001147 15021547125 0022272 0 ustar 00root root 0000000 0000000 OpenMP helpers
==============
We provide a helper function
:func:`~extension_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 extension_helpers import add_openmp_flags_if_available
then once you have defined the extension and before returning it, use it as::
extension = Extension(...)
add_openmp_flags_if_available(extension)
return [extension]
astropy-extension-helpers-4e5b4b9/docs/using.rst 0000664 0000000 0000000 00000010506 15021547125 0022120 0 ustar 00root root 0000000 0000000 Using extension-helpers
=======================
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::
[build-system]
requires = ["setuptools",
"wheel",
"extension-helpers==1.*"]
If you have Cython extensions, you will need to make sure ``cython`` is included
in the above list too.
.. note:: It is highly recommended to pin the version of extension-helpers
to a major version, such as ``1.*``, since extension-helpers uses
`semantic versioning `_
and there will therefore likely be breaking changes when the major version is bumped.
If you do not specify any pinning, then old versions of your package that are already
on PyPI may no longer be installable on source without disabling the build isolation
and installing build dependencies manually.
The main functionality in extension-helpers is the
:func:`~extension_helpers.get_extensions` function which can be
used to collect package extensions. Defining functions is then done in two ways:
* For simple Cython extensions, :func:`~extension_helpers.get_extensions`
will automatically generate extension modules with no further work.
* 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 :class:`setuptools.Extension` objects.
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.
To use this, you should modify your ``setup.py`` file to use
:func:`~extension_helpers.get_extensions` as follows::
from extension_helpers import get_extensions
...
setup(..., ext_modules=get_extensions())
Note that if you use this, extension-helpers will also we create a
``packagename.compiler_version`` submodule that contain information about the
compilers used.
It is also possible to enable extension-helpers in ``setup.cfg`` instead of
``setup.py`` by adding the following configuration to the ``setup.cfg`` file::
[extension-helpers]
use_extension_helpers = true
Moreover, one can also enable extension-helpers in ``pyproject.toml`` by adding
the following configuration to the ``pyproject.toml`` file::
[tool.extension-helpers]
use_extension_helpers = true
.. note::
For backwards compatibility, the setting of ``use_extension_helpers`` in
``setup.cfg`` will override any setting of it in ``pyproject.toml``.
Python limited API
------------------
Your package may opt in to the :pep:`384` Python Limited API so that a single
binary wheel works with many different versions of Python on the same platform.
For this to work, any C extensions you write needs to make use only of
`certain C functions `__.
To opt in to the Python Limited API, add the following standard setuptools
option to your project's ``setup.cfg`` file::
[bdist_wheel]
py_limited_api = cp311
Here, ``311`` denotes API compatibility with Python >= 3.11. Replace with the
lowest major and minor version number that you wish to support.
You can also set this option in ``pyproject.toml``, using::
[tool.distutils.bdist_wheel]
py-limited-api = "cp312"
although note that this option is not formally documented/supported by the Python
packaging infrastructure and may change in future.
Alternatively, if you use setuptools 65.4 or later, you can dynamically opt in
to limited API builds by setting the ``EXTENSION_HELPERS_PY_LIMITED_API``
environment variable, e.g.::
EXTENSION_HELPERS_PY_LIMITED_API='cp311' python -m build
If you define ``py_limited_api`` in ``setup.cfg``, you can use
``EXTENSION_HELPERS_PY_LIMITED_API`` to opt **out** of the limited API builds
by setting ``EXTENSION_HELPERS_PY_LIMITED_API`` to an empty string. There is however
no way to opt out if you use ``py-limited-api`` in ``pyproject.toml``.
The ``get_extensions()`` functions will automatically detect these options and
add the necessary compiler flags to build your extension modules.
astropy-extension-helpers-4e5b4b9/extension_helpers/ 0000775 0000000 0000000 00000000000 15021547125 0023045 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/extension_helpers/__init__.py 0000664 0000000 0000000 00000003212 15021547125 0025154 0 ustar 00root root 0000000 0000000 import sys
from configparser import ConfigParser
from ._openmp_helpers import add_openmp_flags_if_available # noqa: F401
from ._setup_helpers import get_compiler, get_extensions, pkg_config # noqa: F401
from ._utils import import_file, write_if_different # noqa: F401
from .version import version as __version__ # noqa: F401
def _finalize_distribution_hook(distribution):
"""
Entry point for setuptools which allows extension-helpers to be enabled
from setup.cfg without the need for setup.py.
"""
import os
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
found_config = False
config_files = distribution.find_config_files()
if len(config_files) > 0:
cfg = ConfigParser()
cfg.read(config_files[0])
if cfg.has_option("extension-helpers", "use_extension_helpers"):
found_config = True
if cfg.get("extension-helpers", "use_extension_helpers").lower() == "true":
distribution.ext_modules = get_extensions()
pyproject = Path(distribution.src_root or os.curdir, "pyproject.toml")
if pyproject.exists() and not found_config:
with pyproject.open("rb") as f:
pyproject_cfg = tomllib.load(f)
if (
"tool" in pyproject_cfg
and "extension-helpers" in pyproject_cfg["tool"]
and "use_extension_helpers" in pyproject_cfg["tool"]["extension-helpers"]
and pyproject_cfg["tool"]["extension-helpers"]["use_extension_helpers"]
):
distribution.ext_modules = get_extensions()
astropy-extension-helpers-4e5b4b9/extension_helpers/_openmp_helpers.py 0000664 0000000 0000000 00000024305 15021547125 0026602 0 ustar 00root root 0000000 0000000 # This module defines functions that can be used to check whether OpenMP is
# available and if so what flags to use. To use this, import the
# add_openmp_flags_if_available function in a setup_package.py file where you
# are defining your extensions:
#
# from extension_helpers.openmp_helpers import add_openmp_flags_if_available
#
# then call it with a single extension as the only argument:
#
# add_openmp_flags_if_available(extension)
#
# this will add the OpenMP flags if available.
__doctest_skip__ = ["_get_flag_value_from_var"]
import datetime
import glob
import logging
import os
import subprocess
import sys
import tempfile
import time
from setuptools.command.build_ext import (
customize_compiler,
get_config_var,
new_compiler,
)
from ._setup_helpers import get_compiler
__all__ = ["add_openmp_flags_if_available"]
try:
# Check if this has already been instantiated, only set the default once.
_EXTENSION_HELPERS_DISABLE_OPENMP_SETUP_ # noqa: B018
except NameError:
import builtins
# It hasn't, so do so.
builtins._EXTENSION_HELPERS_DISABLE_OPENMP_SETUP_ = False
log = logging.getLogger(__name__)
CCODE = """
#include
#include
int main(void) {
#pragma omp parallel
printf("nthreads=%d\\n", omp_get_num_threads());
return 0;
}
"""
CCODE_ICX = """
#ifndef __INTEL_LLVM_COMPILER
#error This is not the Intel oneAPI compiler
#endif
"""
def _get_flag_value_from_var(flag, var, delim=" "):
"""
Extract flags from an environment variable.
Parameters
----------
flag : str
The flag to extract, for example '-I' or '-L'
var : str
The environment variable to extract the flag from, e.g. CFLAGS or LDFLAGS.
delim : str, optional
The delimiter separating flags inside the environment variable
Examples
--------
Let's assume the LDFLAGS is set to '-L/usr/local/include -customflag'. This
function will then return the following:
>>> _get_flag_value_from_var('-L', 'LDFLAGS')
'/usr/local/include'
Notes
-----
Environment variables are first checked in ``os.environ[var]``, then in
``sysconfig.get_config_var(var)``.
This function is not supported on Windows.
"""
if sys.platform.startswith("win"):
return None
# Simple input validation
if not var or not flag:
return None
flag_length = len(flag)
if not flag_length:
return None
# Look for var in os.eviron then in get_config_var
if var in os.environ:
flags = os.environ[var]
else:
try:
flags = get_config_var(var)
except KeyError:
return None
# Extract flag from {var:value}
if flags:
for item in flags.split(delim):
if item.startswith(flag):
return item[flag_length:]
def _check_if_compiler_is_icx():
"""
Check whether the compiler is the Intel oneAPI compiler.
Returns
-------
result : bool
`True` if the test passed, `False` otherwise.
"""
ccompiler = new_compiler()
customize_compiler(ccompiler)
with tempfile.TemporaryDirectory() as tmp_dir:
start_dir = os.path.abspath(".")
try:
os.chdir(tmp_dir)
# Write test program
with open("test_icx.c", "w") as f:
f.write(CCODE_ICX)
os.mkdir("objects")
# Compile program
ccompiler.compile(["test_icx.c"], output_dir="objects")
except Exception:
is_icx = False
else:
is_icx = True
finally:
os.chdir(start_dir)
return is_icx
def get_openmp_flags():
"""
Utility for returning compiler and linker flags possibly needed for
OpenMP support.
Returns
-------
result : `{'compiler_flags':, 'linker_flags':}`
Notes
-----
The flags returned are not tested for validity, use
`check_openmp_support(openmp_flags=get_openmp_flags())` to do so.
"""
compile_flags = []
link_flags = []
if get_compiler() == "msvc":
compile_flags.append("-openmp")
else:
include_path = _get_flag_value_from_var("-I", "CFLAGS")
if include_path:
compile_flags.append("-I" + include_path)
lib_path = _get_flag_value_from_var("-L", "LDFLAGS")
if lib_path:
link_flags.append("-L" + lib_path)
link_flags.append("-Wl,-rpath," + lib_path)
if _check_if_compiler_is_icx():
openmp_flags = "-qopenmp"
else:
openmp_flags = "-fopenmp"
compile_flags.append(openmp_flags)
link_flags.append(openmp_flags)
return {"compiler_flags": compile_flags, "linker_flags": link_flags}
def check_openmp_support(openmp_flags=None):
"""
Check whether OpenMP test code can be compiled and run.
Parameters
----------
openmp_flags : dict, optional
This should be a dictionary with keys ``compiler_flags`` and
``linker_flags`` giving the compilation and linking flags respectively.
These are passed as `extra_postargs` to `compile()` and
`link_executable()` respectively. If this is not set, the flags will
be automatically determined using environment variables.
Returns
-------
result : bool
`True` if the test passed, `False` otherwise.
"""
ccompiler = new_compiler()
customize_compiler(ccompiler)
if not openmp_flags:
# customize_compiler() extracts info from os.environ. If certain keys
# exist it uses these plus those from sysconfig.get_config_vars().
# If the key is missing in os.environ it is not extracted from
# sysconfig.get_config_var(). E.g. 'LDFLAGS' get left out, preventing
# clang from finding libomp.dylib because -L is not passed to
# linker. Call get_openmp_flags() to get flags missed by
# customize_compiler().
openmp_flags = get_openmp_flags()
compile_flags = openmp_flags.get("compiler_flags")
link_flags = openmp_flags.get("linker_flags")
with tempfile.TemporaryDirectory() as tmp_dir:
start_dir = os.path.abspath(".")
try:
os.chdir(tmp_dir)
# Write test program
with open("test_openmp.c", "w") as f:
f.write(CCODE)
os.mkdir("objects")
# Compile, test program
ccompiler.compile(["test_openmp.c"], output_dir="objects", extra_postargs=compile_flags)
# Link test program
objects = glob.glob(os.path.join("objects", "*" + ccompiler.obj_extension))
ccompiler.link_executable(objects, "test_openmp", extra_postargs=link_flags)
# Run test program
output = subprocess.check_output("./test_openmp")
output = output.decode(sys.stdout.encoding or "utf-8").splitlines()
if "nthreads=" in output[0]:
nthreads = int(output[0].strip().split("=")[1])
if len(output) == nthreads:
is_openmp_supported = True
else:
log.warning(
"Unexpected number of lines from output of test OpenMP "
"program (output was {})".format(output)
)
is_openmp_supported = False
else:
log.warning("Unexpected output from test OpenMP program (output was %s)", output)
is_openmp_supported = False
except Exception:
is_openmp_supported = False
finally:
os.chdir(start_dir)
return is_openmp_supported
def is_openmp_supported():
"""
Determine whether the build compiler has OpenMP support.
"""
log_threshold = log.level
log.setLevel("CRITICAL")
ret = check_openmp_support()
log.setLevel(log_threshold)
return ret
def add_openmp_flags_if_available(extension):
"""
Add OpenMP compilation flags, if supported (if not a warning will be
printed to the console and no flags will be added.)
Returns `True` if the flags were added, `False` otherwise.
"""
if _EXTENSION_HELPERS_DISABLE_OPENMP_SETUP_: # noqa: F821
log.info("OpenMP support has been explicitly disabled.")
return False
openmp_flags = get_openmp_flags()
using_openmp = check_openmp_support(openmp_flags=openmp_flags)
if using_openmp:
compile_flags = openmp_flags.get("compiler_flags")
link_flags = openmp_flags.get("linker_flags")
log.info("Compiling Cython/C/C++ extension with OpenMP support")
extension.extra_compile_args.extend(compile_flags)
extension.extra_link_args.extend(link_flags)
else:
log.warning(
"Cannot compile Cython/C/C++ extension with OpenMP, reverting to non-parallel code"
)
return using_openmp
_IS_OPENMP_ENABLED_SRC = """
# Autogenerated by {packagename}'s setup.py on {timestamp!s}
def is_openmp_enabled():
\"\"\"
Determine whether this package was built with OpenMP support.
\"\"\"
return {return_bool}
"""[
1:
]
def generate_openmp_enabled_py(packagename, srcdir=".", disable_openmp=None):
"""
Generate ``package.openmp_enabled.is_openmp_enabled``, which can then be used
to determine, post build, whether the package was built with or without
OpenMP support.
"""
epoch = int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
if sys.version_info >= (3, 11):
timestamp = datetime.datetime.fromtimestamp(epoch, datetime.UTC)
else:
timestamp = datetime.datetime.utcfromtimestamp(epoch)
if disable_openmp is not None:
import builtins
builtins._EXTENSION_HELPERS_DISABLE_OPENMP_SETUP_ = disable_openmp
if _EXTENSION_HELPERS_DISABLE_OPENMP_SETUP_: # noqa: F821
log.info("OpenMP support has been explicitly disabled.")
openmp_support = False
else:
openmp_support = is_openmp_supported()
src = _IS_OPENMP_ENABLED_SRC.format(
packagename=packagename, timestamp=timestamp, return_bool=openmp_support
)
package_srcdir = os.path.join(srcdir, *packagename.split("."))
is_openmp_enabled_py = os.path.join(package_srcdir, "openmp_enabled.py")
with open(is_openmp_enabled_py, "w") as f:
f.write(src)
astropy-extension-helpers-4e5b4b9/extension_helpers/_setup_helpers.py 0000664 0000000 0000000 00000027170 15021547125 0026447 0 ustar 00root root 0000000 0000000 # 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 logging
import os
import shutil
import subprocess
import sys
from collections import defaultdict
from setuptools import Extension, find_packages
from setuptools.command.build_ext import new_compiler
from ._utils import (
abi_to_versions,
get_limited_api_option,
import_file,
walk_skip_hidden,
)
__all__ = ["get_compiler", "get_extensions", "pkg_config"]
log = logging.getLogger(__name__)
def get_compiler():
"""
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.
"""
return new_compiler().compiler_type
def get_extensions(srcdir="."):
"""
Collect all extensions from Cython files and ``setup_package.py`` files.
If numpy is importable, the numpy include path will be added to all Cython
extensions which are automatically generated.
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 :class:`setuptools.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.
includes = []
try:
import numpy
includes = [numpy.get_include()]
except ImportError:
pass
ext_modules.extend(get_cython_extensions(srcdir, packages, ext_modules, includes))
# 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() == "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.join(os.path.dirname(__file__), "src")
shutil.copyfile(
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)
# Since https://github.com/astropy/extension-helpers/pull/67,
# extensions that used absolute paths in source names stopped working.
# Absolute paths in source paths are undesirable but we need to
# preserve backward-compatibility until we bump the major release,
# so we check for the case of absolute paths and emit a deprecation
# warning for now.
for extension in ext_modules:
sources = []
fixed = []
for source in extension.sources:
if os.path.isabs(source):
try:
source = os.path.relpath(source)
except ValueError:
# In some cases it's impossible to use a relative path, for
# instance if the source files are on a different drive. In
# this case there's not much we can do so we just proceed.
pass
fixed.append(source)
sources.append(source)
if fixed:
log.warning(
"Extension {} contains source files "
"({}) that are specified using an absolute "
"path, which will not be supported in future.".format(
extension.name, ", ".join(fixed)
)
)
extension.sources = sources
abi = get_limited_api_option(srcdir=srcdir)
if abi:
version_info, version_hex = abi_to_versions(abi)
if version_info is None:
raise ValueError(f"Unrecognized abi version for limited API: {abi}")
log.info(
f"Targeting PEP 384 limited API supporting Python >= {version_info[0], version_info[1]}"
)
for ext in ext_modules:
ext.py_limited_api = True
ext.define_macros.append(("Py_LIMITED_API", version_hex))
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.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.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
The extensions that are already defined, as a list of of
`~setuptools.Extension` objects. Any .pyx files already here will
be ignored.
extincludedirs : list or None
Directories to include as the `include_dirs` argument to the generated
`~setuptools.Extension` objects, as a list of strings.
Returns
-------
exts : list
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
def pkg_config(packages, default_libraries, executable="pkg-config"):
"""
Uses pkg-config to update a set of setuptools 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
The pkg-config packages to look up, as a list of strings.
default_libraries : list
The library names to use if the pkg-config lookup fails, a list of
strings.
Returns
-------
config : dict
A dictionary containing keyword arguments to
:class:`~setuptools.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 = f"{executable} --libs --cflags {' '.join(packages)}"
result = defaultdict(list)
try:
pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
output = pipe.communicate()[0].strip()
except subprocess.CalledProcessError as e:
lines = [
(f"{executable} failed. This may cause the build to fail below."),
f" command: {e.cmd}",
f" returncode: {e.returncode}",
f" output: {e.output}",
]
log.warning("\n".join(lines))
result["libraries"].extend(default_libraries)
else:
if pipe.returncode != 0:
lines = [
f"pkg-config could not lookup up package(s) {', '.join(packages)}.",
"This may cause the build to fail below.",
]
log.warning("\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
astropy-extension-helpers-4e5b4b9/extension_helpers/_utils.py 0000664 0000000 0000000 00000014556 15021547125 0024731 0 ustar 00root root 0000000 0000000 # Licensed under a 3-clause BSD style license - see LICENSE.rst
import os
import re
import sys
import tempfile
from configparser import ConfigParser
from importlib import machinery as import_machinery
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
__all__ = ["write_if_different", "import_file", "get_limited_api_option", "abi_to_versions"]
if sys.platform == "win32":
import ctypes
def _has_hidden_attribute(filepath):
"""
Returns True if the given filepath has the hidden attribute on
MS-Windows. Based on a post here:
http://stackoverflow.com/questions/284115/cross-platform-hidden-file-detection
"""
if isinstance(filepath, bytes):
filepath = filepath.decode(sys.getfilesystemencoding())
try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(filepath)
assert attrs != -1
result = bool(attrs & 2)
except (AttributeError, AssertionError):
result = False
return result
else:
def _has_hidden_attribute(filepath):
return False
def is_path_hidden(filepath):
"""
Determines if a given file or directory is hidden.
Parameters
----------
filepath : str
The path to a file or directory
Returns
-------
hidden : bool
Returns `True` if the file is hidden
"""
name = os.path.basename(os.path.abspath(filepath))
if isinstance(name, bytes):
is_dotted = name.startswith(b".")
else:
is_dotted = name.startswith(".")
return is_dotted or _has_hidden_attribute(filepath)
def walk_skip_hidden(top, onerror=None, followlinks=False):
"""
A wrapper for `os.walk` that skips hidden files and directories.
This function does not have the parameter `topdown` from
`os.walk`: the directories must always be recursed top-down when
using this function.
See also
--------
os.walk : For a description of the parameters
"""
for root, dirs, files in os.walk(top, topdown=True, onerror=onerror, followlinks=followlinks):
# These lists must be updated in-place so os.walk will skip
# hidden directories
dirs[:] = [d for d in dirs if not is_path_hidden(d)]
files[:] = [f for f in files if not is_path_hidden(f)]
yield root, dirs, files
def write_if_different(filename, data):
"""
Write ``data`` to ``filename``, if the content of the file is different.
This can be useful if e.g. generating ``.c`` or ``.h`` files, to make sure
that Python does not re-build unchanged files.
Parameters
----------
filename : str or `pathlib.Path`
The file name to be written to.
data : bytes
The data to be written to ``filename``.
"""
filepath = Path(filename)
assert isinstance(data, bytes)
if filepath.exists():
original_data = filepath.read_bytes()
else:
original_data = None
if original_data != data:
filepath.write_bytes(data)
def import_file(filename, name=None):
"""
Imports a module from a single file without importing the package that
the file is in.
This is useful for cases where a file needs to be imported from
``setup_package.py`` files without importing the parent package. The
returned module will have the optional ``name`` if given, or else a name
generated from the filename.
"""
# Specifying a traditional dot-separated fully qualified name here
# results in a number of "Parent module '...' not found while
# handling absolute import" warnings. Using the same name, the
# namespaces of the modules get merged together. So, this
# generates an underscore-separated name which is more likely to
# be unique, and it doesn't really matter because the name isn't
# used directly here anyway.
filepath = Path(filename)
if name is None:
name = "_".join(filepath.resolve().with_suffix("").parts[1:])
if not filepath.exists():
raise ImportError(f"Could not import file {filepath}")
loader = import_machinery.SourceFileLoader(name, str(filepath))
spec = spec_from_file_location(name, str(filepath))
mod = module_from_spec(spec)
loader.exec_module(mod)
return mod
def get_limited_api_option(srcdir):
"""
Checks setup.cfg and pyproject.toml files in the current directory
for the py_limited_api setting
"""
py_limited_api = os.environ.get("EXTENSION_HELPERS_PY_LIMITED_API")
if py_limited_api is not None:
if "DIST_EXTRA_CONFIG" in os.environ:
raise ValueError(
"Cannot use EXTENSION_HELPERS_PY_LIMITED_API if DIST_EXTRA_CONFIG is already defined"
)
dist_extra_config_filename = tempfile.mktemp()
with open(dist_extra_config_filename, "w") as f:
f.write(f"[bdist_wheel]\npy_limited_api={py_limited_api}")
os.environ["DIST_EXTRA_CONFIG"] = dist_extra_config_filename
return py_limited_api
srcdir = Path(srcdir)
setup_cfg = srcdir / "setup.cfg"
if setup_cfg.exists():
cfg = ConfigParser()
cfg.read(setup_cfg)
if cfg.has_option("bdist_wheel", "py_limited_api"):
return cfg.get("bdist_wheel", "py_limited_api")
pyproject = srcdir / "pyproject.toml"
if pyproject.exists():
with pyproject.open("rb") as f:
pyproject_cfg = tomllib.load(f)
if (
"tool" in pyproject_cfg
and "distutils" in pyproject_cfg["tool"]
and "bdist_wheel" in pyproject_cfg["tool"]["distutils"]
and "py-limited-api" in pyproject_cfg["tool"]["distutils"]["bdist_wheel"]
):
return pyproject_cfg["tool"]["distutils"]["bdist_wheel"]["py-limited-api"]
def _abi_to_version_info(abi):
match = re.fullmatch(r"^cp(\d)(\d+)$", abi)
if match is None:
return None
else:
return int(match[1]), int(match[2])
def _version_info_to_version_hex(major=0, minor=0):
"""Returns a PY_VERSION_HEX for {major}.{minor).0"""
return f"0x{major:02X}{minor:02X}0000"
def abi_to_versions(abi):
version_info = _abi_to_version_info(abi)
if version_info is None:
return None, None
else:
return version_info, _version_info_to_version_hex(*version_info)
astropy-extension-helpers-4e5b4b9/extension_helpers/conftest.py 0000664 0000000 0000000 00000004517 15021547125 0025253 0 ustar 00root root 0000000 0000000 # 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 extension-helpers.
import glob
import os
try:
from coverage import CoverageData
from coverage import __version__ as coverage_version
except ImportError:
HAS_COVERAGE = False
CoverageData = None
else:
# Set to the major version number
HAS_COVERAGE = int(coverage_version.split(".")[0])
SUBPROCESS_COVERAGE = []
def pytest_configure(config):
if HAS_COVERAGE:
SUBPROCESS_COVERAGE.clear()
def pytest_unconfigure(config):
if HAS_COVERAGE:
# 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("extension_helpers", "**", "*.py"), recursive=True):
lines[os.path.abspath(filename)] = []
for cdata in SUBPROCESS_COVERAGE:
# For each CoverageData object, we go through all the files and
# change the filename from one which might be a temporary path
# to the local filename. We then only keep files that actually
# exist.
for filename in cdata.measured_files():
try:
pos = filename.rindex("extension_helpers")
except ValueError:
continue
short_filename = filename[pos:]
if os.path.exists(short_filename):
lines[os.path.abspath(short_filename)].extend(cdata.lines(filename))
if HAS_COVERAGE >= 5:
# Support coverage<5 and >=5; see
# https://github.com/astropy/extension-helpers/issues/24
# We create an empty coverage data object
combined_cdata = CoverageData(suffix="subprocess")
combined_cdata.add_lines(lines)
combined_cdata.write()
else:
combined_cdata = CoverageData()
combined_cdata.add_lines(lines)
combined_cdata.write_file(".coverage.subprocess")
astropy-extension-helpers-4e5b4b9/extension_helpers/src/ 0000775 0000000 0000000 00000000000 15021547125 0023634 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/extension_helpers/src/compiler.c 0000664 0000000 0000000 00000005240 15021547125 0025613 0 ustar 00root root 0000000 0000000 #include
/***************************************************************************
* Macros for determining the compiler version.
*
* These are borrowed from boost, and majorly abridged to include only
* the compilers we care about.
***************************************************************************/
#define STRINGIZE(X) DO_STRINGIZE(X)
#define DO_STRINGIZE(X) #X
#if defined __clang__
/* Clang C++ emulates GCC, so it has to appear early. */
# define COMPILER "Clang version " __clang_version__
#elif defined(__INTEL_COMPILER) || defined(__ICL) || defined(__ICC) || defined(__ECC)
/* Intel */
# if defined(__INTEL_COMPILER)
# define INTEL_VERSION __INTEL_COMPILER
# elif defined(__ICL)
# define INTEL_VERSION __ICL
# elif defined(__ICC)
# define INTEL_VERSION __ICC
# elif defined(__ECC)
# define INTEL_VERSION __ECC
# endif
# define COMPILER "Intel C compiler version " STRINGIZE(INTEL_VERSION)
#elif defined(__GNUC__)
/* gcc */
# define COMPILER "GCC version " __VERSION__
#elif defined(__SUNPRO_CC)
/* Sun Workshop Compiler */
# define COMPILER "Sun compiler version " STRINGIZE(__SUNPRO_CC)
#elif defined(_MSC_VER)
/* Microsoft Visual C/C++
Must be last since other compilers define _MSC_VER for compatibility as well */
# if _MSC_VER < 1200
# define COMPILER_VERSION 5.0
# elif _MSC_VER < 1300
# define COMPILER_VERSION 6.0
# elif _MSC_VER == 1300
# define COMPILER_VERSION 7.0
# elif _MSC_VER == 1310
# define COMPILER_VERSION 7.1
# elif _MSC_VER == 1400
# define COMPILER_VERSION 8.0
# elif _MSC_VER == 1500
# define COMPILER_VERSION 9.0
# elif _MSC_VER == 1600
# define COMPILER_VERSION 10.0
# else
# define COMPILER_VERSION _MSC_VER
# endif
# define COMPILER "Microsoft Visual C++ version " STRINGIZE(COMPILER_VERSION)
#else
/* Fallback */
# define COMPILER "Unknown compiler"
#endif
/***************************************************************************
* Module-level
***************************************************************************/
struct module_state {
/* The Sun compiler can't handle empty structs */
#if defined(__SUNPRO_C) || defined(_MSC_VER)
int _dummy;
#endif
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"compiler_version",
NULL,
sizeof(struct module_state),
NULL,
NULL,
NULL,
NULL,
NULL
};
#define INITERROR return NULL
PyMODINIT_FUNC
PyInit_compiler_version(void)
{
PyObject* m;
m = PyModule_Create(&moduledef);
if (m == NULL)
INITERROR;
PyModule_AddStringConstant(m, "compiler", COMPILER);
return m;
}
astropy-extension-helpers-4e5b4b9/extension_helpers/tests/ 0000775 0000000 0000000 00000000000 15021547125 0024207 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/extension_helpers/tests/__init__.py 0000664 0000000 0000000 00000010476 15021547125 0026330 0 ustar 00root root 0000000 0000000 import os
import subprocess as sp
import sys
import pytest
from ..conftest import HAS_COVERAGE, SUBPROCESS_COVERAGE, CoverageData
PACKAGE_DIR = os.path.dirname(__file__)
def run_cmd(cmd, args, path=None, raise_error=True):
"""
Runs a shell command with the given argument list. Changes directory to
``path`` if given, otherwise runs the command in the current directory.
Returns a 3-tuple of (stdout, stderr, exit code)
If ``raise_error=True`` raise an exception on non-zero exit codes.
"""
if path is not None:
# Transparently support py.path objects
path = str(path)
p = sp.Popen([cmd] + list(args), stdout=sp.PIPE, stderr=sp.PIPE, cwd=path)
streams = tuple(s.decode("latin1").strip() for s in p.communicate())
return_code = p.returncode
if raise_error and return_code != 0:
raise RuntimeError(
f"The command `{cmd}` with args {list(args)!r} exited with code {return_code}.\n"
f"Stdout:\n\n{streams[0]}\n\nStderr:\n\n{streams[1]}"
)
return streams + (return_code,)
def run_setup(setup_script, args):
# This used to call setuptools.sandbox's run_setup, but due to issues with
# this and Cython (which caused segmentation faults), we now use subprocess.
setup_script = os.path.abspath(setup_script)
path = os.path.dirname(setup_script)
setup_script = os.path.basename(setup_script)
if HAS_COVERAGE:
# In this case, we run the command using the coverage command and we
# then collect the coverage data into a SUBPROCESS_COVERAGE list which
# is set up at the start of the testing process and is then combined
# into a single .coverage file at the end of the testing process.
p = sp.Popen(
["coverage", "run", setup_script] + list(args), cwd=path, stdout=sp.PIPE, stderr=sp.PIPE
)
stdout, stderr = p.communicate()
cdata = CoverageData()
if HAS_COVERAGE >= 5:
# Support coverage<5 and >=5; see
# https://github.com/astropy/extension-helpers/issues/24
cdata.read()
else:
cdata.read_file(os.path.join(path, ".coverage"))
SUBPROCESS_COVERAGE.append(cdata)
else:
# Otherwise we just run the tests with Python
p = sp.Popen(
[sys.executable, setup_script] + list(args), cwd=path, stdout=sp.PIPE, stderr=sp.PIPE
)
stdout, stderr = p.communicate()
sys.stdout.write(stdout.decode("utf-8"))
sys.stderr.write(stderr.decode("utf-8"))
if p.returncode != 0:
raise SystemExit(p.returncode)
TEST_PACKAGE_SETUP_PY = """\
#!/usr/bin/env python
from setuptools import setup
NAME = 'extension-helpers-test'
VERSION = {version!r}
setup(name=NAME, version=VERSION,
packages=['_extension_helpers_test_'],
zip_safe=False)
"""
def create_testpackage(tmp_path, version="0.1"):
source = tmp_path / "testpkg"
os.mkdir(source)
with source.as_cwd():
source.mkdir("_extension_helpers_test_")
init = source.join("_extension_helpers_test_", "__init__.py")
init.write(f"__version__ = {version!r}")
setup_py = TEST_PACKAGE_SETUP_PY.format(version=version)
source.join("setup.py").write(setup_py)
# Make the new test package into a git repo
run_cmd("git", ["init"])
run_cmd("git", ["add", "--all"])
run_cmd("git", ["commit", "-m", "test package"])
return source
@pytest.fixture
def testpackage(tmp_path, version="0.1"):
"""
This fixture creates a simplified package called _extension_helpers_test_
used primarily for testing ah_boostrap, but without using the
extension_helpers package directly and getting it confused with the
extension_helpers package already under test.
"""
return create_testpackage(tmp_path, version=version)
def cleanup_import(package_name):
"""Remove all references to package_name from sys.modules"""
for k in list(sys.modules):
if not isinstance(k, str):
# Some things will actually do this =_=
continue
elif k.startswith("extension_helpers.tests"):
# Don't delete imported test modules or else the tests will break,
# badly
continue
if k == package_name or k.startswith(package_name + "."):
del sys.modules[k]
astropy-extension-helpers-4e5b4b9/extension_helpers/tests/py311_backports.py 0000664 0000000 0000000 00000005643 15021547125 0027516 0 ustar 00root root 0000000 0000000 """
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
"""
import os
from contextlib import AbstractContextManager
# this class is vendored from Python 3.11.0
class chdir(AbstractContextManager):
"""Non thread-safe context manager to change the current working directory."""
def __init__(self, path):
self.path = path
self._old_cwd = []
def __enter__(self):
self._old_cwd.append(os.getcwd())
os.chdir(self.path)
def __exit__(self, *excinfo):
os.chdir(self._old_cwd.pop())
astropy-extension-helpers-4e5b4b9/extension_helpers/tests/test_openmp_helpers.py 0000664 0000000 0000000 00000003052 15021547125 0030640 0 ustar 00root root 0000000 0000000 import os
import types
from importlib import machinery
import pytest
from setuptools import Extension
from .._openmp_helpers import add_openmp_flags_if_available, generate_openmp_enabled_py
@pytest.fixture
def openmp_expected(request):
try:
openmp_expected = request.config.getoption("--openmp-expected")
if openmp_expected is not None:
return openmp_expected.lower() == "true"
except ValueError:
return None
def test_add_openmp_flags_if_available(openmp_expected):
using_openmp = add_openmp_flags_if_available(Extension("test", []))
# Make sure that on Travis (Linux) and AppVeyor OpenMP does get used (for
# MacOS X usually it will not work but this will depend on the compiler).
# Having this is useful because we'll find out if OpenMP no longer works
# for any reason on platforms on which it does work at the time of writing.
if openmp_expected is not None:
assert openmp_expected is using_openmp
def test_generate_openmp_enabled_py(openmp_expected):
# Test file generation
generate_openmp_enabled_py("")
assert os.path.isfile("openmp_enabled.py")
# Load openmp_enabled file as a module to check the result
loader = machinery.SourceFileLoader("openmp_enabled", "openmp_enabled.py")
mod = types.ModuleType(loader.name)
loader.exec_module(mod)
is_openmp_enabled = mod.is_openmp_enabled()
# Test is_openmp_enabled()
assert isinstance(is_openmp_enabled, bool)
if openmp_expected is not None:
assert openmp_expected is is_openmp_enabled
astropy-extension-helpers-4e5b4b9/extension_helpers/tests/test_setup_helpers.py 0000664 0000000 0000000 00000041435 15021547125 0030511 0 ustar 00root root 0000000 0000000 import importlib
import os
import subprocess
import sys
import uuid
from textwrap import dedent
import pytest
from .._setup_helpers import get_compiler, get_extensions
from . import cleanup_import, run_setup
if sys.version_info >= (3, 11):
from contextlib import chdir
else:
from .py311_backports import chdir
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)
POSSIBLE_COMPILERS = ["unix", "msvc", "bcpp", "cygwin", "mingw32"]
def test_get_compiler():
assert get_compiler() in POSSIBLE_COMPILERS
def _extension_test_package(
tmp_path,
request=None,
extension_type="c",
include_numpy=False,
include_setup_py=True,
):
"""Creates a simple test package with an extension module."""
test_pkg = tmp_path / "test_pkg"
os.makedirs(test_pkg / "helpers_test_package")
(test_pkg / "helpers_test_package" / "__init__.py").touch()
# 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 / "helpers_test_package" / "unit01.c").write_text(
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 / "helpers_test_package" / "unit02.pyx").write_text(
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 = [
f"Extension('helpers_test_package.{os.path.splitext(extension)[0]}', "
f"[join('helpers_test_package', '{extension}')], "
f"{include_dirs=})"
for extension in extensions
]
(test_pkg / "helpers_test_package" / "setup_package.py").write_text(
dedent(
"""\
from setuptools import Extension
from os.path import join
def get_extensions():
return [{}]
""".format(
", ".join(extensions_list)
)
)
)
if include_setup_py:
(test_pkg / "setup.py").write_text(
dedent(
f"""\
import sys
from os.path import join
from setuptools import setup, find_packages
sys.path.insert(0, r'{extension_helpers_PATH}')
from extension_helpers import get_extensions
setup(
name='helpers_test_package',
version='0.1',
packages=find_packages(),
ext_modules=get_extensions()
)
"""
)
)
if "" in sys.path:
sys.path.remove("")
sys.path.insert(0, "")
def finalize():
cleanup_import("helpers_test_package")
if request:
request.addfinalizer(finalize)
return test_pkg
@pytest.fixture
def extension_test_package(tmp_path, request):
return _extension_test_package(tmp_path, request, extension_type="both")
@pytest.fixture
def c_extension_test_package(tmp_path, request):
# Check whether numpy is installed in the test environment
has_numpy = bool(importlib.util.find_spec("numpy"))
return _extension_test_package(tmp_path, request, extension_type="c", include_numpy=has_numpy)
@pytest.fixture
def pyx_extension_test_package(tmp_path, request):
return _extension_test_package(tmp_path, request, extension_type="pyx")
def test_cython_autoextensions(tmp_path):
"""
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 = tmp_path / "test_pkg"
os.makedirs(test_pkg / "yoda" / "luke")
(test_pkg / "yoda" / "__init__.py").touch()
(test_pkg / "yoda" / "luke" / "__init__.py").touch()
(test_pkg / "yoda" / "luke" / "dagobah.pyx").write_text("""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 / "install_temp"
os.mkdir(install_temp)
with chdir(test_pkg):
# 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",
f"--install-lib={install_temp}",
"--record={}".format(install_temp / "record.txt"),
],
)
with chdir(install_temp):
import helpers_test_package
# Make sure we imported the helpers_test_package package from the correct place
dirname = os.path.abspath(os.path.dirname(helpers_test_package.__file__))
assert dirname == str(install_temp / "helpers_test_package")
import helpers_test_package.compiler_version
assert helpers_test_package.compiler_version != "unknown"
@pytest.mark.parametrize("use_extension_helpers", [None, False, True])
@pytest.mark.parametrize("pyproject_use_helpers", [None, False, True])
def test_no_setup_py(tmp_path, use_extension_helpers, pyproject_use_helpers):
"""
Test that makes sure that extension-helpers can be enabled without a
setup.py file.
"""
package_name = "helpers_test_package_" + str(uuid.uuid4()).replace("-", "_")
test_pkg = tmp_path / "test_pkg"
os.makedirs(test_pkg / package_name)
(test_pkg / package_name / "__init__.py").touch()
simple_c = test_pkg / package_name / "simple.c"
simple_c.write_text(
dedent(
"""\
#include
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"simple",
NULL,
-1,
NULL
};
PyMODINIT_FUNC
PyInit_simple(void) {
return PyModule_Create(&moduledef);
}
"""
)
)
(test_pkg / package_name / "setup_package.py").write_text(
dedent(
f"""\
from setuptools import Extension
from os.path import join
def get_extensions():
return [Extension('{package_name}.simple', [join('{package_name}', 'simple.c')])]
"""
)
)
if use_extension_helpers is None:
(test_pkg / "setup.cfg").write_text(
dedent(
f"""\
[metadata]
name = {package_name}
version = 0.1
[options]
packages = find:
"""
)
)
else:
(test_pkg / "setup.cfg").write_text(
dedent(
f"""\
[metadata]
name = {package_name}
version = 0.1
[options]
packages = find:
[extension-helpers]
use_extension_helpers = {str(use_extension_helpers).lower()}
"""
)
)
if pyproject_use_helpers is None:
(test_pkg / "pyproject.toml").write_text(
dedent(
"""\
[build-system]
requires = ["setuptools>=43.0.0",
"wheel"]
build-backend = 'setuptools.build_meta'
"""
)
)
else:
(test_pkg / "pyproject.toml").write_text(
dedent(
f"""\
[build-system]
requires = ["setuptools>=43.0.0",
"wheel"]
build-backend = 'setuptools.build_meta'
[tool.extension-helpers]
use_extension_helpers = {str(pyproject_use_helpers).lower()}
"""
)
)
install_temp = test_pkg / "install_temp"
os.mkdir(install_temp)
with chdir(test_pkg):
# NOTE: we disable build isolation as we need to pick up the current
# developer version of extension-helpers
subprocess.call(
[
sys.executable,
"-m",
"pip",
"install",
".",
"--no-build-isolation",
f"--target={install_temp}",
]
)
if "" in sys.path:
sys.path.remove("")
sys.path.insert(0, "")
with chdir(install_temp):
importlib.import_module(package_name)
if use_extension_helpers or (use_extension_helpers is None and pyproject_use_helpers):
compiler_version_mod = importlib.import_module(package_name + ".compiler_version")
assert compiler_version_mod.compiler != "unknown"
else:
try:
importlib.import_module(package_name + ".compiler_version")
except ImportError:
pass
else:
raise AssertionError(package_name + ".compiler_version should not exist")
@pytest.mark.parametrize("pyproject_use_helpers", [None, False, True])
def test_only_pyproject(tmp_path, pyproject_use_helpers):
"""
Test that makes sure that extension-helpers can be enabled without a
setup.py and without a setup.cfg file.
"""
pytest.importorskip("setuptools", minversion="62.0")
package_name = "helpers_test_package_" + str(uuid.uuid4()).replace("-", "_")
test_pkg = tmp_path / "test_pkg"
os.makedirs(test_pkg / package_name)
(test_pkg / package_name / "__init__.py").touch()
simple_pyx = test_pkg / package_name / "simple.pyx"
simple_pyx.write_text(
dedent(
"""\
def test():
pass
"""
)
)
if pyproject_use_helpers is None:
extension_helpers_option = ""
else:
extension_helpers_option = dedent(
f"""
[tool.extension-helpers]
use_extension_helpers = {str(pyproject_use_helpers).lower()}
"""
)
buildtime_requirements = ["setuptools>=43.0.0", "wheel", "Cython"]
(test_pkg / "pyproject.toml").write_text(
dedent(
f"""\
[project]
name = "{package_name}"
version = "0.1"
[tool.setuptools.packages]
find = {{namespaces = false}}
[build-system]
requires = [{', '.join(f'"{_}"' for _ in buildtime_requirements)}]
build-backend = 'setuptools.build_meta'
"""
)
+ extension_helpers_option
)
install_temp = test_pkg / "install_temp"
os.mkdir(install_temp)
with chdir(test_pkg):
# NOTE: we disable build isolation as we need to pick up the current
# developer version of extension-helpers
# In order to do so, we need to ensure that build-time dependencies are
# installed first
cmd1 = [
sys.executable,
"-m",
"pip",
"install",
*buildtime_requirements,
f"--target={install_temp}",
]
subprocess.call(cmd1)
cmd2 = [
sys.executable,
"-m",
"pip",
"install",
".",
"--no-build-isolation",
f"--target={install_temp}",
]
subprocess.call(cmd2)
if "" in sys.path:
sys.path.remove("")
sys.path.insert(0, "")
with chdir(install_temp):
importlib.import_module(package_name)
if pyproject_use_helpers:
compiler_version_mod = importlib.import_module(package_name + ".compiler_version")
assert compiler_version_mod.compiler != "unknown"
else:
try:
importlib.import_module(package_name + ".compiler_version")
except ImportError:
pass
else:
raise AssertionError(package_name + ".compiler_version should not exist")
# Tests to make sure that limited API support works correctly
@pytest.mark.parametrize("config", ("setup.cfg", "pyproject.toml"))
@pytest.mark.parametrize("envvar", (False, True))
@pytest.mark.parametrize("limited_api", (None, "cp310"))
@pytest.mark.parametrize("extension_type", ("c", "pyx", "both"))
def test_limited_api(tmp_path, config, envvar, limited_api, extension_type):
if sys.version_info < (3, 11):
pytest.skip(
"This test requires setuptools>=65.4 which is only available for Python 3.11 and later"
)
package = _extension_test_package(
tmp_path, extension_type=extension_type, include_numpy=True, include_setup_py=False
)
if config == "setup.cfg":
setup_cfg = dedent(
"""\
[metadata]
name = helpers_test_package
version = 0.1
[options]
packages = find:
[extension-helpers]
use_extension_helpers = true
"""
)
if limited_api and not envvar:
setup_cfg += f"\n[bdist_wheel]\npy_limited_api={limited_api}"
elif envvar:
# Make sure if we are using the environment variable that it takes
# precedence over this setting (this only works for setup.cfg)
setup_cfg += "\n[bdist_wheel]\npy_limited_api=cp35"
(package / "setup.cfg").write_text(setup_cfg)
# Still require a minimal pyproject.toml file if no setup.py file
(package / "pyproject.toml").write_text(
dedent(
"""
[build-system]
requires = ["setuptools>=43.0.0",
"wheel"]
build-backend = 'setuptools.build_meta'
[tool.extension-helpers]
use_extension_helpers = true
"""
)
)
elif config == "pyproject.toml":
pyproject_toml = dedent(
"""\
[build-system]
requires = ["setuptools>=43.0.0",
"wheel"]
build-backend = 'setuptools.build_meta'
[project]
name = "helpers_test_package"
version = "0.1"
[tool.setuptools.packages]
find = {namespaces = false}
[tool.extension-helpers]
use_extension_helpers = true
"""
)
if limited_api and not envvar:
pyproject_toml += f'\n[tool.distutils.bdist_wheel]\npy-limited-api = "{limited_api}"'
(package / "pyproject.toml").write_text(pyproject_toml)
env = os.environ.copy()
if envvar:
if limited_api:
env["EXTENSION_HELPERS_PY_LIMITED_API"] = limited_api
else:
env["EXTENSION_HELPERS_PY_LIMITED_API"] = ""
with chdir(package):
subprocess.run(
[sys.executable, "-m", "build", "--wheel", "--no-isolation"], env=env, check=True
)
wheels = os.listdir(package / "dist")
assert len(wheels) == 1
assert ("abi3" in wheels[0]) == (limited_api is not None)
def test_limited_api_invalid_abi(tmp_path, capsys):
package = _extension_test_package(
tmp_path, extension_type="c", include_numpy=True, include_setup_py=False
)
(package / "setup.cfg").write_text(
dedent(
"""\
[metadata]
name = helpers_test_package
version = 0.1
[options]
packages = find:
[extension-helpers]
use_extension_helpers = true
[bdist_wheel]
py_limited_api=invalid
"""
)
)
(package / "pyproject.toml").write_text(
dedent(
"""
[build-system]
requires = ["setuptools>=43.0.0",
"wheel"]
build-backend = 'setuptools.build_meta'
"""
)
)
with chdir(package):
result = subprocess.run(
[sys.executable, "-m", "build", "--wheel", "--no-isolation"], stderr=subprocess.PIPE
)
assert result.stderr.strip().endswith(
b"ValueError: Unrecognized abi version for limited API: invalid"
)
astropy-extension-helpers-4e5b4b9/extension_helpers/tests/test_utils.py 0000664 0000000 0000000 00000006132 15021547125 0026762 0 ustar 00root root 0000000 0000000 import os
import time
import pytest
from .._utils import (
abi_to_versions,
get_limited_api_option,
import_file,
write_if_different,
)
@pytest.mark.parametrize("path_type", ("str", "path"))
def test_import_file(tmp_path, path_type):
filepath = tmp_path / "spam.py"
if path_type == "str":
filepath = str(filepath)
with open(filepath, "w") as f:
f.write("magic = 12345")
module = import_file(filepath)
assert module.magic == 12345
@pytest.mark.parametrize("path_type", ("str", "path"))
def test_write_if_different(tmp_path, path_type):
filepath = tmp_path / "test.txt"
if path_type == "str":
filepath = str(filepath)
write_if_different(filepath, b"abc")
time1 = os.path.getmtime(filepath)
time.sleep(0.01)
write_if_different(filepath, b"abc")
time2 = os.path.getmtime(filepath)
assert time2 == time1
time.sleep(0.01)
write_if_different(filepath, b"abcd")
time3 = os.path.getmtime(filepath)
assert time3 > time1
class TestGetLimitedAPIOption:
def test_nofiles(self, tmp_path):
assert get_limited_api_option(tmp_path) is None
def test_empty_setup_cfg(self, tmp_path):
(tmp_path / "setup.cfg").write_text("")
assert get_limited_api_option(tmp_path) is None
def test_empty_pyproject_toml(self, tmp_path):
(tmp_path / "pyproject.toml").write_text("")
assert get_limited_api_option(tmp_path) is None
def test_setup_cfg(self, tmp_path):
(tmp_path / "setup.cfg").write_text("[bdist_wheel]\npy_limited_api=cp311")
assert get_limited_api_option(tmp_path) == "cp311"
# Make sure things still work even if an empty pyproject.toml file is present
(tmp_path / "pyproject.toml").write_text("")
assert get_limited_api_option(tmp_path) == "cp311"
# And if the pyproject.toml has the right section but not the right option
(tmp_path / "setup.cfg.toml").write_text("[tool.distutils.bdist_wheel]\nspam=1\n")
assert get_limited_api_option(tmp_path) == "cp311"
def test_pyproject(self, tmp_path):
(tmp_path / "pyproject.toml").write_text(
'[tool.distutils.bdist_wheel]\npy-limited-api="cp312"\n'
)
assert get_limited_api_option(tmp_path) == "cp312"
# Make sure things still work even if an empty setup.cfg file is present
(tmp_path / "setup.cfg.toml").write_text("\n")
assert get_limited_api_option(tmp_path) == "cp312"
# And if the setup.cfg has the right section but not the right option
(tmp_path / "setup.cfg.toml").write_text("[bdist_wheel]\nspam=1\n")
assert get_limited_api_option(tmp_path) == "cp312"
def test_abi_to_versions_invalid():
assert abi_to_versions("spam") == (None, None)
def test_abi_to_versions_valid():
assert abi_to_versions("cp39") == ((3, 9), "0x03090000")
assert abi_to_versions("cp310") == ((3, 10), "0x030A0000")
assert abi_to_versions("cp311") == ((3, 11), "0x030B0000")
assert abi_to_versions("cp312") == ((3, 12), "0x030C0000")
assert abi_to_versions("cp313") == ((3, 13), "0x030D0000")
astropy-extension-helpers-4e5b4b9/licenses/ 0000775 0000000 0000000 00000000000 15021547125 0021114 5 ustar 00root root 0000000 0000000 astropy-extension-helpers-4e5b4b9/licenses/LICENSE_ASTROSCRAPPY.rst 0000664 0000000 0000000 00000003154 15021547125 0024745 0 ustar 00root root 0000000 0000000 # The OpenMP helpers include code heavily adapted from astroscrappy, released
# under the following license:
#
# Copyright (c) 2015, Curtis McCully
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of the Astropy Team nor the names of its contributors may be
# used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
astropy-extension-helpers-4e5b4b9/pyproject.toml 0000664 0000000 0000000 00000010104 15021547125 0022217 0 ustar 00root root 0000000 0000000 [project]
name = "extension-helpers"
authors = [{name = "The Astropy Developers", email = "astropy.team@gmail.com"}]
license = {text = "BSD 3-Clause License"}
description = "Utilities for building and installing packages with compiled extensions"
readme = "README.rst"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Framework :: Setuptools Plugin",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Archiving :: Packaging",
]
requires-python = ">=3.10"
dependencies = [
"setuptools>=64",
"tomli>=1.0.0 ; python_version < '3.11'",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/astropy/extension-helpers"
[project.entry-points."setuptools.finalize_distribution_options"]
extension_helpers_get_extensions = "extension_helpers:_finalize_distribution_hook"
[project.optional-dependencies]
test = [
"wheel",
"pytest",
"pytest-cov",
"cython",
"build"
]
docs = [
"sphinx",
"sphinx-automodapi",
]
[tool.setuptools]
zip-safe = false
provides = ["extension_helpers"]
license-files = ["LICENSE.rst"]
include-package-data = false
[tool.setuptools.packages]
find = {namespaces = false}
[tool.setuptools.package-data]
extension_helpers = ["src/compiler.c"]
[tool.pytest.ini_options]
minversion = "6"
addopts = ["-ra", "--strict-config", "--strict-markers"]
log_cli_level = "INFO"
xfail_strict = true
testpaths = ['"extension_helpers"', '"docs"']
norecursedirs = ["build", "docs/_build"]
markers = ["flaky"]
filterwarnings = ["error"]
[tool.coverage.run]
omit = [
"extension_helpers/*/setup_package.py",
"extension_helpers/tests/*",
"extension_helpers/conftest.py",
"*/extension_helpers/*/setup_package.py",
"*/extension_helpers/tests/*",
"*/extension_helpers/conftest.py",
]
[tool.coverage.report]
exclude_lines = [
# Have to re-enable the standard pragma
"pragma: no cover",
# Don't complain about packages we have installed
"except ImportError",
# Don't complain if tests don't hit assertions
"raise AssertionError",
"raise NotImplementedError",
# Don't complain about script hooks
'def main\(.*\):',
# Ignore branches that don't pertain to this version of Python
"pragma: py{ignore_python_version}",
# Don't complain about IPython completion helper
"def _ipython_key_completions_",
]
[build-system]
requires = ["setuptools>=43.0.0",
"setuptools_scm>=6.2"]
build-backend = 'setuptools.build_meta'
[tool.setuptools_scm]
write_to = "extension_helpers/version.py"
[tool.isort]
profile = "black"
multi_line_output = 3
extend_skip_glob = [
"docs/*",
"setup.py"]
line_length = 100
known_third_party = ["astropy"]
known_first_party = ["reproject"]
group_by_package = true
indented_import_headings = false
length_sort_sections = ["future", "stdlib"]
[tool.black]
line-length = 100
target-version = ['py38']
[tool.numpydoc_validation]
checks = [
"all", # report on all checks, except the below
"EX01",
"SA01",
"SS06",
"ES01",
"GL08",
]
[tool.repo-review]
ignore = [
"MY", # ignore MyPy setting checks
"GH102", # auto-cancel of PRs
"PC111", # ignore using `blacken-docs` in pre-commit
"PC140", # ignore using `mypy` in pre-commit
"PC180", # ignore using `prettier` in pre-commit
"PC901", # ignore using custom update message (we have many of the default ones in our history already)
"PC170", # ignore using pygrep
"PY005", # ignore having a tests/ folder
]
[tool.ruff]
[tool.ruff.lint]
extend-select = [
"B", # flake8-bugbear
"I", # isort
"UP", # pyupgrade
]
[tool.ruff.lint.extend-per-file-ignores]
"docs/conf.py" = ["F405"] # Sphinx injects variables into namespace
"extension_helpers/_openmp_helpers.py" = ["UP032"] # Avoid using f-strings in logger
[tool.codespell]
ignore-words-list = """
ccompiler,
"""
astropy-extension-helpers-4e5b4b9/tox.ini 0000664 0000000 0000000 00000003435 15021547125 0020627 0 ustar 00root root 0000000 0000000 [tox]
envlist =
py{310,311,312,313}-test{,-osxclang,-linuxgcc}{,-conda}{,-devdeps}
py{310,311,312,313}-downstream
style
# conda jobs need this because it is pulling in tox 3, not 4
isolated_build = true
[testenv]
passenv =
CONDA_BUILD_SYSROOT,CI
setenv =
osxclang: CC=clang-20
linuxgcc: CC=gcc_linux-64
changedir =
test: .tmp/{envname}
whitelist_externals =
devdeps: bash
description =
test: run tests with pytest
devdeps: run tests with developer versions of setuptools
oldestdeps: run tests with oldest supported version of setuptools
deps =
oldestdeps: setuptools==64
devdeps: git+https://github.com/pypa/setuptools.git
conda_deps =
osxclang: clang-20
osxclang: llvm-openmp
linuxgcc: gcc_linux-64
conda_channels =
linuxgcc: conda-forge
extras =
test: test
commands =
pip freeze
test: python -c 'import setuptools; print(setuptools.__version__)'
test: pytest --pyargs extension_helpers {toxinidir}/docs --cov extension_helpers --cov-config={toxinidir}/pyproject.toml {posargs}
[testenv:py{310,311,312,313}-downstream]
changedir = .tmp/downstream
commands =
pip install setuptools setuptools_scm wheel cython numpy
pip install --no-build-isolation "astropy[test] @ git+https://github.com/astropy/astropy.git"
pytest --pyargs astropy -m "not hypothesis" -Wdefault
pip install --no-build-isolation "sunpy[all,tests] @ git+https://github.com/sunpy/sunpy.git"
pip freeze
pytest --pyargs sunpy -k "not test_saveframe and not test_hpc_observer_version and not test_hcc_observer_version and not test_simple_write_compressed_difftypeinst" -Wdefault
[testenv:style]
skip_install = true
deps =
pre-commit
commands =
pre-commit install-hooks
pre-commit run --color always --all-files --show-diff-on-failure