diff --git a/.bumpversion.toml b/.bumpversion.toml deleted file mode 100644 index a0b9821..0000000 --- a/.bumpversion.toml +++ /dev/null @@ -1,10 +0,0 @@ -[tool.bumpversion] -current_version = "0.41.1.dev1" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.dev(?P\\d+))?" -serialize = ["{major}.{minor}.{patch}.dev{dev}"] - -[[tool.bumpversion.files]] -filename = "src/atlas4py/_version.py" - -[[tool.bumpversion.files]] -filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index 6a48aa1..0284c31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ description = 'Python bindings for Atlas: a ECMWF library for parallel data-structures' name = 'atlas4py' -version = '0.41.1.dev1' +version = '0.41.1.dev2' # ...dev : ...dev license = {text = "Apache License 2.0"} readme = {file = 'README.md', content-type = 'text/markdown'} authors = [{email = 'willem.deconinck@ecmwf.int'}, {name = 'Willem Deconinck'}] @@ -41,6 +41,7 @@ test = ['pytest', 'pytest-cache'] [tool.scikit-build] minimum-version = '0.5' +build-dir="build/{wheel_tag}" cmake.minimum-version = '3.25' cmake.verbose = true cmake.source-dir = "src/atlas4py" @@ -48,8 +49,7 @@ cmake.build-type = "Release" cmake.args = [ "-DATLAS4PY_ECBUILD_VERSION=3.9.1", "-DATLAS4PY_ECKIT_VERSION=1.28.6", - "-DATLAS4PY_ATLAS_VERSION=0.41.1", - "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=build", + "-DATLAS4PY_ATLAS_VERSION=0.41.1" ] wheel.expand-macos-universal-tags = true wheel.install-dir = "atlas4py" @@ -75,3 +75,12 @@ exclude = ''' | dist )/ ''' + +[tool.bumpversion] +# To update atlas4py dev version: +# pip install bump-my-version +# bump-my-version bump dev +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.dev(?P\\d+))?" +serialize = ["{major}.{minor}.{patch}.dev{dev}"] +[[tool.bumpversion.files]] +filename = "pyproject.toml" diff --git a/requirements-dev.txt b/requirements-dev.txt index 8381ec5..a51c892 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ black>=21.12b0 bump2version>=1.0 +bump-my-version>=1.2.6 numpy>=1.17 pytest>=6.1 tox>=4.0 diff --git a/src/atlas4py/CMakeLists.txt b/src/atlas4py/CMakeLists.txt index 1f61c0c..6431e0f 100644 --- a/src/atlas4py/CMakeLists.txt +++ b/src/atlas4py/CMakeLists.txt @@ -1,64 +1,174 @@ cmake_minimum_required(VERSION 3.25.0) -project(atlas4py LANGUAGES CXX) +if (NOT SKBUILD) + set (PROJECT_ROOT_DIR "../..") + cmake_path(ABSOLUTE_PATH PROJECT_ROOT_DIR BASE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} NORMALIZE OUTPUT_VARIABLE PROJECT_ROOT_DIR) + message(FATAL_ERROR "\ + + This CMake file is meant to be executed using 'scikit-build'. Running + it directly will almost certainly not produce the desired result. If + you are a user trying to install this package, please use the command + below, which will install all necessary build dependencies, compile + the package in an isolated environment, and then install it. + ===================================================================== + $ pip install ${PROJECT_ROOT_DIR} + ===================================================================== + For development purposes, it is usually much more efficient to + install the build dependencies in your environment once and use the + following command that avoids a costly creation of a new virtual + environment at every compilation: + ===================================================================== + $ pip install pybind11 scikit-build-core + $ pip install --no-build-isolation -ve ${PROJECT_ROOT_DIR} + ===================================================================== + You may optionally add -Ceditable.rebuild=true to auto-rebuild when + the package is imported. Otherwise, you need to re-run the above + after editing C++ files.") +endif() + +set( PROJECT_NAME ${SKBUILD_PROJECT_NAME} ) +set( PROJECT_VERSION ${SKBUILD_PROJECT_VERSION} ) +set( PROJECT_VERSION_FULL ${SKBUILD_PROJECT_VERSION_FULL} ) +string( REPLACE "${PROJECT_VERSION}.dev" "" PROJECT_VERSION_DEV "${PROJECT_VERSION_FULL}" ) + +message( STATUS "[${PROJECT_NAME}] (${PROJECT_VERSION_FULL})" ) +project(${PROJECT_NAME} LANGUAGES CXX VERSION ${PROJECT_VERSION}) + +# Policies cmake_policy(SET CMP0074 NEW) +if (POLICY CMP0148) + cmake_policy(SET CMP0148 NEW) +endif() +if (POLICY CMP0168) + cmake_policy(SET CMP0168 NEW) +endif() + set(CMAKE_CXX_STANDARD 17) include(FetchContent) -find_package(atlas QUIET PATHS $ENV{ATLAS_INSTALL_DIR}) -if( atlas_FOUND ) - message( "Found atlas: ${atlas_DIR} (found version \"${atlas_VERSION}\")" ) +if (NOT build_atlas) + if (DEFINED ENV{ATLAS_INSTALL_DIR}) + set( atlas_ROOT ${ENV{ATLAS_INSTALL_DIR}} ) + endif() + find_package(atlas CONFIG QUIET) + if( atlas_FOUND ) + message( STATUS "Found atlas: ${atlas_DIR} (found version \"${atlas_VERSION}\")" ) + message( STATUS "Found eckit: ${eckit_DIR} (found version \"${eckit_VERSION}\")" ) + if (NOT atlas_VERSION VERSION_EQUAL ATLAS4PY_ATLAS_VERSION) + message( WARNING "Found atlas version \"${atlas_VERSION}\", but configured version is \"${ATLAS4PY_ATLAS_VERSION}\"" ) + endif() + if (NOT eckit_VERSION VERSION_EQUAL ATLAS4PY_ECKIT_VERSION) + message( WARNING "Found eckit version \"${eckit_VERSION}\", but configured version is \"${ATLAS4PY_ECKIT_VERSION}\"" ) + endif() + else() + set(build_atlas TRUE CACHE INTERNAL "build atlas") + endif() endif() -if(NOT atlas_FOUND) - find_package(ecbuild) - if(NOT ecbuild_FOUND) - FetchContent_Declare( +if(build_atlas) + if (NOT build_ecbuild) + find_package(ecbuild CONFIG QUIET) + if (ecbuild_FOUND) + message( STATUS "Found ecbuild: ${ecbuild_DIR} (found version \"${ecbuild_VERSION}\")" ) + if (NOT ecbuild_VERSION VERSION_EQUAL ATLAS4PY_ECBUILD_VERSION) + message( WARNING "Found ecbuild version \"${ecbuild_VERSION}\", but configured version is \"${ATLAS4PY_ECBUILD_VERSION}\"" ) + endif() + else() + set(build_ecbuild TRUE CACHE INTERNAL "build ecbuild") + endif() + endif() + if (build_ecbuild) + set ( ecbuild_SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/ecbuild ) + message( STATUS "Downloading ecbuild version \"${ATLAS4PY_ECBUILD_VERSION}\" to ${ecbuild_SOURCE_DIR}" ) + FetchContent_Populate( ecbuild GIT_REPOSITORY https://github.com/ecmwf/ecbuild.git GIT_TAG ${ATLAS4PY_ECBUILD_VERSION} + SOURCE_DIR ${ecbuild_SOURCE_DIR} + QUIET ) - FetchContent_MakeAvailable(ecbuild) + set( ecbuild_ROOT ${ecbuild_SOURCE_DIR} CACHE INTERNAL "Found ecbuild" ) endif() - find_package(eckit) - if( eckit_FOUND ) - message( "Found eckit: ${eckit_DIR} (found version \"${eckit_VERSION}\")" ) + + if (NOT build_eckit) + find_package(eckit CONFIG QUIET) + if( eckit_FOUND ) + message( STATUS "Found eckit: ${eckit_DIR} (found version \"${eckit_VERSION}\")" ) + if (NOT eckit_VERSION VERSION_EQUAL ATLAS4PY_ECKIT_VERSION) + message( WARNING "Found eckit version \"${eckit_VERSION}\", but configured version is \"${ATLAS4PY_ECKIT_VERSION}\"" ) + endif() + else() + set(build_eckit TRUE CACHE INTERNAL "build eckit") + endif() endif() + if (build_eckit) + message( STATUS "Downloading and building eckit version \"${ATLAS4PY_ECKIT_VERSION}\"" ) - if(NOT eckit_FOUND) + # Disable unused features for faster compilation + set(ECKIT_ENABLE_TESTS OFF) + set(ECKIT_ENABLE_DOCS OFF) + set(ECKIT_ENABLE_PKGCONFIG OFF) + set(ECKIT_ENABLE_ECKIT_GEO OFF) + set(ECKIT_ENABLE_ECKIT_SQL OFF) + set(ECKIT_ENABLE_ECKIT_CMD OFF) FetchContent_Declare( eckit GIT_REPOSITORY https://github.com/ecmwf/eckit.git GIT_TAG ${ATLAS4PY_ECKIT_VERSION} ) FetchContent_MakeAvailable(eckit) - set(_atlas4py_built_eckit ON) + set( eckit_ROOT ${eckit_BINARY_DIR} CACHE INTERNAL "Found eckit" ) endif() + message( STATUS "Downloading and building atlas version \"${ATLAS4PY_ATLAS_VERSION}\"" ) + + # Disable unused features for faster compilation + set(ATLAS_ENABLE_TESTS OFF) + set(ATLAS_ENABLE_DOCS OFF) + set(ATLAS_ENABLE_PKGCONFIG OFF) + set(ECKIT_ENABLE_ECKIT_GEO OFF) + set(ECKIT_ENABLE_ECKIT_SQL OFF) + set(ATLAS_ENABLE_ECKIT_CMD OFF) FetchContent_Declare( atlas GIT_REPOSITORY https://github.com/ecmwf/atlas.git GIT_TAG ${ATLAS4PY_ATLAS_VERSION} ) - set( ENABLE_GRIDTOOLS_STORAGE OFF CACHE BOOL "" FORCE ) FetchContent_MakeAvailable(atlas) endif() -find_package(pybind11) +### Find pybind11 + +message( STATUS "${PROJECT_NAME}: find_package(pybind11 CONFIG)..." ) +find_package(pybind11 CONFIG) +if (NOT pybind11_FOUND) + message( FATAL_ERROR "pybind11 not found. Please install pybind11 or use pip to install this package." ) +endif() + +### RPATH handling +include(GNUInstallDirs) # defines CMAKE_INSTALL_LIBDIR if(APPLE) set(rpath_origin_install_libdir "@loader_path/${CMAKE_INSTALL_LIBDIR}") else() set(rpath_origin_install_libdir "$ORIGIN/${CMAKE_INSTALL_LIBDIR}") endif() +set( CMAKE_INSTALL_RPATH "${rpath_origin_install_libdir}" ) # A semicolon-separated list specifying the RPATH to use in installed targets +set( CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE ) # add the automatic parts to RPATH which point to dirs outside build tree +set( CMAKE_SKIP_BUILD_RPATH FALSE ) # use RPATHs for the build tree +set( CMAKE_BUILD_WITH_INSTALL_RPATH TRUE ) # build with *relative* rpaths by default + +### Python bindings module atlas4py pybind11_add_module(_atlas4py _atlas4py.cpp) target_link_libraries(_atlas4py PUBLIC atlas) -install(TARGETS _atlas4py DESTINATION .) -set_target_properties(_atlas4py PROPERTIES INSTALL_RPATH "${rpath_origin_install_libdir}") +target_compile_definitions(_atlas4py PRIVATE ATLAS4PY_VERSION_STRING="${PROJECT_VERSION_FULL}") +### Installation + +install(TARGETS _atlas4py DESTINATION .) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ DESTINATION . FILES_MATCHING PATTERN "*.py" diff --git a/src/atlas4py/__init__.py b/src/atlas4py/__init__.py index 3296ead..685ab22 100644 --- a/src/atlas4py/__init__.py +++ b/src/atlas4py/__init__.py @@ -5,7 +5,7 @@ """ import atexit -from ._version import __version__ +from ._atlas4py import __version__ from ._atlas4py import * _atlas4py._initialise() diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 96ba4a2..569f255 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -55,34 +55,78 @@ void atlasInitialise() { atlas::initialise(argc, argv); } +py::object toPyObject( eckit::Configuration const& v ); +py::object toPyObject( eckit::Configuration const& v, std::string& key ); -py::object toPyObject( eckit::Value const& v ) { - if ( v.isBool() ) - return py::bool_( v.as() ); - else if ( v.isNumber() ) - return py::int_( v.as() ); - else if ( v.isDouble() ) - return py::float_( v.as() ); - else if ( v.isMap() ) { - py::dict ret; - auto const& map = v.as(); - for ( auto const& [k, v] : map ) { - ret[k.as().c_str()] = toPyObject( v ); - } - return ret; +py::object toPyObject(bool v) { + return py::bool_(v); +} +py::object toPyObject(long v) { + return py::int_(v); +} +py::object toPyObject(double v) { + return py::float_(v); +} +py::object toPyObject(std::string const& v) { + return py::str(v); +} +template +py::object toPyObject( std::vector const& v ) { + py::list ret; + for ( auto const& val : v ) { + ret.append( toPyObject( val ) ); } - else if ( v.isList() ) { - py::list ret; - auto const& list = v.as(); - for ( auto const& v : list ) - ret.append( toPyObject( v ) ); - return ret; + return ret; +} + +py::object toPyObject( eckit::Configuration const& v, std::string const& key ) { + if ( v.isSubConfiguration ( key ) ) { + return toPyObject( v.getSubConfiguration( key ) ); + } + else if (v.isBoolean( key )) { + return toPyObject( v.getBool( key ) ); + } + else if (v.isIntegral( key )) { + return toPyObject( v.getLong( key ) ); + } + else if (v.isFloatingPoint( key )) { + return toPyObject( v.getDouble( key ) ); + } + else if (v.isString( key )) { + return toPyObject( v.getString( key ) ); + } + else if (v.isSubConfigurationList( key )) { + std::vector subconfigs = v.getSubConfigurations( key ); + return toPyObject( subconfigs ); + } + else if (v.isIntegralList( key )) { + std::vector values = v.getLongVector( key ); + return toPyObject( values ); + } + else if (v.isFloatingPointList( key )) { + std::vector values = v.getDoubleVector( key ); + return toPyObject( values ); + } + else if (v.isStringList( key )) { + std::vector values = v.getStringVector( key ); + return toPyObject( values ); + } + else if (v.isBooleanList( key )) { + throw std::out_of_range( "boolean lists not supported for key " + key ); + } + else { + throw std::out_of_range( "type of value unsupported for key " + key ); } - else if ( v.isString() ) - return py::str( v.as() ); - else - throw std::out_of_range( "type of value unsupported (" + v.typeName() + ")" ); } + +py::object toPyObject( eckit::Configuration const& v ) { + py::dict ret; + for ( auto const& key : v.keys()) { + ret[ key.c_str() ] = toPyObject( v, key ); + } + return ret; +} + std::string atlasToPybind( array::DataType const& dt ) { switch ( dt.kind() ) { case array::DataType::KIND_INT32: @@ -116,10 +160,14 @@ array::DataType pybindToAtlas( py::dtype const& dtype ) { } // namespace +#define STRINGIFY(s) STRINGIFY_HELPER(s) +#define STRINGIFY_HELPER(s) #s + PYBIND11_MODULE( _atlas4py, m ) { m.def("_initialise", atlasInitialise) .def("_finalise", atlas::finalise); m.attr("version") = atlas::Library::instance().version(); + m.attr("__version__") = STRINGIFY(ATLAS4PY_VERSION_STRING); py::class_( m, "PointLonLat" ) .def( py::init( []( double lon, double lat ) { @@ -144,19 +192,19 @@ PYBIND11_MODULE( _atlas4py, m ) { } ); py::class_( m, "Projection" ).def( "__repr__", []( Projection const& p ) { - return "_atlas4py.Projection("_s + py::str( toPyObject( p.spec().get() ) ) + ")"_s; + return "_atlas4py.Projection("_s + py::str( toPyObject( p.spec() ) ) + ")"_s; } ); py::class_( m, "Domain" ) .def_property_readonly( "type", &Domain::type ) .def_property_readonly( "global", &Domain::global ) .def_property_readonly( "units", &Domain::units ) .def( "__repr__", []( Domain const& d ) { - return "_atlas4py.Domain("_s + ( d ? py::str( toPyObject( d.spec().get() ) ) : "" ) + ")"_s; + return "_atlas4py.Domain("_s + ( d ? py::str( toPyObject( d.spec() ) ) : "" ) + ")"_s; } ); py::class_( m, "RectangularDomain" ) .def( py::init( []( std::tuple xInterval, std::tuple yInterval ) { auto [xFrom, xTo] = xInterval; - auto [yFrom, yTo] = xInterval; + auto [yFrom, yTo] = yInterval; return RectangularDomain( { xFrom, xTo }, { yFrom, yTo } ); } ), "x_interval"_a, "y_interval"_a ); @@ -169,13 +217,13 @@ PYBIND11_MODULE( _atlas4py, m ) { .def_property_readonly( "projection", &Grid::projection ) .def_property_readonly( "domain", &Grid::domain ) .def( "__repr__", - []( Grid const& g ) { return "_atlas4py.Grid("_s + py::str( toPyObject( g.spec().get() ) ) + ")"_s; } ); + []( Grid const& g ) { return "_atlas4py.Grid("_s + py::str( toPyObject( g.spec() ) ) + ")"_s; } ); py::class_( m, "Spacing" ) .def( "__len__", &grid::Spacing::size ) .def( "__getitem__", &grid::Spacing::operator[]) .def( "__repr__", []( grid::Spacing const& spacing ) { - return "_atlas4py.Spacing("_s + py::str( toPyObject( spacing.spec().get() ) ) + ")"_s; + return "_atlas4py.Spacing("_s + py::str( toPyObject( spacing.spec() ) ) + ")"_s; } ); py::class_( m, "LinearSpacing" ) .def( py::init( []( double start, double stop, long N, bool endpoint ) { @@ -241,10 +289,10 @@ PYBIND11_MODULE( _atlas4py, m ) { // not be done (see comment in Config::get). We cannot // avoid this right now because otherwise we cannot query // the type of the underlying data. - return toPyObject( config.get().element( key ) ); + return toPyObject( config, key ); } ) .def( "__repr__", []( util::Config const& config ) { - return "_atlas4py.Config("_s + py::str( toPyObject( config.get() ) ) + ")"_s; + return "_atlas4py.Config("_s + py::str( toPyObject( config ) ) + ")"_s; } ); py::class_( m, "StructuredMeshGenerator" ) @@ -413,10 +461,10 @@ PYBIND11_MODULE( _atlas4py, m ) { // not be done (see comment in Config::get). We cannot // avoid this right now because otherwise we cannot query // the type of the underlying data. - return toPyObject( metadata.get().element( key ) ); + return toPyObject( metadata, key ); } ) .def( "__repr__", []( util::Metadata const& metadata ) { - return "_atlas4py.Metadata("_s + py::str( toPyObject( metadata.get() ) ) + ")"_s; + return "_atlas4py.Metadata("_s + py::str( toPyObject( metadata ) ) + ")"_s; } ); py::class_( m, "Field", py::buffer_protocol() ) diff --git a/src/atlas4py/_version.py b/src/atlas4py/_version.py deleted file mode 100644 index 90ba6b6..0000000 --- a/src/atlas4py/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.41.1.dev1"