PythonPackage: add import module smoke tests (#20023)
This commit is contained in:
@@ -90,7 +90,7 @@ Instead of using the ``PythonPackage`` base class, you should extend
|
||||
the ``Package`` base class and implement the following custom installation
|
||||
procedure:
|
||||
|
||||
.. code-block::
|
||||
.. code-block:: python
|
||||
|
||||
def install(self, spec, prefix):
|
||||
pip = which('pip')
|
||||
@@ -255,7 +255,7 @@ Many packages are hosted on PyPI, but are developed on GitHub or another
|
||||
version control systems. The tarball can be downloaded from either
|
||||
location, but PyPI is preferred for the following reasons:
|
||||
|
||||
#. PyPI contains the bare minimum of files to install the package.
|
||||
#. PyPI contains the bare minimum number of files needed to install the package.
|
||||
|
||||
You may notice that the tarball you download from PyPI does not
|
||||
have the same checksum as the tarball you download from GitHub.
|
||||
@@ -292,19 +292,6 @@ location, but PyPI is preferred for the following reasons:
|
||||
PyPI is nice because it makes it physically impossible to
|
||||
re-release the same version of a package with a different checksum.
|
||||
|
||||
There are some reasons to prefer downloading from GitHub:
|
||||
|
||||
#. The GitHub tarball may contain unit tests.
|
||||
|
||||
As previously mentioned, the PyPI tarball contains the bare minimum
|
||||
of files to install the package. Unless explicitly specified by the
|
||||
developers, it will not contain development files like unit tests.
|
||||
If you desire to run the unit tests during installation, you should
|
||||
use the GitHub tarball instead.
|
||||
|
||||
If you really want to run these unit tests, no one will stop you from
|
||||
submitting a PR for a new package that downloads from GitHub.
|
||||
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Build system dependencies
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -569,7 +556,8 @@ If the package uses ``setuptools``, check for the following clues:
|
||||
|
||||
These are packages that are required to run the unit tests for the
|
||||
package. These dependencies can be specified using the
|
||||
``type='test'`` dependency type.
|
||||
``type='test'`` dependency type. However, the PyPI tarballs rarely
|
||||
contain unit tests, so there is usually no reason to add these.
|
||||
|
||||
In the root directory of the package, you may notice a
|
||||
``requirements.txt`` file. It may look like this file contains a list
|
||||
@@ -625,7 +613,8 @@ add run-time dependencies if they aren't needed, so you need to
|
||||
determine whether or not setuptools is needed. Grep the installation
|
||||
directory for any files containing a reference to ``setuptools`` or
|
||||
``pkg_resources``. Both modules come from ``py-setuptools``.
|
||||
``pkg_resources`` is particularly common in scripts in ``prefix/bin``.
|
||||
``pkg_resources`` is particularly common in scripts found in
|
||||
``prefix/bin``.
|
||||
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Passing arguments to setup.py
|
||||
@@ -699,49 +688,65 @@ a "package" is a directory containing files like:
|
||||
foo/baz.py
|
||||
|
||||
|
||||
whereas a "module" is a single Python file. Since ``find_packages``
|
||||
only returns packages, you'll have to determine the correct module
|
||||
names yourself. You can now add these packages and modules to the
|
||||
package like so:
|
||||
whereas a "module" is a single Python file.
|
||||
|
||||
The ``PythonPackage`` base class automatically detects these module
|
||||
names for you. If, for whatever reason, the module names detected
|
||||
are wrong, you can provide the names yourself by overriding
|
||||
``import_modules`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import_modules = ['six']
|
||||
|
||||
|
||||
When you run ``spack install --test=root py-six``, Spack will attempt
|
||||
to import the ``six`` module after installation.
|
||||
Sometimes the list of module names to import depends on how the
|
||||
package was built. For example, the ``py-pyyaml`` package has a
|
||||
``+libyaml`` variant that enables the build of a faster optimized
|
||||
version of the library. If the user chooses ``~libyaml``, only the
|
||||
``yaml`` library will be importable. If the user chooses ``+libyaml``,
|
||||
both the ``yaml`` and ``yaml.cyaml`` libraries will be available.
|
||||
This can be expressed like so:
|
||||
|
||||
These tests most often catch missing dependencies and non-RPATHed
|
||||
.. code-block:: python
|
||||
|
||||
@property
|
||||
def import_modules(self):
|
||||
modules = ['yaml']
|
||||
|
||||
if '+libyaml' in self.spec:
|
||||
modules.append('yaml.cyaml')
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
These tests often catch missing dependencies and non-RPATHed
|
||||
libraries. Make sure not to add modules/packages containing the word
|
||||
"test", as these likely won't end up in installation directory.
|
||||
"test", as these likely won't end up in the installation directory,
|
||||
or may require test dependencies like pytest to be installed.
|
||||
|
||||
These tests can be triggered by running ``spack install --test=root``
|
||||
or by running ``spack test run`` after the installation has finished.
|
||||
|
||||
""""""""""
|
||||
Unit tests
|
||||
""""""""""
|
||||
|
||||
The package you want to install may come with additional unit tests.
|
||||
By default, Spack runs:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python setup.py test
|
||||
|
||||
|
||||
if it detects that the ``setup.py`` file supports a ``test`` phase.
|
||||
You can add additional build-time or install-time tests by overriding
|
||||
``test`` or adding a custom install-time test function. For example,
|
||||
``py-numpy`` adds:
|
||||
You can add additional build-time or install-time tests by adding
|
||||
additional testing functions. For example, ``py-numpy`` adds:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
install_time_test_callbacks = ['install_test', 'import_module_test']
|
||||
|
||||
@run_after('install')
|
||||
@on_package_attributes(run_tests=True)
|
||||
def install_test(self):
|
||||
with working_dir('..'):
|
||||
python('-c', 'import numpy; numpy.test("full", verbose=2)')
|
||||
with working_dir('spack-test', create=True):
|
||||
python('-c', 'import numpy; numpy.test("full", verbose=2)')
|
||||
|
||||
|
||||
These tests can be triggered by running ``spack install --test=root``.
|
||||
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Setup file in a sub-directory
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -781,7 +786,7 @@ PythonPackage vs. packages that use Python
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are many packages that make use of Python, but packages that depend
|
||||
on Python are not necessarily ``PythonPackages``.
|
||||
on Python are not necessarily ``PythonPackage``'s.
|
||||
|
||||
"""""""""""""""""""""""
|
||||
Choosing a build system
|
||||
@@ -878,8 +883,8 @@ and ``pip`` may be a perfectly valid alternative to using Spack. The
|
||||
main advantage of Spack over ``pip`` is its ability to compile
|
||||
non-Python dependencies. It can also build cythonized versions of a
|
||||
package or link to an optimized BLAS/LAPACK library like MKL,
|
||||
resulting in calculations that run orders of magnitude faster.
|
||||
Spack does not offer a significant advantage to other python-management
|
||||
resulting in calculations that run orders of magnitudes faster.
|
||||
Spack does not offer a significant advantage over other python-management
|
||||
systems for installing and using tools like flake8 and sphinx.
|
||||
But if you need packages with non-Python dependencies like
|
||||
numpy and scipy, Spack will be very valuable to you.
|
||||
|
@@ -93,10 +93,17 @@ in the site-packages directory:
|
||||
$ python
|
||||
>>> import setuptools
|
||||
>>> setuptools.find_packages()
|
||||
['QtPy5']
|
||||
[
|
||||
'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtHelp',
|
||||
'PyQt5.QtMultimedia', 'PyQt5.QtMultimediaWidgets', 'PyQt5.QtNetwork',
|
||||
'PyQt5.QtOpenGL', 'PyQt5.QtPrintSupport', 'PyQt5.QtQml',
|
||||
'PyQt5.QtQuick', 'PyQt5.QtSvg', 'PyQt5.QtTest', 'PyQt5.QtWebChannel',
|
||||
'PyQt5.QtWebSockets', 'PyQt5.QtWidgets', 'PyQt5.QtXml',
|
||||
'PyQt5.QtXmlPatterns'
|
||||
]
|
||||
|
||||
|
||||
Large, complex packages like ``QtPy5`` will return a long list of
|
||||
Large, complex packages like ``py-pyqt5`` will return a long list of
|
||||
packages, while other packages may return an empty list. These packages
|
||||
only install a single ``foo.py`` file. In Python packaging lingo,
|
||||
a "package" is a directory containing files like:
|
||||
@@ -108,21 +115,25 @@ a "package" is a directory containing files like:
|
||||
foo/baz.py
|
||||
|
||||
|
||||
whereas a "module" is a single Python file. Since ``find_packages``
|
||||
only returns packages, you'll have to determine the correct module
|
||||
names yourself. You can now add these packages and modules to the
|
||||
package like so:
|
||||
whereas a "module" is a single Python file.
|
||||
|
||||
The ``SIPPackage`` base class automatically detects these module
|
||||
names for you. If, for whatever reason, the module names detected
|
||||
are wrong, you can provide the names yourself by overriding
|
||||
``import_modules`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import_modules = ['PyQt5']
|
||||
|
||||
|
||||
When you run ``spack install --test=root py-pyqt5``, Spack will attempt
|
||||
to import the ``PyQt5`` module after installation.
|
||||
These tests often catch missing dependencies and non-RPATHed
|
||||
libraries. Make sure not to add modules/packages containing the word
|
||||
"test", as these likely won't end up in the installation directory,
|
||||
or may require test dependencies like pytest to be installed.
|
||||
|
||||
These tests most often catch missing dependencies and non-RPATHed
|
||||
libraries.
|
||||
These tests can be triggered by running ``spack install --test=root``
|
||||
or by running ``spack test run`` after the installation has finished.
|
||||
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
External documentation
|
||||
|
@@ -10,8 +10,9 @@
|
||||
from spack.package import PackageBase, run_after
|
||||
|
||||
from llnl.util.filesystem import (working_dir, get_filetype, filter_file,
|
||||
path_contains_subdirectory, same_path)
|
||||
path_contains_subdirectory, same_path, find)
|
||||
from llnl.util.lang import match_predicate
|
||||
import llnl.util.tty as tty
|
||||
|
||||
|
||||
class PythonPackage(PackageBase):
|
||||
@@ -74,25 +75,12 @@ def configure(self, spec, prefix):
|
||||
# Default phases
|
||||
phases = ['build', 'install']
|
||||
|
||||
# Name of modules that the Python package provides
|
||||
# This is used to test whether or not the installation succeeded
|
||||
# These names generally come from running:
|
||||
#
|
||||
# >>> import setuptools
|
||||
# >>> setuptools.find_packages()
|
||||
#
|
||||
# in the source tarball directory
|
||||
import_modules = []
|
||||
|
||||
# To be used in UI queries that require to know which
|
||||
# build-system class we are using
|
||||
build_system_class = 'PythonPackage'
|
||||
|
||||
#: Callback names for build-time test
|
||||
build_time_test_callbacks = ['build_test']
|
||||
|
||||
#: Callback names for install-time test
|
||||
install_time_test_callbacks = ['import_module_test']
|
||||
install_time_test_callbacks = ['test']
|
||||
|
||||
extends('python')
|
||||
|
||||
@@ -100,6 +88,45 @@ def configure(self, spec, prefix):
|
||||
|
||||
py_namespace = None
|
||||
|
||||
@property
|
||||
def import_modules(self):
|
||||
"""Names of modules that the Python package provides.
|
||||
|
||||
These are used to test whether or not the installation succeeded.
|
||||
These names generally come from running:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>> import setuptools
|
||||
>> setuptools.find_packages()
|
||||
|
||||
in the source tarball directory. If the module names are incorrectly
|
||||
detected, this property can be overridden by the package.
|
||||
|
||||
Returns:
|
||||
list: list of strings of module names
|
||||
"""
|
||||
modules = []
|
||||
|
||||
# Python libraries may be installed in lib or lib64
|
||||
# See issues #18520 and #17126
|
||||
for lib in ['lib', 'lib64']:
|
||||
root = os.path.join(self.prefix, lib, 'python{0}'.format(
|
||||
self.spec['python'].version.up_to(2)), 'site-packages')
|
||||
# Some Python libraries are packages: collections of modules
|
||||
# distributed in directories containing __init__.py files
|
||||
for path in find(root, '__init__.py', recursive=True):
|
||||
modules.append(path.replace(root + os.sep, '', 1).replace(
|
||||
os.sep + '__init__.py', '').replace('/', '.'))
|
||||
# Some Python libraries are modules: individual *.py files
|
||||
# found in the site-packages directory
|
||||
for path in find(root, '*.py', recursive=False):
|
||||
modules.append(path.replace(root + os.sep, '', 1).replace(
|
||||
'.py', '').replace('/', '.'))
|
||||
|
||||
tty.debug('Detected the following modules: {0}'.format(modules))
|
||||
return modules
|
||||
|
||||
def setup_file(self):
|
||||
"""Returns the name of the setup file to use."""
|
||||
return 'setup.py'
|
||||
@@ -118,27 +145,6 @@ def setup_py(self, *args, **kwargs):
|
||||
with working_dir(self.build_directory):
|
||||
self.python('-s', setup, '--no-user-cfg', *args, **kwargs)
|
||||
|
||||
def _setup_command_available(self, command):
|
||||
"""Determines whether or not a setup.py command exists.
|
||||
|
||||
Args:
|
||||
command (str): The command to look for
|
||||
|
||||
Returns:
|
||||
bool: True if the command is found, else False
|
||||
"""
|
||||
kwargs = {
|
||||
'output': os.devnull,
|
||||
'error': os.devnull,
|
||||
'fail_on_error': False
|
||||
}
|
||||
|
||||
python = inspect.getmodule(self).python
|
||||
setup = self.setup_file()
|
||||
|
||||
python('-s', setup, '--no-user-cfg', command, '--help', **kwargs)
|
||||
return python.returncode == 0
|
||||
|
||||
# The following phases and their descriptions come from:
|
||||
# $ python setup.py --help-commands
|
||||
|
||||
@@ -359,33 +365,16 @@ def check_args(self, spec, prefix):
|
||||
|
||||
# Testing
|
||||
|
||||
def build_test(self):
|
||||
"""Run unit tests after in-place build.
|
||||
|
||||
These tests are only run if the package actually has a 'test' command.
|
||||
"""
|
||||
if self._setup_command_available('test'):
|
||||
args = self.test_args(self.spec, self.prefix)
|
||||
|
||||
self.setup_py('test', *args)
|
||||
|
||||
def test_args(self, spec, prefix):
|
||||
"""Arguments to pass to test."""
|
||||
return []
|
||||
|
||||
run_after('build')(PackageBase._run_default_build_time_test_callbacks)
|
||||
|
||||
def import_module_test(self):
|
||||
"""Attempts to import the module that was just installed.
|
||||
|
||||
This test is only run if the package overrides
|
||||
:py:attr:`import_modules` with a list of module names."""
|
||||
def test(self):
|
||||
"""Attempts to import modules of the installed package."""
|
||||
|
||||
# Make sure we are importing the installed modules,
|
||||
# not the ones in the current directory
|
||||
with working_dir('spack-test', create=True):
|
||||
for module in self.import_modules:
|
||||
self.python('-c', 'import {0}'.format(module))
|
||||
# not the ones in the source directory
|
||||
for module in self.import_modules:
|
||||
self.run_test(inspect.getmodule(self).python.path,
|
||||
['-c', 'import {0}'.format(module)],
|
||||
purpose='checking import of {0}'.format(module),
|
||||
work_dir='spack-test')
|
||||
|
||||
run_after('install')(PackageBase._run_default_install_time_test_callbacks)
|
||||
|
||||
|
@@ -4,11 +4,12 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from llnl.util.filesystem import working_dir, join_path
|
||||
from spack.build_systems.python import PythonPackage
|
||||
from spack.directives import depends_on, extends
|
||||
from spack.package import PackageBase, run_after
|
||||
import os
|
||||
|
||||
|
||||
class SIPPackage(PackageBase):
|
||||
@@ -36,13 +37,15 @@ class SIPPackage(PackageBase):
|
||||
sip_module = 'sip'
|
||||
|
||||
#: Callback names for install-time test
|
||||
install_time_test_callbacks = ['import_module_test']
|
||||
install_time_test_callbacks = ['test']
|
||||
|
||||
extends('python')
|
||||
|
||||
depends_on('qt')
|
||||
depends_on('py-sip')
|
||||
|
||||
import_modules = PythonPackage.import_modules
|
||||
|
||||
def python(self, *args, **kwargs):
|
||||
"""The python ``Executable``."""
|
||||
inspect.getmodule(self).python(*args, **kwargs)
|
||||
@@ -98,17 +101,7 @@ def install_args(self):
|
||||
|
||||
# Testing
|
||||
|
||||
def import_module_test(self):
|
||||
"""Attempts to import the module that was just installed.
|
||||
|
||||
This test is only run if the package overrides
|
||||
:py:attr:`import_modules` with a list of module names."""
|
||||
|
||||
# Make sure we are importing the installed modules,
|
||||
# not the ones in the current directory
|
||||
with working_dir('spack-test', create=True):
|
||||
for module in self.import_modules:
|
||||
self.python('-c', 'import {0}'.format(module))
|
||||
test = PythonPackage.test
|
||||
|
||||
run_after('install')(PackageBase._run_default_install_time_test_callbacks)
|
||||
|
||||
|
@@ -1761,7 +1761,7 @@ def run_test(self, exe, options=[], expected=[], status=0,
|
||||
work_dir (str or None): path to the smoke test directory
|
||||
"""
|
||||
wdir = '.' if work_dir is None else work_dir
|
||||
with fsys.working_dir(wdir):
|
||||
with fsys.working_dir(wdir, create=True):
|
||||
try:
|
||||
runner = which(exe)
|
||||
if runner is None and skip_missing:
|
||||
|
Reference in New Issue
Block a user