ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PythonPluginManager.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "PythonPluginManager.h"
9 #include "Utilities.h"
10 
11 #include <QDirIterator>
12 #include <QFileInfo>
13 
14 using namespace pybind11::literals;
15 
16 const std::vector<Runtime::RegisteredPlugin> &PythonPluginManager::plugins() const
17 {
18  return m_plugins;
19 }
20 
22 {
23  plgPrint() << "Searching for custom plugins (checking metadata in site-package)";
24  const py::object versionInfo = py::module::import("sys").attr("version_info");
25  const py::object metadata = py::module::import("importlib.metadata");
26  std::vector<py::object> all_entries;
27 
28  auto collectEntries = [&](const std::string &group)
29  {
30  py::iterable entries;
31  if (versionInfo < py::make_tuple(3, 10))
32  {
33  const py::dict entries_dict = metadata.attr("entry_points")();
34  if (entries_dict.contains(group))
35  {
36  entries = entries_dict[group.c_str()];
37  }
38  else
39  {
40  plgVerbose() << "No custom plugin registered in site-packages";
41  return;
42  }
43  }
44  else
45  {
46  entries = metadata.attr("entry_points")("group"_a = group.c_str());
47  }
48 
49  if (py::len(entries) == 0)
50  {
51  plgPrint() << "No entries found for group: " << QString::fromStdString(group);
52  return;
53  }
54 
55  for (auto &entry : entries)
56  {
57  all_entries.push_back(py::cast<py::object>(entry));
58  }
59  };
60 
61  // Get entry points filtered by group="cloudviewer.plugins" or "cloudcompare.plugins"
62  collectEntries("cloudviewer.plugins");
63  collectEntries("cloudcompare.plugins");
64 
65  if (all_entries.empty())
66  {
67  plgVerbose() << "No custom plugin registered in site-packages";
68  return;
69  }
70  else
71  {
72  plgVerbose() << "Found the following plugins:";
73  for (auto &entry : all_entries)
74  {
75  QString pluginName = entry.attr("name").cast<QString>();
76  plgVerbose() << "\tPlugin: " << pluginName;
77  }
78  }
79 
80  for (auto &entry : all_entries)
81  {
82  QString pluginName = entry.attr("name").cast<QString>();
83  QString entry_point = entry.attr("value").cast<QString>();
84  QStringList entry_pieces = entry_point.split(':');
85  if (entry_pieces.size() != 2)
86  { // Entry point value should be in the form package_and_module:pluginclass
87  plgWarning() << "Malformed entry point specification for '" << pluginName << "':'"
88  << entry_point << "'";
89  continue;
90  }
91 
92  // No need to check that it's a subclass of PythonPluginInterface, the instanciation
93  // and exception will work for us.
94  try
95  {
96  py::object plugin_class = py::module::import(entry_pieces[0].toStdString().c_str())
97  .attr(entry_pieces[1].toStdString().c_str());
99  Runtime::RegisteredPlugin::InstanciatePlugin(plugin_class, pluginName);
100  m_plugins.push_back(plugin);
101  plgPrint() << "\tLoaded plugin: '" << pluginName << "'";
102  }
103  catch (const std::exception &e)
104  {
105  plgWarning() << "\tFailed to instantiate plugin named '" << pluginName
106  << "'\nThe error was:\n"
107  << e.what();
108  }
109  }
110 }
111 
112 // Loading python plugins from path works in 3 steps
113 // 1. Add the path from where we want to load the plugin(s) to
114 // the PythonPath (so that we can import them)
115 // 2. Import the file if it's a .py file or import directory name
116 // 3. Create instances of subclasses of our interface for Python Plugin
117 void PythonPluginManager::loadPluginsFrom(const QStringList &paths)
118 {
119  if (paths.isEmpty())
120  {
121  return;
122  }
123 
124  plgPrint() << "Searching for custom plugins (in custom paths)";
125  py::object appendToPythonSysPath = py::module::import("sys").attr("path").attr("append");
126  for (const QString &path : paths)
127  {
128  plgPrint() << "Searching in " << path;
129  appendToPythonSysPath(path);
130  QDirIterator iter(path);
131  while (iter.hasNext())
132  {
133  iter.next();
134  QFileInfo entry = iter.fileInfo();
135  QString fileName = entry.fileName();
136 
137  if (fileName == "." || fileName == ".." || fileName == "__pycache__")
138  {
139  continue;
140  }
141 
142  QString nameToImport = fileName;
143  if (!entry.isDir() && fileName.endsWith(".py"))
144  {
145  nameToImport = fileName.left(fileName.size() - 3);
146  }
147 
148  const std::string nameToImportStd = nameToImport.toStdString();
149  try
150  {
151  py::module::import(nameToImportStd.c_str());
152  plgVerbose() << "\tLoaded python module '" << nameToImportStd.c_str() << "'";
153  }
154  catch (const std::exception &e)
155  {
156  plgWarning() << "\tFailed to python module '" << nameToImportStd.c_str()
157  << e.what();
158  }
159  }
160  }
161 
162  py::list subClassTypes =
163  py::module::import("pycc_runtime").attr("PythonPluginInterface").attr("__subclasses__")();
164  for (auto &subClassType : subClassTypes)
165  {
166  QString pluginName;
167  if (hasattr(subClassType, "__name__"))
168  {
169  pluginName = subClassType.attr("__name__").cast<QString>();
170  }
171  try
172  {
174  Runtime::RegisteredPlugin::InstanciatePlugin(subClassType.cast<py::object>());
175  m_plugins.push_back(plugin);
176  plgPrint() << "\tLoaded plugin: '" << pluginName << "'";
177  }
178  catch (const std::exception &e)
179  {
180  plgWarning() << "\tFailed to instantiate plugin named '" << pluginName
181  << "'\nThe error was:\n"
182  << e.what();
183  }
184  }
185 }
186 
188 {
189  m_plugins.clear();
190 }
PluginLogger< CVLog::LOG_WARNING > plgWarning
Definition: Utilities.h:102
PluginLogger< CVLog::LOG_STANDARD > plgPrint
Definition: Utilities.h:100
PluginLogger< CVLog::LOG_VERBOSE > plgVerbose
Definition: Utilities.h:99
void loadPluginsFrom(const QStringList &paths)
const std::vector< Runtime::RegisteredPlugin > & plugins() const
Returns the currently loaded plugins.
void unloadPlugins()
This MUST be called before finalizing the interpreter.
static const std::string path
Definition: PointCloud.cpp:59
static RegisteredPlugin InstanciatePlugin(pybind11::object class_type, const QString &name) noexcept(false)
Instanciate a plugin with a known name.
Definition: Runtime.h:40