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 99 of file Python.cxx.

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

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 169 of file Python.cxx.

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

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 58 of file Python.cxx.

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

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 74 of file Python.cxx.

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

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 300 of file Python.cxx.

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

References getMembers().