aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArthur Zamarin <arthurzam@gentoo.org>2023-03-24 14:07:43 +0300
committerArthur Zamarin <arthurzam@gentoo.org>2023-03-24 14:07:43 +0300
commit5033316f416e6ff829f6c04a3d1f78d5733e93dd (patch)
treebf87f3994d1c4f12a06a6a39239f904a725573f6
parentCI: add Python 3.12 alphas (diff)
downloadsnakeoil-5033316f416e6ff829f6c04a3d1f78d5733e93dd.tar.gz
snakeoil-5033316f416e6ff829f6c04a3d1f78d5733e93dd.tar.bz2
snakeoil-5033316f416e6ff829f6c04a3d1f78d5733e93dd.zip
remove deprecated distutils_extensions
Resolves: https://github.com/pkgcore/snakeoil/issues/77 Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
-rw-r--r--src/snakeoil/dist/distutils_extensions.py1049
1 files changed, 0 insertions, 1049 deletions
diff --git a/src/snakeoil/dist/distutils_extensions.py b/src/snakeoil/dist/distutils_extensions.py
deleted file mode 100644
index 10a77ed..0000000
--- a/src/snakeoil/dist/distutils_extensions.py
+++ /dev/null
@@ -1,1049 +0,0 @@
-"""
-A collection of distutils extensions adding things like automatic 2to3
-translation, a test runner, and working around broken stdlib extensions CFLAG
-passing in distutils.
-
-Specifically, this module is only meant to be imported in setup.py scripts.
-"""
-
-import copy
-import errno
-import inspect
-import operator
-import os
-import re
-import shlex
-import shutil
-import subprocess
-import sys
-import textwrap
-import warnings
-from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout
-from multiprocessing import cpu_count
-
-from setuptools import find_packages
-from setuptools.command import build_py as dst_build_py
-from setuptools.command import install as dst_install
-from setuptools.dist import Distribution
-from distutils import log
-from distutils.command import build as dst_build
-from distutils.command import build_ext as dst_build_ext
-from distutils.command import build_scripts as dst_build_scripts
-from distutils.command import config as dst_config
-from distutils.command import sdist as dst_sdist
-from distutils.core import Command, Extension
-from distutils.errors import DistutilsError, DistutilsExecError
-
-from ..contexts import syspath
-from ..version import get_git_version
-from .generate_docs import generate_html, generate_man
-
-warnings.warn("the distutils_extensions module is deprecated", DeprecationWarning, stacklevel=2)
-
-# forcibly disable lazy module loading
-os.environ['SNAKEOIL_DEMANDIMPORT'] = 'false'
-
-# top level repo/tarball directory
-REPODIR = os.environ.get('PKGDIST_REPODIR')
-if REPODIR is None:
- # hack to verify we're running under a setup.py script and grab its info
- for _frameinfo in reversed(inspect.stack(0)):
- _filename = _frameinfo[1]
- if os.path.basename(_filename) == 'setup.py':
- REPODIR = os.path.dirname(os.path.abspath(_filename))
- break
- _filename_dir = os.path.dirname(os.path.abspath(_filename))
- if os.path.exists(os.path.join(_filename_dir, 'setup.py')):
- REPODIR = _filename_dir
- break
- else:
- REPODIR = os.getcwd() # try CWD
- if not os.path.exists(os.path.join(REPODIR, 'setup.py')):
- raise ImportError('this module is only meant to be imported in setup.py scripts')
-
-# running under pip
-PIP = os.path.basename(os.environ.get('_', '')) == 'pip' or any(part.startswith('pip-') for part in REPODIR.split(os.sep))
-
-# executable scripts directory
-SCRIPTS_DIR = os.path.join(REPODIR, 'bin')
-
-
-def find_moduledir(searchdir=REPODIR):
- """Determine a module's directory path.
-
- Based on the assumption that the project is only distributing one main
- module.
- """
- modules = []
- moduledir = None
- searchdir_depth = len(searchdir.split('/'))
- # allow modules to be found inside a top-level dir, e.g. 'src'
- searchdir_depth += 1
-
- # look for a top-level module
- for root, dirs, files in os.walk(searchdir):
- # only descend to a specified level
- if len(root.split('/')) > searchdir_depth + 1:
- continue
- if '__init__.py' in files:
- # only match modules with __title__ defined in the main module
- with open(os.path.join(root, '__init__.py'), encoding='utf-8') as f:
- try:
- if re.search(r'^__title__\s*=\s*[\'"]([^\'"]*)[\'"]',
- f.read(), re.MULTILINE):
- modules.append(root)
- except AttributeError:
- continue
-
- if len(modules) == 1:
- moduledir = modules[0]
- elif len(modules) > 1:
- raise ValueError(
- 'Multiple main modules found in %r: %s' % (
- searchdir, ', '.join(os.path.basename(x) for x in modules)))
-
- if moduledir is None:
- raise ValueError('No main module found')
-
- return moduledir
-
-
-# determine the main module we're being used to package
-MODULEDIR = find_moduledir()
-PACKAGEDIR = os.path.dirname(MODULEDIR)
-MODULE_NAME = os.path.basename(MODULEDIR)
-
-# running against git/unreleased version
-GIT = not os.path.exists(os.path.join(MODULEDIR, '_verinfo.py'))
-
-
-def module_version(moduledir=MODULEDIR):
- """Determine a module's version.
-
- Based on the assumption that a module defines __version__.
- """
- from .utilities import module_version
- return module_version(REPODIR, moduledir)
-
-
-def generate_verinfo(target_dir):
- """Generate project version module.
-
- This is used by the --version option in interactive programs among
- other things.
- """
- data = get_git_version(REPODIR)
- path = os.path.join(target_dir, '_verinfo.py')
- log.info(f'generating version info: {path}')
- with open(path, 'w') as f:
- f.write('version_info=%r' % (data,))
- return path
-
-
-def readme(topdir=REPODIR):
- """Determine a project's long description."""
- for doc in ('README.rst', 'README'):
- try:
- with open(os.path.join(topdir, doc), encoding='utf-8') as f:
- return f.read()
- except IOError as e:
- if e.errno == errno.ENOENT:
- pass
- else:
- raise
-
- return None
-
-
-class BinaryDistribution(Distribution):
- """Distribution forcing binary wheel package creation.
-
- Set the 'distclass' setup param to this class to force binary wheel creation.
- """
-
- def has_ext_modules(self):
- return True
-
-
-def setup():
- """Parameters and commands for setuptools."""
- # pip installing from git forces development versions to be used
- if PIP and GIT:
- install_deps = _requires('dev.txt')
- else:
- install_deps = _requires('install.txt')
-
- params = {
- 'name': MODULE_NAME,
- 'version': module_version(),
- 'long_description': readme(),
- 'packages': find_packages(PACKAGEDIR),
- 'package_dir': {'': os.path.basename(PACKAGEDIR)},
- 'install_requires': install_deps,
- 'tests_require': _requires('test.txt'),
- 'python_requires': '>=3.8',
- }
-
- cmds = {
- 'sdist': sdist,
- 'build_py': build_py,
- 'install': install,
- 'test': pytest,
- 'lint': pylint,
- 'build': build,
- 'build_docs': build_docs,
- 'install_docs': install_docs,
- 'build_html': build_html,
- 'install_html': install_html,
- 'build_man': build_man,
- 'install_man': install_man,
- }
-
- # check for scripts
- if os.path.exists(SCRIPTS_DIR):
- params['scripts'] = [os.path.join(SCRIPTS_DIR, x) for x in os.listdir(SCRIPTS_DIR)]
- cmds['build_scripts'] = build_scripts
-
- # set default commands -- individual commands can be overridden as required
- params['cmdclass'] = cmds
-
- return params, cmds
-
-
-def _requires(filename):
- """Determine a project's various dependencies from requirements files."""
- try:
- with open(os.path.join(REPODIR, 'requirements', filename)) as f:
- return f.read().splitlines()
- except FileNotFoundError:
- pass
- return None
-
-
-def get_file_paths(path):
- """Get list of all file paths under a given path."""
- for root, dirs, files in os.walk(path):
- for f in files:
- yield os.path.join(root, f)[len(path):].lstrip('/')
-
-
-def data_mapping(host_prefix, path, skip=None):
- """Map repo paths to host paths for installed data files."""
- skip = list(skip) if skip is not None else []
- for root, dirs, files in os.walk(path):
- host_path = os.path.join(host_prefix, root.partition(path)[2].lstrip('/'))
- repo_path = os.path.join(path, root.partition(path)[2].lstrip('/'))
- if repo_path not in skip:
- yield (host_path, [os.path.join(root, x) for x in files
- if os.path.join(root, x) not in skip])
-
-
-class sdist(dst_sdist.sdist):
- """sdist command wrapper to bundle generated files for release."""
-
- def make_release_tree(self, base_dir, files):
- """Create and populate the directory tree that is put in source tars.
-
- This copies or hardlinks "normal" source files that should go
- into the release and adds generated files that should not
- exist in a working tree.
- """
- build_man = self.reinitialize_command('build_man')
- # force sphinx to run at our chosen verbosity
- build_man.verbosity = self.verbose
- build_man.ensure_finalized()
- if paths := build_man.build():
- built_path, dist_path = paths
- shutil.copytree(os.path.join(os.getcwd(), built_path),
- os.path.join(base_dir, dist_path))
-
- dst_sdist.sdist.make_release_tree(self, base_dir, files)
-
- # generate version module
- build_py = self.reinitialize_command('build_py')
- build_py.ensure_finalized()
- generate_verinfo(os.path.join(
- base_dir, build_py.package_dir.get('', ''), MODULE_NAME))
-
- # replace pyproject.toml file with release version if it exists
- try:
- shutil.copy(os.path.join(base_dir, 'requirements', 'pyproject.toml'), base_dir)
- except FileNotFoundError:
- pass
-
- def run(self):
- build_ext = self.reinitialize_command('build_ext')
- build_ext.ensure_finalized()
-
- # generate cython extensions if any exist
- extensions = list(cython_pyx())
- if extensions:
- from Cython.Build import cythonize
- cythonize(extensions, nthreads=cpu_count())
-
- super().run()
-
-
-class build_py(dst_build_py.build_py):
- """build_py command wrapper."""
-
- user_options = dst_build_py.build_py.user_options + \
- [("inplace", "i", "do any source conversions in place")]
-
- generate_verinfo = True
-
- def initialize_options(self):
- super().initialize_options()
- self.inplace = False
-
- def finalize_options(self):
- self.inplace = bool(self.inplace)
- if self.inplace:
- self.build_lib = PACKAGEDIR
- super().finalize_options()
-
- def _run_generate_verinfo(self, rebuilds=None):
- ver_path = generate_verinfo(os.path.join(self.build_lib, MODULE_NAME))
- self.byte_compile([ver_path])
- if rebuilds is not None:
- rebuilds.append((ver_path, os.lstat(ver_path).st_mtime))
-
- def run(self):
- super().run()
- if self.generate_verinfo:
- self._run_generate_verinfo()
-
-
-class build_docs(Command):
- """Generic documentation build command."""
-
- # use custom verbosity option since distutils appears to
- # statically assign the default verbose option
- user_options = [
- ('force', 'f', 'force build as needed'),
- ('verbosity', 'v', 'run verbosely (default disabled)'),
- ]
-
- content_search_path = ()
- sphinx_targets = None
-
- def initialize_options(self):
- self.force = False
- self.verbosity = 0
-
- def finalize_options(self):
- self.force = bool(self.force)
- self.verbosity = int(bool(self.verbosity))
-
- @property
- def skip(self):
- if not os.path.exists(os.path.join(REPODIR, self.exists_path)):
- log.info(f'{self.__class__.__name__}: nonexistent content, skipping build')
- return True
- elif any(os.path.exists(x) for x in self.content_search_path):
- # don't rebuild if one of the output dirs exist
- log.info(f'{self.__class__.__name__}: already built, skipping regeneration')
- return True
- return False
-
- def _generate_doc_content(self):
- """Hook to generate custom doc content used by sphinx."""
-
- def build(self):
- if self.force or not self.skip:
- # TODO: report this to upstream sphinx
- # Workaround for sphinx doing include directive path mangling in
- # order to interpret absolute paths "correctly", but at the same
- # time causing relative paths to fail. This just bypasses the
- # sphinx mangling and lets docutils handle include directives
- # directly which works as expected.
- from docutils.parsers.rst.directives.misc import Include as BaseInclude
- from sphinx.directives.other import Include
- Include.run = BaseInclude.run
-
- # Use a built version for the man page generation process that
- # imports script modules.
- build_py = self.reinitialize_command('build_py')
- build_py.ensure_finalized()
- self.run_command('build_py')
-
- # Override the module search path before running sphinx. This fixes
- # generating man pages for scripts that need to import modules
- # generated via 2to3 or other conversions instead of straight from
- # the build directory.
- with syspath(os.path.abspath(build_py.build_lib)):
- # Generating man pages with sphinx is unnecessarily noisy by
- # default since sphinx assumes files are laid out in a manner
- # for technical doc generation. Therefore, suppress all stderr
- # by default unless verbose mode is enabled.
- with suppress(self.verbosity):
- self._generate_doc_content()
- for target in self.sphinx_targets:
- build_sphinx = self.reinitialize_command('build_sphinx')
- build_sphinx.builder = target
- build_sphinx.ensure_finalized()
- self.run_command('build_sphinx')
- return self.content_search_path
-
- def run(self):
- if self.sphinx_targets:
- # run regular build
- self.build()
- else:
- # build all docs
- for target in ('man', 'html'):
- cmd = f'build_{target}'
- build_cmd = self.reinitialize_command(cmd)
- build_cmd.ensure_finalized()
- build_cmd.build()
-
-
-class build_man(build_docs):
- """Build man pages."""
-
- description = 'build man pages'
- exists_path = 'doc/man'
- content_search_path = ('build/sphinx/man', 'man')
- sphinx_targets = ('man',)
-
- def _generate_doc_content(self):
- # generate man page content for scripts we create
- if 'build_scripts' in self.distribution.cmdclass:
- generate_man(REPODIR, PACKAGEDIR, MODULE_NAME)
-
-
-class build_html(build_docs):
- """Build html docs."""
-
- description = 'build HTML documentation'
- exists_path = 'doc'
- content_search_path = ('build/sphinx/html', 'html')
- sphinx_targets = ('html',)
-
- def _generate_doc_content(self):
- # generate man pages -- html versions of man pages are provided
- self.run_command('build_man')
-
- # generate API docs
- generate_html(REPODIR, PACKAGEDIR, MODULE_NAME)
-
-
-class build_ext(dst_build_ext.build_ext):
- """Build native extensions."""
-
- user_options = dst_build_ext.build_ext.user_options + [
- ("disable-distutils-flag-fixing", None,
- "disable fixing of issue 969718 in python, adding missing -fno-strict-aliasing"),
- ]
-
- def initialize_options(self):
- super().initialize_options()
- self.disable_distutils_flag_fixing = False
- self.default_header_install_dir = None
-
- def finalize_options(self):
- super().finalize_options()
- # add header install dir to the search path
- # (fixes virtualenv builds for consumer extensions)
- self.set_undefined_options(
- 'install',
- ('install_headers', 'default_header_install_dir'))
- if self.default_header_install_dir:
- self.default_header_install_dir = os.path.dirname(self.default_header_install_dir)
- for e in self.extensions:
- # include_dirs may actually be shared between multiple extensions
- if self.default_header_install_dir not in e.include_dirs:
- e.include_dirs.append(self.default_header_install_dir)
-
- @staticmethod
- def determine_ext_lang(ext_path):
- """Determine file extensions for generated cython extensions."""
- with open(ext_path) as f:
- for line in f:
- line = line.lstrip()
- if not line:
- continue
- elif line[0] != '#':
- return None
- line = line[1:].lstrip()
- if line[:10] == 'distutils:':
- key, _, value = [s.strip() for s in line[10:].partition('=')]
- if key == 'language':
- return value
- else:
- return None
-
- def no_cythonize(self):
- """Determine file paths for generated cython extensions."""
- extensions = copy.deepcopy(self.extensions)
- for extension in extensions:
- sources = []
- for sfile in extension.sources:
- path, ext = os.path.splitext(sfile)
- if ext in ('.pyx', '.py'):
- lang = build_ext.determine_ext_lang(sfile)
- if lang == 'c++':
- ext = '.cpp'
- else:
- ext = '.c'
- sfile = path + ext
- sources.append(sfile)
- extension.sources[:] = sources
- return extensions
-
- def run(self):
- # ensure that the platform checks were performed
- self.run_command('config')
-
- # only regenerate cython extensions if requested or required
- use_cython = (
- os.environ.get('USE_CYTHON', False) or
- any(not os.path.exists(x) for ext in self.no_cythonize() for x in ext.sources))
- if use_cython:
- from Cython.Build import cythonize
- cythonize(self.extensions, nthreads=cpu_count())
-
- self.extensions = self.no_cythonize()
- super().run()
-
- def build_extensions(self):
- for x in ("compiler_so", "compiler", "compiler_cxx"):
- if self.debug:
- l = [y for y in getattr(self.compiler, x) if y != '-DNDEBUG']
- l.append('-Wall')
- setattr(self.compiler, x, l)
- if not self.disable_distutils_flag_fixing:
- val = getattr(self.compiler, x)
- if "-fno-strict-aliasing" not in val:
- val.append("-fno-strict-aliasing")
- if getattr(self.distribution, 'check_defines', None):
- val = getattr(self.compiler, x)
- for d, result in self.distribution.check_defines.items():
- if result:
- val.append(f'-D{d}=1')
- else:
- val.append(f'-U{d}')
- super().build_extensions()
-
-
-class build_scripts(dst_build_scripts.build_scripts):
- """Create and build (copy and modify shebang lines) wrapper scripts."""
-
- def finalize_options(self):
- super().finalize_options()
- self.script_dir = os.path.join(
- os.path.dirname(self.build_dir), '.generated_scripts')
- self.scripts = [os.path.join(self.script_dir, os.path.basename(x)) for x in self.scripts]
-
- def run(self):
- self.mkpath(self.script_dir)
- for script in self.scripts:
- with open(script, 'w') as f:
- f.write(textwrap.dedent(f"""\
- #!{sys.executable}
- from os.path import basename
- from {MODULE_NAME} import scripts
- scripts.run(basename(__file__))
- """))
- self.copy_scripts()
-
-
-class build(dst_build.build):
- """Generic build command."""
-
- user_options = dst_build.build.user_options[:]
- user_options.append(('enable-man-pages', None, 'build man pages'))
- user_options.append(('enable-html-docs', None, 'build html docs'))
-
- boolean_options = dst_build.build.boolean_options[:]
- boolean_options.extend(['enable-man-pages', 'enable-html-docs'])
-
- sub_commands = dst_build.build.sub_commands[:]
- sub_commands.append(('build_ext', None))
- sub_commands.append(('build_py', None))
- sub_commands.append(('build_scripts', None))
- sub_commands.append(('build_man', operator.attrgetter('enable_man_pages')))
- sub_commands.append(('build_html', operator.attrgetter('enable_html_docs')))
-
- def initialize_options(self):
- super().initialize_options()
- self.enable_man_pages = False
- self.enable_html_docs = False
-
- def finalize_options(self):
- super().finalize_options()
- if self.enable_man_pages is None:
- path = os.path.dirname(os.path.abspath(__file__))
- self.enable_man_pages = not os.path.exists(os.path.join(path, 'man'))
-
- if self.enable_html_docs is None:
- self.enable_html_docs = False
-
-
-class install_docs(Command):
- """Generic documentation install command."""
-
- content_search_path = ()
- description = "install documentation"
- user_options = [
- ('build-dir=', None, 'build directory'),
- ('docdir=', None, 'override docs install path'),
- ('htmldir=', None, 'override html install path'),
- ('mandir=', None, 'override man install path'),
- ]
- build_command = None
-
- def initialize_options(self):
- self.root = None
- self.prefix = None
- self.docdir = None
- self.htmldir = None
- self.mandir = None
- self.build_dir = None
-
- def finalize_options(self):
- self.set_undefined_options(
- 'install',
- ('root', 'root'),
- ('install_base', 'prefix'),
- )
- if not self.root:
- self.root = '/'
- if self.docdir is None:
- self.docdir = os.path.join(
- self.prefix, 'share', 'doc',
- MODULE_NAME + f'-{module_version()}',
- )
- if self.htmldir is None:
- self.htmldir = os.path.join(self.docdir, 'html')
- if self.mandir is None:
- self.mandir = os.path.join(self.prefix, 'share', 'man')
-
- def find_content(self):
- """Determine if generated doc files exist."""
- for possible_path in self.content_search_path:
- if self.build_dir is not None:
- possible_path = os.path.join(self.build_dir, possible_path)
- possible_path = os.path.join(REPODIR, possible_path)
- if os.path.isdir(possible_path):
- return possible_path
- else:
- return None
-
- def _map_paths(self, content):
- """Map doc files to install paths."""
- return {x: x for x in content}
-
- @property
- def install_dir(self):
- """Target install directory."""
- return self.docdir
-
- def install(self):
- """Install docs to target dirs."""
- source_path = self.find_content()
- if source_path is None:
- raise DistutilsExecError('no generated sphinx content')
-
- # determine mapping from doc files to install paths
- content = self._map_paths(get_file_paths(source_path))
-
- # create directories
- directories = set(map(os.path.dirname, content.values()))
- directories.discard('')
- for x in sorted(directories):
- self.mkpath(os.path.join(self.install_dir, x))
-
- # copy docs over
- for src, dst in sorted(content.items()):
- self.copy_file(
- os.path.join(source_path, src),
- os.path.join(self.install_dir, dst))
-
- def run(self):
- if self.build_command is not None:
- if not os.path.exists(os.path.join(REPODIR, self.exists_path)):
- log.info(f'{self.__class__.__name__}: nonexistent content, skipping install')
- return
- # run regular install, rebuilding as necessary
- try:
- self.install()
- except DistutilsExecError:
- self.run_command(self.build_command)
- self.install()
- else:
- # install all docs that have been generated
- for target in ('man', 'html'):
- install_cmd = self.reinitialize_command(f'install_{target}')
- install_cmd.docdir = self.docdir
- install_cmd.htmldir = self.htmldir
- install_cmd.mandir = self.mandir
- install_cmd.ensure_finalized()
-
- if not os.path.exists(os.path.join(REPODIR, install_cmd.exists_path)):
- log.info(f'{self.__class__.__name__}: nonexistent {target} docs, skipping install')
- continue
-
- try:
- install_cmd.install()
- except DistutilsExecError:
- log.info(f'{self.__class__.__name__}: built {target} pages missing, skipping install')
-
-
-class install_html(install_docs):
- """Install html documentation."""
-
- description = "install HTML documentation"
- exists_path = build_html.exists_path
- content_search_path = build_html.content_search_path
- build_command = 'build_html'
-
- @property
- def install_dir(self):
- return self.htmldir
-
-
-class install_man(install_docs):
- """Install man pages."""
-
- description = "install man pages"
- exists_path = build_man.exists_path
- content_search_path = build_man.content_search_path
- build_command = 'build_man'
-
- @property
- def install_dir(self):
- return self.mandir
-
- def _map_paths(self, content):
- d = {}
- for x in content:
- if len(x) >= 3 and x[-2] == '.' and x[-1].isdigit():
- # Only consider extensions .1, .2, .3, etc, and files that
- # have at least a single char beyond the extension (thus ignore
- # .1, but allow a.1).
- d[x] = f'man{x[-1]}/{os.path.basename(x)}'
- return d
-
-
-class install(dst_install.install):
- """Generic install command."""
-
- user_options = dst_install.install.user_options[:]
- user_options.extend([
- ('enable-man-pages', None, 'install man pages'),
- ('enable-html-docs', None, 'install html docs'),
- ('docdir=', None, 'override docs install path'),
- ('htmldir=', None, 'override html install path'),
- ('mandir=', None, 'override man install path'),
- ])
-
- boolean_options = dst_install.install.boolean_options[:]
- boolean_options.extend(['enable-man-pages', 'enable-html-docs'])
-
- def initialize_options(self):
- super().initialize_options()
- self.enable_man_pages = False
- self.enable_html_docs = False
- self.docdir = None
- self.htmldir = None
- self.mandir = None
-
- def finalize_options(self):
- build_options = self.distribution.command_options.setdefault('build', {})
- build_options['enable_html_docs'] = ('command_line', self.enable_html_docs and 1 or 0)
- man_pages = self.enable_man_pages
- if man_pages and os.path.exists('man'):
- man_pages = False
- build_options['enable_man_pages'] = ('command_line', man_pages and 1 or 0)
- super().finalize_options()
-
- sub_commands = dst_install.install.sub_commands[:]
- sub_commands.append(('install_man', operator.attrgetter('enable_man_pages')))
- sub_commands.append(('install_html', operator.attrgetter('enable_html_docs')))
-
- def run(self):
- super().run()
-
- # don't install docs by default
- if self.enable_man_pages:
- install_man = self.reinitialize_command('install_man')
- if install_man.find_content() is None:
- raise DistutilsError("built man pages missing")
- else:
- install_man.docdir = self.docdir
- install_man.mandir = self.mandir
- install_man.ensure_finalized()
- self.run_command('install_man')
-
- if self.enable_html_docs:
- install_html = self.reinitialize_command('install_html')
- if install_html.find_content() is None:
- raise DistutilsError("built html docs missing")
- else:
- install_html.docdir = self.docdir
- install_html.htmldir = self.htmldir
- install_html.ensure_finalized()
- self.run_command('install_html')
-
-
-class pytest(Command):
- """Run tests using pytest."""
-
- description = "run unit tests in a built copy using pytest"
- user_options = [
- ('pytest-args=', 'a', 'arguments to pass to py.test'),
- ('coverage', 'c', 'generate coverage info'),
- ('skip-build', 's', 'skip building the module'),
- ('test-dir=', 'd', 'directory to source tests from'),
- ('report=', 'r', 'generate and/or show a coverage report'),
- ('jobs=', 'j', 'run X parallel tests at once'),
- ('match=', 'k', 'run only tests that match the provided expressions'),
- ('targets=', 't', 'target tests to run'),
- ]
-
- def initialize_options(self):
- self.pytest_args = ''
- self.coverage = False
- self.skip_build = False
- self.test_dir = None
- self.match = None
- self.targets = None
- self.jobs = None
- self.report = None
-
- def finalize_options(self):
- # if a test dir isn't specified explicitly try to find one
- if self.test_dir is None:
- for path in (os.path.join(REPODIR, 'test', 'module'),
- os.path.join(REPODIR, 'test'),
- os.path.join(REPODIR, 'tests', 'module'),
- os.path.join(REPODIR, 'tests'),
- os.path.join(MODULEDIR, 'test'),
- os.path.join(MODULEDIR, 'tests')):
- if os.path.exists(path):
- self.test_dir = path
- break
- else:
- raise DistutilsExecError('cannot automatically determine test directory')
-
- self.pytest_cmd = ['pytest', f'--verbosity={self.verbose}']
- if self.targets is not None:
- targets = [os.path.join(self.test_dir, x) for x in self.targets.split()]
- self.pytest_cmd.extend(targets)
- else:
- self.pytest_cmd.append(self.test_dir)
- self.coverage = bool(self.coverage)
- self.skip_build = bool(self.skip_build)
- if self.match is not None:
- self.pytest_cmd.extend(['-k', self.match])
-
- if self.coverage or self.report:
- try:
- import pytest_cov
- self.pytest_cmd.extend(['--cov', MODULE_NAME])
- except ImportError:
- raise DistutilsExecError('install pytest-cov for coverage support')
-
- if self.report is None:
- # disable coverage report output
- self.pytest_cmd.extend(['--cov-report='])
- else:
- self.pytest_cmd.extend(['--cov-report', self.report])
-
- if self.jobs is not None:
- try:
- import xdist
- self.pytest_cmd.extend(['-n', self.jobs])
- except ImportError:
- raise DistutilsExecError('install pytest-xdist for -j/--jobs support')
-
- # add custom pytest args
- self.pytest_cmd.extend(shlex.split(self.pytest_args))
-
- def run(self):
- try:
- import pytest
- except ImportError:
- raise DistutilsExecError('pytest is not installed')
-
- env = os.environ.copy()
-
- if self.skip_build:
- packagedir = PACKAGEDIR
- else:
- # install package into builddir
- install = self.reinitialize_command('install')
- install.prefix = os.path.join(REPODIR, 'build', 'install')
- install.ensure_finalized()
- self.run_command('install')
- packagedir = os.path.abspath(install.install_lib)
- # prepend built package directory to PYTHONPATH
- pythonpath = os.environ.get('PYTHONPATH', '')
- env['PYTHONPATH'] = f"{packagedir}:{pythonpath}"
-
- p = subprocess.run(self.pytest_cmd, env=env)
- sys.exit(p.returncode)
-
-
-class pylint(Command):
- """Run pylint on a module."""
-
- description = "run pylint on a module"
- user_options = [
- ('errors-only', 'E', 'Check only errors with pylint'),
- ('output-format=', 'f', 'Change the output format'),
- ]
-
- def initialize_options(self):
- self.errors_only = False
- self.output_format = 'colorized'
-
- def finalize_options(self):
- self.errors_only = bool(self.errors_only)
-
- def run(self):
- try:
- from pylint import lint
- except ImportError:
- raise DistutilsExecError('pylint is not installed')
-
- lint_args = [MODULEDIR]
- rcfile = os.path.join(REPODIR, '.pylintrc')
- if os.path.exists(rcfile):
- lint_args.extend(['--rcfile', rcfile])
- if self.errors_only:
- lint_args.append('-E')
- lint_args.extend(['--output-format', self.output_format])
- lint.Run(lint_args)
-
-
-def print_check(message, if_yes='found', if_no='not found'):
- """Decorator to print pre/post-check messages."""
- def sub_decorator(f):
- def sub_func(*args, **kwargs):
- sys.stderr.write(f'-- {message}\n')
- result = f(*args, **kwargs)
- result_output = if_yes if result else if_no
- sys.stderr.write(f'-- {message} -- {result_output}\n')
- return result
- sub_func.pkgdist_config_decorated = True
- return sub_func
- return sub_decorator
-
-
-def cache_check(cache_key):
- """Method decorate to cache check result."""
- def sub_decorator(f):
- def sub_func(self, *args, **kwargs):
- if cache_key in self.cache:
- return self.cache[cache_key]
- result = f(self, *args, **kwargs)
- self.cache[cache_key] = result
- return result
- sub_func.pkgdist_config_decorated = True
- return sub_func
- return sub_decorator
-
-
-def check_define(define_name):
- """Method decorator to store check result."""
- def sub_decorator(f):
- @cache_check(define_name)
- def sub_func(self, *args, **kwargs):
- result = f(self, *args, **kwargs)
- self.check_defines[define_name] = result
- return result
- sub_func.pkgdist_config_decorated = True
- return sub_func
- return sub_decorator
-
-
-class config(dst_config.config):
- """Perform platform checks for extension build."""
-
- user_options = dst_config.config.user_options + [
- ("cache-path", "C", "path to read/write configuration cache"),
- ]
-
- def initialize_options(self):
- self.cache_path = None
- self.build_base = None
- super().initialize_options()
-
- def finalize_options(self):
- if self.cache_path is None:
- self.set_undefined_options(
- 'build',
- ('build_base', 'build_base'))
- self.cache_path = os.path.join(self.build_base, 'config.cache')
- super().finalize_options()
-
- def _cache_env_key(self):
- return (self.cc, self.include_dirs, self.libraries, self.library_dirs)
-
- @cache_check('_sanity_check')
- @print_check('Performing basic C toolchain sanity check', 'works', 'broken')
- def _sanity_check(self):
- return self.try_link("int main(int argc, char *argv[]) { return 0; }")
-
- def run(self):
- with syspath(PACKAGEDIR, MODULE_NAME == 'snakeoil'):
- from snakeoil.pickling import dump, load
-
- # try to load the cached results
- try:
- with open(self.cache_path, 'rb') as f:
- cache_db = load(f)
- except (OSError, IOError):
- cache_db = {}
- else:
- if self._cache_env_key() == cache_db.get('env_key'):
- sys.stderr.write(f'-- Using cache: {self.cache_path}\n')
- else:
- sys.stderr.write('-- Build environment changed, discarding cache\n')
- cache_db = {}
-
- self.cache = cache_db.get('cache', {})
- self.check_defines = {}
-
- if not self._sanity_check():
- sys.stderr.write('The C toolchain is unable to compile & link a simple C program!\n')
- sys.exit(1)
-
- # run all decorated methods
- for k in dir(self):
- if k.startswith('_'):
- continue
- if hasattr(getattr(self, k), 'pkgdist_config_decorated'):
- getattr(self, k)()
-
- # store results in Distribution instance
- self.distribution.check_defines = self.check_defines
- # store updated cache
- cache_db = {
- 'cache': self.cache,
- 'env_key': self._cache_env_key(),
- }
- self.mkpath(os.path.dirname(self.cache_path))
- with open(self.cache_path, 'wb') as f:
- dump(cache_db, f)
-
- # == methods for custom checks ==
- def check_struct_member(self, typename, member, headers=None, include_dirs=None, lang="c"):
- """Check whether typename (must be struct or union) has the named member."""
- return self.try_compile(
- 'int main() { %s x; (void) x.%s; return 0; }'
- % (typename, member), headers, include_dirs, lang)
-
-
-@contextmanager
-def suppress(verbosity=0):
- """Context manager that conditionally suppresses stdout/stderr."""
- with ExitStack() as stack:
- with open(os.devnull, 'w') as null:
- if verbosity < 0:
- stack.enter_context(redirect_stdout(null))
- if verbosity < 1:
- stack.enter_context(redirect_stderr(null))
- yield