From 076ea3db77c3b9638fa396b7f7be8086c303deba Mon Sep 17 00:00:00 2001 From: Michael Sargado Date: Thu, 31 Oct 2019 16:38:06 +0100 Subject: [PATCH] Added pycmake folder. --- python/pycmake/FindPythonModule.cmake | 168 ++++++++++++++++++++++++++ python/pycmake/PythonPackage.cmake | 93 ++++++++++++++ python/pycmake/pycmake_test_runner.py | 48 ++++++++ 3 files changed, 309 insertions(+) create mode 100644 python/pycmake/FindPythonModule.cmake create mode 100644 python/pycmake/PythonPackage.cmake create mode 100644 python/pycmake/pycmake_test_runner.py diff --git a/python/pycmake/FindPythonModule.cmake b/python/pycmake/FindPythonModule.cmake new file mode 100644 index 000000000..e3c830c38 --- /dev/null +++ b/python/pycmake/FindPythonModule.cmake @@ -0,0 +1,168 @@ +# Copyright (C) 2016 Statoil ASA, Norway. +# +# This file is part of ERT - Ensemble based Reservoir Tool. +# +# ERT is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# ERT is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. +# +# See the GNU General Public License at +# for more details + + + +# The basic assumption of this package is PEP 396 -- Module Version Numbers as +# layed out in https://www.python.org/dev/peps/pep-0396/ + +# Unfortunately, not all Python modules expose a version number, like inspect. +# Other Python modules expose several version numbers, e.g. one for the +# underlying software and one for the python packaging, like SQLite and PyQt. + +cmake_minimum_required (VERSION 2.8.1) + + + +# try import python module, if success, check its version, store as PY_module. +# the module is imported as-is, hence the case (e.g. PyQt4) must be correct. +# +# if given a second argument, the accessor, we call accessor on the module +# instead of the default __version__. +# +# (Yes, accessor could potentially be a function like "os.delete_everything()".) +macro(python_module_version module) + set(PY_VERSION_ACCESSOR "__version__") + set(PY_module_name ${module}) + + if(${PY_module_name} STREQUAL "PyQt4") + set(PY_module_name "PyQt4.Qt") + endif() + if(${PY_module_name} STREQUAL "PyQt4.Qt") + set(PY_VERSION_ACCESSOR "PYQT_VERSION_STR") + endif() + + if(${PY_module_name} STREQUAL "serial") + set(PY_VERSION_ACCESSOR "VERSION") + endif() + + if(${PY_module_name} STREQUAL "sqlite") + set(PY_VERSION_ACCESSOR "version") + endif() + + + # ARGUMENTS: module accessor + set (extra_macro_args ${ARGN}) + list(LENGTH extra_macro_args num_extra_args) + if (${num_extra_args} GREATER 0) + list(GET extra_macro_args 0 accessor) + set(PY_VERSION_ACCESSOR ${accessor}) + endif() + + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "# set module's version to py_mv and print it +import ${PY_module_name} as py_m +py_mv = '0.0.0' # output if no accessor is found +if hasattr(py_m, '${PY_VERSION_ACCESSOR}'): + py_mv = py_m.${PY_VERSION_ACCESSOR} +print(py_mv) +" + RESULT_VARIABLE _${module}_fail # error code 0 if module is importable + OUTPUT_VARIABLE _${module}_version # module.accessor or "0.0.0" if no such + ERROR_VARIABLE stderr_output + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT _${module}_fail) + set(PY_${module} ${_${module}_version}) + endif() + + # clean up + unset(PY_VERSION_ACCESSOR) + unset(PY_module_name) + unset(extra_macro_args) +endmacro() + + + +# If we find the correct module and new enough version, set PY_package, where +# "package" is the given argument to the version we found else, display warning +# and do not set any variables. +macro(python_module package) + + # ARGUMENTS: package package_req module_version version_req accessor + set (extra_macro_args ${ARGN}) + # Did we get any optional args? + list(LENGTH extra_macro_args num_extra_args) + if (${num_extra_args} GREATER 0) + list(GET extra_macro_args 0 package_req) + else() + set(package_req "REQUIRED") # requirement not set, is required + endif () + if (${num_extra_args} GREATER 1) + list(GET extra_macro_args 1 module_version) + else() + set(module_version "0.0.0") # module_version not set, 0.0.0 is ok + endif () + if (${num_extra_args} GREATER 2) + list(GET extra_macro_args 2 version_req) + else() + set(version_req "MINIMUM") # version requirement not set, is minimum + endif () + if (${num_extra_args} GREATER 3) + list(GET extra_macro_args 3 accessor) + endif () + + # Setting warning/error output level + set(PY_MSG_ERR SEND_ERROR) + set(PY_MSG_WARN WARNING) + if(${package_req} STREQUAL "QUIET") + set(PY_MSG_ERR STATUS) + set(PY_MSG_WARN STATUS) + endif() + + # We are done expanding the optional arguments + + python_module_version(${package} ${accessor}) + + # package not found in system + if(NOT DEFINED PY_${package}) + if(${package_req} STREQUAL "OPTIONAL") + message(${PY_MSG_WARN} "Could not find Python module " ${package}) + else() + message(${PY_MSG_ERR} "Could not find Python module " ${package}) + endif() + + else() + # package found in system + + if (${version_req} STREQUAL "EXACT" AND NOT ${PY_${package}} VERSION_EQUAL ${module_version}) + message(${PY_MSG_ERR} "Python module ${package} not exact. " + "Wanted EXACT ${module_version}, found ${PY_${package}}") + elseif (${version_req} STREQUAL "OPTIONAL" AND ${PY_${package}} VERSION_LESS ${module_version}) + message(${PY_MSG_WARN} "Python module ${package} too old. " + "Wanted ${module_version}, found ${PY_${package}}") + elseif (${version_req} STREQUAL "MINIMUM" AND ${PY_${package}} VERSION_LESS ${module_version}) + message(${PY_MSG_ERR} "Python module ${package} too old. " + "Wanted MINIMUM ${module_version}, found ${PY_${package}}") + else() + if(NOT DEFINED accessor) + message(STATUS "Found ${package}. " + "${PY_${package}} >= ${module_version}") + else() + message(STATUS "Found ${package}. " + "${PY_${package}} >= ${module_version} (" ${accessor} ")") + endif() + endif() + endif() + + # clean up + unset(package_req) + unset(module_version) + unset(version_req) + unset(accessor) + unset(extra_macro_args) + set(PY_MSG_ERR) + set(PY_MSG_WARN) +endmacro() diff --git a/python/pycmake/PythonPackage.cmake b/python/pycmake/PythonPackage.cmake new file mode 100644 index 000000000..b3826fbc0 --- /dev/null +++ b/python/pycmake/PythonPackage.cmake @@ -0,0 +1,93 @@ +# Copyright (C) 2016 Statoil ASA, Norway. +# +# pymake is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pymake is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. +# +# See the GNU General Public License at +# for more details + +# This module exports the following functions: +# * to_path_list(var path) +# which takes a list of paths and constructs a valid PYTHONPATH string, +# regardless of platform (path1:path2 for unix, path1;path2 for windows) +# +# * add_python_package(package_name package_path python_files) where +# which takes a package name, a path and the series of python files that +# makes that package. It exports the cmake target package_${package_name} +# in copies all ${python_files} sources to python/${package_path}, and +# sets up so you can install with `make install`. +# +# * add_python_test(testname python_test_file) +# which sets up a test target (using pycmake_test_runner.py, distributed +# with this module) and registeres it with ctest. +# +# * add_python_example(example testname test_file [args...]) +# which sets up an example program which will be run with the arguments +# [args...] (can be empty) Useful to make sure some program runs +# correctly with the given arguments, and which will report as a unit +# test failure. + +configure_file(${CMAKE_CURRENT_LIST_DIR}/pycmake_test_runner.py ${CMAKE_BINARY_DIR}/python/tests/pycmake_test_runner.py COPYONLY) + +function(to_path_list var path1) + if("${CMAKE_HOST_SYSTEM}" MATCHES ".*Windows.*") + set(sep "\\;") + else() + set(sep ":") + endif() + set(result "${path1}") # First element doesn't require separator at all... + foreach(path ${ARGN}) + set(result "${result}${sep}${path}") # .. but other elements do. + endforeach() + set(${var} "${result}" PARENT_SCOPE) +endfunction() + +if (EXISTS "/etc/debian_version") + set( PYTHON_PACKAGE_PATH "dist-packages") +else() + set( PYTHON_PACKAGE_PATH "site-packages") +endif() +set(PYTHON_INSTALL_PREFIX "lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/${PYTHON_PACKAGE_PATH}" CACHE STRING "Subdirectory to install Python modules in") + +function(add_python_package PACKAGE_NAME PACKAGE_PATH PYTHON_FILES) + add_custom_target(package_${PACKAGE_NAME} ALL) + + foreach (file ${PYTHON_FILES}) + add_custom_command(TARGET package_${PACKAGE_NAME} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/python/${PACKAGE_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${file} ${CMAKE_BINARY_DIR}/python/${PACKAGE_PATH} + ) + endforeach () + set_target_properties(package_${PACKAGE_NAME} PROPERTIES PACKAGE_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_PREFIX}/${PACKAGE_PATH}) + install(FILES ${PYTHON_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_PREFIX}/${PACKAGE_PATH}) +endfunction() + +function(add_python_test TESTNAME PYTHON_TEST_FILE arg) + configure_file(${PYTHON_TEST_FILE} ${PYTHON_TEST_FILE} COPYONLY) + + add_test(NAME ${TESTNAME} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python/tests + COMMAND python pycmake_test_runner.py ${PYTHON_TEST_FILE} ${arg} + ) + + to_path_list(pythonpath "${CMAKE_BINARY_DIR}/python" "$ENV{PYTHONPATH}") + set_tests_properties(${TESTNAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${pythonpath}") +endfunction() + +function(add_python_example TESTNAME PYTHON_TEST_FILE) + configure_file(${PYTHON_TEST_FILE} ${PYTHON_TEST_FILE} COPYONLY) + + add_test(NAME ${TESTNAME} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/examples + COMMAND python ${PYTHON_TEST_FILE} ${ARGN} + ) + to_path_list(pythonpath "${CMAKE_BINARY_DIR}/python" "$ENV{PYTHONPATH}") + set_tests_properties(${TESTNAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${pythonpath}") +endfunction() + diff --git a/python/pycmake/pycmake_test_runner.py b/python/pycmake/pycmake_test_runner.py new file mode 100644 index 000000000..539e08a22 --- /dev/null +++ b/python/pycmake/pycmake_test_runner.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +import inspect +import os +import sys + +import imp + +try: + from unittest2 import TextTestRunner, TestLoader, TestCase +except ImportError: + from unittest import TextTestRunner, TestLoader, TestCase + + +def runTestCase(tests, verbosity=0): + test_result = TextTestRunner(verbosity=verbosity).run(tests) + + if len(test_result.errors) or len(test_result.failures): + test_result.printErrors() + return False + else: + return True + +def getTestClassFromModule(module_path): + test_module = imp.load_source('test', module_path) + for name, obj in inspect.getmembers(test_module): + if inspect.isclass(obj) and issubclass(obj, TestCase) and not obj == TestCase: + return obj + +def getTestsFromModule(module_path): + klass = getTestClassFromModule(module_path) + if klass is None: + raise UserWarning("No tests classes found in: '%s'" % module_path) + + loader = TestLoader() + return loader.loadTestsFromTestCase(klass) + + +if __name__ == '__main__': + test_module = sys.argv[1] + argv = [] + + tests = getTestsFromModule(test_module) + + # Set verbosity to 2 to see which test method in a class that fails. + if runTestCase(tests, verbosity=0): + sys.exit(0) + else: + sys.exit(1)