LDMX Software
Python.cxx
Go to the documentation of this file.
1
10#include "Framework/Configure/Python.h"
11
12#include "Framework/Exception/Exception.h"
13
14/*~~~~~~~~~~~~*/
15/* python */
16/*~~~~~~~~~~~~*/
17#include "Python.h"
18
19#if PY_MAJOR_VERSION != 3
20#error("Framework requires compiling with Python3")
21#endif
22
23#undef DEV_IMAGE_MAJOR
24#if PY_MINOR_VERSION == 6
25#define DEV_IMAGE_MAJOR 3
26#elif PY_MINOR_VERSION == 10
27#define DEV_IMAGE_MAJOR 4
28#elif PY_MINOR_VERSION == 12
29#define DEV_IMAGE_MAJOR 5
30#endif
31
32#ifndef DEV_IMAGE_MAJOR
33#warning("Unrecognized Python3 minor version. The usage of the Python C API is untested!")
34#endif
35
36/*~~~~~~~~~~~~~~~~*/
37/* C++ StdLib */
38/*~~~~~~~~~~~~~~~~*/
39#include <any>
40#include <cstring>
41#include <iostream>
42#include <memory>
43#include <sstream>
44#include <string>
45#include <vector>
46
47namespace framework::config {
48
58static std::string getPyString(PyObject* pyObj) {
59 std::string retval;
60 PyObject* py_str = PyUnicode_AsEncodedString(pyObj, "utf-8", "Error ~");
61 retval = PyBytes_AS_STRING(py_str);
62 Py_XDECREF(py_str);
63 return retval;
64}
65
74std::string repr(PyObject* obj) {
75 PyObject* py_repr = PyObject_Repr(obj);
76 if (py_repr == nullptr) return "";
77 std::string str = getPyString(py_repr);
78 Py_XDECREF(py_repr);
79 return str;
80}
81
99PyObject* extractDictionary(PyObject* obj) {
111 PyObject** p_dictionary{_PyObject_GetDictPtr(obj)};
112 if (p_dictionary == NULL) {
113 if (PyDict_Check(obj)) {
114 return obj;
115 } else {
116 EXCEPTION_RAISE("ObjFail",
117 "Python Object '" + repr(obj) +
118 "' does not have __dict__ member and is not a dict.");
119 }
120 }
121 return *p_dictionary;
122}
123
169static Parameters getMembers(PyObject* object) {
170 PyObject* dictionary{extractDictionary(object)};
171 PyObject *key(0), *value(0);
172 Py_ssize_t pos = 0;
173
174 Parameters params;
175
176 while (PyDict_Next(dictionary, &pos, &key, &value)) {
177 std::string skey{getPyString(key)};
178
179 if (PyLong_Check(value)) {
180 if (PyBool_Check(value)) {
181 params.add(skey, bool(PyLong_AsLong(value)));
182 } else {
183 params.add(skey, int(PyLong_AsLong(value)));
184 }
185 } else if (PyFloat_Check(value)) {
186 params.add(skey, PyFloat_AsDouble(value));
187 } else if (PyUnicode_Check(value)) {
188 params.add(skey, getPyString(value));
189 } else if (PyList_Check(value)) {
190 // assume everything is same value as first value
191 if (PyList_Size(value) > 0) {
192 auto vec0{PyList_GetItem(value, 0)};
193
194 if (PyLong_Check(vec0)) {
195 std::vector<int> vals;
196
197 for (auto j{0}; j < PyList_Size(value); j++)
198 vals.push_back(PyLong_AsLong(PyList_GetItem(value, j)));
199
200 params.add(skey, vals);
201
202 } else if (PyFloat_Check(vec0)) {
203 std::vector<double> vals;
204
205 for (auto j{0}; j < PyList_Size(value); j++)
206 vals.push_back(PyFloat_AsDouble(PyList_GetItem(value, j)));
207
208 params.add(skey, vals);
209
210 } else if (PyUnicode_Check(vec0)) {
211 std::vector<std::string> vals;
212 for (Py_ssize_t j = 0; j < PyList_Size(value); j++) {
213 PyObject* elem = PyList_GetItem(value, j);
214 vals.push_back(getPyString(elem));
215 }
216
217 params.add(skey, vals);
218 } else if (PyList_Check(vec0)) {
219 // a list in a list ??? oof-dah
220 if (PyList_Size(vec0) > 0) {
221 auto vecvec0{PyList_GetItem(vec0, 0)};
222 if (PyLong_Check(vecvec0)) {
223 std::vector<std::vector<int>> vals;
224 for (auto j{0}; j < PyList_Size(value); j++) {
225 auto subvec{PyList_GetItem(value, j)};
226 std::vector<int> subvals;
227 for (auto k{0}; k < PyList_Size(subvec); k++) {
228 subvals.push_back(PyLong_AsLong(PyList_GetItem(subvec, k)));
229 }
230 vals.push_back(subvals);
231 }
232 params.add(skey, vals);
233 } else if (PyFloat_Check(vecvec0)) {
234 std::vector<std::vector<double>> vals;
235 for (auto j{0}; j < PyList_Size(value); j++) {
236 auto subvec{PyList_GetItem(value, j)};
237 std::vector<double> subvals;
238 for (auto k{0}; k < PyList_Size(subvec); k++) {
239 subvals.push_back(
240 PyFloat_AsDouble(PyList_GetItem(subvec, k)));
241 }
242 vals.push_back(subvals);
243 }
244 params.add(skey, vals);
245 } else if (PyUnicode_Check(vecvec0)) {
246 std::vector<std::vector<std::string>> vals;
247 for (auto j{0}; j < PyList_Size(value); j++) {
248 auto subvec{PyList_GetItem(value, j)};
249 std::vector<std::string> subvals;
250 for (auto k{0}; k < PyList_Size(subvec); k++) {
251 subvals.push_back(getPyString(PyList_GetItem(subvec, k)));
252 }
253 vals.push_back(subvals);
254 }
255 params.add(skey, vals);
256 } else if (PyList_Check(vecvec0)) {
257 EXCEPTION_RAISE("BadConf",
258 "A python list with dimension greater than 2 is "
259 "not supported.");
260 } else {
261 // RECURSION zoinks!
262 std::vector<std::vector<framework::config::Parameters>> vals;
263 for (auto j{0}; j < PyList_Size(value); j++) {
264 auto subvec{PyList_GetItem(value, j)};
265 std::vector<framework::config::Parameters> subvals;
266 for (auto k{0}; k < PyList_Size(subvec); k++) {
267 subvals.emplace_back(getMembers(PyList_GetItem(subvec, k)));
268 }
269 vals.push_back(subvals);
270 }
271 params.add(skey, vals);
272 }
273 } // non-zero size
274 } else {
275 // RECURSION zoinks!
276 // If the objects stored in the list doesn't
277 // satisfy any of the above conditions, just
278 // create a vector of parameters objects
279 std::vector<framework::config::Parameters> vals;
280 for (auto j{0}; j < PyList_Size(value); ++j) {
281 auto elem{PyList_GetItem(value, j)};
282 vals.emplace_back(getMembers(elem));
283 }
284 params.add(skey, vals);
285 } // type of object in python list
286 } // python list has non-zero size
287 } else {
288 // object got here, so we assume
289 // it is a higher level object
290 //(same logic as last option for a list)
291
292 // RECURSION zoinks!
293 params.add(skey, getMembers(value));
294 } // python object type
295 } // loop through python dictionary
296
297 return params;
298}
299
300Parameters run(const std::string& root_object, const std::string& pythonScript,
301 char* args[], int nargs) {
302 // assumes that nargs >= 0
303 // this is true always because we error out if no python script has been
304 // found
305
306 // load a handle to the config file into memory (and check that it exists)
307 std::unique_ptr<FILE, int (*)(FILE*)> fp{fopen(pythonScript.c_str(), "r"),
308 &fclose};
309 if (fp.get() == NULL) {
310 EXCEPTION_RAISE("ConfigDNE",
311 "Passed config script '" + pythonScript +
312 "' is not accessible.\n"
313 " Did you make a typo in the path to the script?\n"
314 " Are you referencing a directory that is not "
315 "mounted to the container?");
316 }
317
318 // python needs the argument list as if you are on the command line
319 // targs = [ script , arg0 , arg1 , ... ] ==> len(targs) = nargs+1
320 // the updated Python3.12 (DEV_IMAGE_MAJOR == 5) C API looks to have
321 // more helper functions to avoid having to do this ourselves, but
322 // I think sharing the same targs between the different Python versions
323 // makes the code cleaner
324 wchar_t** targs = new wchar_t*[nargs + 1];
325 targs[0] = Py_DecodeLocale(pythonScript.c_str(), NULL);
326 for (int i = 0; i < nargs; i++) targs[i + 1] = Py_DecodeLocale(args[i], NULL);
327
328#if DEV_IMAGE_MAJOR < 5
329 // name our program after the script that is being run
330 Py_SetProgramName(targs[0]);
331
332 // start up python interpreter
333 Py_Initialize();
334
335 // The third argument to PySys_SetArgvEx tells python to import
336 // the args and add the directory of the first argument to
337 // the PYTHONPATH
338 // This way, the command to import the module_ just needs to be
339 // the name of the python script
340 PySys_SetArgvEx(nargs + 1, targs, 1);
341#else
342 PyStatus status;
343 PyConfig config;
344 PyConfig_InitPythonConfig(&config);
345 // we do not want python to parse our args (we are already doing that)
346 config.parse_argv = 0;
347 // note to future developers: the embedding docs encourage users to
348 // set config.isolated = 1 in order to more securely embed python.
349 // we do /not/ want to do this because we want to inherit the
350 // external environment of python
351
352 // copy over program name
353 status = PyConfig_SetString(&config, &config.program_name, targs[0]);
354 if (PyStatus_Exception(status)) {
355 PyConfig_Clear(&config);
356 Py_ExitStatusException(status);
357 EXCEPTION_RAISE("PyConfigInit",
358 "Unable to set the program name in the python config.");
359 }
360 // copy over updated argument vector
361 status = PyConfig_SetArgv(&config, nargs + 1, targs);
362 if (PyStatus_Exception(status)) {
363 PyConfig_Clear(&config);
364 Py_ExitStatusException(status);
365 EXCEPTION_RAISE("PyConfigInit",
366 "Unable to set argv for the python config.");
367 }
368 // read and solidify the configuration
369 status = PyConfig_Read(&config);
370 if (PyStatus_Exception(status)) {
371 PyConfig_Clear(&config);
372 Py_ExitStatusException(status);
373 EXCEPTION_RAISE("PyConfigInit", "Unable to read the python config.");
374 }
375 // initialize the python interpreter with our deduced configuration
376 status = Py_InitializeFromConfig(&config);
377 if (PyStatus_Exception(status)) {
378 PyConfig_Clear(&config);
379 Py_ExitStatusException(status);
380 Py_FinalizeEx();
381 EXCEPTION_RAISE("PyConfigInit",
382 "Unable to initilize the python interpreter.");
383 }
384 // don't need config anymore now that the initialization is done
385 PyConfig_Clear(&config);
386#endif
387
388 if (PyRun_SimpleFile(fp.get(), pythonScript.c_str()) != 0) {
389 // running the script executed with an error
390 PyErr_Print();
391 Py_FinalizeEx();
392 EXCEPTION_RAISE("Python", "Execution of python script failed.");
393 }
394
395 // script has been run so we can
396 // free up arguments to python script
397 for (int i = 0; i < nargs + 1; i++) PyMem_RawFree(targs[i]);
398 delete[] targs;
399
400 // running a python script effectively imports the script into the top-level
401 // code environment called '__main__'
402 // we "import" this module_ which is already imported to get a handle
403 // on the necessary objects
404 PyObject* py_root_obj = PyImport_ImportModule("__main__");
405 if (!py_root_obj) {
406 PyErr_Print();
407 Py_FinalizeEx();
408 EXCEPTION_RAISE("Python",
409 "I don't know what happened. This should never happen.");
410 }
411
412 // descend the hierarchy of modules that hold the root_object
413 // manually expanding the '.' allows us to handle all of the different
414 // cases of how the configuration Python class could have been imported
415 // and constructed
416 std::string attr;
417 std::stringstream root_obj_ss{root_object};
418 while (std::getline(root_obj_ss, attr, '.')) {
419 PyObject* one_level_down =
420 PyObject_GetAttrString(py_root_obj, attr.c_str());
421 if (one_level_down == 0) {
422 Py_FinalizeEx();
423 EXCEPTION_RAISE("Python", "Unable to find python object '" + attr + "'.");
424 }
425 Py_DECREF(py_root_obj); // don't need previous python object anymore
426 py_root_obj = one_level_down;
427 }
428
429 // now py_root_obj should hold the root configuration object
430 if (py_root_obj == Py_None) {
431 // root config object left undefined
432 Py_FinalizeEx();
433 EXCEPTION_RAISE("Python",
434 "Root configuration object " + root_object +
435 " not defined. This object is required to run.");
436 }
437
438 // okay, now we have fully imported the script and gotten the handle
439 // to the root configuration object defined in the script.
440 // We can now look at this object and recursively get all of our parameters
441 // out of it.
442
443 Parameters configuration(getMembers(py_root_obj));
444
445 // all done with python nonsense
446 // delete one parent python object
447 // MEMORY still not sure if this is enough, but not super worried about it
448 // because this only happens once per run
449 Py_DECREF(py_root_obj);
450 // close up python interpreter
451 if (Py_FinalizeEx() < 0) {
452 PyErr_Print();
453 EXCEPTION_RAISE("Python",
454 "I wasn't able to close up the python interpreter!");
455 }
456
457 return configuration;
458}
459
460} // namespace framework::config
Class encapsulating parameters for configuring a processor.
Definition Parameters.h:29
void add(const std::string &name, const T &value)
Add a parameter to the parameter list.
Definition Parameters.h:42
python execution and parameter extraction
Definition Parameters.h:19
Parameters run(const std::string &root_object, const std::string &pythonScript, char *args[], int nargs)
run the python script and extract the parameters
Definition Python.cxx:300
std::string repr(PyObject *obj)
Get a C++ string representation of the input python object.
Definition Python.cxx:74
PyObject * extractDictionary(PyObject *obj)
extract the dictionary of attributes from the input python object
Definition Python.cxx:99
static Parameters getMembers(PyObject *object)
Extract members from a python object.
Definition Python.cxx:169
static std::string getPyString(PyObject *pyObj)
Turn the input python string object into a std::string.
Definition Python.cxx:58