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