diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 0000000..6a4917b --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,3 @@ +codecov: + notify: + after_n_builds: 5 diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000..8fb235d --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..491deae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..77681a6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,40 @@ +name: CI + +on: + workflow_dispatch: + inputs: + upload-wheel: + type: boolean + required: false + default: false + description: Upload wheel as an artifact + pull_request: + branches: [ main ] + push: + branches: [ main ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + uses: ./.github/workflows/step_test.yaml + + build-wheel: + uses: ./.github/workflows/step_build-wheel.yaml + needs: [ tests ] + with: + upload: ${{ inputs.upload-wheel || false }} + pass: + needs: [tests, build-wheel] + runs-on: ubuntu-latest + steps: + - name: Check all CI action + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + if: always() diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..343df84 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,52 @@ +name: Prepare release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" + workflow_dispatch: + inputs: + ref: + description: Tag to release + required: true + type: string + +permissions: + contents: read + +jobs: + tests: + uses: ./.github/workflows/step_test.yaml + build-wheel: + needs: [ tests ] + uses: ./.github/workflows/step_build-wheel.yaml + with: + ref: ${{ inputs.ref }} + upload_pypi: + name: Upload to PyPI repository + needs: [ tests, build-wheel ] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/click-option-group/ + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + release: + needs: [ upload_pypi ] + name: Create release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: softprops/action-gh-release@v1 + with: + name: click-option-group ${{ github.ref_name }} + prerelease: ${{ contains(github.ref, 'rc') }} + generate_release_notes: true diff --git a/.github/workflows/step_build-wheel.yaml b/.github/workflows/step_build-wheel.yaml new file mode 100644 index 0000000..b1cb720 --- /dev/null +++ b/.github/workflows/step_build-wheel.yaml @@ -0,0 +1,29 @@ +on: + workflow_call: + inputs: + upload: + description: Upload wheel as artifact + required: false + type: boolean + default: true + ref: + description: Tag to release + required: false + type: string + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - name: Build package + run: pipx run build + - uses: actions/upload-artifact@v3 + with: + path: dist/* + if: ${{ inputs.upload }} diff --git a/.github/workflows/step_test.yaml b/.github/workflows/step_test.yaml new file mode 100644 index 0000000..a9cd07d --- /dev/null +++ b/.github/workflows/step_test.yaml @@ -0,0 +1,45 @@ +on: + workflow_call: + +permissions: + contents: read + +jobs: + tests: + name: Check with Python ${{ matrix.python-version }} ${{ matrix.experimental && '(Experimental)' }} + needs: [ pre-commit ] + continue-on-error: ${{ matrix.experimental || false }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + include: + - python-version: "3.12" + experimental: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Install package + run: pip install -e .[test-cov] + - name: Test package + run: pytest --cov --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + name: python ${{ matrix.python-version }} + flags: python-${{ matrix.python-version }} + + pass: + needs: [ tests ] + runs-on: ubuntu-latest + steps: + - name: Check test jobs + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + if: always() diff --git a/.gitignore b/.gitignore index 7294e66..78e0626 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ docs/_build .tox _gitmsg.saved.txt +### Basic setups +cmake-build-* + +### Project specific +src/fypp/_version.py +CMakeUserPresets.json diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c85c8d8..2f96010 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,4 +18,6 @@ sphinx: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: docs/requirements.txt + - path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 595e383..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: python - -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "3.9" - -script: test/runtests.sh diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9309f4f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.15) +# CMake version compatibility +# TODO: Remove when cmake 3.25 is commonly distributed +if (POLICY CMP0140) + cmake_policy(SET CMP0140 NEW) +endif () + +#[==============================================================================================[ +# Basic project defintion # +]==============================================================================================] + +project(Fypp + VERSION 3.1.0 + LANGUAGES NONE) + +# Back-porting to PROJECT_IS_TOP_LEVEL to older cmake +# TODO: Remove when requiring cmake >= 3.21 +if (NOT DEFINED Fypp_IS_TOP_LEVEL) + if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + set(PROJECT_IS_TOP_LEVEL ON) + else () + set(PROJECT_IS_TOP_LEVEL OFF) + endif () +endif () + +#[==============================================================================================[ +# Options # +]==============================================================================================] + +option(FYPP_TESTS "Fypp: Build unit tests" ${PROJECT_IS_TOP_LEVEL}) +option(FYPP_INSTALL "Fypp: Install project" ${PROJECT_IS_TOP_LEVEL}) + +#[==============================================================================================[ +# Project configuration # +]==============================================================================================] + +# Include basic tools +if (FYPP_INSTALL) + include(CMakePackageConfigHelpers) + if (UNIX) + include(GNUInstallDirs) + endif () +endif () +list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) + +if (FYPP_TESTS) + enable_testing() + add_subdirectory(test) +endif () + +#[==============================================================================================[ +# Install or Export # +]==============================================================================================] +# Installation +if (FYPP_INSTALL) + # cmake export files + write_basic_package_version_file( + FyppConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMinorVersion + ARCH_INDEPENDENT) + configure_package_config_file( + cmake/FyppConfig.cmake.in + FyppConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/cmake/Fypp) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/FyppConfigVersion.cmake + ${CMAKE_CURRENT_BINARY_DIR}/FyppConfig.cmake + cmake/CMakeDetermineFyppCompiler.cmake + cmake/CMakeFyppCompiler.cmake.in + cmake/CMakeFyppInformation.cmake + cmake/CMakeTestFyppCompiler.cmake + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/cmake/Fypp) +endif () + +# Make project available for FetchContent +if (NOT PROJECT_IS_TOP_LEVEL) + # Set variables for FetchContent + # All variables have to be consistent with FyppConfig.cmake + # Propagate variables + if (CMAKE_VERSION VERSION_LESS 3.25) + # TODO: Remove when cmake 3.25 is commonly distributed + set(Fypp_VERSION ${Fypp_VERSION} PARENT_SCOPE) + set(Fypp_VERSION_MAJOR ${Fypp_VERSION_MAJOR} PARENT_SCOPE) + set(Fypp_VERSION_MINOR ${Fypp_VERSION_MINOR} PARENT_SCOPE) + set(Fypp_VERSION_PATCH ${Fypp_VERSION_PATCH} PARENT_SCOPE) + set(Fypp_VERSION_TWEAK ${Fypp_VERSION_TWEAK} PARENT_SCOPE) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} PARENT_SCOPE) + else () + return(PROPAGATE + Fypp_VERSION + Fypp_VERSION_MAJOR + Fypp_VERSION_MINOR + Fypp_VERSION_PATCH + Fypp_VERSION_TWEAK + CMAKE_MODULE_PATH + ) + endif () +endif () diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..3db1fed --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,12 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "include": [ + "cmake/CMakePresets-defaults.json", + "cmake/CMakePresets-CI.json" + ] +} diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ed630c2..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include bin/fypp -include LICENSE.txt -include CHANGELOG.rst -recursive-include test *.sh *.inc *.py -global-exclude *.pyc -global-exclude *~ diff --git a/cmake/CMakePresets-CI.json b/cmake/CMakePresets-CI.json new file mode 100644 index 0000000..04b3580 --- /dev/null +++ b/cmake/CMakePresets-CI.json @@ -0,0 +1,193 @@ +{ + "version": 6, + "include": [ + "CMakePresets-defaults.json" + ], + "configurePresets": [ + { + "name": "ci-base", + "hidden": true, + "generator": "Ninja", + "inherits": [ + "default" + ], + "cacheVariables": { + "FYPP_TESTS": { + "type": "BOOL", + "value": true + } + }, + "errors": { + "deprecated": true + } + }, + { + "name": "gcc-ci", + "displayName": "Configure preset for GCC toolchain", + "inherits": [ + "ci-base" + ], + "binaryDir": "cmake-build-release-gcc", + "cacheVariables": { + "CMAKE_Fortran_COMPILER": { + "type": "FILEPATH", + "value": "gfortran" + } + } + }, + { + "name": "intel-ci", + "displayName": "Configure preset for Intel toolchain", + "inherits": [ + "ci-base" + ], + "binaryDir": "cmake-build-release-intel", + "cacheVariables": { + "CMAKE_Fortran_COMPILER": { + "type": "FILEPATH", + "value": "ifx" + } + } + }, + { + "name": "llvm-ci", + "displayName": "Configure preset for LLVM (Clang, Flang) toolchain", + "inherits": [ + "ci-base" + ], + "binaryDir": "cmake-build-release-llvm", + "cacheVariables": { + "CMAKE_Fortran_COMPILER": { + "type": "FILEPATH", + "value": "fortran-new" + } + } + } + ], + "buildPresets": [ + { + "name": "ci-base", + "hidden": true, + "inherits": [ + "default" + ], + "cleanFirst": true + }, + { + "name": "gcc-ci", + "displayName": "Build preset for GCC toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "gcc-ci" + }, + { + "name": "intel-ci", + "displayName": "Build preset for Intel toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "intel-ci" + }, + { + "name": "llvm-ci", + "displayName": "Build preset for LLVM (Clang, Flang) toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "llvm-ci" + } + ], + "testPresets": [ + { + "name": "ci-base", + "hidden": true, + "inherits": [ + "default" + ], + "output": { + "outputOnFailure": true + } + }, + { + "name": "gcc-ci", + "displayName": "Test preset for GCC toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "gcc-ci" + }, + { + "name": "intel-ci", + "displayName": "Test preset for Intel toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "intel-ci" + }, + { + "name": "llvm-ci", + "displayName": "Test preset for LLVM (Clang, Flang) toolchain", + "inherits": [ + "ci-base" + ], + "configurePreset": "llvm-ci" + } + ], + "workflowPresets": [ + { + "name": "gcc-ci", + "displayName": "CI test for GCC toolchain", + "steps": [ + { + "type": "configure", + "name": "gcc-ci" + }, + { + "type": "build", + "name": "gcc-ci" + }, + { + "type": "test", + "name": "gcc-ci" + } + ] + }, + { + "name": "intel-ci", + "displayName": "CI test for Intel toolchain", + "steps": [ + { + "type": "configure", + "name": "intel-ci" + }, + { + "type": "build", + "name": "intel-ci" + }, + { + "type": "test", + "name": "intel-ci" + } + ] + }, + { + "name": "llvm-ci", + "displayName": "CI test for LLVM (Clang, Flang) toolchain", + "steps": [ + { + "type": "configure", + "name": "llvm-ci" + }, + { + "type": "build", + "name": "llvm-ci" + }, + { + "type": "test", + "name": "llvm-ci" + } + ] + } + ] +} diff --git a/cmake/CMakePresets-defaults.json b/cmake/CMakePresets-defaults.json new file mode 100644 index 0000000..bfc642c --- /dev/null +++ b/cmake/CMakePresets-defaults.json @@ -0,0 +1,50 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "default", + "displayName": "Default configuration preset", + "binaryDir": "cmake-build-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": { + "type": "STRING", + "value": "Release" + } + } + } + ], + "buildPresets": [ + { + "name": "default", + "displayName": "Default build preset", + "configurePreset": "default" + } + ], + "testPresets": [ + { + "name": "default", + "displayName": "Default test preset", + "configurePreset": "default" + } + ], + "workflowPresets": [ + { + "name": "default", + "displayName": "Default workflow", + "steps": [ + { + "type": "configure", + "name": "default" + }, + { + "type": "build", + "name": "default" + }, + { + "type": "test", + "name": "default" + } + ] + } + ] +} diff --git a/cmake/Fypp.cmake b/cmake/Fypp.cmake new file mode 100644 index 0000000..9cf0157 --- /dev/null +++ b/cmake/Fypp.cmake @@ -0,0 +1,319 @@ +include_guard(GLOBAL) +cmake_minimum_required(VERSION 3.20) +# CMake version compatibility +# TODO: Remove when cmake 3.25 is commonly distributed +if (POLICY CMP0140) + cmake_policy(SET CMP0140 NEW) +endif () +if (POLICY CMP0118) + cmake_policy(SET CMP0118 NEW) +endif () + +#[==============================================================================================[ +# Preparations # +]==============================================================================================] + +# Find the appropriate fypp executable +find_package(Python3 REQUIRED) +cmake_path(GET Python3_EXECUTABLE PARENT_PATH Python3_BINDIR) +find_program( + Fypp_EXECUTABLE + NAMES fypp + HINTS "${Python3_BINDIR}" + DOC "Fypp preprocessor" +) +mark_as_advanced(Fypp_EXECUTABLE) + +#[==============================================================================================[ +# Main interface # +]==============================================================================================] + +function(Fypp_target_sources target) + #[===[ + # Fypp_target_sources + + Equivalent to `target_sources`. Separates the fypp file sources from the non-fypp files, passing the latter + directly to the `target_sources`. Does not support `FILE_SET` interface yet. + + This is the preferred interface format over `Fypp_add_library` and `Fypp_add_executable`. + + ## Notes + + - Due to cmake limitations only the target properties derived up to the point of this function call are parsed, + including those defined with `target_compile_definitions`. + [Upstream discussion](https://discourse.cmake.org/t/add-custom-command-with-target-properties-at-build-time/8464) + + ## Synopsis + ```cmake + ``` + TODO: Documentation + ]===] + + set(ARGS_Options "") + set(ARGS_OneValue "FILE_SET") + set(ARGS_MultiValue "INTERFACE;PUBLIC;PRIVATE") + cmake_parse_arguments(PARSE_ARGV 1 ARGS "${ARGS_Options}" "${ARGS_OneValue}" "${ARGS_MultiValue}") + + if (DEFINED ARGS_FILE_SET) + # FILE_SET parsing is not yet supported + # TODO: Figure out how to parse FILE_SET + message(FATAL_ERROR + "Fypp: FILE_SET is not yet supported in Fypp_target_sources") + endif () + + # Without FILE_SET, all values in ARGS_INTERFACE/PUBLIC/PRIVATE are source files + # Forward the files to _Fypp_add_source + foreach (type IN ITEMS INTERFACE PUBLIC PRIVATE) + if (DEFINED ARGS_${type}) + get_Fypp_sources(FYPP_SOURCES_VAR fypp_sources OTHER_SOURCES_VAR other_sources + SOURCES ${ARGS_${type}}) + target_sources(${target} ${type} ${other_sources}) + _Fypp_add_source(${target} ${type} ${fypp_sources}) + endif () + endforeach () +endfunction() + +function(Fypp_add_library name) + #[===[ + # Fypp_add_library + + Equivalent to `add_library( [])` and `Fypp_target_sources( PRIVATE )` + + ## Synopsis + ```cmake + ``` + TODO: Documentation + ]===] + set(ARGS_Options "STATIC;SHARED;OBJECT;MODULE;EXCLUDE_FROM_ALL;IMPORTED;ALIAS") + set(ARGS_OneValue "") + set(ARGS_MultiValue "") + cmake_parse_arguments(PARSE_ARGV 1 ARGS "${ARGS_Options}" "${ARGS_OneValue}" "${ARGS_MultiValue}") + + if (ARGS_IMPORTED OR ARGS_ALIAS) + # IMPORTED and ALIAS library types are ill-defined with Fypp + message(FATAL_ERROR + "Fypp: IMPORTED and ALIAS library types are ill-defined as Fypp libraries") + endif () + + # Gather the add_library inputs + # Input sanitization will be done by base add_library + set(add_library_inputs "") + if (ARGS_STATIC) + list(APPEND add_library_inputs STATIC) + endif () + if (ARGS_SHARED) + list(APPEND add_library_inputs SHARED) + endif () + if (ARGS_MODULE) + list(APPEND add_library_inputs MODULE) + endif () + if (ARGS_OBJECT) + list(APPEND add_library_inputs OBJECT) + endif () + if (ARGS_EXCLUDE_FROM_ALL) + list(APPEND add_library_inputs EXCLUDE_FROM_ALL) + endif () + + # Create the base library target + add_library(${name} ${add_library_inputs}) + + # All other arguments should be source files + if (DEFINED ARGS_UNPARSED_ARGUMENTS) + Fypp_target_sources(${name} PRIVATE ${ARGS_UNPARSED_ARGUMENTS}) + endif () +endfunction() + +function(Fypp_add_executable name) + #[===[ + # Fypp_add_executable + + Equivalent to `add_executable()` and `Fypp_target_sources( PRIVATE )` + + ## Synopsis + ```cmake + ``` + TODO: Documentation + ]===] + set(ARGS_Options "WIN32;MACOSX_BUNDLE;EXCLUDE_FROM_ALL;IMPORTED;ALIAS") + set(ARGS_OneValue "") + set(ARGS_MultiValue "") + cmake_parse_arguments(PARSE_ARGV 1 ARGS "${ARGS_Options}" "${ARGS_OneValue}" "${ARGS_MultiValue}") + + if (ARGS_IMPORTED OR ARGS_ALIAS) + # IMPORTED and ALIAS library types are ill-defined with Fypp + message(FATAL_ERROR + "Fypp: IMPORTED and ALIAS library types are ill-defined as Fypp libraries") + endif () + + # Gather the add_executable inputs + # Input sanitization will be done by base add_executable + set(add_executable_inputs "") + if (ARGS_WIN32) + list(APPEND add_executable_inputs WIN32) + endif () + if (ARGS_MACOSX_BUNDLE) + list(APPEND add_executable_inputs MACOSX_BUNDLE) + endif () + if (ARGS_EXCLUDE_FROM_ALL) + list(APPEND add_executable_inputs EXCLUDE_FROM_ALL) + endif () + + # Create the base executable target + add_executable(${name} ${add_executable_inputs}) + + # All other arguments should be source files + if (DEFINED ARGS_UNPARSED_ARGUMENTS) + Fypp_target_sources(${name} PRIVATE ${ARGS_UNPARSED_ARGUMENTS}) + endif () +endfunction() + +#[==============================================================================================[ +# Auxiliary interface # +]==============================================================================================] + +function(get_Fypp_sources) + #[===[ + # get_Fypp_sources + + Separate the fypp files from the other source files + + ## Synopsis + ```cmake + ``` + TODO: Documentation + ]===] + set(ARGS_Options "") + set(ARGS_OneValue "FYPP_SOURCES_VAR;OTHER_SOURCES_VAR") + set(ARGS_MultiValue "SOURCES") + cmake_parse_arguments(PARSE_ARGV 0 ARGS "${ARGS_Options}" "${ARGS_OneValue}" "${ARGS_MultiValue}") + + if (NOT DEFINED ARGS_FYPP_SOURCES_VAR) + message(FATAL_ERROR + "Fypp: FYPP_SOURCES_VAR is required for get_Fypp_sources() function") + endif () + if (NOT DEFINED ARGS_SOURCES) + message(FATAL_ERROR + "Fypp: SOURCES is required for get_Fypp_sources() function") + endif () + + # Initialize the output variables to empty + set(${ARGS_FYPP_SOURCES_VAR} "") + if (DEFINED ARGS_OTHER_SOURCES_VAR) + set(${ARGS_OTHER_SOURCES_VAR} "") + endif () + + # Loop through the sources and check the extension + foreach (source IN LISTS ARGS_SOURCES) + cmake_path(GET source EXTENSION LAST_ONLY source_ext) + if (source_ext MATCHES fypp|fpp|FYPP|FPP) + list(APPEND ${ARGS_FYPP_SOURCES_VAR} ${source}) + elseif (DEFINED ARGS_OTHER_SOURCES_VAR) + list(APPEND ${ARGS_OTHER_SOURCES_VAR} ${source}) + endif () + endforeach () + + # Return the values to the caller + if (CMAKE_VERSION VERSION_LESS 3.25) + # TODO: Remove when cmake 3.25 is commonly distributed + set(${ARGS_FYPP_SOURCES_VAR} ${${ARGS_FYPP_SOURCES_VAR}} PARENT_SCOPE) + if (DEFINED ARGS_OTHER_SOURCES_VAR) + set(${ARGS_OTHER_SOURCES_VAR} ${${ARGS_OTHER_SOURCES_VAR}} PARENT_SCOPE) + endif () + else () + return(PROPAGATE + ${ARGS_FYPP_SOURCES_VAR} + # TODO: Does this one work when none is defined? + ${ARGS_OTHER_SOURCES_VAR} + ) + endif () +endfunction() + +#[==============================================================================================[ +# Private interface # +]==============================================================================================] + +# Main implementation +function(_Fypp_add_source target type) + #[===[ + # _Fypp_add_source + + Main implementation of the fypp wrapper. + Adds fypp generated source to target with appropriate add_custom_command dependencies + ]===] + + # Early return if no sources are passed + if (NOT ARGN) + return() + endif () + + # Create a pseudo file with the fypp metadata + get_property(target_bindir TARGET ${target} + PROPERTY BINARY_DIR) + # TODO: Get the appropriate path to `target.dir` folder + # TODO: back-port cmake_path or convert to get_filename_component + cmake_path(APPEND target_bindir CMakeFiles ${target}.dir OUTPUT_VARIABLE target_cmake_bindir) + cmake_path(APPEND target_cmake_bindir fypp_files OUTPUT_VARIABLE target_fypp_files) + if (NOT EXISTS ${target_fypp_files}) + # Create the main custom_command with appropriate COMMENT + # TODO: Add depfile support + add_custom_command(OUTPUT ${target_fypp_files} + # Make sure all the parent directories exist + COMMAND ${CMAKE_COMMAND} -E make_directory ${target_cmake_bindir} + # Initialize the file fypp_files with the name of the target + COMMAND ${CMAKE_COMMAND} -E echo "${target}:" ${target_fypp_files} + COMMENT "Parsing the fypp files for ${target} target" + WORKING_DIRECTORY ${target_bindir} + ) + endif () + + # Get the target properties to forward them to fypp + # Note: This does not pick up the final properties used at build time or genex + # https://discourse.cmake.org/t/add-custom-command-with-target-properties-at-build-time/8464 + get_property(target_defines TARGET ${target} + PROPERTY COMPILE_DEFINITIONS) + get_property(target_includes TARGET ${target} + PROPERTY INCLUDE_DIRECTORIES) + # Normalize the properties to fypp format + set(define_flags "") + foreach (define IN LISTS target_defines) +# string(REGEX REPLACE "=(.*)" "=\"\\1\"" define_sanitized ${define}) +# list(APPEND define_flags "-D${define_sanitized}") + string(REGEX REPLACE "=(.*)" "=\"\\1\"" define_sanitized ${define}) + list(APPEND define_flags "-D${define}") + endforeach () + set(include_flags "") + foreach (include IN LISTS target_includes) + list(APPEND include_flags "-I${include}") + endforeach () + + set(fypp_compiled_files "") + # Add each fypp file to add_custom_command + foreach (source IN LISTS ARGN) + # Get the absolute path of the source file + if (IS_ABSOLUTE source) + cmake_path(NATIVE_PATH source NORMALIZE source_path) + else () + cmake_path(ABSOLUTE_PATH source NORMALIZE OUTPUT_VARIABLE source_path_abs) + cmake_path(NATIVE_PATH source_path_abs NORMALIZE source_path) + endif () + # Create a variable for the output file + cmake_path(RELATIVE_PATH source_path BASE_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE rel_path) + cmake_path(APPEND target_cmake_bindir ${rel_path}.f90 OUTPUT_VARIABLE out_file) + cmake_path(NATIVE_PATH out_file NORMALIZE out_path) + # Append fypp command to the main target custom_command + add_custom_command(OUTPUT ${out_file} + # Append the name of the generated files to the fypp_files + # TODO: This might duplicate the file names when dependency is rerun + COMMAND ${CMAKE_COMMAND} -E echo ${out_file} >> ${target_fypp_files} + COMMAND ${Fypp_EXECUTABLE} -p ${define_flags} ${include_flags} ${source_path} ${out_path} + DEPENDS ${source} + COMMENT "Adding file ${out_file}") + set_source_files_properties(${out_file} PROPERTIES + GENERATED True) + list(APPEND fypp_compiled_files ${out_file}) + endforeach () + # Finally add the generated source to the target + # TODO: Sanitize the output files better to not duplicate folder structure + target_sources(${target} ${type} ${fypp_compiled_files}) +endfunction() diff --git a/cmake/FyppConfig.cmake.in b/cmake/FyppConfig.cmake.in new file mode 100644 index 0000000..dd37934 --- /dev/null +++ b/cmake/FyppConfig.cmake.in @@ -0,0 +1,4 @@ +@PACKAGE_INIT@ + +list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) +include(Fypp) diff --git a/docs/fypp.rst b/docs/fypp.rst index a35576b..8b57100 100644 --- a/docs/fypp.rst +++ b/docs/fypp.rst @@ -2145,10 +2145,10 @@ Fypp :members: -FyppOptions +FyppDefaults =========== -.. autoclass:: FyppOptions +.. autoclass:: FyppDefaults :members: diff --git a/docs/requirements.in b/docs/requirements.in deleted file mode 100644 index 7c475e9..0000000 --- a/docs/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -sphinx==6.2.1 -sphinx-rtd-theme diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 89b855e..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,57 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile requirements.in -# -alabaster==0.7.13 - # via sphinx -babel==2.12.1 - # via sphinx -certifi==2023.7.22 - # via requests -charset-normalizer==3.2.0 - # via requests -docutils==0.18.1 - # via - # sphinx - # sphinx-rtd-theme -idna==3.4 - # via requests -imagesize==1.4.1 - # via sphinx -jinja2==3.1.2 - # via sphinx -markupsafe==2.1.3 - # via jinja2 -packaging==23.1 - # via sphinx -pygments==2.15.1 - # via sphinx -requests==2.31.0 - # via sphinx -snowballstemmer==2.2.0 - # via sphinx -sphinx==6.2.1 - # via - # -r requirements.in - # sphinx-rtd-theme - # sphinxcontrib-jquery -sphinx-rtd-theme==1.2.2 - # via -r requirements.in -sphinxcontrib-applehelp==1.0.4 - # via sphinx -sphinxcontrib-devhelp==1.0.2 - # via sphinx -sphinxcontrib-htmlhelp==2.0.1 - # via sphinx -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==1.0.3 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 - # via sphinx -urllib3==2.0.4 - # via requests diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3199392 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "fypp" +description = "Python powered Fortran preprocessor" +dynamic = ["version"] +authors = [ + { name = "Bálint Aradi", email = "aradi@uni-bremen.de" }, +] +requires-python = ">=3.5" +license = { text = "BSD-2-Clause" } +license-files = { paths = ["LICENSE.txt"] } +readme = "README.rst" + +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Topic :: Software Development :: Code Generators', + 'Topic :: Software Development :: Pre-processors', + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +keywords=[ + "fortran", + "metaprogramming", + "pre-processor", +] + +[project.urls] +homepage = "https://github.com/aradi/fypp" +documentation = "https://fypp.readthedocs.io/" +repository = "https://github.com/aradi/fypp" + +[project.scripts] +fypp = "fypp.cli:run_fypp" + +[project.optional-dependencies] +test = [ + "pytest", +] +test-cov = [ + "fypp[test]", + "pytest-cov", +] +docs = [ + "sphinx", + "sphinx-rtd-theme", +] +dev = [ + "fypp[test]", +] + +[tool.hatch] +version.source = "vcs" +build.hooks.vcs.version-file = "src/fypp/_version.py" + +[tool.pytest.ini_options] +testpaths = ["test"] +python_files = "test_*.py" + +[tool.tox] +legacy_tox_ini = """ + [tox] + envlist = py34, py35, py36, py37, py38, py39 + + [testenv] + skip_missing_interpreters = + true + setenv = + PYTHONPATH = {toxinidir}/src + changedir=test + commands=python test_fypp.py +""" diff --git a/setup.py b/setup.py deleted file mode 100644 index ff587cd..0000000 --- a/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -from setuptools import setup -from codecs import open -from os import path - -here = path.abspath(path.dirname(__file__)) - -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -setup( - name='fypp', - - version='3.2', - - description='Python powered Fortran preprocessor', - long_description=long_description, - - url='https://github.com/aradi/fypp', - - author='Bálint Aradi', - author_email='aradi@uni-bremen.de', - - license='BSD', - - classifiers=[ - 'Development Status :: 5 - Production/Stable', - - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Topic :: Software Development :: Code Generators', - 'Topic :: Software Development :: Pre-processors', - - 'License :: OSI Approved :: BSD License', - - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - - keywords='fortran metaprogramming pre-processor', - - package_dir={'': 'src'}, - py_modules=['fypp'], - - entry_points={ - 'console_scripts': [ - 'fypp=fypp:run_fypp', - ], - }, -) diff --git a/src/fypp.py b/src/fypp.py deleted file mode 120000 index fea8343..0000000 --- a/src/fypp.py +++ /dev/null @@ -1 +0,0 @@ -../bin/fypp \ No newline at end of file diff --git a/src/fypp/__init__.py b/src/fypp/__init__.py new file mode 100644 index 0000000..308f088 --- /dev/null +++ b/src/fypp/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from ._version import version as __version__ +from .fypp import Fypp + +__all__ = ["__version__", "Fypp"] diff --git a/src/fypp/__main__.py b/src/fypp/__main__.py new file mode 100644 index 0000000..de33fa1 --- /dev/null +++ b/src/fypp/__main__.py @@ -0,0 +1,3 @@ +from .cli import run_fypp + +run_fypp() diff --git a/src/fypp/cli.py b/src/fypp/cli.py new file mode 100644 index 0000000..8da7f15 --- /dev/null +++ b/src/fypp/cli.py @@ -0,0 +1,161 @@ +import argparse +import sys +from .fypp import FyppDefaults, Fypp, FyppStopRequest, FyppFatalError, _formatted_exception, USER_ERROR_EXIT_CODE, \ + ERROR_EXIT_CODE +from ._version import __version__ + + +def get_option_parser() -> argparse.ArgumentParser: + """ + Returns an option parser for the Fypp command line tool. + + :return: Parser which can create an optparse.Values object with + Fypp settings based on command line arguments. + """ + defs = FyppDefaults() + parser = argparse.ArgumentParser( + prog="fypp", + description=""" + Preprocesses source code with Fypp directives. The input is + read from INFILE (default: \'-\', stdin) and written to + OUTFILE (default: \'-\', stdout). + """) + parser.add_argument('--version', action='version', version=__version__) + + parser.add_argument('-D', '--define', action='append', dest='defines', + metavar='VAR[=VALUE]', default=defs.defines, + help=""" + define variable, equivalent to the -D pre-processor flags. Depending on + the value of -d/--define-value-type, the value is either a string + or a Python expression + """) + parser.add_argument('-P', '--python-define', action='append', dest='python_defines', + metavar='VAR[=VALUE]', default=defs.defines, + help=""" + equivalent to -D/--define, but interprets the string value as + a Python expression + """) + parser.add_argument('-d', '--define-value-type', dest='define_type', + choices=['str', 'python'], default=defs.define_type, + help=""" + whether to parse -D/--define values as string or Python expressions. + Default is 'python' to preserve backwards compatibility, but **NOTE**, + this will be reversed in 4.0 and later. + """) + + parser.add_argument('-I', '--include', action='append', dest='includes', + metavar='INCDIR', default=defs.includes, + help=""" + add directory to the search paths for include files + """) + parser.add_argument('-m', '--module', action='append', dest='modules', + metavar='MOD', default=defs.modules, + help=""" + import a python module at startup (import only trustworthy modules + as they have access to an **unrestricted** Python environment!) + """) + + parser.add_argument('-M', '--module-dir', action='append', + dest='moduledirs', metavar='MODDIR', + default=defs.moduledirs, + help=""" + directory to be searched for user imported modules before + looking up standard locations in sys.path + """) + + parser.add_argument('-n', '--line-numbering', action='store_true', + dest='line_numbering', default=defs.line_numbering, + help=""" + emit line numbering markers + """) + parser.add_argument('-N', '--line-numbering-mode', metavar='MODE', + choices=['full', 'nocontlines'], + default=defs.line_numbering_mode, + dest='line_numbering_mode', + help=""" + line numbering mode, 'full' (default): line numbering + markers generated whenever source and output lines are out + of sync, 'nocontlines': line numbering markers omitted + for continuation lines + """) + parser.add_argument('--line-marker-format', metavar='FMT', + choices=['cpp', 'gfortran5', 'std'], + dest='line_marker_format', + default=defs.line_marker_format, + help=""" + line numbering marker format, currently 'std', 'cpp' and + 'gfortran5' are supported, where 'std' emits #line pragmas + similar to standard tools, 'cpp' produces line directives as + emitted by GNU cpp, and 'gfortran5' cpp line directives with a + workaround for a bug introduced in GFortran 5. Default: 'cpp'. + """) + parser.add_argument('-l', '--line-length', type=int, metavar='LEN', + dest='line_length', default=defs.line_length, + help=""" + maximal line length (default: 132), lines modified by the + preprocessor are folded if becoming longer + """) + parser.add_argument('-f', '--folding-mode', metavar='MODE', + choices=['smart', 'simple', 'brute'], dest='folding_mode', + default=defs.folding_mode, + help=""" + line folding mode, 'smart' (default): indentation context + and whitespace aware, 'simple': indentation context aware, + 'brute': mechnical folding + """) + + parser.add_argument('-F', '--no-folding', action='store_true', + dest='no_folding', default=defs.no_folding, + help=""" + suppress line folding + """) + parser.add_argument('--indentation', type=int, metavar='IND', + dest='indentation', default=defs.indentation, + help=""" + indentation to use for continuation lines (default 4) + """) + parser.add_argument('--fixed-format', action='store_true', + dest='fixed_format', default=defs.fixed_format, + help=""" + produce fixed format output (any settings for options + --line-length, --folding-method and --indentation are ignored) + """) + parser.add_argument('--encoding', metavar='ENC', default=defs.encoding, + help=""" + character encoding for reading/writing files. Default: 'utf-8'. + Note: reading from stdin and writing to stdout is encoded + according to the current locale and is not affected by this setting. + """) + parser.add_argument('-p', '--create-parents', action='store_true', + dest='create_parent_folder', + default=defs.create_parent_folder, + help=""" + create parent folders of the output file if they do not exist + """) + parser.add_argument('--file-var-root', metavar='DIR', + dest='file_var_root', + default=defs.file_var_root, + help=""" + in variables _FILE_ and _THIS_FILE_, use relative paths with DIR + as root directory. Note: the input file and all included files + must be in DIR or in a directory below. + """) + + return parser + + +def run_fypp(): + """Run the Fypp command line tool.""" + parser = get_option_parser() + opts, leftover = parser.parse_known_args() + infile = leftover[0] if len(leftover) > 0 else '-' + outfile = leftover[1] if len(leftover) > 1 else '-' + try: + tool = Fypp(opts) + tool.process_file(infile, outfile) + except FyppStopRequest as exc: + sys.stderr.write(_formatted_exception(exc)) + sys.exit(USER_ERROR_EXIT_CODE) + except FyppFatalError as exc: + sys.stderr.write(_formatted_exception(exc)) + sys.exit(ERROR_EXIT_CODE) diff --git a/bin/fypp b/src/fypp/fypp.py similarity index 93% rename from bin/fypp rename to src/fypp/fypp.py index 4207219..f7263dc 100755 --- a/bin/fypp +++ b/src/fypp/fypp.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- ################################################################################ # # fypp -- Python powered Fortran preprocessor @@ -37,7 +35,7 @@ * `Fypp`_: The actual Fypp preprocessor. It returns for a given input the preprocessed output. -* `FyppOptions`_: Contains customizable settings controlling the behaviour of +* `FyppDefaults`_: Contains customizable settings controlling the behaviour of `Fypp`_. Alternatively, the function `get_option_parser()`_ can be used to obtain an option parser, which can create settings based on command line arguments. @@ -62,12 +60,11 @@ import io import platform import builtins +import warnings # Prevent cluttering user directory with Python bytecode sys.dont_write_bytecode = True -VERSION = '3.2' - STDIN = '' FILEOBJ = '' @@ -2435,10 +2432,10 @@ class Fypp: tool = fypp.Fypp() output = tool.process_text('#:if DEBUG > 0\\nprint *, "DEBUG"\\n#:endif\\n') - If you want to fine tune Fypps behaviour, pass a customized `FyppOptions`_ + If you want to fine tune Fypps behaviour, pass a customized `FyppDefaults`_ instance at initialization:: - options = fypp.FyppOptions() + options = fypp.FyppDefaults() options.fixed_format = True tool = fypp.Fypp(options) @@ -2476,9 +2473,9 @@ def __init__(self): Args: options (object): Object containing the settings for Fypp. You typically - would pass a customized `FyppOptions`_ instance or an + would pass a customized `FyppDefaults`_ instance or an ``optparse.Values`` object as returned by the option parser. If not - present, the default settings in `FyppOptions`_ are used. + present, the default settings in `FyppDefaults`_ are used. evaluator_factory (function): Factory function that returns an Evaluator object. Its call signature must match that of the Evaluator constructor. If not present, ``Evaluator`` is used. @@ -2499,7 +2496,7 @@ def __init__(self, options=None, evaluator_factory=Evaluator, syspath = self._get_syspath_without_scriptdir() self._adjust_syspath(syspath) if options is None: - options = FyppOptions() + options = FyppDefaults() if inspect.signature(evaluator_factory) == inspect.signature(Evaluator): evaluator = evaluator_factory() else: @@ -2509,7 +2506,10 @@ def __init__(self, options=None, evaluator_factory=Evaluator, self._import_modules(options.modules, evaluator, syspath, options.moduledirs) if options.defines: - self._apply_definitions(options.defines, evaluator) + # For normal text reuse the evaluator, but interpret the values as string + self._apply_python_definitions(options.defines, evaluator, as_string=True) + if options.python_defines: + self._apply_python_definitions(options.python_defines, evaluator) if inspect.signature(parser_factory) == inspect.signature(Parser): parser = parser_factory(includedirs=options.includes, encoding=self._encoding) @@ -2588,18 +2588,21 @@ def process_text(self, txt): @staticmethod - def _apply_definitions(defines, evaluator): + def _apply_python_definitions(defines, evaluator, as_string=False): for define in defines: words = define.split('=', 1) name = words[0] value = None if len(words) > 1: - try: - value = evaluator.evaluate(words[1]) - except Exception as exc: - msg = "exception at evaluating '{0}' in definition for " \ - "'{1}'".format(words[1], name) - raise FyppFatalError(msg) from exc + if as_string: + value = words[1] + else: + try: + value = evaluator.evaluate(words[1]) + except Exception as exc: + msg = "exception at evaluating '{0}' in definition for " \ + "'{1}'".format(words[1], name) + raise FyppFatalError(msg) from exc evaluator.define(name, value) @@ -2630,7 +2633,7 @@ def _adjust_syspath(syspath): sys.path = syspath -class FyppOptions(optparse.Values): +class FyppDefaults: '''Container for Fypp options with default values. @@ -2671,6 +2674,8 @@ class FyppOptions(optparse.Values): def __init__(self): optparse.Values.__init__(self) self.defines = [] + self.python_defines = [] + self.define_type = 'python' self.includes = [] self.line_numbering = False self.line_numbering_mode = 'full' @@ -2807,131 +2812,6 @@ def __call__(self, line): return [line] -def get_option_parser(): - '''Returns an option parser for the Fypp command line tool. - - Returns: - OptionParser: Parser which can create an optparse.Values object with - Fypp settings based on command line arguments. - ''' - defs = FyppOptions() - fypp_name = 'fypp' - fypp_desc = 'Preprocesses source code with Fypp directives. The input is '\ - 'read from INFILE (default: \'-\', stdin) and written to '\ - 'OUTFILE (default: \'-\', stdout).' - fypp_version = fypp_name + ' ' + VERSION - usage = '%prog [options] [INFILE] [OUTFILE]' - parser = optparse.OptionParser(prog=fypp_name, description=fypp_desc, - version=fypp_version, usage=usage) - - msg = 'define variable, value is interpreted as ' \ - 'Python expression (e.g \'-DDEBUG=1\' sets DEBUG to the ' \ - 'integer 1) or set to None if omitted' - parser.add_option('-D', '--define', action='append', dest='defines', - metavar='VAR[=VALUE]', default=defs.defines, help=msg) - - msg = 'add directory to the search paths for include files' - parser.add_option('-I', '--include', action='append', dest='includes', - metavar='INCDIR', default=defs.includes, help=msg) - - msg = 'import a python module at startup (import only trustworthy modules '\ - 'as they have access to an **unrestricted** Python environment!)' - parser.add_option('-m', '--module', action='append', dest='modules', - metavar='MOD', default=defs.modules, help=msg) - - msg = 'directory to be searched for user imported modules before '\ - 'looking up standard locations in sys.path' - parser.add_option('-M', '--module-dir', action='append', - dest='moduledirs', metavar='MODDIR', - default=defs.moduledirs, help=msg) - - msg = 'emit line numbering markers' - parser.add_option('-n', '--line-numbering', action='store_true', - dest='line_numbering', default=defs.line_numbering, - help=msg) - - msg = 'line numbering mode, \'full\' (default): line numbering '\ - 'markers generated whenever source and output lines are out '\ - 'of sync, \'nocontlines\': line numbering markers omitted '\ - 'for continuation lines' - parser.add_option('-N', '--line-numbering-mode', metavar='MODE', - choices=['full', 'nocontlines'], - default=defs.line_numbering_mode, - dest='line_numbering_mode', help=msg) - - msg = 'line numbering marker format, currently \'std\', \'cpp\' and '\ - '\'gfortran5\' are supported, where \'std\' emits #line pragmas '\ - 'similar to standard tools, \'cpp\' produces line directives as '\ - 'emitted by GNU cpp, and \'gfortran5\' cpp line directives with a '\ - 'workaround for a bug introduced in GFortran 5. Default: \'cpp\'.' - parser.add_option('--line-marker-format', metavar='FMT', - choices=['cpp', 'gfortran5', 'std'], - dest='line_marker_format', - default=defs.line_marker_format, help=msg) - - msg = 'maximal line length (default: 132), lines modified by the '\ - 'preprocessor are folded if becoming longer' - parser.add_option('-l', '--line-length', type=int, metavar='LEN', - dest='line_length', default=defs.line_length, help=msg) - - msg = 'line folding mode, \'smart\' (default): indentation context '\ - 'and whitespace aware, \'simple\': indentation context aware, '\ - '\'brute\': mechnical folding' - parser.add_option('-f', '--folding-mode', metavar='MODE', - choices=['smart', 'simple', 'brute'], dest='folding_mode', - default=defs.folding_mode, help=msg) - - msg = 'suppress line folding' - parser.add_option('-F', '--no-folding', action='store_true', - dest='no_folding', default=defs.no_folding, help=msg) - - msg = 'indentation to use for continuation lines (default 4)' - parser.add_option('--indentation', type=int, metavar='IND', - dest='indentation', default=defs.indentation, help=msg) - - msg = 'produce fixed format output (any settings for options '\ - '--line-length, --folding-method and --indentation are ignored)' - parser.add_option('--fixed-format', action='store_true', - dest='fixed_format', default=defs.fixed_format, help=msg) - - msg = 'character encoding for reading/writing files. Default: \'utf-8\'. '\ - 'Note: reading from stdin and writing to stdout is encoded '\ - 'according to the current locale and is not affected by this setting.' - parser.add_option('--encoding', metavar='ENC', default=defs.encoding, - help=msg) - - msg = 'create parent folders of the output file if they do not exist' - parser.add_option('-p', '--create-parents', action='store_true', - dest='create_parent_folder', - default=defs.create_parent_folder, help=msg) - - msg = 'in variables _FILE_ and _THIS_FILE_, use relative paths with DIR '\ - 'as root directory. Note: the input file and all included files '\ - 'must be in DIR or in a directory below.' - parser.add_option('--file-var-root', metavar='DIR', dest='file_var_root', - default=defs.file_var_root, help=msg) - - return parser - - -def run_fypp(): - '''Run the Fypp command line tool.''' - options = FyppOptions() - optparser = get_option_parser() - opts, leftover = optparser.parse_args(values=options) - infile = leftover[0] if len(leftover) > 0 else '-' - outfile = leftover[1] if len(leftover) > 1 else '-' - try: - tool = Fypp(opts) - tool.process_file(infile, outfile) - except FyppStopRequest as exc: - sys.stderr.write(_formatted_exception(exc)) - sys.exit(USER_ERROR_EXIT_CODE) - except FyppFatalError as exc: - sys.stderr.write(_formatted_exception(exc)) - sys.exit(ERROR_EXIT_CODE) - - def linenumdir_cpp(linenr, fname, flag=None): """Returns a GNU cpp style line directive. @@ -3082,7 +2962,3 @@ def _formatted_exception(exc): out.append('\n' + _formatted_exception(exc.__cause__)) out.append('\n') return ''.join(out) - - -if __name__ == '__main__': - run_fypp() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..2a2a702 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,6 @@ +# Basic tests +add_test(NAME version + COMMAND ${CMAKE_Fypp_COMPILER} --version) + +# Integration tests +add_subdirectory(cmake) diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt new file mode 100644 index 0000000..0006170 --- /dev/null +++ b/test/cmake/CMakeLists.txt @@ -0,0 +1,73 @@ +function(Fypp_add_test test_dir) + #[===[.md: + # Fypp_add_test + + Internal function for adding Fypp cmake tests + + ## Synopsis + ```cmake + Main interface + Fypp_add_test( + [TEST_NAME ] + [CTEST_OPTIONS ...] + [BUILD_OPTIONS ...]) + ``` + + ## Options + `` + Path to the test CMake project + + `TEST_NAME` [Default ``] + Name of the test + + `CTEST_OPTIONS` + Additional test commands passed to the nested ctest call + + `BUILD_OPTIONS` + CMake configure options passed to the testing cmake + + ## See also + - + + ]===] + + + set(ARGS_Options + ) + set(ARGS_OneValue + TEST_NAME + ) + set(ARGS_MultiValue + BUILD_OPTIONS + CTEST_OPTIONS + ) + + cmake_parse_arguments(PARSE_ARGV 1 ARGS "${ARGS_Options}" "${ARGS_OneValue}" "${ARGS_MultiValue}") + + # Resolve default options + if (NOT DEFINED ARGS_TEST_NAME) + set(ARGS_TEST_NAME ${test_dir}) + endif () + + add_test(NAME ${ARGS_TEST_NAME} COMMAND ${CMAKE_CTEST_COMMAND} + --build-and-test + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/${test_dir} + ${CMAKE_CURRENT_BINARY_DIR}/${test_dir} + --build-generator "${CMAKE_GENERATOR}" + --build-options + ${ARGS_BUILD_OPTIONS} + --test-command ${CMAKE_CTEST_COMMAND} --test-dir=${CMAKE_CURRENT_BINARY_DIR}/${test_dir} --no-tests=ignore ${ARGS_CTEST_OPTIONS} + ) +endfunction() + +set(build_options) +set(ctest_options) +set(test_dir_rel) +foreach (test_dir IN ITEMS + module +) + block() + cmake_path(APPEND test_dir_rel ${test_dir}) + add_subdirectory(${test_dir}) + endblock() +endforeach () diff --git a/test/cmake/module/CMakeLists.txt b/test/cmake/module/CMakeLists.txt new file mode 100644 index 0000000..eada8c2 --- /dev/null +++ b/test/cmake/module/CMakeLists.txt @@ -0,0 +1,13 @@ +list(APPEND build_options + "-DCMAKE_MODULE_PATH=${PROJECT_SOURCE_DIR}/cmake" +) +foreach (test IN ITEMS fypp) + set(args) + if (build_options) + list(APPEND args BUILD_OPTIONS ${build_options}) + endif () + if (test_command) + list(APPEND args TEST_COMMAND ${test_command}) + endif () + Fypp_add_test(${test_dir_rel}/${test} ${args}) +endforeach () diff --git a/test/cmake/module/fypp/CMakeLists.txt b/test/cmake/module/fypp/CMakeLists.txt new file mode 100644 index 0000000..1bc609b --- /dev/null +++ b/test/cmake/module/fypp/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) + +project(test_fypp LANGUAGES Fortran) +include(Fypp) + +Fypp_add_executable(exec src/test_exec.fypp) +#Fypp_add_library(test_obj OBJECT src/test_obj.fypp src/test_bare.f90) +Fypp_add_library(test_static STATIC src/test_static.fypp) +Fypp_add_library(test_shared SHARED src/test_shared.fypp) +add_library(test_obj_other OBJECT) +target_compile_definitions(test_obj_other PRIVATE FOO BAR=some_value) +Fypp_target_sources(test_obj_other PRIVATE src/test_obj.fypp src/test_bare.f90) + +enable_testing() +add_test(NAME test_exec COMMAND $) diff --git a/test/cmake/module/fypp/src b/test/cmake/module/fypp/src new file mode 120000 index 0000000..929cb3d --- /dev/null +++ b/test/cmake/module/fypp/src @@ -0,0 +1 @@ +../../src \ No newline at end of file diff --git a/test/cmake/src/test_bare.f90 b/test/cmake/src/test_bare.f90 new file mode 100644 index 0000000..ee494d3 --- /dev/null +++ b/test/cmake/src/test_bare.f90 @@ -0,0 +1,3 @@ +module test_bare_m + integer :: a +end module test_bare_m diff --git a/test/cmake/src/test_exec.fypp b/test/cmake/src/test_exec.fypp new file mode 100644 index 0000000..64b945e --- /dev/null +++ b/test/cmake/src/test_exec.fypp @@ -0,0 +1,3 @@ +program test + print *, "hello" +end program test diff --git a/test/cmake/src/test_obj.fypp b/test/cmake/src/test_obj.fypp new file mode 100644 index 0000000..e68fd75 --- /dev/null +++ b/test/cmake/src/test_obj.fypp @@ -0,0 +1,3 @@ +module test_obj_m + integer :: a +end module test_obj_m diff --git a/test/cmake/src/test_shared.fypp b/test/cmake/src/test_shared.fypp new file mode 100644 index 0000000..f4f1639 --- /dev/null +++ b/test/cmake/src/test_shared.fypp @@ -0,0 +1,3 @@ +module test_shared_m + integer :: a +end module test_shared_m diff --git a/test/cmake/src/test_static.fypp b/test/cmake/src/test_static.fypp new file mode 100644 index 0000000..c1a546b --- /dev/null +++ b/test/cmake/src/test_static.fypp @@ -0,0 +1,3 @@ +module test_static_m + integer :: a +end module test_static_m diff --git a/test/runtests.sh b/test/runtests.sh deleted file mode 100755 index 422e171..0000000 --- a/test/runtests.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -testdir="$(dirname $0)" -if [ $# -gt 0 ]; then - pythons=$* -else - pythons="python3" -fi -root=".." -if [ -z "$PYTHONPATH" ]; then - export PYTHONPATH="$root/src" -else - export PYTHONPATH="$root/src:$PYTHONPATH" -fi -cd $testdir -failed="0" -failing_pythons="" -for python in $pythons; do - echo "Testing with interpreter '$python'" - $python test_fypp.py - exitcode=$? - if [ $exitcode != 0 ]; then - failed="$(($failed + 1))" - if [ -z "$failing_pythons" ]; then - failing_pythons=$python - else - failing_pythons="$failing_pythons, $python" - fi - fi -done -echo -if [ $failed -gt 0 ]; then - echo "Failing test runs: $failed" >&2 - echo "Failing interpreter(s): $failing_pythons" >&2 - exit 1 -else - echo "All test runs finished successfully" - exit 0 -fi diff --git a/test/test_fypp.py b/test/test_fypp.py index 1dcc3ac..13eee71 100644 --- a/test/test_fypp.py +++ b/test/test_fypp.py @@ -1,8 +1,32 @@ '''Unit tests for testing Fypp.''' +from contextlib import contextmanager +from os import getcwd, chdir from pathlib import Path import platform import unittest -import fypp +import fypp.fypp as fypp +from fypp.cli import get_option_parser + +DIR = Path(__file__).parent.resolve() +BASE = DIR.parent + + +@contextmanager +def cd(target): + """ + Manage cd in a pushd/popd fashion. + + Usage: + + with cd(tmpdir): + do something in tmpdir + """ + curdir = getcwd() + chdir(target) + try: + yield + finally: + chdir(curdir) def _linenum(linenr, fname=None, flag=None): @@ -1412,7 +1436,7 @@ def _importmodule(module): ), ('escape_comment', ([], - 'A\n #\! Comment\n', + 'A\n #\\! Comment\n', 'A\n #! Comment\n', ) ), @@ -2150,8 +2174,8 @@ def _importmodule(module): ) ), ('file_var_root_abs', - ([f"--file-var-root={Path.cwd()}"], - f"{Path.cwd() / 'input/filevarroot.fypp'}", + ([f"--file-var-root={DIR}"], + f"{DIR / 'input/filevarroot.fypp'}", 'FILE: input/filevarroot.fypp:1\n' 'THIS_FILE: input/filevarroot.fypp:2\n' '---\n' @@ -2957,11 +2981,13 @@ def _get_test_output_method(args, inp, out): def test_output(self): '''Tests whether Fypp result matches expected output.''' - optparser = fypp.get_option_parser() - options, leftover = optparser.parse_args(args) + parser = get_option_parser() + options, leftover = parser.parse_known_args(args) self.assertEqual(len(leftover), 0) - tool = fypp.Fypp(options) - result = tool.process_text(inp) + # TODO: Use proper test fixture to temporary folder + with cd(DIR): + tool = fypp.Fypp(options) + result = tool.process_text(inp) self.assertEqual(out, result) return test_output @@ -2980,11 +3006,13 @@ def _get_test_output_from_file_input_method(args, inputfile, out): def test_output_from_file_input(self): '''Tests whether Fypp result matches expected output when input is in a file.''' - optparser = fypp.get_option_parser() - options, leftover = optparser.parse_args(args) + parser = get_option_parser() + options, leftover = parser.parse_known_args(args) self.assertEqual(len(leftover), 0) - tool = fypp.Fypp(options) - result = tool.process_file(inputfile) + # TODO: Use proper test fixture to temporary folder + with cd(DIR): + tool = fypp.Fypp(options) + result = tool.process_file(inputfile) self.assertEqual(out, result) return test_output_from_file_input @@ -3006,12 +3034,14 @@ def _get_test_exception_method(args, inp, exceptions): def test_exception(self): '''Tests whether Fypp throws the correct exception.''' - optparser = fypp.get_option_parser() - options, leftover = optparser.parse_args(args) + parser = get_option_parser() + options, leftover = parser.parse_known_args(args) self.assertEqual(len(leftover), 0) try: - tool = fypp.Fypp(options) - _ = tool.process_text(inp) + # TODO: Use proper test fixture to temporary folder + with cd(DIR): + tool = fypp.Fypp(options) + _ = tool.process_text(inp) except Exception as e: raised = e else: diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 295aed6..0000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py34, py35, py36, py37, py38, py39 - -[testenv] -skip_missing_interpreters = - true -setenv = - PYTHONPATH = {toxinidir}/src -changedir=test -commands=python test_fypp.py diff --git a/utils/bump-version.py b/utils/bump-version.py deleted file mode 100755 index de4e739..0000000 --- a/utils/bump-version.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -import sys -import re -import os - -VERSION_PATTERN = r'\d+\.\d+(?:\.\d+)?(?:-\w+)?' -FILES_PATTERNS = [ ('bin/fypp', - r'^VERSION\s*=\s*([\'"]){}\1'.format(VERSION_PATTERN), - "VERSION = '{version}'"), - ('docs/fypp.rst', - r'Fypp Version[ ]*{}.'.format(VERSION_PATTERN), - 'Fypp Version {shortversion}.'), - ('setup.py', - r'version\s*=\s*([\'"]){}\1'.format(VERSION_PATTERN), - "version='{version}'"), - ('docs/conf.py', - r'version\s*=\s*([\'"]){}\1'.format(VERSION_PATTERN), - "version = '{shortversion}'"), - ('docs/conf.py', - r'release\s*=\s*([\'"]){}\1'.format(VERSION_PATTERN), - "release = '{version}'"), -] - -if len(sys.argv) < 2: - print("Missing version string") - sys.exit(1) - - -version = sys.argv[1] -shortversion = '.'.join(version.split('.')[0:2]) - -match = re.match(VERSION_PATTERN, version) -if match is None: - print("Invalid version string") - sys.exit(1) - -rootdir = os.path.join(os.path.dirname(sys.argv[0]), '..') -for fname, regexp, repl in FILES_PATTERNS: - fname = os.path.join(rootdir, fname) - print("Replacments in '{}': ".format(fname), end='') - fp = open(fname, 'r') - txt = fp.read() - fp.close() - replacement = repl.format(version=version, shortversion=shortversion) - newtxt, nsub = re.subn(regexp, replacement, txt, flags=re.MULTILINE) - print(nsub) - fp = open(fname, 'w') - fp.write(newtxt) - fp.close() - - -# Replace version number in Change Log and adapt decoration below -fname = os.path.join(rootdir, 'CHANGELOG.rst') -print("Replacments in '{}': ".format(fname), end='') -fp = open(fname, 'r') -txt = fp.read() -fp.close() -decoration = '=' * len(version) -newtxt, nsub = re.subn( - '^Unreleased\s*\n=+', version + '\n' + decoration, txt, - count=1, flags=re.MULTILINE) -print(nsub) -fp = open(fname, 'w') -fp.write(newtxt) -fp.close()