diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp index c5054eb2fd2e8bfa5e7fca898f343ce630643dbd..9c4c197e99406b0d4b4e9afdc5e4fb0c5479da62 100644 --- a/include/aidge/utils/DynamicAttributes.hpp +++ b/include/aidge/utils/DynamicAttributes.hpp @@ -46,40 +46,48 @@ public: * exist * \note at() throws if the Attribute does not exist, using find to test for Attribute existance */ - template<class T> T& getAttr(const std::string& name) + template<class T> const T& getAttr(const std::string& name) const { - AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes."); -#ifdef PYBIND - // If attribute does not exist in C++, it might have been created or modified in Python - auto it = mAttrs.find(name); - if (it == mAttrs.end()) { - auto itPy = mAttrsPy.find(pascalToSnake(name)); - if (itPy != mAttrsPy.end()) { - // Insert the attribute back in C++ - mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>()))); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", name); +#ifdef PYBIND + // If attribute does not exist in C++, it might have been created or modified in Python + auto it = mAttrs.find(name); + if (it == mAttrs.end()) { + auto itPy = mAttrsPy.find(pascalToSnake(name)); + if (itPy != mAttrsPy.end()) { + // Insert the attribute back in C++ + mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>()))); + } } - } #endif - return future_std::any_cast<T&>(mAttrs.at(name)); - } + return future_std::any_cast<const T&>(mAttrs.at(name)); + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); - template<class T> const T& getAttr(const std::string& name) const - { - AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes."); -#ifdef PYBIND - // If attribute does not exist in C++, it might have been created or modified in Python - auto it = mAttrs.find(name); - if (it == mAttrs.end()) { - auto itPy = mAttrsPy.find(pascalToSnake(name)); - if (itPy != mAttrsPy.end()) { - // Insert the attribute back in C++ - mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>()))); +#ifdef PYBIND + // If attribute does not exist in C++, it might have been created or modified in Python + auto it = mAttrs.find(ns); + if (it == mAttrs.end()) { + auto itPy = mAttrsPy.find(ns); + if (itPy != mAttrsPy.end()) { + // Insert the attribute back in C++ + mAttrs.emplace(std::make_pair(ns, future_std::any(itPy->second.cast<DynamicAttributes>()))); + } } - } #endif + return future_std::any_cast<const DynamicAttributes&>(mAttrs.at(ns)).getAttr<T>(nsName); + } + } - return future_std::any_cast<const T&>(mAttrs.at(name)); + template<class T> T& getAttr(const std::string& name) { + // Scott Meyers' solution to avoid code duplication + return const_cast<T&>( + static_cast<const DynamicAttributes&>(*this).getAttr<T>(name)); } ///\brief Add a new Attribute, identified by its name. If it already exists, asserts. @@ -88,17 +96,35 @@ public: ///\param value Attribute value template<class T> void addAttr(const std::string& name, const T& value) { - AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes."); - const auto& res = mAttrs.emplace(std::make_pair(name, future_std::any(value))); - AIDGE_ASSERT(res.second, "attribute already exists"); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", name); + const auto& res = mAttrs.emplace(std::make_pair(name, future_std::any(value))); + AIDGE_ASSERT(res.second, "attribute \"{}\" already exists", name); #ifdef PYBIND - // We cannot handle Python object if the Python interpreter is not running - if (Py_IsInitialized()) { - // Keep a copy of the attribute in py::object that is updated everytime - mAttrsPy.emplace(std::make_pair(pascalToSnake(name), py::cast(value))); + // We cannot handle Python object if the Python interpreter is not running + if (Py_IsInitialized()) { + // Keep a copy of the attribute in py::object that is updated everytime + mAttrsPy.emplace(std::make_pair(pascalToSnake(name), py::cast(value))); + } +#endif } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + const auto& res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes()))); + +#ifdef PYBIND + // We cannot handle Python object if the Python interpreter is not running + if (Py_IsInitialized()) { + // Keep a copy of the attribute in py::object that is updated everytime + mAttrsPy.emplace(std::make_pair(ns, py::cast(DynamicAttributes()))); + } #endif + + future_std::any_cast<DynamicAttributes&>(res.first->second).addAttr(nsName, value); + } } ///\brief Set an Attribute value, identified by its name. If it already exists, its value (and type, if different) is changed. @@ -107,49 +133,98 @@ public: ///\param value Attribute value template<class T> void setAttr(const std::string& name, const T& value) { - auto res = mAttrs.emplace(std::make_pair(name, future_std::any(value))); - if (!res.second) - res.first->second = future_std::any(value); + const auto dot = name.find('.'); + if (dot == name.npos) { + auto res = mAttrs.emplace(std::make_pair(name, future_std::any(value))); + if (!res.second) + res.first->second = future_std::any(value); #ifdef PYBIND - // We cannot handle Python object if the Python interpreter is not running - if (Py_IsInitialized()) { - // Keep a copy of the attribute in py::object that is updated everytime - auto resPy = mAttrsPy.emplace(std::make_pair(name, py::cast(value))); - if (!resPy.second) - resPy.first->second = std::move(py::cast(value)); + // We cannot handle Python object if the Python interpreter is not running + if (Py_IsInitialized()) { + // Keep a copy of the attribute in py::object that is updated everytime + auto resPy = mAttrsPy.emplace(std::make_pair(name, py::cast(value))); + if (!resPy.second) + resPy.first->second = std::move(py::cast(value)); + } +#endif } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + auto res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes()))); + +#ifdef PYBIND + // We cannot handle Python object if the Python interpreter is not running + if (Py_IsInitialized()) { + // Keep a copy of the attribute in py::object that is updated everytime + auto resPy = mAttrsPy.emplace(std::make_pair(ns, py::cast(DynamicAttributes()))); + } #endif + + future_std::any_cast<DynamicAttributes&>(res.first->second).setAttr<T>(nsName, value); + } } void delAttr(const std::string& name) { - mAttrs.erase(name); + const auto dot = name.find('.'); + if (dot == name.npos) { + mAttrs.erase(name); #ifdef PYBIND - mAttrsPy.erase(name); + mAttrsPy.erase(name); #endif + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + future_std::any_cast<DynamicAttributes&>(mAttrs.at(ns)).delAttr(nsName); +#ifdef PYBIND + mAttrsPy.erase(name); +#endif + } } #ifdef PYBIND void addAttrPy(const std::string& name, py::object&& value) { - AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python."); - auto it = mAttrs.find(snakeToPascal(name)); - AIDGE_ASSERT(it == mAttrs.end(), "attribute already exists"); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name); + auto it = mAttrs.find(snakeToPascal(name)); + AIDGE_ASSERT(it == mAttrs.end(), "attribute \"{}\" already exists", name); + + const auto& res = mAttrsPy.emplace(std::make_pair(name, value)); + AIDGE_ASSERT(res.second, "attribute \"{}\" already exists", name); + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + const auto& res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes()))); - const auto& res = mAttrsPy.emplace(std::make_pair(name, value)); - AIDGE_ASSERT(res.second, "attribute already exists"); + future_std::any_cast<DynamicAttributes&>(res.first->second).addAttrPy(nsName, std::move(value)); + } } void setAttrPy(const std::string& name, py::object&& value) override final { - AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python."); - auto resPy = mAttrsPy.emplace(std::make_pair(name, value)); - if (!resPy.second) - resPy.first->second = std::move(value); - - // Force getAttr() to take attribute value from mAttrsPy and update mAttrs - const std::string pascalName = snakeToPascal(name); - mAttrs.erase(pascalName); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name); + auto resPy = mAttrsPy.emplace(std::make_pair(name, value)); + if (!resPy.second) + resPy.first->second = std::move(value); + + // Force getAttr() to take attribute value from mAttrsPy and update mAttrs + const std::string pascalName = snakeToPascal(name); + mAttrs.erase(pascalName); + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + const auto& res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes()))); + + future_std::any_cast<DynamicAttributes&>(res.first->second).setAttrPy(nsName, std::move(value)); + } } py::dict dict() const override { @@ -177,15 +252,49 @@ public: /// Generic Attributes API ////////////////////////////////////// bool hasAttr(const std::string& name) const override final { - AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes."); - return (mAttrs.find(name) != mAttrs.cend()); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", name); + return (mAttrs.find(name) != mAttrs.cend()); + } + else { + const auto ns = name.substr(0, dot); + const auto it = mAttrs.find(ns); + if (it != mAttrs.cend()) { + const auto nsName = name.substr(dot + 1); + return future_std::any_cast<const DynamicAttributes&>(it->second).hasAttr(nsName); + } + else { + return false; + } + } } #ifdef PYBIND bool hasAttrPy(const std::string& name) const override final { - AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python."); - // Attributes might have been created in Python, the second condition is necessary. - return (mAttrs.find(snakeToPascal(name)) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend()); + const auto dot = name.find('.'); + if (dot == name.npos) { + AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name); + // Attributes might have been created in Python, the second condition is necessary. + return (mAttrs.find(snakeToPascal(name)) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend()); + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + const auto it = mAttrs.find(ns); + if (it != mAttrs.cend()) { + return future_std::any_cast<const DynamicAttributes&>(it->second).hasAttrPy(nsName); + } + else { + const auto itPy = mAttrsPy.find(ns); + if (itPy != mAttrsPy.cend()) { + return itPy->second.cast<DynamicAttributes>().hasAttrPy(nsName); + } + else { + return false; + } + } + } } #endif @@ -193,18 +302,38 @@ public: // In order to remain consistent between C++ and Python, with or without PyBind, the name of the type is: // - C-style for C++ created attributes // - Python-style for Python created attributes + const auto dot = name.find('.'); + if (dot == name.npos) { #ifdef PYBIND - // If attribute does not exist in C++, it might have been created in Python - auto it = mAttrs.find(name); - if (it == mAttrs.end()) { - auto itPy = mAttrsPy.find(name); - if (itPy != mAttrsPy.end()) { - return std::string(Py_TYPE(itPy->second.ptr())->tp_name); + // If attribute does not exist in C++, it might have been created in Python + auto it = mAttrs.find(name); + if (it == mAttrs.end()) { + auto itPy = mAttrsPy.find(name); + if (itPy != mAttrsPy.end()) { + return std::string(Py_TYPE(itPy->second.ptr())->tp_name); + } } +#endif + + return mAttrs.at(name).type().name(); } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + +#ifdef PYBIND + // If attribute does not exist in C++, it might have been created in Python + auto it = mAttrs.find(ns); + if (it == mAttrs.end()) { + auto itPy = mAttrsPy.find(ns); + if (itPy != mAttrsPy.end()) { + return itPy->second.cast<DynamicAttributes>().getAttrType(nsName); + } + } #endif - return mAttrs.at(name).type().name(); + return future_std::any_cast<const DynamicAttributes&>(mAttrs.at(ns)).getAttrType(nsName); + } } std::set<std::string> getAttrsName() const override final { @@ -226,7 +355,15 @@ public: * The strategy here is to keep a copy of each attribute in py::object that is updated everytime. */ inline py::object getAttrPy(const std::string& name) const override final { - return mAttrsPy.at(name); + const auto dot = name.find('.'); + if (dot == name.npos) { + return mAttrsPy.at(name); + } + else { + const auto ns = name.substr(0, dot); + const auto nsName = name.substr(dot + 1); + return mAttrsPy.at(ns).cast<DynamicAttributes>().getAttrPy(nsName); + } }; #endif diff --git a/unit_tests/utils/Test_DynamicAttributes.cpp b/unit_tests/utils/Test_DynamicAttributes.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c687af95d5e865328bc5f9043e65cdec510e7b22 --- /dev/null +++ b/unit_tests/utils/Test_DynamicAttributes.cpp @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#include <catch2/catch_test_macros.hpp> + +#include <string> +#include <vector> + +#include "aidge/utils/DynamicAttributes.hpp" + +using namespace Aidge; + +TEST_CASE("[core/attributes] DynamicAttributes") { + SECTION("TestAttr") { + DynamicAttributes attrs; + attrs.addAttr("A", 1); + attrs.addAttr("B", 1.0f); + attrs.addAttr("C", std::string("test")); + attrs.addAttr<std::vector<bool>>("D", {false, true, false}); + + REQUIRE(attrs.getAttr<int>("A") == 1); + REQUIRE(attrs.getAttr<float>("B") == 1.0f); + REQUIRE(attrs.getAttr<std::string>("C") == "test"); + REQUIRE(attrs.getAttr<std::vector<bool>>("D") == std::vector<bool>{{false, true, false}}); + + attrs.addAttr("E", DynamicAttributes()); + attrs.getAttr<DynamicAttributes>("E").addAttr("E1", 1.0f); + attrs.getAttr<DynamicAttributes>("E").addAttr("E2", std::string("test")); + + REQUIRE(attrs.getAttr<DynamicAttributes>("E").getAttr<float>("E1") == 1.0f); + REQUIRE(attrs.getAttr<DynamicAttributes>("E").getAttr<std::string>("E2") == "test"); + } + + SECTION("TestAttrNS") { + DynamicAttributes attrs; + attrs.addAttr("Mem.A", 1); + attrs.addAttr("Mem.Data.B", 1.0f); + attrs.addAttr("Impl.C", std::string("test")); + attrs.addAttr<std::vector<bool>>("D", {false, true, false}); + + REQUIRE(attrs.getAttr<int>("Mem.A") == 1); + REQUIRE(attrs.getAttr<float>("Mem.Data.B") == 1.0f); + REQUIRE(attrs.getAttr<std::string>("Impl.C") == "test"); + REQUIRE(attrs.getAttr<std::vector<bool>>("D") == std::vector<bool>{{false, true, false}}); + + attrs.getAttr<DynamicAttributes>("Mem.Data").addAttr("E", 2.0f); + attrs.getAttr<DynamicAttributes>("Impl").addAttr("F", std::string("test2")); + REQUIRE(attrs.getAttr<float>("Mem.Data.E") == 2.0f); + REQUIRE(attrs.getAttr<std::string>("Impl.F") == "test2"); + + REQUIRE(attrs.getAttr<DynamicAttributes>("Mem.Data").getAttr<float>("B") == 1.0f); + REQUIRE(attrs.getAttr<DynamicAttributes>("Impl").getAttr<std::string>("C") == "test"); + } +}