LDMX Software
ConfigurePython.cxx
1
2#include "Framework/ConfigurePython.h"
3
4/*~~~~~~~~~~~~*/
5/* python */
6/*~~~~~~~~~~~~*/
7#include "Python.h"
8
9/*~~~~~~~~~~~~~~~~*/
10/* C++ StdLib */
11/*~~~~~~~~~~~~~~~~*/
12#include <any>
13#include <cstring>
14#include <iostream>
15#include <string>
16#include <vector>
17
18namespace framework {
19
20std::string ConfigurePython::root_module = "ldmxcfg";
21std::string ConfigurePython::root_class = "Process";
22std::string ConfigurePython::root_object = "lastProcess";
23
33static std::string getPyString(PyObject* pyObj) {
34 std::string retval;
35 PyObject* pyStr = PyUnicode_AsEncodedString(pyObj, "utf-8", "Error ~");
36 retval = PyBytes_AS_STRING(pyStr);
37 Py_XDECREF(pyStr);
38 return retval;
39}
40
49std::string repr(PyObject* obj) {
50 PyObject* py_repr = PyObject_Repr(obj);
51 if (py_repr == nullptr) return "";
52 std::string str = getPyString(py_repr);
53 Py_XDECREF(py_repr);
54 return str;
55}
56
74PyObject* extractDictionary(PyObject* obj) {
75#if PY_MAJOR_VERSION != 3
76#error("Framework requires compiling with Python3")
77#else
78#if PY_MINOR_VERSION != 10 && PY_MINOR_VERSION != 6
79#warning("Unrecognized Python3 minor version. Unsure if accessing C API properly for configuration.")
80#endif
92 PyObject** p_dictionary{_PyObject_GetDictPtr(obj)};
93 if (p_dictionary == NULL) {
94 if (PyDict_Check(obj)) {
95 return obj;
96 } else {
97 EXCEPTION_RAISE("ObjFail",
98 "Python Object '" + repr(obj) +
99 "' does not have __dict__ member and is not a dict.");
100 }
101 }
102 return *p_dictionary;
103#endif
104}
105
136static std::map<std::string, std::any> getMembers(PyObject* object) {
137 PyObject* dictionary{extractDictionary(object)};
138 PyObject *key(0), *value(0);
139 Py_ssize_t pos = 0;
140
141 std::map<std::string, std::any> params;
142
143 while (PyDict_Next(dictionary, &pos, &key, &value)) {
144 std::string skey{getPyString(key)};
145 if (PyLong_Check(value)) {
146 if (PyBool_Check(value)) {
147 params[skey] = bool(PyLong_AsLong(value));
148 } else {
149 params[skey] = int(PyLong_AsLong(value));
150 }
151 } else if (PyFloat_Check(value)) {
152 params[skey] = PyFloat_AsDouble(value);
153 } else if (PyUnicode_Check(value)) {
154 params[skey] = getPyString(value);
155 } else if (PyList_Check(value)) {
156 // assume everything is same value as first value
157 if (PyList_Size(value) > 0) {
158 auto vec0{PyList_GetItem(value, 0)};
159
160 if (PyLong_Check(vec0)) {
161 std::vector<int> vals;
162
163 for (auto j{0}; j < PyList_Size(value); j++)
164 vals.push_back(PyLong_AsLong(PyList_GetItem(value, j)));
165
166 params[skey] = vals;
167
168 } else if (PyFloat_Check(vec0)) {
169 std::vector<double> vals;
170
171 for (auto j{0}; j < PyList_Size(value); j++)
172 vals.push_back(PyFloat_AsDouble(PyList_GetItem(value, j)));
173
174 params[skey] = vals;
175
176 } else if (PyUnicode_Check(vec0)) {
177 std::vector<std::string> vals;
178 for (Py_ssize_t j = 0; j < PyList_Size(value); j++) {
179 PyObject* elem = PyList_GetItem(value, j);
180 vals.push_back(getPyString(elem));
181 }
182
183 params[skey] = vals;
184 } else if (PyList_Check(vec0)) {
185 // a list in a list ??? oof-dah
186 if (PyList_Size(vec0) > 0) {
187 auto vecvec0{PyList_GetItem(vec0, 0)};
188 if (PyLong_Check(vecvec0)) {
189 std::vector<std::vector<int>> vals;
190 for (auto j{0}; j < PyList_Size(value); j++) {
191 auto subvec{PyList_GetItem(value, j)};
192 std::vector<int> subvals;
193 for (auto k{0}; k < PyList_Size(subvec); k++) {
194 subvals.push_back(PyLong_AsLong(PyList_GetItem(subvec, k)));
195 }
196 vals.push_back(subvals);
197 }
198 params[skey] = vals;
199 } else if (PyFloat_Check(vecvec0)) {
200 std::vector<std::vector<double>> vals;
201 for (auto j{0}; j < PyList_Size(value); j++) {
202 auto subvec{PyList_GetItem(value, j)};
203 std::vector<double> subvals;
204 for (auto k{0}; k < PyList_Size(subvec); k++) {
205 subvals.push_back(
206 PyFloat_AsDouble(PyList_GetItem(subvec, k)));
207 }
208 vals.push_back(subvals);
209 }
210 params[skey] = vals;
211 } else if (PyUnicode_Check(vecvec0)) {
212 std::vector<std::vector<std::string>> vals;
213 for (auto j{0}; j < PyList_Size(value); j++) {
214 auto subvec{PyList_GetItem(value, j)};
215 std::vector<std::string> subvals;
216 for (auto k{0}; k < PyList_Size(subvec); k++) {
217 subvals.push_back(getPyString(PyList_GetItem(subvec, k)));
218 }
219 vals.push_back(subvals);
220 }
221 params[skey] = vals;
222 } else if (PyList_Check(vecvec0)) {
223 EXCEPTION_RAISE("BadConf",
224 "A python list with dimension greater than 2 is "
225 "not supported.");
226 } else {
227 // RECURSION zoinks!
228 std::vector<std::vector<framework::config::Parameters>> vals;
229 for (auto j{0}; j < PyList_Size(value); j++) {
230 auto subvec{PyList_GetItem(value, j)};
231 std::vector<framework::config::Parameters> subvals;
232 for (auto k{0}; k < PyList_Size(subvec); k++) {
233 subvals.emplace_back();
234 subvals.back().setParameters(
235 getMembers(PyList_GetItem(subvec, k)));
236 }
237 vals.push_back(subvals);
238 }
239 params[skey] = vals;
240 }
241 } // non-zero size
242 } else {
243 // RECURSION zoinks!
244 // If the objects stored in the list doesn't
245 // satisfy any of the above conditions, just
246 // create a vector of parameters objects
247 std::vector<framework::config::Parameters> vals;
248 for (auto j{0}; j < PyList_Size(value); ++j) {
249 auto elem{PyList_GetItem(value, j)};
250 vals.emplace_back();
251 vals.back().setParameters(getMembers(elem));
252 }
253 params[skey] = vals;
254 } // type of object in python list
255 } // python list has non-zero size
256 } else {
257 // object got here, so we assume
258 // it is a higher level object
259 //(same logic as last option for a list)
260
261 // RECURSION zoinks!
263 val.setParameters(getMembers(value));
264
265 params[skey] = val;
266
267 } // python object type
268 } // loop through python dictionary
269
270 return params;
271}
272
273ConfigurePython::ConfigurePython(const std::string& pythonScript, char* args[],
274 int nargs) {
275 // assumes that nargs >= 0
276 // this is true always because we error out if no python script has been
277 // found
278
279 // load a handle to the config file into memory (and check that it exists)
280 FILE* config_file{fopen(pythonScript.c_str(), "r")};
281 if (config_file == NULL) {
282 EXCEPTION_RAISE("ConfigDNE",
283 "Passed config script '" + pythonScript +
284 "' is not accessible.\n"
285 " Did you make a typo in the path to the script?\n"
286 " Are you referencing a directory that is not "
287 "mounted to the container?");
288 }
289
290 std::string cmd = pythonScript;
291 if (pythonScript.rfind("/") != std::string::npos) {
292 cmd = pythonScript.substr(pythonScript.rfind("/") + 1);
293 }
294 cmd = cmd.substr(0, cmd.find(".py"));
295
296 // python needs the argument list as if you are on the command line
297 // targs = [ script , arg0 , arg1 , ... ] ==> len(targs) = nargs+1
298 // PySys_SetArgvEx uses wchar_t instead of char in python3
299 wchar_t** targs = new wchar_t*[nargs + 1];
300 targs[0] = Py_DecodeLocale(pythonScript.c_str(), NULL);
301 for (int i = 0; i < nargs; i++) targs[i + 1] = Py_DecodeLocale(args[i], NULL);
302
303 // name our program after the script that is being run
304 Py_SetProgramName(targs[0]);
305
306 // start up python interpreter
307 Py_Initialize();
308
309 // The third argument to PySys_SetArgvEx tells python to import
310 // the args and add the directory of the first argument to
311 // the PYTHONPATH
312 // This way, the command to import the module just needs to be
313 // the name of the python script
314 PySys_SetArgvEx(nargs + 1, targs, 1);
315
316 // run the file as a python script
317 if (PyRun_AnyFile(config_file, pythonScript.c_str()) != 0) {
318 PyErr_Print();
319 EXCEPTION_RAISE("ConfigureError", "Problem running python script.");
320 }
321
322 // script has been run so we can
323 // free up arguments to python script
324 fclose(config_file);
325 for (int i = 0; i < nargs + 1; i++) PyMem_RawFree(targs[i]);
326 delete[] targs;
327
328 // when a script runs in python, the script itself becomes the module
329 // named __main__, we retrieve a handle to that module by "importing"
330 // it (since it is already within the python interpreter, we are just
331 // getting the handle and not actually running anything here
332 PyObject* script = PyImport_ImportModule("__main__");
333 if (script == NULL) {
334 PyErr_Print();
335 EXCEPTION_RAISE("ConfigureError", "Problem loading python script");
336 }
337 PyObject* pCMod = PyObject_GetAttrString(script, root_module.c_str());
338 Py_DECREF(script); // don't need the script anymore
339 if (pCMod == 0) {
340 PyErr_Print();
341 EXCEPTION_RAISE("ConfigureError",
342 "Did not import root configuration module " + root_module);
343 }
344
345 PyObject* pProcessClass = PyObject_GetAttrString(pCMod, root_class.c_str());
346 Py_DECREF(pCMod); // don't need the config module anymore
347 if (pProcessClass == 0) {
348 PyErr_Print();
349 EXCEPTION_RAISE("ConfigureError",
350 "Did not import root configuration class " + root_class);
351 }
352
353 PyObject* pProcess =
354 PyObject_GetAttrString(pProcessClass, root_object.c_str());
355 Py_DECREF(pProcessClass); // don't need the Process class anymore
356 if (pProcess == 0) {
357 // wasn't able to get lastProcess class member
358 PyErr_Print();
359 EXCEPTION_RAISE(
360 "ConfigureError",
361 "Process object not defined. This object is required to run.");
362 } else if (pProcess == Py_None) {
363 // lastProcess was left undefined
364 EXCEPTION_RAISE("ConfigureError",
365 "Did not create a configuration class instance");
366 }
367
368 // okay, now we have fully imported the script and gotten the handle
369 // to the last Process object defined in the script.
370 // We can now look at pProcess and get all of our parameters out of it.
371
373
374 // all done with python nonsense
375 // delete one parent python object
376 // MEMORY still not sure if this is enough, but not super worried about it
377 // because this only happens once per run
378 Py_DECREF(pProcess);
379 // close up python interpreter
380 if (Py_FinalizeEx() < 0) {
381 PyErr_Print();
382 EXCEPTION_RAISE("PyError",
383 "I wasn't able to close up the python interpreter!");
384 }
385}
386
388 // no python nonsense happens in here,
389 // this just takes the parameters determined earlier
390 // and puts them into the Process + EventProcessor framework
391
392 return std::make_unique<Process>(configuration_);
393}
394
395} // namespace framework
framework::config::Parameters configuration_
The entire configuration for this process.
static std::string root_object
the root configuration object name
static std::string root_module
the root configuration module name
ProcessHandle makeProcess()
Create a process object based on the python file information.
ConfigurePython(const std::string &pythonScript, char *args[], int nargs)
Class constructor.
static std::string root_class
the root configuration class name
Class encapsulating parameters for configuring a processor.
Definition Parameters.h:27
void setParameters(std::map< std::string, std::any > parameters)
Set the mapping of parameter names to value.
Definition Parameters.h:41
All classes in the ldmx-sw project use this namespace.
Definition PerfDict.cxx:45
std::string repr(PyObject *obj)
Get a C++ string representation of the input python object.
std::unique_ptr< Process > ProcessHandle
A handle to the current process Used to pass a process from ConfigurePython to fire....
Definition Process.h:248
PyObject * extractDictionary(PyObject *obj)
extract the dictionary of attributes from the input python object
static std::map< std::string, std::any > getMembers(PyObject *object)
Extract members from a python object.
static std::string getPyString(PyObject *pyObj)
Turn the input python string object into a C++ string.