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

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

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

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

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

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

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

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

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

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

References getMembers().