From 51c82ed6f059dea4222db178469088322ab860f2 Mon Sep 17 00:00:00 2001 From: Willem Deconinck Date: Tue, 1 Sep 2026 22:26:22 +0200 Subject: [PATCH 1/5] Better handle __repr__ (#39) --- src/atlas4py/_atlas4py.cpp | 142 ++++++++++++++++++++++++++++++++----- tests/test_bindings.py | 15 ++++ 2 files changed, 138 insertions(+), 19 deletions(-) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 8f5670b..f2460f1 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -231,19 +231,19 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "lon", nb::overload_cast<>( &PointLonLat::lon, nb::const_ ) ) .def_prop_ro( "lat", nb::overload_cast<>( &PointLonLat::lat, nb::const_ ) ) .def( "__repr__", []( PointLonLat const& p ) { - return "_atlas4py.PointLonLat(lon=" + std::to_string( p.lon() ) + ", lat=" + std::to_string( p.lat() ) + ")"; + return "atlas4py.PointLonLat(lon="_s + nb::str( nb::float_( p.lon() ) ) + ", lat="_s + nb::str( nb::float_( p.lat() ) ) + ")"_s; } ); nb::class_( m, "PointXY" ) .def( nb::init(), "x"_a, "y"_a ) .def_prop_ro( "x", nb::overload_cast<>( &PointXY::x, nb::const_ ) ) .def_prop_ro( "y", nb::overload_cast<>( &PointXY::y, nb::const_ ) ) .def( "__repr__", []( PointXY const& p ) { - return "_atlas4py.PointXY(x=" + std::to_string( p.x() ) + ", y=" + std::to_string( p.y() ) + ")"; + return "atlas4py.PointXY(x="_s + nb::str( nb::float_( p.x() ) ) + ", y="_s + nb::str( nb::float_( p.y() ) ) + ")"_s; } ); nb::class_( m, "Projection" ) .def( "__repr__", []( Projection const& p ) { - return "_atlas4py.Projection("_s + nb::str( atlas4py::make_object( p.spec() ) ) + ")"_s; + return ""_s; } ); nb::class_( m, "Domain" ) @@ -252,9 +252,9 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "units", &Domain::units ) .def( "__repr__", []( Domain const& d ) { if (d) { - return nb::str("_atlas4py.Domain("_s + nb::str( atlas4py::make_object( d.spec() ) ) + ")"_s); + return nb::str(""_s); } - return nb::str("_atlas4py.Domain()"_s); + return nb::str(""_s); } ); nb::class_( m, "RectangularDomain" ) .def( nb::init(), "x_interval"_a, "y_interval"_a, "units"_a = "degrees" ); @@ -267,13 +267,13 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "projection", &Grid::projection ) .def_prop_ro( "domain", &Grid::domain ) .def( "__repr__", - []( Grid const& g ) { return "_atlas4py.Grid("_s + nb::str( atlas4py::make_object( g.spec() ) ) + ")"_s; } ); + []( Grid const& g ) { return ""_s; } ); nb::class_( m, "Spacing" ) .def( "__len__", &grid::Spacing::size ) .def( "__getitem__", &grid::Spacing::operator[]) .def( "__repr__", []( grid::Spacing const& spacing ) { - return "_atlas4py.Spacing("_s + nb::str( atlas4py::make_object( spacing.spec() ) ) + ")"_s; + return ""_s; } ); nb::class_( m, "LinearSpacing" ) .def( nb::init(), "start"_a, "stop"_a, "N"_a, "endpoint_included"_a = true ); @@ -343,6 +343,20 @@ NB_MODULE( _atlas4py, m ) { }) .def("__dlpack_device__", [](nb::handle /*self*/) { return std::make_pair(nb::device::cpu::value, 0); + }) + .def("__repr__", []( Field const& field ) -> std::string { + std::ostringstream oss; + oss << " 0) oss << ", "; + oss << field.shape()[i]; + } + oss << ")" + << " dtype=" << atlas4py::dtype::to_python_name(field.datatype()) + << ">"; + return oss.str(); }); nb::class_( m, "Mesh" ) @@ -350,13 +364,28 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "projection", &Mesh::projection ) .def_prop_ro( "nodes", nb::overload_cast<>( &Mesh::nodes, nb::const_ )) .def_prop_ro( "edges", nb::overload_cast<>( &Mesh::edges, nb::const_ )) - .def_prop_ro( "cells", nb::overload_cast<>( &Mesh::cells, nb::const_ )); + .def_prop_ro( "cells", nb::overload_cast<>( &Mesh::cells, nb::const_ )) + .def("__repr__", []( Mesh const& mesh ) -> std::string { + size_t halo = 0; + mesh.metadata().get("halo", halo); + std::ostringstream oss; + oss << ""; + return oss.str(); + } ); nb::class_( m, "StructuredMeshGenerator" ) // TODO in FunctionSpace below we expose config options, not the whole config object .def( nb::init(), "config"_a ) .def( nb::init() ) - .def( "generate", nb::overload_cast( &StructuredMeshGenerator::generate, nb::const_ ) ); + .def( "generate", nb::overload_cast( &StructuredMeshGenerator::generate, nb::const_ ) ) + .def( "__repr__", []( StructuredMeshGenerator const& smg ) -> std::string { + return ""; + } ); m.def( "build_edges", []( Mesh& mesh, const eckit::Configuration& config ) { mesh::actions::build_edges( mesh, config); @@ -381,7 +410,17 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "rows", &mesh::IrregularConnectivity::rows ) .def( "cols", &mesh::IrregularConnectivity::cols, "row_idx"_a ) .def_prop_ro( "maxcols", &mesh::IrregularConnectivity::maxcols ) - .def_prop_ro( "mincols", &mesh::IrregularConnectivity::mincols ); + .def_prop_ro( "mincols", &mesh::IrregularConnectivity::mincols ) + .def("__repr__", []( mesh::IrregularConnectivity const& c ) { + std::ostringstream oss; + if (c.rows() == 0) { + oss << ""; + } + else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m, "BlockConnectivity" ) .def( "__getitem__", @@ -390,7 +429,17 @@ NB_MODULE( _atlas4py, m ) { return c( row, col ); } ) .def_prop_ro( "rows", &mesh::BlockConnectivity::rows ) - .def_prop_ro( "cols", &mesh::BlockConnectivity::cols ); + .def_prop_ro( "cols", &mesh::BlockConnectivity::cols ) + .def("__repr__", []( mesh::BlockConnectivity const& c ) { + std::ostringstream oss; + if (c.rows() == 0) { + oss << ""; + } + else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m, "MultiBlockConnectivity" ) .def( "__getitem__", @@ -408,7 +457,17 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "maxcols", &mesh::MultiBlockConnectivity::maxcols ) .def_prop_ro( "mincols", &mesh::MultiBlockConnectivity::mincols ) .def_prop_ro( "blocks", &mesh::MultiBlockConnectivity::blocks ) - .def( "block", nb::overload_cast( &mesh::MultiBlockConnectivity::block, nb::const_ ), nb::rv_policy::reference_internal ); + .def( "block", nb::overload_cast( &mesh::MultiBlockConnectivity::block, nb::const_ ), nb::rv_policy::reference_internal ) + .def("__repr__", []( mesh::MultiBlockConnectivity const& c ) { + std::ostringstream oss; + if (c.rows() == 0) { + oss << ""; + } + else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m, "Nodes" ) .def_prop_ro( "size", &mesh::Nodes::size ) @@ -416,7 +475,12 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "cell_connectivity", nb::overload_cast<>( &mesh::Nodes::cell_connectivity, nb::const_ ) ) .def_prop_ro( "lonlat", nb::overload_cast<>( &Mesh::Nodes::lonlat, nb::const_ ) ) .def("field", []( mesh::Nodes const& n, std::string const& name ) { return n.field( name ); }, "name"_a, nb::rv_policy::reference_internal ) - .def( "flags", []( mesh::Nodes const& n ) { return n.flags(); }, nb::rv_policy::reference_internal); + .def( "flags", []( mesh::Nodes const& n ) { return n.flags(); }, nb::rv_policy::reference_internal) + .def("__repr__", []( mesh::Nodes const& n ) { + std::ostringstream oss; + oss << ""; + return oss.str(); + } ); nb::class_( m, "HybridElements" ) .def_prop_ro( "size", &mesh::HybridElements::size ) @@ -426,7 +490,12 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "edge_connectivity", nb::overload_cast<>( &mesh::HybridElements::edge_connectivity, nb::const_ ) ) .def_prop_ro( "cell_connectivity", nb::overload_cast<>( &mesh::HybridElements::cell_connectivity, nb::const_ ) ) .def( "field", []( mesh::HybridElements const& he, std::string const& name ) { return he.field( name ); }, "name"_a, nb::rv_policy::reference_internal ) - .def( "flags", []( mesh::HybridElements const& he ) { return he.flags(); }, nb::rv_policy::reference_internal ); + .def( "flags", []( mesh::HybridElements const& he ) { return he.flags(); }, nb::rv_policy::reference_internal ) + .def("__repr__", []( mesh::HybridElements const& he ) { + std::ostringstream oss; + oss << ""; + return oss.str(); + } ); auto m_fs = m.def_submodule( "functionspace" ); nb::class_( m_fs, "FunctionSpace" ) @@ -450,27 +519,59 @@ NB_MODULE( _atlas4py, m ) { config = config | option::variables( *variables ); config = config | option::datatype( atlas4py::dtype::from_python_object( dtype ) ); return fs.createField( config ); - }, "dtype"_a, "name"_a = std::nullopt, "levels"_a = std::nullopt, "variables"_a = std::nullopt ); + }, "dtype"_a, "name"_a = std::nullopt, "levels"_a = std::nullopt, "variables"_a = std::nullopt ) + .def("__repr__", []( FunctionSpace const& fs ) { + std::ostringstream oss; + oss << ""; + return oss.str(); + } ); nb::class_( m_fs, "EdgeColumns" ) .def("__init__", [](functionspace::EdgeColumns *t, const Mesh&m, int halo) { new (t) functionspace::EdgeColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) .def_prop_ro( "nb_edges", &functionspace::EdgeColumns::nb_edges ) .def_prop_ro( "mesh", &functionspace::EdgeColumns::mesh ) .def_prop_ro( "edges", &functionspace::EdgeColumns::edges ) - .def_prop_ro( "valid", &functionspace::EdgeColumns::valid ); + .def_prop_ro( "valid", &functionspace::EdgeColumns::valid ) + .def("__repr__", []( functionspace::EdgeColumns const& ec ) -> std::string { + std::ostringstream oss; + if ( !ec.valid() ) { + oss << ""; + } else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m_fs, "NodeColumns" ) .def("__init__", [](functionspace::NodeColumns *t, const Mesh&m, int halo) { new (t) functionspace::NodeColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) .def_prop_ro( "nb_nodes", &functionspace::NodeColumns::nb_nodes ) .def_prop_ro( "mesh", &functionspace::NodeColumns::mesh ) .def_prop_ro( "nodes", &functionspace::NodeColumns::nodes ) - .def_prop_ro( "valid", &functionspace::NodeColumns::valid ); + .def_prop_ro( "valid", &functionspace::NodeColumns::valid ) + .def("__repr__", []( functionspace::NodeColumns const& nc ) -> std::string { + std::ostringstream oss; + if ( !nc.valid() ) { + oss << ""; + } else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m_fs, "CellColumns" ) .def("__init__", [](functionspace::CellColumns *t, const Mesh&m, int halo) { new (t) functionspace::CellColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) .def_prop_ro( "nb_cells", &functionspace::CellColumns::nb_cells ) .def_prop_ro( "mesh", &functionspace::CellColumns::mesh ) .def_prop_ro( "cells", &functionspace::CellColumns::cells ) - .def_prop_ro( "valid", &functionspace::CellColumns::valid ); + .def_prop_ro( "valid", &functionspace::CellColumns::valid ) + .def("__repr__", []( functionspace::CellColumns const& cc ) -> std::string { + std::ostringstream oss; + if ( !cc.valid() ) { + oss << ""; + } else { + oss << ""; + } + return oss.str(); + } ); nb::class_( m, "Metadata" ) .def( "__repr__", []( util::Metadata const& metadata ) { @@ -495,6 +596,9 @@ NB_MODULE( _atlas4py, m ) { topology.def_static( "check", &mesh::Nodes::Topology::check ); topology.def_static( "check_all", &mesh::Nodes::Topology::check_all ); topology.def_static( "check_any", &mesh::Nodes::Topology::check_any ); + topology.def("__repr__", []( mesh::Nodes::Topology const& topology ) { + return ""; + } ); nb::class_( m, "Gmsh" ) .def( nb::init(), "path"_a ) @@ -503,5 +607,5 @@ NB_MODULE( _atlas4py, m ) { .def( "write", []( output::Gmsh& gmsh, Mesh const& mesh ) { gmsh.write( mesh ); }, "mesh"_a ) .def( "write", []( output::Gmsh& gmsh, Field const& field ) { gmsh.write( field ); }, "field"_a ) .def( "write", []( output::Gmsh& gmsh, Field const& field, FunctionSpace const& fs ) { gmsh.write( field, fs ); }, "field"_a, "functionspace"_a ) - .def("__repr__", []( output::Gmsh const& gmsh ) { return "_atlas4py.output.Gmsh()"; } ); + .def("__repr__", []( output::Gmsh const& gmsh ) { return ""; } ); } diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 1d818bb..166b708 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -40,6 +40,21 @@ def test_version(): assert isinstance(atlas4py.__version__, str) +@pytest.mark.parametrize( + ("point", "coordinates"), + [ + (atlas4py.PointLonLat(12.345678901234, -3.456789012345), ("lon", "lat")), + (atlas4py.PointXY(-2.345678901234, 8.456789012345), ("x", "y")), + ], +) +def test_point_repr_roundtrip(point, coordinates): + reconstructed = eval(repr(point)) + + assert type(reconstructed) is type(point) + for coordinate in coordinates: + assert getattr(reconstructed, coordinate) == getattr(point, coordinate) + + def test_grid_generation(structured_grid): assert structured_grid.domain.type == "rectangular" assert structured_grid.regular == True From 990db138d878bb993cc616f583416066d798ddff Mon Sep 17 00:00:00 2001 From: Willem Deconinck Date: Tue, 1 Sep 2026 22:46:28 +0200 Subject: [PATCH 2/5] Fix the wrong submodules --- src/atlas4py/_atlas4py.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index f2460f1..1c1a7c7 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -369,7 +369,7 @@ NB_MODULE( _atlas4py, m ) { size_t halo = 0; mesh.metadata().get("halo", halo); std::ostringstream oss; - oss << "( &StructuredMeshGenerator::generate, nb::const_ ) ) .def( "__repr__", []( StructuredMeshGenerator const& smg ) -> std::string { - return ""; + return ""; } ); m.def( "build_edges", []( Mesh& mesh, const eckit::Configuration& config ) { @@ -414,10 +414,10 @@ NB_MODULE( _atlas4py, m ) { .def("__repr__", []( mesh::IrregularConnectivity const& c ) { std::ostringstream oss; if (c.rows() == 0) { - oss << ""; + oss << ""; } else { - oss << ""; + oss << ""; } return oss.str(); } ); @@ -433,10 +433,10 @@ NB_MODULE( _atlas4py, m ) { .def("__repr__", []( mesh::BlockConnectivity const& c ) { std::ostringstream oss; if (c.rows() == 0) { - oss << ""; + oss << ""; } else { - oss << ""; + oss << ""; } return oss.str(); } ); @@ -461,10 +461,10 @@ NB_MODULE( _atlas4py, m ) { .def("__repr__", []( mesh::MultiBlockConnectivity const& c ) { std::ostringstream oss; if (c.rows() == 0) { - oss << ""; + oss << ""; } else { - oss << ""; + oss << ""; } return oss.str(); } ); @@ -473,12 +473,12 @@ NB_MODULE( _atlas4py, m ) { .def_prop_ro( "size", &mesh::Nodes::size ) .def_prop_ro( "edge_connectivity", nb::overload_cast<>( &mesh::Nodes::edge_connectivity, nb::const_ ) ) .def_prop_ro( "cell_connectivity", nb::overload_cast<>( &mesh::Nodes::cell_connectivity, nb::const_ ) ) - .def_prop_ro( "lonlat", nb::overload_cast<>( &Mesh::Nodes::lonlat, nb::const_ ) ) + .def_prop_ro( "lonlat", nb::overload_cast<>( &mesh::Nodes::lonlat, nb::const_ ) ) .def("field", []( mesh::Nodes const& n, std::string const& name ) { return n.field( name ); }, "name"_a, nb::rv_policy::reference_internal ) .def( "flags", []( mesh::Nodes const& n ) { return n.flags(); }, nb::rv_policy::reference_internal) .def("__repr__", []( mesh::Nodes const& n ) { std::ostringstream oss; - oss << ""; + oss << ""; return oss.str(); } ); @@ -493,7 +493,7 @@ NB_MODULE( _atlas4py, m ) { .def( "flags", []( mesh::HybridElements const& he ) { return he.flags(); }, nb::rv_policy::reference_internal ) .def("__repr__", []( mesh::HybridElements const& he ) { std::ostringstream oss; - oss << ""; + oss << ""; return oss.str(); } ); @@ -597,7 +597,7 @@ NB_MODULE( _atlas4py, m ) { topology.def_static( "check_all", &mesh::Nodes::Topology::check_all ); topology.def_static( "check_any", &mesh::Nodes::Topology::check_any ); topology.def("__repr__", []( mesh::Nodes::Topology const& topology ) { - return ""; + return ""; } ); nb::class_( m, "Gmsh" ) From a370a039f8cd7e7e559e4afe34d03d6cb67f8110 Mon Sep 17 00:00:00 2001 From: Willem Deconinck Date: Tue, 1 Sep 2026 22:52:05 +0200 Subject: [PATCH 3/5] More tests --- tests/test_bindings.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 166b708..9042bd7 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -132,10 +132,55 @@ def test_mesh_connectivity(structured_mesh): assert block[0, 3] == 21 +def test_mesh_and_connectivity_repr_branches(): + grid = atlas4py.StructuredGrid( + x_spacing=atlas4py.LinearSpacing(-1, 1, 4), + y_spacing=atlas4py.LinearSpacing(-1, 1, 3), + ) + generator = atlas4py.StructuredMeshGenerator() + mesh = generator.generate(grid) + + assert repr(generator) == "" + assert repr(mesh) == "" + assert repr(mesh.nodes) == "" + assert repr(mesh.cells) == "" + assert repr(mesh.nodes.edge_connectivity) == "" + assert repr(mesh.nodes.cell_connectivity) == "" + assert repr(mesh.cells.edge_connectivity) == "" + assert repr(mesh.cells.cell_connectivity) == "" + assert repr(mesh.cells.node_connectivity) == ( + "" + ) + assert repr(mesh.cells.node_connectivity.block(0)) == ( + "" + ) + + atlas4py.build_edges(mesh) + atlas4py.build_node_to_edge_connectivity(mesh) + + assert repr(mesh) == "" + assert repr(mesh.nodes.edge_connectivity) == ( + "" + ) + + def test_function_space_generation(structured_function_space): assert structured_function_space.nb_cells == 380 +def test_function_space_repr(structured_mesh): + edge_columns = atlas4py.functionspace.EdgeColumns(structured_mesh) + node_columns = atlas4py.functionspace.NodeColumns(structured_mesh) + cell_columns = atlas4py.functionspace.CellColumns(structured_mesh) + + assert repr(edge_columns) == "" + assert repr(node_columns) == "" + assert repr(cell_columns) == "" + assert atlas4py.functionspace.FunctionSpace.__repr__(cell_columns) == ( + "" + ) + + def test_field_generation(structured_in_and_out_fields): in_f, out_f = structured_in_and_out_fields assert in_f.rank == 2 @@ -151,6 +196,8 @@ def test_field_generation(structured_in_and_out_fields): assert np.allclose(in_view + 1, out_view) + assert repr(in_f) == "" + def test_metadata_mapping_protocol(structured_in_and_out_fields): field, _ = structured_in_and_out_fields @@ -166,6 +213,10 @@ def test_metadata_mapping_protocol(structured_in_and_out_fields): assert len(metadata) == len(values) assert "source" in metadata assert metadata["source"] == "test" + assert repr(metadata) == ( + "atlas4py.Metadata({'name': 'my_in_field', 'levels': 1, 'variables': 0, " + "'global': False, 'source': 'test', 'level': 1})" + ) def test_field_array_accepts_matching_dtype_and_false_copy(structured_in_and_out_fields): @@ -186,6 +237,7 @@ def test_gmsh_output(structured_mesh): output_file = "test_output.msh" with atlas4py.Gmsh(path=output_file) as gmsh: + assert repr(gmsh) == "" gmsh.write(structured_mesh) # Check that the file was created and has content From 6bb1424f740cff8e564e7f64ff2c7aebf2ef07a4 Mon Sep 17 00:00:00 2001 From: Willem Deconinck Date: Tue, 1 Sep 2026 22:53:30 +0200 Subject: [PATCH 4/5] Python standard for printing 1d tuple --- src/atlas4py/_atlas4py.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 1c1a7c7..645a51c 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -353,6 +353,7 @@ NB_MODULE( _atlas4py, m ) { if (i > 0) oss << ", "; oss << field.shape()[i]; } + if (field.shape().size() == 1) oss << ","; oss << ")" << " dtype=" << atlas4py::dtype::to_python_name(field.datatype()) << ">"; From 5a90693927e3135acf07c866118c7773dfaa0c7e Mon Sep 17 00:00:00 2001 From: Willem Deconinck Date: Tue, 1 Sep 2026 22:59:51 +0200 Subject: [PATCH 5/5] Add missing include --- src/atlas4py/_atlas4py.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 645a51c..7992ac7 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -1,4 +1,5 @@ #include +#include #include #include