LDMX Software
framework::config Namespace Reference

python execution and parameter extraction More...

Classes

class  Parameters
 Class encapsulating parameters for configuring a processor. More...
 

Functions

Parameters run (const std::string &root_object, const std::string &pythonScript, char *args[], int nargs)
 run the python script and extract the parameters
 
static std::string getPyString (PyObject *pyObj)
 Turn the input python string object into a std::string.
 
std::string repr (PyObject *obj)
 Get a C++ string representation of the input python object.
 
PyObject * extractDictionary (PyObject *obj)
 extract the dictionary of attributes from the input python object
 
static Parameters getMembers (PyObject *object)
 Extract members from a python object.
 

Detailed Description

python execution and parameter extraction

this namespace is focused on holding the necessary functions to run and extract the configuration parameters from a python script. The documentation here is focused on the C++-side of the configuration - i.e. the translation of Python objects into their C++ counterparts.

Function Documentation

◆ extractDictionary()

PyObject * framework::config::extractDictionary ( PyObject * obj)

extract the dictionary of attributes from the input python object

This is separated into its own function to isolate the code that depends on the python version. Since memory-saving measures were integrated into Python between 3.6.9 and 3.10.6, we need to alter how we were using the C API to access an objects dict attribute. We are now using a "hidden" Python C API function which has only been added to the public C API documentation in Python 3.11. It has been manually tested for the two different Python versions we use by running the tests (some of which test the ConfigurePython) and running a basic simulation.

Exceptions
Exceptionif object does not have a dict and isn't a dict
Parameters
objpointer to python object to extract from
Returns
pointer to python dictionary for its members

This was developed for Python3.10 when upgrading to Ubuntu 22.04 in the development container image. A lot of memory-saving measures were taken which means we have to explicitly ask Python to construct the dict object for us so that it knows to "waste" the memory on it.

https://docs.python.org/3/c-api/object.html

We use _PyObject_GetDictPtr because that will not set an error if the dict does not exist.

Definition at line 101 of file Python.cxx.

101 {
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}

References repr().

Referenced by getMembers().

◆ getMembers()

static Parameters framework::config::getMembers ( PyObject * object)
static

Extract members from a python object.

Iterates through the object's dictionary and translates the objects inside of it into the type-specified C++ equivalents, then puts these objects into an instance of the Parameters class.

This function is recursive. If a non-base type is encountered, we pass it back along to this function to translate it's own dictionary.

We rely completely on python being awesome. For all higher level class objects, python keeps track of all of its member variables in the member dictionary __dict__.

No Py_DECREF calls are made because all of the members of an object are borrowed references, meaning that when we destory that object, it handles the other members. We destroy the one Python object owning all of these references at the end of this function.

Note
Not sure if this is not leaking memory, kinda just trusting the Python / C API docs on this one.
Empty lists are NOT read in because there is no way for us to know what type should be inside the list. This means list parameters that can be empty need to put in a default empty list value: {}.

This recursive extraction method is able to handle the following cases.

  • User-defined classes (via the __dict__ member) are extracted to Parameters
  • one-dimensional lists whose entries all have the same type are extracted to std::vector of the type of the first entry in the list
  • dict objects are extracted to Parameters
  • Python str are extracted to std::string
  • Python int are extracted to C++ int
  • Python bool are extracted to C++ bool
  • Python float are extracted to C++ double

Known design flaws include

  • No support for nested Python lists
  • Annoying band-aid solution for empty Python lists
Parameters
[in]objectPython object to get members from
Returns
Mapping between member name and value.

Definition at line 171 of file Python.cxx.

171 {
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}
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

References framework::config::Parameters::add(), extractDictionary(), getMembers(), and getPyString().

Referenced by getMembers(), and run().

◆ getPyString()

static std::string framework::config::getPyString ( PyObject * pyObj)
static

Turn the input python string object into a std::string.

Helpful to condense down the multi-line nature of the python3 code.

Parameters
[in]pyObjpython object assumed to be a string python object
Returns
the value stored in it

Definition at line 60 of file Python.cxx.

60 {
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}

Referenced by getMembers(), and repr().

◆ repr()

std::string framework::config::repr ( PyObject * obj)

Get a C++ string representation of the input python object.

This is replicating the repr(obj) syntax of Python.

Parameters
[in]objpython object to get repr for
Returns
std::string form of repr

Definition at line 76 of file Python.cxx.

76 {
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}

References getPyString().

Referenced by extractDictionary().

◆ run()

Parameters framework::config::run ( const std::string & root_object,
const std::string & pythonScript,
char * args[],
int nargs )

run the python script and extract the parameters

This method contains all the parsing and execution of the python script.

Exceptions
Exceptionif the python script does not exit properly
Exceptionif any necessary components of the python configuration are missing. e.g. The Process class or the different members of the lastProcess object.

The basic premise of this function is to execute the python configuration script. Then, after the script has been executed, all of the parameters for the Process are gathered from python. The fact that the script has been executed means that the user can get up to a whole lot of shenanigans that can help them make their work more efficient.

Parameters
[in]fullpythonic path to the object to kickoff extraction
[in]pythonScriptFilename location of the python script.
[in]argsCommandline arguments to be passed to the python script.
[in]nargsNumber of commandline arguments, assumed to be >= 0

Definition at line 302 of file Python.cxx.

303 {
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}

References getMembers().