diff --git a/CMakeLists.txt b/CMakeLists.txt index 59faef772..839f9e21b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,7 +158,7 @@ if(BUILD_PYBIND11_BINDINGS AND BUILD_TESTING) SOURCES "${_pluginplay_examples_dir}/classical_force.cpp" "${_pluginplay_examples_dir}/coulombs_law.cpp" - "${_pluginplay_examples_dir}/load_modules.cpp" + "${_pluginplay_examples_dir}/pluginplay_examples.cpp" "${_pluginplay_examples_dir}/screened_coulombs_law.cpp" "${_pluginplay_examples_dir}/templated_coulombs_law.cpp" TYPE STATIC diff --git a/cxx/include/pluginplay/module/module_class.hpp b/cxx/include/pluginplay/module/module_class.hpp index ca68a5ed2..e321b857f 100644 --- a/cxx/include/pluginplay/module/module_class.hpp +++ b/cxx/include/pluginplay/module/module_class.hpp @@ -16,6 +16,7 @@ #pragma once #include "pluginplay/types.hpp" +#include #include #include #include @@ -49,8 +50,13 @@ struct ModuleManagerPIMPL; * and is needed by the pluginplay framework to correctly use the module. This * state is not accessible (nor relevant) once the module is running. For the * most part users will only be interested in algorithm state. + * + * Modules stored in a ModuleManager, ModuleMap, or SubmoduleRequest are owned + * by shared_ptrs. Module derives from enable_shared_from_this so the Python + * bindings can hand out shared ownership of those modules; a copy of a + * Module is still an independent module. */ -class Module { +class Module : public std::enable_shared_from_this { public: /// Type of a pointer to a PIMPL using pimpl_ptr = std::unique_ptr; @@ -745,6 +751,9 @@ class Module { bool operator!=(const Module& rhs) const { return !((*this) == rhs); } private: + /// Lets the ModuleManager attach its runtime and caches to the module + friend struct detail_::ModuleManagerPIMPL; + /** @brief Unlocks a locked module * * There are very select circumstances when we need to unlock a locked diff --git a/cxx/include/pluginplay/module_manager/module_manager.hpp b/cxx/include/pluginplay/module_manager/module_manager.hpp index cc865eba4..69324771a 100644 --- a/cxx/include/pluginplay/module_manager/module_manager.hpp +++ b/cxx/include/pluginplay/module_manager/module_manager.hpp @@ -17,3 +17,4 @@ #pragma once #include #include +#include diff --git a/cxx/include/pluginplay/module_manager/module_manager_class.hpp b/cxx/include/pluginplay/module_manager/module_manager_class.hpp index 35fdd7280..96bd6d2d6 100644 --- a/cxx/include/pluginplay/module_manager/module_manager_class.hpp +++ b/cxx/include/pluginplay/module_manager/module_manager_class.hpp @@ -20,7 +20,12 @@ #include #include #include +#include +#include +#include #include +#include +#include namespace pluginplay { @@ -30,17 +35,34 @@ struct ModuleManagerPIMPL; /** @brief Class responsible for holding and managing modules. * + * Modules are loaded into a ModuleManager in two ways: as part of a plugin + * (see load_plugin), or one at a time (see add_module). The ModuleManager + * keeps one ModuleMap per loaded plugin, plus a ModuleMap of its own for the + * modules that do not come from a plugin. The empty plugin name, "", refers + * to the ModuleManager's own modules. + * + * Plugin names and module keys are case-insensitive. Module keys are unique + * within a map, but two maps may use the same key. + * Functions that take a module accept a QualifiedKey. A plain module key + * works as long as exactly one map has that key; otherwise the plugin name + * must be given too, *e.g.*, `mm.at({"integrals", "Overlap"})`. */ class ModuleManager { public: /// The type of a pointer to a read/write module using module_base_ptr = std::shared_ptr; - /// The type of a pointer to a read-only module - using const_module_base_ptr = std::shared_ptr; + /// Type used to identify modules + using qualified_key = QualifiedKey; + + /// Type used for the names of plugins + using name_type = Plugin::name_type; + + /// Type of a container of plugin names + using name_container_type = std::vector; - /// Type of a map holding usable modules - using module_map = utilities::CaseInsensitiveMap>; + /// Type of a pointer to a plugin + using plugin_pointer = std::unique_ptr; /// The type of the runtime using runtime_type = parallelzone::runtime::RuntimeView; @@ -49,7 +71,7 @@ class ModuleManager { using runtime_ptr = std::shared_ptr; /// Type of module key container - using key_container_type = std::vector; + using key_container_type = std::vector; /// Type of the cache using cache_type = cache::ModuleManagerCache; @@ -75,32 +97,128 @@ class ModuleManager { /// Standard dtor ~ModuleManager() noexcept; - /** @brief Returns an iterator to the first element of the module map + /** @brief Determines how many modules @p key refers to. * - * @return Iterator to the first element of the map + * @param[in] key The module to look for. If @p key is key-only, all maps + * are searched. + * + * @return For a key-only @p key, the number of maps that have the key. + * For a qualified @p key, 1 if the plugin is loaded and has the + * module and 0 otherwise. + * + * @throw None No throw guarantee. */ - module_map::iterator begin() noexcept; + type::size count(const qualified_key& key) const noexcept; - /** @brief Returns an iterator to the past-the-end element of the module map + /** @brief Gets the number of modules loaded. * - * @return Iterator to the past-the-end element of the map + * @return Number of modules loaded, summed over all maps. */ - module_map::iterator end() noexcept; + type::size size() const noexcept; - /** @brief Used to see if we have a module + /** @brief Creates a plugin of type @p PluginType and loads it. * - * @param key - * @return + * @tparam PluginType The type of the plugin. Must derive from Plugin and + * be default constructible. + * + * @throw std::invalid_argument if a plugin with the same name is already + * loaded. Strong throw guarantee. + * @throw std::bad_alloc if there is a problem allocating the plugin. Weak + * throw guarantee. + */ + template + void load_plugin(); + + /** @brief Loads @p plugin. + * + * *this takes ownership of @p plugin and its modules. Nothing is copied: + * the plugin's ModuleMap is the map *this uses for the plugin's modules. + * Each module is given *this's runtime and caches. A plugin's modules use + * the module cache named `"::"`. Modules that + * are reachable through submodules, but no longer in the plugin's map + * (*e.g.*, the plugin erased them after using them as submodules), are + * given the runtime and caches too, using the key they were last stored + * under. + * + * Loading does not set default submodules between plugins; call + * wire_defaults after all plugins are loaded. + * + * @param[in] plugin The plugin to load. If this function throws, the + * plugin is destroyed. + * + * @throw std::invalid_argument if @p plugin is null, has no name, or has + * the same name as a loaded plugin. Strong + * throw guarantee for *this. + * @throw std::bad_alloc if there is a problem allocating the caches. Weak + * throw guarantee. */ - type::size count(type::key key) const noexcept; + void load_plugin(plugin_pointer plugin); - /** @brief Gets the number of modules loaded. + /** @brief Sets default submodules between the loaded plugins. + * + * For each loaded plugin, in load order, and for each of its preferred + * providers that is loaded, in priority order, this applies the default + * submodules the plugin recorded for that provider (see + * Plugin::default_submod_from). A default only sets a submodule that is + * still unset, so choices made by the user, or by a provider with higher + * priority, are kept. Defaults that name a module the provider does not + * have are skipped, with a warning through the runtime's logger. + * + * Calling this function again, *e.g.*, after loading another plugin, + * only fills submodules that are still unset. * - * @return Number of modules loaded. + * @throw std::runtime_error if a provider's module does not satisfy the + * property type of the submodule. Weak throw + * guarantee. */ - type::size size() const noexcept; + void wire_defaults(); + + /** @brief Is a plugin named @p name loaded? + * + * @param[in] name The name of the plugin. + * + * @return True if a plugin named @p name is loaded and false otherwise. + * + * @throw None No throw guarantee. + */ + bool has_plugin(const name_type& name) const noexcept; + + /** @brief The names of the maps in *this. + * + * @return "", for *this's own modules, followed by the names of the loaded + * plugins in the order they were loaded. + * + * @throw std::bad_alloc if there is a problem allocating the return. + * Strong throw guarantee. + */ + name_container_type plugin_names() const; + + /** @brief Returns the loaded plugin named @p name. + * + * @param[in] name The name of the plugin. + * + * @return A read-only reference to the plugin. + * + * @throw std::out_of_range if no plugin named @p name is loaded. Strong + * throw guarantee. + */ + const Plugin& get_plugin(const name_type& name) const; + + /** @brief Returns the modules in the map named @p name. + * + * @param[in] name The name of a loaded plugin, or "" for the modules that + * do not come from a plugin. + * + * @return A read-only reference to the map. + * + * @throw std::out_of_range if @p name is not "" and no plugin named + * @p name is loaded. Strong throw guarantee. + */ + const ModuleMap& get_modules(const name_type& name) const; /** @brief Used to add a new module to the list + * + * The module is added to *this's own map (plugin name ""). * * @tparam ModuleType The type of the module being added. This type must be * constructible with no arguments and must inherit from @@ -111,6 +229,8 @@ class ModuleManager { void add_module(type::key module_key); /** @brief Used to add a new module to the list + * + * The module is added to *this's own map (plugin name ""). * * @param module_key * @param base @@ -120,14 +240,21 @@ class ModuleManager { ///@{ /** @name Module retrievers * - * @param module_key - * @return + * @param[in] module_key The module to retrieve. + * + * @return A reference to the requested module. + * + * @throw std::out_of_range if @p module_key does not refer to a module. + * Strong throw guarantee. + * @throw std::invalid_argument if @p module_key is key-only and more than + * one map has the key. Strong throw + * guarantee. */ - Module& at(const type::key& module_key) { + Module& at(const qualified_key& module_key) { const auto& rv = const_cast(*this).at(module_key); return const_cast(rv); } - const Module& at(const type::key& module_key) const; + const Module& at(const qualified_key& module_key) const; ///@} /** @brief Makes a "deep copy" of a module @@ -138,12 +265,21 @@ class ModuleManager { * that changing @p new_key 's inputs or submodules will not affect the * module stored under @p old_key. * - * @param[in] old_key The key for the module which is being copied. + * The copy is always stored in *this's own map (plugin name ""), even if + * @p old_key is one of a plugin's modules. Plugins' maps only hold the + * modules the plugins provide. + * + * @param[in] old_key The module which is being copied. * @param[in] new_key The key for the resulting copy. * + * @throw std::out_of_range if @p old_key does not refer to a module. + * Strong throw guarantee. + * @throw std::invalid_argument if @p old_key is ambiguous or *this's own + * map already has a module with key + * @p new_key. Strong throw guarantee. * @throw std::bad_alloc if an allocation error arises. */ - void copy_module(const type::key& old_key, type::key new_key); + void copy_module(const qualified_key& old_key, type::key new_key); /** @brief Unloads the specified module. * @@ -152,9 +288,12 @@ class ModuleManager { * NOT clean any data out of the cache. This function is a no-op if @p key * does not exist. * - * @param[in] key The key for the module which should be erased. + * @param[in] key The module which should be erased. + * + * @throw std::invalid_argument if @p key is key-only and more than one map + * has the key. Strong throw guarantee. */ - void erase(const type::key& key); + void erase(const qualified_key& key); /** @brief Changes the key a module is stored under. * @@ -162,12 +301,15 @@ class ModuleManager { * After a call to this function there will be no module stored under * @p old_key. * - * @param[in] old_key The key we are renaming. + * Like copy_module, the renamed module is stored in *this's own map, so + * renaming one of a plugin's modules moves it out of the plugin's map. + * + * @param[in] old_key The module we are renaming. * @param[in] new_key The value for the new key. * - * @throw std::bad_alloc if there is an allocation error. + * @throw ??? Throws if copy_module throws. Same guarantee. */ - void rename_module(const type::key& old_key, type::key new_key); + void rename_module(const qualified_key& old_key, type::key new_key); /** @brief Changes the value of an input bound to a module * @@ -177,7 +319,7 @@ class ModuleManager { * @param new_value */ template - void change_input(const type::key& key, const type::key& option, + void change_input(const qualified_key& key, const type::key& option, T&& new_value) { at(key).change_input(option, std::forward(new_value)); } @@ -190,19 +332,21 @@ class ModuleManager { * by specifying the key of the module, the callback point's key, and the * key for tne new submodule. * - * @param[in] module_key The key for the module whose submodule is being - * changed. + * @param[in] module_key The module whose submodule is being changed. * @param[in] callback_key The name of the callback point in the module * assigned to @p module_key - * @param[in] submod_key The key for the submodule to call. + * @param[in] submod_key The submodule to call. It may be in a different + * map than @p module_key. * * @throw std::out_of_range if any of the keys do not refer to valid, * already present modules or callback points. Weak * throw guarantee. + * @throw std::invalid_argument if @p module_key or @p submod_key is + * ambiguous. Strong throw guarantee. */ - void change_submod(const type::key& module_key, + void change_submod(const qualified_key& module_key, const type::key& callback_key, - const type::key& submod_key); + const qualified_key& submod_key); /** @brief Runs a given module * @@ -213,7 +357,7 @@ class ModuleManager { * @return */ template - auto run_as(const type::key& key, Args&&... args) { + auto run_as(const qualified_key& key, Args&&... args) { return at(key).run_as(std::forward(args)...); } @@ -238,7 +382,8 @@ class ModuleManager { /** @brief Returns the keys of all the modules in the module manager. * - * @return A vector of keys for all the modules in the module manager. + * @return The qualified keys of all the modules, *this's own modules + * first, followed by each plugin's modules in load order. * * @throw std::bad_alloc if an allocation error arises. */ @@ -276,10 +421,11 @@ void ModuleManager::add_module(type::key module_key) { add_module(std::move(module_key), std::make_shared()); } -inline void ModuleManager::rename_module(const type::key& old_key, - type::key new_key) { - copy_module(old_key, std::move(new_key)); - erase(old_key); +template +void ModuleManager::load_plugin() { + static_assert(std::is_base_of_v, + "Plugins must derive from pluginplay::Plugin"); + load_plugin(std::make_unique()); } } // namespace pluginplay diff --git a/cxx/include/pluginplay/module_manager/module_map.hpp b/cxx/include/pluginplay/module_manager/module_map.hpp index 2f8ae2269..63cfbb9ee 100644 --- a/cxx/include/pluginplay/module_manager/module_map.hpp +++ b/cxx/include/pluginplay/module_manager/module_map.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace pluginplay { @@ -87,6 +88,70 @@ class ModuleMap { */ void add_module(key_type key, module_base_ptr base); + /** @brief Stores the already-created module @p module under @p key. + * + * This overload is for adding a module that already exists, *e.g.*, a + * copy of a module. The module's name is set to @p key. + * + * @param[in] key The key to store the module under. + * @param[in] module The module to store. + * + * @throw std::invalid_argument if @p key is already in use. Strong throw + * guarantee. + * @throw std::bad_alloc if there is a problem allocating the module. + * Strong throw guarantee. + */ + void add_module(key_type key, Module module); + + /** @brief Stores an unlocked copy of the module @p old_key under + * @p new_key. + * + * The copy has its own inputs and submodules, so changing them does not + * change the module stored under @p old_key. The copy shares the + * implementation of @p old_key. + * + * @param[in] old_key The key of the module to copy. + * @param[in] new_key The key to store the copy under. + * + * @throw std::out_of_range if *this does not have a module with key + * @p old_key. Strong throw guarantee. + * @throw std::invalid_argument if @p new_key is already in use. Strong + * throw guarantee. + * @throw std::bad_alloc if there is a problem allocating the copy. Strong + * throw guarantee. + */ + void copy_module(const key_type& old_key, key_type new_key); + + /** @brief Removes the module stored under @p key. + * + * Modules that use the removed module as a submodule keep using it. This + * function is a no-op if *this does not have a module with key @p key. + * + * @param[in] key The key of the module to remove. + * + * @throw None No throw guarantee. + */ + void erase(const key_type& key) noexcept { m_modules_.erase(key); } + + /** @brief Changes the value of an input of the module @p key. + * + * @tparam T The type of the new value. + * + * @param[in] key The key of the module whose input is being changed. + * @param[in] option The key of the input to change. + * @param[in] new_value The new value of the input. + * + * @throw std::out_of_range if *this does not have a module with key + * @p key, or if that module has no input + * @p option. Strong throw guarantee. + * @throw ??? Throws if Module::change_input throws. Same guarantee. + */ + template + void change_input(const key_type& key, const key_type& option, + T&& new_value) { + at(key)->change_input(option, std::forward(new_value)); + } + /** @brief Determines if *this contains a module with key @p key. * * @param[in] key The key to look for. Comparisons are case-insensitive. diff --git a/cxx/include/pluginplay/module_manager/qualified_key.hpp b/cxx/include/pluginplay/module_manager/qualified_key.hpp new file mode 100644 index 000000000..68ec86890 --- /dev/null +++ b/cxx/include/pluginplay/module_manager/qualified_key.hpp @@ -0,0 +1,146 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +namespace pluginplay { + +/** @brief Identifies a module in a ModuleManager. + * + * A ModuleManager holds one ModuleMap per loaded plugin, plus a ModuleMap of + * its own. Module keys are unique within a map, but two maps may use the same + * key. A QualifiedKey names a module in one of two ways: + * + * - key-only, *e.g.*, `"Overlap"`. The ModuleManager searches every map for + * the key. This is the form created implicitly from a string, so code that + * passes a plain module key keeps working. + * - qualified, *e.g.*, `{"integrals", "Overlap"}`. The ModuleManager only + * looks in the named plugin's map. The empty plugin name, `""`, names the + * ModuleManager's own map. Hence `{"", "Overlap"}` is qualified and is + * not the same as `"Overlap"`: it is the only way to reach the + * ModuleManager's own "Overlap" when a plugin also has one. + * + * Like module keys and plugin names elsewhere in PluginPlay, both the plugin + * name and the module key are compared case-insensitively. + */ +class QualifiedKey { +public: + /// Type used for the names of plugins + using name_type = std::string; + + /// Type used for module keys + using key_type = type::key; + + /** @brief Creates a key-only QualifiedKey. + * + * This constructor is implicit so that a module key can be passed + * anywhere a QualifiedKey is expected. + * + * @param[in] key The module key. + * + * @throw None No throw guarantee. + */ + QualifiedKey(key_type key) noexcept : m_key_(std::move(key)) {} + + /** @brief Creates a key-only QualifiedKey from a string literal. + * + * @param[in] key The module key. + * + * @throw std::bad_alloc if there is a problem copying @p key. Strong throw + * guarantee. + */ + QualifiedKey(const char* key) : QualifiedKey(key_type(key)) {} + + /** @brief Creates a QualifiedKey that names the plugin. + * + * @param[in] plugin The name of the plugin the module is in. The empty + * name means the ModuleManager's own modules. + * @param[in] key The module key. + * + * @throw None No throw guarantee. + */ + QualifiedKey(name_type plugin, key_type key) noexcept : + m_plugin_(std::move(plugin)), + m_key_(std::move(key)), + m_qualified_(true) {} + + /// True if *this names a plugin and false otherwise. No throw guarantee. + bool is_qualified() const noexcept { return m_qualified_; } + + /** @brief The name of the plugin the module is in. + * + * @return The name of the plugin. Empty if *this is key-only or names the + * ModuleManager's own modules. + * + * @throw None No throw guarantee. + */ + const name_type& plugin() const noexcept { return m_plugin_; } + + /// The module key. No throw guarantee. + const key_type& key() const noexcept { return m_key_; } + + /** @brief A string representation of *this. + * + * @return `"::"` if *this is qualified and `""` + * otherwise. + * + * @throw std::bad_alloc if there is a problem allocating the string. + * Strong throw guarantee. + */ + std::string to_string() const { + return m_qualified_ ? m_plugin_ + "::" + m_key_ : m_key_; + } + + /** @brief Determines if *this and @p rhs name the same module. + * + * Two QualifiedKeys are equal if they are both key-only or both qualified, + * and have the same plugin name and module key (both compared + * case-insensitively). + * + * @param[in] rhs The QualifiedKey to compare to. + * + * @return True if *this equals @p rhs and false otherwise. + * + * @throw None No throw guarantee. + */ + bool operator==(const QualifiedKey& rhs) const noexcept { + using utilities::strings::case_insensitive_equal; + if(m_qualified_ != rhs.m_qualified_) return false; + return case_insensitive_equal(m_plugin_, rhs.m_plugin_) && + case_insensitive_equal(m_key_, rhs.m_key_); + } + + /// Negates operator== + bool operator!=(const QualifiedKey& rhs) const noexcept { + return !(*this == rhs); + } + +private: + /// The name of the plugin the module is in + name_type m_plugin_; + + /// The module key + key_type m_key_; + + /// Does *this name a plugin? + bool m_qualified_ = false; +}; + +} // namespace pluginplay diff --git a/cxx/include/pluginplay/plugin/plugin.hpp b/cxx/include/pluginplay/plugin/plugin.hpp index 4dbf4598d..8a19fef70 100644 --- a/cxx/include/pluginplay/plugin/plugin.hpp +++ b/cxx/include/pluginplay/plugin/plugin.hpp @@ -19,27 +19,71 @@ #include #include +/** @brief Declares a plugin class. + * + * The resulting class is named @p plugin_name, derives from + * pluginplay::Plugin, and has a default constructor. The constructor is + * defined with PLUGIN_CTOR. + * + * @param[in] plugin_name The name of the plugin class, *e.g.*, `MyPlugin`. + */ +#define DECLARE_PLUGIN(plugin_name) \ + class plugin_name : public pluginplay::Plugin { \ + public: \ + plugin_name(); \ + } + /** @brief Starts the definition of a plugin's constructor. * * The plugin class must derive from pluginplay::Plugin and declare a default - * constructor. The body of the constructor describes the plugin by calling - * the protected member functions of Plugin, *e.g.*, set_name and add_module. + * constructor, *e.g.*, by being declared with DECLARE_PLUGIN. The body of the + * constructor describes the plugin by calling the protected member functions + * of Plugin, *e.g.*, set_name and add_module. * * @param[in] plugin_name The name of the plugin class. */ #define PLUGIN_CTOR(plugin_name) \ plugin_name::plugin_name() : pluginplay::Plugin() -#define DECLARE_PLUGIN(plugin_name) \ - void load_modules(pluginplay::ModuleManager& mm) - -#define DEFINE_PLUGIN(plugin_name) \ - inline void load_modules(pluginplay::ModuleManager& mm) - -#define EXPORT_PLUGIN(plugin_name, m) \ - void pp_export_plugin_name__(pybind11::module_& m); \ - PYBIND11_MODULE(plugin_name, m) { \ - m.def("load_modules", &load_modules); \ - pp_export_plugin_name__(m); \ - } \ - inline void pp_export_plugin_name__(pybind11::module_& m) +/** @brief Creates the Python module for a plugin. + * + * The Python module is named @p module_name and contains the plugin class, + * exported under the name @p plugin_name, so Python users can load the plugin + * with `mm.load_plugin(module_name.plugin_name())`. The macro is followed by + * the body of a function which can export anything else the Python module + * needs, *e.g.*, property types: + * + * @code{.cpp} + * EXPORT_PLUGIN(my_plugin, MyPlugin, m) { + * export_property_types(m); + * } + * @endcode + * + * Loading a plugin **moves** it into the ModuleManager; so do NOT use the + * Python object afterwards. To make the move work, the plugin class also + * gets a private `_pp_load_into` method, which ModuleManager.load_plugin + * calls; users never call it. The method is compiled into the plugin's + * extension library (the one this macro creates), not into PluginPlay's. + * This is because pybind11 can only take ownership of an object from code in + * the library that created it (the ctor for the plugin will also be in the + * same library), when the object is passed as a pointer to its base class + * (pybind11 3.1, macOS). + * + * @param[in] module_name The name of the Python module. + * @param[in] plugin_name The name of the plugin class. + * @param[in] m The name of the pybind11 module variable, for use in the body. + */ +#define EXPORT_PLUGIN(module_name, plugin_name, m) \ + void pp_export_##module_name##__(pybind11::module_& m); \ + PYBIND11_MODULE(module_name, m) { \ + pybind11::module_::import("pluginplay"); \ + pybind11::class_(m, #plugin_name) \ + .def(pybind11::init<>()) \ + .def("_pp_load_into", [](std::unique_ptr self, \ + pluginplay::ModuleManager& mm) { \ + mm.load_plugin(std::move(self)); \ + }); \ + pp_export_##module_name##__(m); \ + } \ + void pp_export_##module_name##__(pybind11::module_& m) diff --git a/cxx/include/pluginplay/plugin/plugin_class.hpp b/cxx/include/pluginplay/plugin/plugin_class.hpp index 2a579bd5d..b16e86307 100644 --- a/cxx/include/pluginplay/plugin/plugin_class.hpp +++ b/cxx/include/pluginplay/plugin/plugin_class.hpp @@ -19,10 +19,14 @@ #include #include #include +#include #include #include namespace pluginplay { +namespace detail_ { +struct ModuleManagerPIMPL; +} /** @brief Base class for all plugins. * @@ -122,15 +126,36 @@ class Plugin { /// Negates operator== bool operator!=(const Plugin& rhs) const { return !(*this == rhs); } + /** @brief Determines if @p name can be used as a plugin name. + * + * Plugin names must be identifiers: a letter or an underscore, followed + * by letters, digits, and underscores, *e.g.*, "my_plugin". Plugin names + * are used as directory names (see printing::document_modules) and in + * module cache names, so they can not contain spaces, path separators, + * or "::". + * + * @param[in] name The name to check. + * + * @return True if @p name is a valid plugin name and false otherwise. + * + * @throw None No throw guarantee. + */ + static bool is_valid_name(const name_type& name) noexcept; + protected: /// Plugins are only created through derived classes Plugin() = default; /** @brief Sets the name of the plugin. * - * @param[in] name The name of the plugin. + * Like module keys, plugin names are case-insensitive. The name is stored + * as given. * - * @throw std::invalid_argument if @p name is empty. The empty name is + * @param[in] name The name of the plugin. Must be an identifier (see + * is_valid_name). + * + * @throw std::invalid_argument if @p name is not a valid plugin name. + * This includes the empty name, which is * reserved for the ModuleManager's own * modules. Strong throw guarantee. */ @@ -182,7 +207,8 @@ class Plugin { * * @param[in] names The names of the providers, highest priority first. * - * @throw std::invalid_argument if a name is empty, if a name appears more + * @throw std::invalid_argument if a name is not a valid plugin name (see + * is_valid_name), if a name appears more * than once, or if a provider that already * has recorded defaults is not in @p names. * Strong throw guarantee. @@ -214,6 +240,9 @@ class Plugin { ModuleMap& modules() noexcept { return m_modules_; } private: + /// The ModuleManager owns loaded plugins and changes their modules + friend struct detail_::ModuleManagerPIMPL; + /// The name of the plugin name_type m_name_; @@ -221,7 +250,7 @@ class Plugin { provider_list_type m_providers_; /// The recorded default submodules, keyed by provider - std::map m_defaults_; + utilities::CaseInsensitiveMap m_defaults_; /// The plugin's modules ModuleMap m_modules_; diff --git a/cxx/include/pluginplay/printing/document_modules.hpp b/cxx/include/pluginplay/printing/document_modules.hpp index e3dc6d212..a8eea2bb5 100644 --- a/cxx/include/pluginplay/printing/document_modules.hpp +++ b/cxx/include/pluginplay/printing/document_modules.hpp @@ -32,6 +32,10 @@ namespace pluginplay::printing { * an index file for the modules to be imported into a documentation generator * like Sphinx. * + * Modules that do not come from a plugin are documented in @p docs_path. A + * plugin's modules are documented in a subdirectory of @p docs_path named after + * the plugin. + * * @param[in] mm Module manager with modules loaded into it. * @param[in] docs_path Path to the directory where documentation is output. */ diff --git a/cxx/include/pluginplay/printing/mermaid.hpp b/cxx/include/pluginplay/printing/mermaid.hpp index 1e7578abf..e8b813b20 100644 --- a/cxx/include/pluginplay/printing/mermaid.hpp +++ b/cxx/include/pluginplay/printing/mermaid.hpp @@ -22,7 +22,7 @@ namespace pluginplay::printing { std::stringstream create_mermaid_graph(const pluginplay::ModuleManager& mm); -void print_submods(const std::string module_key, +void print_submods(const QualifiedKey& module_key, const pluginplay::ModuleManager& mm, std::stringstream& ss, std::string& module_code); } // namespace pluginplay::printing diff --git a/cxx/src/pluginplay/export_submodule_request.cpp b/cxx/src/pluginplay/export_submodule_request.cpp index 544ed1e3b..5906165e1 100644 --- a/cxx/src/pluginplay/export_submodule_request.cpp +++ b/cxx/src/pluginplay/export_submodule_request.cpp @@ -39,8 +39,6 @@ auto py_sr_run_as(py::object sr, py::object pt, py::args args) { } void export_submodule_request(py_module_reference m) { - using value_fxn = Module& (SubmoduleRequest::*)(); - py_class_type(m, "SubmoduleRequest") .def(py::init<>()) .def("has_type", &SubmoduleRequest::has_type) @@ -53,7 +51,10 @@ void export_submodule_request(py_module_reference m) { .def("set_description", &SubmoduleRequest::set_description) .def("submod_uuids", &SubmoduleRequest::submod_uuids) .def("uuid", &SubmoduleRequest::uuid) - .def("value", static_cast(&SubmoduleRequest::value)) + // Returns the submodule itself, with shared ownership, not a copy + .def( + "value", + [](SubmoduleRequest& self) { return self.value().shared_from_this(); }) .def("description", &SubmoduleRequest::description) .def("get_name", [](SubmoduleRequest& self) { diff --git a/cxx/src/pluginplay/module/detail_/module_pimpl.hpp b/cxx/src/pluginplay/module/detail_/module_pimpl.hpp index 0ba01ea0b..b0552c293 100644 --- a/cxx/src/pluginplay/module/detail_/module_pimpl.hpp +++ b/cxx/src/pluginplay/module/detail_/module_pimpl.hpp @@ -18,6 +18,7 @@ #include #include // for put_time #include +#include #include #include #include @@ -65,7 +66,7 @@ inline auto time_stamp() { class ModulePIMPL { public: /// Type used to store the developer-provided implementation - using base_ptr = std::shared_ptr; + using base_ptr = std::shared_ptr; /// Type of the object used to store the results computed by the module using cache_type = cache::ModuleCache; @@ -85,6 +86,12 @@ class ModulePIMPL { /// Type of the submodule key to UUID map using submod_uuid_map = std::map; + /// Type of a pointer to the runtime the implementation uses + using runtime_ptr = typename ModuleBase::runtime_ptr; + + /// Type of a pointer to the ModuleManager's caches + using mm_cache_ptr = std::shared_ptr; + /** @brief Makes a module with no implementation. * * The ModulePIMPL instance resulting from this ctor wraps no algorithm, @@ -485,6 +492,29 @@ class ModulePIMPL { */ bool is_cached(const type::input_map& in_inputs); + /** @brief Associates *this with a ModuleManager's runtime and caches. + * + * Modules are created without a runtime, UUID, or caches. When a + * ModuleManager takes ownership of a module, it calls this function to + * give the module the ModuleManager's runtime and caches. The + * implementation is given a UUID if it does not already have one, and its + * user cache is the one associated with that UUID. Implementations shared + * by more than one module (*e.g.*, copies of a module) thus share a UUID + * and a user cache. + * + * @param[in] runtime The runtime the implementation should use. + * @param[in] caches The ModuleManager's caches. If null, *this is not + * given caches. + * @param[in] cache_key The key of the module cache *this should use. + * + * @throw std::runtime_error if *this does not have an implementation. + * Strong throw guarantee. + * @throw std::bad_alloc if there is a problem allocating a cache. Weak + * throw guarantee. + */ + void attach(runtime_ptr runtime, const mm_cache_ptr& caches, + const type::key& cache_key); + /** @brief Resets cache. * * This function will reset cache. diff --git a/cxx/src/pluginplay/module/detail_/module_pimpl.ipp b/cxx/src/pluginplay/module/detail_/module_pimpl.ipp index b7b2a0376..e8c34f497 100644 --- a/cxx/src/pluginplay/module/detail_/module_pimpl.ipp +++ b/cxx/src/pluginplay/module/detail_/module_pimpl.ipp @@ -126,6 +126,16 @@ inline bool ModulePIMPL::is_cached(const type::input_map& in_inputs) { return m_cache_->count(ps); } +inline void ModulePIMPL::attach(runtime_ptr runtime, const mm_cache_ptr& caches, + const type::key& cache_key) { + assert_mod_(); + m_base_->set_runtime(runtime); + if(!m_base_->has_uuid()) m_base_->set_uuid(utility::generate_uuid()); + if(!caches) return; + m_base_->set_cache(caches->get_or_make_user_cache(m_base_->uuid())); + m_cache_ = caches->get_or_make_module_cache(cache_key); +} + inline void ModulePIMPL::reset_cache() { if(m_cache_) m_cache_->clear(); } @@ -204,7 +214,8 @@ inline bool ModulePIMPL::operator==(const ModulePIMPL& rhs) const { if(locked() != rhs.locked()) return false; if(!has_module()) return true; - if(std::tie(inputs(), submods(), property_types(), python_property_types()) != + if(std::tie(inputs(), submods(), property_types(), + python_property_types()) != std::tie(rhs.inputs(), rhs.submods(), rhs.property_types(), rhs.python_property_types())) return false; diff --git a/cxx/src/pluginplay/module/export_module_class.cpp b/cxx/src/pluginplay/module/export_module_class.cpp index 99fa3afa2..5ff4fb078 100644 --- a/cxx/src/pluginplay/module/export_module_class.cpp +++ b/cxx/src/pluginplay/module/export_module_class.cpp @@ -47,10 +47,14 @@ pybind11::object py_module_run_as_python_only( } void export_module_class(py_module_reference m) { - using ready_fxn = bool (Module::*)(const type::input_map&) const; - using change_submod_fxn = void (Module::*)(type::key, Module); + using ready_fxn = bool (Module::*)(const type::input_map&) const; + // Links the submodule (shares it) rather than copying it, as in C++ + using change_submod_fxn = + void (Module::*)(type::key, std::shared_ptr); - py_class_type(m, "Module") + // Python handles share ownership of modules, so a handle stays valid if + // the module is erased from its map or replaced as a submodule + py_class_type>(m, "Module") .def(pybind11::init<>()) .def("unlocked_copy", &Module::unlocked_copy) .def("has_module", &Module::has_module) diff --git a/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.hpp b/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.hpp index 13f2446ce..478fd42ac 100644 --- a/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.hpp +++ b/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.hpp @@ -21,7 +21,12 @@ #include #include #include +#include +#include #include +#include +#include +#include namespace pluginplay::detail_ { @@ -32,6 +37,10 @@ namespace pluginplay::detail_ { * When using this class calls should go through the member functions as much * as possible so that error checking occurs. That said members are public so * that it is easier to test the implementation. + * + * The modules are stored in one ModuleMap per loaded plugin, plus a ModuleMap + * for the modules that do not come from a plugin (the "own" map). The own map + * is identified by the empty plugin name. */ struct ModuleManagerPIMPL { /// Type *this implements @@ -41,18 +50,34 @@ struct ModuleManagerPIMPL { /// Type of a pointer to a module's implemenation using module_base_ptr = module_manager_type::module_base_ptr; - /// Type of a pointer to a read-only module implementation - using const_module_base_ptr = module_manager_type::const_module_base_ptr; - /// Type of a map from the module implementation's type to the /// implementation - using base_map = std::map; + using base_map = std::map; /// Type of a usable module using shared_module = std::shared_ptr; /// Type of a map holding usable modules - using module_map = module_manager_type::module_map; + using module_map = ModuleMap; + + /// Type used to identify modules + using qualified_key = QualifiedKey; + + /// Type used for the names of plugins + using name_type = typename module_manager_type::name_type; + + /// Type of a container of plugin names + using name_container_type = + typename module_manager_type::name_container_type; + + /// Type of a container of qualified keys + using key_container_type = typename module_manager_type::key_container_type; + + /// Type of a pointer to a loaded plugin + using plugin_pointer = typename module_manager_type::plugin_pointer; + + /// Type of the container holding the loaded plugins + using plugin_container_type = std::vector; /// The type of the runtime using runtime_type = module_manager_type::runtime_type; @@ -68,7 +93,7 @@ struct ModuleManagerPIMPL { /// Type of a map from key to Python implementation // TODO: remove when a more elegant solution is determined - using py_base_map = std::map; + using py_base_map = std::map; ///@} @@ -79,32 +104,22 @@ struct ModuleManagerPIMPL { ModuleManagerPIMPL(runtime_ptr runtime, cache_pointer cache) : m_pcaches(cache), m_runtime_(runtime) {} - /// Makes a deep copy of this instance on the heap - // auto clone() { return std::make_unique(*this); } - - /// Ensures we determine if we have a module consistently - type::size count(const type::key& key) const noexcept { - return m_modules.count(key); - } - - /// Ensures we count the number of modules consistently - type::size size() const noexcept { return m_modules.size(); } - - /** @brief Returns an iterator to the first element of the module map + /** @brief The number of modules @p key refers to. * - * @return Iterator to the first element of the map + * For a key-only @p key this is the number of maps that contain the key. + * For a qualified @p key this is 1 if the plugin is loaded and has the + * module and 0 otherwise. */ - module_map::iterator begin() noexcept { return m_modules.begin(); }; + type::size count(const qualified_key& key) const noexcept; - /** @brief Returns an iterator to the past-the-end element of the module map - * - * @return Iterator to the past-the-end element of the map - */ - module_map::iterator end() noexcept { return m_modules.end(); }; + /// The total number of modules in all maps + type::size size() const noexcept; /** @brief This function actually adds a module to the list of available * modules. * + * The module is added to the own map. + * * @param key The key under which the module will be registered. * @param base The instance containing the algorithm */ @@ -112,34 +127,83 @@ struct ModuleManagerPIMPL { /** @brief Unloads the specified module. * - * This function unloads the module with the specified key. After this - * operation the key is free to be used again. Calling this function does - * NOT clean any data out of the cache. This function is a no-op if @p key - * does not exist. + * This function unloads the module @p key refers to. After this operation + * the key is free to be used again in that map. Calling this function + * does NOT clean any data out of the cache. This function is a no-op if + * @p key does not refer to a module. * - * @param[in] key The key for the module which should be erased. + * @param[in] key The module which should be erased. * - * @throw None No throw guarantee. + * @throw std::invalid_argument if @p key is key-only and more than one + * map has the key. Strong throw guarantee. */ - void erase(const type::key& key) { m_modules.erase(key); } + void erase(const qualified_key& key); /** @brief Makes a deep copy of a module * * This function makes a deep copy of a module. The new module is unlocked * regardless of whether the old module was locked or not. The user can call * lock on the resulting module to make an exact copy - * @param old_key The key for the module to copy - * @param new_key The key under which the new module will live + * + * @param old_key The module to copy + * @param new_key The key under which the new module will live. The copy + * always goes in the own map. */ - void copy_module(const type::key& old_key, type::key new_key); + void copy_module(const qualified_key& old_key, type::key new_key); - /** @brief Returns a module, filling in all non-set submodules with defaults - * if a ready default exists. + /// Copies @p old_key to @p new_key, then erases @p old_key + void rename_module(const qualified_key& old_key, type::key new_key); + + /** @brief Returns the module @p key refers to. + * + * @param key The module you want + * @return A shared_ptr to the requested module * - * @param key The module you want - * @return A shared_ptr to the requested module + * @throw std::out_of_range if @p key does not refer to a module. Strong + * throw guarantee. + * @throw std::invalid_argument if @p key is key-only and more than one + * map has the key. Strong throw guarantee. */ - shared_module at(const type::key& key); + shared_module at(const qualified_key& key) const; + + /** @brief Takes ownership of @p plugin and its modules. + * + * @param[in] plugin The plugin to load. + * + * @throw std::invalid_argument if @p plugin is null, has no name, or has + * the same name as a plugin that is already + * loaded. Strong throw guarantee for *this. + */ + void load_plugin(plugin_pointer plugin); + + /// Applies the default submodules the loaded plugins recorded + void wire_defaults(); + + /// Is a plugin named @p name loaded? + bool has_plugin(const name_type& name) const noexcept; + + /// The names of the maps, "" (the own map) first, then plugins in load + /// order + name_container_type plugin_names() const; + + /** @brief Returns the loaded plugin named @p name. + * + * @throw std::out_of_range if no plugin named @p name is loaded. Strong + * throw guarantee. + */ + const Plugin& get_plugin(const name_type& name) const; + + ///@{ + /** @brief Returns the map named @p name. + * + * @param[in] name The name of a loaded plugin, or "" for the own map. + * + * @throw std::out_of_range if @p name is not "" and no plugin named + * @p name is loaded. Strong throw guarantee. + */ + module_map& get_modules(const name_type& name); + const module_map& get_modules(const name_type& name) const; + ///@} ///@{ /** @name Comparison operators @@ -152,15 +216,16 @@ struct ModuleManagerPIMPL { bool operator!=(const ModuleManagerPIMPL& rhs) const { return !((*this) == rhs); } + ///@} void set_runtime(runtime_ptr runtime) noexcept { m_runtime_ = runtime; } runtime_type& get_runtime() const { return *m_runtime_; } - ModuleManager::key_container_type keys() const; + /// The qualified keys of every module, in plugin_names() order + key_container_type keys() const; bool has_cache() const noexcept { return static_cast(m_pcaches); } - ///@} ///@{ /** @name ModuleManager state @@ -170,7 +235,8 @@ struct ModuleManagerPIMPL { // These are the Modules in the states provided to us by the developer base_map m_bases; - // These are the Modules in the state set by the user + // These are the Modules that do not come from a plugin, in the state set + // by the user module_map m_modules; // Part of the hacky patch to make multiple python modules work @@ -178,6 +244,10 @@ struct ModuleManagerPIMPL { // These are the Python Modules in their developer state py_base_map m_py_bases; + // These are the loaded plugins, in load order. Each plugin's ModuleMap + // holds the plugin's modules in the state set by the user. + plugin_container_type m_plugins; + // These are the results of the modules running in the user's states cache_pointer m_pcaches; @@ -185,10 +255,19 @@ struct ModuleManagerPIMPL { runtime_ptr m_runtime_; ///@} private: - /// Wraps the check for making sure @p key is not in use. - void assert_unique_key_(const type::key& key) const { - if(count(key)) throw std::invalid_argument("Key is in use"); - } + /// Returns the loaded plugin named @p name, or null if there isn't one + Plugin* find_plugin_(const name_type& name) const noexcept; + + /// Returns the map named @p name, or null if there isn't one + const module_map* find_map_(const name_type& name) const noexcept; + + /// Returns the name of the map holding @p key, throwing if there isn't + /// exactly one + name_type resolve_(const qualified_key& key) const; + + /// Gives @p mod the runtime and caches, using @p cache_key for the module + /// cache + void attach_(const type::key& cache_key, Module& mod) const; }; } // namespace pluginplay::detail_ diff --git a/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.ipp b/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.ipp index a7bd4b250..37603c71e 100644 --- a/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.ipp +++ b/cxx/src/pluginplay/module_manager/detail_/module_manager_pimpl.ipp @@ -20,54 +20,179 @@ namespace pluginplay::detail_ { +inline type::size ModuleManagerPIMPL::count( + const qualified_key& key) const noexcept { + if(key.is_qualified()) { + const auto* map = find_map_(key.plugin()); + return map ? map->count(key.key()) : 0; + } + auto rv = m_modules.count(key.key()); + for(const auto& plugin : m_plugins) + rv += plugin->m_modules_.count(key.key()); + return rv; +} + +inline type::size ModuleManagerPIMPL::size() const noexcept { + auto rv = m_modules.size(); + for(const auto& plugin : m_plugins) rv += plugin->m_modules_.size(); + return rv; +} + inline void ModuleManagerPIMPL::add_module(type::key key, module_base_ptr base) { - assert_unique_key_(key); - auto uuid = utility::generate_uuid(); - base->set_runtime(m_runtime_); - base->set_uuid(uuid); - - cache::ModuleManagerCache::module_cache_pointer module_cache; - if(m_pcaches) { - auto internal_cache = m_pcaches->get_or_make_user_cache(uuid); - base->set_cache(internal_cache); - module_cache = m_pcaches->get_or_make_module_cache(key); - } + if(m_modules.count(key)) throw std::invalid_argument("Key is in use"); + if(!base) + throw std::invalid_argument("Module implementation for key: '" + key + + "' is null"); - std::unique_ptr pimpl; + module_base_ptr stored = base; if(base->is_python()) { // This is a hacky patch to allow multiple python modules to be // added while avoiding the type_index collisions. // TODO: remove when a more elegant solution is determined m_py_bases[key] = base; - pimpl = std::make_unique(m_py_bases[key], module_cache); } else { std::type_index type(base->type()); if(!m_bases.count(type)) m_bases[type] = base; - pimpl = std::make_unique(m_bases[type], module_cache); + stored = m_bases[type]; } - auto ptr = std::make_shared(std::move(pimpl)); - ptr->set_name(key); - m_modules.emplace(std::move(key), ptr); + + Module mod(std::make_unique(stored)); + attach_(key, mod); + m_modules.add_module(std::move(key), std::move(mod)); } -inline void ModuleManagerPIMPL::copy_module(const type::key& old_key, +inline void ModuleManagerPIMPL::erase(const qualified_key& key) { + if(!count(key)) return; + get_modules(resolve_(key)).erase(key.key()); +} + +inline void ModuleManagerPIMPL::copy_module(const qualified_key& old_key, type::key new_key) { - assert_unique_key_(new_key); - Module mod = m_modules.at(old_key)->unlocked_copy(); - auto ptr = std::make_shared(std::move(mod)); - ptr->set_name(new_key); - m_modules.emplace(std::move(new_key), ptr); + m_modules.add_module(std::move(new_key), at(old_key)->unlocked_copy()); +} + +inline void ModuleManagerPIMPL::rename_module(const qualified_key& old_key, + type::key new_key) { + // Pin down which module is renamed before the copy can make old_key + // ambiguous + const qualified_key resolved(resolve_(old_key), old_key.key()); + copy_module(resolved, std::move(new_key)); + erase(resolved); } inline ModuleManagerPIMPL::shared_module ModuleManagerPIMPL::at( - const type::key& key) { - if(!count(key)) { - const std::string msg = - "ModuleManager has no module with key: '" + key + "'"; - throw std::out_of_range(msg); + const qualified_key& key) const { + return get_modules(resolve_(key)).at(key.key()); +} + +inline void ModuleManagerPIMPL::load_plugin(plugin_pointer plugin) { + if(!plugin) throw std::invalid_argument("Can not load a null plugin"); + + const auto& name = plugin->name(); + if(name.empty()) + throw std::invalid_argument( + "Plugins must have a name. Call set_name in the plugin's " + "constructor."); + if(has_plugin(name)) + throw std::invalid_argument("A plugin named '" + name + + "' is already loaded"); + + // Attach the plugin's modules, then every module reachable from them + // through submodules. A plugin may set a submodule and then erase that + // module's key; the module is still used, so it needs the runtime and + // caches too. + std::set visited; + std::vector to_visit; + for(auto& [key, mod] : plugin->m_modules_) { + attach_(qualified_key(name, key).to_string(), *mod); + visited.insert(mod.get()); + to_visit.push_back(mod.get()); + } + while(!to_visit.empty()) { + auto* mod = to_visit.back(); + to_visit.pop_back(); + for(auto& [callback, request] : mod->m_pimpl_->submods()) { + if(!request.has_module()) continue; + auto& submod = request.value(); + if(!visited.insert(&submod).second) continue; + + // The module is no longer in the plugin's map, so its cache is + // named after the key it was last stored under + const auto& last_key = + submod.has_name() ? submod.get_name() : callback; + attach_(qualified_key(name, last_key).to_string(), submod); + to_visit.push_back(&submod); + } + } + + m_plugins.push_back(std::move(plugin)); +} + +inline void ModuleManagerPIMPL::wire_defaults() { + for(const auto& plugin : m_plugins) { + auto& modules = plugin->m_modules_; + for(const auto& provider_name : plugin->preferred_providers()) { + const auto* provider = find_plugin_(provider_name); + if(provider == nullptr) continue; + + const auto& provided = provider->m_modules_; + for(const auto& record : + plugin->default_submods_from(provider_name)) { + // The user may have erased the module after loading + if(!modules.count(record.module_key)) continue; + + if(!provided.count(record.provider_key)) { + if(m_runtime_) + m_runtime_->logger().warn( + "Plugin '" + plugin->name() + "' asked for module '" + + record.provider_key + "' from plugin '" + + provider_name + "', which does not have it."); + continue; + } + + modules.default_submod(record.module_key, record.callback_key, + provided.at(record.provider_key)); + } + } } - return m_modules.at(key); +} + +inline bool ModuleManagerPIMPL::has_plugin( + const name_type& name) const noexcept { + return find_plugin_(name) != nullptr; +} + +inline ModuleManagerPIMPL::name_container_type +ModuleManagerPIMPL::plugin_names() const { + name_container_type rv{""}; + rv.reserve(m_plugins.size() + 1); + for(const auto& plugin : m_plugins) rv.push_back(plugin->name()); + return rv; +} + +inline const Plugin& ModuleManagerPIMPL::get_plugin( + const name_type& name) const { + const auto* plugin = find_plugin_(name); + if(plugin == nullptr) + throw std::out_of_range("ModuleManager has no plugin named: '" + name + + "'"); + return *plugin; +} + +inline ModuleManagerPIMPL::module_map& ModuleManagerPIMPL::get_modules( + const name_type& name) { + const auto& const_this = const_cast(*this); + return const_cast(const_this.get_modules(name)); +} + +inline const ModuleManagerPIMPL::module_map& ModuleManagerPIMPL::get_modules( + const name_type& name) const { + const auto* map = find_map_(name); + if(map == nullptr) + throw std::out_of_range("ModuleManager has no plugin named: '" + name + + "'"); + return *map; } inline bool ModuleManagerPIMPL::operator==( @@ -75,6 +200,7 @@ inline bool ModuleManagerPIMPL::operator==( // Try to get out early if(m_bases.size() != rhs.m_bases.size()) return false; if(m_modules.size() != rhs.m_modules.size()) return false; + if(m_plugins.size() != rhs.m_plugins.size()) return false; // TODO: Remove with the rest of the python hack if(m_py_bases.size() != rhs.m_py_bases.size()) return false; @@ -89,19 +215,80 @@ inline bool ModuleManagerPIMPL::operator==( } // Need to check the values b/c user may have switched options - for(const auto& [k, v] : rhs.m_modules) { - if(!m_modules.count(k)) return false; - if(*m_modules.at(k) != *v) return false; + if(m_modules != rhs.m_modules) return false; + + // Load order does not matter, so plugins are matched by name + for(const auto& plugin : rhs.m_plugins) { + const auto* mine = find_plugin_(plugin->name()); + if(mine == nullptr || *mine != *plugin) return false; } return true; } -inline ModuleManager::key_container_type ModuleManagerPIMPL::keys() const { - ModuleManager::key_container_type keys; - keys.reserve(m_modules.size()); - for(const auto& [k, v] : m_modules) keys.push_back(k); - return keys; +inline ModuleManagerPIMPL::key_container_type ModuleManagerPIMPL::keys() const { + key_container_type rv; + rv.reserve(size()); + for(const auto& name : plugin_names()) + for(const auto& key : get_modules(name).keys()) + rv.emplace_back(name, key); + return rv; +} + +// ----------------------------------------------------------------------------- +// -- Private Methods +// ----------------------------------------------------------------------------- + +inline Plugin* ModuleManagerPIMPL::find_plugin_( + const name_type& name) const noexcept { + using utilities::strings::case_insensitive_equal; + for(const auto& plugin : m_plugins) + if(case_insensitive_equal(plugin->name(), name)) return plugin.get(); + return nullptr; +} + +inline const ModuleManagerPIMPL::module_map* ModuleManagerPIMPL::find_map_( + const name_type& name) const noexcept { + if(name.empty()) return &m_modules; + const auto* plugin = find_plugin_(name); + return plugin ? &plugin->m_modules_ : nullptr; +} + +inline ModuleManagerPIMPL::name_type ModuleManagerPIMPL::resolve_( + const qualified_key& key) const { + const std::string no_module = + "ModuleManager has no module with key: '" + key.to_string() + "'"; + + if(key.is_qualified()) { + const auto* map = find_map_(key.plugin()); + if(map == nullptr) + throw std::out_of_range("ModuleManager has no plugin named: '" + + key.plugin() + "'"); + if(!map->count(key.key())) throw std::out_of_range(no_module); + return key.plugin(); + } + + name_container_type holders; + for(const auto& name : plugin_names()) + if(find_map_(name)->count(key.key())) holders.push_back(name); + + if(holders.empty()) throw std::out_of_range(no_module); + if(holders.size() > 1) { + std::string msg = "Module key '" + key.key() + + "' is ambiguous. It is used by the plugins:"; + for(const auto& name : holders) + msg += name.empty() ? " '' (the ModuleManager's own modules)" : + " '" + name + "'"; + msg += ". Pass the plugin name too, e.g., {\"" + holders.back() + + "\", \"" + key.key() + "\"}."; + throw std::invalid_argument(msg); + } + return holders.front(); +} + +inline void ModuleManagerPIMPL::attach_(const type::key& cache_key, + Module& mod) const { + mod.m_pimpl_->attach(m_runtime_, m_pcaches, cache_key); } } // namespace pluginplay::detail_ diff --git a/cxx/src/pluginplay/module_manager/export_module_manager.hpp b/cxx/src/pluginplay/module_manager/export_module_manager.hpp index 13cb6f673..a64fca83a 100644 --- a/cxx/src/pluginplay/module_manager/export_module_manager.hpp +++ b/cxx/src/pluginplay/module_manager/export_module_manager.hpp @@ -21,8 +21,10 @@ namespace pluginplay { void export_module_manager_class(py_module_reference m); void export_module_map(py_module_reference m); +void export_qualified_key(py_module_reference m); inline void export_module_manager(py_module_reference m) { + export_qualified_key(m); export_module_map(m); export_module_manager_class(m); } diff --git a/cxx/src/pluginplay/module_manager/export_module_manager_class.cpp b/cxx/src/pluginplay/module_manager/export_module_manager_class.cpp index 0c9745179..14e4a85a0 100644 --- a/cxx/src/pluginplay/module_manager/export_module_manager_class.cpp +++ b/cxx/src/pluginplay/module_manager/export_module_manager_class.cpp @@ -15,6 +15,7 @@ */ #include "../module/py_module_base.hpp" +#include "../plugin/plugin_view.hpp" #include "export_module_manager.hpp" #include #include @@ -24,7 +25,9 @@ namespace pluginplay { void export_module_manager_class(py_module_reference m) { - using at_fxn = Module& (ModuleManager::*)(const type::key&); + using qualified_key = typename ModuleManager::qualified_key; + using plugin_ptr = typename ModuleManager::plugin_pointer; + using load_fxn = void (ModuleManager::*)(plugin_ptr); using runtime_type = typename ModuleManager::runtime_type; using cache_type = typename ModuleManager::cache_type; using cache_pointer = typename ModuleManager::cache_pointer; @@ -43,19 +46,50 @@ void export_module_manager_class(py_module_reference m) { py::arg("rv"), py::arg("cache") = std::make_shared()) .def("count", &ModuleManager::count) .def("size", &ModuleManager::size) + .def("load_plugin", + [](py_obj self, py_obj plugin) { + // C++ plugins from other extension modules release ownership + // in their own module (see EXPORT_PLUGIN) + if(py::hasattr(plugin, "_pp_load_into")) { + plugin.attr("_pp_load_into")(self); + return; + } + auto& mm = self.cast(); + mm.load_plugin(plugin.cast()); + }) + .def("wire_defaults", &ModuleManager::wire_defaults) + .def("has_plugin", &ModuleManager::has_plugin) + .def("plugin_names", &ModuleManager::plugin_names) + // Python has no const, so read-only views are returned instead of the + // plugin and map themselves + .def( + "get_plugin", + [](const ModuleManager& self, const std::string& name) { + return PluginView(self.get_plugin(name)); + }, + py::keep_alive<0, 1>()) + .def( + "get_modules", + [](const ModuleManager& self, const std::string& name) { + return ModuleMapView(self.get_modules(name)); + }, + py::keep_alive<0, 1>()) .def( "add_module", [](ModuleManager& mm, std::string key, std::shared_ptr p) { mm.add_module(key, p); }, py::keep_alive<1, 3>()) - .def("at", static_cast(&ModuleManager::at), - py::return_value_policy::reference_internal) + // Modules are returned with shared ownership (see the Module bindings) + .def("at", + [](ModuleManager& self, const qualified_key& key) { + return self.at(key).shared_from_this(); + }) .def("copy_module", &ModuleManager::copy_module) .def("erase", &ModuleManager::erase) .def("rename_module", &ModuleManager::rename_module) .def("change_input", - [](ModuleManager& self, std::string mod_key, std::string opt_key, - py_obj in) { + [](ModuleManager& self, const qualified_key& mod_key, + std::string opt_key, py_obj in) { self.change_input(mod_key, opt_key, any::make_any_field(in)); }) @@ -72,8 +106,8 @@ void export_module_manager_class(py_module_reference m) { py::return_value_policy::reference_internal) .def("keys", &ModuleManager::keys) .def("has_cache", &ModuleManager::has_cache) - .def("__getitem__", [](ModuleManager& self, const type::key& key) { - return self.at(key); + .def("__getitem__", [](ModuleManager& self, const qualified_key& key) { + return self.at(key).shared_from_this(); }); m.def("defaulted_mm", []() { return ModuleManager(nullptr); }); } diff --git a/cxx/src/pluginplay/module_manager/export_module_map.cpp b/cxx/src/pluginplay/module_manager/export_module_map.cpp index 2502af13f..5a52c612b 100644 --- a/cxx/src/pluginplay/module_manager/export_module_map.cpp +++ b/cxx/src/pluginplay/module_manager/export_module_map.cpp @@ -16,21 +16,35 @@ #include "../module/py_module_base.hpp" #include "export_module_manager.hpp" +#include "module_map_view.hpp" +#include #include +#include namespace pluginplay { void export_module_map(py_module_reference m) { using key_type = ModuleMap::key_type; - // Module is bound with the default (unique_ptr) holder, so Python can't - // share ownership of a Module. Hence, only the key-based change_submod and - // default_submod overloads are exposed, and at returns a reference. + // Only the key-based change_submod and default_submod overloads are + // exposed. at returns the module with shared ownership. using change_by_key = void (ModuleMap::*)(const key_type&, const key_type&, const key_type&); using default_by_key = bool (ModuleMap::*)(const key_type&, const key_type&, const key_type&); + py_class_type(m, "ModuleMapView") + .def("count", &ModuleMapView::count) + .def("__contains__", &ModuleMapView::count) + .def("size", &ModuleMapView::size) + .def("__len__", &ModuleMapView::size) + .def("empty", &ModuleMapView::empty) + .def("keys", &ModuleMapView::keys) + .def("at", &ModuleMapView::at) + .def("has_submod", &ModuleMapView::has_submod) + .def(py::self == py::self) + .def(py::self != py::self); + py_class_type(m, "ModuleMap") .def(py::init<>()) .def("count", &ModuleMap::count) @@ -38,18 +52,22 @@ void export_module_map(py_module_reference m) { .def("__len__", &ModuleMap::size) .def("empty", &ModuleMap::empty) .def("keys", &ModuleMap::keys) - .def( - "at", - [](ModuleMap& self, const key_type& key) -> Module& { - return *self.at(key); - }, - py::return_value_policy::reference_internal) + .def("at", &ModuleMap::at) .def( "add_module", [](ModuleMap& self, key_type key, std::shared_ptr p) { self.add_module(std::move(key), p); }, py::keep_alive<1, 3>()) + .def("copy_module", &ModuleMap::copy_module) + .def("erase", &ModuleMap::erase) + .def("change_input", + [](ModuleMap& self, const key_type& key, const key_type& option, + py::object value) { + using python::PythonWrapper; + self.change_input(key, option, + any::make_any_field(value)); + }) .def("change_submod", static_cast(&ModuleMap::change_submod)) .def("has_submod", &ModuleMap::has_submod) diff --git a/cxx/src/pluginplay/module_manager/export_qualified_key.cpp b/cxx/src/pluginplay/module_manager/export_qualified_key.cpp new file mode 100644 index 000000000..adaeae6ab --- /dev/null +++ b/cxx/src/pluginplay/module_manager/export_qualified_key.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "export_module_manager.hpp" +#include +#include + +namespace pluginplay { + +void export_qualified_key(py_module_reference m) { + using name_type = QualifiedKey::name_type; + using key_type = QualifiedKey::key_type; + + py_class_type(m, "QualifiedKey") + .def(py::init()) + .def(py::init()) + .def(py::init([](const py::tuple& t) { + if(t.size() != 2) + throw std::invalid_argument( + "A QualifiedKey tuple must be (plugin name, module key)"); + return QualifiedKey(t[0].cast(), t[1].cast()); + })) + .def("is_qualified", &QualifiedKey::is_qualified) + .def("plugin", &QualifiedKey::plugin) + .def("key", &QualifiedKey::key) + .def("to_string", &QualifiedKey::to_string) + .def("__str__", &QualifiedKey::to_string) + .def("__repr__", + [](const QualifiedKey& self) { + return "QualifiedKey('" + self.to_string() + "')"; + }) + .def(py::self == py::self) + .def(py::self != py::self); + + // Lets Python pass "key" or ("plugin", "key") wherever a QualifiedKey is + // expected + py::implicitly_convertible(); + py::implicitly_convertible(); +} + +} // namespace pluginplay diff --git a/cxx/src/pluginplay/module_manager/module_manager_class.cpp b/cxx/src/pluginplay/module_manager/module_manager_class.cpp index 75dbef2ee..64eeed07f 100644 --- a/cxx/src/pluginplay/module_manager/module_manager_class.cpp +++ b/cxx/src/pluginplay/module_manager/module_manager_class.cpp @@ -20,13 +20,9 @@ namespace pluginplay { -using module_map = typename ModuleManager::module_map; using module_base_ptr = typename ModuleManager::module_base_ptr; -using cache_type = typename detail_::ModulePIMPL::cache_type; using runtime_type = typename ModuleManager::runtime_type; using runtime_ptr = typename ModuleManager::runtime_ptr; -template -using CIM = utilities::CaseInsensitiveMap; ModuleManager::ModuleManager() : pimpl_(std::make_unique()) {} @@ -36,36 +32,61 @@ ModuleManager::ModuleManager(ModuleManager&& rhs) noexcept = default; ModuleManager& ModuleManager::operator=(ModuleManager&& rhs) noexcept = default; ModuleManager::~ModuleManager() noexcept = default; -type::size ModuleManager::count(type::key key) const noexcept { +type::size ModuleManager::count(const qualified_key& key) const noexcept { return pimpl_->count(key); } +type::size ModuleManager::size() const noexcept { return pimpl_->size(); } + +void ModuleManager::load_plugin(plugin_pointer plugin) { + pimpl_->load_plugin(std::move(plugin)); +} + +void ModuleManager::wire_defaults() { pimpl_->wire_defaults(); } + +bool ModuleManager::has_plugin(const name_type& name) const noexcept { + return pimpl_->has_plugin(name); +} + +ModuleManager::name_container_type ModuleManager::plugin_names() const { + return pimpl_->plugin_names(); +} + +const Plugin& ModuleManager::get_plugin(const name_type& name) const { + return pimpl_->get_plugin(name); +} + +const ModuleMap& ModuleManager::get_modules(const name_type& name) const { + return const_cast(*pimpl_).get_modules( + name); +} + void ModuleManager::add_module(type::key key, module_base_ptr base) { pimpl_->add_module(std::move(key), base); } -void ModuleManager::erase(const type::key& key) { pimpl_->erase(key); } +void ModuleManager::erase(const qualified_key& key) { pimpl_->erase(key); } -void ModuleManager::copy_module(const type::key& old_key, type::key new_key) { +void ModuleManager::copy_module(const qualified_key& old_key, + type::key new_key) { pimpl_->copy_module(old_key, std::move(new_key)); } -void ModuleManager::change_submod(const type::key& module_key, +void ModuleManager::rename_module(const qualified_key& old_key, + type::key new_key) { + pimpl_->rename_module(old_key, std::move(new_key)); +} + +void ModuleManager::change_submod(const qualified_key& module_key, const type::key& callback_key, - const type::key& submod_key) { + const qualified_key& submod_key) { pimpl_->at(module_key)->change_submod(callback_key, pimpl_->at(submod_key)); } -const Module& ModuleManager::at(const type::key& module_key) const { +const Module& ModuleManager::at(const qualified_key& module_key) const { return *pimpl_->at(module_key); } -module_map::iterator ModuleManager::begin() noexcept { return pimpl_->begin(); } - -module_map::iterator ModuleManager::end() noexcept { return pimpl_->end(); } - -type::size ModuleManager::size() const noexcept { return pimpl_->size(); } - void ModuleManager::set_runtime(runtime_ptr runtime) noexcept { pimpl_->set_runtime(runtime); }; diff --git a/cxx/src/pluginplay/module_manager/module_map.cpp b/cxx/src/pluginplay/module_manager/module_map.cpp index 09fc4bac2..ae2ac2971 100644 --- a/cxx/src/pluginplay/module_manager/module_map.cpp +++ b/cxx/src/pluginplay/module_manager/module_map.cpp @@ -29,11 +29,23 @@ void ModuleMap::add_module(key_type key, module_base_ptr base) { "' is null"); auto pimpl = std::make_unique(base); - auto ptr = std::make_shared(std::move(pimpl)); + add_module(std::move(key), Module(std::move(pimpl))); +} + +void ModuleMap::add_module(key_type key, Module module) { + if(count(key)) + throw std::invalid_argument( + "ModuleMap already has a module with key: '" + key + "'"); + + auto ptr = std::make_shared(std::move(module)); ptr->set_name(key); m_modules_.emplace(std::move(key), std::move(ptr)); } +void ModuleMap::copy_module(const key_type& old_key, key_type new_key) { + add_module(std::move(new_key), at(old_key)->unlocked_copy()); +} + ModuleMap::module_pointer ModuleMap::at(const key_type& key) const { if(!count(key)) throw std::out_of_range("ModuleMap has no module with key: '" + key + diff --git a/cxx/src/pluginplay/module_manager/module_map_view.hpp b/cxx/src/pluginplay/module_manager/module_map_view.hpp new file mode 100644 index 000000000..48f8d154d --- /dev/null +++ b/cxx/src/pluginplay/module_manager/module_map_view.hpp @@ -0,0 +1,88 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +namespace pluginplay { + +/** @brief A read-only view of a ModuleMap, for the Python bindings. + * + * C++ can hand out a `const ModuleMap&`, but Python has no const, so a + * ModuleMap returned to Python could be changed through ModuleMap's + * bindings, *e.g.*, have modules added that never receive the + * ModuleManager's runtime and caches. Functions that return a read-only + * ModuleMap to Python return a ModuleMapView instead, which only exposes the + * read-only members. + * + * The view does not own the map; the bindings keep the map's owner alive for + * as long as the view exists. The modules themselves can still be changed + * (*e.g.*, their inputs), just as with ModuleManager.at, and at returns them + * with shared ownership. + */ +class ModuleMapView { +public: + /// Type used for module keys + using key_type = ModuleMap::key_type; + + /// Type of a container of module keys + using key_container_type = ModuleMap::key_container_type; + + /// Creates a view of @p map. No throw guarantee. + explicit ModuleMapView(const ModuleMap& map) noexcept : m_map_(&map) {} + + /// The map *this views. No throw guarantee. + const ModuleMap& map() const noexcept { return *m_map_; } + + /// Forwards to ModuleMap::count + type::size count(const key_type& key) const noexcept { + return m_map_->count(key); + } + + /// Forwards to ModuleMap::size + type::size size() const noexcept { return m_map_->size(); } + + /// Forwards to ModuleMap::empty + bool empty() const noexcept { return m_map_->empty(); } + + /// Forwards to ModuleMap::keys + key_container_type keys() const { return m_map_->keys(); } + + /// Forwards to ModuleMap::at + ModuleMap::module_pointer at(const key_type& key) const { + return m_map_->at(key); + } + + /// Forwards to ModuleMap::has_submod + bool has_submod(const key_type& module_key, + const key_type& callback_key) const { + return m_map_->has_submod(module_key, callback_key); + } + + /// True if the viewed maps are equal + bool operator==(const ModuleMapView& rhs) const { + return *m_map_ == *rhs.m_map_; + } + + /// Negates operator== + bool operator!=(const ModuleMapView& rhs) const { return !(*this == rhs); } + +private: + /// The map *this views + const ModuleMap* m_map_; +}; + +} // namespace pluginplay diff --git a/cxx/src/pluginplay/plugin/export_plugin_class.cpp b/cxx/src/pluginplay/plugin/export_plugin_class.cpp index 649d4abfb..a273de916 100644 --- a/cxx/src/pluginplay/plugin/export_plugin_class.cpp +++ b/cxx/src/pluginplay/plugin/export_plugin_class.cpp @@ -16,6 +16,7 @@ #include "../module/py_module_base.hpp" #include "export_plugin.hpp" +#include "plugin_view.hpp" #include "py_plugin.hpp" namespace pluginplay { @@ -37,8 +38,16 @@ void export_plugin_class(py_module_reference m) { static_cast(&PluginPublicist::set_preferred_providers); auto modules = static_cast(&PluginPublicist::modules); + py_class_type(m, "PluginView") + .def("name", &PluginView::name) + .def("preferred_providers", &PluginView::preferred_providers) + .def("default_submods_from", &PluginView::default_submods_from) + .def("get_modules", &PluginView::get_modules, py::keep_alive<0, 1>()) + .def(py::self == py::self) + .def(py::self != py::self); + auto py_plugin = - py_class_type>(m, "Plugin"); + py_class_type(m, "Plugin"); py_class_type(py_plugin, "DefaultSubmod") .def_readonly("module_key", &default_submod::module_key) @@ -51,8 +60,10 @@ void export_plugin_class(py_module_reference m) { .def("name", &Plugin::name) .def("preferred_providers", &Plugin::preferred_providers) .def("default_submods_from", &Plugin::default_submods_from) - .def("get_modules", &Plugin::get_modules, - py::return_value_policy::reference_internal) + .def( + "get_modules", + [](const Plugin& self) { return ModuleMapView(self.get_modules()); }, + py::keep_alive<0, 1>()) .def("set_name", &PluginPublicist::set_name) .def( "add_module", diff --git a/cxx/src/pluginplay/plugin/plugin_class.cpp b/cxx/src/pluginplay/plugin/plugin_class.cpp index c2d4e792d..62d9aafda 100644 --- a/cxx/src/pluginplay/plugin/plugin_class.cpp +++ b/cxx/src/pluginplay/plugin/plugin_class.cpp @@ -15,10 +15,12 @@ */ #include +#include #include #include #include #include +#include namespace pluginplay { @@ -35,19 +37,38 @@ bool Plugin::operator==(const Plugin& rhs) const { rhs.m_modules_); } +bool Plugin::is_valid_name(const name_type& name) noexcept { + if(name.empty()) return false; + const auto first = static_cast(name.front()); + if(!std::isalpha(first) && first != '_') return false; + return std::all_of(name.begin(), name.end(), [](unsigned char c) { + return std::isalnum(c) || c == '_'; + }); +} + void Plugin::set_name(name_type name) { if(name.empty()) throw std::invalid_argument( "Plugin names can not be empty. The empty name is reserved for the " "ModuleManager's own modules."); + if(!is_valid_name(name)) + throw std::invalid_argument( + "'" + name + + "' is not a valid plugin name. Plugin names must be identifiers: " + "letters, digits, and underscores, not starting with a digit."); m_name_ = std::move(name); } void Plugin::set_preferred_providers(provider_list_type names) { - std::set seen; + // Plugin names are case-insensitive + using name_less = utilities::CaseInsensitiveMap::key_compare; + std::set seen; for(const auto& name : names) { - if(name.empty()) - throw std::invalid_argument("Provider names can not be empty"); + if(!is_valid_name(name)) + throw std::invalid_argument( + "'" + name + + "' is not a valid plugin name, so it can not be a provider. " + "Plugin names must be identifiers."); if(!seen.insert(name).second) throw std::invalid_argument("Provider '" + name + "' is listed more than once"); @@ -63,9 +84,12 @@ void Plugin::set_preferred_providers(provider_list_type names) { void Plugin::default_submod_from(const name_type& provider, key_type module_key, key_type callback_key, key_type provider_key) { - auto begin = m_providers_.begin(); - auto end = m_providers_.end(); - if(std::find(begin, end, provider) == end) + auto begin = m_providers_.begin(); + auto end = m_providers_.end(); + auto matches = [&provider](const name_type& name) { + return utilities::strings::case_insensitive_equal(name, provider); + }; + if(std::find_if(begin, end, matches) == end) throw std::invalid_argument( "'" + provider + "' is not a preferred provider. Call set_preferred_providers first."); diff --git a/cxx/src/pluginplay/plugin/plugin_view.hpp b/cxx/src/pluginplay/plugin/plugin_view.hpp new file mode 100644 index 000000000..8fb89824d --- /dev/null +++ b/cxx/src/pluginplay/plugin/plugin_view.hpp @@ -0,0 +1,80 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include "../module_manager/module_map_view.hpp" +#include + +namespace pluginplay { + +/** @brief A read-only view of a Plugin, for the Python bindings. + * + * ModuleManager::get_plugin returns a `const Plugin&`. Python has no const, + * and the Plugin bindings include the setters a plugin written in Python + * uses in its constructor, so a loaded plugin returned to Python as a + * Plugin could be changed. ModuleManager.get_plugin returns a PluginView + * instead, which only exposes Plugin's read-only members. + * + * The view does not own the plugin; the bindings keep the plugin's owner + * alive for as long as the view exists. + */ +class PluginView { +public: + /// Type used for plugin names + using name_type = Plugin::name_type; + + /// Type of a list of plugin names + using provider_list_type = Plugin::provider_list_type; + + /// Type of a list of recorded default submodules + using default_list_type = Plugin::default_list_type; + + /// Creates a view of @p plugin. No throw guarantee. + explicit PluginView(const Plugin& plugin) noexcept : m_plugin_(&plugin) {} + + /// Forwards to Plugin::name + const name_type& name() const noexcept { return m_plugin_->name(); } + + /// Forwards to Plugin::preferred_providers + const provider_list_type& preferred_providers() const noexcept { + return m_plugin_->preferred_providers(); + } + + /// Forwards to Plugin::default_submods_from + const default_list_type& default_submods_from( + const name_type& provider) const noexcept { + return m_plugin_->default_submods_from(provider); + } + + /// A read-only view of the plugin's modules + ModuleMapView get_modules() const noexcept { + return ModuleMapView(m_plugin_->get_modules()); + } + + /// True if the viewed plugins are equal + bool operator==(const PluginView& rhs) const { + return *m_plugin_ == *rhs.m_plugin_; + } + + /// Negates operator== + bool operator!=(const PluginView& rhs) const { return !(*this == rhs); } + +private: + /// The plugin *this views + const Plugin* m_plugin_; +}; + +} // namespace pluginplay diff --git a/cxx/src/pluginplay/plugin/py_plugin.hpp b/cxx/src/pluginplay/plugin/py_plugin.hpp index 5bcd7e108..339f6725b 100644 --- a/cxx/src/pluginplay/plugin/py_plugin.hpp +++ b/cxx/src/pluginplay/plugin/py_plugin.hpp @@ -16,6 +16,7 @@ #pragma once #include +#include namespace pluginplay { @@ -23,9 +24,14 @@ namespace pluginplay { * * Plugin's constructor is protected, so pybind11 cannot create a Plugin * directly. PyPlugin makes the constructor public. Plugin has no virtual - * functions to forward to Python, so PyPlugin adds nothing else. + * functions to forward to Python. + * + * ModuleManager::load_plugin takes ownership of the plugin away from Python. + * Deriving from trampoline_self_life_support keeps the Python part of a + * plugin written in Python (*e.g.*, its Python modules) alive for as long as + * the ModuleManager owns the plugin. */ -class PyPlugin : public Plugin { +class PyPlugin : public Plugin, public pybind11::trampoline_self_life_support { public: PyPlugin() = default; }; diff --git a/cxx/src/pluginplay/printing/document_modules.cpp b/cxx/src/pluginplay/printing/document_modules.cpp index 71331e752..d2fbe23d3 100644 --- a/cxx/src/pluginplay/printing/document_modules.cpp +++ b/cxx/src/pluginplay/printing/document_modules.cpp @@ -32,27 +32,41 @@ void document_modules(pluginplay::ModuleManager& mm, // List of files for index file std::vector toc; - // Loop over key-value pairs of module names and module objects for docs - for(const auto& [name, module] : mm) { - std::string clean_name = - replace_invalid_chars(name, INVALID_CHARS, REPLACEMENT_CHAR); - - // Gather file name in the toc for the index file - toc.push_back(clean_name); - - // Generate documentation - std::string docs = document_module(name, *module); - - // Create the file path - std::filesystem::path file_path = docs_path; - file_path /= clean_name + ".rst"; - - fout.open(file_path); - - // Write to documentation file - fout << docs; - - fout.close(); + // Loop over the maps, then the key-value pairs of module names and module + // objects for docs. The ModuleManager's own modules go in docs_path; each + // plugin's modules go in a subdirectory named after the plugin. Plugin + // names are identifiers (see Plugin::is_valid_name), so they are already + // safe, distinct directory names. + for(const auto& plugin : mm.plugin_names()) { + std::filesystem::path dir = docs_path; + std::string toc_prefix; + if(!plugin.empty()) { + dir /= plugin; + std::filesystem::create_directories(dir); + toc_prefix = plugin + "/"; + } + + for(const auto& [name, module] : mm.get_modules(plugin)) { + std::string clean_name = + replace_invalid_chars(name, INVALID_CHARS, REPLACEMENT_CHAR); + + // Gather file name in the toc for the index file + toc.push_back(toc_prefix + clean_name); + + // Generate documentation + std::string docs = document_module(name, *module); + + // Create the file path + std::filesystem::path file_path = dir; + file_path /= clean_name + ".rst"; + + fout.open(file_path); + + // Write to documentation file + fout << docs; + + fout.close(); + } } // Create index file diff --git a/cxx/src/pluginplay/printing/mermaid.cpp b/cxx/src/pluginplay/printing/mermaid.cpp index 7a1e734ff..bbc2554e2 100644 --- a/cxx/src/pluginplay/printing/mermaid.cpp +++ b/cxx/src/pluginplay/printing/mermaid.cpp @@ -32,15 +32,17 @@ void add_letter(std::string& code) { } } -void print_submods(const std::string module_key, - const pluginplay::ModuleManager& mm, std::stringstream& ss, - std::string& module_code) { - const auto& mm_module = mm.at(module_key); // Results in a PluginPlay Module - const auto& submods = mm_module.submods(); // Results in list of Submodules - auto main_mod_code = module_code; +namespace { + +// Recurses over the submodules of mod. Submodules are followed through the +// Module objects, not looked up by key, because a submodule's key may be used +// by more than one plugin. +void print_submods_(const Module& mod, std::stringstream& ss, + std::string& module_code) { + auto main_mod_code = module_code; // Key is the ID/Tag, Value is the reference to the Module - for(const auto& [key, value] : submods) { + for(const auto& [key, value] : mod.submods()) { ss << "\n\t" << main_mod_code << "-->" << "|" << key << "| "; if(value.has_module() == false) { @@ -49,23 +51,33 @@ void print_submods(const std::string module_key, } else { add_letter(module_code); ss << module_code << "[" << value.get_name() << "]"; - print_submods(value.get_name(), mm, ss, module_code); + print_submods_(value.value(), ss, module_code); } } } +} // namespace + +void print_submods(const QualifiedKey& module_key, + const pluginplay::ModuleManager& mm, std::stringstream& ss, + std::string& module_code) { + print_submods_(mm.at(module_key), ss, module_code); +} + std::stringstream create_mermaid_graph(const pluginplay::ModuleManager& mm) { - auto n_modules = mm.size(); std::stringstream ss; std::string module_code = "AAA"; - if(n_modules == 0) { + const auto keys = mm.keys(); + if(keys.empty()) { ss << "No modules are loaded, load some modules!"; } else { - for(decltype(n_modules) i = 0; i < n_modules; i++) { + for(const auto& mod : keys) { + // The ModuleManager's own modules are labeled by key alone + const auto label = + mod.plugin().empty() ? mod.key() : mod.to_string(); ss << "\n```mermaid"; ss << "\nflowchart LR\n"; - auto mod = mm.keys()[i]; - ss << "\t" << module_code << "[" << mod << "]"; + ss << "\t" << module_code << "[" << label << "]"; print_submods(mod, mm, ss, module_code); ss << "\n```"; add_letter(module_code); diff --git a/docs/source/developer/design/pluginplay_initialization.rst b/docs/source/developer/design/pluginplay_initialization.rst index 8f612822b..0ad619523 100644 --- a/docs/source/developer/design/pluginplay_initialization.rst +++ b/docs/source/developer/design/pluginplay_initialization.rst @@ -105,7 +105,10 @@ its ``ModuleMap`` (see :ref:`plugin_design`). So the ``ModuleManager`` has one ``ModuleMap`` per loaded plugin, plus a ``ModuleMap`` of its own. The ``ModuleManager``'s own map holds modules that do not come from a plugin, such as modules added directly by the user, lambda modules, and facade -modules. The maps are **not** merged into one pool. A submodule may still be +modules. Copies the user makes through the ``ModuleManager`` (``copy_module`` +and ``rename_module``) also go in its own map, even when the original is one +of a plugin's modules, so a plugin's map only holds the modules the plugin +provides. The maps are **not** merged into one pool. A submodule may still be any module in any map, so links can cross maps. The empty plugin name, ``""``, is reserved for the ``ModuleManager``'s own @@ -116,7 +119,9 @@ Uniqueness and Lookup - ``ModuleMap`` enforces unique module keys within a plugin. - The ``ModuleManager`` enforces unique plugin names. Loading a second plugin - with the same name throws. + with the same name throws. Like module keys, plugin names are compared + case-insensitively, everywhere they are used: when loading, in lookups, and + when matching preferred providers. Together these guarantee that the pair (plugin name, module key) is unique. The ``ModuleManager`` therefore offers two ways to look up a module, and @@ -127,22 +132,84 @@ every function that takes a module key (``at``, ``change_input``, name means the ``ModuleManager``'s own map. #. With only a module key. This works as long as the key appears in exactly one map. If it appears in more than one map the call throws and the error - lists the plugins that contain the key. To support this, the - ``ModuleManager`` keeps track of which keys appear in more than one map. + lists the plugins that contain the key. + +Both forms are spelled with a ``QualifiedKey``, a small class holding an +optional plugin name and a module key. A ``QualifiedKey`` is implicitly +created from a string, which gives the key-only form, so the functions keep +a single signature and existing calls keep working: + +.. code-block:: c++ + + mm.at("SCF Driver"); // key-only + mm.at({"scf", "SCF Driver"}); // plugin name and module key + mm.change_submod({"scf", "SCF Driver"}, "Hamiltonian", + {"nux", "Born-Oppenheimer approximation"}); + +In Python, a string or a ``(plugin name, module key)`` tuple can be passed +wherever a ``QualifiedKey`` is expected. + +Note that ``{"", key}`` is a qualified key, not a key-only one. The two forms +look up different things: a key-only key searches every map, while ``{"", +key}`` only looks in the ``ModuleManager``'s own map. They find the same +module only when no plugin uses ``key``. The qualified form is the only way to +reach a module in the ``ModuleManager``'s own map when a plugin also uses its +key: + +.. code-block:: c++ + + mm.add_module("Shared"); // own map + mm.load_plugin(); // also has a "Shared" module + + mm.at("Shared"); // throws: ambiguous + mm.at({"", "Shared"}); // the own map's module + mm.at({"provider_a", "Shared"}); // ProviderA's module The key-only form keeps current code working, since today's module keys are effectively unique across the NWChemEx plugins. +The ``ModuleManager`` does not store which keys appear in more than one map. +Instead, a key-only lookup searches every map. There are only a handful of +plugins, so this is cheap, and a stored record could go out of date when the +user copies or erases a module after loading. + +``count(key)`` returns the number of maps with the key, ``size()`` returns +the total number of modules, and ``keys()`` returns the qualified key of +every module. The ``ModuleManager`` no longer offers ``begin()`` and +``end()``; code that needs the modules loops over ``plugin_names()`` and +calls ``get_modules(name)``, which returns that map. + +Caches +====== + +Memoized results are stored in module caches, which are looked up by name. +Two plugins may use the same module key, so a plugin's module uses the cache +named ``"::"``. Modules in the ``ModuleManager``'s +own map use the module key, as before. + +This naming is not collision-free: a module in the ``ModuleManager``'s own +map with the key ``"my_plugin::Area"`` shares a cache with the module +``"Area"`` of the plugin ``"my_plugin"``. Since plugin names must be +identifiers, plugin names can not collide this way, only module keys +containing ``"::"`` can. Rather than change the cache names of the +``ModuleManager``'s own modules, the user documentation asks developers to +avoid ``"::"`` in module keys. + +A plugin may set a submodule and then erase that module's key. The module is +still used, so ``load_plugin`` also gives every module reachable through +submodules the runtime and caches, naming its cache after the key it was last +stored under. + Loading Plugins =============== .. code-block:: c++ pluginplay::ModuleManager mm; - mm.load_plugin(); - mm.load_plugin(); - mm.load_plugin(); - mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); ``load_plugin()``: @@ -150,11 +217,12 @@ Loading Plugins constructible). ``T``'s constructor sets the plugin's name and preferred providers and builds its ``ModuleMap``, including its internal wiring, #. throws if a plugin with the same ``name()`` is already loaded, +#. gives each of the plugin's modules the ``ModuleManager``'s runtime and + caches, and #. takes ownership of the plugin object. The plugin's ``ModuleMap`` is now - the ``ModuleManager``'s map for that plugin; nothing is copied, and -#. updates the record of which keys appear in more than one map. + the ``ModuleManager``'s map for that plugin; nothing is copied. -Python has an equivalent, ``mm.load_plugin(scf.scf())``, which takes an +Python has an equivalent, ``mm.load_plugin(scf.SCF())``, which takes an instance because Python has no template parameters. Because the plugin holds live modules, the same instance must not be loaded into two ``ModuleManager`` objects. @@ -180,10 +248,12 @@ plugin ``P``: #. For each default triple: - - If ``P``'s module ``module_key`` has a submodule with key + - If the user erased ``P``'s module ``module_key``, move to the next + triple. + - If ``P``'s module ``module_key`` has a submodule with key ``callback_key`` that is already set, move to the next triple. - Else, if the provider does not have a module with key ``provider_key``, - move to the next triple. + log a warning and move to the next triple. - Otherwise set ``callback_key`` to the provider's module ``provider_key``. These changes are done to ``P``'s live map, so it includes any changes the user diff --git a/docs/source/developer/design/plugins.rst b/docs/source/developer/design/plugins.rst index 947f08576..8e9af072c 100644 --- a/docs/source/developer/design/plugins.rst +++ b/docs/source/developer/design/plugins.rst @@ -45,8 +45,8 @@ A plugin is loaded into a ``ModuleManager`` as a single unit. Why Do We Need a Plugin Class? ****************************** -The original approach was a set of free functions. ``DECLARE_PLUGIN`` and -``DEFINE_PLUGIN`` (in ``pluginplay/plugin/plugin.hpp``) declare and define a +The original approach was a set of free functions. The original +``DECLARE_PLUGIN`` and ``DEFINE_PLUGIN`` macros declared and defined a ``load_modules(ModuleManager&)`` function. By convention, most plugins also wrote a ``set_defaults(ModuleManager&)`` function for wiring that depended on other plugins. This approach has several problems: @@ -74,7 +74,13 @@ Plugin Considerations #. Identity - Each plugin has a name, *e.g.*, ``"my_plugin"``. + - Plugin names must be identifiers (letters, digits, and underscores, not + starting with a digit). Plugin names are used as directory names when + documenting modules and in the names of module caches, so they can not + contain spaces, path separators, ``"."``, or ``"::"``. - Plugin names must be unique within a ``ModuleManager``. + - Like module keys, plugin names are case-insensitive, *e.g.*, + ``"my_plugin"`` and ``"My_Plugin"`` are the same name. #. Module creation is separate from the ``ModuleManager`` @@ -256,7 +262,9 @@ Notes on the members: - **One instance per ModuleManager.** Since a plugin holds live modules, a plugin object must not be loaded into two ``ModuleManager`` objects. ``load_plugin()`` creates a new ``T`` each time, so this only matters - for the Python form, which takes an instance. + for the Python form, which takes an instance. ``load_plugin`` takes the + plugin by ``std::unique_ptr``, and in Python it takes ownership from the + Python object, so loading the same instance again throws. - **Provenance.** Setters for version and citations would follow the same convention, *e.g.*, ``set_version`` and ``add_citation`` in the constructor, mirroring ``ModuleBase::citation``. @@ -282,6 +290,12 @@ type as the current ``ModuleManager::module_map``. ``ModuleMap`` provides: - ``default_submod(...)``, a convenience function with the same overloads as ``change_submod``. It sets the submodule only if it is unset. The ``ModuleManager`` uses it to apply ``default_submod_from`` records. +- ``copy_module(old_key, new_key)``, ``change_input(key, option, value)``, + and ``erase(key)``, which mirror the ``ModuleManager`` functions. They let + a plugin set up variants of its own modules, *e.g.*, a copy of a module + with a tighter threshold. +- ``add_module(key, Module)``, which stores an already-created module, *e.g.*, + a copy of a module. ``ModuleMap`` enforces that module keys are unique within the map, which means they are unique within a plugin. Uniqueness across plugins is the @@ -291,11 +305,15 @@ Declaring a Plugin ================== The plugin macros mirror the module macros (``DECLARE_MODULE``, -``MODULE_CTOR``, and ``MODULE_RUN``): +``MODULE_CTOR``, and ``MODULE_RUN``). They are declared in +``pluginplay/plugin/plugin.hpp``: - ``DECLARE_PLUGIN(plugin_name)`` declares a class ``plugin_name`` that - derives from ``Plugin`` and has a default constructor. + derives from ``Plugin`` and has a default constructor. Like any other class, + ``plugin_name`` is CamelCase, *e.g.*, ``MyPlugin``. - ``PLUGIN_CTOR(plugin_name)`` starts the definition of the constructor. +- ``EXPORT_PLUGIN(module_name, plugin_name, m)`` creates the Python module + for the plugin (see `Python`_). A plugin's constructor can also pass ``modules()`` to helper functions that take a ``ModuleMap&``. This lets a large plugin split module registration @@ -361,11 +379,29 @@ Python support follows the pattern used for modules: ``set_name``, ``add_module``, ``change_submod``, ``set_preferred_providers``, and ``default_submod_from``, just as a Python module's ``__init__`` calls ``description`` and ``add_input``. -- ``EXPORT_PLUGIN(plugin_name, m)`` exports the C++ plugin class to Python +- ``EXPORT_PLUGIN(module_name, plugin_name, m)`` creates the Python module + ``module_name`` and exports the C++ plugin class to it as ``plugin_name`` (instead of a ``load_modules`` function), so a Python user can pass an - instance to ``ModuleManager.load_plugin``. + instance to ``ModuleManager.load_plugin``. The Python module and the plugin + class have different names because the module name is lowercase, while the + class name follows the CamelCase convention for classes. The macro is + followed by a function body that can export anything else the module needs, + *e.g.*, property types: + + .. code-block:: c++ + + EXPORT_PLUGIN(my_plugin, MyPlugin, m) { + export_property_types(m); + } - ``ModuleMap`` is exported so Python plugins can use ``default_submod`` and related functions. +- Python has no ``const``. Functions that return a read-only plugin or map in + C++ (``ModuleManager::get_plugin``, ``ModuleManager::get_modules``, and + ``Plugin::get_modules``) return a ``PluginView`` or ``ModuleMapView`` in + Python instead. The views only expose the read-only members, so a loaded + plugin can not be changed through them, *e.g.*, by adding a module that + never receives the ``ModuleManager``'s runtime and caches. The modules + themselves can still be changed, as with ``ModuleManager.at``. ********************** Further Considerations diff --git a/docs/source/tutorials/plugins/document_modules.rst b/docs/source/tutorials/plugins/document_modules.rst index 8902b89d7..56a1686c0 100644 --- a/docs/source/tutorials/plugins/document_modules.rst +++ b/docs/source/tutorials/plugins/document_modules.rst @@ -18,7 +18,7 @@ Documenting Modules in a Collection This page shows how to document the modules in your module collection using the ``document_modules`` function in the SDE. This page assumes that you can create -a module collection according to :doc:`./overview` and :doc:`load_modules`. +a module collection according to :doc:`./overview` and :doc:`writing_a_plugin`. Generate the Documentation ========================== @@ -29,22 +29,21 @@ documentation program code below in :ref:`example-module-doc-gen`. Currently, documentation generation is expected to be performed in its own binary, which we will refer to as ``build_docs`` here. This program must -include both the ``sde::printing::document_modules`` function and the -``load_modules`` function defined for your modules. +include both the ``pluginplay::printing::document_modules`` function and the +header declaring your plugin. .. literalinclude:: ../../../../tests/cxx/doc_snippets/generate_module_documentation.cpp :language: c++ :lines: 17, 19-21 -Next, set up a ``sde::ModuleManager`` and load your modules with the -``load_modules`` you wrote. +Next, set up a ``pluginplay::ModuleManager`` and load your plugin. .. literalinclude:: ../../../../tests/cxx/doc_snippets/generate_module_documentation.cpp :language: c++ :lines: 33-37 The final step is to generate documentation through -``sde::printing::document_modules``, by providing your ``ModuleManager`` +``pluginplay::printing::document_modules``, by providing your ``ModuleManager`` instance (``mm`` here) and the ``std::filesystem::path`` to an existing directory where the documentation should be output (``doc_path`` here). How this path is generated is a detail left to the developer's discretion. For simplicity @@ -61,7 +60,8 @@ Example Program =============== The following code is a minimal example program generating documentation for -the test modules in SDE. Modify the code according to the TODO comments to +the test modules in PluginPlay. A plugin's modules are documented in a +subdirectory of the output directory, named after the plugin. Modify the code according to the TODO comments to generate documentation for your modules. .. literalinclude:: ../../../../tests/cxx/doc_snippets/generate_module_documentation.cpp diff --git a/docs/source/tutorials/plugins/index.rst b/docs/source/tutorials/plugins/index.rst index fd8c8c1c2..7da203f14 100644 --- a/docs/source/tutorials/plugins/index.rst +++ b/docs/source/tutorials/plugins/index.rst @@ -32,5 +32,5 @@ collection. :caption: Contents: overview - load_modules + writing_a_plugin document_modules diff --git a/docs/source/tutorials/plugins/load_modules.rst b/docs/source/tutorials/plugins/load_modules.rst deleted file mode 100644 index 360f9c066..000000000 --- a/docs/source/tutorials/plugins/load_modules.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. Copyright 2022 NWChemEx-Project -.. -.. Licensed under the Apache License, Version 2.0 (the "License"); -.. you may not use this file except in compliance with the License. -.. You may obtain a copy of the License at -.. -.. http://www.apache.org/licenses/LICENSE-2.0 -.. -.. Unless required by applicable law or agreed to in writing, software -.. distributed under the License is distributed on an "AS IS" BASIS, -.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -.. See the License for the specific language governing permissions and -.. limitations under the License. - -*************************** -Loading a Module Collection -*************************** - -The key piece of a module collection is the ``load_modules`` function. When a -user starts a run with PluginPlay one of the first things they do is initialize -a ``ModuleManager`` instance. The code to do this looks something like: - -.. code-block:: c++ - - // Initialize ModuleManager - ModuleManager mm; - library1::load_modules(mm); - library2::load_modules(mm); - -where ``load_modules(mm)`` calls load the module collections provided by the -various libraries. - -Public API -========== - -As far as the public API for ``load_modules`` goes, the declaration is simple: - -.. literalinclude:: ../../../../tests/cxx/doc_snippets/load_modules.hpp - :language: c++ - -In particular note that by forward declaring the ``load_modules`` function it is -not necessary for the user to know the types of your modules and more -importantly any libraries you use to implement them. - -.. note:: - - If your modules use property types defined elsewhere (such as in centralized - community resources like NWChemEx/SimDE) and your - module-specific inputs are standard C++ types (or types found in the - property types) this is your entire public API!!! - -Implementation -============== - -The actual implementation of ``load_modules`` typically consists of two parts: -loading the modules and setting the default submodules. - -Modules are loaded into the ``ModuleManager`` by calling ``add_module``. For -your convenience the ``ModuleManager`` has an overload of ``add_module`` which -takes a template type parameter that is the type of your module (as in the name -of the class which contains the algorithm) and a module key. The module key is -how users will refer to your module, *e.g.*, when they want to run your module -they will call ``ModuleManager::run_as`` providing your module's key. If your -module requires more detailed set-up other overloads exist that will bind to -existing instances. - -Continuing with our electric field examples, adding our modules to the -``ModuleManager`` looks like: - -.. literalinclude:: ../../../../tests/cxx/doc_snippets/load_modules.cpp - :language: c++ - :lines: 17-27,30-32 - - -Once you have added all your modules the next step is to set the default -submodules. In our examples, the ``ClassicalForce`` module needed a submodule -which can compute electric fields. Presently we have already loaded three such -modules; their keys are: - -- "Coulomb's Law" -- "Coulomb's Law with screening" -- "Single-precision Coulomb's law" - -If by default we want the ``ClassicalForce`` module we loaded under the key -"Force" to use a ``CoulombsLaw`` module we can accomplish this by: - -.. literalinclude:: ../../../../tests/cxx/doc_snippets/load_modules.cpp - :language: c++ - :lines: 29 - -.. note:: - - It is not necessary to set all submodules in the ``load_modules`` function. - In particular if your module depends on a property that none of your modules - can compute you would not set that submodule. In this case, it will be the - responsability of the caller to set the submodule. - -The entire implementation of our ``load_modules`` function looks like: - -.. literalinclude:: ../../../../tests/cxx/doc_snippets/load_modules.cpp - :language: c++ diff --git a/docs/source/tutorials/plugins/overview.rst b/docs/source/tutorials/plugins/overview.rst index f6fd20ea1..77a059961 100644 --- a/docs/source/tutorials/plugins/overview.rst +++ b/docs/source/tutorials/plugins/overview.rst @@ -44,12 +44,12 @@ code will look something like: where: - the ``include/`` directory contains the public API, -- ``our_module.hpp`` includes the ``load_modules`` function (TODO: add link) and - any other headers needed to use the collection, +- ``our_module.hpp`` declares the plugin (see :doc:`writing_a_plugin`) and + includes any other headers needed to use the collection, - the ``src/`` directory contains the implementation of the collection, - ``module1.cpp`` and ``module2.cpp`` are the implementation of the modules, - ``modules.hpp`` contains the declaration of the modules, and -- ``our_module.cpp`` contains the definition of the ``load_modules`` function. +- ``our_module.cpp`` contains the definition of the plugin's constructor. In practice you will also have build system files, readmes, licenses, unit tests (hopefully), etc, but from the perspective of the pluginplay those are the key parts. diff --git a/docs/source/tutorials/plugins/writing_a_plugin.rst b/docs/source/tutorials/plugins/writing_a_plugin.rst new file mode 100644 index 000000000..68330715d --- /dev/null +++ b/docs/source/tutorials/plugins/writing_a_plugin.rst @@ -0,0 +1,171 @@ +.. Copyright 2022 NWChemEx-Project +.. +.. Licensed under the Apache License, Version 2.0 (the "License"); +.. you may not use this file except in compliance with the License. +.. You may obtain a copy of the License at +.. +.. http://www.apache.org/licenses/LICENSE-2.0 +.. +.. Unless required by applicable law or agreed to in writing, software +.. distributed under the License is distributed on an "AS IS" BASIS, +.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +.. See the License for the specific language governing permissions and +.. limitations under the License. + +################ +Writing a Plugin +################ + +A module collection is distributed as a plugin. When a user starts a run with +PluginPlay one of the first things they do is initialize a ``ModuleManager`` +instance and load the plugins they want to use. The code to do this looks +something like: + +.. code-block:: c++ + + // Initialize ModuleManager + ModuleManager mm; + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + +where the ``load_plugin`` calls load the plugins provided by the various +libraries and ``wire_defaults`` sets the default submodules the plugins want +from each other (see `Defaults From Other Plugins`_). + +Public API +========== + +As far as the public API for a plugin goes, the declaration is simple: + +.. literalinclude:: ../../../../tests/cxx/doc_snippets/pluginplay_examples.hpp + :language: c++ + :lines: 17- + +``DECLARE_PLUGIN`` declares a class (here ``PluginplayExamples``) that derives +from ``pluginplay::Plugin`` and has a default constructor. Note that it is not +necessary for the user to know the types of your modules and more importantly +any libraries you use to implement them. + +.. note:: + + If your modules use property types defined elsewhere (such as in centralized + community resources like NWChemEx/SimDE) and your + module-specific inputs are standard C++ types (or types found in the + property types) this is your entire public API!!! + +Implementation +============== + +Plugins are written like modules: everything is set up in the plugin's +constructor, which is started with the ``PLUGIN_CTOR`` macro. The constructor +typically does three things: names the plugin, adds the plugin's modules, and +sets the default submodules. + +Every plugin has a name, set by calling ``set_name``. Plugin names must be +identifiers: letters, digits, and underscores, not starting with a digit, +*e.g.*, ``"pluginplay_examples"``. Plugin names must be unique within a +``ModuleManager``. Like module keys, plugin names are case-insensitive. + +Modules are added to the plugin by calling ``add_module``. For your +convenience ``add_module`` has an overload which takes a template type +parameter that is the type of your module (as in the name of the class which +contains the algorithm) and a module key. The module key is how users will +refer to your module, *e.g.*, when they want to run your module they will call +``ModuleManager::run_as`` providing your module's key. Module keys must be +unique within your plugin. If your module requires more detailed set-up other +overloads exist that will bind to existing instances. + +.. note:: + + Avoid using ``"::"`` in module keys. PluginPlay names the caches of a + plugin's modules ``"::"``, and caches of modules + added directly to the ``ModuleManager`` after the module key alone. A + module key containing ``"::"`` can therefore end up sharing a cache with a + different module, *e.g.*, a module added to the ``ModuleManager`` under + ``"my_plugin::Area"`` and the module ``"Area"`` of the plugin + ``"my_plugin"``. + +Continuing with our electric field examples, naming our plugin and adding our +modules looks like: + +.. literalinclude:: ../../../../tests/cxx/doc_snippets/pluginplay_examples.cpp + :language: c++ + :lines: 17-29 + +Once you have added all your modules the next step is to set the default +submodules. In our examples, the ``ClassicalForce`` module needed a submodule +which can compute electric fields. Presently we have already loaded three such +modules; their keys are: + +- "Coulomb's Law" +- "Coulomb's Law with screening" +- "Single-precision Coulomb's law" + +If by default we want the ``ClassicalForce`` module we loaded under the key +"Classical Force" to use a ``CoulombsLaw`` module we can accomplish this by: + +.. literalinclude:: ../../../../tests/cxx/doc_snippets/pluginplay_examples.cpp + :language: c++ + :lines: 31 + +.. note:: + + It is not necessary to set all submodules in the plugin's constructor. + In particular if your module depends on a property that none of your modules + can compute you would not set that submodule. In this case, the submodule + can come from another plugin (see `Defaults From Other Plugins`_), or it + will be the responsibility of the caller to set the submodule. + +The entire implementation of our plugin looks like: + +.. literalinclude:: ../../../../tests/cxx/doc_snippets/pluginplay_examples.cpp + :language: c++ + :lines: 17- + +Defaults From Other Plugins +=========================== + +A plugin can not set a submodule to a module from another plugin in its +constructor, because the other plugin may not be loaded. Instead, the plugin +lists the plugins it would like defaults from, highest priority first, and +records which of their modules it would like to use: + +.. code-block:: c++ + + PLUGIN_CTOR(MyPlugin) { + set_name("my_plugin"); + set_preferred_providers("provider_a", "provider_b"); + add_module("My driver"); + + default_submod_from("provider_a", "My driver", "Data", "A data"); + default_submod_from("provider_b", "My driver", "Data", "B data"); + } + +When the user calls ``ModuleManager::wire_defaults``, the ``"Data"`` +submodule of ``"My driver"`` is set to ``"A data"`` if ``provider_a`` is +loaded, otherwise to ``"B data"`` if ``provider_b`` is loaded. Submodules the +user has already set are never changed. + +Python +====== + +To make your plugin usable from Python, create its Python module with the +``EXPORT_PLUGIN`` macro. The macro takes the name of the Python module, the +name of the plugin class, and the name of the pybind11 module variable. It is +followed by the body of a function which can export anything else the Python +module needs, *e.g.*, property types: + +.. literalinclude:: ../../../../tests/python/doc_snippets/export_pluginplay_examples.cpp + :language: c++ + :lines: 24-39 + +Python users then load the plugin with: + +.. code-block:: python + + import pluginplay as pp + import pluginplay_examples as ppe + + mm = pp.ModuleManager() + mm.load_plugin(ppe.PluginplayExamples()) diff --git a/pyproject.toml b/pyproject.toml index f64492a53..9836eadc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires = [ # [project].dependencies; leave the floors unbounded so the newest # release is always resolved. "nwchemex-nwxcmake>=0.1.0", - "nwchemex-utilities>=0.1.45", + "nwchemex-utilities>=0.1.49", "nwchemex-parallelzone>=0.1.43", ] build-backend = "scikit_build_core.build" @@ -39,7 +39,7 @@ dependencies = [ # registries and breaks cross-module type exchange. "pybind11==3.1.0", "nwchemex-nwxcmake>=0.1.0", - "nwchemex-utilities>=0.1.45", + "nwchemex-utilities>=0.1.49", "nwchemex-parallelzone>=0.1.43", ] diff --git a/tests/cxx/doc_snippets/generate_module_documentation.cpp b/tests/cxx/doc_snippets/generate_module_documentation.cpp index 90ba945bc..3df5c80ba 100644 --- a/tests/cxx/doc_snippets/generate_module_documentation.cpp +++ b/tests/cxx/doc_snippets/generate_module_documentation.cpp @@ -17,8 +17,8 @@ #include #include -// TODO: Replace with the header containing your ``load_modules`` definition -#include "load_modules.hpp" +// TODO: Replace with the header declaring your plugin +#include "pluginplay_examples.hpp" namespace fs = std::filesystem; @@ -33,8 +33,8 @@ int main(int argc, char** argv) { // Set up module manager pluginplay::ModuleManager mm; - // TODO: Load your modules - pluginplay_examples::load_modules(mm); + // TODO: Load your plugin + mm.load_plugin(); // Generate module documentation at the given path pluginplay::printing::document_modules(mm, docs_path); diff --git a/tests/cxx/doc_snippets/load_modules.cpp b/tests/cxx/doc_snippets/pluginplay_examples.cpp similarity index 64% rename from tests/cxx/doc_snippets/load_modules.cpp rename to tests/cxx/doc_snippets/pluginplay_examples.cpp index f165bb61d..b75fcdb87 100644 --- a/tests/cxx/doc_snippets/load_modules.cpp +++ b/tests/cxx/doc_snippets/pluginplay_examples.cpp @@ -14,19 +14,21 @@ * limitations under the License. */ -#include "load_modules.hpp" #include "modules.hpp" +#include "pluginplay_examples.hpp" namespace pluginplay_examples { -void load_modules(pluginplay::ModuleManager& mm) { +PLUGIN_CTOR(PluginplayExamples) { + set_name("pluginplay_examples"); + using coulombs_law_float = TemplatedCoulombsLaw; - mm.add_module("Classical Force"); - mm.add_module("Coulomb's Law"); - mm.add_module("Coulomb's Law with screening"); - mm.add_module("Single-precision Coulomb's law"); + add_module("Classical Force"); + add_module("Coulomb's Law"); + add_module("Coulomb's Law with screening"); + add_module("Single-precision Coulomb's law"); - mm.change_submod("Classical Force", "electric field", "Coulomb's Law"); + change_submod("Classical Force", "electric field", "Coulomb's Law"); } } // namespace pluginplay_examples diff --git a/tests/cxx/doc_snippets/load_modules.hpp b/tests/cxx/doc_snippets/pluginplay_examples.hpp similarity index 93% rename from tests/cxx/doc_snippets/load_modules.hpp rename to tests/cxx/doc_snippets/pluginplay_examples.hpp index 110630eda..b4d1480bf 100644 --- a/tests/cxx/doc_snippets/load_modules.hpp +++ b/tests/cxx/doc_snippets/pluginplay_examples.hpp @@ -19,6 +19,6 @@ namespace pluginplay_examples { -void load_modules(pluginplay::ModuleManager& mm); +DECLARE_PLUGIN(PluginplayExamples); } diff --git a/tests/cxx/doc_snippets/test_modules.cpp b/tests/cxx/doc_snippets/test_modules.cpp index 26e3ddd30..77673f27e 100644 --- a/tests/cxx/doc_snippets/test_modules.cpp +++ b/tests/cxx/doc_snippets/test_modules.cpp @@ -16,8 +16,8 @@ #include "electric_field.hpp" #include "force.hpp" -#include "load_modules.hpp" #include "modules.hpp" +#include "pluginplay_examples.hpp" #include #include #include @@ -26,8 +26,8 @@ using namespace pluginplay_examples; TEST_CASE("Tutorial modules") { // Set up module manager pluginplay::ModuleManager mm; - // Load your modules - load_modules(mm); + // Load your plugin + mm.load_plugin(); REQUIRE(mm.count("Classical Force") == 1); REQUIRE(mm.count("Coulomb's Law") == 1); REQUIRE(mm.count("Coulomb's Law with screening") == 1); diff --git a/tests/cxx/doc_snippets/test_tutorial_module_documents.cpp b/tests/cxx/doc_snippets/test_tutorial_module_documents.cpp index 8a153feeb..e972b05e9 100644 --- a/tests/cxx/doc_snippets/test_tutorial_module_documents.cpp +++ b/tests/cxx/doc_snippets/test_tutorial_module_documents.cpp @@ -21,7 +21,7 @@ #include #include -#include "load_modules.hpp" +#include "pluginplay_examples.hpp" TEST_CASE("tutorial_modules_docs") { // Need a docs path @@ -45,15 +45,23 @@ TEST_CASE("tutorial_modules_docs") { pluginplay::ModuleManager mm; SECTION("Modules Loaded") { - // Load your modules - pluginplay_examples::load_modules(mm); + // Load your plugin + mm.load_plugin(); // Generate module documentation at the given path pluginplay::printing::document_modules(mm, docs_path); + // docs_path holds index.rst and the plugin's directory + auto plugin_path = docs_path / "pluginplay_examples"; + REQUIRE(std::filesystem::exists(docs_path / "index.rst")); + REQUIRE(std::filesystem::is_directory(plugin_path)); + REQUIRE(std::distance(std::filesystem::directory_iterator(docs_path), + std::filesystem::directory_iterator{}) == 2); + + // The plugin's directory holds one file per module decltype(mm.size()) distance( - std::distance(std::filesystem::directory_iterator(docs_path), + std::distance(std::filesystem::directory_iterator(plugin_path), std::filesystem::directory_iterator{})); - REQUIRE(distance == mm.size() + 1); // +1 for index.rst + REQUIRE(distance == mm.size()); } } diff --git a/tests/cxx/unit_tests/pluginplay/examples/lambda_module.cpp b/tests/cxx/unit_tests/pluginplay/examples/lambda_module.cpp index d5a71edd0..7d2db4920 100644 --- a/tests/cxx/unit_tests/pluginplay/examples/lambda_module.cpp +++ b/tests/cxx/unit_tests/pluginplay/examples/lambda_module.cpp @@ -48,7 +48,7 @@ // For the purposes of this tutorial we reuse the property types from our // property type tutorial and the modules from our module tutorial. The next // two lines include the relevant header files -#include "load_modules.hpp" +#include "writing_a_plugin.hpp" #include // TUTORIAL_START_SKIP @@ -96,7 +96,7 @@ TEST_CASE("How to use a lambda module") { // instance and then retrieves the relevant modules for this tutorial: pluginplay::ModuleManager mm; - load_modules(mm); + mm.load_plugin(); // At this point volume_mod's "area" submodule is set to Rectangle auto volume_mod = mm.at("Prism"); diff --git a/tests/cxx/unit_tests/pluginplay/examples/load_modules.hpp b/tests/cxx/unit_tests/pluginplay/examples/load_modules.hpp deleted file mode 100644 index f3d6bf001..000000000 --- a/tests/cxx/unit_tests/pluginplay/examples/load_modules.hpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2022 NWChemEx-Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* The purpose of this tutorial is to showcase the packaging of module groups. - * - * For all intents an purposes the ``ModuleManager`` class can be thought of as - * the code embodiment of the pluginplay. It is responsible for storing all of - * the available modules, assembling the call graph, integrating the modules - * into the actual runtime, and checkpointing the progress of the program. To - * get the ``ModuleManager`` class one includes ``ModuleManager.hpp``. The - * remainder of the following header files are needed for this tutorial. - */ -#pragma once -#include "writing_a_module.hpp" -#include "writing_a_property_type.hpp" -#include - -/* Populating the ModuleManager - * ---------------------------- - * - * Before the ``ModuleManager`` can manage some modules, it needs modules. The - * primary means of providing the ``ModuleManager`` with modules is manually. - * While this sounds tedious, and it is, it only needs to be done once. To this - * end libraries meant for use with the pluginplay will provide a function - * ``load_modules`` which takes a ``ModuleManager`` instance and adds to it - * the modules contained within that library. It is also typically during this - * function call that default modules are assigned. - * - * The following function indicates how this would be done assuming that our - * ``Area``, ``PrismVolume``, ``Rectangle`` and ``Prism`` classes are part of a - * library meant for use with the pluginplay. - */ -inline void load_modules(pluginplay::ModuleManager& mm) { - /* The function starts off by registering the two modules provided by our - * library. Registration involves two things: - * - * - creation of an instance of the module - * - association of that instance with a module key - * - * By taking an already instantiated object, the pluginplay remains - * decoupled from the procedure for initializing the module. It remains - * decoupled from the modules' C++ types (here ``Rectangle`` and ``Prism``) - * by taking the objects via pointers to the ``ModuleBase`` class it - * necessarily inherits from. To avoid gotchas related to handling - * polymorphic data types the pluginplay requires that the user provide the - * module as a ``shared_ptr``. The provided key can be used to retrieve the - * module (*vide infra*). The key must be unique (if it's not an error will - * be raised). - */ - mm.add_module("Rectangle", std::make_shared()); - mm.add_module("Prism", std::make_shared()); - - /* Our ``Prism`` module needs a submodule which computes an ``Area``. Since - * our library provides one, we set it as the default submodule. Users can - * still choose a different submodule later. - */ - mm.change_submod("Prism", "area", "Rectangle"); -} // end load_modules() diff --git a/tests/cxx/unit_tests/pluginplay/examples/workflow.cpp b/tests/cxx/unit_tests/pluginplay/examples/workflow.cpp index d7ccc6374..4f9407db7 100644 --- a/tests/cxx/unit_tests/pluginplay/examples/workflow.cpp +++ b/tests/cxx/unit_tests/pluginplay/examples/workflow.cpp @@ -18,9 +18,9 @@ #include "workflow.hpp" #include -TEST_CASE("load_modules") { +TEST_CASE("load_plugin") { pluginplay::ModuleManager mm; - load_modules(mm); + mm.load_plugin(); REQUIRE(mm.count("Rectangle") == 1); REQUIRE(mm.count("Prism") == 1); } diff --git a/tests/cxx/unit_tests/pluginplay/examples/workflow.hpp b/tests/cxx/unit_tests/pluginplay/examples/workflow.hpp index 07591adfe..b35fd0df4 100644 --- a/tests/cxx/unit_tests/pluginplay/examples/workflow.hpp +++ b/tests/cxx/unit_tests/pluginplay/examples/workflow.hpp @@ -25,25 +25,27 @@ * remainder of the following header files are needed for this tutorial. */ #pragma once -#include "load_modules.hpp" +#include "writing_a_plugin.hpp" #include /* Running a Computation * --------------------- * - * The ``load_modules()`` functions are provided by libraries designed to - * interact with the pluginplay. You typically will not be instantiating them, - * rather you will simply be consuming them in your workflow. The following + * Plugins are provided by libraries designed to interact with pluginplay. + * You typically will not be writing them, rather you will simply be consuming + * them in your workflow. The following * function is an example workflow that a end-user would actually do. Typically * it would be run from Python (the contents being nearly identical). */ static void example_main() { /* Ultimately the run starts by creating and initializing a ``ModuleManger`` - * instance. This is where we leverage the ``load_modules()`` functions - * provided by libraries . + * instance. This is where we load the plugins provided by libraries. Once + * all plugins are loaded, ``wire_defaults`` sets the default submodules + * the plugins want from each other (our single plugin has none). */ pluginplay::ModuleManager mm; - load_modules(mm); + mm.load_plugin(); + mm.wire_defaults(); /* With the modules loaded it's time to set the options up so that they * suite our specific run. For sake of example we'll change the name of diff --git a/tests/cxx/unit_tests/pluginplay/examples/writing_a_plugin.hpp b/tests/cxx/unit_tests/pluginplay/examples/writing_a_plugin.hpp new file mode 100644 index 000000000..17f687c2f --- /dev/null +++ b/tests/cxx/unit_tests/pluginplay/examples/writing_a_plugin.hpp @@ -0,0 +1,84 @@ +/* + * Copyright 2022 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* The purpose of this tutorial is to showcase the packaging of modules into a + * plugin. + * + * For all intents an purposes the ``ModuleManager`` class can be thought of as + * the code embodiment of pluginplay. It is responsible for storing all of + * the available modules, assembling the call graph, integrating the modules + * into the actual runtime, and checkpointing the progress of the program. + * Libraries meant for use with pluginplay distribute their modules as a + * plugin, which the user loads into a ``ModuleManager``. To declare a plugin + * one includes ``pluginplay/plugin/plugin.hpp`` (included by + * ``pluginplay/pluginplay.hpp``). The remainder of the following header files + * are needed for this tutorial. + */ +#pragma once +#include "writing_a_module.hpp" +#include "writing_a_property_type.hpp" +#include + +/* Declaring the Plugin + * -------------------- + * + * A plugin is a class that derives from ``pluginplay::Plugin``. The + * ``DECLARE_PLUGIN`` macro declares such a class, with a default constructor. + * In a real library this declaration is the library's public API; users only + * need it to load the plugin, not the types of your modules. + */ +DECLARE_PLUGIN(ExamplePlugin); + +/* Defining the Plugin + * ------------------- + * + * Plugins are written like modules: everything is set up in the constructor, + * which is started with the ``PLUGIN_CTOR`` macro. Normally the constructor is + * defined in a source file; since this tutorial is a header, it is marked + * ``inline``. + * + * The following constructor assumes that our ``Area``, ``PrismVolume``, + * ``Rectangle`` and ``Prism`` classes are part of a library meant for use with + * pluginplay. + */ +inline PLUGIN_CTOR(ExamplePlugin) { + /* Every plugin has a name. Plugin names must be unique within a + * ``ModuleManager``. + */ + set_name("example_plugin"); + + /* Next, the plugin adds its modules. Adding a module involves two things: + * + * - creation of an instance of the module + * - association of that instance with a module key + * + * The module key is how users refer to the module (*vide infra*). The key + * must be unique within the plugin (if it's not an error will be raised). + */ + add_module("Rectangle"); + add_module("Prism"); + + /* Our ``Prism`` module needs a submodule which computes an ``Area``. Since + * our plugin provides one, we set it as the default submodule. Users can + * still choose a different submodule later. + * + * If a default submodule should come from a different plugin, the plugin + * instead lists that plugin with ``set_preferred_providers`` and records + * the default with ``default_submod_from``. The ``ModuleManager`` applies + * those defaults when the user calls ``wire_defaults``. + */ + change_submod("Prism", "area", "Rectangle"); +} // end ExamplePlugin ctor diff --git a/tests/cxx/unit_tests/pluginplay/module_manager/detail_/module_manager_pimpl.cpp b/tests/cxx/unit_tests/pluginplay/module_manager/detail_/module_manager_pimpl.cpp index 1c10c971b..d292ededf 100644 --- a/tests/cxx/unit_tests/pluginplay/module_manager/detail_/module_manager_pimpl.cpp +++ b/tests/cxx/unit_tests/pluginplay/module_manager/detail_/module_manager_pimpl.cpp @@ -17,6 +17,7 @@ #include "../../catch.hpp" #include "../../examples/writing_a_module.hpp" #include "pluginplay/module_manager/detail_/module_manager_pimpl.hpp" +#include using namespace pluginplay; using namespace pluginplay::detail_; @@ -24,12 +25,24 @@ using namespace pluginplay::detail_; using base_map = typename ModuleManagerPIMPL::base_map; using module_map = typename ModuleManagerPIMPL::module_map; +namespace { + +DECLARE_PLUGIN(PimplTestPlugin); + +PLUGIN_CTOR(PimplTestPlugin) { + set_name("test_plugin"); + add_module("Rectangle"); +} + +} // namespace + TEST_CASE("ModuleManagerPIMPL : Default ctor") { ModuleManagerPIMPL pimpl; SECTION("State") { REQUIRE(pimpl.size() == 0); REQUIRE(pimpl.m_bases == base_map{}); REQUIRE(pimpl.m_modules == module_map{}); + REQUIRE(pimpl.m_plugins.empty()); } } @@ -85,17 +98,15 @@ TEST_CASE("ModuleManagerPIMPL : comparison") { } SECTION("Different modules") { SECTION("Different instances") { - auto ptr1 = std::make_shared(); - auto ptr2 = std::make_shared(); SECTION("Same key") { - pimpl1.m_modules["key"] = ptr1; - pimpl2.m_modules["key"] = ptr2; + pimpl1.m_modules.add_module("key", Module{}); + pimpl2.m_modules.add_module("key", Module{}); REQUIRE(pimpl1 == pimpl2); REQUIRE(!(pimpl1 != pimpl2)); } SECTION("Different key") { - pimpl1.m_modules["key1"] = ptr1; - pimpl2.m_modules["key2"] = ptr2; + pimpl1.m_modules.add_module("key1", Module{}); + pimpl2.m_modules.add_module("key2", Module{}); REQUIRE(pimpl1 != pimpl2); REQUIRE(!(pimpl1 == pimpl2)); } @@ -107,11 +118,18 @@ TEST_CASE("ModuleManagerPIMPL : add_module") { auto ptr1 = std::make_shared(); ModuleManagerPIMPL pimpl1, pimpl2; pimpl2.m_bases[ptr1->type()] = ptr1; - auto pimpl = std::make_unique(ptr1); - pimpl2.m_modules["key1"] = std::make_shared(std::move(pimpl)); - pimpl2.m_modules["key1"]->set_name("key1"); + pimpl2.m_modules.add_module("key1", ptr1); pimpl1.add_module("key1", ptr1); REQUIRE(pimpl1 == pimpl2); + SECTION("Module is given the runtime and caches") { + REQUIRE(&ptr1->get_runtime() == &pimpl1.get_runtime()); + REQUIRE(ptr1->has_uuid()); + REQUIRE(pimpl1.m_modules.at("key1")->is_memoizable()); + } + SECTION("Can't add a null module") { + REQUIRE_THROWS_AS(pimpl1.add_module("key2", nullptr), + std::invalid_argument); + } SECTION("Can't use the key") { REQUIRE_THROWS_AS(pimpl1.add_module("key1", ptr1), std::invalid_argument); @@ -126,10 +144,7 @@ TEST_CASE("ModuleManagerPIMPL : copy_module") { SECTION("Copies non-locked module") { pimpl1.copy_module("key", "new_key"); - auto pimpl = std::make_unique(ptr1); - pimpl2.m_modules["new_key"] = - std::make_shared(std::move(pimpl)); - pimpl2.m_modules["new_key"]->set_name("new_key"); + pimpl2.m_modules.add_module("new_key", ptr1); REQUIRE(pimpl1 == pimpl2); } SECTION("Copy locked module") { @@ -194,12 +209,49 @@ TEST_CASE("ModuleManagerPIMPL : keys") { auto ptr1 = std::make_shared(); pimpl1.add_module("rectangle", ptr1); REQUIRE(pimpl1.keys().size() == 1); - REQUIRE(pimpl1.keys().at(0) == "rectangle"); + REQUIRE(pimpl1.keys().at(0) == QualifiedKey("", "rectangle")); auto ptr2 = std::make_shared(); pimpl1.add_module("prism", ptr2); REQUIRE(pimpl1.keys().size() == 2); - REQUIRE(pimpl1.keys().at(0) == "prism"); - REQUIRE(pimpl1.keys().at(1) == "rectangle"); + REQUIRE(pimpl1.keys().at(0) == QualifiedKey("", "prism")); + REQUIRE(pimpl1.keys().at(1) == QualifiedKey("", "rectangle")); + } +} + +TEST_CASE("ModuleManagerPIMPL : plugins") { + ModuleManagerPIMPL pimpl1, pimpl2; + auto plugin = std::make_unique(); + const auto* pplugin = plugin.get(); + pimpl1.load_plugin(std::move(plugin)); + + SECTION("State") { + REQUIRE(pimpl1.m_plugins.size() == 1); + REQUIRE(pimpl1.m_plugins[0].get() == pplugin); + REQUIRE(pimpl1.m_modules.empty()); + } + + SECTION("Plugin modules are given the runtime and caches") { + auto mod = pplugin->get_modules().at("Rectangle"); + REQUIRE(mod->is_memoizable()); + REQUIRE(mod->run_as(2.0, 3.0) == 6.0); + } + + SECTION("Comparison") { + REQUIRE(pimpl1 != pimpl2); + pimpl2.load_plugin(std::make_unique()); + REQUIRE(pimpl1 == pimpl2); + pimpl2.get_modules("test_plugin") + .change_input("Rectangle", "Dimension 1", 1.0); + REQUIRE(pimpl1 != pimpl2); + } + + SECTION("count, size, and keys span all maps") { + pimpl1.add_module("Rectangle", std::make_shared()); + REQUIRE(pimpl1.count("Rectangle") == 2); + REQUIRE(pimpl1.size() == 2); + REQUIRE(pimpl1.keys() == ModuleManagerPIMPL::key_container_type{ + QualifiedKey("", "Rectangle"), + QualifiedKey("test_plugin", "Rectangle")}); } } diff --git a/tests/cxx/unit_tests/pluginplay/module_manager/module_manager_class.cpp b/tests/cxx/unit_tests/pluginplay/module_manager/module_manager_class.cpp index 7b00c2d32..b7f392174 100644 --- a/tests/cxx/unit_tests/pluginplay/module_manager/module_manager_class.cpp +++ b/tests/cxx/unit_tests/pluginplay/module_manager/module_manager_class.cpp @@ -15,11 +15,101 @@ */ #include "../catch.hpp" +#include "../examples/writing_a_module.hpp" #include "test_common.hpp" #include +#include + +using pluginplay::ModuleManager; +using pluginplay::QualifiedKey; + +namespace { + +// Computes an area and counts how many times it actually ran. Unlike a lambda +// module it is memoizable, so the count shows whether a result came from the +// cache. +struct CountingArea : pluginplay::ModuleBase { + static inline int n_runs = 0; + + CountingArea() : pluginplay::ModuleBase(this) { + satisfies_property_type(); + } + + pluginplay::type::result_map run_( + pluginplay::type::input_map inputs, + pluginplay::type::submodule_map) const override { + ++n_runs; + const auto [base, height] = Area::unwrap_inputs(inputs); + auto rv = results(); + return Area::wrap_results(rv, base * height); + } +}; + +DECLARE_PLUGIN(ProviderA); +DECLARE_PLUGIN(ProviderB); +DECLARE_PLUGIN(Consumer); +DECLARE_PLUGIN(Unnamed); + +PLUGIN_CTOR(ProviderA) { + set_name("provider_a"); + add_module("A area"); + add_module("Shared"); +} + +PLUGIN_CTOR(ProviderB) { + set_name("provider_b"); + add_module("B area"); + add_module("Shared"); +} + +// Prism prefers provider_a's area. Prism 2 prefers provider_b's area, and +// asks provider_a for a module it does not have. +PLUGIN_CTOR(Consumer) { + set_name("consumer"); + set_preferred_providers("provider_a", "provider_b"); + add_module("Prism"); + add_module("Prism 2"); + default_submod_from("provider_a", "Prism", "area", "A area"); + default_submod_from("provider_b", "Prism", "area", "B area"); + default_submod_from("provider_a", "Prism 2", "area", "Not a module"); + default_submod_from("provider_b", "Prism 2", "area", "B area"); +} + +PLUGIN_CTOR(Unnamed) {} + +// Uses a module as a submodule, then erases that module's key +DECLARE_PLUGIN(ErasesSubmod); +PLUGIN_CTOR(ErasesSubmod) { + set_name("erases_submod"); + add_module("Rectangle"); + add_module("Prism"); + change_submod("Prism", "area", "Rectangle"); + modules().erase("Rectangle"); +} + +// Same name as ProviderA, in a different case +DECLARE_PLUGIN(ShoutingProviderA); +PLUGIN_CTOR(ShoutingProviderA) { set_name("PROVIDER_A"); } + +// Like Consumer, but spells the provider's name in a different case +DECLARE_PLUGIN(CaseConsumer); +PLUGIN_CTOR(CaseConsumer) { + set_name("case_consumer"); + set_preferred_providers("Provider_A"); + add_module("Case Prism"); + default_submod_from("PROVIDER_A", "Case Prism", "area", "A area"); +} + +// The module a submodule of a module is set to +const pluginplay::Module& submod(ModuleManager& mm, const QualifiedKey& key, + const std::string& callback) { + return mm.at(key).submods().at(callback).value(); +} + +} // namespace TEST_CASE("ModuleManager") { - pluginplay::ModuleManager mm; + ModuleManager mm; SECTION("add_module(string)") { using mod_t = testing::NoPTModule; // Type of the Module we're adding @@ -27,6 +117,7 @@ TEST_CASE("ModuleManager") { mm.add_module("a mod"); auto corr = testing::make_module("a mod"); REQUIRE(mm.at("a mod") == *corr); + REQUIRE(mm.get_modules("").count("a mod")); } SECTION("add_module(string, shared_ptr") { @@ -37,20 +128,6 @@ TEST_CASE("ModuleManager") { REQUIRE(mm.at("a mod") == *corr); } - SECTION("iterator") { - using mod_t = testing::NoPTModule; // Type of the Module we're adding - - mm.add_module("a key", std::make_shared()); - mm.add_module("b key", std::make_shared()); - decltype(mm.size()) count = 0; - for(auto& [key, mod] : mm) { - count += 1; - REQUIRE("key" == key.substr(key.size() - 3)); - REQUIRE_FALSE(mod->has_description()); - } - REQUIRE(count == mm.size()); - } - SECTION("set_runtime") { auto prv = std::make_shared(); mm.set_runtime(prv); @@ -66,15 +143,302 @@ TEST_CASE("ModuleManager") { mm.add_module("a key", std::make_shared()); mm.add_module("b key", std::make_shared()); + mm.load_plugin(); auto keys = mm.keys(); REQUIRE(keys.size() == mm.size()); - REQUIRE(keys[0] == "a key"); - REQUIRE(keys[1] == "b key"); + REQUIRE(keys[0] == QualifiedKey("", "a key")); + REQUIRE(keys[1] == QualifiedKey("", "b key")); + REQUIRE(keys[2] == QualifiedKey("provider_a", "A area")); + REQUIRE(keys[3] == QualifiedKey("provider_a", "Shared")); } SECTION("has_cache") { REQUIRE(mm.has_cache()); - pluginplay::ModuleManager no_cache(nullptr, nullptr); + ModuleManager no_cache(nullptr, nullptr); REQUIRE_FALSE(no_cache.has_cache()); } } + +TEST_CASE("ModuleManager : plugins") { + ModuleManager mm; + + SECTION("No plugins") { + REQUIRE_FALSE(mm.has_plugin("provider_a")); + REQUIRE(mm.plugin_names() == ModuleManager::name_container_type{""}); + REQUIRE(mm.get_modules("").empty()); + REQUIRE_THROWS_AS(mm.get_plugin("provider_a"), std::out_of_range); + REQUIRE_THROWS_AS(mm.get_modules("provider_a"), std::out_of_range); + } + + SECTION("load_plugin()") { + mm.load_plugin(); + REQUIRE(mm.has_plugin("provider_a")); + REQUIRE(mm.plugin_names() == + ModuleManager::name_container_type{"", "provider_a"}); + REQUIRE(mm.get_plugin("provider_a") == ProviderA{}); + REQUIRE(&mm.get_modules("provider_a") == + &mm.get_plugin("provider_a").get_modules()); + REQUIRE(mm.size() == 2); + REQUIRE(mm.count("A area") == 1); + + // The modules can be run and are memoizable + REQUIRE(mm.at("A area").is_memoizable()); + REQUIRE(mm.run_as("A area", 2.0, 3.0) == 6.0); + } + + SECTION("load_plugin(unique_ptr)") { + auto plugin = std::make_unique(); + const auto* pplugin = plugin.get(); + mm.load_plugin(std::move(plugin)); + + // *this owns the plugin; nothing was copied + REQUIRE(&mm.get_plugin("provider_a") == pplugin); + } + + SECTION("Can't load two plugins with the same name") { + mm.load_plugin(); + REQUIRE_THROWS_AS(mm.load_plugin(), std::invalid_argument); + REQUIRE(mm.plugin_names().size() == 2); + } + + SECTION("Plugin names are case-insensitive") { + mm.load_plugin(); + REQUIRE(mm.has_plugin("Provider_A")); + REQUIRE(&mm.get_plugin("PROVIDER_A") == &mm.get_plugin("provider_a")); + REQUIRE(mm.get_modules("Provider_A").count("A area") == 1); + REQUIRE(mm.count(QualifiedKey("PROVIDER_A", "A area")) == 1); + REQUIRE(&mm.at(QualifiedKey("Provider_A", "A area")) == + &mm.at("A area")); + + // The name keeps the plugin's spelling + REQUIRE(mm.plugin_names() == + ModuleManager::name_container_type{"", "provider_a"}); + + // Same name in a different case is still a duplicate + REQUIRE_THROWS_AS(mm.load_plugin(), + std::invalid_argument); + } + + SECTION("Erased submodules are given the runtime and caches") { + mm.load_plugin(); + REQUIRE(mm.count("Rectangle") == 0); + + // Prism is only memoizable if its submodule has a UUID, which it only + // gets by being attached + REQUIRE(mm.at("Prism").is_memoizable()); + REQUIRE(mm.at("Prism").submods().at("area").value().is_memoizable()); + } + + SECTION("Can't load unnamed or null plugins") { + REQUIRE_THROWS_AS(mm.load_plugin(), std::invalid_argument); + REQUIRE_THROWS_AS(mm.load_plugin(nullptr), std::invalid_argument); + REQUIRE(mm.plugin_names().size() == 1); + } + + SECTION("Modules with the same key get different caches") { + mm.load_plugin(); + mm.load_plugin(); + CountingArea::n_runs = 0; + + REQUIRE(mm.run_as(QualifiedKey("provider_a", "Shared"), 2.0, + 3.0) == 6.0); + REQUIRE(CountingArea::n_runs == 1); + + // Same inputs, but a different module, so it must actually run + REQUIRE(mm.run_as(QualifiedKey("provider_b", "Shared"), 2.0, + 3.0) == 6.0); + REQUIRE(CountingArea::n_runs == 2); + + // Same module and inputs, so the result comes from the cache + REQUIRE(mm.run_as(QualifiedKey("provider_a", "Shared"), 2.0, + 3.0) == 6.0); + REQUIRE(CountingArea::n_runs == 2); + } +} + +TEST_CASE("ModuleManager : QualifiedKey lookup") { + ModuleManager mm; + mm.add_module("Shared"); + mm.load_plugin(); + mm.load_plugin(); + + const QualifiedKey a_shared("provider_a", "Shared"); + const QualifiedKey b_shared("provider_b", "Shared"); + const QualifiedKey own_shared("", "Shared"); + + SECTION("count") { + REQUIRE(mm.count("Shared") == 3); + REQUIRE(mm.count("A area") == 1); + REQUIRE(mm.count("not a key") == 0); + REQUIRE(mm.count(a_shared) == 1); + REQUIRE(mm.count(QualifiedKey("provider_a", "B area")) == 0); + REQUIRE(mm.count(QualifiedKey("not a plugin", "Shared")) == 0); + } + + SECTION("size") { REQUIRE(mm.size() == 5); } + + SECTION("at") { + // Key-only lookups work when the key is unique + REQUIRE(&mm.at("A area") == + &mm.at(QualifiedKey("provider_a", "A area"))); + + // Qualified lookups pick the map + REQUIRE(&mm.at(a_shared) == + mm.get_modules("provider_a").at("Shared").get()); + REQUIRE(&mm.at(own_shared) == mm.get_modules("").at("Shared").get()); + + REQUIRE_THROWS_AS(mm.at("Shared"), std::invalid_argument); + REQUIRE_THROWS_AS(mm.at("not a key"), std::out_of_range); + REQUIRE_THROWS_AS(mm.at(QualifiedKey("provider_a", "B area")), + std::out_of_range); + REQUIRE_THROWS_AS(mm.at(QualifiedKey("not a plugin", "Shared")), + std::out_of_range); + } + + SECTION("Ambiguity error names the plugins") { + try { + mm.at("Shared"); + FAIL("Expected an exception"); + } catch(const std::invalid_argument& e) { + const std::string msg = e.what(); + REQUIRE(msg.find("'provider_a'") != std::string::npos); + REQUIRE(msg.find("'provider_b'") != std::string::npos); + REQUIRE(msg.find("own modules") != std::string::npos); + } + } + + SECTION("change_input") { + mm.change_input(a_shared, "Dimension 1", 2.0); + const auto& inputs = mm.at(a_shared).inputs(); + REQUIRE(inputs.at("Dimension 1").value() == 2.0); + REQUIRE_FALSE(mm.at(b_shared).inputs().at("Dimension 1").has_value()); + REQUIRE_THROWS_AS(mm.change_input("Shared", "Dimension 1", 2.0), + std::invalid_argument); + } + + SECTION("change_submod across maps") { + mm.load_plugin(); + mm.change_submod("Prism", "area", b_shared); + REQUIRE(submod(mm, "Prism", "area") == mm.at(b_shared)); + REQUIRE_THROWS_AS(mm.change_submod("Prism", "area", "Shared"), + std::invalid_argument); + } + + SECTION("copy_module") { + SECTION("Copies of plugin modules go in the own map") { + mm.copy_module(a_shared, "Copy"); + REQUIRE(mm.count("Copy") == 1); + REQUIRE(mm.count(QualifiedKey("", "Copy")) == 1); + REQUIRE(mm.count(QualifiedKey("provider_a", "Copy")) == 0); + REQUIRE_FALSE(mm.at("Copy").locked()); + } + SECTION("The copy may reuse a key a plugin uses") { + // Only the own map has to be free of the key + mm.copy_module(a_shared, "A area"); + REQUIRE(mm.count("A area") == 2); + REQUIRE(mm.count(QualifiedKey("", "A area")) == 1); + } + SECTION("Errors") { + REQUIRE_THROWS_AS(mm.copy_module("Shared", "Copy"), + std::invalid_argument); + REQUIRE_THROWS_AS(mm.copy_module(a_shared, "Shared"), + std::invalid_argument); + REQUIRE_THROWS_AS(mm.copy_module("not a key", "Copy"), + std::out_of_range); + } + } + + SECTION("erase") { + mm.erase(a_shared); + REQUIRE(mm.count("Shared") == 2); + REQUIRE(mm.count(a_shared) == 0); + + mm.erase("not a key"); // No-op + REQUIRE(mm.size() == 4); + + REQUIRE_THROWS_AS(mm.erase("Shared"), std::invalid_argument); + } + + SECTION("rename_module") { + SECTION("A plugin's module moves to the own map") { + mm.rename_module(a_shared, "Renamed"); + REQUIRE(mm.count(a_shared) == 0); + REQUIRE(mm.count(QualifiedKey("", "Renamed")) == 1); + } + SECTION("Same key, different map") { + // Moving A area to the own map under the same key must not trip + // the ambiguity check when the original is erased + mm.rename_module("A area", "A area"); + REQUIRE(mm.count(QualifiedKey("", "A area")) == 1); + REQUIRE(mm.count(QualifiedKey("provider_a", "A area")) == 0); + } + } +} + +TEST_CASE("ModuleManager : wire_defaults") { + ModuleManager mm; + + SECTION("Highest priority loaded provider wins") { + mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + REQUIRE(submod(mm, "Prism", "area") == mm.at("A area")); + // provider_a lacks "Not a module", so provider_b fills Prism 2 + REQUIRE(submod(mm, "Prism 2", "area") == mm.at("B area")); + } + + SECTION("Lower priority provider is used if higher is not loaded") { + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + REQUIRE(submod(mm, "Prism", "area") == mm.at("B area")); + } + + SECTION("No providers leaves submodules unset") { + mm.load_plugin(); + mm.wire_defaults(); + REQUIRE_FALSE(mm.at("Prism").submods().at("area").has_module()); + } + + SECTION("Load order does not matter") { + mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + REQUIRE(submod(mm, "Prism", "area") == mm.at("A area")); + } + + SECTION("User choices are kept") { + mm.load_plugin(); + mm.load_plugin(); + mm.load_plugin(); + mm.change_submod("Prism", "area", "B area"); + mm.wire_defaults(); + REQUIRE(submod(mm, "Prism", "area") == mm.at("B area")); + } + + SECTION("Can be called again after loading another plugin") { + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + mm.load_plugin(); + mm.wire_defaults(); + // Already set from provider_b, so not overwritten by provider_a + REQUIRE(submod(mm, "Prism", "area") == mm.at("B area")); + } + + SECTION("Provider names are matched case-insensitively") { + mm.load_plugin(); + mm.load_plugin(); + mm.wire_defaults(); + REQUIRE(submod(mm, "Case Prism", "area") == mm.at("A area")); + } + + SECTION("Erased modules are skipped") { + mm.load_plugin(); + mm.load_plugin(); + mm.erase("Prism"); + REQUIRE_NOTHROW(mm.wire_defaults()); + } +} diff --git a/tests/cxx/unit_tests/pluginplay/module_manager/module_map.cpp b/tests/cxx/unit_tests/pluginplay/module_manager/module_map.cpp index 331f6b2e6..96ea196e5 100644 --- a/tests/cxx/unit_tests/pluginplay/module_manager/module_map.cpp +++ b/tests/cxx/unit_tests/pluginplay/module_manager/module_map.cpp @@ -181,6 +181,52 @@ TEST_CASE("ModuleMap") { REQUIRE(same != has_mods); } + SECTION("add_module(key, Module)") { + defaulted.add_module("Copy", has_mods.at("Rectangle")->unlocked_copy()); + auto mod = defaulted.at("Copy"); + REQUIRE(mod->get_name() == "Copy"); + REQUIRE(mod != has_mods.at("Rectangle")); + REQUIRE_THROWS_AS(defaulted.add_module("Copy", Module{}), + std::invalid_argument); + } + + SECTION("copy_module") { + has_mods.at("Rectangle")->lock(); + has_mods.copy_module("Rectangle", "Copy"); + auto copy = has_mods.at("Copy"); + REQUIRE(copy->get_name() == "Copy"); + REQUIRE_FALSE(copy->locked()); + + // The copy has its own inputs + copy->change_input("Dimension 1", 1.23); + REQUIRE(*copy != *has_mods.at("Rectangle")); + + REQUIRE_THROWS_AS(has_mods.copy_module("not a key", "Copy 2"), + std::out_of_range); + REQUIRE_THROWS_AS(has_mods.copy_module("Rectangle", "Prism"), + std::invalid_argument); + } + + SECTION("erase") { + has_mods.erase("Rectangle"); + REQUIRE(has_mods.count("Rectangle") == 0); + REQUIRE(has_mods.size() == 1); + has_mods.erase("not a key"); // No-op + REQUIRE(has_mods.size() == 1); + } + + SECTION("change_input") { + has_mods.change_input("Rectangle", "Dimension 1", 1.23); + const auto& inputs = has_mods.at("Rectangle")->inputs(); + REQUIRE(inputs.at("Dimension 1").value() == 1.23); + REQUIRE_THROWS_AS( + has_mods.change_input("not a key", "Dimension 1", 1.0), + std::out_of_range); + REQUIRE_THROWS_AS( + has_mods.change_input("Rectangle", "not an input", 1.0), + std::out_of_range); + } + SECTION("copies share modules") { ModuleMap copy(has_mods); REQUIRE(copy.at("Prism") == has_mods.at("Prism")); diff --git a/tests/cxx/unit_tests/pluginplay/module_manager/qualified_key.cpp b/tests/cxx/unit_tests/pluginplay/module_manager/qualified_key.cpp new file mode 100644 index 000000000..5b539814c --- /dev/null +++ b/tests/cxx/unit_tests/pluginplay/module_manager/qualified_key.cpp @@ -0,0 +1,62 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../catch.hpp" +#include + +using pluginplay::QualifiedKey; + +TEST_CASE("QualifiedKey") { + QualifiedKey key_only("Overlap"); + QualifiedKey qualified("integrals", "Overlap"); + QualifiedKey own("", "Overlap"); + + SECTION("Key-only") { + REQUIRE_FALSE(key_only.is_qualified()); + REQUIRE(key_only.plugin().empty()); + REQUIRE(key_only.key() == "Overlap"); + REQUIRE(key_only.to_string() == "Overlap"); + + // Implicit from std::string too + QualifiedKey from_string = std::string("Overlap"); + REQUIRE(from_string == key_only); + } + + SECTION("Qualified") { + REQUIRE(qualified.is_qualified()); + REQUIRE(qualified.plugin() == "integrals"); + REQUIRE(qualified.key() == "Overlap"); + REQUIRE(qualified.to_string() == "integrals::Overlap"); + + // The empty plugin name still makes a qualified key + REQUIRE(own.is_qualified()); + REQUIRE(own.plugin().empty()); + } + + SECTION("Comparisons") { + // Module keys are case-insensitive + REQUIRE(qualified == QualifiedKey("integrals", "OVERLAP")); + REQUIRE(key_only == QualifiedKey("overlap")); + + // Plugin names are case-insensitive too + REQUIRE(qualified == QualifiedKey("Integrals", "Overlap")); + REQUIRE(qualified != QualifiedKey("nux", "Overlap")); + + REQUIRE(qualified != key_only); + REQUIRE(own != key_only); + REQUIRE(qualified != QualifiedKey("integrals", "Overlaps")); + } +} diff --git a/tests/cxx/unit_tests/pluginplay/plugin/plugin_class.cpp b/tests/cxx/unit_tests/pluginplay/plugin/plugin_class.cpp index 18446fc1a..7c2a274c5 100644 --- a/tests/cxx/unit_tests/pluginplay/plugin/plugin_class.cpp +++ b/tests/cxx/unit_tests/pluginplay/plugin/plugin_class.cpp @@ -29,10 +29,7 @@ void load_modules(ModuleMap& modules) { } // A plugin written the way the design documentation describes -class MyPlugin : public Plugin { -public: - MyPlugin(); -}; +DECLARE_PLUGIN(MyPlugin); PLUGIN_CTOR(MyPlugin) { set_name("my_plugin"); @@ -91,10 +88,20 @@ TEST_CASE("Plugin") { } SECTION("set_name") { - defaulted.set_name("a name"); - REQUIRE(defaulted.name() == "a name"); + defaulted.set_name("a_name"); + REQUIRE(defaulted.name() == "a_name"); REQUIRE_THROWS_AS(defaulted.set_name(""), std::invalid_argument); - REQUIRE(defaulted.name() == "a name"); + // Names must be identifiers + for(const auto* bad : {"a name", "..", "a::b", "1abc", "a/b", "a-b"}) + REQUIRE_THROWS_AS(defaulted.set_name(bad), std::invalid_argument); + REQUIRE(defaulted.name() == "a_name"); + } + + SECTION("is_valid_name") { + for(const auto* good : {"a", "_a", "my_plugin", "Plugin2", "SCF"}) + REQUIRE(Plugin::is_valid_name(good)); + for(const auto* bad : {"", ".", "..", "a name", "a::b", "1abc", "a-b"}) + REQUIRE_FALSE(Plugin::is_valid_name(bad)); } SECTION("add_module") { @@ -132,8 +139,13 @@ TEST_CASE("Plugin") { defaulted.set_preferred_providers("a"); REQUIRE_THROWS_AS(defaulted.set_preferred_providers(""), std::invalid_argument); + REQUIRE_THROWS_AS(defaulted.set_preferred_providers("not valid"), + std::invalid_argument); REQUIRE_THROWS_AS(defaulted.set_preferred_providers("b", "b"), std::invalid_argument); + // Names are case-insensitive, so this is a duplicate too + REQUIRE_THROWS_AS(defaulted.set_preferred_providers("b", "B"), + std::invalid_argument); REQUIRE(defaulted.preferred_providers() == provider_list{"a"}); // Can't drop a provider that has recorded defaults @@ -143,9 +155,9 @@ TEST_CASE("Plugin") { std::invalid_argument); REQUIRE(defaulted.preferred_providers() == provider_list{"a"}); - // Keeping it, and adding others, is fine - defaulted.set_preferred_providers("b", "a"); - REQUIRE(defaulted.preferred_providers() == provider_list{"b", "a"}); + // Keeping it (in any case), and adding others, is fine + defaulted.set_preferred_providers("b", "A"); + REQUIRE(defaulted.preferred_providers() == provider_list{"b", "A"}); } SECTION("default_submod_from") { @@ -157,6 +169,13 @@ TEST_CASE("Plugin") { // Recording a default doesn't change the module REQUIRE_FALSE(defaulted.get_modules().has_submod("Prism", "area")); + + // Provider names are case-insensitive + defaulted.default_submod_from("A", "Prism", "area", "key 2"); + default_list corr{default_submod{"Prism", "area", "key"}, + default_submod{"Prism", "area", "key 2"}}; + REQUIRE(defaulted.default_submods_from("a") == corr); + REQUIRE(defaulted.default_submods_from("A") == corr); } SECTION("default_submod_from throws") { diff --git a/tests/cxx/unit_tests/pluginplay/printing/document_modules.cpp b/tests/cxx/unit_tests/pluginplay/printing/document_modules.cpp index f58e69a5b..f99c90d96 100644 --- a/tests/cxx/unit_tests/pluginplay/printing/document_modules.cpp +++ b/tests/cxx/unit_tests/pluginplay/printing/document_modules.cpp @@ -15,9 +15,31 @@ */ #include "../catch.hpp" +#include "../examples/writing_a_module.hpp" #include +#include #include +#include #include +#include + +namespace { + +DECLARE_PLUGIN(DocumentedPlugin); + +PLUGIN_CTOR(DocumentedPlugin) { + set_name("documented_plugin"); + add_module("Rectangle"); + add_module("Prism"); +} + +// Number of entries in the directory dir +auto n_entries(const std::filesystem::path& dir) { + return std::distance(std::filesystem::directory_iterator(dir), + std::filesystem::directory_iterator{}); +} + +} // namespace TEST_CASE("document_modules") { // Need a docs path @@ -50,4 +72,27 @@ TEST_CASE("document_modules") { std::filesystem::directory_iterator{}) == correct); } + + SECTION("Own modules and a plugin") { + mm.add_module("Own Rectangle"); + mm.load_plugin(); + + pluginplay::printing::document_modules(mm, docs_path); + + // index.rst, Own_Rectangle.rst, and the plugin's directory + const auto plugin_path = docs_path / "documented_plugin"; + REQUIRE(n_entries(docs_path) == 3); + REQUIRE(std::filesystem::exists(docs_path / "Own_Rectangle.rst")); + REQUIRE(std::filesystem::is_directory(plugin_path)); + REQUIRE(n_entries(plugin_path) == 2); + REQUIRE(std::filesystem::exists(plugin_path / "Rectangle.rst")); + REQUIRE(std::filesystem::exists(plugin_path / "Prism.rst")); + + // The index points into the plugin's directory + std::ifstream index(docs_path / "index.rst"); + std::stringstream contents; + contents << index.rdbuf(); + REQUIRE(contents.str().find("documented_plugin/Prism") != + std::string::npos); + } } diff --git a/tests/python/doc_snippets/export_pluginplay_examples.cpp b/tests/python/doc_snippets/export_pluginplay_examples.cpp index 4c40e12a6..c9346c97c 100644 --- a/tests/python/doc_snippets/export_pluginplay_examples.cpp +++ b/tests/python/doc_snippets/export_pluginplay_examples.cpp @@ -16,14 +16,14 @@ #include "../../cxx/doc_snippets/electric_field.hpp" #include "../../cxx/doc_snippets/force.hpp" -#include "../../cxx/doc_snippets/load_modules.hpp" +#include "../../cxx/doc_snippets/pluginplay_examples.hpp" #include "../../cxx/doc_snippets/point_charge.hpp" #include #include namespace pluginplay_examples { -EXPORT_PLUGIN(pluginplay_examples, m) { +EXPORT_PLUGIN(pluginplay_examples, PluginplayExamples, m) { pybind11::class_(m, "PointCharge") .def(pybind11::init<>()) .def(pybind11::init()) diff --git a/tests/python/doc_snippets/test_modules.py b/tests/python/doc_snippets/test_modules.py index 2410f8638..dac6c6f52 100644 --- a/tests/python/doc_snippets/test_modules.py +++ b/tests/python/doc_snippets/test_modules.py @@ -22,9 +22,14 @@ class TestTutorialModules(unittest.TestCase): def test_modules(self): mm = pp.ModuleManager() - ppe.load_modules(mm) + plugin = ppe.PluginplayExamples() + mm.load_plugin(plugin) self.assertEqual(mm.size(), 4) + # The ModuleManager took ownership of the C++ plugin + self.assertRaises(ValueError, plugin.name) + self.assertRaises(ValueError, pp.ModuleManager().load_plugin, plugin) + module_names = [ "Coulomb's Law", "Classical Force", diff --git a/tests/python/unit_tests/module/test_module.py b/tests/python/unit_tests/module/test_module.py index 273d0b69e..d938dc29d 100644 --- a/tests/python/unit_tests/module/test_module.py +++ b/tests/python/unit_tests/module/test_module.py @@ -285,6 +285,11 @@ def test_change_submod(self): has_submod.change_submod(key, submod) self.assertEqual(has_submod.submods()[key].value(), submod) + # The submodule is linked, not copied + self.assertIs(has_submod.submods()[key].value(), submod) + submod.turn_off_memoization() + self.assertFalse(has_submod.submods()[key].value().is_memoizable()) + def test_run_as(self): # Can't run a module with no implementation self.assertRaises(Exception, self.defaulted.run_as, test_pp.NullPT()) diff --git a/tests/python/unit_tests/module_manager/test_module_manager_class.py b/tests/python/unit_tests/module_manager/test_module_manager_class.py index ab8470a6b..00d4cf86b 100644 --- a/tests/python/unit_tests/module_manager/test_module_manager_class.py +++ b/tests/python/unit_tests/module_manager/test_module_manager_class.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import gc import unittest import parallelzone @@ -31,6 +32,37 @@ def run_(self, inputs, submods): return pt.wrap_results(rv, i0) # +class AConsumer(pp.ModuleBase): + def __init__(self): + pp.ModuleBase.__init__(self) + self.satisfies_property_type(test_pp.OneInOneOut()) + self.add_submodule(test_pp.OneInOneOut(), "sub") + + def run_(self, inputs, submods): + pt = test_pp.OneInOneOut() + (i0,) = pt.unwrap_inputs(inputs) + rv = submods["sub"].run_as(pt, i0) + return pt.wrap_results(self.results(), rv) + + +class ProviderPlugin(pp.Plugin): + def __init__(self): + pp.Plugin.__init__(self) + self.set_name("provider") + self.add_module("Provider", APythonModule()) + self.add_module("Shared", APythonModule()) + + +class ConsumerPlugin(pp.Plugin): + def __init__(self): + pp.Plugin.__init__(self) + self.set_name("consumer") + self.set_preferred_providers("provider") + self.add_module("Consumer", AConsumer()) + self.add_module("Shared", APythonModule()) + self.default_submod_from("provider", "Consumer", "sub", "Provider") + + class TestModuleManager(unittest.TestCase): def test_ctor(self): rv = parallelzone.runtime.RuntimeView() @@ -215,6 +247,31 @@ def test_get_item(self): self.assertTrue(module_with_description.has_description()) self.assertNotEqual(self.has_mods["C++ no PT"], None) + def test_module_handles_share_ownership(self): + key = "C++ no PT" + + # Repeated lookups give the same object, which is the live module + self.assertIs(self.has_mods.at(key), self.has_mods.at(key)) + self.has_mods[key].turn_off_memoization() + self.assertFalse(self.has_mods.at(key).is_memoizable()) + + # A handle stays valid after the module is erased + mod = self.has_mods.at(key) + self.has_mods.erase(key) + gc.collect() + self.assertFalse(mod.is_memoizable()) + self.assertTrue(mod.has_module()) + + def test_submodule_value_is_the_submodule(self): + mod_key = "C++ module needing a submodule" + self.has_mods.change_submod(mod_key, "Submodule 1", "C++ Null PT") + submod = self.has_mods.at(mod_key).submods()["Submodule 1"].value() + + # Changes made through value() reach the actual submodule + self.assertIs(submod, self.has_mods.at("C++ Null PT")) + submod.turn_off_memoization() + self.assertFalse(self.has_mods.at("C++ Null PT").is_memoizable()) + def test_set_runtime(self): new_rv = parallelzone.runtime.RuntimeView() self.defaulted.set_runtime(new_rv) @@ -226,7 +283,105 @@ def test_get_runtime(self): def test_keys(self): self.assertEqual(len(self.defaulted.keys()), self.defaulted.size()) self.assertEqual(len(self.has_mods.keys()), self.has_mods.size()) - self.assertTrue("C++ Null PT" in self.has_mods.keys()) + key = pp.QualifiedKey("", "C++ Null PT") + self.assertTrue(key in self.has_mods.keys()) + self.assertFalse( + pp.QualifiedKey("C++ Null PT") in self.has_mods.keys() + ) + + def test_load_plugin(self): + mm = pp.ModuleManager() + self.assertFalse(mm.has_plugin("provider")) + + mm.load_plugin(ProviderPlugin()) + self.assertTrue(mm.has_plugin("provider")) + self.assertEqual(mm.plugin_names(), ["", "provider"]) + self.assertEqual(mm.get_plugin("provider").name(), "provider") + self.assertEqual(mm.get_modules("provider").count("Provider"), 1) + self.assertEqual(mm.size(), 2) + + # Can't load a plugin with the same name twice + self.assertRaises(ValueError, mm.load_plugin, ProviderPlugin()) + + # load_plugin takes ownership, so the plugin can't be loaded again. + # The Python part of a Python plugin stays alive (and readable) for as + # long as the ModuleManager owns the plugin. + plugin = ConsumerPlugin() + mm.load_plugin(plugin) + self.assertTrue(mm.has_plugin("consumer")) + self.assertEqual(plugin.name(), "consumer") + self.assertRaises(RuntimeError, pp.ModuleManager().load_plugin, plugin) + + def test_read_only_views(self): + mm = pp.ModuleManager() + mm.load_plugin(ProviderPlugin()) + + # get_modules returns a read-only view of the live map + modules = mm.get_modules("provider") + self.assertIsInstance(modules, pp.ModuleMapView) + self.assertEqual(modules.size(), 2) + self.assertTrue("Provider" in modules) + self.assertEqual(sorted(modules.keys()), ["Provider", "Shared"]) + for name in ("add_module", "copy_module", "erase", "change_submod"): + self.assertFalse(hasattr(modules, name)) + + # The view's modules are the live modules + modules.at("Provider").turn_off_memoization() + self.assertFalse(mm.at("Provider").is_memoizable()) + + # get_plugin returns a read-only view of the loaded plugin + plugin = mm.get_plugin("provider") + self.assertIsInstance(plugin, pp.PluginView) + self.assertEqual(plugin.name(), "provider") + self.assertEqual(plugin.get_modules().size(), 2) + for name in ("set_name", "add_module", "modules", "change_submod"): + self.assertFalse(hasattr(plugin, name)) + + def test_views_keep_owner_alive(self): + mm = pp.ModuleManager() + mm.load_plugin(ProviderPlugin()) + modules = mm.get_plugin("provider").get_modules() + del mm + gc.collect() + self.assertEqual(modules.size(), 2) + + def test_load_plugin_keeps_python_alive(self): + # Like test_pluginplay_309, but the Python modules are owned by a + # plugin that only the ModuleManager references + mm = pp.ModuleManager() + mm.load_plugin(ProviderPlugin()) + gc.collect() + rv = mm.run_as(test_pp.OneInOneOut(), "Provider", 1) + self.assertEqual(rv, 1) + + def test_qualified_keys(self): + mm = pp.ModuleManager() + mm.load_plugin(ProviderPlugin()) + mm.load_plugin(ConsumerPlugin()) + pt = test_pp.OneInOneOut() + + # Strings, tuples, and QualifiedKeys are accepted + self.assertEqual(mm.count("Shared"), 2) + self.assertEqual(mm.count(("provider", "Shared")), 1) + self.assertEqual(mm.count(pp.QualifiedKey("consumer", "Shared")), 1) + self.assertEqual(mm.run_as(pt, ("provider", "Shared"), 1), 1) + mm.change_submod("Consumer", "sub", ("consumer", "Shared")) + + # A key used by more than one plugin is ambiguous + self.assertRaises(ValueError, mm.at, "Shared") + self.assertRaises(IndexError, mm.at, ("provider", "Consumer")) + + def test_wire_defaults(self): + mm = pp.ModuleManager() + mm.load_plugin(ConsumerPlugin()) + mm.load_plugin(ProviderPlugin()) + submods = mm.at("Consumer").submods() + self.assertFalse(submods["sub"].has_module()) + + mm.wire_defaults() + sub = mm.at("Consumer").submods()["sub"].value() + self.assertEqual(sub, mm.at("Provider")) + self.assertEqual(mm.run_as(test_pp.OneInOneOut(), "Consumer", 2), 2) def setUp(self): self.defaulted = pp.defaulted_mm() diff --git a/tests/python/unit_tests/module_manager/test_module_map.py b/tests/python/unit_tests/module_manager/test_module_map.py index b22ed44ba..55bbb6d8b 100644 --- a/tests/python/unit_tests/module_manager/test_module_map.py +++ b/tests/python/unit_tests/module_manager/test_module_map.py @@ -77,6 +77,33 @@ def test_keys(self): sorted(self.has_mods.keys()), ["Consumer", "Provider"] ) + def test_copy_module(self): + self.has_mods.copy_module("Provider", "Copy") + self.assertEqual(self.has_mods.at("Copy").get_name(), "Copy") + self.assertRaises( + IndexError, self.has_mods.copy_module, "not a key", "Copy 2" + ) + self.assertRaises( + ValueError, self.has_mods.copy_module, "Provider", "Consumer" + ) + + def test_erase(self): + # Handles share ownership, so they stay valid after the erase + mod = self.has_mods.at("Provider") + self.has_mods.erase("Provider") + self.assertEqual(mod.get_name(), "Provider") + self.assertEqual(self.has_mods.count("Provider"), 0) + self.has_mods.erase("not a key") + self.assertEqual(self.has_mods.size(), 1) + + def test_change_input(self): + self.has_mods.change_input("Provider", "Option 1", 1) + inputs = self.has_mods.at("Provider").inputs() + self.assertEqual(inputs["Option 1"].value(), 1) + self.assertRaises( + IndexError, self.has_mods.change_input, "not a key", "Option 1", 1 + ) + def test_change_submod_by_key(self): self.assertFalse(self.has_mods.has_submod("Consumer", "sub")) self.has_mods.change_submod("Consumer", "sub", "Provider") diff --git a/tests/python/unit_tests/module_manager/test_qualified_key.py b/tests/python/unit_tests/module_manager/test_qualified_key.py new file mode 100644 index 000000000..eb3852f9d --- /dev/null +++ b/tests/python/unit_tests/module_manager/test_qualified_key.py @@ -0,0 +1,46 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import pluginplay as pp + + +class TestQualifiedKey(unittest.TestCase): + def test_key_only(self): + key = pp.QualifiedKey("Overlap") + self.assertFalse(key.is_qualified()) + self.assertEqual(key.plugin(), "") + self.assertEqual(key.key(), "Overlap") + self.assertEqual(str(key), "Overlap") + + def test_qualified(self): + key = pp.QualifiedKey("integrals", "Overlap") + self.assertTrue(key.is_qualified()) + self.assertEqual(key.plugin(), "integrals") + self.assertEqual(key.key(), "Overlap") + self.assertEqual(str(key), "integrals::Overlap") + + def test_from_tuple(self): + key = pp.QualifiedKey(("integrals", "Overlap")) + self.assertEqual(key, pp.QualifiedKey("integrals", "Overlap")) + self.assertRaises(ValueError, pp.QualifiedKey, ("a", "b", "c")) + + def test_comparisons(self): + key = pp.QualifiedKey("integrals", "Overlap") + self.assertEqual(key, pp.QualifiedKey("integrals", "OVERLAP")) + self.assertEqual(key, pp.QualifiedKey("Integrals", "Overlap")) + self.assertNotEqual(key, pp.QualifiedKey("nux", "Overlap")) + self.assertNotEqual(key, pp.QualifiedKey("Overlap")) + self.assertNotEqual(pp.QualifiedKey("", "a"), pp.QualifiedKey("a"))