diff options
author | 2023-03-24 14:07:43 +0300 | |
---|---|---|
committer | 2023-03-24 14:07:43 +0300 | |
commit | 5033316f416e6ff829f6c04a3d1f78d5733e93dd (patch) | |
tree | bf87f3994d1c4f12a06a6a39239f904a725573f6 | |
parent | CI: add Python 3.12 alphas (diff) | |
download | snakeoil-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.py | 1049 |
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 |