diff --git a/src/Base/ParmParse.cpp b/src/Base/ParmParse.cpp index 852fe404..d731dfc1 100644 --- a/src/Base/ParmParse.cpp +++ b/src/Base/ParmParse.cpp @@ -9,8 +9,10 @@ #include #include +#include #include #include +#include #include @@ -103,5 +105,91 @@ void init_ParmParse(py::module &m) ) // TODO: dumpTable, hasUnusedInputs, getUnusedInputs, getEntries + + .def( + "to_dict", + [](ParmParse &pp) { + py::dict d; + + auto g_table = pp.table(); + + // sort all keys + std::vector sorted_names; + sorted_names.reserve(g_table.size()); + for (auto const& [name, entry] : g_table) { + sorted_names.push_back(name); + } + std::sort(sorted_names.begin(), sorted_names.end()); + + // helper function to unroll any nested parameter.sub.options into dict of dict of value + auto add_nested = [&d](auto && value, std::string_view s) { + py::dict d_inner = d; // just hold a handle to the current dict + + while (true) { + auto pos = s.find('.'); + bool last = pos == std::string_view::npos; + py::str key(s.substr(0, pos)); + + if (last) { + d_inner[key] = value; + break; + } else { + // Create nested dict if missing or wrong type + if (!d_inner.contains(key) || + !py::isinstance(d_inner[key])) { + d_inner[key] = py::dict(); + } + + // Move one level deeper (safe, keeps reference alive) + d_inner = d_inner[key].cast(); + } + s.remove_prefix(pos + 1); + } + }; + + for (auto const& name : sorted_names) { + auto const& entry = g_table[name]; + for (auto const & vals : entry.m_vals) { + if (vals.size() == 1) { + std::visit( + [&](auto&& arg) { + using T = std::remove_pointer_t>; + T v; + pp.get(name.c_str(), v); + add_nested(v, name); + }, + entry.m_typehint + ); + } else { + std::visit( + [&](auto&& arg) { + using T = std::remove_pointer_t>; + if constexpr (!std::is_same_v) { + std::vector valarr; + pp.getarr(name.c_str(), valarr); + add_nested(valarr, name); + } + }, + entry.m_typehint + ); + } + } + } + + return d; + }, + R"(Convert to a nested Python dictionary. + +.. code-block:: python + + # Example: dump all ParmParse entries to YAML or TOML + import toml + import yaml + + pp = amr.ParmParse("").to_dict() + yaml_string = yaml.dump(d) + toml_string = toml.dumps(d) +)" + ) ; } diff --git a/tests/test_parmparse.py b/tests/test_parmparse.py index a6d7cd27..f47e0f74 100644 --- a/tests/test_parmparse.py +++ b/tests/test_parmparse.py @@ -15,8 +15,42 @@ def test_parmparse(): dt = pp_param.get_real("dt") dopml = pp_param.get_bool("do_pml") + pp_param.add("ncell", 42) # overwrite file + pp_param.add( + "question", "What is the answer to life, the universe, and everything?" + ) + pp_param.add("answer", 41) + pp_param.add("answer", 42) # last wins + pp_param.add("pi_approx", 3.1415) + pp_param.addarr("floats", [1.0, 2.0, 3.0]) + pp_param.addarr("ints", [4, 5, 6]) + pp_param.addarr("strs", ["Who", "Where", "What", "When", "How"]) + assert dopml assert np.isclose(dt, 1.0e-5) assert ncell == 100 + # printing pp.pretty_print_table() + + # type hints + d = pp.to_dict() + assert isinstance(d["param"]["ncell"], int) # overwritten + assert isinstance(d["param"]["dt"], str) # file + assert isinstance(d["param"]["do_pml"], str) # file + assert isinstance(d["param"]["question"], str) + assert isinstance(d["param"]["answer"], int) + assert d["param"]["answer"] == 42 # last wins + assert isinstance(d["param"]["pi_approx"], float) + assert isinstance(d["param"]["floats"], list) + assert isinstance(d["param"]["ints"], list) + assert isinstance(d["param"]["strs"], list) + assert d["param"]["floats"] == [1.0, 2.0, 3.0] + assert d["param"]["ints"] == [4, 5, 6] + assert d["param"]["strs"] == ["Who", "Where", "What", "When", "How"] + + # You can now dump to YAML or TOML or any other format + # import toml + # import yaml + # yaml_string = yaml.dump(d) + # toml_string = toml.dumps(d)