Skip to content
Snippets Groups Projects
Commit f7cc9fcb authored by Carsten Gräser's avatar Carsten Gräser
Browse files

[python] Add support for keyword arguments

This allows to pass keyword arguments to python callable
objects. You can now e.g. use `f(x,y, Python::arg("keyword", value))`
or `f(x,y, "keyword"_a=value)` after `using namespace Python::Literals`.

* This drops `operator()` implementations for fixed argument count
  in favour of a variadic template.
* `operator()` now supports positional and keyword arguments.
* positional and keyword arguments can be mixed and will be
  filtered automatically.
* The interface for passing keyword arguments basically follows the one
  in `std::format`.
* The mechanism for implementing keyword arguments is actually
  independent of the python bindings and may be of interest in other
  places. It is zero cost in the sense that argument positions
  are evaluated statically and arguments can be forwarded perfectly.
parent 0e1791f0
Branches
No related tags found
1 merge request!86[python] Add support for keyword arguments
Pipeline #31958 passed
......@@ -20,6 +20,7 @@
#include <dune/common/function.hh>
#include <dune/common/parametertree.hh>
#include <dune/common/typetraits.hh>
#include <dune/common/indices.hh>
#include <dune/grid/common/gridfactory.hh>
......@@ -35,6 +36,114 @@ namespace Python
// forward declarations
void handlePythonError(const std::string&, const std::string&);
Reference dict();
namespace Imp
{
// forward declarations
PyObject* inc(PyObject* p);
// Helper for marking a keyword argument
template<class Key, class Value>
struct KeyWordArgument
{
Key key;
Value value;
};
// Check for KeyWordArgument
template<class T>
struct IsKeyWordArgument : std::false_type {};
template<class Key, class Value>
struct IsKeyWordArgument<KeyWordArgument<Key, Value>> : std::true_type {};
// Count positional (non-keyword) arguments up to pos-1.
// The result is returned as compile time constant using index_constant.
template<std::size_t pos, class... Args>
auto positionalArgumentCount()
{
using ArgTuple = std::tuple<Args...>;
return Dune::unpackIntegerSequence([] (auto... i) {
return Dune::index_constant<(0 + ... + not(Imp::IsKeyWordArgument<std::decay_t<std::tuple_element_t<i, ArgTuple>>>::value))>();
}, std::make_index_sequence<pos>{});
}
// Helper for tagging string as keyword
template<class Char>
struct KeyWord {
std::string key;
template <typename Value>
KeyWordArgument<std::string, Value> operator=(Value&& value) const {
return {key, std::forward<Value>(value)};
}
};
// Parse argument list. The list may contain positional and keyword arguments.
// The parameters to this function are two callback functions, followed by
// the arguments to be parsed. Positional and keyword arguments may be interleaved.
// In any case only positional arguments are counted when determining the position
// of a positional argument.
//
// The first callback will be called with (pos,arg) for each positional argument arg
// with its position pos (as compile time value using Dune::index_constant).
// The second callback will be called with (keyword, value) for each
// keyword argument.
template<class PositionalCallback, class KeyWordCallBack, class... Args>
void parseKeywordArguments(PositionalCallback&& posCallback, KeyWordCallBack&& kwCallBack, Args&&... args)
{
auto processArgument = [&](auto i, auto&& arg)
{
if constexpr (IsKeyWordArgument<std::decay_t<decltype(arg)>>::value)
kwCallBack(arg.key, arg.value);
else
posCallback(positionalArgumentCount<i, Args...>(), std::forward<decltype(arg)>(arg));
};
// Unfold argument pack to process arguments. Doing this via
// std::initializer_list guarantees the proper evaluation order.
// To additionally pass the argument indices as variadic argument pack,
// we use unpackIntegerSequence.
Dune::unpackIntegerSequence([&](auto... i) {
std::initializer_list<int>{ (processArgument(i, args), 0)...};
}, std::index_sequence_for<Args...>());
}
} // namespace Imp
/**
* \brief Create a keyword argument
*
* While the value may be stored as reference or
* value, if an l-value or r-value reference
* is passed, the key is always stored by value.
* As a special case, when passing a
* const char* it will be converted to a std::string.
*
* \param key Identifier of the argument
* \param value Value of the argument
*/
template<class Key, class Value>
auto arg(Key key, Value&& value)
{
using StoredKey = std::conditional_t<std::is_same_v<Key, const char*>, std::string, Key>;
return Imp::KeyWordArgument<StoredKey, Value>{key, std::forward<Value>(value)};
}
namespace Literals {
/**
* \brief User defined literal for defining string-based keyword arguments
*
* This allows to create keyword arguments using "keyword"_a=value.
*/
Imp::KeyWord<char> operator "" _a(const char* keyword, std::size_t) {
return {keyword};
}
}
......@@ -150,46 +259,60 @@ class Callable :
}
/**
* \brief Call this object without arguments
* \brief Call this Reference with positional arguments given as tuple and keyword arguments given as dictionary
*
* Shortcut for callWithArgumentTuple(makeTuple()).
*/
Reference operator() () const
{
return callWithArgumentTuple(makeTuple());
}
/**
* \brief Call this object with one argument
* If the Reference represents a function it's called.
* If the Reference represents an instance its __call__ method is invoked.
* If the Reference represents a class a constructor is invoked.
*
* Shortcut for callWithArgumentTuple(makeTuple(t0)).
*/
template<class T0>
Reference operator() (const T0& t0) const
{
return callWithArgumentTuple(makeTuple(t0));
}
/**
* \brief Call this object with two argument
* Although the method is const it might change the refered object!
* This cannot be avoided since the python api does not know
* about const pointers so we use a mutable pointer internally.
*
* \param args Positional arguments represented as tuple
* \param keywordArgs Keyword arguments represented as dictionary
* \returns The result of the call
*
* Shortcut for callWithArgumentTuple(makeTuple(t0,t1)).
*/
template<class T0, class T1>
Reference operator() (const T0& t0, const T1& t1) const
Reference callWithArgumentTupleAndKeywordArgs(const Reference& args, const Reference& keywordArgs) const
{
return callWithArgumentTuple(makeTuple(t0, t1));
assertPyObject("Callable::callWithArgumentTupleAndKeywordArgs()");
PyObject* result = PyObject_Call(p_, args, keywordArgs);
if (not result)
handlePythonError("Callable::callWithArgumentTupleAndKeywordArgs()", "failed to call object");
return result;
}
/**
* \brief Call this object with three argument
* \brief Call this object with given arguments
*
* Convert given arguments to Python-types and pass them
* as arguments to Python-callable objects. Keyword arguments
* can be passed using either Python::arg("keyword", value)
* or "keyword"_a = value. For the latter you have to import
* the namespace Python::Literals.
*
* Shortcut for callWithArgumentTuple(makeTuple(t0,t1,t2)).
* \returns Result of the call as Python object.
*/
template<class T0, class T1, class T2>
Reference operator() (const T0& t0, const T1& t1, const T2& t2) const
template<class... Args>
Reference operator() (const Args&... args) const
{
return callWithArgumentTuple(makeTuple(t0, t1, t2));
static constexpr std::size_t kwArgCount = (0 + ... + Imp::IsKeyWordArgument<Args>::value);
if constexpr (kwArgCount == 0)
return callWithArgumentTuple(makeTuple(args...));
else
{
Reference kwArgDict = dict();
Reference argTuple = PyTuple_New(sizeof...(Args)-kwArgCount);
Imp::parseKeywordArguments(
[&](auto position, auto&& arg) {
PyTuple_SetItem(argTuple, position, Imp::inc(makeObject(arg)));
}, [&](auto keyword, auto&& value) {
PyDict_SetItem(kwArgDict, makeObject(keyword), makeObject(value));
// We have to use inc() since PyTuple_SetItem STEALS a reference!
}, args...);
return callWithArgumentTupleAndKeywordArgs(argTuple, kwArgDict);
}
}
protected:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment