diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..2534a45ee555bb5ebefd37210d19399634c7a4ee --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 80 +ignore = E501,W503,E302,E402,E203 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6352dfc534c96dd6ea1c8bc7c061f4745b199881 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/timothycrosley/isort + rev: 5.10.1 + hooks: + - id: isort + args: [--settings-path, "pyproject.toml"] + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: check-added-large-files + - id: check-yaml + exclude: .*/meta.yaml diff --git a/bob/__init__.py b/bob/__init__.py index 2ab1e28b150f0549def9963e9e87de3fdd6b2579..edbb4090fca046b19d22d3982711084621bff3be 100644 --- a/bob/__init__.py +++ b/bob/__init__.py @@ -1,3 +1,4 @@ # see https://docs.python.org/3/library/pkgutil.html from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/bob/extension/__init__.py b/bob/extension/__init__.py index 50422783c95d6277b4f3e4f92bc5f292583d7f65..16c9116c79011f1c68112a6f2dbf26301e87a2a7 100644 --- a/bob/extension/__init__.py +++ b/bob/extension/__init__.py @@ -1,39 +1,20 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : # Andre Anjos <andre.anjos@idiap.ch> -# Mon 28 Jan 2013 16:40:27 CET +# Amir Mohammadi <amir.mohammadi@idiap.ch> """A custom build class for Pkg-config based extensions """ import contextlib -import os -import sys -import platform -import subprocess +import logging + import pkg_resources -from setuptools.extension import Extension as DistutilsExtension -from setuptools.command.build_ext import build_ext as _build_ext -import distutils.sysconfig -from pkg_resources import resource_filename +from .rc_config import _loadrc -import logging logger = logging.getLogger(__name__) -DEFAULT_PREFIXES = [ - "/opt/local", - "/usr/local", - "/usr", -] -"""The list common places to search for library-related files.""" - - -from .pkgconfig import pkgconfig -from .boost import boost -from .utils import uniq, uniq_paths, find_executable, find_library, construct_search_paths -from .cmake import CMakeListsGenerator -from .rc_config import _loadrc __version__ = pkg_resources.require(__name__)[0].version @@ -42,859 +23,88 @@ rc = _loadrc() """The content of the global configuration file loaded as a dictionary. The value for any non-existing key is ``None``.""" + @contextlib.contextmanager def rc_context(dict): - """A context manager for bob.extension.rc. - You can use this context manager to temporarily change a value in - ``bob.extension.rc``. - - Example - ------- - >>> from bob.extension import rc, rc_context - >>> assert rc.get("non-existing-key") is None - >>> with rc_context({"non-existing-key": 1}): - ... a = rc.get("non-existing-key") - >>> a - 1 - """ - old_rc = rc.copy() - try: - rc.update(dict) - yield - finally: - rc.clear() - rc.update(old_rc) - - -def check_packages(packages): - """Checks if the requirements for the given packages are satisfied. - - Parameters: - - :py:class:`list` of :py:class:`str`: Each representing a requirement that - must be statistfied. Package requirements can be set like this:: - - "pkg > VERSION" - - In this case, the package version should be greater than the given - version number. Comparisons are done using - :py:mod:`distutils.version.LooseVersion`. You can use other comparators - such as ``<``, ``<=``, ``>=`` or ``==``. If no version number is given, - then we only require that the package is installed. - - - Raises: - - ``RuntimeError``: in case requirements are not satisfied. This means either - not finding a package if no version number is specified or verifying that - the package version does not match the required version by the builder. - - """ - - from re import split - - used = set() - retval = [] - - for requirement in uniq(packages): - - splitreq = split(r'\s*(?P<cmp>[<>=]+)\s*', requirement) - - if len(splitreq) not in (1, 3): - - raise RuntimeError("cannot parse requirement `%s'", requirement) - - p = pkgconfig(splitreq[0]) - - if len(splitreq) == 3: # package + version number - - if splitreq[1] == '>': - assert p > splitreq[2], "%s version is not > `%s'" % (p.name, splitreq[2]) - elif splitreq[1] == '>=': - assert p >= splitreq[2], "%s version is not >= `%s'" % (p.name, splitreq[2]) - elif splitreq[1] == '<': - assert p < splitreq[2], "%s version is not < `%s'" % (p, splitreq[2]) - elif splitreq[1] == '<=': - assert p <= splitreq[2], "%s version is not <= `%s'" % (p, splitreq[2]) - elif splitreq[1] == '==': - assert p <= splitreq[2], "%s version is not == `%s'" % (p, splitreq[2]) - else: - raise RuntimeError("cannot parse requirement `%s'", requirement) - - retval.append(p) - - if p.name in used: - raise RuntimeError("package `%s' had already been requested - cannot (currently) handle recurring requirements") - used.add(p.name) - - return retval - -def generate_self_macros(extname, version): - """Generates standard macros with library, module names and prefix""" - - if version is None: - return [] - - s = extname.rsplit('.', 1) - - retval = [ - ('BOB_EXT_MODULE_PREFIX', '"%s"' % s[0]), - ('BOB_EXT_MODULE_NAME', '"%s"' % s[1]), - ] - - if sys.version_info[0] >= 3: - retval.append(('BOB_EXT_ENTRY_NAME', 'PyInit_%s' % s[1])) - else: - retval.append(('BOB_EXT_ENTRY_NAME', 'init%s' % s[1])) - - if version: retval.append(('BOB_EXT_MODULE_VERSION', '"%s"' % version)) - - return retval - -def reorganize_isystem(args): - """Re-organizes the -isystem includes so that more specific paths come - first""" - - remainder = [] - includes = [] - - skip = False - for i in range(len(args)): - if skip: - skip = False - continue - if args[i] == '-isystem': - includes.append(args[i+1]) - skip = True - else: - remainder.append(args[i]) - - includes = uniq_paths(includes[::-1])[::-1] - - # sort includes so that the shortest system includes go last - # this algorithm will ensure '/usr/include' comes after other - # overwrites - includes.sort(key=lambda item: (-len(item), item)) - - retval = [tuple(remainder)] + [('-isystem', k) for k in includes] - from itertools import chain - return list(chain.from_iterable(retval)) - -def normalize_requirements(requirements): - """Normalizes the requirements keeping only the most tight""" - - from re import split - - parsed = {} - - for requirement in requirements: - splitreq = split(r'\s*(?P<cmp>[<>=]+)\s*', requirement) - - if len(splitreq) not in (1, 3): - - raise RuntimeError("cannot parse requirement `%s'", requirement) - - if len(splitreq) == 1: # only package - - parsed.setdefault(splitreq[0], []) - - if len(splitreq) == 3: # package + version number - - parsed.setdefault(splitreq[0], []).append(tuple(splitreq[1:])) - - # at this point, all requirements are organised: - # requirement -> [(op, version), (op, version), ...] - - leftovers = [] - - for key, value in parsed.items(): - value = uniq(value) - - if not value: - leftovers.append(key) - continue - - for v in value: - leftovers.append(' '.join((key, v[0], v[1]))) - - return leftovers - - -def get_bob_libraries(bob_packages): - """Returns a list of include directories, libraries and library directories - for the given bob libraries.""" - includes = [] - libraries = [] - library_directories = [] - macros = [] - # iterate through the list of bob packages - if bob_packages is not None: - # TODO: need to handle versions? - bob_packages = normalize_requirements([k.strip().lower() for k in bob_packages]) - for package in bob_packages: - import importlib - pkg = importlib.import_module(package) - location = os.path.dirname(pkg.__file__) - includes.append(os.path.join(location, 'include')) - # check if the package contains a get_include_directories() function - if hasattr(pkg, 'get_include_directories'): - includes.extend(pkg.get_include_directories()) - if hasattr(pkg, 'get_macros'): - macros.extend(pkg.get_macros()) - - lib_name = package.replace('.', '_') - libs = find_library(lib_name, prefixes=[location]) - # add the FIRST lib that we found, if any - if len(libs): - libraries.append(lib_name) - library_directories.append(os.path.dirname(libs[0])) - - return includes, libraries, library_directories, macros - - -def get_full_libname(name, path=None, version=None): - """Generates the name of the library from the given name, path and version.""" - libname = 'lib' + name.replace('.', '_') - if sys.platform == 'darwin': - libname += ('.' + version if version is not None else '') + '.dylib' - elif sys.platform == 'win32': - libname += '.dll' + ('.' + version if version is not None else '') - else: # linux like - libname += '.so' + ('.' + version if version is not None else '') - - if path is not None: - return os.path.join(path, libname) - else: - return libname - - -def load_bob_library(name, _file_): - """Loads the bob Library for the given package name in the given version (if given). - The _file_ parameter is expected to be the ``__file__`` member of the main ``__init__.py`` of the package. - It is used to determine the directory, where the library should be loaded from. - - Keyword parameters - - name : string - The name of the bob package to load the library from, e.g. ``bob.core`` - - _file_ : string - The ``__file__`` member of the ``__init__.py`` file in which the library is loaded. - """ - - full_libname = get_full_libname(name, os.path.dirname(_file_)) - import ctypes - ctypes.cdll.LoadLibrary(full_libname) - - -def find_system_include_paths(): - """Finds system include paths if the environment variable ``CC`` is set - - Returns: - - list: A list of include paths defined by the compiler - - """ - - start1 = '#include "..." search starts here:\n' - start2 = '#include <...> search starts here:\n' - end = 'End of search list' - - def make_list(s): - tmp = [k.strip() for k in s.split('\n') if k.strip()] - remove = ' (framework directory)' - ret = [] - for k in tmp: - if k.endswith(remove): ret.append(k[:-len(remove)]) - else: ret.append(k) - return ret - - CC = find_executable("cc") - if CC: CC = CC[0] - else: CC = None - CC = os.environ.get('CC', CC) - - try: - cmd = "echo | %s -v -E -" % CC - out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) - out = out.decode() - section1 = out[out.find(start1)+len(start1)+1:out.find(start2)] - section2 = out[out.find(start2)+len(start2)+1:out.find(end)] - section1 = make_list(section1) - section2 = make_list(section2) - return section1 + section2 - - except Exception as e: - logger.error('Cannot get system include paths with CC=%s', CC) - logger.error('The output of `%s\' was:\n%s', cmd, out) - return [] - - if CC is None: - logger.warn('Cannot get system include paths because CC is NOT set on ' \ - 'the environment nor it can be found on your PATH - set it to fix ' \ - 'possible "-isystem" usage issues ' \ - '(see: https://stackoverflow.com/questions/37218953/isystem-on-a-system-include-directory-causes-errors)') - return [] - - -class Extension(DistutilsExtension): - """Extension building with pkg-config packages. - - See the documentation for :py:class:`distutils.extension.Extension` for more - details on input parameters. - """ - - def __init__(self, name, sources, **kwargs): - """Initialize the extension with parameters. - - External package extensions (mostly coming from pkg-config), adds a single - parameter to the standard arguments of the constructor: - - packages : [string] - This should be a list of strings indicating the name of the bob - (pkg-config) modules you would like to have linked to your extension - **additionally** to ``bob-python``. Candidates are module names like - "bob-machine" or "bob-math". - - For convenience, you can also specify "opencv" or other 'pkg-config' - registered packages as a dependencies. - - boost_modules : [string] - A list of boost modules that we need to link against. - - bob_packages : [string] - A list of bob libraries (such as ``'bob.core'``) containing C++ code - that should be included and linked - - system_include_dirs : [string] - A list of include directories that are not in one of our packages, - and which should be included with the -isystem compiler option - - """ - - packages = [] - - if 'packages' in kwargs: - if isinstance(kwargs['packages'], str): - packages.append(kwargs['packages']) - else: - packages.extend(kwargs['packages']) - del kwargs['packages'] - - # uniformize packages - packages = normalize_requirements([k.strip().lower() for k in packages]) - - # check if we have bob libraries to link against - if 'bob_packages' in kwargs: - self.bob_packages = kwargs['bob_packages'] - del kwargs['bob_packages'] - else: - self.bob_packages = None - - bob_includes, bob_libraries, bob_library_dirs, bob_macros = get_bob_libraries(self.bob_packages) - - # system include directories - if 'system_include_dirs' in kwargs: - system_includes = kwargs['system_include_dirs'] - del kwargs['system_include_dirs'] - else: - system_includes = [] - - # Boost requires a special treatment - boost_req = '' - for i, pkg in enumerate(packages): - if pkg.startswith('boost'): - boost_req = pkg - del packages[i] - - # We still look for the keyword 'boost_modules' - boost_modules = [] - if 'boost_modules' in kwargs: - if isinstance(kwargs['boost_modules'], str): - boost_modules.append(kwargs['boost_modules']) - else: - boost_modules.extend(kwargs['boost_modules']) - del kwargs['boost_modules'] - - if boost_modules and not boost_req: boost_req = 'boost >= 1.0' - - # Was a version parameter given? - version = None - if 'version' in kwargs: - version = kwargs['version'] - del kwargs['version'] - - # Mixing - parameters = { - 'define_macros': generate_self_macros(name, version) + bob_macros, - 'extra_compile_args': os.environ.get('CXXFLAGS', '').split(), - 'extra_link_args': [], - 'library_dirs': [], - 'libraries': bob_libraries, - } - - # Compilation options for macOS builds - if platform.system() == 'Darwin': - logger.warn(str(os.environ)) - logger.warn(rc['bob.extension.macosx_deployment_target']) - logger.warn("######################################") - - target = os.environ.get('MACOSX_DEPLOYMENT_TARGET') - if target is None: #not set on the environment, try resource - target = rc['bob.extension.macosx_deployment_target'] - if target is None or not target: - raise EnvironmentError('${MACOSX_DEPLOYMENT_TARGET} environment ' \ - 'variable is **NOT** set - To avoid this error, either set ' \ - '${MACOSX_DEPLOYMENT_TARGET} or Bob\'s resource ' \ - '"bob.extension.macosx_deployment_target" to a suitable ' \ - 'value (e.g. ' \ - '"bob config set bob.extension.macosx_deployment_target 10.9") ' \ - 'before trying to build C/C++ extensions') - os.environ.setdefault('MACOSX_DEPLOYMENT_TARGET', target) - - sdkroot = os.environ.get('SDKROOT', os.environ.get('CONDA_BUILD_SYSROOT')) - if sdkroot is None: #not set on the environment, try resource - sdkroot = rc['bob.extension.macosx_sdkroot'] - if sdkroot is None or not sdkroot: - raise EnvironmentError('${SDKROOT} environment variable is **NOT** ' \ - 'set - To avoid this error, either set ${SDKROOT} or Bob\'s ' \ - 'resource "bob.extension.macosx_sdkroot" to a suitable value ' \ - '(e.g. "bob config ' \ - 'set bob.extension.macosx_sdkroot /opt/MacOSX10.9.sdk")' \ - 'before trying to build C/C++ extensions') - - # test for the compatibility between deployment target and sdk root - sdkversion = os.path.basename(sdkroot)[6:-4] - if sdkversion != target: - logger.warn('There is an inconsistence between the value ' \ - 'set for ${MACOSX_DEPLOYMENT_TARGET} (%s) and ' \ - '${SDKROOT}/${CONDA_BUILD_SYSROOT} (%s) - Fix it by properly ' \ - 'setting up these environment variables via Bob\'s ' \ - 'configuration (refer to bob.extension\'s user guide for ' \ - 'detailed instructions)', target, sdkroot) - - os.environ.setdefault('SDKROOT', sdkroot) - - parameters['extra_compile_args'] = \ - ['-mmacosx-version-min=%s' % target] + ['-isysroot', sdkroot] + \ - parameters['extra_compile_args'] + ['-Wno-#warnings'] - - user_includes = kwargs.get('include_dirs', []) - self.pkg_includes = [] - self.pkg_libraries = [] - self.pkg_library_directories = [] - self.pkg_macros = [] - - # Updates for boost - if boost_req: - - boost_pkg = boost(boost_req.replace('boost', '').strip()) - - # Adds macros - parameters['define_macros'] += boost_pkg.macros() - - # Adds the include directory (enough for using just the template library) - if boost_pkg.include_directory not in user_includes: - system_includes.append(boost_pkg.include_directory) - self.pkg_includes.append(boost_pkg.include_directory) - - # Adds specific boost libraries requested by the user - if boost_modules: - boost_libdirs, boost_libraries = boost_pkg.libconfig(boost_modules) - parameters['library_dirs'].extend(boost_libdirs) - self.pkg_library_directories.extend(boost_libdirs) - parameters['libraries'].extend(boost_libraries) - self.pkg_libraries.extend(boost_libraries) - - # Checks all other pkg-config requirements - pkgs = check_packages(packages) - - for pkg in pkgs: - - # Adds parameters for each package, in order - parameters['define_macros'] += pkg.package_macros() - self.pkg_macros += pkg.package_macros() - - # Include directories are added with a special path - for k in pkg.include_directories(): - if k in user_includes or k in self.pkg_includes: continue - system_includes.append(k) - self.pkg_includes.append(k) - - parameters['library_dirs'] += pkg.library_directories() - self.pkg_library_directories += pkg.library_directories() - - if pkg.name.find('bob-') == 0: # one of bob's packages - - # make-up the names of versioned Bob libraries we must link against - - if platform.system() == 'Darwin': - libs = ['%s.%s' % (k, pkg.version) for k in pkg.libraries()] - elif platform.system() == 'Linux': - libs = [':lib%s.so.%s' % (k, pkg.version) for k in pkg.libraries()] - else: - raise RuntimeError("supports only MacOSX and Linux builds") - - else: - - libs = pkg.libraries() - - parameters['libraries'] += libs - self.pkg_libraries += libs - - # if used libraries require extra compilation flags, add them to the mix - parameters['extra_compile_args'].extend( - pkg.cflags_other().get('extra_compile_args', []) - ) - - parameters['extra_link_args'] += pkg.other_libraries() - - # add the -isystem to all system include dirs - compiler_includes = find_system_include_paths() - system_includes = [k for k in system_includes if k not in compiler_includes] - for k in system_includes: - parameters['extra_compile_args'].extend(['-isystem', k]) - - # Filter and make unique - for key in parameters.keys(): - - # Tune input parameters if they were set, but assure that our parameters come first - if key in kwargs: - kwargs[key] = parameters[key] + kwargs[key] - else: kwargs[key] = parameters[key] - - if key in ('extra_compile_args'): continue - - kwargs[key] = uniq(kwargs[key]) - - # add our include dir by default - self_include_dir = resource_filename(__name__, 'include') - kwargs.setdefault('include_dirs', []).append(self_include_dir) - kwargs['include_dirs'] = user_includes + bob_includes + kwargs['include_dirs'] - - # Uniq'fy parameters that are not on our parameter list - kwargs['include_dirs'] = uniq_paths(kwargs['include_dirs']) - - # Stream-line '-isystem' includes - kwargs['extra_compile_args'] = reorganize_isystem(kwargs['extra_compile_args']) - - # Make sure the language is correctly set to C++ - kwargs['language'] = 'c++' - - # On Linux, set the runtime path - if platform.system() == 'Linux': - kwargs.setdefault('runtime_library_dirs', []) - kwargs['runtime_library_dirs'] += kwargs['library_dirs'] - kwargs['runtime_library_dirs'] = uniq_paths(kwargs['runtime_library_dirs']) - - # .. except for the bob libraries - kwargs['library_dirs'] += bob_library_dirs - - # Uniq'fy library directories - kwargs['library_dirs'] = uniq_paths(kwargs['library_dirs']) - - # Run the constructor for the base class - DistutilsExtension.__init__(self, name, sources, **kwargs) - - -class Library (Extension): - """A class to compile a pure C++ code library used within and outside an extension using CMake.""" - - def __init__(self, name, sources, version, bob_packages = [], packages = [], boost_modules=[], include_dirs = [], system_include_dirs = [], libraries = [], library_dirs = [], define_macros = []): - """Initializes a pure C++ library that will be compiled with CMake. - - By default, the include directory of this package is automatically added to the ``include_dirs``. - It is expected to be in the `include`` directory in the main package directory (which, e.g., is ``bob/core`` for package ``bob.core``). - - .. note:: - This library, including the library and include directories, is also automatically added to **all other** :py:class:`Extension`'s that are compiled within this package. - - .. warning:: - IMPORTANT! To compile this library with CMake, the :py:class:`build_ext` class provided in this module is required. - Please include:: - - cmdclass = { - 'build_ext': build_ext - }, - - as a parameter to the ``setup`` function in your setup.py. - - Keyword parameters: - - name : string - The name of the library to generate, e.g., ``'bob.core.bob_core'`` - - sources : [string] - A list of files (relative to the base directory) that should be compiled and linked by CMake - - version : string - The version of the library, which is usually identical to the version of the package - - bob_packages : [string] - A list of bob packages that the pure C++ code relies on. - Libraries and include directories of these packages will be automatically added. - - packages : [string] - A list of pkg-config based packages, see :py:class:`Extension`. - Macros, libraries and include directories of these packages will be automatically added. - - boost_modules : [string] - A list of boost modules that we need to link against. - - include_dirs : [string] - An additional list of include directories that is not covered by ``bob_packages`` and ``packages`` - - system_include_dirs : [string] - A list of include directories that are not in one of our packages, - and which should be included with the SYSTEM option - - libraries : [string] - An additional list of libraries that is not covered by ``bob_packages`` and ``packages`` - - library_dirs : [string] - An additional list of library directories that is not covered by ``bob_packages`` and ``packages`` - - define_macros : [(string, string)] - An additional list of preprocessor definitions that is not covered by ``packages`` + """A context manager for bob.extension.rc. + You can use this context manager to temporarily change a value in + ``bob.extension.rc``. + + Example + ------- + >>> from bob.extension import rc, rc_context + >>> assert rc.get("non-existing-key") is None + >>> with rc_context({"non-existing-key": 1}): + ... a = rc.get("non-existing-key") + >>> a + 1 """ - name_split = name.split('.') - if len(name_split) <= 1: - raise ValueError("The name of the library must contain the package name, e.g., bob.core.bob_core") - self.c_name = name_split[-1] - self.c_package_directory = os.path.realpath('.') - self.c_sub_directory = os.path.join(*(name_split[:-1])) - self.c_sources = sources - self.c_version = version - self.c_self_include_directory = os.path.join(self.c_package_directory, self.c_sub_directory, 'include') - self.c_include_directories = [self.c_self_include_directory] + include_dirs - self.c_system_include_directories = system_include_dirs - self.c_libraries = libraries[:] - self.c_library_directories = library_dirs[:] - self.c_define_macros = define_macros[:] - - # add includes and libs for bob packages as the PREFERRED path (i.e., in front) - bob_includes, bob_libraries, bob_library_dirs, bob_macros = get_bob_libraries(bob_packages) - self.c_include_directories = bob_includes + self.c_include_directories - self.c_libraries = bob_libraries + self.c_libraries - self.c_library_directories = bob_library_dirs + self.c_library_directories - self.c_define_macros = bob_macros + self.c_define_macros - - # find the cmake executable - cmake = find_executable("cmake") - if not cmake: - raise OSError("The Library class needs CMake version >= 2.8 to be installed, but CMake cannot be found") - self.c_cmake = cmake[0] + old_rc = rc.copy() + try: + rc.update(dict) + yield + finally: + rc.clear() + rc.update(old_rc) - # call base class constructor, i.e., to handle the packages - Extension.__init__(self, name, sources, packages=packages, boost_modules=boost_modules) - # add the include directories for the packages as well - self.c_system_include_directories.extend(self.pkg_includes) - self.c_libraries.extend(self.pkg_libraries) - self.c_library_directories.extend(self.pkg_library_directories) - self.c_define_macros.extend(self.pkg_macros) - - - def compile(self, build_directory, compiler = None, stdout=None): - """This function will automatically create a CMakeLists.txt file in the ``package_directory`` including the required information. - Afterwards, the library is built using CMake in the given ``build_directory``. - The build type is automatically taken from the debug option in the buildout.cfg. - To change the compiler, use the ``compiler`` parameter. - """ - self.c_target_directory = os.path.join(os.path.realpath(build_directory), self.c_sub_directory) - if not os.path.exists(self.c_target_directory): - os.makedirs(self.c_target_directory) - # generate CMakeLists.txt makefile - generator = CMakeListsGenerator( - name = self.c_name, - sources = self.c_sources, - target_directory = self.c_target_directory, - version = self.c_version, - include_directories = uniq_paths(self.c_include_directories), - system_include_directories = uniq_paths(self.c_system_include_directories), - libraries = uniq(self.c_libraries), - library_directories = uniq_paths(self.c_library_directories), - macros = uniq(self.c_define_macros) - ) - - # compile our stuff in a different directory - final_build_dir = os.path.join(os.path.dirname(os.path.realpath(build_directory)), 'build_cmake', self.c_name) - if not os.path.exists(final_build_dir): - os.makedirs(final_build_dir) - generator.generate(self.c_package_directory, final_build_dir) - - # compile in the build directory - import subprocess - env = {'VERBOSE' : '1'} - env.update(os.environ) - if compiler is not None: - env['CXX'] = compiler - # configure cmake - command = [self.c_cmake, final_build_dir] - if subprocess.call(command, cwd=final_build_dir, env=env, stdout=stdout) != 0: - raise OSError("Could not generate makefiles with CMake") - # run make - make_call = ['make'] - if "BOB_BUILD_PARALLEL" in os.environ: make_call += ['-j%s' % os.environ["BOB_BUILD_PARALLEL"]] - if subprocess.call(make_call, cwd=final_build_dir, env=env, stdout=stdout) != 0: - raise OSError("CMake compilation stopped with an error; stopping ...") - - -class build_ext(_build_ext): - """Compile the C++ :py:class`Library`'s using CMake, and the python extensions afterwards - - See the documentation for :py:class:`distutils.command.build_ext` for more - information. - """ +def get_config(package=__name__, externals=None, api_version=None): + """Returns a string containing the configuration information for the given ``package`` name. + By default, it returns the configuration of this package. - def finalize_options(self): - # check if the "BOB_BUILD_DIRECTORY" environment variable is set - env = os.environ - if 'BOB_BUILD_DIRECTORY' in env and env['BOB_BUILD_DIRECTORY']: - # HACKISH: check if we are currently developed by inspecting the way we got called - if 'develop' in sys.argv: - self.build_temp = os.path.join(env['BOB_BUILD_DIRECTORY'], 'build_temp') - self.build_lib = os.path.join(env['BOB_BUILD_DIRECTORY'], 'build_lib') - _build_ext.finalize_options(self) + This function can be reused by other packages. + If these packages have external C or C++ dependencies, the ``externals`` dictionary can be specified. + Also, if the package provides an API version, it can be specified. - def run(self): - """Iterates through the list of Extension packages and reorders them, so that the Library's come first - """ - # here, we simply re-order the extensions such that we get the Library first - self.extensions = [ext for ext in self.extensions if isinstance(ext, Library)] + [ext for ext in self.extensions if not isinstance(ext, Library)] - # call the base class function - return _build_ext.run(self) + **Keyword parameters:** + package : *str* + The name of the package to get the configuration information from. + Usually, the ``__name__`` of the package. - def build_extension(self, ext): - """Builds the given extension. + externals : *{dep: description}* + A dictionary of external C or C++ dependencies, with the ``dep``endent package name as key, and a free ``description`` as value. - When the extension is of type Library, it compiles the library with CMake, otherwise the default compilation mechanism is used. - Afterwards, it adds the according library, and the include and library directories of the Library's, so that other Extensions can find the newly generated lib. + api_version : *int* (usually in hexadecimal) + The API version of the ``package``, if any. """ - # HACK: remove the "-Wstrict-prototypes" option keyword - self.compiler.compiler = [c for c in self.compiler.compiler if c != "-Wstrict-prototypes"] - self.compiler.compiler_so = [c for c in self.compiler.compiler_so if c != "-Wstrict-prototypes"] - if "-Wno-strict-aliasing" not in self.compiler.compiler: - self.compiler.compiler.append("-Wno-strict-aliasing") - if "-Wno-strict-aliasing" not in self.compiler.compiler_so: - self.compiler.compiler_so.append("-Wno-strict-aliasing") + import pkg_resources - # check if it is our type of extension - if isinstance(ext, Library): - # TODO: get compiler and add it to the compiler - # TODO: get the debug status and add the build_type parameter - # build libraries using the provided functions - # compile - ext.compile(self.build_lib) - libs = [ext.c_name] - lib_dirs = [ext.c_target_directory] - include_dirs = [ext.c_self_include_directory] + packages = pkg_resources.require(package) + this = packages[0] + deps = packages[1:] - # set the DEFAULT library path and include path for all other extensions - for other_ext in self.extensions: - if other_ext != ext: - other_ext.libraries = libs + (other_ext.libraries if other_ext.libraries else []) - other_ext.library_dirs = lib_dirs + (other_ext.library_dirs if other_ext.library_dirs else []) - other_ext.include_dirs = include_dirs + (other_ext.include_dirs if other_ext.include_dirs else []) + if api_version is not None: + retval = "%s: %s [api=0x%04x] (%s)\n" % ( + this.key, + this.version, + api_version, + this.location, + ) else: - # all other libs are build with the default command - _build_ext.build_extension(self, ext) - - - def get_ext_filename(self, fullname): - """Returns the library path for the given name""" - filename = _build_ext.get_ext_filename(self, fullname) - if fullname in self.ext_map: - ext = self.ext_map[fullname] - if isinstance(ext, Library): - # remove any extension that was artificially added by python - basename = filename.replace(distutils.sysconfig.get_config_var("SO"), "") - # HACK: (for some python versions the above code doesn't seem to work) - index = basename.find("cpython-") - if index > 0: - basename = basename[:index-1] - - return get_full_libname(os.path.basename(basename), os.path.dirname(basename)) - else: - return _build_ext.get_ext_filename(self, fullname) - - - -## Compile in parallel, when BOB_BUILD_PARALLEL is given -# see http://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils -if "BOB_BUILD_PARALLEL" in os.environ: - # monkey-patch for parallel compilation - def parallelCCompile(self, sources, output_dir=None, macros=None, include_dirs=None, debug=0, extra_preargs=None, extra_postargs=None, depends=None): - # those lines are copied from distutils.ccompiler.CCompiler directly - macros, objects, extra_postargs, pp_opts, build = self._setup_compile(output_dir, macros, include_dirs, sources, depends, extra_postargs) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - # parallel code - N = min(int(os.environ["BOB_BUILD_PARALLEL"]), len(objects)) # number of parallel compilations - import multiprocessing.pool - def _single_compile(obj): - try: src, ext = build[obj] - except KeyError: return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - # create process pool - pool = multiprocessing.pool.ThreadPool(N) - # execute each compilation in a separate process - pool.map(_single_compile, objects) - # wait until all processes finished - pool.close() - pool.join() - - return objects - - import distutils.ccompiler - distutils.ccompiler.CCompiler.compile=parallelCCompile - - -def get_config(package=__name__, externals=None, api_version=None): - """Returns a string containing the configuration information for the given ``package`` name. - By default, it returns the configuration of this package. - - This function can be reused by other packages. - If these packages have external C or C++ dependencies, the ``externals`` dictionary can be specified. - Also, if the package provides an API version, it can be specified. - - **Keyword parameters:** - - package : *str* - The name of the package to get the configuration information from. - Usually, the ``__name__`` of the package. - - externals : *{dep: description}* - A dictionary of external C or C++ dependencies, with the ``dep``endent package name as key, and a free ``description`` as value. - - api_version : *int* (usually in hexadecimal) - The API version of the ``package``, if any. - """ - - import pkg_resources - packages = pkg_resources.require(package) - this = packages[0] - deps = packages[1:] + retval = "%s: %s (%s)\n" % (this.key, this.version, this.location) - if api_version is not None: - retval = "%s: %s [api=0x%04x] (%s)\n" % (this.key, this.version, api_version, this.location) - else: - retval = "%s: %s (%s)\n" % (this.key, this.version, this.location) + if externals is not None: + retval += "* C/C++ dependencies:\n" + for k in sorted(externals): + retval += " - %s: %s\n" % (k, externals[k]) - if externals is not None: - retval += "* C/C++ dependencies:\n" - for k in sorted(externals): retval += " - %s: %s\n" % (k, externals[k]) + if len(deps): + retval += "* Python dependencies:\n" + # sort python dependencies and make them unique + deps_dict = {} + for d in deps: + deps_dict[d.key] = d + for k in sorted(deps_dict): + retval += " - %s: %s (%s)\n" % ( + deps_dict[k].key, + deps_dict[k].version, + deps_dict[k].location, + ) - if len(deps): - retval += "* Python dependencies:\n" - # sort python dependencies and make them unique - deps_dict = {} - for d in deps: deps_dict[d.key] = d - for k in sorted(deps_dict): - retval += " - %s: %s (%s)\n" % (deps_dict[k].key, deps_dict[k].version, deps_dict[k].location) + return retval.strip() - return retval.strip() # gets sphinx autodoc done right - don't remove it -__all__ = [_ for _ in dir() if not _.startswith('_')] +__all__ = [_ for _ in dir() if not _.startswith("_")] diff --git a/bob/extension/boost.py b/bob/extension/boost.py deleted file mode 100644 index 9e1008d92940dc830d1295f3de5fc8e1744c1331..0000000000000000000000000000000000000000 --- a/bob/extension/boost.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# Andre Anjos <andre.anjos@idiap.ch> -# Thu Mar 20 12:38:14 CET 2014 - -"""Helps looking for Boost on stock file-system locations""" - -import os -import re -import sys -import glob -from distutils.version import LooseVersion - -from .utils import uniq, egrep, find_header, find_library - -def boost_version(version_hpp): - - matches = egrep(version_hpp, r"^#\s*define\s+BOOST_VERSION\s+(\d+)\s*$") - if not len(matches): return None - - # we have a match, produce a string version of the version number - version_int = int(matches[0].group(1)) - version_tuple = ( - version_int // 100000, - (version_int // 100) % 1000, - version_int % 100, - ) - return '.'.join([str(k) for k in version_tuple]) - -class boost: - """A class for capturing configuration information from boost - - Example usage: - - .. doctest:: - :options: +NORMALIZE_WHITESPACE +ELLIPSIS - - >>> from bob.extension import boost - >>> pkg = boost('>= 1.35') - >>> pkg.include_directory - '...' - >>> pkg.version - '...' - - You can also use this class to retrieve information about installed Boost - libraries and link information: - - .. doctest:: - :options: +NORMALIZE_WHITESPACE +ELLIPSIS - - >>> from bob.extension import boost - >>> pkg = boost('>= 1.35') - >>> pkg.libconfig(['python', 'system']) - (...) - - """ - - def __init__ (self, requirement=''): - """ - Searches for the Boost library in stock locations. Allows user to override. - - If the user sets the environment variable BOB_PREFIX_PATH, that prefixes - the standard path locations. - """ - - candidates = find_header('version.hpp', subpaths=['boost', 'boost?*']) - - if not candidates: - raise RuntimeError("could not find boost's `version.hpp' - have you installed Boost on this machine?") - - found = False - - if not requirement: - # since we use boost headers **including the boost/ directory**, we need to go one level lower - self.include_directory = os.path.dirname(os.path.dirname(candidates[0])) - self.version = boost_version(candidates[0]) - found = True - - else: - - # requirement is 'operator' 'version' - operator, required = [k.strip() for k in requirement.split(' ', 1)] - - # now check for user requirements - for path in candidates: - version = boost_version(path) - available = LooseVersion(version) - if (operator == '<' and available < required) or \ - (operator == '<=' and available <= required) or \ - (operator == '>' and available > required) or \ - (operator == '>=' and available >= required) or \ - (operator == '==' and available == required): - self.include_directory = path - self.version = version - found = True - break - - if not found: - raise RuntimeError("could not find the required (%s) version of boost on the file system (looked at: %s)" % (requirement, ', '.join(candidates))) - - # normalize - self.include_directory = os.path.normpath(self.include_directory) - - - def libconfig(self, modules, only_static=False, - templates=['boost_%(name)s-mt-%(py)s', 'boost_%(name)s-%(py)s', 'boost_%(name)s%(pyv)s', 'boost_%(name)s-mt', 'boost_%(name)s']): - """Returns a tuple containing the library configuration for requested - modules. - - This function respects the path location where the include files for Boost - are installed. - - Parameters: - - modules (list of strings) - A list of string specifying the requested libraries to search for. For - example, to search for `libboost_mpi.so`, pass only ``mpi``. - - static (bool) - A boolean, indicating if we should try only to search for static versions - of the libraries. If not set, any would do. - - templates (list of template strings) - A list that defines in which order to search for libraries on the default - search path, defined by ``self.include_directory``. Tune this list if you - have compiled specific versions of Boost with support to multi-threading - (``-mt``), debug (``-g``), STLPORT (``-p``) or required to insert - compiler, the underlying thread API used or your own namespace. - - Here are the keywords you can use: - - %(name)s - resolves to the module name you are searching for - - %(ver)s - resolves to the current boost version string (e.g. ``'1.50.0'``) - - %(py)s - resolves to the string ``'pyXY'`` where ``XY`` represent the major and - minor versions of the current python interpreter. - - %(pyv)s - resolves to the string ``'XY'`` where ``XY`` represent the major and - minor versions of the current python interpreter. - - Example templates: - - * ``'boost_%(name)s-mt'`` - * ``'boost_%(name)s'`` - * ``'boost_%(name)s-gcc43-%(ver)s'`` - - Returns: - - directories (list of strings) - A list of directories indicating where the libraries are installed - - libs (list of strings) - A list of strings indicating the names of the libraries you can use - """ - - # make the include header prefix preferential - prefix = os.path.dirname(self.include_directory) - - py = 'py%d%d' % sys.version_info[:2] - pyv = '%d%d' % sys.version_info[:2] - - filenames = [] - for module in modules: - candidates = [] - modnames = [k % dict(name=module, ver=self.version, py=py, pyv=pyv) for k in - templates] - - for modname in modnames: - candidates += find_library(modname, version=self.version, - prefixes=[prefix], only_static=only_static) - - if not candidates: - raise RuntimeError("cannot find required boost module `%s' - make sure boost is installed on `%s' and that this module is named %s on the filesystem" % (module, prefix, ' or '.join(modnames))) - - # take the first choice that includes the prefix (or the absolute first choice otherwise) - index = 0 - for i, candidate in enumerate(candidates): - if candidate.find(prefix) == 0: - index = i - break - filenames.append(candidates[index]) - - # libraries - libraries = [] - for f in filenames: - name, ext = os.path.splitext(os.path.basename(f)) - if ext in ['.so', '.a', '.dylib', '.dll']: - libraries.append(name[3:]) #strip 'lib' from the name - else: #link against the whole thing - libraries.append(':' + os.path.basename(f)) - - # library paths - libpaths = [os.path.dirname(k) for k in filenames] - - return uniq(libpaths), uniq(libraries) - - def macros(self): - """Returns package availability and version number macros - - This method returns a python list with 2 macros indicating package - availability and a version number, using standard GNU compatible names. - Example: - - .. doctest:: - :options: +NORMALIZE_WHITESPACE +ELLIPSIS - - >>> from bob.extension import boost - >>> pkg = boost('>= 1.34') - >>> pkg.macros() - [('HAVE_BOOST', '1')] - - """ - return [('HAVE_BOOST', '1')] diff --git a/bob/extension/cmake.py b/bob/extension/cmake.py deleted file mode 100644 index 471f9a21379ee5b6421f87d9835a66c90b4a83b0..0000000000000000000000000000000000000000 --- a/bob/extension/cmake.py +++ /dev/null @@ -1,132 +0,0 @@ -import os - -HEADER = ( - '\n' - '# For both C and C++\n' - 'set(COMMON_FLAGS "-pedantic -Wall")\n' - 'if (WIN32)\n' - ' set(COMMON_FLAGS "-D_WIN32_WINNT=0x501") # Set min. Windows version to XP\n' - 'else(WIN32)\n' - ' set(COMMON_FLAGS "${COMMON_FLAGS} -pthread")\n' - 'endif (WIN32)\n' - 'if (NOT CMAKE_COMPILER_IS_GNUCC)\n' - ' # Then, it must be clang/clang++\n' - ' set(COMMON_FLAGS "${COMMON_FLAGS} -Qunused-arguments")\n' - 'endif ()\n' - '\n' - '# Force __LP64__ scheme on Mac OSX\n' - 'if(APPLE)\n' - ' set(CMAKE_MACOSX_RPATH TRUE CACHE BOOL "Enables the MACOS_RPATH feature for MacOSX builds" FORCE)\n' - ' set(COMMON_FLAGS "${COMMON_FLAGS} -m64")\n' - 'endif(APPLE)\n' - '\n' - '# For both RELEASE and DEBUG builds\n' - 'if(APPLE AND CMAKE_COMPILER_IS_GNUCC)\n' - ' if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "4.4")\n' - ' message(FATAL_ERROR "Minimum GCC version required on OSX is 4.4, but you have ${CMAKE_CXX_COMPILER_VERSION}")\n' - ' endif()\n' - ' set(COMMON_FLAGS "${COMMON_FLAGS} -Wno-long-long -Wno-variadic-macros")\n' - ' set(COMMON_CXX_FLAGS "-std=c++0x")\n' - ' set(COMMON_C_FLAGS "-std=c99")\n' - 'elseif(WIN32)\n' - ' set(COMMON_CXX_FLAGS "-std=gnu++0x")\n' - ' set(COMMON_C_FLAGS "-std=gnu99")\n' - 'else()\n' - ' set(COMMON_CXX_FLAGS "-std=c++0x")\n' - ' set(COMMON_C_FLAGS "-std=c99")\n' - 'endif()\n' - '\n' - '# These are used in type checks for cmake, be aware and don\'t change those\n' - 'set(CMAKE_CXX_FLAGS "${COMMON_CXX_FLAGS} ${COMMON_FLAGS} $ENV{CXXFLAGS} $ENV{CPPFLAGS}" CACHE STRING "Flags used by the compiler during release builds" FORCE)\n' - 'set(CMAKE_C_FLAGS "${COMMON_C_FLAGS} ${COMMON_FLAGS} $ENV{CFLAGS} $ENV{CPPFLAGS}" CACHE STRING "Flags used by the compiler during release builds" FORCE)\n' - '\n' - 'set(BUILD_SHARED_LIBS "ON" CACHE BOOL "Build shared libs")\n\n' -) - - -class CMakeListsGenerator: - """Generates a CMakeLists.txt file for the given sources, include directories and libraries.""" - - def __init__(self, name, sources, target_directory, version = '1.0.0', include_directories = [], system_include_directories=[], libraries = [], library_directories = [], macros = []): - """Initializes the CMakeLists generator. - - Keyword parameters: - - name : string - The name of the library to generate - - sources : [string] - The list of source files that should be compiled with CMake - - target_directory : [string] - The directory where the final library should be placed - - version - The version of the library, major.minor.patch - - include_directories : [string] - A list of include directories required to compile the ``sources`` - - system_include_directories : [string] - A list of include directories required to compile the ``sources``, which will be added as SYSTEM includes - - libraries : [string] - A list of libraries to be linked into the generated library - - library_directories : [string] - A list of directories, where the ``libraries`` can be found. - Note that the order of this list might be important. - - macros : [(string, string)] - A list of preprocessor defines ``name=value`` that will be added to the compilation - """ - - self.name = name - self.sources = sources - self.target_directory = target_directory - self.version = version - self.includes = include_directories - self.system_includes = system_include_directories - self.libraries = libraries - self.library_directories = library_directories - self.macros = macros - - def generate(self, source_directory, build_directory): - """Generates the CMakeLists.txt file in the given directory.""" - - # check if CFLAGS or CXXFLAGS are set, and set them if not - if 'CFLAGS' not in os.environ: - os.environ['CFLAGS'] = '-O3 -g0 -DNDEBUG -mtune=native' - if 'CXXFLAGS' not in os.environ: - os.environ['CXXFLAGS'] = '-O3 -g0 -DNDEBUG -mtune=native' - - source_dir = os.path.realpath(source_directory) - - # source and target in different directories -> use absolute paths - source_files = [os.path.join(source_dir, s) for s in self.sources] - - filename = os.path.join(build_directory, "CMakeLists.txt") - with open(filename, 'w') as f: - f.write('# WARNING! This file is automatically generated. Do not change its contents.\n\n') - f.write('cmake_minimum_required(VERSION 2.8)\n') - f.write('project(%s)\n' % self.name) - f.write(HEADER) - # add include directories - for directory in self.includes: - f.write('include_directories(%s)\n' % directory) - for directory in self.system_includes: - f.write('include_directories(SYSTEM %s)\n' % directory) - # add link directories - # TODO: handle RPATH and Non-RPATH differently (don't know, how, though) - for directory in self.library_directories: - f.write('link_directories(%s)\n' % directory) - # add defines - for macro in self.macros: - f.write('add_definitions(-D%s=%s)\n' % macro) - # compile this library - f.write('\nadd_library(${PROJECT_NAME} \n\t' + "\n\t".join(source_files) + '\n)\n') - f.write('set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE TRUE)\n') - f.write('set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY %s)\n\n' % self.target_directory) - # link libraries - if self.libraries: - f.write('target_link_libraries(${PROJECT_NAME} %s)\n\n' % " ".join(self.libraries)) diff --git a/bob/extension/config.py b/bob/extension/config.py index 5421621bcbc032aa969d1b66b1151ec98b5d5b9b..372b84bc76ee02d1f62fae1f9324cb7521c55b81 100644 --- a/bob/extension/config.py +++ b/bob/extension/config.py @@ -1,13 +1,15 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : -'''Functionality to implement python-based config file parsing and loading. -''' +"""Functionality to implement python-based config file parsing and loading. +""" -import types +import logging import pkgutil +import types + from os.path import isfile -import logging + import pkg_resources logger = logging.getLogger(__name__) @@ -16,276 +18,293 @@ LOADED_CONFIGS = [] def _load_context(path, mod): - '''Loads the Python file as module, returns a resolved context + """Loads the Python file as module, returns a resolved context + + This function is implemented in a way that is both Python 2 and Python 3 + compatible. It does not directly load the python file, but reads its contents + in memory before Python-compiling it. It leaves no traces on the file system. + + Parameters + ---------- + path : str + The full path of the Python file to load the module contents + from + mod : module + A preloaded module to use as context for the next module + loading. You can create a new module using :py:mod:`types` as in ``m = + types.ModuleType('name'); m.__dict__.update(ctxt)`` where ``ctxt`` is a + python dictionary with string -> object values representing the contents + of the module to be created. + + Returns + ------- + mod : :any:`module` + A python module with the fully resolved context + """ + + # executes the module code on the context of previously imported modules + with open(path, "rb") as f: + exec(compile(f.read(), path, "exec"), mod.__dict__) - This function is implemented in a way that is both Python 2 and Python 3 - compatible. It does not directly load the python file, but reads its contents - in memory before Python-compiling it. It leaves no traces on the file system. - - Parameters - ---------- - path : str - The full path of the Python file to load the module contents - from - mod : module - A preloaded module to use as context for the next module - loading. You can create a new module using :py:mod:`types` as in ``m = - types.ModuleType('name'); m.__dict__.update(ctxt)`` where ``ctxt`` is a - python dictionary with string -> object values representing the contents - of the module to be created. - - Returns - ------- - mod : :any:`module` - A python module with the fully resolved context - ''' - - # executes the module code on the context of previously imported modules - with open(path, "rb") as f: - exec(compile(f.read(), path, 'exec'), mod.__dict__) - - return mod + return mod def _get_module_filename(module_name): - """Resolves a module name to an actual Python file. - - Parameters - ---------- - module_name : str - The name of the module - - Returns - ------- - str - The Python files that corresponds to the module name. - """ - loader = pkgutil.get_loader(module_name) - if loader is None: - return '' - try: - return loader.path - except AttributeError: - return loader.filename + """Resolves a module name to an actual Python file. + + Parameters + ---------- + module_name : str + The name of the module + + Returns + ------- + str + The Python files that corresponds to the module name. + """ + loader = pkgutil.get_loader(module_name) + if loader is None: + return "" + try: + return loader.path + except AttributeError: + return loader.filename def _object_name(path, common_name): - path = path.rsplit(':', 1) - name = path[1] if len(path) > 1 else common_name - path = path[0] - return path, name - - -def _resolve_entry_point_or_modules(paths, entry_point_group, - common_name=None): - """Resolves a mixture of paths, entry point names, and module names to just - paths. For example paths can be: - ``paths = ['/tmp/config.py', 'config1', 'bob.extension.config2']``. - - Parameters - ---------- - paths : [str] - An iterable strings that either point to actual files, are entry point - names, or are module names. - entry_point_group : str - The entry point group name to search in entry points. - common_name : None or str - It will be used as a default name for object names. See the - attribute_name parameter from :any:`load`. - - Raises - ------ - ValueError - If one of the paths cannot be resolved to an actual path to a file. - - Returns - ------- - paths : [str] - The resolved paths pointing to existing files. - module_names : [str] - The valid python module names to bind each of the files to - object_names : [str] - The name of objects that are supposed to be picked from paths. - """ - - entries = {e.name: e for e in - pkg_resources.iter_entry_points(entry_point_group)} - - files = [] - module_names = [] - object_names = [] - - for i, path in enumerate(paths): - - old_path = path - module_name = 'user_config' # fixed module name for files with full paths - path, object_name = _object_name(path, common_name) - - # if it already points to a file - if isfile(path): - pass - - # If it is an entry point name, collect path and module name - elif path in entries: - entry = entries[path] - module_name = entry.module_name - object_name = entry.attrs[0] if entry.attrs else common_name - path = _get_module_filename(module_name) - if not isfile(path): - raise ValueError( - "The specified entry point: `{}' pointing to module: `{}' and " - "resolved to: `{}' does not point to an existing " - "file.".format(old_path, module_name, path)) - - # If it is not a path nor an entry point name, it is a module name then? - else: - # if we have gotten here so far then path is the module_name. - module_name = path - path = _get_module_filename(path) - if not isfile(path): - raise ValueError( - "The specified path: `{}' resolved to: `{}' is not a file, not a " - "entry point name of `{}', nor a module name".format( - old_path, path, entry_point_group or '')) - - files.append(path) - module_names.append(module_name) - object_names.append(object_name) - - return files, module_names, object_names + path = path.rsplit(":", 1) + name = path[1] if len(path) > 1 else common_name + path = path[0] + return path, name + + +def _resolve_entry_point_or_modules(paths, entry_point_group, common_name=None): + """Resolves a mixture of paths, entry point names, and module names to just + paths. For example paths can be: + ``paths = ['/tmp/config.py', 'config1', 'bob.extension.config2']``. + + Parameters + ---------- + paths : [str] + An iterable strings that either point to actual files, are entry point + names, or are module names. + entry_point_group : str + The entry point group name to search in entry points. + common_name : None or str + It will be used as a default name for object names. See the + attribute_name parameter from :any:`load`. + + Raises + ------ + ValueError + If one of the paths cannot be resolved to an actual path to a file. + + Returns + ------- + paths : [str] + The resolved paths pointing to existing files. + module_names : [str] + The valid python module names to bind each of the files to + object_names : [str] + The name of objects that are supposed to be picked from paths. + """ + + entries = { + e.name: e for e in pkg_resources.iter_entry_points(entry_point_group) + } + + files = [] + module_names = [] + object_names = [] + + for i, path in enumerate(paths): + + old_path = path + module_name = ( + "user_config" # fixed module name for files with full paths + ) + path, object_name = _object_name(path, common_name) + + # if it already points to a file + if isfile(path): + pass + + # If it is an entry point name, collect path and module name + elif path in entries: + entry = entries[path] + module_name = entry.module_name + object_name = entry.attrs[0] if entry.attrs else common_name + path = _get_module_filename(module_name) + if not isfile(path): + raise ValueError( + "The specified entry point: `{}' pointing to module: `{}' and " + "resolved to: `{}' does not point to an existing " + "file.".format(old_path, module_name, path) + ) + + # If it is not a path nor an entry point name, it is a module name then? + else: + # if we have gotten here so far then path is the module_name. + module_name = path + path = _get_module_filename(path) + if not isfile(path): + raise ValueError( + "The specified path: `{}' resolved to: `{}' is not a file, not a " + "entry point name of `{}', nor a module name".format( + old_path, path, entry_point_group or "" + ) + ) + + files.append(path) + module_names.append(module_name) + object_names.append(object_name) + + return files, module_names, object_names def load(paths, context=None, entry_point_group=None, attribute_name=None): - '''Loads a set of configuration files, in sequence - - This method will load one or more configuration files. Every time a - configuration file is loaded, the context (variables) loaded from the - previous file is made available, so the new configuration file can override - or modify this context. - - Parameters - ---------- - paths : [str] - A list or iterable containing paths (relative or absolute) of - configuration files that need to be loaded in sequence. Each - configuration file is loaded by creating/modifying the context generated - after each file readout. - context : :py:class:`dict`, optional - If provided, start the readout of the first configuration file with the - given context. Otherwise, create a new internal context. - entry_point_group : :py:class:`str`, optional - If provided, it will treat non-existing file paths as entry point names - under the ``entry_point_group`` name. - attribute_name : None or str - If provided, will look for the attribute_name variable inside the loaded - files. Paths ending with `some_path:variable_name` can override the - attribute_name. The entry_point_group must provided as well - attribute_name is not None. - - Returns - ------- - mod : :any:`module` or object - A module representing the resolved context, after loading the provided - modules and resolving all variables. If attribute_name is given, the - object with the attribute_name name (or the name provided by user) is - returned instead of the module. - - Raises - ------ - ImportError - If attribute_name is given but the object does not exist in the paths. - ValueError - If attribute_name is given but entry_point_group is not given. - - ''' - if attribute_name and not entry_point_group: - raise ValueError( - "entry_point_group must be provided when using the " - "attribute_name parameter.") - - # resolve entry points to paths - if entry_point_group is not None: - paths, names, object_names = _resolve_entry_point_or_modules( - paths, entry_point_group, attribute_name) - else: - names = len(paths) * ['user_config'] - - ctxt = types.ModuleType('initial_context') - if context is not None: - ctxt.__dict__.update(context) - # Small gambiarra (https://www.urbandictionary.com/define.php?term=Gambiarra) - # to avoid the garbage collector to collect some already imported modules. - LOADED_CONFIGS.append(ctxt) - - # if no paths are provided, return context - if not paths: - return ctxt - - for k, n in zip(paths, names): - logger.debug("Loading configuration file `%s'...", k) - mod = types.ModuleType(n) - # remove the keys that might break the loading of the next config file. - ctxt.__dict__.pop('__name__', None) - ctxt.__dict__.pop('__package__', None) - # do not propogate __ variables - context = {k: v for k, v in ctxt.__dict__.items() if not k.startswith('__')} - mod.__dict__.update(context) - LOADED_CONFIGS.append(mod) - ctxt = _load_context(k, mod) - - if not attribute_name: - return mod - - # We pick the last object_name here. Normally users should provide just one - # path when enabling the attribute_name parameter. - attribute_name = object_names[-1] - if not hasattr(mod, attribute_name): - raise ImportError( - "The desired variable '%s' does not exist in any of " - "your configuration files: %s" % (attribute_name, ', '.join(paths))) - - return getattr(mod, attribute_name) + """Loads a set of configuration files, in sequence + + This method will load one or more configuration files. Every time a + configuration file is loaded, the context (variables) loaded from the + previous file is made available, so the new configuration file can override + or modify this context. + + Parameters + ---------- + paths : [str] + A list or iterable containing paths (relative or absolute) of + configuration files that need to be loaded in sequence. Each + configuration file is loaded by creating/modifying the context generated + after each file readout. + context : :py:class:`dict`, optional + If provided, start the readout of the first configuration file with the + given context. Otherwise, create a new internal context. + entry_point_group : :py:class:`str`, optional + If provided, it will treat non-existing file paths as entry point names + under the ``entry_point_group`` name. + attribute_name : None or str + If provided, will look for the attribute_name variable inside the loaded + files. Paths ending with `some_path:variable_name` can override the + attribute_name. The entry_point_group must provided as well + attribute_name is not None. + + Returns + ------- + mod : :any:`module` or object + A module representing the resolved context, after loading the provided + modules and resolving all variables. If attribute_name is given, the + object with the attribute_name name (or the name provided by user) is + returned instead of the module. + + Raises + ------ + ImportError + If attribute_name is given but the object does not exist in the paths. + ValueError + If attribute_name is given but entry_point_group is not given. + + """ + if attribute_name and not entry_point_group: + raise ValueError( + "entry_point_group must be provided when using the " + "attribute_name parameter." + ) + + # resolve entry points to paths + if entry_point_group is not None: + paths, names, object_names = _resolve_entry_point_or_modules( + paths, entry_point_group, attribute_name + ) + else: + names = len(paths) * ["user_config"] + + ctxt = types.ModuleType("initial_context") + if context is not None: + ctxt.__dict__.update(context) + # Small gambiarra (https://www.urbandictionary.com/define.php?term=Gambiarra) + # to avoid the garbage collector to collect some already imported modules. + LOADED_CONFIGS.append(ctxt) + + # if no paths are provided, return context + if not paths: + return ctxt + + for k, n in zip(paths, names): + logger.debug("Loading configuration file `%s'...", k) + mod = types.ModuleType(n) + # remove the keys that might break the loading of the next config file. + ctxt.__dict__.pop("__name__", None) + ctxt.__dict__.pop("__package__", None) + # do not propogate __ variables + context = { + k: v for k, v in ctxt.__dict__.items() if not k.startswith("__") + } + mod.__dict__.update(context) + LOADED_CONFIGS.append(mod) + ctxt = _load_context(k, mod) + + if not attribute_name: + return mod + + # We pick the last object_name here. Normally users should provide just one + # path when enabling the attribute_name parameter. + attribute_name = object_names[-1] + if not hasattr(mod, attribute_name): + raise ImportError( + "The desired variable '%s' does not exist in any of " + "your configuration files: %s" % (attribute_name, ", ".join(paths)) + ) + + return getattr(mod, attribute_name) def mod_to_context(mod): - """Converts the loaded module of :any:`load` to a dictionary context. - This function removes all the variables that start and end with ``__``. - - Parameters - ---------- - mod : object - What is returned by :any:`load` - - Returns - ------- - dict - The context that was in ``mod``. - """ - return {k: v for k, v in mod.__dict__.items() - if not (k.startswith('__') and k.endswith('__'))} - - -def resource_keys(entry_point_group, exclude_packages=[], strip=['dummy']): - """Reads and returns all resources that are registered with the given - entry_point_group. Entry points from the given ``exclude_packages`` are - ignored. - - Parameters - ---------- - entry_point_group : str - The entry point group name. - exclude_packages : :any:`list`, optional - List of packages to exclude when finding resources. - strip : :any:`list`, optional - Entrypoint names that start with any value in ``strip`` will be ignored. - - Returns - ------- - :any:`list` - List of found resources. - """ - ret_list = [entry_point.name for entry_point in - pkg_resources.iter_entry_points(entry_point_group) - if (entry_point.dist.project_name not in exclude_packages and - not entry_point.name.startswith(tuple(strip)))] - return sorted(ret_list) + """Converts the loaded module of :any:`load` to a dictionary context. + This function removes all the variables that start and end with ``__``. + + Parameters + ---------- + mod : object + What is returned by :any:`load` + + Returns + ------- + dict + The context that was in ``mod``. + """ + return { + k: v + for k, v in mod.__dict__.items() + if not (k.startswith("__") and k.endswith("__")) + } + + +def resource_keys(entry_point_group, exclude_packages=[], strip=["dummy"]): + """Reads and returns all resources that are registered with the given + entry_point_group. Entry points from the given ``exclude_packages`` are + ignored. + + Parameters + ---------- + entry_point_group : str + The entry point group name. + exclude_packages : :any:`list`, optional + List of packages to exclude when finding resources. + strip : :any:`list`, optional + Entrypoint names that start with any value in ``strip`` will be ignored. + + Returns + ------- + :any:`list` + List of found resources. + """ + ret_list = [ + entry_point.name + for entry_point in pkg_resources.iter_entry_points(entry_point_group) + if ( + entry_point.dist.project_name not in exclude_packages + and not entry_point.name.startswith(tuple(strip)) + ) + ] + return sorted(ret_list) diff --git a/bob/extension/data/config_with_module.py b/bob/extension/data/config_with_module.py index ecb2d0d7920abe07bf1ef0d7660f457cb9b01861..a0311e761de7e9a2b693068464d572c524686135 100644 --- a/bob/extension/data/config_with_module.py +++ b/bob/extension/data/config_with_module.py @@ -1,4 +1,5 @@ import numpy + def return_zeros(): return numpy.zeros(shape=(2,)) diff --git a/bob/extension/data/defaults-config b/bob/extension/data/defaults-config index 39e8f86c8c91996ba8d159da85115697914bfff4..f332e2b115a3091f8162ae94d5404bbd03ad614e 100644 --- a/bob/extension/data/defaults-config +++ b/bob/extension/data/defaults-config @@ -1,4 +1,4 @@ { "bob.db.atnt.directory": "/home/bob/databases/atnt", "bob.db.mobio.directory": "/home/bob/databases/mobio" -} \ No newline at end of file +} diff --git a/bob/extension/data/load_config.py b/bob/extension/data/load_config.py index 21abd01b385338741042deb0c5a554810c2fdde8..c908703b9f352b20f38d60353257f8ca4513f1a3 100644 --- a/bob/extension/data/load_config.py +++ b/bob/extension/data/load_config.py @@ -1,3 +1,3 @@ # the b variable from the last config file is available here -c = b + 1 -b = b + 3 +c = b + 1 # noqa: F821 +b = b + 3 # noqa: F821 diff --git a/bob/extension/data/resource_config.py b/bob/extension/data/resource_config.py index 2ba8046898a68e9f1b34d121f38441e4644e8db8..a15dd71ba699f4ba70a34ef6bf8ffa53d4fbf542 100644 --- a/bob/extension/data/resource_config.py +++ b/bob/extension/data/resource_config.py @@ -1,3 +1,3 @@ a = 1 b = a + 2 -from .. import rc +from .. import rc # noqa: F401 diff --git a/bob/extension/data/subpackage/config.py b/bob/extension/data/subpackage/config.py index 3abcf96fdcb4d94ec5a9889224d7bff10ed507d1..ddb815b1328b6d8046f733dfd1a527b21184c412 100644 --- a/bob/extension/data/subpackage/config.py +++ b/bob/extension/data/subpackage/config.py @@ -1 +1 @@ -from ..basic_config import * +from ..basic_config import * # noqa: F401,F403 diff --git a/bob/extension/download.py b/bob/extension/download.py index f55aad4743af4a8a1d1ade80d5c41bf9ee1dec81..e4e43826d32def7144899a243b82e3bcab41af8c 100644 --- a/bob/extension/download.py +++ b/bob/extension/download.py @@ -8,6 +8,7 @@ import logging import os import tarfile import zipfile + from pathlib import Path from shutil import copyfileobj from urllib.request import urlopen @@ -18,7 +19,9 @@ logger = logging.getLogger(__name__) def _bob_data_folder(): - return rc.get("bob_data_folder", os.path.join(os.path.expanduser("~"), "bob_data")) + return rc.get( + "bob_data_folder", os.path.join(os.path.expanduser("~"), "bob_data") + ) def _unzip(zip_file, directory): @@ -116,7 +119,9 @@ def download_file_from_possible_urls(urls, out_file): download_file(url, out_file) break except Exception: - logger.warning("Could not download from the %s url", url, exc_info=True) + logger.warning( + "Could not download from the %s url", url, exc_info=True + ) else: # else is for the for loop raise RuntimeError( f"Could not download the requested file from the following urls: {urls}" diff --git a/bob/extension/include/bob.extension/defines.h b/bob/extension/include/bob.extension/defines.h deleted file mode 100644 index 84d0b4af2cce5b62574fbc4aa5401a77abc8a03b..0000000000000000000000000000000000000000 --- a/bob/extension/include/bob.extension/defines.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file bob/extension/include/bob.extension/documentation.h - * @date Fri Nov 21 10:27:38 CET 2014 - * @author Manuel Guenther <manuel.guenther@idiap.ch> - * - * @brief Implements a few functions to generate doc strings - * - * Copyright (C) 2011-2014 Idiap Research Institute, Martigny, Switzerland - */ - - -/** By including this file, you will be able to -* -* 1. Use the Python2 functions PyInt_Check, PyInt_AS_LONG, PyString_Check, PyString_FromString and PyString_AS_STRING within the bindings for python3 -* 2. Add try{ ... } catch {...} blocks around your bindings so that you make sure that **all** exceptions in the C++ code are handled correctly. -* -*/ - -#ifndef BOB_EXTENSION_DEFINES_H_INCLUDED -#define BOB_EXTENSION_DEFINES_H_INCLUDED - - -#if PY_VERSION_HEX >= 0x03000000 -#define PyInt_Check PyLong_Check -#define PyInt_AS_LONG PyLong_AS_LONG -#define PyString_Check PyUnicode_Check -#define PyString_FromString PyUnicode_FromString -#define PyString_FromFormat PyUnicode_FromFormat -#define PyString_AS_STRING(x) PyBytes_AS_STRING(make_safe(PyUnicode_AsUTF8String(x)).get()) -#define PyString_AsString(x) PyUnicode_AsUTF8(x) -#endif - -#define PyBob_NumberCheck(x) (PyInt_Check(x) || PyLong_Check(x) || PyFloat_Check(x) || PyComplex_Check(x)) - - -// BOB_TRY is simply a try{ -#define BOB_TRY try{ - -// for catching exceptions, you can define a message, and you have to select -// the error return value (i.e., -1 for constructors, and 0 for other -// functions) - -// There exist two macros that will print slightly different messages. -// BOB_CATCH_MEMBER is to be used within the binding of a class, and it will -// use the "self" pointer -// BOB_CATCH_FUNCTION is to be used to bind functions outside a class -#define BOB_CATCH_MEMBER(message,ret) }\ - catch (std::exception& e) {\ - PyErr_Format(PyExc_RuntimeError, "%s - %s: C++ exception caught: '%s'", Py_TYPE(self)->tp_name, message, e.what());\ - return ret;\ - } \ - catch (...) {\ - PyErr_Format(PyExc_RuntimeError, "%s - %s: unknown exception caught", Py_TYPE(self)->tp_name, message);\ - return ret;\ - } - -#define BOB_CATCH_FUNCTION(message, ret) }\ - catch (std::exception& e) {\ - PyErr_Format(PyExc_RuntimeError, "%s: C++ exception caught: '%s'", message, e.what());\ - return ret;\ - } \ - catch (...) {\ - PyErr_Format(PyExc_RuntimeError, "%s: unknown exception caught", message);\ - return ret;\ - } - -#endif // BOB_EXTENSION_DEFINES_H_INCLUDED diff --git a/bob/extension/include/bob.extension/documentation.h b/bob/extension/include/bob.extension/documentation.h deleted file mode 100644 index d444cb1340aabdc2654ef0ea42410f03d4b89ad6..0000000000000000000000000000000000000000 --- a/bob/extension/include/bob.extension/documentation.h +++ /dev/null @@ -1,779 +0,0 @@ -/** - * @file bob/extension/include/bob.extension/documentation.h - * @date Fri Feb 21 18:29:37 CET 2014 - * @author Manuel Guenther <manuel.guenther@idiap.ch> - * - * @brief Implements a few functions to generate doc strings - * - * Copyright (C) 2011-2014 Idiap Research Institute, Martigny, Switzerland - */ - -#ifndef BOB_EXTENSION_DOCUMENTATION_H_INCLUDED -#define BOB_EXTENSION_DOCUMENTATION_H_INCLUDED - -#include <string> -#include <vector> -#include <set> -#include <stdexcept> -#include <iostream> -#include <string.h> - -#include <bob.extension/defines.h> - -namespace bob{ - namespace extension{ - - /** - * Use a static object of this class to document a variable. - * This class can be used to document both global variables as well as class member variables. - */ - class VariableDoc { - friend class ClassDoc; - public: - /** - * Generates a VariableDoc object. Please assure that use use this as a static member variable. - * @param variable_name The name of the variable - * @param variable_type The type of the variable, e.g., "float" or "array_like (float, 2D)" - * @param short_description A short description of the variable - * @param long_description An optional long description of the variable - */ - VariableDoc( - const char* const variable_name, - const char* const variable_type, - const char* const short_description, - const char* const long_description = 0 - ); - - /** - * Returns the name of the variable that is documented, i.e., the "variable_name" parameter of the constructor. - */ - char* name() const {return const_cast<char*>(variable_name.c_str());} - - /** - * Generates and returns the documentation string. - * @param alignment The default alignment is 80 characters. - * Since the package level documentation is automatically indented by 8 spaces in the python documentation, we need to subtract these values here... - * @return The documentation string, properly aligned, possibly including "ToDo's" for detected problems. - */ - char* doc(const unsigned alignment = 72) const; - - private: - // variable name and type - std::string variable_name; - std::string variable_type; - // variable description - std::string variable_description; - - // an internal string that is generated and returned. - mutable std::string description; - - }; - - /** - * Use a static object of this class to generate a properly aligned function documentation. - * Documentation generated by this class can be used for non-member functions as well as for member functions and constructors. - */ - class FunctionDoc { - friend class ClassDoc; - public: - /** - * Generates a FunctionDoc object. Please assure that use use this as a static member variable. - * @param function_name The name of the function you want to document - * @param short_description A short description of what the function does - * @param long_description An optional long description of the function - * @param is_member_function Set this to true if this documentation is the documentation of a member function - */ - FunctionDoc( - const char* const function_name, - const char* const short_description, - const char* const long_description = 0, - bool is_member_function = false - ); - - /** - * Copy constructor; will deep-copy the kwlists - */ - FunctionDoc(const FunctionDoc& other); - - /** Destrcutor */ - ~FunctionDoc(); - - /** - * Clones this FunctionDoc by providing a new function name - * This is useful, when a function is bound with several names. - * @param function_name The new name of the function - */ - FunctionDoc clone(const char* const function_name){ - FunctionDoc retval = FunctionDoc(*this); - retval.function_name = function_name; - return retval; - } - - /** - * Add a prototypical call for this function by defining the parameters and the return values. - * This function has to be called at least ones. - * @param variables A string containing a comma-separated list of parameters, e.g., "param1, param2" - * @param return_value A string containing a comma-separated list of return values, e.g., "retval1, retval2". - * If the function does not return anything, this value can be left at its default "None". - * To document a constructor, please use "" as return value. - */ - FunctionDoc& add_prototype( - const char* const variables, - const char* const return_value = "None" - ); - - /** - * Add the documentation for a parameter added with the add_prototype function - * @param parameter_name The name of the parameter, e.g. "param1" - * @param parameter_type The type of the parameter, e.g. "float" or "array_like (float, 2D)"; indicate if the parameter is optional here - * @param parameter_description The description of the parameter - */ - FunctionDoc& add_parameter( - const char* const parameter_name, - const char* const parameter_type, - const char* const parameter_description - ); - /** - * Add the documentation of a return value added with the add_prototype function - * @param return_name The name assigned to the return value - * @param return_type The tape of the returned value - * @param return_description The description of the return value - */ - FunctionDoc& add_return( - const char* const return_name, - const char* const return_type, - const char* const return_description - ); - - /** - * Returns the name of the function that is documented (i.e., the function_name parameter of the constructor) - */ - const char* const name() const {return function_name.c_str();} - - /** - * Generates and returns the documentation string. - * A .. todo:: directive is added for each detected mistake. - * @param alignment The default alignment is 80 characters. - * Since the package level documentation is automatically indented by 8 spaces in the python documentation, we need to subtract these values here... - * @return The documentation string, properly aligned, possibly including "ToDo's" for detected problems. - */ - const char* const doc(const unsigned alignment = 72, const unsigned indent = 0) const; - - /** - * Returns the (NULL-terminated) list of variables for the given prototype index, which can be used as kwlist argument in the bindings. - * @param index The index of the prototype - * @return A NULL-terminated list of variable names - */ - char** kwlist(unsigned index = 0) const{ - if (index >= kwlists.size()) throw std::runtime_error("The prototype for the given index is not found"); - return kwlists[index]; - } - - /** - * Generates and prints the usage string, simply listing the possible ways to call the function. - * - * The error meaasge is printed to the std::cerr stream - */ - void print_usage() const; - - - private: - // the function name - std::string function_name; - // the description - std::string function_description; - // if this is a member function, the indentation must be shorter - bool is_member; - // prototypes - std::vector<std::string> prototype_variables; - std::vector<std::string> prototype_returns; - // parameter documentation - std::vector<std::string> parameter_names; - std::vector<std::string> parameter_types; - std::vector<std::string> parameter_descriptions; - // return value documentation - std::vector<std::string> return_names; - std::vector<std::string> return_types; - std::vector<std::string> return_descriptions; - - // the list of wey-word arguments - std::vector<char**> kwlists; - - // an internal string that is generated and returned. - mutable std::string description; - }; - - - /** - * Use a static object of this class to document a class. - * Documenting a class includes the documentation of the constructor, - * but not the documentation of the other member functions. - * For those, please use the FunctionDoc class. - */ - class ClassDoc{ - public: - /** - * Generates a ClassDoc object. Please assure that use use this as a static member variable. - * @param class_name The name of the class to be documented - * @param short_description A short description of the class - * @param long_description An optional long description of the class - */ - ClassDoc( - const char* const class_name, - const char* const short_description, - const char* const long_description = 0 - ); - - /** - * Add the documentation of the constructor. - * This function can be called only once. - * @param constructor_documentation An instance of the FunctionDoc class that contains the documentation of the constructor. - * Please read the documentation of that class on how to generate constructor documentations. - */ - ClassDoc& add_constructor( - const FunctionDoc& constructor_documentation - ); - - /** - * Adds the given function to the highlighted section. - * @param function_documentation An instance of the FunctionDoc class that should be highlighted. - */ - ClassDoc& highlight( - const FunctionDoc& function_documentation - ); - - /** - * Adds the given variable to the highlighted section. - * @param function_documentation An instance of the FunctionDoc class that should be highlighted. - */ - ClassDoc& highlight( - const VariableDoc& variable_documentation - ); - - /** - * Returns the name of the class that is documented, i.e., the "class_name" parameter of the constructor. - */ - char* name() const {return const_cast<char*>(class_name.c_str());} - - /** - * Generates and returns the documentation string. - * @param alignment The default alignment is 80 characters. - * Since the package level documentation is automatically indented by 8 spaces in the python documentation, we need to subtract these values here... - * @return The documentation string, properly aligned, possibly including "ToDo's" for detected problems. - */ - char* doc(const unsigned alignment = 72) const; - - /** - * Returns the (NULL-terminated) list of variables of the constructor documentation for the given prototype index, which can be used as kwlist argument in the bindings. - * @param index The index of the prototype - * @return A NULL-terminated list of variable names - */ - char** kwlist(unsigned index = 0) const{ - if (constructor.empty()) throw std::runtime_error("The class documentation does not have constructor documentation"); - return constructor.front().kwlist(index); - } - - /** - * Prints the usage string of the constructor, if available. - */ - void print_usage() const {if (!constructor.empty()) constructor.front().print_usage();} - - - private: - // class name - std::string class_name; - // class description - std::string class_description; - // constructor; should be only one, though - std::vector<FunctionDoc> constructor; - - // highlighting - std::vector<FunctionDoc> highlighted_functions; - std::vector<VariableDoc> highlighted_variables; - - // an internal string that is generated and returned. - mutable std::string description; - }; - - } -} - - -///////////////////////////////////////////////////////////// -//////////////////// Implementations //////////////////////// -///////////////////////////////////////////////////////////// - - -///////////////////////////////////////////////////////////// -/// helper functions - -#ifndef BOB_SHORT_DOCSTRINGS -// removes leading and trailing spaces -static std::string _strip(const std::string& str, const std::string& sep = " []()|"){ - unsigned first = 0, last = str.size(); - while (first < str.size() && sep.find(str[first]) != std::string::npos) ++first; - while (last > 0 && sep.find(str[last-1]) != std::string::npos) --last; - return str.substr(first, last-first); -} - -// splits the given string by the given separator -static std::vector<std::string> _split(const std::string& str, char limit = ' ', bool allow_empty=true){ - std::vector<std::string> splits; - size_t j = str.find_first_not_of(limit); - size_t i = str.find(limit, j); - j = 0; - while (i != std::string::npos){ - splits.push_back(str.substr(j, i-j)); - j = i+1; - i = str.find(limit, j); - } - splits.push_back(str.substr(j)); - if (!allow_empty && !splits.empty() && splits.back().empty()) - splits.pop_back(); - return splits; -} - -// aligns the given string using the given indent to the given alignment length; -// line breaks are handled carefully. -static std::string _align(std::string str, unsigned indent, unsigned alignment){ - // first, split the newlines - auto lines = _split(str, '\n'); - - std::string aligned; - unsigned current_indent = indent; - bool first_line = true; - // now, split each line - for (auto line_it = lines.begin(); line_it != lines.end(); ++line_it){ - auto words = _split(*line_it); - // fill in one line - unsigned len = 0; - unsigned new_indent = indent; - if (!line_it->empty()){ - // increase indent? - const std::string& w = _strip(words[0], " "); - if ((w.size() == 2 && w[0] == '.' && w[1] == '.') || - (w.size() >= 1 && '0' <= w[0] && '9' >= w[0]) || - (w.size() == 1 && '*' == w[0]) ){ - new_indent += w.size() + 1; - } - size_t first_word_indent = line_it->find_first_not_of(' '); - if (first_word_indent != std::string::npos && first_word_indent != 0){ - new_indent += first_word_indent; - } - } - for (auto word_it = words.begin(); word_it != words.end(); ++word_it){ - if (aligned.empty() || len + word_it->size() >= alignment || !first_line){ - // line reached alignment - if (!aligned.empty()){ - aligned += "\n"; - } - // add indent and start new line - for (unsigned j = current_indent; j--;) aligned += " "; - len = current_indent; - first_line = true; - } - // set new indent - current_indent = new_indent; - // add word - aligned += *word_it + " "; - len += word_it->size() + 1; - } - current_indent = indent; - first_line = false; - } - - return aligned; -} - -// Aligns the parameter description -static void _align_parameter(std::string& str, const std::string& name, const std::string& type, const std::string& description, unsigned indent, unsigned alignment){ - if (type.find(':') != std::string::npos && type.find('`') != std::string::npos) - // we expect that this is a :py:class: directive, which is simply written - str += _align("``" + name + "`` : " + type + "", indent, alignment) + "\n\n"; - else - // otherwise we emphasize the parameter type with *...* - str += _align("``" + name + "`` : *" + type + "*", indent, alignment) + "\n\n"; - str += _align(description, indent + 4, alignment) + "\n\n"; -} - -static std::string _prototype(const std::string& name, const std::string& variables, const std::string& retval){ - if (retval.empty()) - return "**" + name + "** (" + variables + ")"; - else - return name + "(" + variables + ") -> " + retval; -} - -static std::string _usage(const std::string& name, const std::string& variables, const std::string& retval){ - if (retval.empty()) - return name + "(" + variables + ")"; - else - return name + "(" + variables + ") -> " + retval; -} - - -static void _check(std::string& doc, const std::vector<std::string>& vars, const std::vector<std::string>& docs, const std::string& type){ - // check that all parameters are documented. If not, add a TODO - std::set<std::string> undoc; - std::set<std::string> unused; - // gather parameters - for (auto pit = vars.begin(); pit != vars.end(); ++pit){ - const auto splits = _split(*pit, ','); - for (auto sit = splits.begin(); sit != splits.end(); ++sit){ - undoc.insert(_strip(*sit)); - } - } - for (auto pit = docs.begin(); pit != docs.end(); ++pit){ - const auto splits = _split(*pit, ','); - for (auto sit = splits.begin(); sit != splits.end(); ++sit){ - std::string x = _strip(*sit); - if (undoc.find(x) == undoc.end()){ - unused.insert(x); - } else { - undoc.erase(x); - } - } - } - if (undoc.size()){ - std::string all; - for (auto pit = undoc.begin(); pit != undoc.end(); ++pit){ - if (*pit != "None"){ - if (!all.empty()) all += ", "; - all += *pit; - } - } - if (!all.empty()){ - doc += "\n" + _align(".. todo:: The " + type + "(s) '" + all + "' are used, but not documented.", 0, (unsigned)-1) + "\n"; - } - } - if (unused.size()){ - std::string all; - for (auto pit = unused.begin(); pit != unused.end(); ++pit){ - if (!all.empty()) all += ", "; - all += *pit; - } - doc += "\n" + _align(".. todo:: The " + type + "(s) '" + all + "' are documented, but nowhere used.", 0, (unsigned)-1) + "\n"; - } -} - -#endif // ! BOB_SHORT_DOCSTRINGS - - -///////////////////////////////////////////////////////////// -/// FunctionDoc - -inline bob::extension::FunctionDoc::FunctionDoc( - const char* const function_name, - const char* const short_description, - const char* const long_description, - bool is_member_function -) : function_name(function_name), function_description(short_description), is_member(is_member_function) -{ -#ifndef BOB_SHORT_DOCSTRINGS - if (long_description){ - function_description += "\n\n"; - function_description += long_description; - } -#endif -} - -inline bob::extension::FunctionDoc::FunctionDoc( - const bob::extension::FunctionDoc& other -) -: function_name(other.function_name), - function_description(other.function_description), - is_member(other.is_member), - prototype_variables(other.prototype_variables), - prototype_returns(other.prototype_returns), - parameter_names(other.parameter_names), - parameter_types(other.parameter_types), - parameter_descriptions(other.parameter_descriptions), - return_names(other.return_names), - return_types(other.return_types), - return_descriptions(other.return_descriptions) -{ - // Add the variables to the kwlists - kwlists.resize(other.kwlists.size()); - for (unsigned i = 0; i < kwlists.size(); ++i){ - // copy all kwlists - unsigned counts = _split(prototype_variables[i], ',', false).size(); - char** names = new char*[counts + 1]; - for (unsigned j = 0; j < counts; ++j){ - names[j] = const_cast<char*>(strdup(other.kwlists[i][j])); - } - // add terminating NULL pointer - names[counts] = 0; - kwlists[i] = names; - } -} - -inline bob::extension::FunctionDoc::~FunctionDoc(){ - - for (unsigned i = 0; i < kwlists.size(); ++i){ - unsigned counts = _split(prototype_variables[i], ',').size(); - for (unsigned j = 0; j < counts; ++j){ - free(kwlists[i][j]); - } - delete[] kwlists[i]; - } -} - -inline bob::extension::FunctionDoc& bob::extension::FunctionDoc::add_prototype( - const char* const variables, - const char* const return_values -){ - // Add the variables to the kwlists - std::vector<std::string> vars = _split(variables, ',', false); - char** names = new char*[vars.size() + 1]; - for (unsigned i = 0; i < vars.size(); ++i){ - names[i] = const_cast<char*>(strdup(_strip(vars[i]).c_str())); - } - // add terminating NULL pointer - names[vars.size()] = 0; - kwlists.push_back(names); - - prototype_variables.push_back(variables); -#ifndef BOB_SHORT_DOCSTRINGS - if (!return_values) - prototype_returns.push_back(""); - else - prototype_returns.push_back(return_values); -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - -inline bob::extension::FunctionDoc& bob::extension::FunctionDoc::add_parameter( - const char* const parameter_name, - const char* const parameter_type, - const char* const parameter_description -) -{ -#ifndef BOB_SHORT_DOCSTRINGS - parameter_names.push_back(parameter_name); - parameter_types.push_back(parameter_type); - parameter_descriptions.push_back(parameter_description); -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - -inline bob::extension::FunctionDoc& bob::extension::FunctionDoc::add_return( - const char* const parameter_name, - const char* const parameter_type, - const char* const parameter_description -) -{ -#ifndef BOB_SHORT_DOCSTRINGS - return_names.push_back(parameter_name); - return_types.push_back(parameter_type); - return_descriptions.push_back(parameter_description); -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - -inline const char* const bob::extension::FunctionDoc::doc( - const unsigned alignment, - const unsigned indent -) const -{ -#ifdef BOB_SHORT_DOCSTRINGS - return function_description.c_str(); -#else - if (description.empty()){ - // in case of member functions, the alignment has to be decreased further since class member function are automatically indented by 4 further spaces. - unsigned align = is_member ? alignment - 4 : alignment; - description = ""; - switch(prototype_variables.size()){ - case 0: - description = _align(".. todo:: Please use ``FunctionDoc.add_prototype`` to add at least one prototypical way to call this function", indent, (unsigned)-1) + "\n"; - break; - case 1: - // only one way to call; use the default way - description = _align(_prototype(function_name, prototype_variables[0], prototype_returns[0]), indent, unsigned(-1)) + "\n"; - break; - default: - // several ways to call; list them - for (unsigned n = 0; n < prototype_variables.size(); ++n) - description += _align("* " + _prototype(function_name, prototype_variables[n], prototype_returns[n]), indent, unsigned(-1)) + "\n"; - } - // add function description - description += "\n" + _align(function_description, indent, align) + "\n"; - - // check that all parameters are documented - _check(description, prototype_variables, parameter_names, "parameter"); - - // check that all return values are documented - _check(description, prototype_returns, return_names, "return value"); - - if (!parameter_names.empty()){ - // add parameter description - // description += "\n" + _align("Parameters") + _align("----------"); - description += "\n" + _align("**Parameters:**", indent, align) + "\n\n"; - for (unsigned i = 0; i < parameter_names.size(); ++i){ - _align_parameter(description, parameter_names[i], parameter_types[i], parameter_descriptions[i], indent, align); - } - } - - if (!return_names.empty()){ - // add return value description - // description += "\n" + _align("Returns") + _align("--------"); - description += "\n" + _align("**Returns:**", indent, align) + "\n\n"; - for (unsigned i = 0; i < return_names.size(); ++i){ - _align_parameter(description, return_names[i], return_types[i], return_descriptions[i], indent, align); - } - } - } - - // return the description - return description.c_str(); -#endif // BOB_SHORT_DOCSTRINGS -} - -inline void bob::extension::FunctionDoc::print_usage() const -{ -#ifdef BOB_SHORT_DOCSTRINGS - return function_description.c_str(); -#else - // in case of member functions, the alignment has to be decreased further since class member function are automatically indented by 4 further spaces. - std::cerr << "\nUsage (for details, see help):\n"; - switch(prototype_variables.size()){ - case 0: - std::cerr << _align("Error: The usage of this function is unknown", 0, (unsigned)-1) << "\n"; - break; - case 1: - // only one way to call; use the default way - std::cerr << _align(_usage(function_name, prototype_variables[0], prototype_returns[0]), 0, unsigned(-1)) << "\n"; - break; - default: - // several ways to call; list them - for (unsigned n = 0; n < prototype_variables.size(); ++n) - std::cerr << _align(_usage(function_name, prototype_variables[n], prototype_returns[n]), 0, unsigned(-1)) << "\n"; - } - std::cerr << std::endl; -#endif // BOB_SHORT_DOCSTRINGS -} - - - -///////////////////////////////////////////////////////////// -/// ClassDoc - -inline bob::extension::ClassDoc::ClassDoc( - const char* const class_name, - const char* const short_description, - const char* const long_description -) : class_name(class_name), class_description(short_description) -{ -#ifndef BOB_SHORT_DOCSTRINGS - if (long_description){ - class_description += "\n\n"; - class_description += long_description; - } -#endif // ! BOB_SHORT_DOCSTRINGS -} - -inline bob::extension::ClassDoc& bob::extension::ClassDoc::add_constructor( - const bob::extension::FunctionDoc& constructor_documentation -) -{ -#ifndef BOB_SHORT_DOCSTRINGS - if (!constructor.empty()){ - throw std::runtime_error("The class documentation can have only a single constructor documentation"); - } - constructor.push_back(constructor_documentation); - // since we indent the constructor documentation ourselves, we don't need to consider it to be a member function. - constructor.back().is_member = false; - constructor.back().function_name = class_name; -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - -inline bob::extension::ClassDoc& bob::extension::ClassDoc::highlight( - const bob::extension::FunctionDoc& function_documentation -) -{ -#ifndef BOB_SHORT_DOCSTRINGS - highlighted_functions.push_back(function_documentation); -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - -inline bob::extension::ClassDoc& bob::extension::ClassDoc::highlight( - const bob::extension::VariableDoc& variable_documentation -) -{ -#ifndef BOB_SHORT_DOCSTRINGS - highlighted_variables.push_back(variable_documentation); -#endif // BOB_SHORT_DOCSTRINGS - return *this; -} - - -inline char* bob::extension::ClassDoc::doc( - const unsigned alignment -) const -{ -#ifdef BOB_SHORT_DOCSTRINGS - return const_cast<char*>(class_description.c_str()); -#else - if (description.empty()){ - description = _align(class_description, 0, alignment) + "\n"; - if (!constructor.empty()){ - description += "\n" + _align("**Constructor Documentation:**", 0, alignment) + "\n\n"; - description += constructor.front().doc(alignment, 4) + std::string("\n"); - } - description += "\n" + _align("**Class Members:**", 0, alignment) + "\n\n"; - if (!highlighted_functions.empty()){ - // description += "\n" + _align("Methods") + _align("-------"); - description += "\n" + _align("**Highlighted Methods:**", 2, alignment) + "\n\n"; - for (auto hit = highlighted_functions.begin(); hit != highlighted_functions.end(); ++hit){ - description += _align("* :func:`" + hit->function_name + "`", 2, alignment) + _align(_split(hit->function_description, '\n')[0], 4, alignment) + "\n"; - } - } - if (!highlighted_variables.empty()){ - // description += "\n" + _align("Attributes") + _align("----------"); - description += "\n" + _align("**Highlighted Attributes:**", 2, alignment) + "\n\n"; - for (auto hit = highlighted_variables.begin(); hit != highlighted_variables.end(); ++hit){ - description += _align("* :obj:`" + hit->variable_name + "`", 2, alignment) + _align(_split(hit->variable_description, '\n')[0], 4, alignment) + "\n"; - } - } - } - return const_cast<char*>(description.c_str()); -#endif // BOB_SHORT_DOCSTRINGS -} - -///////////////////////////////////////////////////////////// -/// VariableDoc - -inline bob::extension::VariableDoc::VariableDoc( - const char* const variable_name, - const char* const variable_type, - const char* const short_description, - const char* const long_description -) : variable_name(variable_name), variable_type(variable_type), variable_description(short_description) -{ -#ifndef BOB_SHORT_DOCSTRINGS - if (long_description){ - variable_description += "\n\n"; - variable_description += long_description; - } -#endif // ! BOB_SHORT_DOCSTRINGS -} - -inline char* bob::extension::VariableDoc::doc( - const unsigned alignment -) const -{ -#ifdef BOB_SHORT_DOCSTRINGS - return const_cast<char*>(variable_description.c_str()); -#else - if (description.empty()){ - if (variable_type.find(':') != std::string::npos && variable_type.find('`') != std::string::npos) - // we expect that this is a :py:class: directive, which is simply written (otherwise the *...* - description = _align(variable_type + " <-- " + variable_description, 0, alignment); - else - description = _align("*" + variable_type + "* <-- " + variable_description, 0, alignment); - } - return const_cast<char*>(description.c_str()); -#endif // BOB_SHORT_DOCSTRINGS -} - -#endif // BOB_EXTENSION_DOCUMENTATION_H_INCLUDED diff --git a/bob/extension/log.py b/bob/extension/log.py index 3c12d2f4965b989abb6f905a83e10137e7cd0edf..9b71b80dd4ae6bbd7d7744abb16e6aa71e9df1cb 100644 --- a/bob/extension/log.py +++ b/bob/extension/log.py @@ -5,11 +5,11 @@ """Sets-up logging, centrally for Bob. """ -import sys import logging +import sys # get the default root logger of Bob -_logger = logging.getLogger('bob') +_logger = logging.getLogger("bob") # by default, warning and error messages should be written to sys.stderr _warn_err = logging.StreamHandler(sys.stderr) @@ -20,8 +20,8 @@ _logger.addHandler(_warn_err) class _InfoFilter: - def filter(self, record): - return record.levelno <= logging.INFO + def filter(self, record): + return record.levelno <= logging.INFO _debug_info = logging.StreamHandler(sys.stdout) @@ -31,82 +31,84 @@ _logger.addHandler(_debug_info) # helper functions to instantiate and set-up logging -def setup(logger_name, - format="%(name)s@%(asctime)s -- %(levelname)s: %(message)s"): - """This function returns a logger object that is set up to perform logging - using Bob loggers. - - Parameters - ---------- - logger_name : str - The name of the module to generate logs for - format : :obj:`str`, optional - The format of the logs, see :py:class:`logging.LogRecord` for more - details. By default, the log contains the logger name, the log time, the - log level and the massage. - - Returns - ------- - logger : :py:class:`logging.Logger` - The logger configured for logging. The same logger can be retrieved using - the :py:func:`logging.getLogger` function. - """ - # generate new logger object - logger = logging.getLogger(logger_name) - - # add log the handlers if not yet done - if not logger_name.startswith("bob") and not logger.handlers: - logger.addHandler(_warn_err) - logger.addHandler(_debug_info) - - # this formats the logger to print the desired information - formatter = logging.Formatter(format) - # we have to set the formatter to all handlers registered in the current - # logger - for handler in logger.handlers: - handler.setFormatter(formatter) - - # set the same formatter for bob loggers - for handler in _logger.handlers: - handler.setFormatter(formatter) - - return logger +def setup( + logger_name, format="%(name)s@%(asctime)s -- %(levelname)s: %(message)s" +): + """This function returns a logger object that is set up to perform logging + using Bob loggers. + + Parameters + ---------- + logger_name : str + The name of the module to generate logs for + format : :obj:`str`, optional + The format of the logs, see :py:class:`logging.LogRecord` for more + details. By default, the log contains the logger name, the log time, the + log level and the massage. + + Returns + ------- + logger : :py:class:`logging.Logger` + The logger configured for logging. The same logger can be retrieved using + the :py:func:`logging.getLogger` function. + """ + # generate new logger object + logger = logging.getLogger(logger_name) + + # add log the handlers if not yet done + if not logger_name.startswith("bob") and not logger.handlers: + logger.addHandler(_warn_err) + logger.addHandler(_debug_info) + + # this formats the logger to print the desired information + formatter = logging.Formatter(format) + # we have to set the formatter to all handlers registered in the current + # logger + for handler in logger.handlers: + handler.setFormatter(formatter) + + # set the same formatter for bob loggers + for handler in _logger.handlers: + handler.setFormatter(formatter) + + return logger def set_verbosity_level(logger, level): - """Sets the log level for the given logger. - - Parameters - ---------- - logger : :py:class:`logging.Logger` or str - The logger to generate logs for, or the name of the module to generate - logs for. - level : int - Possible log levels are: 0: Error; 1: Warning; 2: Info; 3: Debug. - - Raises - ------ - ValueError - If the level is not in range(0, 4). - """ - if level not in range(0, 4): - raise ValueError( - "The verbosity level %d does not exist. Please reduce the number of " - "'--verbose' parameters in your command line" % level) - # set up the verbosity level of the logging system - log_level = { - 0: logging.ERROR, - 1: logging.WARNING, - 2: logging.INFO, - 3: logging.DEBUG - }[level] - - # set this log level to the logger with the specified name - if isinstance(logger, str): - logger = logging.getLogger(logger) - logger.setLevel(log_level) - # set the same log level for the bob logger - _logger.setLevel(log_level) - - -__all__ = [_ for _ in dir() if not _.startswith('_')] + """Sets the log level for the given logger. + + Parameters + ---------- + logger : :py:class:`logging.Logger` or str + The logger to generate logs for, or the name of the module to generate + logs for. + level : int + Possible log levels are: 0: Error; 1: Warning; 2: Info; 3: Debug. + + Raises + ------ + ValueError + If the level is not in range(0, 4). + """ + if level not in range(0, 4): + raise ValueError( + "The verbosity level %d does not exist. Please reduce the number of " + "'--verbose' parameters in your command line" % level + ) + # set up the verbosity level of the logging system + log_level = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, + }[level] + + # set this log level to the logger with the specified name + if isinstance(logger, str): + logger = logging.getLogger(logger) + logger.setLevel(log_level) + # set the same log level for the bob logger + _logger.setLevel(log_level) + + +__all__ = [_ for _ in dir() if not _.startswith("_")] diff --git a/bob/extension/pkgconfig.py b/bob/extension/pkgconfig.py deleted file mode 100644 index bc0d1f7a76323894eb0fdd7c67af11bd2f46de81..0000000000000000000000000000000000000000 --- a/bob/extension/pkgconfig.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python -# vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Wed 16 Oct 10:08:42 2013 CEST - -import os -import sys -import subprocess -import logging -from .utils import uniq, uniq_paths, find_executable, construct_search_paths - - -def call_pkgconfig(cmd, paths=None): - """Calls pkg-config with a constructed PKG_CONFIG_PATH environment variable. - - The PKG_CONFIG_PATH variable is constructed using - :any:`construct_search_paths`. - - Parameters - ---------- - cmd : [str] - A list of commands to be given to pkg-config. - paths : [str] - A list of paths to be added to PKG_CONFIG_PATH. - - Returns - ------- - returncode : int - The exit status of pkg-config. - stdout : str - The stdout output of pkg-config. - stderr : str - The stderr output of pkg-config. - - Raises - ------ - OSError - If the `pkg-config' executable is not found. - """ - - # locates the pkg-config program - pkg_config = find_executable('pkg-config') - if not pkg_config: - raise OSError("Cannot find `pkg-config' - did you install it?") - - pkg_path = construct_search_paths( - prefixes=paths, suffix=os.sep + 'lib' + os.sep + 'pkgconfig') - - env = os.environ.copy() - var = os.pathsep.join(pkg_path) - old = env.get('PKG_CONFIG_PATH', False) - env['PKG_CONFIG_PATH'] = os.pathsep.join([var, old]) if old else var - - # calls the program - cmd = pkg_config[:1] + [str(k) for k in cmd] - subproc = subprocess.Popen( - cmd, - env=env, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE - ) - - logging.debug("Running `%s'" % (" ".join(cmd),)) - stdout, stderr = subproc.communicate() - - # handles py3k string conversion, if necessary - if isinstance(stdout, bytes) and not isinstance(stdout, str): - stdout = stdout.decode('utf8') - - if isinstance(stderr, bytes) and not isinstance(stderr, str): - stderr = stderr.decode('utf8') - - # always print the stdout - logger = logging.getLogger('pkgconfig') - for k in stdout.split('\n'): - if k: logger.debug(k) - - return subproc.returncode, stdout, stderr - -def version(): - """Determines the version of pkg-config which is installed""" - - status, stdout, stderr = call_pkgconfig(['--version']) - - if status != 0: - raise RuntimeError("pkg-config is not installed - please do it") - - return stdout.strip() - - -class pkgconfig: - """A class for capturing configuration information from pkg-config - - Example usage: - - .. doctest:: - :options: +NORMALIZE_WHITESPACE +ELLIPSIS - - >>> from bob.extension import pkgconfig - >>> blitz = pkgconfig('blitz') - >>> blitz.include_directories() - [...] - >>> blitz.library_directories() - [...] - - If the package does not exist, a RuntimeError is raised. All calls to any - methods of a ``pkgconfig`` object are translated into a subprocess call that - queries for that specific information. If ``pkg-config`` fails, a - RuntimeError is raised. - """ - - def __init__(self, name, paths=None): - """Constructor - - Parameters: - - name - The name of the package of interest, as you would pass on the command - line - - paths - Search paths to be added to the environment's PKG_CONFIG_PATH to search - for packages. - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config <name> - - """ - - status, stdout, stderr = call_pkgconfig(['--modversion', name], paths) - - if status != 0: - raise RuntimeError("pkg-config package `%s' was not found" % name) - - self.name = name - self.version = stdout.strip() - self.paths = paths - - def __xcall__(self, cmd): - """Calls call_pkgconfig() with self.name and self.paths""" - - return call_pkgconfig(cmd + [self.name], self.paths) - - def __cmp__(self, other): - """Compares this package with a version number - - We create a new ``distutils.version.LooseVersion`` object out of your input - argument and then, compare it to our own version, returning the result. - - Returns an integer smaller than zero if this package's version number is - smaller than the provided value. Returns zero in case of a match and - greater than zero in the other case. - """ - - from distutils.version import LooseVersion - return cmp(self.version, LooseVersion(other)) - - def __ge__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) >= LooseVersion(other) - - def __gt__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) > LooseVersion(other) - - def __le__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) <= LooseVersion(other) - - def __lt__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) < LooseVersion(other) - - def __eq__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) == LooseVersion(other) - - def __ne__(self, other): - from distutils.version import LooseVersion - return LooseVersion(self.version) != LooseVersion(other) - - def include_directories(self): - """Returns a pre-processed list containing include directories. - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --cflags-only-I <name> - - """ - - status, stdout, stderr = self.__xcall__(['--cflags-only-I']) - - if status != 0: - raise RuntimeError("error querying --cflags-only-I for package `%s': %s" % (self.name, stderr)) - - retval = [] - for token in stdout.split(): - retval.append(token[2:]) - - return uniq(retval) - - def cflags_other(self): - """Returns a pre-processed dictionary containing compilation options. - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --cflags-only-other <name> - - The returned dictionary contains two entries ``extra_compile_args`` and - ``define_macros``. The ``define_macros`` entries are ready for deployment - in the ``setup()`` function of your package. - """ - - status, stdout, stderr = self.__xcall__(['--cflags-only-other']) - - if status != 0: - raise RuntimeError("error querying --cflags-only-other for package `%s': %s" % (self.name, stderr)) - - flag_map = { - '-D': 'define_macros', - } - - kw = {} - - for token in stdout.split(): - if token[:2] in flag_map: - kw.setdefault(flag_map.get(token[:2]), []).append(token[2:]) - - else: # throw others to extra_link_args - kw.setdefault('extra_compile_args', []).append(token) - - # make it uniq - for k, v in kw.items(): kw[k] = uniq(v) - - # for macros, separate them so they can be plugged on C/C++ extensions - if 'define_macros' in kw: - for k, string in enumerate(kw['define_macros']): - if string.find('=') != -1: - kw['define_macros'][k] = string.split('=', 2) - else: - kw['define_macros'][k] = (string, None) - - return kw - - def libraries(self): - """Returns a pre-processed list containing libraries to link against - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-l <name> - - """ - - status, stdout, stderr = self.__xcall__(['--libs-only-l']) - - if status != 0: - raise RuntimeError("error querying --libs-only-l for package `%s': %s" % (self.name, stderr)) - - retval = [] - for token in stdout.split(): - retval.append(token[2:]) - - return uniq(retval) - - def other_libraries(self): - """Returns a pre-processed list containing libraries to link against - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-other <name> - - """ - - status, stdout, stderr = self.__xcall__(['--libs-only-other']) - - if status != 0: - raise RuntimeError("error querying --libs-only-other for package `%s': %s" % (self.name, stderr)) - - return uniq(stdout.split()) - - def library_directories(self): - """Returns a pre-processed list containing library directories. - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-L <name> - - """ - - status, stdout, stderr = self.__xcall__(['--libs-only-L']) - - if status != 0: - raise RuntimeError("error querying --libs-only-L for package `%s': %s" % (self.name, stderr)) - - retval = [] - for token in stdout.split(): - retval.append(token[2:]) - - return uniq(retval) - - def extra_link_args(self): - """Returns a pre-processed list containing extra link arguments. - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-other <name> - - """ - - status, stdout, stderr = self.__xcall__(['--libs-only-other']) - - if status != 0: - raise RuntimeError("error querying --libs-only-other for package `%s': %s" % (self.name, stderr)) - - return stdout.strip().split() - - def variable_names(self): - """Returns a list with all variable names know to this package - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --print-variables <name> - - """ - - status, stdout, stderr = self.__xcall__(['--print-variables']) - - if status != 0: - raise RuntimeError("error querying --print-variables for package `%s': %s" % (self.name, stderr)) - - return stdout.strip().split() - - def variable(self, name): - """Returns a variable with a specific name (if it exists) - - Equivalent command line version: - - .. code-block:: sh - - $ PKG_CONFIG_PATH=<paths> pkg-config --variable=<variable-name> <name> - - .. warning:: - - If a variable does not exist in a package, pkg-config does not signal an - error. Instead, it returns an empty string. So, do we. - """ - - status, stdout, stderr = self.__xcall__(['--variable=%s' % name]) - - if status != 0: - raise RuntimeError("error querying --variable=%s for package `%s': %s" % (name, self.name, stderr)) - - return stdout.strip() - - def package_macros(self): - """Returns package availability and version number macros - - This method returns a python list with 1 macro indicating package - availability, using standard GNU compatible names. For - example, if the package is named ``blitz``, this - command would return: - - .. doctest:: - :options: +NORMALIZE_WHITESPACE +ELLIPSIS - - >>> from bob.extension import pkgconfig - >>> blitz = pkgconfig('blitz') - >>> blitz.package_macros() - [('HAVE_BLITZ', '1')] - - """ - from re import sub - NAME = sub(r'[\.\-\s]', '_', self.name.upper()) - return [('HAVE_' + NAME, '1')] - -__all__ = ['pkgconfig'] diff --git a/bob/extension/rc_config.py b/bob/extension/rc_config.py index c84252cf11a5db41ecfce717047af165b842f4a5..b318f21abd41312c8884563262a5f621d31356ad 100644 --- a/bob/extension/rc_config.py +++ b/bob/extension/rc_config.py @@ -1,98 +1,99 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : -'''Implements a global configuration system for bob using json.''' +"""Implements a global configuration system for bob using json.""" -from collections import defaultdict import json import logging import os +from collections import defaultdict + logger = logging.getLogger(__name__) -ENVNAME = 'BOBRC' +ENVNAME = "BOBRC" """Name of environment variable to look for an alternative for the RC file""" -RCFILENAME = '~' + os.sep + '.bobrc' +RCFILENAME = "~" + os.sep + ".bobrc" """Default name to be used for the RC file to load""" def _get_rc_path(): - """Returns the path to the bob rc file. - This method will return the path to **exactly** one (global) resource - configuration file in this fixed order of preference: + """Returns the path to the bob rc file. + This method will return the path to **exactly** one (global) resource + configuration file in this fixed order of preference: - 1. A path pointed by the environment variable BOBRC - 2. A file named :py:attr:`RCFILENAME` on your HOME directory + 1. A path pointed by the environment variable BOBRC + 2. A file named :py:attr:`RCFILENAME` on your HOME directory - Returns - ------- - str - The path to the rc file. - """ - if 'BOBRC' in os.environ: - path = os.environ['BOBRC'] - else: - path = os.path.expanduser(RCFILENAME) + Returns + ------- + str + The path to the rc file. + """ + if "BOBRC" in os.environ: + path = os.environ["BOBRC"] + else: + path = os.path.expanduser(RCFILENAME) - return path + return path def _loadrc(): - '''Loads the default configuration file, or an override if provided + """Loads the default configuration file, or an override if provided - This method will load **exactly** one (global) resource configuration file as - returned by :py:func:`_get_rc_path`. + This method will load **exactly** one (global) resource configuration file as + returned by :py:func:`_get_rc_path`. - Returns: + Returns: - dict: A dictionary of key-values representing the resolved context, after - loading the provided modules and resolving all variables. + dict: A dictionary of key-values representing the resolved context, after + loading the provided modules and resolving all variables. - ''' + """ - def _default_none_dict(dct): - dct2 = defaultdict(lambda: None) - dct2.update(dct) - return dct2 + def _default_none_dict(dct): + dct2 = defaultdict(lambda: None) + dct2.update(dct) + return dct2 - path = _get_rc_path() - if not os.path.exists(path): - logger.debug("No RC file found") - return _default_none_dict({}) + path = _get_rc_path() + if not os.path.exists(path): + logger.debug("No RC file found") + return _default_none_dict({}) - logger.debug("Loading RC file `%s'...", path) + logger.debug("Loading RC file `%s'...", path) - with open(path, 'rt') as f: - return json.load(f, object_hook=_default_none_dict) + with open(path, "rt") as f: + return json.load(f, object_hook=_default_none_dict) def _rc_to_str(context): - """Converts the configurations into a pretty JSON formatted string. + """Converts the configurations into a pretty JSON formatted string. - Parameters - ---------- - context : dict - All the configurations to save into the rc file. + Parameters + ---------- + context : dict + All the configurations to save into the rc file. - Returns - ------- - str - The configurations in a JSON formatted string. - """ + Returns + ------- + str + The configurations in a JSON formatted string. + """ - return json.dumps(context, sort_keys=True, indent=4, separators=(',', ': ')) + return json.dumps(context, sort_keys=True, indent=4, separators=(",", ": ")) def _saverc(context): - """Saves the context into the global rc file. + """Saves the context into the global rc file. - Parameters - ---------- - context : dict - All the configurations to save into the rc file. - """ + Parameters + ---------- + context : dict + All the configurations to save into the rc file. + """ - path = _get_rc_path() - with open(path, 'wt') as f: - f.write(_rc_to_str(context)) + path = _get_rc_path() + with open(path, "wt") as f: + f.write(_rc_to_str(context)) diff --git a/bob/extension/scripts/__init__.py b/bob/extension/scripts/__init__.py index 58bb841d4f20aef1ff88f6ce03f83925d31fd991..e356eaff0b5e2ebe54b2fd7dd7db84fe8771633c 100644 --- a/bob/extension/scripts/__init__.py +++ b/bob/extension/scripts/__init__.py @@ -1,5 +1,5 @@ -from .main_cli import main as main_cli -from . import click_helper +from . import click_helper # noqa: F401 +from .main_cli import main as main_cli # noqa: F401 # gets sphinx autodoc done right - don't remove it -__all__ = [_ for _ in dir() if not _.startswith('_')] +__all__ = [_ for _ in dir() if not _.startswith("_")] diff --git a/bob/extension/scripts/click_helper.py b/bob/extension/scripts/click_helper.py index 9de2ec08e6785933b757b48ab455d01f2b525f5a..d840ceae51fdc7785aec39400716fb12b0b8b3d1 100644 --- a/bob/extension/scripts/click_helper.py +++ b/bob/extension/scripts/click_helper.py @@ -3,11 +3,10 @@ import time import traceback import click + from click.core import ParameterSource -from ..config import load -from ..config import mod_to_context -from ..config import resource_keys +from ..config import load, mod_to_context, resource_keys from ..log import set_verbosity_level logger = logging.getLogger(__name__) @@ -191,7 +190,9 @@ class ConfigCommand(click.Command): The name of entry point that will be used to load the config files. """ - def __init__(self, name, *args, help=None, entry_point_group=None, **kwargs): + def __init__( + self, name, *args, help=None, entry_point_group=None, **kwargs + ): self.entry_point_group = entry_point_group configs_argument_name = "CONFIG" # Augment help for the config file argument @@ -209,7 +210,9 @@ file.""".format( # Add the config argument to the command def configs_argument_callback(ctx, param, value): - config_context = load(value, entry_point_group=self.entry_point_group) + config_context = load( + value, entry_point_group=self.entry_point_group + ) config_context = mod_to_context(config_context) ctx.config_context = config_context logger.debug("Augmenting context with config context") @@ -263,7 +266,9 @@ file.""".format( if not isinstance(param, ResourceOption): continue - config_file.write("\n# %s = %s\n" % (param.name, str(param.default))) + config_file.write( + "\n# %s = %s\n" % (param.name, str(param.default)) + ) config_file.write("'''") if param.required: @@ -274,7 +279,8 @@ file.""".format( " [default: {}]".format(param.default), ) config_file.write( - "%s: %s (%s)%s" % (begin, param.name, ", ".join(param.opts), dflt) + "%s: %s (%s)%s" + % (begin, param.name, ", ".join(param.opts), dflt) ) if param.help is not None: @@ -292,7 +298,9 @@ file.""".format( config_file.write("'''\n") click.echo( - "Configuration file '{}' was written; exiting".format(config_file.name) + "Configuration file '{}' was written; exiting".format( + config_file.name + ) ) config_file.close() @@ -373,7 +381,9 @@ class ResourceOption(click.Option): self.entry_point_group = entry_point_group if entry_point_group is not None: - name, _, _ = self._parse_decls(param_decls, kwargs.get("expose_value")) + name, _, _ = self._parse_decls( + param_decls, kwargs.get("expose_value") + ) help = help or "" help += ( " Can be a ``{entry_point_group}`` entry point, a module name, or " @@ -399,7 +409,9 @@ class ResourceOption(click.Option): self.string_exceptions = string_exceptions or [] def consume_value(self, ctx, opts): - if (not hasattr(ctx, "config_context")) and self.entry_point_group is None: + if ( + not hasattr(ctx, "config_context") + ) and self.entry_point_group is None: raise TypeError( "The ResourceOption class is not meant to be used this way. " "Please see the docs of the class." @@ -442,7 +454,9 @@ class ResourceOption(click.Option): # if the value is a string and an entry_point_group is provided, load it if self.entry_point_group is not None: - while isinstance(value, str) and value not in self.string_exceptions: + while ( + isinstance(value, str) and value not in self.string_exceptions + ): value = load( [value], entry_point_group=self.entry_point_group, @@ -498,7 +512,10 @@ def log_parameters(logger_handle, ignore=tuple()): def assert_click_runner_result(result, exit_code=0, exception_type=None): """Helper for asserting click runner results""" - m = "Click command exited with code `{}' and exception:\n{}" "\nThe output was:\n{}" + m = ( + "Click command exited with code `{}' and exception:\n{}" + "\nThe output was:\n{}" + ) exception = ( "None" if result.exc_info is None diff --git a/bob/extension/scripts/config.py b/bob/extension/scripts/config.py index db7bf5656f22f46d05760cab38b0febe645e7d13..3776304fb152f5a618a11447631bd4ab0c29d015 100644 --- a/bob/extension/scripts/config.py +++ b/bob/extension/scripts/config.py @@ -1,11 +1,13 @@ """The manager for bob's main configuration. """ -from .. import rc -from ..rc_config import _saverc, _rc_to_str, _get_rc_path -from .click_helper import verbosity_option, AliasedGroup import logging + import click +from .. import rc +from ..rc_config import _get_rc_path, _rc_to_str, _saverc +from .click_helper import AliasedGroup, verbosity_option + # Use the normal logging module. Verbosity and format of logging will be set by # adding the verbosity_option form bob.extension.scripts.click_helper logger = logging.getLogger(__name__) @@ -18,8 +20,9 @@ def config(**kwargs): # Load the config file again. This may be needed since the environment # variable might change the config path during the tests. Otherwise, this # should not be important. - logger.debug('Reloading the global configuration file.') + logger.debug("Reloading the global configuration file.") from ..rc_config import _loadrc + rc.clear() rc.update(_loadrc()) @@ -36,7 +39,7 @@ def show(): @config.command() -@click.argument('key') +@click.argument("key") def get(key): """Prints a key. @@ -57,13 +60,14 @@ def get(key): if value is None: # Exit the command line with ClickException in case of errors. raise click.ClickException( - "The requested key `{}' does not exist".format(key)) + "The requested key `{}' does not exist".format(key) + ) click.echo(value) @config.command() -@click.argument('key') -@click.argument('value') +@click.argument("key") +@click.argument("value") def set(key, value): """Sets the value for a key. @@ -90,10 +94,25 @@ def set(key, value): logger.error("Could not configure the rc file", exc_info=True) raise click.ClickException("Failed to change the configuration.") + @config.command() -@click.argument('substr') -@click.option('-c', '--contain', is_flag=True, default=False, type=click.BOOL, show_default=True) -@click.option('-f', '--force', is_flag=True, default=False, type=click.BOOL, show_default=True) +@click.argument("substr") +@click.option( + "-c", + "--contain", + is_flag=True, + default=False, + type=click.BOOL, + show_default=True, +) +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + type=click.BOOL, + show_default=True, +) def unset(substr, contain=False, force=False): """Clear all variables starting (containing) with substring. @@ -106,7 +125,7 @@ def unset(substr, contain=False, force=False): --------- substring : str The starting substring of one or several key(s) - + \b Parameters ---------- @@ -125,15 +144,23 @@ def unset(substr, contain=False, force=False): if substr in key: to_delete.append(key) found = True - + if not found: if not contain: - logger.error("The key starting with '{}' was not found in the rc file".format(substr)) + logger.error( + "The key starting with '{}' was not found in the rc file".format( + substr + ) + ) else: - logger.error("The key containing '{}' was not found in the rc file".format(substr)) - + logger.error( + "The key containing '{}' was not found in the rc file".format( + substr + ) + ) + raise click.ClickException("Failed to change the configuration.") - + if force: for key in to_delete: del rc[key] @@ -146,4 +173,4 @@ def unset(substr, contain=False, force=False): for key in to_delete: del rc[key] - _saverc(rc) + _saverc(rc) diff --git a/bob/extension/scripts/main_cli.py b/bob/extension/scripts/main_cli.py index 46ab57f9b6196e8d8510f0dd6c1bc2e2019fb681..12444d32abda3bd818550889fb74623e2cbe08ac 100644 --- a/bob/extension/scripts/main_cli.py +++ b/bob/extension/scripts/main_cli.py @@ -1,17 +1,22 @@ """This is the main entry to bob's scripts. """ -import pkg_resources import click +import pkg_resources + from click_plugins import with_plugins -from .click_helper import AliasedGroup + from ..log import setup -logger = setup('bob') +from .click_helper import AliasedGroup + +logger = setup("bob") -@with_plugins(pkg_resources.iter_entry_points('bob.cli')) -@click.group(cls=AliasedGroup, - context_settings=dict(help_option_names=['-?', '-h', '--help'])) +@with_plugins(pkg_resources.iter_entry_points("bob.cli")) +@click.group( + cls=AliasedGroup, + context_settings=dict(help_option_names=["-?", "-h", "--help"]), +) def main(): - """The main command line interface for bob. Look below for available - commands.""" - pass + """The main command line interface for bob. Look below for available + commands.""" + pass diff --git a/bob/extension/test_boost.py b/bob/extension/test_boost.py deleted file mode 100644 index bf6909163c2ead2f01be57a82dc2f901593b9547..0000000000000000000000000000000000000000 --- a/bob/extension/test_boost.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Thu 20 Mar 2014 12:43:48 CET - -"""Tests for boost configuration -""" - -import os -import sys -import nose -from .boost import boost -from distutils.version import LooseVersion - -def test_boost_version(): - - b = boost('>= 1.30') - assert LooseVersion(b.version) >= '1.30' - -def test_boost_simple_modules(): - - b = boost() - directories, libname = b.libconfig(['system']) - nose.tools.eq_(len(directories), 1) - assert os.path.exists(directories[0]) - nose.tools.eq_(len(libname), 1) - -def test_boost_python_modules(): - - b = boost() - directories, libname = b.libconfig(['python']) - nose.tools.eq_(len(directories), 1) - assert os.path.exists(directories[0]) - nose.tools.eq_(len(libname), 1) - #assert libname[0].find('-py%d%d' % sys.version_info[:2]) >= 0 - -def test_boost_multiple_modules(): - - b = boost() - directories, libname = b.libconfig(['python', 'system']) - nose.tools.eq_(len(directories), 1) - assert os.path.exists(directories[0]) - assert libname - nose.tools.eq_(len(libname), 2) - #assert libname[0].find('-py%d%d' % sys.version_info[:2]) >= 0 - #assert libname[1].find('-py%d%d' % sys.version_info[:2]) < 0 - -def test_common_prefix(): - - b = boost() - directories, libname = b.libconfig(['python', 'system']) - nose.tools.eq_(len(directories), 1) - assert os.path.exists(directories[0]) - os.path.commonprefix([directories[0], b.include_directory]) - assert len(os.path.commonprefix([directories[0], b.include_directory])) > 1 diff --git a/bob/extension/test_click_helper.py b/bob/extension/test_click_helper.py index 3f09ee5c80fac7329e28da8dbfe6eaf6717e097d..e5f0c73ce67c85eb702cf6a0c2d7abccec4e429d 100644 --- a/bob/extension/test_click_helper.py +++ b/bob/extension/test_click_helper.py @@ -1,21 +1,26 @@ -import click import time + +import click import pkg_resources + from click.testing import CliRunner + from bob.extension.scripts.click_helper import ( - verbosity_option, - bool_option, - list_float_option, + AliasedGroup, ConfigCommand, ResourceOption, - AliasedGroup, assert_click_runner_result, + bool_option, + list_float_option, + verbosity_option, ) def test_verbosity_option(): - for VERBOSITY, OPTIONS in zip([0, 1, 2, 3], [[], ["-v"], ["-vv"], ["-vvv"]]): + for VERBOSITY, OPTIONS in zip( + [0, 1, 2, 3], [[], ["-v"], ["-vv"], ["-vvv"]] + ): @click.command() @verbosity_option() @@ -187,6 +192,9 @@ def _assert_config_dump(ref, ref_date): with open("TEST_CONF", "r") as f, open(ref, "r") as f2: text = f.read().replace("'''", '"""') ref_text = f2.read().replace(ref_date, today) + # remove the starting whitespace of each line so the tests are more relaxed + text = "\n".join(line.lstrip() for line in text.splitlines()) + ref_text = "\n".join(line.lstrip() for line in ref_text.splitlines()) assert text == ref_text, "\n".join( [text, "########################\n" * 2, ref_text] ) @@ -213,7 +221,9 @@ def test_config_dump(): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False) + result = runner.invoke( + cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False + ) ref = pkg_resources.resource_filename( "bob.extension", "data/test_dump_config.py" ) @@ -226,7 +236,9 @@ def test_config_dump2(): def cli(): pass - @cli.command(cls=ConfigCommand, entry_point_group="bob.extension.test_dump_config") + @cli.command( + cls=ConfigCommand, entry_point_group="bob.extension.test_dump_config" + ) @click.option( "--database", "-d", @@ -244,13 +256,21 @@ def test_config_dump2(): help="bli bli bli", ) @click.option( - "--output-dir", "-o", required=True, cls=ResourceOption, help="blo blo blo" + "--output-dir", + "-o", + required=True, + cls=ResourceOption, + help="blo blo blo", ) @click.option( "--force", "-f", is_flag=True, cls=ResourceOption, help="lalalalalala" ) @click.option( - "--array", type=click.INT, default=1, cls=ResourceOption, help="lililili" + "--array", + type=click.INT, + default=1, + cls=ResourceOption, + help="lililili", ) @click.option( "--database-directories-file", @@ -275,7 +295,9 @@ def test_config_dump2(): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False) + result = runner.invoke( + cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False + ) ref = pkg_resources.resource_filename( "bob.extension", "data/test_dump_config2.py" ) diff --git a/bob/extension/test_cmake.py b/bob/extension/test_cmake.py deleted file mode 100644 index b9b26123303fc6aaa2209203a666b9ee0036d484..0000000000000000000000000000000000000000 --- a/bob/extension/test_cmake.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python -# vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Thu 20 Mar 2014 12:43:48 CET - -"""Tests for file search utilities -""" - -import os -import sys -import shutil -import nose.tools -import platform - -import bob.extension -import pkg_resources - -import tempfile - -def _find(lines, start): - for i, line in enumerate(lines): - if line.find(start) == 0: - return i - assert False - -def test_cmake_list(): - # checks that the CMakeLists.txt file is generated properly - - generator = bob.extension.CMakeListsGenerator( - name = 'bob_cmake_test', - sources = ['cmake_test.cpp'], - target_directory = "test_target", - version = '3.2.1', - include_directories = ["/usr/include/test"], - system_include_directories = ["/usr/include/test_system"], - libraries = ['some_library'], - library_directories = ["/usr/include/test"], - macros = [("TEST", "MACRO")] - ) - - temp_dir = tempfile.mkdtemp(prefix="bob_extension_test_") - - generator.generate(temp_dir, temp_dir) - - # read created file - lines = [line.rstrip() for line in open(os.path.join(temp_dir, "CMakeLists.txt"))] - - # check that all elements are properly written in the file - assert lines[_find(lines, 'project')] == 'project(bob_cmake_test)' - assert lines[_find(lines, 'include')] == 'include_directories(/usr/include/test)' - assert lines[_find(lines, 'include_directories(SYSTEM')] == 'include_directories(SYSTEM /usr/include/test_system)' - assert lines[_find(lines, 'link')] == 'link_directories(/usr/include/test)' - assert lines[_find(lines, 'add')] == 'add_definitions(-DTEST=MACRO)' - - index = _find(lines, 'add_library') - assert lines[index+1].find('cmake_test.cpp') >= 0 - - index = _find(lines, 'set_target_properties') - assert lines[index+1].find('test_target') >= 0 - - assert lines[_find(lines, 'target_link_libraries')].find('some_library') >= 0 - - # finally, clean up the mess - shutil.rmtree(temp_dir) - - -def test_library(): - old_dir = os.getcwd() - temp_dir = tempfile.mkdtemp(prefix="bob_extension_test_") - target_dir = os.path.join(temp_dir, 'build', 'lib', 'target') - # copy test file to temp directory - shutil.copyfile(pkg_resources.resource_filename(__name__, 'test_documentation.cpp'), os.path.join(temp_dir, 'test_documentation.cpp')) - os.chdir(temp_dir) - # check that the library compiles and links properly - library = bob.extension.Library( - name = 'target.bob_cmake_test', - sources = ['test_documentation.cpp'], - include_dirs = [pkg_resources.resource_filename(__name__, 'include')], - version = '3.2.1' - ) - - # redirect output of functions to /dev/null to avoid spamming the console - devnull = open(os.devnull, 'w') - # compile - compile_dir = os.path.join(temp_dir, 'build', 'lib') - os.makedirs(compile_dir) - os.makedirs(target_dir) - library.compile(compile_dir,stdout=devnull) - - # check that the library was generated sucessfully - if platform.system() == 'Darwin': - lib_name = 'libbob_cmake_test.dylib' - else: - lib_name = 'libbob_cmake_test.so' - - assert os.path.exists(os.path.join(target_dir, lib_name)) - - os.chdir(old_dir) - - # TODO: compile a test executable to actually link the library - - # finally, clean up the mess - shutil.rmtree(temp_dir) diff --git a/bob/extension/test_config.py b/bob/extension/test_config.py index 1f54e4ae7e36c9d9c0f06094e385468a86140154..874be3e8a851e8b4787c6b5347ada95f58bc98f5 100644 --- a/bob/extension/test_config.py +++ b/bob/extension/test_config.py @@ -1,84 +1,96 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : -'''Tests for the python-based config functionality''' +"""Tests for the python-based config functionality""" -from .config import load, mod_to_context import os -import pkg_resources + import numpy -path = pkg_resources.resource_filename('bob.extension', 'data') +import pkg_resources + +from .config import load, mod_to_context + +path = pkg_resources.resource_filename("bob.extension", "data") def test_basic(): - c = load([os.path.join(path, 'basic_config.py')]) - assert hasattr(c, "a") and c.a == 1 - assert hasattr(c, "b") and c.b == 3 + c = load([os.path.join(path, "basic_config.py")]) + assert hasattr(c, "a") and c.a == 1 + assert hasattr(c, "b") and c.b == 3 - ctx = mod_to_context(c) - assert ctx == {'a': 1, 'b': 3} + ctx = mod_to_context(c) + assert ctx == {"a": 1, "b": 3} def test_basic_with_context(): - c = load([os.path.join(path, 'basic_config.py')], {'d': 35, 'a': 0}) - assert hasattr(c, "a") and c.a == 1 - assert hasattr(c, "b") and c.b == 3 - assert hasattr(c, "d") and c.d == 35 + c = load([os.path.join(path, "basic_config.py")], {"d": 35, "a": 0}) + assert hasattr(c, "a") and c.a == 1 + assert hasattr(c, "b") and c.b == 3 + assert hasattr(c, "d") and c.d == 35 def test_chain_loading(): - file1 = os.path.join(path, 'basic_config.py') - file2 = os.path.join(path, 'load_config.py') - c = load([file1, file2]) - assert hasattr(c, "a") and c.a == 1 - assert hasattr(c, "b") and c.b == 6 + file1 = os.path.join(path, "basic_config.py") + file2 = os.path.join(path, "load_config.py") + c = load([file1, file2]) + assert hasattr(c, "a") and c.a == 1 + assert hasattr(c, "b") and c.b == 6 def test_config_with_module(): - c = load([os.path.join(path, 'config_with_module.py')]) - assert hasattr(c, "return_zeros") and numpy.allclose( - c.return_zeros(), numpy.zeros(shape=(2,))) + c = load([os.path.join(path, "config_with_module.py")]) + assert hasattr(c, "return_zeros") and numpy.allclose( + c.return_zeros(), numpy.zeros(shape=(2,)) + ) def test_entry_point_configs(): - # test when all kinds of paths - c = load([ - os.path.join(path, 'basic_config.py'), - 'resource_config', - 'bob.extension.data.resource_config', - 'bob.extension.data.basic_config', - 'subpackage_config', - ], entry_point_group='bob.extension.test_config_load') - assert hasattr(c, "a") and c.a == 1 - assert hasattr(c, "b") and c.b == 3 - assert hasattr(c, "rc") + # test when all kinds of paths + c = load( + [ + os.path.join(path, "basic_config.py"), + "resource_config", + "bob.extension.data.resource_config", + "bob.extension.data.basic_config", + "subpackage_config", + ], + entry_point_group="bob.extension.test_config_load", + ) + assert hasattr(c, "a") and c.a == 1 + assert hasattr(c, "b") and c.b == 3 + assert hasattr(c, "rc") def test_load_resource(): - for p, ref in [ - (os.path.join(path, 'resource_config2.py'), 1), - (os.path.join(path, 'resource_config2.py:a'), 1), - (os.path.join(path, 'resource_config2.py:b'), 2), - ('resource1', 1), - ('resource2', 2), - ('bob.extension.data.resource_config2', 1), - ('bob.extension.data.resource_config2:a', 1), - ('bob.extension.data.resource_config2:b', 2), - ]: - c = load([p], entry_point_group='bob.extension.test_config_load', - attribute_name='a') - assert c == ref, c - - try: - load(['bob.extension.data.resource_config2:c'], - entry_point_group='bob.extension.test_config_load', - attribute_name='a') - assert False, 'The code above should have raised an ImportError' - except ImportError: - pass + for p, ref in [ + (os.path.join(path, "resource_config2.py"), 1), + (os.path.join(path, "resource_config2.py:a"), 1), + (os.path.join(path, "resource_config2.py:b"), 2), + ("resource1", 1), + ("resource2", 2), + ("bob.extension.data.resource_config2", 1), + ("bob.extension.data.resource_config2:a", 1), + ("bob.extension.data.resource_config2:b", 2), + ]: + c = load( + [p], + entry_point_group="bob.extension.test_config_load", + attribute_name="a", + ) + assert c == ref, c + + try: + load( + ["bob.extension.data.resource_config2:c"], + entry_point_group="bob.extension.test_config_load", + attribute_name="a", + ) + assert False, "The code above should have raised an ImportError" + except ImportError: + pass diff --git a/bob/extension/test_documentation.cpp b/bob/extension/test_documentation.cpp deleted file mode 100644 index c6ee024c1aafc31c2c8132593eda7e6319d3167e..0000000000000000000000000000000000000000 --- a/bob/extension/test_documentation.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include <bob.extension/documentation.h> - -int test_documenation(){ - - auto function_doc = bob::extension::FunctionDoc( - "TestClass", - "This is the constructor of the TestClass", - ".. todo:: Add more information here", - true - ) - .add_prototype("para1", "") - .add_parameter("para1", "int", "A parameter of type int"); - - auto class_doc = bob::extension::ClassDoc( - "TestClass", - "This is the documentation for a test class.", - "Just to check that it works" - ) - .add_constructor(function_doc); - - auto var_doc = bob::extension::VariableDoc( - "test_variable", - "float", - "A float variable", - "With more documentation" - ); - - // create documentation, just to see if it works - class_doc.name(); - class_doc.doc(72); - - var_doc.name(); - var_doc.doc(72); - - return 0; -} diff --git a/bob/extension/test_download.py b/bob/extension/test_download.py index 27f18f5037eb76f71b6d0d67b23aecb817315201..43ed52638bca86640dec38498e35d51f373d62e2 100644 --- a/bob/extension/test_download.py +++ b/bob/extension/test_download.py @@ -3,16 +3,23 @@ import shutil import tempfile import pkg_resources + from bob.extension import rc_context -from bob.extension.download import download_and_unzip -from bob.extension.download import find_element_in_tarball, search_file, _untar -from bob.extension.download import get_file, list_dir +from bob.extension.download import ( + _untar, + download_and_unzip, + find_element_in_tarball, + get_file, + list_dir, + search_file, +) def test_download_unzip(): def download(filename): download_and_unzip( - "http://www.idiap.ch/software/bob/databases/latest/mnist.tar.bz2", filename + "http://www.idiap.ch/software/bob/databases/latest/mnist.tar.bz2", + filename, ) uncompressed_filename = os.path.join(os.path.dirname(filename), "data") @@ -70,7 +77,8 @@ def test_find_element_in_tarball(): ) assert ( find_element_in_tarball( - filename, "example_csv_filelist/protocol_dev_eval/norm/train_world.csv" + filename, + "example_csv_filelist/protocol_dev_eval/norm/train_world.csv", ) is not None ) @@ -90,7 +98,10 @@ def test_search_file(): __name__, "data/example_csv_filelist.tar.gz" ) # Search in the tarball - assert search_file(filename, "protocol_dev_eval/norm/train_world.csv") is not None + assert ( + search_file(filename, "protocol_dev_eval/norm/train_world.csv") + is not None + ) assert search_file(filename, "protocol_dev_eval/norm/xuxa.csv") is None # Search in a file structure @@ -100,7 +111,10 @@ def test_search_file(): _untar(filename, final_path, ".gz") - assert search_file(final_path, "protocol_dev_eval/norm/train_world.csv") is not None + assert ( + search_file(final_path, "protocol_dev_eval/norm/train_world.csv") + is not None + ) assert search_file(final_path, "protocol_dev_eval/norm/xuxa.csv") is None shutil.rmtree(final_path) @@ -115,7 +129,10 @@ def test_list_dir(): for root_folder in (folder, tar1, tar2): fldrs = list_dir(root_folder) - assert fldrs == ["README.rst", "database1", "database2"], (fldrs, root_folder) + assert fldrs == ["README.rst", "database1", "database2"], ( + fldrs, + root_folder, + ) fldrs = list_dir(root_folder, files=False) assert fldrs == ["database1", "database2"], (fldrs, root_folder) fldrs = list_dir(root_folder, folders=False) @@ -124,7 +141,10 @@ def test_list_dir(): assert fldrs == [], (fldrs, root_folder) fldrs = list_dir(root_folder, "database1") - assert fldrs == ["README1.rst", "protocol1", "protocol2"], (fldrs, root_folder) + assert fldrs == ["README1.rst", "protocol1", "protocol2"], ( + fldrs, + root_folder, + ) fldrs = list_dir(root_folder, "database1", files=False) assert fldrs == ["protocol1", "protocol2"], (fldrs, root_folder) fldrs = list_dir(root_folder, "database1", folders=False) diff --git a/bob/extension/test_pkgconfig.py b/bob/extension/test_pkgconfig.py deleted file mode 100644 index abf4082564c459f237a4baa7183754f264ce6e8b..0000000000000000000000000000000000000000 --- a/bob/extension/test_pkgconfig.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python -# vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Wed 16 Oct 12:16:49 2013 - -"""Tests for pkgconfig -""" - -import nose -from .pkgconfig import pkgconfig, version - -test_package = 'blitz' -pkg_config_version = '0.0' - -def test_pkgconfig_version(): - - assert version() - global pkg_config_version - pkg_config_version = version() - -def test_detect_ok(): - pkg = pkgconfig(test_package) - nose.tools.eq_(pkg.name, test_package) - assert pkg.version - #print pkg.name, pkg.version - -@nose.tools.raises(RuntimeError) -def test_detect_not_ok(): - pkg = pkgconfig('foobarfoo') - -def test_include_directories(): - pkg = pkgconfig(test_package) - obj = pkg.include_directories() - assert isinstance(obj, list) - #assert obj - #for k in obj: - # assert k.find('-I') != 0 - #print obj - -def test_cflags_other(): - pkg = pkgconfig('freetype2') - obj = pkg.cflags_other() - #print obj - - #assert obj['extra_compile_args'] - #assert isinstance(obj['extra_compile_args'], list) - #assert isinstance(obj['extra_compile_args'][0], tuple) - - #assert obj['define_macros'] - #assert isinstance(obj['define_macros'], list) - #assert isinstance(obj['define_macros'][0], tuple) - - assert isinstance(obj, dict) - -def test_libraries(): - pkg = pkgconfig(test_package) - obj = pkg.libraries() - assert isinstance(obj, list) - assert obj - for k in obj: - assert k.find('-l') != 0 - #print obj - -def test_library_directories(): - pkg = pkgconfig(test_package) - obj = pkg.library_directories() - assert isinstance(obj, list) - #assert obj - #for k in obj: - # assert k.find('-L') != 0 - #print obj - -def test_extra_link_args(): - pkg = pkgconfig(test_package) - obj = pkg.extra_link_args() - assert isinstance(obj, list) - #print obj - -def skip_on_pkgconfig_lt_024(func): - from nose.plugins.skip import SkipTest - from distutils.version import LooseVersion - pkg_config_has_variables = LooseVersion(version()) >= '0.24' - def _(): - if not pkg_config_has_variables: raise SkipTest('pkg-config version (%s) does not support variable listing' % pkg_config_version) - _.__name__ = func.__name__ - return _ - -@skip_on_pkgconfig_lt_024 -def test_variable_names(): - pkg = pkgconfig(test_package) - obj = pkg.variable_names() - assert isinstance(obj, list) - assert obj - #print obj - -@skip_on_pkgconfig_lt_024 -def test_variable(): - pkg = pkgconfig(test_package) - names = pkg.variable_names() - assert isinstance(names, list) - assert names - v = pkg.variable(names[0]) - assert v - #print v - -def test_macros(): - pkg = pkgconfig(test_package) - macros = pkg.package_macros() - assert isinstance(macros, list) - assert len(macros) == 1 - assert len(macros[0]) == 2 - assert macros[0][0].find('HAVE_') == 0 - assert macros[0][1] == '1' diff --git a/bob/extension/test_rc.py b/bob/extension/test_rc.py index 9fd76402e2fabdceb60ab10acadec56eeb6272c8..b20f342ba11da437f6fe2ec8e0ea66340c35ecbc 100644 --- a/bob/extension/test_rc.py +++ b/bob/extension/test_rc.py @@ -1,90 +1,97 @@ -'''Tests for the global bob's configuration functionality''' +"""Tests for the global bob's configuration functionality""" -from .rc_config import _loadrc, ENVNAME -from .scripts import main_cli -from .scripts.click_helper import assert_click_runner_result -from click.testing import CliRunner import os + import pkg_resources -path = pkg_resources.resource_filename('bob.extension', 'data') + +from click.testing import CliRunner + +from .rc_config import ENVNAME, _loadrc +from .scripts import main_cli +from .scripts.click_helper import assert_click_runner_result + +path = pkg_resources.resource_filename("bob.extension", "data") def test_rc_env(): - os.environ[ENVNAME] = os.path.join(path, 'defaults-config') - c = _loadrc() # should load from environment variable - REFERENCE = { - "bob.db.atnt.directory": "/home/bob/databases/atnt", - "bob.db.mobio.directory": "/home/bob/databases/mobio" - } + os.environ[ENVNAME] = os.path.join(path, "defaults-config") + c = _loadrc() # should load from environment variable + REFERENCE = { + "bob.db.atnt.directory": "/home/bob/databases/atnt", + "bob.db.mobio.directory": "/home/bob/databases/mobio", + } - assert c == REFERENCE - assert c['random'] is None + assert c == REFERENCE + assert c["random"] is None def test_bob_config(): - defaults_config = os.path.join(path, 'defaults-config') - runner = CliRunner(env={ENVNAME: defaults_config}) - - # test config show - result = runner.invoke(main_cli, ['config', 'show']) - assert_click_runner_result(result, 0) - assert 'defaults-config' in result.output, result.output - assert open(defaults_config).read() in result.output, result.output - - # test config get (existing key) - result = runner.invoke(main_cli, - ['config', 'get', 'bob.db.atnt.directory']) - assert_click_runner_result(result, 0) - assert result.output == '/home/bob/databases/atnt\n', result.output - - # test config get (non-existing key) - result = runner.invoke(main_cli, ['config', 'get', 'bob.db.atnt']) - assert_click_runner_result(result, 1) - - # test config set - runner = CliRunner() - with runner.isolated_filesystem(): - bobrcfile = 'bobrc' - result = runner.invoke( - main_cli, [ - 'config', 'set', 'bob.db.atnt.directory', - '/home/bob/databases/orl_faces' - ], - env={ - ENVNAME: bobrcfile - }) + defaults_config = os.path.join(path, "defaults-config") + runner = CliRunner(env={ENVNAME: defaults_config}) + + # test config show + result = runner.invoke(main_cli, ["config", "show"]) assert_click_runner_result(result, 0) + assert "defaults-config" in result.output, result.output + assert open(defaults_config).read() in result.output, result.output - # read the config back to make sure it is ok. - result = runner.invoke( - main_cli, ['config', 'show'], env={ - ENVNAME: bobrcfile - }) + # test config get (existing key) + result = runner.invoke(main_cli, ["config", "get", "bob.db.atnt.directory"]) assert_click_runner_result(result, 0) - expected_output = '''Displaying `bobrc': + assert result.output == "/home/bob/databases/atnt\n", result.output + + # test config get (non-existing key) + result = runner.invoke(main_cli, ["config", "get", "bob.db.atnt"]) + assert_click_runner_result(result, 1) + + # test config set + runner = CliRunner() + with runner.isolated_filesystem(): + bobrcfile = "bobrc" + result = runner.invoke( + main_cli, + [ + "config", + "set", + "bob.db.atnt.directory", + "/home/bob/databases/orl_faces", + ], + env={ENVNAME: bobrcfile}, + ) + assert_click_runner_result(result, 0) + + # read the config back to make sure it is ok. + result = runner.invoke( + main_cli, ["config", "show"], env={ENVNAME: bobrcfile} + ) + assert_click_runner_result(result, 0) + expected_output = """Displaying `bobrc': { "bob.db.atnt.directory": "/home/bob/databases/orl_faces" } -''' - assert expected_output == result.output, result.output - - # test config unset (with starting substring) - result = runner.invoke(main_cli, ['config', 'unset', 'bob.db.atnt']) - result = runner.invoke(main_cli, ['config', 'get', 'bob.db.atnt']) - assert_click_runner_result(result, 1) +""" + assert expected_output == result.output, result.output - # test config unset (with substring contained) - # reset the key / value pair - result = runner.invoke( - main_cli, [ - 'config', 'set', 'bob.db.atnt.directory', - '/home/bob/databases/orl_faces' - ], - env={ - ENVNAME: bobrcfile - }) - result = runner.invoke(main_cli, ['config', 'unset', '--contain', 'atnt']) - result = runner.invoke(main_cli, ['config', 'get', 'bob.db.atnt']) - assert_click_runner_result(result, 1) + # test config unset (with starting substring) + result = runner.invoke(main_cli, ["config", "unset", "bob.db.atnt"]) + result = runner.invoke(main_cli, ["config", "get", "bob.db.atnt"]) + assert_click_runner_result(result, 1) + # test config unset (with substring contained) + # reset the key / value pair + result = runner.invoke( + main_cli, + [ + "config", + "set", + "bob.db.atnt.directory", + "/home/bob/databases/orl_faces", + ], + env={ENVNAME: bobrcfile}, + ) + result = runner.invoke( + main_cli, ["config", "unset", "--contain", "atnt"] + ) + result = runner.invoke(main_cli, ["config", "get", "bob.db.atnt"]) + assert_click_runner_result(result, 1) diff --git a/bob/extension/test_utils.py b/bob/extension/test_utils.py index 77af867df9eeb90b645a0b78735797e27e5499b5..ae6bca1ab61750ddcb8d78c7ea0f8c52f397a121 100644 --- a/bob/extension/test_utils.py +++ b/bob/extension/test_utils.py @@ -8,97 +8,17 @@ import os import sys -import nose.tools -import pkg_resources -from nose.plugins.skip import SkipTest -from .utils import uniq, egrep, find_file, find_header, find_library, \ - load_requirements, find_packages, link_documentation - -def test_uniq(): - - a = [1, 2, 3, 7, 3, 2] - - nose.tools.eq_(uniq(a), [1, 2, 3, 7]) - -def test_find_file(): - - f = find_file('array.h', subpaths=[os.path.join('include', 'blitz')]) - - assert f - - nose.tools.eq_(os.path.basename(f[0]), 'array.h') - -def test_find_header(): - - f1 = find_file('array.h', subpaths=[os.path.join('include', 'blitz')]) - - assert f1 - - nose.tools.eq_(os.path.basename(f1[0]), 'array.h') - - f2 = find_header(os.path.join('blitz', 'array.h')) - - nose.tools.eq_(os.path.basename(f2[0]), 'array.h') - - assert f2 - - nose.tools.eq_(f1, f2) - -def test_find_library(): - - f = find_library('blitz') - - assert f - assert len(f) >= 1 - - for k in f: - assert k.find('blitz') >= 0 - -def test_egrep(): - - f = find_header('version.hpp', subpaths=['boost', 'boost?*']) - - assert f - - matches = egrep(f[0], r"^#\s*define\s+BOOST_VERSION\s+(\d+)\s*$") - - nose.tools.eq_(len(matches), 1) - -def test_find_versioned_library(): - - f = find_header('version.hpp', subpaths=['boost', 'boost?*']) - - assert f - - matches = egrep(f[0], r"^#\s*define\s+BOOST_VERSION\s+(\d+)\s*$") - - nose.tools.eq_(len(matches), 1) - - version_int = int(matches[0].group(1)) - version_tuple = ( - version_int // 100000, - (version_int // 100) % 1000, - version_int % 100, - ) - version = '.'.join([str(k) for k in version_tuple]) - - lib = find_library('boost_system', version=version) - lib += find_library('boost_system-mt', version=version) +import pkg_resources - assert len(lib) >= 1 +from .utils import find_packages, link_documentation, load_requirements - for k in lib: - assert k.find('boost_system') >= 0 def test_requirement_readout(): - if sys.version_info[0] == 3: from io import StringIO as stringio - else: - from cStringIO import StringIO as stringio - f = """ # this is my requirements file + f = """ # this is my requirements file package-a >= 0.42 package-b package-c @@ -109,32 +29,29 @@ package-z -e http://example.com/mypackage-1.0.4.zip """ - result = load_requirements(stringio(f)) - expected = ['package-a >= 0.42', 'package-b', 'package-c', 'package-z'] - nose.tools.eq_(result, expected) + result = load_requirements(stringio(f)) + expected = ["package-a >= 0.42", "package-b", "package-c", "package-z"] + assert result == expected def test_find_packages(): - # tests the find-packages command inside the bob.extension package + # tests the find-packages command inside the bob.extension package - basedir = pkg_resources.resource_filename('bob.extension', ".") - packages = find_packages(os.path.abspath(os.path.join(basedir, ".."))) + basedir = pkg_resources.resource_filename("bob.extension", ".") + packages = find_packages(os.path.abspath(os.path.join(basedir, ".."))) - site_packages = os.path.dirname(os.path.commonprefix(packages)) - packages = [os.path.relpath(k, site_packages) for k in packages] + site_packages = os.path.dirname(os.path.commonprefix(packages)) + packages = [os.path.relpath(k, site_packages) for k in packages] - assert 'bob' in packages - assert 'bob.extension' in packages - assert 'bob.extension.scripts' in packages + assert "bob" in packages + assert "bob.extension" in packages + assert "bob.extension.scripts" in packages def test_documentation_generation(): - if sys.version_info[0] == 3: from io import StringIO as stringio - else: - from cStringIO import StringIO as stringio - f = """ # this is my requirements file + f = """ # this is my requirements file package-a >= 0.42 package-b package-c @@ -146,80 +63,86 @@ package-z -e http://example.com/mypackage-1.0.4.zip """ - # keep the nose tests quiet - _stdout = sys.stdout - - try: - devnull = open(os.devnull, 'w') - sys.stdout = devnull - - # test NumPy and SciPy docs - try: - import numpy - result = link_documentation(['numpy'], None) - assert len(result) == 1 - key = list(result.keys())[0] - assert 'numpy' in key - except ImportError: - pass - - try: - import scipy - result = link_documentation(['scipy'], None) - assert len(result) == 1 - key = list(result.keys())[0] - assert 'scipy' in key - except ImportError: - pass + # keep the tests quiet + _stdout = sys.stdout try: - import matplotlib - result = link_documentation(['matplotlib'], None) - assert len(result) == 1 - key = list(result.keys())[0] - assert 'matplotlib' in key - except ImportError: - pass - - # test pypi packages - additional_packages = [ - 'python', - 'matplotlib', - 'bob.extension', - 'gridtk', - 'other.bob.package', + devnull = open(os.devnull, "w") + sys.stdout = devnull + + # test NumPy and SciPy docs + try: + import numpy # noqa: F401 + + result = link_documentation(["numpy"], None) + assert len(result) == 1 + key = list(result.keys())[0] + assert "numpy" in key + except ImportError: + pass + + try: + import scipy # noqa: F401 + + result = link_documentation(["scipy"], None) + assert len(result) == 1 + key = list(result.keys())[0] + assert "scipy" in key + except ImportError: + pass + + try: + import matplotlib # noqa: F401 + + result = link_documentation(["matplotlib"], None) + assert len(result) == 1 + key = list(result.keys())[0] + assert "matplotlib" in key + except ImportError: + pass + + # test pypi packages + additional_packages = [ + "python", + "matplotlib", + "bob.extension", + "gridtk", + "other.bob.package", ] - # test linkage to official documentation - server = "http://www.idiap.ch/software/bob/docs/bob/%s/master/" - os.environ["BOB_DOCUMENTATION_SERVER"] = server - result = link_documentation(additional_packages, stringio(f)) - expected = [ - 'https://docs.python.org/%d.%d/' % sys.version_info[:2], - 'https://matplotlib.org/stable/', - 'https://setuptools.readthedocs.io/en/latest/', - server % 'bob.extension', - server % 'gridtk', + # test linkage to official documentation + server = "http://www.idiap.ch/software/bob/docs/bob/%s/master/" + os.environ["BOB_DOCUMENTATION_SERVER"] = server + result = link_documentation(additional_packages, stringio(f)) + expected = [ + "https://docs.python.org/%d.%d/" % sys.version_info[:2], + "https://matplotlib.org/stable/", + "https://setuptools.readthedocs.io/en/latest/", + server % "bob.extension", + server % "gridtk", ] - result = [k[0] for k in result.values()] - nose.tools.eq_(sorted(result), sorted(expected)) + result = [k[0] for k in result.values()] + assert sorted(result) == sorted(expected) - finally: - sys.stdout = _stdout + finally: + sys.stdout = _stdout def test_get_config(): - # Test the generic get_config() function - import bob.extension - cfg = bob.extension.get_config() - splits = cfg.split("\n") - assert splits[0].startswith('bob.extension') - assert splits[1].startswith("* Python dependencies") - assert any([s.startswith(" - setuptools") for s in splits[2:]]) - - cfg = bob.extension.get_config("setuptools", {'MyPackage' : {'my_dict' : 42}}, 0x0204) - splits = cfg.split("\n") - assert splits[0].startswith('setuptools') - assert "api=0x0204" in splits[0] - assert splits[1].startswith("* C/C++ dependencies") - assert any([s.startswith(" - MyPackage") for s in splits[2:]]) + # Test the generic get_config() function + import bob.extension + + cfg = bob.extension.get_config() + splits = cfg.split("\n") + assert splits[0].startswith("bob.extension") + assert splits[1].startswith("* Python dependencies") + assert any([s.startswith(" - setuptools") for s in splits[2:]]) + + cfg = bob.extension.get_config( + "setuptools", {"MyPackage": {"my_dict": 42}}, 0x0204 + ) + splits = cfg.split("\n") + assert splits[0].startswith("setuptools") + assert "api=0x0204" in splits[0] + assert splits[1].startswith("* C/C++ dependencies") + assert any([s.startswith(" - MyPackage") for s in splits[2:]]) diff --git a/bob/extension/utils.py b/bob/extension/utils.py index 8f7b6640d21aa5f32a9fa0930e8e9f8a2a8a9e9f..36f0d13296c179541b38eb731d978638b4fbe501 100644 --- a/bob/extension/utils.py +++ b/bob/extension/utils.py @@ -3,602 +3,260 @@ # Andre Anjos <andre.dos.anjos@gmail.com> # Fri 21 Mar 2014 10:37:40 CET -'''General utilities for building extensions''' +"""General utilities for building extensions""" import os import re import sys -import glob -import platform + import pkg_resources -from . import DEFAULT_PREFIXES - - -def construct_search_paths(prefixes=None, subpaths=None, suffix=None): - """Constructs a list of candidate paths to search for. - - The list of paths is constructed using the following order of priority: - - 1. ``BOB_PREFIX_PATH`` environment variable, if set. ``BOB_PREFIX_PATH`` can - contain several paths divided by :any:`os.pathsep`. - 2. The paths provided with the ``prefixes`` parameter. - 3. The current python executable prefix. - 4. The ``CONDA_PREFIX`` environment variable, if set. - 5. :any:`DEFAULT_PREFIXES`. - - Parameters - ---------- - prefixes : [:obj:`str`], optional - The list of paths to be added to the results. - subpaths : [:obj:`str`], optional - A list of subpaths to be appended to each path at the end. For - example, if you specify ``['foo', 'bar']`` for this parameter, then - ``os.path.join(paths[0], 'foo')``, - ``os.path.join(paths[0], 'bar')``, and so on are added to the returned - paths. Globs are accepted in this list and resolved using the function - :py:func:`glob.glob`. - suffix : :obj:`str`, optional - ``suffix`` will be appended to all paths except ``prefixes``. - - Returns - ------- - paths : [str] - A list of unique and existing paths to be used in your search. - """ - search = [] - suffix = suffix or '' - - # Priority 1: the environment - if 'BOB_PREFIX_PATH' in os.environ: - paths = os.environ['BOB_PREFIX_PATH'].split(os.pathsep) - search += [p + suffix for p in paths] - - # Priority 2: user passed paths - if prefixes: - search += prefixes - - # Priority 3: the current system executable - search.append(os.path.dirname(os.path.dirname(sys.executable)) + suffix) - - # Priority 4: the conda prefix - conda_prefix = os.environ.get('CONDA_PREFIX') - if conda_prefix: - search.append(conda_prefix + suffix) - - # Priority 5: the default search prefixes - search += [p + suffix for p in DEFAULT_PREFIXES] - - # Make unique to avoid searching twice - search = uniq_paths(search) - - # Exhaustive combination of paths and subpaths - if subpaths: - subsearch = [] - for s in search: - for p in subpaths: - subsearch.append(os.path.join(s, p)) - subsearch.append(s) - search = subsearch - - # Before we do a file-system check, filter out the un-existing paths - tmp = [] - for k in search: - tmp += glob.glob(k) - search = tmp - - return search - - -def find_file(name, subpaths=None, prefixes=None): - """Finds a generic file on the file system. Returns all occurrences. - - This method will find all occurrences of a given name on the file system and - will return them to the user. It uses :any:`construct_search_paths` to - construct the candidate folders that file may exist in. - - Parameters - ---------- - name : str - The name of the file. For example, ``gcc``. - subpaths : [:obj:`str`], optional - See :any:`construct_search_paths` - subpaths : :obj:`str`, optional - See :any:`construct_search_paths` - - Returns - ------- - [str] - A list of filenames that exist on the filesystem, matching your - description. - """ - - search = construct_search_paths(prefixes=prefixes, subpaths=subpaths) - - retval = [] - for path in search: - candidate = os.path.join(path, name) - if os.path.exists(candidate): - retval.append(candidate) - - return retval - - -def find_header(name, subpaths=None, prefixes=None): - """Finds a header file on the file system. Returns all candidates. - - This method will find all occurrences of a given name on the file system and - will return them to the user. It uses :any:`construct_search_paths` to - construct the candidate folders that header may exist in accounting - automatically for typical header folder names. - - Parameters - ---------- - name : str - The name of the header file. - subpaths : [:obj:`str`], optional - See :any:`construct_search_paths` - subpaths : :obj:`str`, optional - See :any:`construct_search_paths` - - Returns - ------- - [str] - A list of filenames that exist on the filesystem, matching your - description. - """ - - headerpaths = [] - - # arm-based system (e.g. raspberry pi 32 or 64-bit) - if platform.machine().startswith('arm'): - headerpaths += [os.path.join('include', 'arm-linux-gnueabihf')] - - # else, consider it intel compatible - elif platform.architecture()[0] == '32bit': - headerpaths += [os.path.join('include', 'i386-linux-gnu')] - else: - headerpaths += [os.path.join('include', 'x86_64-linux-gnu')] - - # Raspberry PI search directory (arch independent) + normal include - headerpaths += ['include'] - - # Exhaustive combination of paths and subpaths - if subpaths: - my_subpaths = [] - for hp in headerpaths: - my_subpaths += [os.path.join(hp, k) for k in subpaths] - else: - my_subpaths = headerpaths - - return find_file(name, my_subpaths, prefixes) - - -def find_library(name, version=None, subpaths=None, prefixes=None, - only_static=False): - """Finds a library file on the file system. Returns all candidates. - - This method will find all occurrences of a given name on the file system and - will return them to the user. It uses :any:`construct_search_paths` to - construct the candidate folders that the library may exist in accounting - automatically for typical library folder names. - - Parameters - ---------- - name : str - The name of the module to be found. If you'd like to find libz.so, for - example, specify ``"z"``. For libmath.so, specify ``"math"``. - version : :obj:`str`, optional - The version of the library we are searching for. If not specified, then - look only for the default names, such as ``libz.so`` and the such. - subpaths : [:obj:`str`], optional - See :any:`construct_search_paths` - subpaths : :obj:`str`, optional - See :any:`construct_search_paths` - only_static : :obj:`bool`, optional - A boolean, indicating if we should try only to search for static versions - of the libraries. If not set, any would do. - - Returns - ------- - [str] - A list of filenames that exist on the filesystem, matching your - description. - """ - - libpaths = [] - - # arm-based system (e.g. raspberry pi 32 or 64-bit) - if platform.machine().startswith('arm'): - libpaths += [os.path.join('lib', 'arm-linux-gnueabihf')] - - # else, consider it intel compatible - elif platform.architecture()[0] == '32bit': - libpaths += [ - os.path.join('lib', 'i386-linux-gnu'), - os.path.join('lib32'), - ] - else: - libpaths += [ - os.path.join('lib', 'x86_64-linux-gnu'), - os.path.join('lib64'), - ] - - libpaths += ['lib'] - - # Exhaustive combination of paths and subpaths - if subpaths: - my_subpaths = [] - for lp in libpaths: - my_subpaths += [os.path.join(lp, k) for k in subpaths] - else: - my_subpaths = libpaths - - # Extensions to consider - if only_static: - extensions = ['.a'] - else: - if sys.platform == 'darwin': - extensions = ['.dylib', '.a'] - elif sys.platform == 'win32': - extensions = ['.dll', '.a'] - else: # linux like - extensions = ['.so', '.a'] - - # The module names can be set with or without version number - retval = [] - if version: - for ext in extensions: - if sys.platform == 'darwin': # version in the middle - libname = 'lib' + name + '.' + version + ext - else: # version at the end - libname = 'lib' + name + ext + '.' + version - - retval += find_file(libname, my_subpaths, prefixes) - - for ext in extensions: - libname = 'lib' + name + ext - retval += find_file(libname, my_subpaths, prefixes) - - return retval - -def find_executable(name, subpaths=None, prefixes=None): - """Finds an executable on the file system. Returns all candidates. - - This method will find all occurrences of a given name on the file system and - will return them to the user. It uses :any:`construct_search_paths` to - construct the candidate folders that the executable may exist in accounting - automatically for typical executable folder names. - - Parameters - ---------- - name : str - The name of the file. For example, ``gcc``. - subpaths : [:obj:`str`], optional - See :any:`construct_search_paths` - prefixes : :obj:`str`, optional - See :any:`construct_search_paths` - - Returns - ------- - [str] - A list of filenames that exist on the filesystem, matching your - description. - """ - - binpaths = [] - - # arm-based system (e.g. raspberry pi 32 or 64-bit) - if platform.machine().startswith('arm'): - binpaths += [os.path.join('bin', 'arm-linux-gnueabihf')] - - # else, consider it intel compatible - elif platform.architecture()[0] == '32bit': - binpaths += [ - os.path.join('bin', 'i386-linux-gnu'), - os.path.join('bin32'), - ] - else: - binpaths += [ - os.path.join('bin', 'x86_64-linux-gnu'), - os.path.join('bin64'), - ] - - binpaths += ['bin'] - - # Exhaustive combination of paths and subpaths - if subpaths: - my_subpaths = [] - for lp in binpaths: - my_subpaths += [os.path.join(lp, k) for k in subpaths] - else: - my_subpaths = binpaths - - # if conda-build's BUILD_PREFIX is set, use it as it may contain build tools - # which are not available on the host environment - prefixes = prefixes if prefixes is not None else [] - if 'BUILD_PREFIX' in os.environ: - prefixes += [os.environ['BUILD_PREFIX']] - - # The module names can be set with or without version number - return find_file(name, my_subpaths, prefixes) - -def uniq(seq, idfun=None): - """Very fast, order preserving uniq function""" - - # order preserving - if idfun is None: - def idfun(x): return x - seen = {} - result = [] - for item in seq: - marker = idfun(item) - # in old Python versions: - # if seen.has_key(marker) - # but in new ones: - if marker in seen: continue - seen[marker] = 1 - result.append(item) - return result - -def uniq_paths(seq): - """Uniq'fy a list of paths taking into consideration their real paths""" - return uniq([os.path.realpath(k) for k in seq if os.path.exists(k)]) - -def egrep(filename, expression): - """Runs grep for a given expression on each line of the file - - Parameters: - - filename, str - The name of the file to grep for the expression - - expression - A regular expression, that will be initialized using :py:func:`re.compile`. - - Returns a list of re matches. - """ - - retval = [] - - with open(filename, 'rt') as f: - rexp = re.compile(expression) - for line in f: - p = rexp.match(line) - if p: retval.append(p) - - return retval + def load_requirements(f=None): - """Loads the contents of requirements.txt on the given path. - - Defaults to "./requirements.txt" - """ - - def readlines(f): - retval = [str(k.strip()) for k in f] - return [k for k in retval if k and k[0] not in ('#', '-')] - - # if f is None, use the default ('requirements.txt') - if f is None: - f = 'requirements.txt' - if isinstance(f, str): - f = open(f, 'rt') - # read the contents - return readlines(f) - -def find_packages(directories=['bob']): - """This function replaces the ``find_packages`` command from ``setuptools`` to search for packages only in the given directories. - Using this function will increase the building speed, especially when you have (links to) deep non-code-related directory structures inside your package directory. - The given ``directories`` should be a list of top-level sub-directories of your package, where package code can be found. - By default, it uses ``'bob'`` as the only directory to search. - """ - from setuptools import find_packages as _original - if isinstance(directories, str): - directories = [directories] - packages = [] - for d in directories: - packages += [d] - packages += ["%s.%s" % (d, p) for p in _original(d)] - return packages - -def link_documentation(additional_packages = ['python', 'numpy'], requirements_file = "../requirements.txt", server = None): - """Generates a list of documented packages on our documentation server for the packages read from the "requirements.txt" file and the given list of additional packages. - - Parameters: - - additional_packages : [str] - A list of additional bob packages for which the documentation urls are added. - By default, 'numpy' is added - - requirements_file : str or file-like - The file (relative to the documentation directory), where to read the requirements from. - If ``None``, it will be skipped. - - server : str or None - The url to the server which provides the documentation. - If ``None`` (the default), the ``BOB_DOCUMENTATION_SERVER`` environment variable is taken if existent. - If neither ``server`` is specified, nor a ``BOB_DOCUMENTATION_SERVER`` environment variable is set, the default ``"http://www.idiap.ch/software/bob/docs/bob/%(name)s/%(version)s/"`` is used. - - """ - - def smaller_than(v1, v2): - """Compares scipy/numpy version numbers""" - - c1 = v1.split('.') - c2 = v2.split('.')[:len(c1)] #clip to the compared version - for i in range(len(c2)): - n1 = c1[i] - n2 = c2[i] - try: - n1 = int(n1) - n2 = int(n2) - except ValueError: - n1 = str(n1) - n2 = str(n2) - if n1 < n2: return True - if n1 > n2: return False - return False - - - if sys.version_info[0] <= 2: - import urllib2 as urllib - from urllib2 import HTTPError, URLError - else: - import urllib.request as urllib - import urllib.error as error - HTTPError = error.HTTPError - URLError = error.URLError + """Loads the contents of requirements.txt on the given path. + + Defaults to "./requirements.txt" + """ + + def readlines(f): + retval = [str(k.strip()) for k in f] + return [k for k in retval if k and k[0] not in ("#", "-")] + # if f is None, use the default ('requirements.txt') + if f is None: + f = "requirements.txt" + if isinstance(f, str): + f = open(f, "rt") + # read the contents + return readlines(f) - # collect packages are automatically included in the list of indexes - packages = [] - version_re = re.compile(r'\s*[\<\>=]+\s*') - if requirements_file is not None: - if not isinstance(requirements_file, str) or \ - os.path.exists(requirements_file): - requirements = load_requirements(requirements_file) - packages += [version_re.split(k)[0] for k in requirements] - packages += additional_packages +def find_packages(directories=["bob"]): + """This function replaces the ``find_packages`` command from ``setuptools`` to search for packages only in the given directories. + Using this function will increase the building speed, especially when you have (links to) deep non-code-related directory structures inside your package directory. + The given ``directories`` should be a list of top-level sub-directories of your package, where package code can be found. + By default, it uses ``'bob'`` as the only directory to search. + """ + from setuptools import find_packages as _original + + if isinstance(directories, str): + directories = [directories] + packages = [] + for d in directories: + packages += [d] + packages += ["%s.%s" % (d, p) for p in _original(d)] + return packages - def _add_index(name, addr, packages=packages): - """Helper to add a new doc index to the intersphinx catalog + +def link_documentation( + additional_packages=["python", "numpy"], + requirements_file="../requirements.txt", + server=None, +): + """Generates a list of documented packages on our documentation server for the packages read from the "requirements.txt" file and the given list of additional packages. Parameters: - name (str): Name of the package that will be added to the catalog - addr (str): The URL (except the ``objects.inv`` file), that will be added + additional_packages : [str] + A list of additional bob packages for which the documentation urls are added. + By default, 'numpy' is added + + requirements_file : str or file-like + The file (relative to the documentation directory), where to read the requirements from. + If ``None``, it will be skipped. + + server : str or None + The url to the server which provides the documentation. + If ``None`` (the default), the ``BOB_DOCUMENTATION_SERVER`` environment variable is taken if existent. + If neither ``server`` is specified, nor a ``BOB_DOCUMENTATION_SERVER`` environment variable is set, the default ``"http://www.idiap.ch/software/bob/docs/bob/%(name)s/%(version)s/"`` is used. """ - if name in packages: - print ("Adding intersphinx source for `%s': %s" % (name, addr)) - mapping[name] = (addr, None) - packages = [k for k in packages if k != name] + def smaller_than(v1, v2): + """Compares scipy/numpy version numbers""" + + c1 = v1.split(".") + c2 = v2.split(".")[: len(c1)] # clip to the compared version + for i in range(len(c2)): + n1 = c1[i] + n2 = c2[i] + try: + n1 = int(n1) + n2 = int(n2) + except ValueError: + n1 = str(n1) + n2 = str(n2) + if n1 < n2: + return True + if n1 > n2: + return False + return False + import urllib.error as error + import urllib.request as urllib - def _add_numpy_index(): - """Helper to add the numpy manual""" + HTTPError = error.HTTPError + URLError = error.URLError - try: - import numpy - ver = numpy.version.version - ver = '.'.join(ver.split('.')[:-1]) - _add_index('numpy', 'https://numpy.org/doc/%s/' % ver) + # collect packages are automatically included in the list of indexes + packages = [] + version_re = re.compile(r"\s*[\<\>=]+\s*") + if requirements_file is not None: + if not isinstance(requirements_file, str) or os.path.exists( + requirements_file + ): + requirements = load_requirements(requirements_file) + packages += [version_re.split(k)[0] for k in requirements] + packages += additional_packages - except ImportError: - _add_index('numpy', 'https://numpy.org/devdocs/') + def _add_index(name, addr, packages=packages): + """Helper to add a new doc index to the intersphinx catalog + Parameters: - def _add_scipy_index(): - """Helper to add the scipy manual""" + name (str): Name of the package that will be added to the catalog + addr (str): The URL (except the ``objects.inv`` file), that will be added - try: - import scipy - ver = scipy.version.version - if smaller_than(ver, '0.9.0'): - ver = '.'.join(ver.split('.')[:-1]) + '.x' - else: - ver = '.'.join(ver.split('.')[:-1]) + '.0' - _add_index('scipy', 'https://docs.scipy.org/doc/scipy-%s/reference/' % ver) + """ - except ImportError: - _add_index('scipy', 'https://docs.scipy.org/doc/scipy/reference/') + if name in packages: + print("Adding intersphinx source for `%s': %s" % (name, addr)) + mapping[name] = (addr, None) + packages = [k for k in packages if k != name] + def _add_numpy_index(): + """Helper to add the numpy manual""" - def _add_click_index(): - """Helper to add the click manual""" + try: + import numpy + + ver = numpy.version.version + ver = ".".join(ver.split(".")[:-1]) + _add_index("numpy", "https://numpy.org/doc/%s/" % ver) + + except ImportError: + _add_index("numpy", "https://numpy.org/devdocs/") + + def _add_scipy_index(): + """Helper to add the scipy manual""" - import click - major, minor = [int(x) for x in click.__version__.split('.')[0:2]] - if major < 8: - ver = f"{major}.x" - else: - ver = f"{major}.{minor}.x" - _add_index('click', 'https://click.palletsprojects.com/en/%s/' % ver) - - - mapping = {} - - # add indexes for common packages used in Bob - _add_index('python', 'https://docs.python.org/%d.%d/' % sys.version_info[:2]) - _add_numpy_index() - _add_index('scipy', 'https://docs.scipy.org/doc/scipy/') - _add_index('matplotlib', 'https://matplotlib.org/stable/') - _add_index('setuptools', 'https://setuptools.readthedocs.io/en/latest/') - _add_index('six', 'https://six.readthedocs.io') - _add_index('sqlalchemy', 'https://docs.sqlalchemy.org/en/latest/') - _add_index('docopt', 'https://docopt.readthedocs.io/en/latest/') - _add_index('scikit-learn', 'https://scikit-learn.org/stable/') - _add_index('scikit-image', 'https://scikit-image.org/docs/dev/') - _add_index('pillow', 'https://pillow.readthedocs.io/en/latest/') - _add_index('PIL', 'https://pillow.readthedocs.io/en/latest/') - _add_index('pandas', 'https://pandas.pydata.org/pandas-docs/stable/') - _add_index('dask', 'https://docs.dask.org/en/latest/') - _add_index('dask-jobqueue', 'https://jobqueue.dask.org/en/latest/') - _add_index('distributed', 'https://distributed.dask.org/en/latest/') - _add_click_index() - _add_index('torch', 'https://pytorch.org/docs/stable/') - _add_index('xarray', 'https://xarray.pydata.org/en/stable/') - _add_index("h5py", "https://docs.h5py.org/en/stable/") - - - # get the server for the other packages - if server is None: - if "BOB_DOCUMENTATION_SERVER" in os.environ: - server = os.environ["BOB_DOCUMENTATION_SERVER"] - else: - server = "http://www.idiap.ch/software/bob/docs/bob/%(name)s/%(version)s/|http://www.idiap.ch/software/bob/docs/bob/%(name)s/master/" - - # array support for BOB_DOCUMENTATION_SERVER - # transforms "(file:///path/to/dir https://example.com/dir| http://bla )" - # into ["file:///path/to/dir", "https://example.com/dir", "http://bla"] - # so, trim eventual parenthesis/white-spaces and splits by white space or | - if server.strip(): - server = re.split(r'[|\s]+', server.strip('() ')) - else: - server = [] - - # check if the packages have documentation on the server - for p in packages: - if p in mapping: continue #do not add twice... - - for s in server: - # generate URL - package_name = p.split()[0] - if s.count('%s') == 1: #old style - url = s % package_name - else: #use new style, with mapping, try to link against specific version try: - version = 'v' + pkg_resources.require(package_name)[0].version - except pkg_resources.DistributionNotFound: - version = 'stable' #package is not a runtime dep, only referenced - url = s % {'name': package_name, 'version': version} - - try: - # otherwise, urlopen will fail - if url.startswith('file://'): - f = urllib.urlopen(urllib.Request(url + 'objects.inv')) - url = url[7:] #intersphinx does not like file:// - else: - f = urllib.urlopen(urllib.Request(url)) + import scipy - # request url - print("Found documentation for %s on %s; adding intersphinx source" % (p, url)) - mapping[p] = (url, None) - break #inner loop, for server, as we found a candidate! + ver = scipy.version.version + if smaller_than(ver, "0.9.0"): + ver = ".".join(ver.split(".")[:-1]) + ".x" + else: + ver = ".".join(ver.split(".")[:-1]) + ".0" + _add_index( + "scipy", "https://docs.scipy.org/doc/scipy-%s/reference/" % ver + ) - except HTTPError as exc: - if exc.code != 404: - # url request failed with a something else than 404 Error - print("Requesting URL %s returned error: %s" % (url, exc)) - # notice mapping is not updated here, as the URL does not exist + except ImportError: + _add_index("scipy", "https://docs.scipy.org/doc/scipy/reference/") - except URLError as exc: - print("Requesting URL %s did not succeed (maybe offline?). " \ - "The error was: %s" % (url, exc)) + def _add_click_index(): + """Helper to add the click manual""" - except IOError as exc: - print ("Path %s does not exist. The error was: %s" % (url, exc)) + import click - return mapping + major, minor = [int(x) for x in click.__version__.split(".")[0:2]] + if major < 8: + ver = f"{major}.x" + else: + ver = f"{major}.{minor}.x" + _add_index("click", "https://click.palletsprojects.com/en/%s/" % ver) + + mapping = {} + + # add indexes for common packages used in Bob + _add_index( + "python", "https://docs.python.org/%d.%d/" % sys.version_info[:2] + ) + _add_numpy_index() + _add_index("scipy", "https://docs.scipy.org/doc/scipy/") + _add_index("matplotlib", "https://matplotlib.org/stable/") + _add_index("setuptools", "https://setuptools.readthedocs.io/en/latest/") + _add_index("six", "https://six.readthedocs.io") + _add_index("sqlalchemy", "https://docs.sqlalchemy.org/en/latest/") + _add_index("docopt", "https://docopt.readthedocs.io/en/latest/") + _add_index("scikit-learn", "https://scikit-learn.org/stable/") + _add_index("scikit-image", "https://scikit-image.org/docs/dev/") + _add_index("pillow", "https://pillow.readthedocs.io/en/latest/") + _add_index("PIL", "https://pillow.readthedocs.io/en/latest/") + _add_index("pandas", "https://pandas.pydata.org/pandas-docs/stable/") + _add_index("dask", "https://docs.dask.org/en/latest/") + _add_index("dask-jobqueue", "https://jobqueue.dask.org/en/latest/") + _add_index("distributed", "https://distributed.dask.org/en/latest/") + _add_click_index() + _add_index("torch", "https://pytorch.org/docs/stable/") + _add_index("xarray", "https://xarray.pydata.org/en/stable/") + _add_index("h5py", "https://docs.h5py.org/en/stable/") + + # get the server for the other packages + if server is None: + if "BOB_DOCUMENTATION_SERVER" in os.environ: + server = os.environ["BOB_DOCUMENTATION_SERVER"] + else: + server = "http://www.idiap.ch/software/bob/docs/bob/%(name)s/%(version)s/|http://www.idiap.ch/software/bob/docs/bob/%(name)s/master/" + + # array support for BOB_DOCUMENTATION_SERVER + # transforms "(file:///path/to/dir https://example.com/dir| http://bla )" + # into ["file:///path/to/dir", "https://example.com/dir", "http://bla"] + # so, trim eventual parenthesis/white-spaces and splits by white space or | + if server.strip(): + server = re.split(r"[|\s]+", server.strip("() ")) + else: + server = [] + + # check if the packages have documentation on the server + for p in packages: + if p in mapping: + continue # do not add twice... + + for s in server: + # generate URL + package_name = p.split()[0] + if s.count("%s") == 1: # old style + url = s % package_name + else: # use new style, with mapping, try to link against specific version + try: + version = ( + "v" + pkg_resources.require(package_name)[0].version + ) + except pkg_resources.DistributionNotFound: + version = "stable" # package is not a runtime dep, only referenced + url = s % {"name": package_name, "version": version} + + try: + # otherwise, urlopen will fail + if url.startswith("file://"): + urllib.urlopen(urllib.Request(url + "objects.inv")) + url = url[7:] # intersphinx does not like file:// + else: + urllib.urlopen(urllib.Request(url)) + + # request url + print( + "Found documentation for %s on %s; adding intersphinx source" + % (p, url) + ) + mapping[p] = (url, None) + break # inner loop, for server, as we found a candidate! + + except HTTPError as exc: + if exc.code != 404: + # url request failed with a something else than 404 Error + print("Requesting URL %s returned error: %s" % (url, exc)) + # notice mapping is not updated here, as the URL does not exist + + except URLError as exc: + print( + "Requesting URL %s did not succeed (maybe offline?). " + "The error was: %s" % (url, exc) + ) + + except IOError as exc: + print("Path %s does not exist. The error was: %s" % (url, exc)) + + return mapping diff --git a/conda/meta.yaml b/conda/meta.yaml index 2c40acc8c73caa3877d42f0cee8d5af22537313f..2bdfc5c1f283159aff294bfaa6bb7eec6db5cae2 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -40,25 +40,18 @@ test: - bob config --help # fix for the CONDA_BUILD_SYSROOT variable missing at test time - export CONDA_BUILD_SYSROOT={{ CONDA_BUILD_SYSROOT }} # [osx] - - nosetests --with-coverage --cover-package={{ name }} -sv {{ name }} + - pytest --verbose --cov {{ name }} --cov-report term-missing --cov-report html:{{ project_dir }}/sphinx/coverage --cov-report xml:{{ project_dir }}/coverage.xml --pyargs {{ name }} - sphinx-build -aEW {{ project_dir }}/doc {{ project_dir }}/sphinx - sphinx-build -aEb doctest {{ project_dir }}/doc sphinx - conda inspect linkages -p $PREFIX {{ name }} # [not win] - conda inspect objects -p $PREFIX {{ name }} # [osx] requires: - - nose {{ nose }} + - pytest {{ pytest }} + - pytest-cov {{ pytest_cov }} - coverage {{ coverage }} - sphinx {{ sphinx }} - sphinx_rtd_theme {{ sphinx_rtd_theme }} - - boost {{ boost }} - - cmake {{ cmake }} - - make {{ make }} - - pkg-config {{ pkg_config }} - - freetype {{ freetype }} - - libblitz {{ libblitz }} - numpy {{ numpy }} - - {{ compiler('c') }} - - {{ compiler('cxx') }} about: home: https://www.idiap.ch/software/bob/ diff --git a/doc/annotate.py b/doc/annotate.py index c00b3b7ff0cfdc8588846de8cae5dc26e4177015..96344a59de70b13945cf44a6e4bd28f4abf14f73 100644 --- a/doc/annotate.py +++ b/doc/annotate.py @@ -4,34 +4,68 @@ # line's --help option and its auto-complete feature in terminal (if enabled). Instead, # put your imports inside the function. import logging + import click + from bob.extension.scripts.click_helper import ( - verbosity_option, ConfigCommand, ResourceOption, log_parameters) + ConfigCommand, + ResourceOption, + log_parameters, + verbosity_option, +) logger = logging.getLogger(__name__) -@click.command(entry_point_group='bob.bio.config', cls=ConfigCommand, - epilog='''\b +@click.command( + entry_point_group="bob.bio.config", + cls=ConfigCommand, + epilog="""\b Examples: $ bob bio annotate -vvv -d <database> -a <annotator> -o /tmp/annotations $ jman submit --array 64 -- bob bio annotate ... --array 64 -''') -@click.option('--database', '-d', required=True, cls=ResourceOption, - entry_point_group='bob.bio.database', - help='''The database that you want to annotate.''') -@click.option('--annotator', '-a', required=True, cls=ResourceOption, - entry_point_group='bob.bio.annotator', - help='A callable that takes the database and a sample (biofile) ' - 'of the database and returns the annotations in a dictionary.') -@click.option('--output-dir', '-o', required=True, cls=ResourceOption, - help='The directory to save the annotations.') -@click.option('--force', '-f', is_flag=True, cls=ResourceOption, - help='Whether to overwrite existing annotations.') -@click.option('--array', type=click.INT, default=1, cls=ResourceOption, - help='Use this option alongside gridtk to submit this script as ' - 'an array job.') +""", +) +@click.option( + "--database", + "-d", + required=True, + cls=ResourceOption, + entry_point_group="bob.bio.database", + help="""The database that you want to annotate.""", +) +@click.option( + "--annotator", + "-a", + required=True, + cls=ResourceOption, + entry_point_group="bob.bio.annotator", + help="A callable that takes the database and a sample (biofile) " + "of the database and returns the annotations in a dictionary.", +) +@click.option( + "--output-dir", + "-o", + required=True, + cls=ResourceOption, + help="The directory to save the annotations.", +) +@click.option( + "--force", + "-f", + is_flag=True, + cls=ResourceOption, + help="Whether to overwrite existing annotations.", +) +@click.option( + "--array", + type=click.INT, + default=1, + cls=ResourceOption, + help="Use this option alongside gridtk to submit this script as " + "an array job.", +) @verbosity_option(cls=ResourceOption) def annotate(database, annotator, output_dir, force, array, **kwargs): """Annotates a database. diff --git a/doc/conf.py b/doc/conf.py index 77fbf79ebad4761a59e970898570b61339c5b838..68775d7f2d8a01fbd429fe367477c3ac6e25eca7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -2,31 +2,29 @@ # vim: set fileencoding=utf-8 : import os -import sys -import glob -import pkg_resources +import pkg_resources # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.3' +needs_sphinx = "1.3" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.graphviz', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', - ] + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.ifconfig", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.graphviz", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", +] # Be picky about warnings nitpicky = True @@ -35,16 +33,12 @@ nitpicky = True nitpick_ignore = [] # Allows the user to override warnings from a separate file -if os.path.exists('nitpick-exceptions.txt'): - for line in open('nitpick-exceptions.txt'): +if os.path.exists("nitpick-exceptions.txt"): + for line in open("nitpick-exceptions.txt"): if line.strip() == "" or line.startswith("#"): continue dtype, target = line.split(None, 1) target = target.strip() - try: # python 2.x - target = unicode(target) - except NameError: - pass nitpick_ignore.append((dtype, target)) # Always includes todos @@ -57,25 +51,27 @@ autosummary_generate = True numfig = True # If we are on OSX, the 'dvipng' path maybe different -dvipng_osx = '/opt/local/libexec/texlive/binaries/dvipng' -if os.path.exists(dvipng_osx): pngmath_dvipng = dvipng_osx +dvipng_osx = "/opt/local/libexec/texlive/binaries/dvipng" +if os.path.exists(dvipng_osx): + pngmath_dvipng = dvipng_osx # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'bob.extension' +project = "bob.extension" import time -copyright = u'%s, Idiap Research Institute' % time.strftime('%Y') + +copyright = "%s, Idiap Research Institute" % time.strftime("%Y") # Grab the setup entry distribution = pkg_resources.require(project)[0] @@ -91,42 +87,42 @@ release = distribution.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['links.rst'] +exclude_patterns = ["links.rst"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # Some variables which are useful for generated material -project_variable = project.replace('.', '_') -short_description = u'Building of Python/C++ extensions for Bob' -owner = [u'Idiap Research Institute'] +project_variable = project.replace(".", "_") +short_description = "Building of Python/C++ extensions for Bob" +owner = ["Idiap Research Institute"] # -- Options for HTML output --------------------------------------------------- @@ -134,80 +130,81 @@ owner = [u'Idiap Research Institute'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import sphinx_rtd_theme -html_theme = 'sphinx_rtd_theme' + +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = project_variable +# html_short_title = project_variable # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = 'img/logo.png' +html_logo = "img/logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = 'img/favicon.ico' +html_favicon = "img/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a <link> tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = project_variable + u'_doc' +htmlhelp_basename = project_variable + "_doc" # -- Post configuration -------------------------------------------------------- @@ -217,22 +214,27 @@ rst_epilog = """ .. |project| replace:: Bob .. |version| replace:: %s .. |current-year| date:: %%Y -""" % (version,) +""" % ( + version, +) # Default processing flags for sphinx -autoclass_content = 'class' -autodoc_member_order = 'bysource' +autoclass_content = "class" +autodoc_member_order = "bysource" autodoc_default_options = { - "members": True, - "undoc-members": True, - "show-inheritance": True, + "members": True, + "undoc-members": True, + "show-inheritance": True, } # For inter-documentation mapping: from bob.extension.utils import link_documentation, load_requirements + sphinx_requirements = "extra-intersphinx.txt" if os.path.exists(sphinx_requirements): - intersphinx_mapping = link_documentation(additional_packages=load_requirements(sphinx_requirements)) + intersphinx_mapping = link_documentation( + additional_packages=load_requirements(sphinx_requirements) + ) else: intersphinx_mapping = link_documentation() diff --git a/doc/cplusplus_library.rst b/doc/cplusplus_library.rst deleted file mode 100644 index 5a579b904617c3b1a0f0b55b1fe1872031547573..0000000000000000000000000000000000000000 --- a/doc/cplusplus_library.rst +++ /dev/null @@ -1,135 +0,0 @@ -.. vim: set fileencoding=utf-8 : - -=============================== -C/C++ libraries in your package -=============================== - -Typically, |project|'s core packages include both a pure C++ library as well as Python bindings for the C++ code. - -If you want to provide a library with pure C++ code in your package as well, -you can use the :py:class:`bob.extension.Library` class. It will automatically -compile your C/C++ code using `CMake <http://www.cmake.org>`_ into a shared -library that you can link to your own C/C++-Python bindings, as well as in the -C++ code of other C++/Python packages. Again, a complete example can be -downloaded via: - -.. code-block:: sh - - $ git clone https://gitlab.idiap.ch/bob/bob.extension.git - $ cp -R bob.extension/bob/extension/data/bob.example.extension ./ - $ rm -rf bob.extension # optionally remove the cloned source of bob.extension - $ cd bob.example.extension - - ------------------------ -Setting up your package ------------------------ - -If you would like to generate a Library out of your C++ code, simply add it in the list of ``ext_modules``: - -.. code-block:: python - - ... - # import the Extension and Library classes and the build_ext function from bob.blitz - from bob.blitz.extension import Extension, Library, build_ext - ... - - setup( - - ext_modules = [ - # declare a pure C/C++ library just the same way as an extension - Library("bob.example.library.bob_example_library", - # list of pure C/C++ files compiled into this library - [ - "bob/example/library/cpp/Function.cpp", - ], - version = version, - bob_packages = bob_packages, - ), - # all other extensions will automatically link against the Library defined above - Extension("bob.example.library._library", - # list of files compiled into this extension - [ - # the Python bindings - "bob/example/library/main.cpp", - ], - version = version, - bob_packages = bob_packages, - ), - ... #add more Extensions if you wish - ], - - cmdclass = { - 'build_ext': build_ext - }, - - ... - ) - - - -Again, we use the overloaded library class -``bob.blitz.extension.Library`` instead of the -:py:class:`bob.extension.Library`, but the parameters are identical, and -identical to the ones of the :py:class:`bob.extension.Extension`. To avoid -later complications, you should follow the guidelines for libraries in bob -packages: - -1. The name of the C++ library need to be identical to the name of your package (replacing the '.' by '_'). - Also, the package name need to be part of it. - For example, to create a library for the ``bob.example.library`` package, it should be called ``bob.example.library.bob_example_library``. - In this way it is assured that the libraries are found by the ``bob_packages`` parameter (see above). - -2. All header files that your C++ library should export need to be placed in the directory ``bob/example/library/include/bob.example.library``. - Again, this is the default directory, where the ``bob_packages`` expect the includes to be. - This is also the directory that is added to your own library and to your extensions, so you don't need to specify that by hand. - -3. The include directory should contain a ``config.h`` file, which contains C/C++ preprocessor directives that contains the current version of your C/C++ API. - With this, we make sure that the version of the library that is linked into other packages is the expected one. - One such file is again given in our ``bob.example.library`` example. - -4. To avoid conflicts with other functions, you should put all your exported C++ functions into an appropriate namespace. - In our example, this should be something like ``bob::example::library``. - -The newly generated Library will be automatically linked to **all other** Extensions in the package. -No worries, if the library is not used in the extension, the linker should be able to figure that out... - -.. note: - The clang linker seems not to be smart enough to detect unused libraries... - -.. note:: - You can also export a library without bindings, for it to be used in other C++/Python packages. - - ---------------------- -Building your package ---------------------- - -As shown above, to compile your C++ Python bindings and the pure C++ libraries, you can follow the simple instructions: - -.. code-block:: sh - - $ buildout - ... - -This will automatically check out all required ``bob_packages`` and compile them locally. -Afterwards, the C++ code from this package will be compiled, using a newly created ``build`` directory for temporary output. -After compilation, this directory can be safely removed (re-compiling will re-create it). - -To get the source code compiled using another build directory, you can define a ``BOB_BUILD_DIRECTORY`` environment variable, e.g.: - -.. code-block:: sh - - $ BOB_BUILD_DIRECTORY=/tmp/build_bob buildout - ... - -The C++ code of this package, **and the code of all other** ``bob_packages`` will be compiled using the selected directory. -Again, after compilation this directory can be safely removed. - -Another environment variable enables parallel compilation of C or C++ code. -Use ``BOB_BUILD_PARALLEL=X`` (where ``X`` is the number of parallel processes you want) to enable parallel building. - -.. note:: - For macOS-based builds, you may need to setup additional environment - variables **before** successfully building libraries. Refer to the section - :ref:`extension-c++` for details. diff --git a/doc/cplusplus_modules.rst b/doc/cplusplus_modules.rst deleted file mode 100644 index d595364638d4f8a582a9e60b4c3c1a9fc6ef97fa..0000000000000000000000000000000000000000 --- a/doc/cplusplus_modules.rst +++ /dev/null @@ -1,276 +0,0 @@ -.. vim: set fileencoding=utf-8 : -.. Manuel Guenther <manuel.guenther@idiap.ch> -.. Mon Oct 13 16:57:44 CEST 2014 - -.. _extension-c++: - -============================== - C/C++ modules in your package -============================== - -|project| massively relies on a mixture between the user-friendly and easy-to-develop Python interface, and a fast implementation of identified bottlenecks using C++. - -Creating C++/Python bindings should be rather straightforward. -Only few adaptations need to be performed to get the C/C++ code being compiled and added as an extension. -For simplicity, we created an example package that includes a simple example of a C++ extension. -You can check it out by: - -.. code-block:: sh - - $ git clone https://gitlab.idiap.ch/bob/bob.extension.git - $ cp -R bob.extension/bob/extension/data/bob.example.extension ./ - $ rm -rf bob.extension # optionally remove the cloned source of bob.extension - $ cd bob.example.extension - - -Setting up your package ------------------------ - -Typically, Python extensions written in C/C++ for Bob should use a set of standard APIs allowing C++ Blitz++ Arrays to be transparently converted to Python NumPy Arrays. -The build of your package will therefore depend on, at least, two packages: (1) bob.extension (this package): will provide build instructions and resources for defining and building your extension (2) bob.blitz: will provide a bridge between pure C++ code, depending on Blitz++ Arrays and NumPy arrays. -To be able to import ``bob.extension`` and ``bob.blitz`` in the setup.py, we need to include some code: - -.. code-block:: python - - setup_packages = ['bob.extension', 'bob.blitz'] - - # C++ modules needed at runtime of your package - bob_packages = [] - - from setuptools import setup, find_packages, dist - dist.Distribution(dict(setup_requires = setup_packages + bob_packages)) - -We keep the ``setup_packages`` and ``bob_packages`` in separate variables since we will need them later. -The ``bob_packages`` contain a list of bob packages that this extension **directly** depends on. -In our example, we only depend on ``bob.blitz``, and we can leave the list empty. - -.. warning:: - - ``bob.blitz`` is required in all C++/Python packages since it contains all the mechanisms - to deal with arrays amongst other things. - -As the second step, we need to add some lines in the header of the file to tell the ``setuptools`` system to compile our library with our ``Extension`` class: - -.. code-block:: python - - # import the Extension class and the build_ext function from bob.blitz - from bob.blitz.extension import Extension, build_ext - - # load the requirements.txt for additional requirements - from bob.extension.utils import load_requirements - build_requires = setup_packages + bob_packages + load_requirements() - -In fact, we don't use the extension from :py:class:`bob.extension.Extension`, but the one from ``bob.blitz.extension``, which is a derivation of this package. -The difference is that in ``bob.blitz.extension.Extension`` all header files and libraries for the ``Blitz++`` library are added. - -Third, we have to add an extension using the ``Extension`` class, by listing all C/C++ files that should be compiled into the extension: - -.. code-block:: python - - # read version from version.txt file - version = open("version.txt").read().rstrip() - - setup( - ... - setup_requires = build_requires, - install_requires = build_requires, - ... - ext_modules = [ - Extension("bob.example.extension._module", - [ - # the pure C++ code - "bob/example/extension/Function.cpp", - # the Python bindings - "bob/example/extension/main.cpp", - ], - version = version, - bob_packages = bob_packages - ), - ... #add more extensions if you wish - ], - ... - ) - -These modifications will allow you to compile extensions that are linked against our core Python-C++ bridge ``bob.blitz`` (by default). -You can specify any other ``pkg-config`` module and that will be linked in (for example, ``boost`` or ``opencv``) using the ``packages`` parameter. -For ``boost`` packages, you might need to define, which boost modules are required. -By default, when using boost you should at least add the ``system`` module, i.e., by: - -.. code-block:: python - - setup( - ... - ext_modules = [ - Extension( - ... - packages = ['boost'], - boost_modules = ['system'], - ), - ... - ], - ... - ) - -Other modules and options can be set manually using `the standard options for Python extensions <https://docs.python.org/2/extending/building.html>`_. - -When your module compiles and links against the pure C++ code, you can simply use the ``bob_packages`` to specify dependencies in your C++ code. -This will automatically add the desired include and library directories, as well as the libraries and the required preprocessor options. - -In our example, we have defined a small C++ function, which also shows the basic bridge between ``numpy.ndarray`` and our C++ pendant ``Blitz++``. -Basically, there are two C++ files for our extension. -``bob/example/extension/Function.cpp`` contains the pure C++ implementation of the function. -In ``bob/example/extension/main.cpp``, we define the Python bindings to that function. -Finally, the function ``reverse`` from the module ``_library`` is imported into our module in the ``bob/example/extension/__init__.py`` file. - -.. - including the creation of a complete Python module called ``_library``. - Additionally, we give a short example of how to use our documentation classes provided in this module (see below for more details). - -.. note:: - In the bindings of the ``reverse`` function in ``bob/example/extension/main.cpp``, we make use of some C++ defines that makes the life easier. - see :ref:`helpers` - - -Building your package ---------------------- - -To compile your C++ Python bindings and the corresponding C++ implementation, -just do: - -.. code-block:: sh - - $ buildout - ... - -.. note:: - By default, we compile the source code (of this and **all dependent packages**, both the ones installed as ``eggs``, and the ones developed using ``mr.developer``) in debug mode. - If you want to change that, switch the according flag in the ``buildout.cfg`` to ``debug = False``, and the compilation will be done with optimization flags and C++ exception handling enabled. - -.. note:: - For macOS-based builds, one also needs to ensure the environment variables - ``MACOSX_DEPLOYMENT_TARGET``, ``SDKROOT``, and ``CONDA_BUILD_SYSROOT`` are - properly set. This is automatically handled for conda-build based runs. If - you are using buildout or any other setuptools-based system (such as pip - installs) to build your package, you should ensure that is the case with one - of these 2 methods (more to least recommended): - - 1. You set the RC variables (see: :ref:`bob.extension.rc`) - `bob.extension.macosx_deployment_target` and - `bob.extension.macosx_sdkroot` to suitable values. Example: - - .. code-block:: sh - - $ bob config get bob.extension.macosx_deployment_target - Error: The requested key `bob.extension.macosx_deployment_target` does not exist - $ bob config set bob.extension.macosx_deployment_target "10.9" - - $ bob config get bob.extension.macosx_sdkroot - Error: The requested key `bob.extension.macosx_sdkroot` does not exist - $ bob config set bob.extension.macosx_sdkroot "/opt/MacOSX10.9.sdk" - - With this method you set the default for your particular machine. It is - the recommended way to set up such variables as those settings do not - affect builds in other machines and are preserved across package builds, - guaranteeing uniformity. - - Unfortunately, the variable `CONDA_BUILD_SYSROOT` must be set on the - environment (conda will preset it otherwise). Change your login profile - shell or similar to add the following: - - .. code-block:: sh - - $ export CONDA_BUILD_SYSROOT="/opt/MacOSX10.9.sdk" - - 2. You set the environment variables directly on the current environment. - Example: - - .. code-block:: sh - - $ export MACOSX_DEPLOYMENT_TARGET="10.9" - $ export SDKROOT="/opt/MacOSX10.9.sdk" - $ export CONDA_BUILD_SYSROOT="${SDKROOT}" - - Note that this technique is the least ephemeral from all available - options. As soon as you leave the current environment, the variables - will not be available anymore. - - **Precedence**: Values set on the environment have precedence over values - set on your Bob RC configuration. - - **Compatibility**: We recommend you check our stock - `conda_build_config.yaml` for ensuring cross-package compatibility - (currently available through our admin package "bob.devtools"). At the time - of writing, we use a "10.9" macOS SDK for Bob packages. That may change in - the future. - - **Obtaining an SDK**: We recommend `Phracker macOS SDKs available on Github - <https://github.com/phracker/MacOSX-SDKs>`_. Install the SDK on - ``/opt/MacOSX<version>.sdk``. - - -Now, we can use the script ``./bin/bob_example_extension_reverse.py`` (that we have registered in the ``setup.py``) to reverse a list of floats, using the C++ implementation of the ``reverse`` function: - -.. code-block:: sh - - $ ./bin/bob_example_extension_reverse.py 1 2 3 4 5 - [1.0, 2.0, 3.0, 4.0, 5.0] reversed is [ 5. 4. 3. 2. 1.] - -We can also see that the function documentation has made it into the module, too: - -.. code-block:: sh - - $ ./bin/python - >>> import bob.example.extension - >>> help(bob.example.extension) - -and that we can list version and the dependencies of our package: - -.. code-block:: sh - - >>> print (bob.example.extension.version) - 0.0.1a0 - >>> print (bob.example.extension.get_config()) - ... - - -.. _helpers: - -Helper utilities ----------------- - -In the header file ``<bob.extension/defines.h>`` we have added some functions that help you to keep your code short and clean. -Particularly, we provide three preprocessor directives: - -.. c:macro:: BOB_TRY - - Starts a try-catch block to protect your bound function against exceptions of any kinds (which would lead to a Python interpreter crash otherwise). - -.. c:macro:: BOB_CATCH_FUNCTION(message, ret) - - Catches C++ exceptions of any kind, adds the ``message`` in case an unknown exception is caught, and returns with the given error return (which is usually 0 for normal functions or -1 for constructors and setter functions). - This macro should be used when binding a stand-alone function, for binding class member functions, please use :c:macro:`BOB_CATCH_MEMBER`. - -.. c:macro:: BOB_CATCH_MEMBER(message, ret) - - Catches C++ exceptions of any kind, adds the ``message`` in case an unknown exception is caught, and returns with the given error return (which is usually 0 for normal functions or -1 for constructors and setter functions). - This macro should be used when binding a member function of a class, for binding stand-alone functions, please use :c:macro:`BOB_CATCH_FUNCTION`. - -These preprocessor directives will catch any C++ exception that is raised inside the C/C++ code that you bind to python and translate them into proper Python exceptions. - -.. warning:: - These directives will only be active in **release** mode, when compiling - with ``debug = true``, they will not do anything. This is in order to - support C++ debuggers like ``gdb`` or ``gdb-python`` to be able to handle - these exceptions. - -Additionally, we added some preprocessor directives that help in the bindings: - -.. c:macro:: PyBob_NumberCheck(o) - - Checks if the given object ``o`` is a number, i.e., an int, a long, a float - or a complex. - -After including the above mentioned header, we also re-define the functions -:c:func:`PyInt_Check`, :c:func:`PyInt_AS_LONG`, :c:func:`PyString_Check` and -:c:func:`PyString_AS_STRING` (which doesn't exist in the bindings for Python3) -so that they can be used in bindings for both Python2 and Python3. diff --git a/doc/cpp_api.rst b/doc/cpp_api.rst deleted file mode 100644 index 3e97c773823bc3379ab71fae80e147d5eeace302..0000000000000000000000000000000000000000 --- a/doc/cpp_api.rst +++ /dev/null @@ -1,270 +0,0 @@ -.. vim: set fileencoding=utf-8 : -.. Manuel Guenther <manuel.guenther@idiap.ch> -.. Fri Oct 10 14:03:53 CEST 2014 -.. -.. Copyright (C) 2011-2014 Idiap Research Institute, Martigny, Switzerland - - -.. _cpp_api: - - -===================================== - C++ API of the Documentation classes -===================================== - -This section includes information for using the pure C++ API for the documentation classes, which can be accessed after including: - -.. code-block:: c++ - - # include <bob.extension/documentation.h> - -The classes, which are described in more detail below, can be used to format the documentation of your C/C++ functions that are bound to Python. -Any free text that you specify to describe your functions will be interpreted as `reStructuredText <http://docutils.sourceforge.net/rst.html>`_. -Hence, it is possible to use any directives like ``.. note::``, ``.. math::``, and even links inside the documentation like ``:py:class:`` and references as ``[REF]_``. - - ----------------------- -Function Documentation ----------------------- - -.. cpp:class:: bob::extension::FunctionDoc - - To document a function (either a stand-alone function or a member function of a class), you should use the :cpp:func:`bob::extension::FunctionDoc`. - - .. cpp:function:: bob::extension::FunctionDoc(\ - const char* const function_name,\ - const char* const short_desctiption,\ - const char* const long_description = NULL,\ - bool is_member_function = false\ - ) - - In the constructor, you specify the function name and a short description. - If wanted, you can define a longer description as well. - When you use this FunctionDoc to document a member function of a class, please set ``is_member_function = true``. - - .. cpp:function:: FunctionDoc clone(\ - const char* const function_name\ - ) - - Returns a copy of this documentation class, where the function name is replaced by the given one. - This is useful, when a function is bound with several names. - - .. cpp:function:: FunctionDoc& add_prototype(\ - const char* const variables,\ - const char* const return_value = "None"\ - ) - - Adds a prototype of the documented function declaration to the function. - All ``variables`` and all ``return_value``'s listed must be documented using the :cpp:func:`add_parameter` or :cpp:func:`add_return` functions. - Only the default return value ``None`` does not need documentation. - - ``variables`` is a single string containing a comma-separated list of parameter names. - Use ``..., [name]`` to indicate that name is ``name`` is an optional parameter. - - ``return_value`` is a single string containing a comma-separated list of return value names. - If a single name is given, only a single value is returned, otherwise a tuple will be returned by your function. - - .. note:: - Each :cpp:class:`FunctionDoc` needs at least one prototype. - In opposition to pure Python functions, specifying multiple prototypes is allowed here as well. - - - .. cpp:function:: FunctionDoc& add_parameter(\ - const char* const parameter_name,\ - const char* const parameter_type,\ - const char* const parameter_description\ - ) - - Adds a description for a given parameter. - - ``parameter_name`` must be one of the names listed in the ``variables`` of the :cpp:func:`add_prototype` function. - - ``parameter_type`` specifies the expected type of this parameter. - You can use any free text to describe the type. - When ``:py:class:`` directives or similar are used, they will be interpreted correctly. - - ``parameter_description`` includes free text to describe, what the parameter is used for. - - - .. cpp:function:: FunctionDoc& add_return(\ - const char* const return_name,\ - const char* const return_type,\ - const char* const return_description\ - ) - - Adds a description for a given return value. - - ``return_name`` must be one of the names listed as a ``return_value`` of the :cpp:func:`add_prototype` function. - - ``return_type`` specifies the type of this return value. - You can use any free text to describe the type. - When ``:py:class:`` directives or similar are used, they will be interpreted correctly. - - ``return_description`` includes free text to describe, what the return value contains. - - - .. cpp:function:: const char* const name() const - - Returns the name of the function defined in the constructor. - - - .. cpp:function:: const char* const doc(const unsigned alignment = 72, const unsigned indent = 0) const - - Generates and returns the documentation string. - The free text in the documentation is aligned to ``alignment`` characters, by default 72, so that it can be viewed correctly inside of an 80-character Python console. - The ``indent`` is an internal parameter and should not be changed. - - - .. cpp:function:: char** kwlist(unsigned index) const - - Returns the list of keyword arguments for the given prototype index added with the :cpp:func:`add_prototype` function. - This list is in the desired format to be passed as the ``keywords`` parameter to the :c:func:`PyArg_ParseTupleAndKeywords` function during your bindings. - - - .. cpp:function:: void print_usage() const - - Prints a function usage string to console, including all information specified by the member functions above. - - -All functions adding information to the :cpp:class:`bob::extension::FunctionDoc` return a reference to the current object, so that you can use it inline, like: - -.. code-block:: c++ - - auto function_doc = bob::extension::FunctionDoc( - "function_name", - "Short description of the function", - "Long description of the function using reStructuredText including directives like :py:class:`bob.blitz.array`." - ) - .add_prototype("param1, [param2]", "ret") - .add_parameter("param1", "int", "An int value used for ...") - .add_parameter("param2", "float", "[Default: ``0.5``] A float value describing ...") - .add_return("ret", ":py:class:`bob.blitz.array`", "An array ...") - ; - - -During the binding of your function, you can use it, like: - -.. code-block:: c++ - - static PyMethodDef module_methods[] = { - ... - { - function_doc.name(), - (PyCFunction)function, - METH_VARARGS|METH_KEYWORDS, - function_doc.doc() - }, - ... - }; - - ------------------------ -Variables Documentation ------------------------ - -.. cpp:class:: bob::extension::VariableDoc - - To document a variable (either a stand-alone function or a member function - of a class), you should use the :cpp:func:`bob::extension::VariableDoc`. - - .. cpp:function:: bob::extension::VariableDoc(\ - const char* const variable_name,\ - const char* const variable_type,\ - const char* const short_desctiption,\ - const char* const long_description = NULL\ - ) - - In the constructor, you specify the variable name, its type and a short - description. The structure is identical to the - :cpp:func:`FunctionDoc::add_parameter` function. If wanted, you can - define a longer description as well. - - - .. cpp:function:: char* name() const - - Returns the name of the variable defined in the constructor. - - - .. cpp:function:: char* doc(const unsigned alignment = 72) const - - Generates and returns the documentation string, which is composed of the - information provided in the constructor. The free text in the - documentation is aligned to ``alignment`` characters, by default 72, so - that it can be viewed correctly inside of an 80-character Python console. - - -------------------- -Class Documentation -------------------- - -.. cpp:class:: bob::extension::ClassDoc - - To document a class including its constructor, you should use the :cpp:func:`bob::extension::ClassDoc`. - - .. cpp:function:: bob::extension::ClassDoc(\ - const char* const class_name,\ - const char* const short_desctiption,\ - const char* const long_description = NULL\ - ) - - In the constructor, you specify the class name and a short description. - If wanted, you can define a longer description as well. - - - .. cpp:function:: ClassDoc& add_constructor(\ - const FunctionDoc& constructor_doc\ - ) - - Adds the documentation of the constructor, which itself is a :cpp:class:`FunctionDoc`. - - .. note:: - You should specify the return value of your constructor to be ``""`` to overwrite the default value ``"None"``. - - .. note:: - A class can have only a single constructor documentation. - Hence, this function can be called only once for each class. - - - .. cpp:function:: char* name() const - - Returns the name of the class defined in the constructor. - - - .. cpp:function:: char* doc(const unsigned alignment = 72) const - - Generates and returns the documentation string, which is composed of the information provided in the constructor, and the constructor documentation. - The free text in the documentation is aligned to ``alignment`` characters, by default 72, so that it can be viewed correctly inside of an 80-character Python console. - - - .. cpp:function:: char** kwlist(unsigned index) const - - Returns the list of keyword arguments of the constructor for the given prototype index added with the :cpp:func:`FunctionDoc::add_prototype` function. - This list is in the desired format to be passed as the ``keywords`` parameter to the :c:func:`PyArg_ParseTupleAndKeywords` function during your bindings. - - - .. cpp:function:: void print_usage() const - - Prints the usage of the constructor. - See :cpp:func:`FunctionDoc::print_usage` for details. - - -As for functions, the :cpp:class:`bob::extension::ClassDoc` is designed to be used inline, like: - -.. code-block:: c++ - - auto class_doc = bob::extension::ClassDoc( - "class_name", - "Short description of the class", - "Long description of the class using reStructuredText including directives like :py:class:`bob.blitz.array`." - ) - .add_constructor( - bob::extension::FunctionDoc( - "class_name", - "Short description of the constructor", - "Long description of the constructor" - true - ) - .add_prototype("param1, [param2]", "") - .add_parameter("param1", "int", "An int value used for ...") - .add_parameter("param2", "float", "[Default: ``0.5``] A float value describing ...") - ); diff --git a/doc/development.rst b/doc/development.rst index 2bd96d2a7a6b5ef17a9f05ba32ea7fb9306eaba1..732e9c11f1ebba2510623fcbcc3404d663a3d7f7 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -4,7 +4,9 @@ Developing existing |project| packages ======================================= -The sources of Bob_ packages are hosted on Idiap's gitlab_ and they -are managed by git_. To develop existing |project| packages their source should be checked out and a proper local environment should be set up. The details for developing packages are in `bob development tools`_. +The sources of Bob_ packages are hosted on Idiap's gitlab_ and they are managed +by git_. To develop existing |project| packages their source should be checked +out and a proper local environment should be set up. The details for developing +packages are in `bob development tools`_. .. include:: links.rst diff --git a/doc/documenting.rst b/doc/documenting.rst index e7a9ad5975ac34dc8917e29d4d0450d84d2a60a7..63b9b22956d8f0dae58464c3ecfbbcad6fb782f2 100644 --- a/doc/documenting.rst +++ b/doc/documenting.rst @@ -6,11 +6,11 @@ Documenting your package ======================== -If you intend to distribute your newly created package, please consider carefully documenting it. +If you intend to distribute your newly created package, please consider carefully documenting it. Documentation is an essential starting point for new users. Undocumented code tends to be barely re-used and may end up being abandoned. -First you should have a proper README file (such as the one provided in packages previousy provided as examples). +First you should have a proper README file (such as the one provided in packages previousy provided as examples). We made a simple, minimal ``README.rst`` template that you can get by doing: .. code-block:: sh @@ -32,7 +32,7 @@ Replace the following tags by hand if you don't like/trust the `sed` lines above for extending Bob_ by building packages using either pure python or a mix of C++ and python.` -Additional information should be made available in the documentation. +Additional information should be made available in the documentation. Ideally, you should write a user's guide for your package. There are plenty of examples in the existing bob packages - and don't hesitate to tell us (either by opening an issue on gitlab or through our mailing list) if some are missing or outdated. @@ -43,18 +43,18 @@ Documenting Python code To write documentation, use the `Sphinx`_ Documentation Generator. Get familiar with Sphinx and then unleash the writer in you. -To automatically generate API documentation, we make use of the `Napoleon`_ Sphinx extension +To automatically generate API documentation, we make use of the `Napoleon`_ Sphinx extension that enables Sphinx to parse both NumPy and Google style docstrings. It has been agreed -that the style used to document bob packages is the NumPy style. To get familiar on how to document your Python code, +that the style used to document bob packages is the NumPy style. To get familiar on how to document your Python code, you can have a look on the `Napoleon`_ website (and the links within) or in existing bob packages. You can also refer to the official `numpydoc docstring guide`_. .. note:: - You should start by doing the following: + You should start by doing the following: .. code-block:: sh - + $ curl -k --silent https://gitlab.idiap.ch/bob/bob.admin/raw/master/templates/sphinx-conf.py > doc/conf.py $ mkdir -pv doc/img $ curl -k --silent https://gitlab.idiap.ch/bob/bob.admin/raw/master/templates/logo.png > doc/img/logo.png @@ -69,9 +69,9 @@ refer to the official `numpydoc docstring guide`_. + ``extra-intersphinx.txt``, which lists extra packages that should be cross-linked to the documentation (as with Sphinx's intersphinx extension. The format of this text file is simple: it contains the PyPI names of packages to cross-reference. One per line. + ``nitpick-exceptions.txt``, which lists which documentation objects to ignore (for warnings and errors). - The format of this text file is two-column. On the first column, you should refer to Sphinx the object type, e.g. ``py:class``, - followed by a space and then the name of the that should be ignored. E.g.: ``bob.bio.base.Database``. - The file may optionally contain empty lines. Lines starting with # are ignored (so you can comment on why you're ignoring these objects). + The format of this text file is two-column. On the first column, you should refer to Sphinx the object type, e.g. ``py:class``, + followed by a space and then the name of the that should be ignored. E.g.: ``bob.bio.base.Database``. + The file may optionally contain empty lines. Lines starting with # are ignored (so you can comment on why you're ignoring these objects). Ignoring errors should be used only as a last resource. You should first try to fix the errors as best as you can, so your documentation links are properly working. @@ -101,192 +101,4 @@ You can now admire the result in your favorite browser: If the code you are distributing corresponds to the work described in a publication, don't forget to mention it in your ``doc/index.rst`` file. - -Documenting your C/C++ Python Extension ---------------------------------------- - -One part of the ``bob.extension`` package consist in some functions that makes it easy to generate a proper Python documentation for your bound C/C++ functions. -For the API documentation of the package, please read :ref:`cpp_api`. - -One example for a function documentation can be found in the file ``bob/example/library/main.cpp``, which you have downloaded before. -This documentation can be used after: - -.. code-block:: c++ - - #include <bob.extension/documentation.h> - - -Function documentation -++++++++++++++++++++++ - -To generate a properly aligned function documentation, you can use the :cpp:class:`bob::extension::FunctionDoc`: - -.. code-block:: c++ - - bob::extension::FunctionDoc description( - "function_name", - "Short function description", - "Optional long function description" - ); - - -.. note:: - - If you want to document a member function of a class, you should use set fourth boolean option to true. - This is required since the default Python class member documentation is indented four more spaces, which we need to balance: - - .. code-block:: c++ - - bob::extension::FunctionDoc member_function_description( - "function_name", - "Short function description", - "Optional long function description", - true - ); - -Using this object, you can add several parts of the function that need documentation: - -1. ``description.add_prototype("variable1, variable2", "return1, return2");`` can be used to add function definitions (i.e., ways how to use your function). - This function needs to be called at least once. - If the function does not define a return value, it can be left out (in which case the default ``"None"`` is used). - -2. ``description.add_parameter("variable1, variable2", "datatype", "Variable description");`` should be defined for each variable that you have used in the prototypes. - -3. ``description.add_return("return1", "datatype", "Return value description");`` should be defined for each return value that you have used in the prototypes. - -.. note:: - - All these functions return a reference to the object, so that you can use them in line, e.g.: - - .. code-block:: c++ - - static auto description = bob::extension::FunctionDoc(...) - .add_prototype(...) - .add_parameter(...) - .add_return(...) - ; - -A complete working exemplary function documentation from the ``reverse`` function in ``bob.example.library`` package would look like this: - -.. code-block:: c++ - - static bob::extension::FunctionDoc reverse_doc = bob::extension::FunctionDoc( - "reverse", - "This is a simple example of bridging between blitz arrays (C++) and numpy.ndarrays (Python)", - "Detailed documentation of the function goes here." - ) - .add_prototype("array", "reversed") - .add_parameter("array", "array_like (1D, float)", "The array to reverse") - .add_return("reversed", "array_like (1D, float)", "A copy of the ``array`` with reversed order of entries") - ; - -Finally, when binding you function, you can use: - -a. ``description.name()`` to get the name of the function - -b. ``description.doc()`` to get the aligned documentation of the function, properly indented and broken at 80 characters (by default). - This call will check that all parameters and return values are documented, and add a ``.. todo::`` directive if not. - -c. ``description.kwlist(index)`` to get the list of keyword arguments for the given prototype ``index`` that can be passed as the ``keywords`` parameter to the :c:func:`PyArg_ParseTupleAndKeywords` function. - -which can be used during the binding of the function. -In our example, it would look like: - -.. code-block:: c++ - - PyMethodDef methods[] = { - ... - { - reverse_doc.name(), - (PyCFunction)PyBobExampleLibrary_Reverse, - METH_VARARGS|METH_KEYWORDS, - reverse_doc.doc() - }, - ... - {NULL} // Sentinel - }; - - -Sphinx directives like ``.. note::``, ``.. warning::`` or ``.. math::`` will be automatically detected and aligned, when they are used as one-line directive, e.g.: - -.. code-block:: c++ - - "(more text)\n\n.. note:: This is a note\n\n(more text)" - -Also, enumerations and listings (using the ``*`` character to define a list element) are handled automatically: - -.. code-block:: c++ - - "(more text)\n\n* Point 1\n* Point 2\n\n(more text)" - -.. note:: - - Please assure that directives are surrounded by double ``\n`` characters (see example above) so that they are put as paragraphs. - Otherwise, they will not be displayed correctly. - -.. note:: - - The ``.. todo::`` directive seems not to like being broken at 80 characters. - If you want to use ``.. todo::``, please call, e.g., ``description.doc(10000)`` to avoid line breaking. - -.. note:: - - To increase readability, you might want to split your documentation lines, e.g.: - - .. code-block:: c++ - - "(more text)\n" - "\n" - "* Point 1\n" - "* Point 2\n" - "\n" - "(more text)" - -Leading white-spaces in the documentation string are handled correctly, so you can use several layers of indentation. - - -**Class documentation** -+++++++++++++++++++++++ - -To document a bound class, you can use the :cpp:class:`bob::extension::ClassDoc` to align and wrap your documentation. -Again, during binding you can use the functions ``description.name()`` and ``description.doc()`` as above. - -Additionally, the class documentation has a function to add constructor definitions, which takes an :cpp:class:`bob::extension::FunctionDoc` object. -The shortest way to get a proper class documentation is: - -.. code-block:: c++ - - auto my_class_doc = - bob::extension::ClassDoc("class_name", "Short description", "Long Description") - .add_constructor( - bob::extension::FunctionDoc("class_name", "Constructor Description") - .add_prototype("param1", "") - .add_parameter("param1", "type1", "Description of param1") - ) - ; - -.. note:: - - The second parameter ``""`` in ``add_prototype`` prevents the output type (which otherwise defaults to ``"None"``) to be written. - -.. note:: - - For constructor documentations, there is no need to declare them as member functions. - This is done automatically for you. - -.. note:: - You can use the :cpp:func:`bob::extension::ClassDoc::kwlist` function to retrieve the ``kwlist`` of the constructor documentation. - -Currently, the :cpp:class:`bob::extension::ClassDoc` allows to highlight member functions or variables at the beginning of the class documentation. -This highlighting is still under development and might not work as expected. - - -Possible speed issues -===================== - -In order to speed up the loading time of the modules, you might want to reduce the amount of documentation that is generated (though I haven't experienced any speed differences). -For this purpose, just compile your bindings using the ``"-DBOB_SHORT_DOCSTRINGS"`` compiler option, e.g. by simply define an environment variable ``BOB_SHORT_DOCSTRINGS=1`` before invoking ``buildout``. - -In any of these cases, only the short descriptions will be returned as the doc string. - .. include:: links.rst diff --git a/doc/index.rst b/doc/index.rst index caafe8cca9bb25a467ed451208673ab93f74f35f..e8b8ca8be45a4f59f0c08c635debdba86752e4f4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,21 +9,14 @@ .. todolist:: This module contains information on how to build, maintain, and distribute Bob_ -packages written in pure Python or a mix of C/C++ and Python. +packages written in Python. By following these instructions you will be able to: * Setup a local development environment. * Download and install |project| packages and other software into your development environment. -* Implement your own package including either pure Python code, a mixture of - C/C++ and Python code, and even pure C/C++ libraries with clean C/C++ - interfaces. - - .. note:: - If possible, you should try to develop new packages using Python only, - since they are easier to maintain. - +* Implement your own package. * Distribute your work to others in a clean and organized manner. @@ -35,13 +28,10 @@ Documentation development pure_python - cplusplus_modules - cplusplus_library documenting rc framework py_api - cpp_api Indices and tables ------------------ diff --git a/doc/links.rst b/doc/links.rst index f638bfd3ba60a5b35d91030727da1fc1f777c4ae..9e90b56c2666e981c06e365fe102968b3cd6fddf 100644 --- a/doc/links.rst +++ b/doc/links.rst @@ -15,7 +15,6 @@ .. _gitlab: https://gitlab.idiap.ch/bob/ .. _idiap: http://www.idiap.ch .. _ipython: http://ipython.scipy.org -.. _nose: https://nose.readthedocs.org/en/latest/ .. _pep 386: http://www.python.org/dev/peps/pep-0386/ .. _python: http://www.python.org .. _pypi: http://pypi.python.org diff --git a/doc/py_api.rst b/doc/py_api.rst index d335423a07a7d3a3f7d47bba797886c886c7dbd4..3b1c4b5ffbead7ffdb498600543d744acd792e03 100644 --- a/doc/py_api.rst +++ b/doc/py_api.rst @@ -15,28 +15,9 @@ Core Functionality ^^^^^^^^^^^^^^^^^^ .. autosummary:: - bob.extension.boost - bob.extension.build_ext - bob.extension.check_packages - bob.extension.CMakeListsGenerator - bob.extension.construct_search_paths - bob.extension.DEFAULT_PREFIXES - bob.extension.Extension - bob.extension.find_executable - bob.extension.find_library - bob.extension.generate_self_macros - bob.extension.get_bob_libraries bob.extension.get_config - bob.extension.get_full_libname - bob.extension.Library - bob.extension.load_bob_library - bob.extension.normalize_requirements - bob.extension.pkgconfig bob.extension.rc bob.extension.rc_context - bob.extension.reorganize_isystem - bob.extension.uniq - bob.extension.uniq_paths bob.extension.download.get_file @@ -44,9 +25,6 @@ Utilities ^^^^^^^^^ .. autosummary:: - bob.extension.utils.egrep - bob.extension.utils.find_file - bob.extension.utils.find_header bob.extension.utils.find_packages bob.extension.utils.link_documentation bob.extension.utils.load_requirements @@ -86,7 +64,6 @@ Utilities --------- .. automodule:: bob.extension.utils - :exclude-members: find_executable,find_library,uniq,uniq_paths,construct_search_paths .. automodule:: bob.extension.download diff --git a/pyproject.toml b/pyproject.toml index 9787c3bdf008a57ae3cb2e27a8261eb3f134ff73..b738dc847ff9705c5769673db7415f2eb9a75f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,12 @@ [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" + requires = ["setuptools", "wheel", "bob.extension"] + build-backend = "setuptools.build_meta" + +[tool.isort] + profile = "black" + line_length = 80 + order_by_type = true + lines_between_types = 1 + +[tool.black] + line-length = 80 diff --git a/setup.py b/setup.py index d2c8869c12fabf6e002d74f2d687b797b4602a5b..cf6b4c8d828d3a766ba6f76112d74fa69b5abef6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ """A package that contains a helper for Bob Python/C++ extension development """ -from setuptools import setup, find_packages +from setuptools import find_packages, setup # Define package version version = open("version.txt").read().rstrip() @@ -12,48 +12,45 @@ setup( name="bob.extension", version=version, description="Building of Python/C++ extensions for Bob", - url='http://gitlab.idiap.ch/bob/bob.extension', + url="http://gitlab.idiap.ch/bob/bob.extension", license="BSD", - author='Andre Anjos', - author_email='andre.anjos@idiap.ch', - long_description=open('README.rst').read(), - + author="Andre Anjos", + author_email="andre.anjos@idiap.ch", + long_description=open("README.rst").read(), packages=find_packages(), include_package_data=True, zip_safe=False, - - install_requires=['setuptools', 'click >= 8', 'click-plugins'], - + install_requires=["setuptools", "click >= 8", "click-plugins"], entry_points={ - 'console_scripts': [ - 'bob = bob.extension.scripts:main_cli', + "console_scripts": [ + "bob = bob.extension.scripts:main_cli", ], - 'bob.cli': [ - 'config = bob.extension.scripts.config:config', + "bob.cli": [ + "config = bob.extension.scripts.config:config", ], # some test entry_points - 'bob.extension.test_config_load': [ - 'basic_config = bob.extension.data.basic_config', - 'verbose_config = bob.extension.data.verbose_config', - 'resource_config = bob.extension.data.resource_config', - 'subpackage_config = bob.extension.data.subpackage.config', - 'resource1 = bob.extension.data.resource_config2', - 'resource2 = bob.extension.data.resource_config2:b', + "bob.extension.test_config_load": [ + "basic_config = bob.extension.data.basic_config", + "verbose_config = bob.extension.data.verbose_config", + "resource_config = bob.extension.data.resource_config", + "subpackage_config = bob.extension.data.subpackage.config", + "resource1 = bob.extension.data.resource_config2", + "resource2 = bob.extension.data.resource_config2:b", ], - 'bob.extension.test_dump_config': [ - 'basic_config = bob.extension.data.basic_config', - 'resource_config = bob.extension.data.resource_config', - 'subpackage_config = bob.extension.data.subpackage.config', + "bob.extension.test_dump_config": [ + "basic_config = bob.extension.data.basic_config", + "resource_config = bob.extension.data.resource_config", + "subpackage_config = bob.extension.data.subpackage.config", ], }, classifiers=[ - 'Framework :: Bob', - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Framework :: Bob", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", ], )