fhiclmodule.cc
Go to the documentation of this file.
1 //----------------------------------------------------------------------
2 //
3 // Name: fclmodule.cc
4 //
5 // Purpose: A python extension module that wraps the c++ fcl
6 // api. The main purpose of this module is to provide the
7 // capability to read and parse a fcl file and return the
8 // resulting parameter set in the form of a python dictionary.
9 //
10 // This module compiles to a shared library (fhiclmodule.so).
11 // When this shared library is on the python path, it can be
12 // imported as a python module (import fhicl).
13 //
14 // Created: 4-Apr-2017 H. Greenlee
15 //
16 // FCL module interface.
17 //
18 // Module members.
19 //
20 // 1. Function make_pset(fclfile)
21 //
22 // The argument is the name of a fcl file (can be anywhere on $FHICL_FILE_PATH).
23 // The function reads the fcl file and returns the expanded parameter set
24 // in the form of a python dictionary.
25 //
26 // Example:
27 //
28 // #!/usr/bin/env python
29 // import fhicl
30 // pset = fhicl.make_pset('myfile.fcl')
31 //
32 //----------------------------------------------------------------------
33 
34 #include "cetlib/compiler_macros.h"
35 #pragma GCC diagnostic push
36 #if GCC_IS_AT_LEAST(7, 1, 0) || CLANG_IS_AT_LEAST(6, 0, 0) || \
37  APPLE_CLANG_IS_AT_LEAST(10, 0, 0)
38 #pragma GCC diagnostic ignored "-Wregister"
39 #endif
40 #include "pybind11/pybind11.h" // Needs to be (almost) first due to macro definition issues
41 #pragma GCC diagnostic pop
42 #include "cetlib/search_path.h"
43 #include "cetlib_except/exception.h"
44 #include "fhiclcpp/ParameterSet.h"
48 
49 #include <iomanip>
50 #include <iostream>
51 #include <sstream>
52 
53 namespace py = pybind11;
54 
55 namespace {
56 
57  class PythonDictConverter : public fhicl::ParameterSetWalker {
58  public:
59  using key_t = std::string;
60  using any_t = std::any;
61 
62  py::dict result() const;
63 
64  private:
65  // Base class overrides.
66  void enter_table(key_t const& key, any_t const& any) override;
67  void enter_sequence(key_t const& key, any_t const& any) override;
68  void atom(key_t const& key, any_t const& any) override;
69  void exit_table(key_t const& key, any_t const& any) override;
70  void exit_sequence(key_t const& key, any_t const& any) override;
71 
72  void add_object(key_t const& key,
73  py::object pyobj); // Add object to current parent.
74 
75  // Data members.
76 
77  std::vector<pybind11::list> lists_{};
78  // Result stack.
79  // dicts_[0] is the entire parameter set (a python dictionary).
80  // dicts_.back() is the current parent container that is being filled
81  // (a python dictionary or list).
82  std::vector<pybind11::dict> dicts_{pybind11::dict{}};
83  };
84 
85  pybind11::dict
87  //
88  // Purpose: Return result. When this method is called, the result stack
89  // should contain exactly one object, and this object should be a
90  // python dictionary.
91  //
92  // Returns: Python dictionary.
93  //
94  {
95  // Do consistency checks.
96  if (dicts_.size() != 1)
97  throw cet::exception("fclmodule")
98  << "Result stack has wrong size: " << dicts_.size() << std::endl;
99 
100  return dicts_[0];
101  }
102 
103  void
104  PythonDictConverter::enter_table(key_t const& key, any_t const&)
105  //
106  // Purpose: Convert table.
107  //
108  // Arguments: key - Key of this object.
109  // any - Object
110  //
111  {
112  // Make a new empty python dictionary for this table.
113  py::dict dict;
114 
115  // Insert this dictionary into the current parent container.
116  add_object(key, dict);
117 
118  dicts_.push_back(std::move(dict));
119  }
120 
121  void
122  PythonDictConverter::enter_sequence(key_t const& key, any_t const& any)
123  //
124  // Purpose: Convert sequence.
125  //
126  // Arguments: key - Key of this object.
127  // any - Object
128  //
129  {
130  // Get length of sequence.
131  auto const& anyvec = std::any_cast<std::vector<any_t> const&>(any);
132  unsigned int const n = anyvec.size();
133 
134  // Make a new python list of the required size.
135  py::list list{n};
136 
137  // Insert the list into the current parent container.
138  add_object(key, list);
139 
140  // Make the newly created python dictionary the current parent container.
141  lists_.push_back(std::move(list));
142  }
143 
144  void
145  PythonDictConverter::atom(key_t const& key, any_t const& any)
146  //
147  // Purpose: Convert atom.
148  //
149  // Arguments: key - Key of this object.
150  // any - Object
151  //
152  {
153  // Extract atom as string.
154  auto const& atom = std::any_cast<std::string const&>(any);
155 
156  // Get lower case version of argument string.
157  std::string lcatom{atom};
158  std::transform(lcatom.begin(),
159  lcatom.end(),
160  lcatom.begin(),
161  [](unsigned char c) { return std::tolower(c); });
162 
163  // Check for boolean.
164  if (lcatom == std::string("true") || lcatom == std::string("\"true\"")) {
165  return add_object(key, py::bool_{true});
166  } else if (lcatom == std::string("false") ||
167  lcatom == std::string("\"false\"")) {
168  return add_object(key, py::bool_{false});
169  }
170 
171  // Check for quoted string.
172  auto const n = atom.size();
173  if (n >= 2 && atom[0] == '"' && atom[n - 1] == '"') {
174  std::string s = atom.substr(1, n - 2);
175  return add_object(key, py::str(s));
176  }
177 
178  // Check for int.
179  {
180  std::istringstream iss(atom);
181  long i;
182  iss >> std::noskipws >> i;
183  if (iss.eof() && !iss.fail()) {
184  return add_object(key, py::int_(i));
185  }
186  }
187 
188  // Check for float.
189  {
190  std::istringstream iss(atom);
191  double x;
192  iss >> std::noskipws >> x;
193  if (iss.eof() && !iss.fail()) {
194  return add_object(key, py::float_(x));
195  }
196  }
197 
198  // Last resort store a copy of the original string (unquoted string).
199  add_object(key, py::str(atom));
200  }
201 
202  void
203  PythonDictConverter::exit_table(key_t const&, any_t const&)
204  //
205  // Purpose: Close parent table.
206  //
207  {
208  // Do consistency checks.
209  if (dicts_.size() < 2)
210  throw cet::exception("fclmodule")
211  << "Result stack has wrong size: " << dicts_.size() << std::endl;
212 
213  // Pop the current parent (this table) off the result stack.
214  dicts_.pop_back();
215  }
216 
217  void
218  PythonDictConverter::exit_sequence(key_t const&, any_t const&)
219  //
220  // Purpose: Close current sequence.
221  //
222  // Arguments: key - Key of this object.
223  // any - Object
224  //
225  {
226  // Do consistency checks.
227  if (lists_.empty())
228  throw cet::exception("fclmodule")
229  << "Result stack has wrong size: " << dicts_.size() << std::endl;
230 
231  // Pop the current parent (this sequence) off the result stack.
232  lists_.pop_back();
233  }
234 
235  void
236  PythonDictConverter::add_object(key_t const& key, py::object pyobj)
237  //
238  // Purpose: Add object to the current parent container. The parent object
239  // can be either a python dictionary or a python list. The key
240  // argument is only used if the parent is a dictionary.
241  //
242  // Arguments: key - Key of object in parent.
243  // pyobj - Python object.
244  //
245  {
246  assert(!dicts_.empty());
248  auto& parent = dicts_.back();
249  parent[py::str(key)] = pyobj;
250  return;
251  }
252 
253  assert(!lists_.empty());
254  auto& parent_list = lists_.back();
255  auto const i = fhicl::detail::index_for_sequence_element(key);
256  parent_list[i] = pyobj;
257  }
258 
259  py::object
260  format(py::handle const obj,
261  unsigned int pos,
262  unsigned int const indent,
263  unsigned int const maxlen)
264  //
265  // Purpose: Convert a python object to a prettified string. The resulting
266  // string is supposed to be valid python code.
267  //
268  // Arguments: obj - Object to be formatted.
269  // pos - Current character position (number of characters
270  // printed since the last newline).
271  // indent - Indentation level (spaces) for multiline formatting.
272  // maxlen - Maximum line length before breaking.
273  //
274  // Returns: c++ string.
275  //
276  // Usage:
277  //
278  // This function is designed to call itself recursively in order to descend
279  // into structured objects like dictionaries and sequences.
280  //
281  // Dictionaries and sequences may be printed in either single-line or
282  // multiline format, depending on the complexity of the contained objects, and
283  // the indent and maxlen parameters.
284  //
285  {
286  // Result string stream.
287  using py::isinstance;
288  std::ostringstream ss;
289  if (isinstance<py::bool_>(obj)) {
290  ss << (obj.cast<py::bool_>() ? "True" : "False");
291  } else if (isinstance<py::int_>(obj)) {
292  ss << obj.cast<long>();
293  } else if (isinstance<py::float_>(obj)) {
294  ss << obj.cast<double>();
295  } else if (isinstance<py::str>(obj)) {
296  // String objects, add single quotes, but don't do any other formatting.
297  ss << "'" << obj.cast<std::string>() << "'";
298  } else if (isinstance<py::dict>(obj)) {
299  auto const dict = obj.cast<py::dict>();
300  auto const n = dict.size();
301  // Always print dictionary objects in multiline format, one key per line.
302  // Make a first pass over the list of keys to determine the maximum length
303  // key. Keys are assumed to be strings.
304  std::size_t keymaxlen{};
305  for (auto const& pr : dict) {
306  auto const key_str = pr.first.cast<std::string>();
307  if (key_str.size() > keymaxlen)
308  keymaxlen = key_str.size();
309  }
310 
311  // Second pass, loop over keys and values and convert them to strings.
312  char sep = '{';
313  for (auto const& [key, value] : dict) {
314  auto const key_str = key.cast<std::string>();
315  auto const py_value =
316  format(value, indent + keymaxlen + 7, indent + 2, maxlen);
317  auto const value_str = py_value.cast<std::string>();
318  std::string ksquote = std::string("'") + key_str + std::string("'");
319  ss << sep << '\n'
320  << std::setw(indent + 2) << "" << std::setw(keymaxlen + 2)
321  << std::left << ksquote << " : " << value_str;
322  sep = ',';
323  }
324  if (n == 0)
325  ss << "{}";
326  else
327  ss << '\n' << std::setw(indent + 1) << std::right << '}';
328  } else if (isinstance<py::list>(obj) || isinstance<py::tuple>(obj)) {
329  bool const is_list = isinstance<py::list>(obj);
330 
331  // Sequence printing handled here.
332  // Break lines only when position exceeds maxlen.
333  char const open_seq = is_list ? '[' : '(';
334  char const close_seq = is_list ? ']' : ')';
335  auto const seq = obj.cast<py::sequence>();
336 
337  // Loop over elements of this sequence.
338  std::string sep(1, open_seq);
339  unsigned int i{};
340  unsigned int const break_indent = pos + 1;
341  for (auto const ele : seq) {
342  ss << sep;
343  pos += sep.size();
344 
345  // Get the formatted string representation of this object.
346  auto const value_str = format(ele, pos, break_indent, maxlen);
347  auto f = value_str.cast<std::string>();
348 
349  // Get the number of characters before the first newline.
350  auto const fs = f.size();
351  auto const n1 = std::min(f.find('\n'), fs);
352 
353  // Decide if we want to break the line before printing this element.
354  // Never break at the first element of a sequence.
355  // Force a break (except at first element) if this is a structured
356  // element. If we do break here, reformat this element with the updated
357  // position.
358  bool const force_break{isinstance<py::list>(ele) ||
359  isinstance<py::tuple>(ele) ||
360  isinstance<py::dict>(ele)};
361  if (i > 0 && (force_break || pos + n1 > maxlen)) {
362  ss << '\n' << std::setw(break_indent) << "";
363  pos = break_indent;
364  auto const value_str = format(ele, pos, break_indent, maxlen);
365  f = value_str.cast<std::string>();
366  }
367  ++i;
368 
369  // Print this element
370  ss << f;
371 
372  // Update the current character position, taking into account
373  // whether the string we just printed contains a newline.
374  auto const n2 = f.find_last_of('\n');
375  if (n2 >= fs)
376  pos += fs;
377  else
378  pos = fs - n2 - 1;
379 
380  sep = std::string(", ");
381  }
382 
383  // Close sequence.
384  if (seq.size() == 0ull) {
385  ss << open_seq;
386  }
387  ss << close_seq;
388  } else {
389  // Not supported by FHiCL
390  return py::none{};
391  }
392 
393  return py::str{ss.str()};
394  }
395 
396  pybind11::dict
397  make_pset(std::string const& filename)
398  {
399  fhicl::ParameterSet pset;
400  cet::filepath_lookup maker{"FHICL_FILE_PATH"};
401 
402  fhicl::make_ParameterSet(filename, maker, pset);
403  PythonDictConverter converter;
404  pset.walk(converter);
405  return converter.result();
406  }
407 
408  py::object
409  pretty(py::dict dict)
410  //
411  // Purpose: Public module function to convert a python fcl dictionary to a
412  // prettified string.
413  //
414  // Argument: dict - dictionary to formatted.
415  //
416  // Returned value: A python string or none.
417  //
418  {
419  return format(dict, 0, 0, 80);
420  }
421 }
422 
424 {
425  m.doc() = "Extention module for reading a FHiCL configuration file into a "
426  "native Python dictionary.";
427  m.def("make_pset", &make_pset, "Make dictionary from parameter set.");
428  m.def("pretty",
429  &pretty,
430  "Print the in-memory dictionary. Note that what is printed is not a "
431  "FHiCL-conforming string.");
432  py::register_exception<cet::exception>(m, "CetException");
433 }
bool is_sequence_element(std::string const &key)
static QCString result
static const int maxlen
Definition: qregexp.cpp:904
std::string string
Definition: nybbler.cc:12
static bool format(QChar::Decomposition tag, QString &str, int index, int len)
Definition: qstring.cpp:11496
void make_ParameterSet(intermediate_table const &tbl, ParameterSet &ps)
string filename
Definition: train.py:213
def key(type, name=None)
Definition: graph.py:13
std::void_t< T > n
def move(depos, offset)
Definition: depos.py:107
std::size_t index_for_sequence_element(std::string const &name)
void walk(ParameterSetWalker &psw) const
Q_EXPORT QTSManip setw(int w)
Definition: qtextstream.h:331
T min(sqlite3 *const db, std::string const &table_name, std::string const &column_name)
Definition: statistics.h:55
const GenericPointer< typename T::ValueType > T2 value
Definition: pointer.h:1225
PYBIND11_MODULE(fhicl, m)
Definition: fhiclmodule.cc:423
def is_list(x)
Definition: main.py:79
list x
Definition: train.py:276
def maker(G, ac, typename)
Definition: apa.py:280
static QCString * s
Definition: config.cpp:1042
def parent(G, child, parent_type)
Definition: graph.py:67
static QCString str
cet::coded_exception< error, detail::translate > exception
Definition: exception.h:33
QTextStream & endl(QTextStream &s)
std::string key_t
Definition: KeyAssembler.h:70