Source code for build_helpers

Build helpers for

Includes package dependency checks and monkey-patch to numpy.distutils
to work with Cython.

The original version of this file was adapted from NiPy project [1].


# Standard library imports
import os
import shutil
import glob
import fnmatch
from distutils.cmd import Command
from os.path import join as pjoin, dirname
from distutils.command.clean import clean
from distutils.dep_util import newer_group
from distutils.errors import DistutilsError
from pkg_resources import parse_version

from numpy.distutils.misc_util import appendpath
from numpy.distutils import log

import sfepy.version as INFO


[docs]class NoOptionsDocs(Command): user_options = [('None', None, 'this command has no options'),]
[docs] def initialize_options(self): pass
[docs] def finalize_options(self): pass
[docs]def get_sphinx_make_command(): if in ['posix']: return 'make' elif in ['nt']: return 'make.bat' else: raise ValueError('unsupported system! (%s)' %
[docs]class SphinxHTMLDocs(NoOptionsDocs): description = """generate html docs by Sphinx"""
[docs] def run(self): os.chdir('doc') try: cmd = get_sphinx_make_command() os.system(cmd + ' html') finally: os.chdir('..')
[docs]class SphinxPDFDocs(NoOptionsDocs): description = """generate pdf docs by Sphinx"""
[docs] def run(self): cwd = os.getcwd() os.chdir('doc') try: cmd = get_sphinx_make_command() os.system(cmd + ' latex') os.chdir('_build/latex') os.system(cmd + ' all-pdf') os.chdir(cwd) try: os.remove('doc/sfepy_manual.pdf') except: pass os.rename('doc/_build/latex/SfePy.pdf', 'doc/sfepy_manual.pdf') finally: os.chdir(cwd)
[docs]class DoxygenDocs(NoOptionsDocs): description = """generate docs by Doxygen"""
[docs] def run(self): try: shutil.rmtree('doc/html') except OSError: pass fd_in = open('doc/doxygen.config', 'r') fd_out = open('doc/doxygenrc', 'w') for line in fd_in: aux = line.split('=') if len(aux) and (aux[0] == 'PROJECT_NUMBER'): line = '='.join([aux[0], INFO.__version__]) fd_out.write(line) fd_out.close() fd_in.close() os.system('doxygen doc/doxygenrc')
[docs]def recursive_glob(top_dir, pattern): """ Utility function working like `glob.glob()`, but working recursively and returning generator. Parameters ---------- topdir : str The top-level directory. pattern : str or list of str The pattern or list of patterns to match. """ if isinstance(pattern, list): for pat in pattern: for fn in recursive_glob(top_dir, pat): yield fn else: for dirpath, dirnames, filenames in os.walk(top_dir): for fn in [fn for fn in filenames if fnmatch.fnmatchcase(fn, pattern)]: yield os.path.join(dirpath, fn)
[docs]class Clean(clean): """ Distutils Command class to clean, enhanced to clean also files generated during `python build_ext --inplace`. """
[docs] def run(self): print('extra clean:') suffixes = ['*.pyc', '*.o', '*.so', '*.pyd', '*_wrap.c', '*.bak', '*~', '*%'] for filename in recursive_glob('sfepy', suffixes): print(filename) os.remove(filename) for filename in recursive_glob('examples', suffixes): print(filename) os.remove(filename) for filename in recursive_glob('script', suffixes): print(filename) os.remove(filename) for filename in recursive_glob('tests', suffixes): print(filename) os.remove(filename) for filename in glob.glob('*.pyc'): print(filename) os.remove(filename) for _filename in recursive_glob('sfepy', ['*.pyx']): filename = _filename.replace('.pyx', '.c') print(filename) try: os.remove(filename) except OSError: pass filename = _filename.replace('.pyx', '.html') print(filename) try: os.remove(filename) except OSError: pass
# The command classes for distutils, used by cmdclass = { 'htmldocs' : SphinxHTMLDocs, 'pdfdocs' : SphinxPDFDocs, 'doxygendocs' : DoxygenDocs, 'clean': Clean, }
[docs]def have_good_cython(): try: from Cython.Compiler.Version import version except ImportError: return False return parse_version(version) >= parse_version(CYTHON_MIN_VERSION)
[docs]def generate_a_pyrex_source(self, base, ext_name, source, extension): ''' Monkey patch for numpy build_src.build_src method Uses Cython instead of Pyrex. ''' good_cython = have_good_cython() if self.inplace or not good_cython: target_dir = dirname(base) else: target_dir = appendpath(self.build_src, dirname(base)) target_file = pjoin(target_dir, ext_name + '.c') depends = [source] + extension.depends sources_changed = newer_group(depends, target_file, 'newer') if self.force or sources_changed: if good_cython: # add distribution (package-wide) include directories, in order # to pick up needed .pxd files for cython compilation incl_dirs = extension.include_dirs[:] dist_incl_dirs = self.distribution.include_dirs if not dist_incl_dirs is None: incl_dirs += dist_incl_dirs import Cython.Compiler.Main"cythonc:> %s" % (target_file)) self.mkpath(target_dir) options = Cython.Compiler.Main.CompilationOptions( defaults=Cython.Compiler.Main.default_options, include_path=incl_dirs, output_file=target_file) cython_result = Cython.Compiler.Main.compile(source, options=options) if cython_result.num_errors != 0: raise DistutilsError("%d errors while compiling " "%r with Cython" % (cython_result.num_errors, source)) elif sources_changed and os.path.isfile(target_file): raise DistutilsError("Cython >=%s required for compiling %r" " because sources (%s) have changed" % (CYTHON_MIN_VERSION, source, ','.join(depends))) else: raise DistutilsError("Cython >=%s required for compiling %r" " but not available" % (CYTHON_MIN_VERSION, source)) return target_file
[docs]def package_check(pkg_name, version=None, optional=False, checker=parse_version, version_getter=None, messages=None, show_only=False): """ Check if package `pkg_name` is present, and in correct version. Parameters ---------- pkg_name : str or sequence of str The name of the package as imported into python. Alternative names (e.g. for different versions) may be given in a list. version : str, optional The minimum version of the package that is required. If not given, the version is not checked. optional : bool, optional If False, raise error for absent package or wrong version; otherwise warn checker : callable, optional If given, the callable with which to return a comparable thing from a version string. The default is ``pkg_resources.parse_version``. version_getter : callable, optional: If given, the callable that takes `pkg_name` as argument, and returns the package version string - as in:: ``version = version_getter(pkg_name)`` The default is equivalent to:: mod = __import__(pkg_name); version = mod.__version__`` messages : dict, optional If given, the dictionary providing (some of) output messages. show_only : bool If True, do not raise exceptions, only show the package name and version information. """ if version_getter is None: def version_getter(pkg_name): mod = __import__(pkg_name) return mod.__version__ if messages is None: messages = {} msgs = { 'available' : '%s is available', 'missing' : '%s is missing', 'opt suffix' : '; you may get run-time errors', 'version' : '%s is available in version %s', 'version old' : '%s is available in version %s, but >= %s is needed', 'no version' : '%s is available, cannot determine version', } msgs.update(messages) if isinstance(pkg_name, str): names = [pkg_name] else: names = pkg_name import_ok = False for pkg_name in names: try: __import__(pkg_name) except ImportError: pass else: import_ok = True pkg_info = pkg_name + (' (optional)' if optional else '') if not import_ok: if not (optional or show_only): raise RuntimeError(msgs['missing'] % pkg_name) log.warn(msgs['missing'] % pkg_info + msgs['opt suffix']) return if not version: if show_only:['available'] % pkg_info) return try: have_version = version_getter(pkg_name) except AttributeError: raise RuntimeError(msgs['no version'] % pkg_info) if not have_version: if optional or show_only: log.warn(msgs['no version'] % pkg_info) else: raise RuntimeError(msgs['no version'] % pkg_info) elif checker(have_version) < checker(version): if optional or show_only: log.warn(msgs['version old'] % (pkg_info, have_version, version) + msgs['opt suffix']) else: raise RuntimeError(msgs['version old'] % (pkg_info, have_version, version)) elif show_only:['version'] % (pkg_info, have_version))