Skip to content

Commit ee245a8

Browse files
committed
More fun with C++ templates
Use a shim class that exposes pybind11-like attribute registation functions.
1 parent 5fa65b6 commit ee245a8

34 files changed

+388
-257
lines changed

core/include/core/pybind11_compat.h

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ class G3PythonInterpreter {
4848
// pybind11 compatibility functions
4949
namespace boost { namespace python {
5050

51+
namespace detail {
52+
5153
// template magic to collect all the py::arg()'s into one object for boost
5254
template <typename... Args>
53-
auto reorder_keywords(Args&&... args) {
55+
auto extract_keywords(Args&&... args) {
5456
using T = py::detail::keywords<1>;
5557

5658
auto extra = std::tuple_cat(
@@ -75,7 +77,9 @@ auto reorder_keywords(Args&&... args) {
7577
return detail::keywords<sizeof...(kw)>{kw...};
7678
}, kwargs);
7779

78-
return std::tuple_cat(extra, std::make_tuple(kwds));
80+
return std::pair(extra, std::move(kwds));
81+
}
82+
7983
}
8084

8185
class module_ : public scope
@@ -87,11 +91,152 @@ class module_ : public scope
8791

8892
py::object def_submodule(const std::string &name);
8993

94+
template <typename Func>
95+
void def(const char *name, Func && fn) {
96+
py::def(name, std::forward<Func>(fn));
97+
}
98+
99+
template <typename Func>
100+
void def(const char *name, Func && fn, const char *doc) {
101+
py::def(name, std::forward<Func>(fn), doc);
102+
}
103+
104+
template <size_t N, typename Func>
105+
void def(const char *name, Func && fn, py::detail::keywords<N> args, const char *doc) {
106+
py::def(name, std::forward<Func>(fn), args, doc);
107+
}
108+
109+
template <typename Func, typename... Args>
110+
void def(const char *name, Func && fn, Args &&... args) {
111+
auto [extra, kwargs] = py::detail::extract_keywords(std::forward<Args>(args)...);
112+
std::apply([&, &kw=kwargs](auto&&... a) {
113+
py::def(name, std::forward<Func>(fn), kw, a...);
114+
}, extra);
115+
}
116+
};
117+
118+
// class with pybind11-like functions
119+
template <typename...T>
120+
class compat_class_
121+
{
122+
public:
123+
typedef compat_class_<T...> self;
124+
125+
compat_class_(const char *name, const char *docstring = 0)
126+
: cls_(name, docstring, py::no_init) {};
127+
128+
auto ptr() { return cls_.ptr(); }
129+
130+
template <typename... Args>
131+
self& add_property(Args &&... args) {
132+
cls_.add_property(std::forward<Args>(args)...);
133+
return *this;
134+
}
135+
136+
template <typename... Args>
137+
self& add_static_property(Args &&... args) {
138+
cls_.add_static_property(std::forward<Args>(args)...);
139+
return *this;
140+
}
141+
142+
template <typename... Args>
143+
self& def_property(const char *name, Args &&... args) {
144+
cls_.add_property(name, std::forward<Args>(args)...);
145+
return *this;
146+
}
147+
148+
template <typename... Args>
149+
self& def_property_readonly(const char *name, Args &&... args) {
150+
cls_.add_property(name, std::forward<Args>(args)...);
151+
return *this;
152+
}
153+
90154
template <typename... Args>
91-
auto def(Args&&... args) {
92-
std::apply([](auto&&... a) { py::def(a...); },
93-
reorder_keywords(args...));
155+
self& def_readwrite(Args &&... args) {
156+
cls_.def_readwrite(std::forward<Args>(args)...);
157+
return *this;
158+
}
159+
160+
template <typename... Args>
161+
self& def_readonly(Args &&... args) {
162+
cls_.def_readonly(std::forward<Args>(args)...);
163+
return *this;
164+
}
165+
166+
template <typename P>
167+
self& def_pickle(P && p) {
168+
cls_.def_pickle(p);
169+
return *this;
170+
}
171+
172+
self& staticmethod(const char *name) { cls_.staticmethod(name); return *this; }
173+
174+
template <typename Func>
175+
self& def(const char *name, Func && fn) {
176+
cls_.def(name, std::forward<Func>(fn));
177+
return *this;
178+
}
179+
180+
template <typename Func>
181+
self& def(const char *name, Func && fn, const char *doc) {
182+
cls_.def(name, std::forward<Func>(fn), doc);
183+
return *this;
184+
}
185+
186+
template <size_t N, typename Func>
187+
self& def(const char *name, Func && fn, py::detail::keywords<N> args, const char *doc = 0) {
188+
cls_.def(name, std::forward<Func>(fn), args, doc);
189+
return *this;
94190
}
191+
192+
template <typename Func, typename... Args>
193+
self& def(const char *name, Func && fn, Args && ... args) {
194+
if (sizeof...(args) == 0) {
195+
cls_.def(name, std::forward<Func>(fn));
196+
return *this;
197+
}
198+
auto [extra, kwargs] = py::detail::extract_keywords(std::forward<Args>(args)...);
199+
std::apply([&, &kw=kwargs](auto&&... a) {
200+
cls_.def(name, std::forward<Func>(fn), kw, a...);
201+
}, extra);
202+
return *this;
203+
}
204+
205+
template <class V>
206+
self& def(const def_visitor<V> &visitor) {
207+
cls_.def(visitor);
208+
return *this;
209+
}
210+
211+
template <typename... InitArgs, typename... Args>
212+
self& def(const py::init<InitArgs...> &initf, Args &&... args) {
213+
if (sizeof...(args) == 0) {
214+
cls_.def(initf);
215+
return *this;
216+
}
217+
auto [extra, kwargs] = py::detail::extract_keywords(std::forward<Args>(args)...);
218+
auto vis = std::apply([&, &kw=kwargs](auto&&... a) {
219+
return py::init<InitArgs...>(kw, a...);
220+
}, extra);
221+
cls_.def(vis);
222+
return *this;
223+
}
224+
225+
template <typename... Args>
226+
self& def_static(const char *name, Args &&... args) {
227+
this->def(name, std::forward<Args>(args)...);
228+
cls_.staticmethod(name);
229+
return *this;
230+
}
231+
232+
self& def_buffer(PyBufferProcs &p) {
233+
PyTypeObject *obj = (PyTypeObject *)cls_.ptr();
234+
obj->tp_as_buffer = &p;
235+
return *this;
236+
}
237+
238+
private:
239+
py::class_<T...> cls_;
95240
};
96241

97242
class gil_scoped_release : public G3PythonContext

core/include/core/pybindings.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ register_class_copyable(py::module_ &scope, const std::string &name, Args&&...ar
7373
{
7474
(void) scope;
7575

76-
return py::class_<T, py::bases<Bases...>, std::shared_ptr<T> >(name.c_str(),
77-
std::forward<Args>(args)..., py::no_init);
76+
return py::compat_class_<T, py::bases<Bases...>, std::shared_ptr<T> >(name.c_str(),
77+
std::forward<Args>(args)...);
7878
}
7979

8080
// Register a class, with optional base classes.
@@ -85,8 +85,8 @@ register_class(py::module_ &scope, const std::string &name, Args&&...args)
8585
{
8686
(void) scope;
8787

88-
return py::class_<T, py::bases<Bases...>, std::shared_ptr<T>,
89-
boost::noncopyable>(name.c_str(), std::forward<Args>(args)..., py::no_init);
88+
return py::compat_class_<T, py::bases<Bases...>, std::shared_ptr<T>,
89+
boost::noncopyable>(name.c_str(), std::forward<Args>(args)...);
9090
}
9191

9292
// Register a G3Module derived class, for inclusion in a G3Pipeline.

core/src/G3Frame.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ g3frame_char_constructor(std::string max_4_chars)
410410

411411
static py::list g3frame_keys(const G3Frame &map)
412412
{
413-
py::list keys;
413+
py::list keys;
414414
std::vector<std::string> keyvec = map.Keys();
415415

416416
for (auto i = keyvec.begin(); i != keyvec.end(); i++)

core/src/G3InfiniteSource.cxx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ PYBINDINGS("core", scope) {
2020
register_g3module<G3InfiniteSource>(scope, "G3InfiniteSource",
2121
"Emits infinite frames, up to an optional maximum number n")
2222
.def(py::init<>())
23-
.def(py::init<G3Frame::FrameType, int>(
24-
(py::arg("type")=G3Frame::None, py::arg("n")=-1)));
23+
.def(py::init<G3Frame::FrameType, int>(),
24+
py::arg("type")=G3Frame::None, py::arg("n")=-1);
2525
};

core/src/G3Logging.cxx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ PYBINDINGS("core", scope) {
160160
"Logger that does not log. Useful if you don't want log messages");
161161
register_class<G3PrintfLogger, G3Logger>(scope, "G3PrintfLogger",
162162
"Logger that prints error messages to stderr (in color, if stderr is a tty).")
163-
.def(py::init<G3LogLevel>((py::arg("default_level")=G3DefaultLogLevel)))
163+
.def(py::init<G3LogLevel>(), py::arg("default_level")=G3DefaultLogLevel)
164164
.def_readwrite("trim_file_names", &G3PrintfLogger::TrimFileNames)
165165
.def_readwrite("timestamps", &G3PrintfLogger::Timestamps)
166166
;
@@ -172,7 +172,7 @@ PYBINDINGS("core", scope) {
172172
"Pass log messages to the syslog service. Initialize with a string identifier "
173173
"and a logging facility. See syslog(3) for details. Example:\n"
174174
"\timport syslog\n\tlogger = core.G3SyslogLogger('myprogram', syslog.LOG_USER)")
175-
.def(py::init<std::string, int, G3LogLevel>((py::arg("ident"), py::arg("facility"),
176-
py::arg("default_level")=G3DefaultLogLevel)))
175+
.def(py::init<std::string, int, G3LogLevel>(), py::arg("ident"), py::arg("facility"),
176+
py::arg("default_level")=G3DefaultLogLevel)
177177
;
178178
}

core/src/G3MultiFileWriter.cxx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ PYBINDINGS("core", scope) {
183183
"python callable as divide_on. This callable will be passed each "
184184
"frame in turn. If it returns True (or something with positive "
185185
"truth-value), a new file will be started at that frame.")
186-
.def(py::init<py::object, size_t, py::object, size_t>((py::arg("filename"),
187-
py::arg("size_limit"), py::arg("divide_on")=py::object(),
188-
py::arg("buffersize")=1024*1024)))
189-
.def_readonly("current_file", &G3MultiFileWriter::CurrentFile);
186+
.def(py::init<py::object, size_t, py::object, size_t>(), py::arg("filename"),
187+
py::arg("size_limit"), py::arg("divide_on")=py::object(),
188+
py::arg("buffersize")=1024*1024)
189+
.def_property_readonly("current_file", &G3MultiFileWriter::CurrentFile);
190190
}

core/src/G3NetworkSender.cxx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ PYBINDINGS("core", scope) {
5353
"serializing frames to be sent will be distributed across that many "
5454
"background threads, which is useful when high throughput of large frames "
5555
"is required, but is otherwise typically not necessary.")
56-
.def(py::init<std::string, int, int, int>((py::arg("hostname"),
57-
py::arg("port"), py::arg("max_queue_size")=0, py::arg("n_serializers")=0)))
56+
.def(py::init<std::string, int, int, int>(), py::arg("hostname"),
57+
py::arg("port"), py::arg("max_queue_size")=0, py::arg("n_serializers")=0)
5858
.def("Close", &G3NetworkSender::Close);
5959
};
6060

core/src/G3Pipeline.cxx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,10 @@ PYBINDINGS("core", scope) {
451451
"\t- False: discard input frame and return to first module, or end "
452452
"\t processing if returned by first module. Equivalent to [].\n")
453453
.def(py::init<>())
454-
.def("_Add_", &G3Pipeline::Add, py::arg("name")="")
454+
.def("_Add_", &G3Pipeline::Add, py::arg("module"), py::arg("name")="")
455455
.def("Run", &G3Pipeline::Run,
456-
(py::arg("profile")=false, py::arg("graph")=false,
457-
py::arg("signal_halt")=true),
456+
py::arg("profile")=false, py::arg("graph")=false,
457+
py::arg("signal_halt")=true,
458458
"Run pipeline. If profile is True, print execution time "
459459
"statistics for each module when complete. If graph is True, "
460460
"stores control flow data that can be processed with GraphViz "
@@ -464,11 +464,10 @@ PYBINDINGS("core", scope) {
464464
"halt_processing() is called.")
465465
.def("GetGraphInfo", &G3Pipeline::GetGraphInfo,
466466
"Get stored control flow information from Run(graph=True)")
467-
.def("halt_processing", &G3Pipeline_halt_processing,
467+
.def_static("halt_processing", &G3Pipeline_halt_processing,
468468
"Halts all running pipelines after they flush all currently "
469469
"in-flight frames. Once set, the first module will not be "
470470
"called again.")
471-
.staticmethod("halt_processing")
472471
.def_readonly("last_frame",
473472
&G3Pipeline::last_frame)
474473
;

core/src/G3Quat.cxx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -943,31 +943,28 @@ G3TimestreamQuatPtr container_from_object(py::object v)
943943
return quat_vec_container_from_object<G3TimestreamQuat>(v);
944944
}
945945

946-
static int
947-
G3TimestreamQuat_nsamples(const G3TimestreamQuat &r)
948-
{
949-
return r.size();
950-
}
951946

952947
PYBINDINGS("core", scope)
953948
{
954-
py::object q =
949+
auto q =
955950
register_class_copyable<Quat>(scope, "Quat",
956-
"Representation of a quaternion. Data in a,b,c,d.")
951+
"Representation of a quaternion. Data in a,b,c,d.")
957952
.def(py::init<>())
958953
.def(py::init<const Quat &>())
959-
.def(py::init<double, double, double, double>(
960-
"Create a quaternion from its four elements.", py::args("a", "b", "c", "d")))
961-
.def("__init__", py::make_constructor(quat_container_from_object, py::default_call_policies(),
962-
(py::arg("data"))), "Create a quaternion from a numpy array")
954+
.def(py::init<double, double, double, double>(),
955+
py::arg("a"), py::arg("b"), py::arg("c"), py::arg("d"),
956+
"Create a quaternion from its four elements.")
957+
.def("__init__", py::make_constructor(quat_container_from_object,
958+
py::default_call_policies(), (py::arg("data"))),
959+
"Create a quaternion from a numpy array")
963960
.def_pickle(g3frameobject_picklesuite<Quat>())
964-
.add_property("a", &Quat::a, "Scalar component")
965-
.add_property("b", &Quat::b, "First vector component")
966-
.add_property("c", &Quat::c, "Second vector component")
967-
.add_property("d", &Quat::d, "Third vector component")
968-
.add_property("real", &Quat::real,
961+
.def_property_readonly("a", &Quat::a, "Scalar component")
962+
.def_property_readonly("b", &Quat::b, "First vector component")
963+
.def_property_readonly("c", &Quat::c, "Second vector component")
964+
.def_property_readonly("d", &Quat::d, "Third vector component")
965+
.def_property_readonly("real", &Quat::real,
969966
"The real (scalar) part of the quaternion")
970-
.add_property("unreal", &Quat::unreal,
967+
.def_property_readonly("unreal", &Quat::unreal,
971968
"The unreal (vector) part of the quaternion")
972969
.def(~py::self)
973970
.def(-py::self)
@@ -1011,7 +1008,7 @@ PYBINDINGS("core", scope)
10111008
;
10121009

10131010
register_vector_of<Quat>(scope, "Quat");
1014-
py::object vq =
1011+
auto vq =
10151012
register_g3vector<G3VectorQuat>(scope, "G3VectorQuat",
10161013
"List of quaternions. Convertible to a 4xN numpy array. "
10171014
"Arithmetic operations on this object are fast and provide "
@@ -1038,7 +1035,7 @@ PYBINDINGS("core", scope)
10381035
.def("__abs__", vec_abs)
10391036
.def("__neg__", vec_neg)
10401037
.def("abs", vec_abs, "Return the Euclidean norm of each quaternion")
1041-
.add_property("real", vec_real,
1038+
.def_property_readonly("real", vec_real,
10421039
"Return the real (scalar) part of each quaternion")
10431040
;
10441041
PyTypeObject *vqclass = (PyTypeObject *)vq.ptr();
@@ -1048,7 +1045,7 @@ PYBINDINGS("core", scope)
10481045
vqclass->tp_flags |= Py_TPFLAGS_HAVE_NEWBUFFER;
10491046
#endif
10501047

1051-
py::object tq =
1048+
auto tq =
10521049
register_frameobject<G3TimestreamQuat, G3VectorQuat>(scope, "G3TimestreamQuat",
10531050
"Timestream of quaternions. Identical to a G3VectorQuat except "
10541051
"for the addition of start and stop times.")
@@ -1076,15 +1073,15 @@ PYBINDINGS("core", scope)
10761073
.def("__abs__", ts_abs)
10771074
.def("__neg__", ts_neg)
10781075
.def("abs", ts_abs, "Return the Euclidean norm of each quaternion")
1079-
.add_property("real", ts_real,
1076+
.def_property_readonly("real", ts_real,
10801077
"Return the real (scalar) part of each quaternion")
10811078
.def_readwrite("start", &G3TimestreamQuat::start,
10821079
"Time of the first sample in the time stream")
10831080
.def_readwrite("stop", &G3TimestreamQuat::stop,
10841081
"Time of the final sample in the timestream")
1085-
.add_property("sample_rate", &G3TimestreamQuat::GetSampleRate,
1082+
.def_property_readonly("sample_rate", &G3TimestreamQuat::GetSampleRate,
10861083
"Computed sample rate of the timestream.")
1087-
.add_property("n_samples", &G3TimestreamQuat_nsamples,
1084+
.def_property_readonly("n_samples", &G3TimestreamQuat::size,
10881085
"Number of samples in the timestream. Equivalent to len(ts)")
10891086
;
10901087
PyTypeObject *tqclass = (PyTypeObject *)tq.ptr();

core/src/G3Reader.cxx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ PYBINDINGS("core", scope) {
121121
"seek to the beginning of a particular frame in the file. Set "
122122
"track_filename to True to record the filename for each frame in "
123123
"the ._filename attribute (fragile).")
124-
.def(py::init<std::string, int, float, bool, size_t>((py::arg("filename"),
124+
.def(py::init<std::string, int, float, bool, size_t>(), py::arg("filename"),
125125
py::arg("n_frames_to_read")=0, py::arg("timeout")=-1.,
126-
py::arg("track_filename")=false, py::arg("buffersize")=1024*1024)))
127-
.def(py::init<std::vector<std::string>, int, float, bool, size_t>((
126+
py::arg("track_filename")=false, py::arg("buffersize")=1024*1024)
127+
.def(py::init<std::vector<std::string>, int, float, bool, size_t>(),
128128
py::arg("filename"), py::arg("n_frames_to_read")=0, py::arg("timeout")=-1.,
129-
py::arg("track_filename")=false, py::arg("buffersize")=1024*1024)))
129+
py::arg("track_filename")=false, py::arg("buffersize")=1024*1024)
130130
.def("tell", &G3Reader::Tell,
131131
"Return the current byte offset from start of stream.")
132132
.def("seek", &G3Reader::Seek,

0 commit comments

Comments
 (0)