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