Add support for positional args with args/kwargs
This commit rewrites the function dispatcher code to support mixing
regular arguments with py::args/py::kwargs arguments. It also
simplifies the argument loader noticeably as it no longer has to worry
about args/kwargs: all of that is now sorted out in the dispatcher,
which now simply appends a tuple/dict if the function takes
py::args/py::kwargs, then passes all the arguments in a vector.
When the argument loader hit a py::args or py::kwargs, it doesn't do
anything special: it just calls the appropriate type_caster just like it
does for any other argument (thus removing the previous special cases
for args/kwargs).
Switching to passing arguments in a single std::vector instead of a pair
of tuples also makes things simpler, both in the dispatch and the
argument_loader: since this argument list is strictly pybind-internal
(i.e. it never goes to Python) we have no particular reason to use a
Python tuple here.
Some (intentional) restrictions:
- you may not bind a function that has args/kwargs somewhere other than
the end (this somewhat matches Python, and keeps the dispatch code a
little cleaner by being able to not worry about where to inject the
args/kwargs in the argument list).
- If you specify an argument both positionally and via a keyword
argument, you get a TypeError alerting you to this (as you do in
Python).
diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst
index 7c97a0f..7ffdfaa 100644
--- a/docs/advanced/functions.rst
+++ b/docs/advanced/functions.rst
@@ -256,16 +256,21 @@
m.def("generic", &generic);
The class ``py::args`` derives from ``py::tuple`` and ``py::kwargs`` derives
-from ``py::dict``. Note that the ``kwargs`` argument is invalid if no keyword
-arguments were actually provided. Please refer to the other examples for
-details on how to iterate over these, and on how to cast their entries into
-C++ objects. A demonstration is also available in
-``tests/test_kwargs_and_defaults.cpp``.
+from ``py::dict``.
-.. warning::
+You may also use just one or the other, and may combine these with other
+arguments as long as the ``py::args`` and ``py::kwargs`` arguments are the last
+arguments accepted by the function.
- Unlike Python, pybind11 does not allow combining normal parameters with the
- ``args`` / ``kwargs`` special parameters.
+Please refer to the other examples for details on how to iterate over these,
+and on how to cast their entries into C++ objects. A demonstration is also
+available in ``tests/test_kwargs_and_defaults.cpp``.
+
+.. note::
+
+ When combining \*args or \*\*kwargs with :ref:`keyword_args` you should
+ *not* include ``py::arg`` tags for the ``py::args`` and ``py::kwargs``
+ arguments.
Default arguments revisited
===========================
diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h
index 41f59e7..c07cd6c 100644
--- a/include/pybind11/attr.h
+++ b/include/pybind11/attr.h
@@ -69,7 +69,7 @@
template <op_id id, op_type ot, typename L = undefined_t, typename R = undefined_t> struct op_;
template <typename... Args> struct init;
template <typename... Args> struct init_alias;
-inline void keep_alive_impl(int Nurse, int Patient, handle args, handle ret);
+inline void keep_alive_impl(int Nurse, int Patient, function_arguments args, handle ret);
/// Internal data structure which holds metadata about a keyword argument
struct argument_record {
@@ -100,7 +100,7 @@
std::vector<argument_record> args;
/// Pointer to lambda function which converts arguments and performs the actual call
- handle (*impl) (function_record *, handle, handle, handle) = nullptr;
+ handle (*impl) (function_record *, function_arguments, handle) = nullptr;
/// Storage for the wrapped function pointer and captured data, if any
void *data[3] = { };
@@ -129,7 +129,7 @@
/// True if this is a method
bool is_method : 1;
- /// Number of arguments
+ /// Number of arguments (including py::args and/or py::kwargs, if present)
uint16_t nargs;
/// Python method object
@@ -233,8 +233,8 @@
/// Default implementation: do nothing
static void init(const T &, function_record *) { }
static void init(const T &, type_record *) { }
- static void precall(handle) { }
- static void postcall(handle, handle) { }
+ static void precall(function_arguments) { }
+ static void postcall(function_arguments, handle) { }
};
/// Process an attribute specifying the function's name
@@ -362,13 +362,13 @@
*/
template <int Nurse, int Patient> struct process_attribute<keep_alive<Nurse, Patient>> : public process_attribute_default<keep_alive<Nurse, Patient>> {
template <int N = Nurse, int P = Patient, enable_if_t<N != 0 && P != 0, int> = 0>
- static void precall(handle args) { keep_alive_impl(Nurse, Patient, args, handle()); }
+ static void precall(function_arguments args) { keep_alive_impl(Nurse, Patient, args, handle()); }
template <int N = Nurse, int P = Patient, enable_if_t<N != 0 && P != 0, int> = 0>
- static void postcall(handle, handle) { }
+ static void postcall(function_arguments, handle) { }
template <int N = Nurse, int P = Patient, enable_if_t<N == 0 || P == 0, int> = 0>
- static void precall(handle) { }
+ static void precall(function_arguments) { }
template <int N = Nurse, int P = Patient, enable_if_t<N == 0 || P == 0, int> = 0>
- static void postcall(handle args, handle ret) { keep_alive_impl(Nurse, Patient, args, ret); }
+ static void postcall(function_arguments args, handle ret) { keep_alive_impl(Nurse, Patient, args, ret); }
};
/// Recursively iterate over variadic template arguments
@@ -381,11 +381,11 @@
int unused[] = { 0, (process_attribute<typename std::decay<Args>::type>::init(args, r), 0) ... };
ignore_unused(unused);
}
- static void precall(handle fn_args) {
+ static void precall(function_arguments fn_args) {
int unused[] = { 0, (process_attribute<typename std::decay<Args>::type>::precall(fn_args), 0) ... };
ignore_unused(unused);
}
- static void postcall(handle fn_args, handle fn_ret) {
+ static void postcall(function_arguments fn_args, handle fn_ret) {
int unused[] = { 0, (process_attribute<typename std::decay<Args>::type>::postcall(fn_args, fn_ret), 0) ... };
ignore_unused(unused);
}
@@ -395,8 +395,8 @@
template <typename... Extra,
size_t named = constexpr_sum(std::is_base_of<arg, Extra>::value...),
size_t self = constexpr_sum(std::is_same<is_method, Extra>::value...)>
-constexpr bool expected_num_args(size_t nargs) {
- return named == 0 || (self + named) == nargs;
+constexpr bool expected_num_args(size_t nargs, bool has_args, bool has_kwargs) {
+ return named == 0 || (self + named + has_args + has_kwargs) == nargs;
}
NAMESPACE_END(detail)
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index c9312de..5f8eaf8 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -1245,22 +1245,42 @@
NAMESPACE_BEGIN(detail)
+// forward declaration
+struct function_record;
+
+// Helper struct to only allow py::args and/or py::kwargs at the end of the function arguments
+template <bool args, bool kwargs, bool args_kwargs_are_last> struct assert_args_kwargs_must_be_last {
+ static constexpr bool has_args = args, has_kwargs = kwargs;
+ static_assert(args_kwargs_are_last, "py::args/py::kwargs are only permitted as the last argument(s) of a function");
+};
+template <typename... T> struct args_kwargs_must_be_last;
+template <typename T1, typename... Tmore> struct args_kwargs_must_be_last<T1, Tmore...>
+ : args_kwargs_must_be_last<Tmore...> {};
+template <typename... T> struct args_kwargs_must_be_last<args, T...>
+ : assert_args_kwargs_must_be_last<true, false, sizeof...(T) == 0> {};
+template <typename... T> struct args_kwargs_must_be_last<kwargs, T...>
+ : assert_args_kwargs_must_be_last<false, true, sizeof...(T) == 0> {};
+template <typename... T> struct args_kwargs_must_be_last<args, kwargs, T...>
+ : assert_args_kwargs_must_be_last<true, true, sizeof...(T) == 0> {};
+template <> struct args_kwargs_must_be_last<> : assert_args_kwargs_must_be_last<false, false, true> {};
+
+using function_arguments = const std::vector<handle> &;
+
/// Helper class which loads arguments for C++ functions called from Python
template <typename... Args>
class argument_loader {
- using itypes = type_list<intrinsic_t<Args>...>;
using indices = make_index_sequence<sizeof...(Args)>;
-public:
- argument_loader() : value() {} // Helps gcc-7 properly initialize value
+ using check_args_kwargs = args_kwargs_must_be_last<intrinsic_t<Args>...>;
- static constexpr auto has_kwargs = std::is_same<itypes, type_list<args, kwargs>>::value;
- static constexpr auto has_args = has_kwargs || std::is_same<itypes, type_list<args>>::value;
+public:
+ static constexpr bool has_kwargs = check_args_kwargs::has_kwargs;
+ static constexpr bool has_args = check_args_kwargs::has_args;
static PYBIND11_DESCR arg_names() { return detail::concat(make_caster<Args>::name()...); }
- bool load_args(handle args, handle kwargs) {
- return load_impl(args, kwargs, itypes{});
+ bool load_args(function_arguments args) {
+ return load_impl_sequence(args, indices{});
}
template <typename Return, typename Func>
@@ -1275,26 +1295,12 @@
}
private:
- bool load_impl(handle args_, handle, type_list<args>) {
- std::get<0>(value).load(args_, true);
- return true;
- }
- bool load_impl(handle args_, handle kwargs_, type_list<args, kwargs>) {
- std::get<0>(value).load(args_, true);
- std::get<1>(value).load(kwargs_, true);
- return true;
- }
-
- bool load_impl(handle args, handle, ... /* anything else */) {
- return load_impl_sequence(args, indices{});
- }
-
- static bool load_impl_sequence(handle, index_sequence<>) { return true; }
+ static bool load_impl_sequence(function_arguments, index_sequence<>) { return true; }
template <size_t... Is>
- bool load_impl_sequence(handle src, index_sequence<Is...>) {
- for (bool r : {std::get<Is>(value).load(PyTuple_GET_ITEM(src.ptr(), Is), true)...})
+ bool load_impl_sequence(function_arguments args, index_sequence<Is...>) {
+ for (bool r : {std::get<Is>(value).load(args[Is], true)...})
if (!r)
return false;
return true;
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index 420e18e..dc21caf 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -82,8 +82,6 @@
/// Special internal constructor for functors, lambda functions, etc.
template <typename Func, typename Return, typename... Args, typename... Extra /*,*/ PYBIND11_NOEXCEPT_TPL_ARG>
void initialize(Func &&f, Return (*)(Args...) PYBIND11_NOEXCEPT_SPECIFIER, const Extra&... extra) {
- static_assert(detail::expected_num_args<Extra...>(sizeof...(Args)),
- "The number of named arguments does not match the function signature");
struct capture { typename std::remove_reference<Func>::type f; };
@@ -116,12 +114,15 @@
detail::conditional_t<std::is_void<Return>::value, detail::void_type, Return>
>;
+ static_assert(detail::expected_num_args<Extra...>(sizeof...(Args), cast_in::has_args, cast_in::has_kwargs),
+ "The number of named arguments does not match the function signature");
+
/* Dispatch code which converts function arguments and performs the actual function call */
- rec->impl = [](detail::function_record *rec, handle args, handle kwargs, handle parent) -> handle {
+ rec->impl = [](detail::function_record *rec, detail::function_arguments args, handle parent) -> handle {
cast_in args_converter;
/* Try to cast the function arguments into the C++ domain */
- if (!args_converter.load_args(args, kwargs))
+ if (!args_converter.load_args(args))
return PYBIND11_TRY_NEXT_OVERLOAD;
/* Invoke call policy pre-call hook */
@@ -379,66 +380,144 @@
}
/// Main dispatch logic for calls to functions bound using pybind11
- static PyObject *dispatcher(PyObject *self, PyObject *args, PyObject *kwargs) {
+ static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) {
/* Iterator over the list of potentially admissible overloads */
detail::function_record *overloads = (detail::function_record *) PyCapsule_GetPointer(self, nullptr),
*it = overloads;
/* Need to know how many arguments + keyword arguments there are to pick the right overload */
- size_t nargs = (size_t) PyTuple_GET_SIZE(args),
- nkwargs = kwargs ? (size_t) PyDict_Size(kwargs) : 0;
+ const size_t n_args_in = (size_t) PyTuple_GET_SIZE(args_in);
- handle parent = nargs > 0 ? PyTuple_GET_ITEM(args, 0) : nullptr,
+ handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr,
result = PYBIND11_TRY_NEXT_OVERLOAD;
try {
for (; it != nullptr; it = it->next) {
- auto args_ = reinterpret_borrow<tuple>(args);
- size_t kwargs_consumed = 0;
-
/* For each overload:
- 1. If the required list of arguments is longer than the
- actually provided amount, create a copy of the argument
- list and fill in any available keyword/default arguments.
- 2. Ensure that all keyword arguments were "consumed"
- 3. Call the function call dispatcher (function_record::impl)
+ 1. Copy all positional arguments we were given, also checking to make sure that
+ named positional arguments weren't *also* specified via kwarg.
+ 2. If we weren't given enough, try to make up the ommitted ones by checking
+ whether they were provided by a kwarg matching the `py::arg("name")` name. If
+ so, use it (and remove it from kwargs; if not, see if the function binding
+ provided a default that we can use.
+ 3. Ensure that either all keyword arguments were "consumed", or that the function
+ takes a kwargs argument to accept unconsumed kwargs.
+ 4. Any positional arguments still left get put into a tuple (for args), and any
+ leftover kwargs get put into a dict.
+ 5. Pack everything into a vector; if we have py::args or py::kwargs, they are an
+ extra tuple or dict at the end of the positional arguments.
+ 6. Call the function call dispatcher (function_record::impl)
+
+ If one of these fail, move on to the next overload and keep trying until we get a
+ result other than PYBIND11_TRY_NEXT_OVERLOAD.
*/
- size_t nargs_ = nargs;
- if (nargs < it->args.size()) {
- nargs_ = it->args.size();
- args_ = tuple(nargs_);
- for (size_t i = 0; i < nargs; ++i) {
- handle item = PyTuple_GET_ITEM(args, i);
- PyTuple_SET_ITEM(args_.ptr(), i, item.inc_ref().ptr());
- }
- int arg_ctr = 0;
- for (auto const &it2 : it->args) {
- int index = arg_ctr++;
- if (PyTuple_GET_ITEM(args_.ptr(), index))
- continue;
+ size_t pos_args = it->nargs; // Number of positional arguments that we need
+ if (it->has_args) --pos_args; // (but don't count py::args
+ if (it->has_kwargs) --pos_args; // or py::kwargs)
- handle value;
- if (kwargs)
- value = PyDict_GetItemString(kwargs, it2.name);
+ if (!it->has_args && n_args_in > pos_args)
+ continue; // Too many arguments for this overload
+ if (n_args_in < pos_args && it->args.size() < pos_args)
+ continue; // Not enough arguments given, and not enough defaults to fill in the blanks
+
+ std::vector<handle> pass_args;
+ pass_args.reserve(it->nargs);
+
+ size_t args_to_copy = std::min(pos_args, n_args_in);
+ size_t args_copied = 0;
+
+ // 1. Copy any position arguments given.
+ for (; args_copied < args_to_copy; ++args_copied) {
+ // If we find a given positional argument that also has a named kwargs argument,
+ // raise a TypeError like Python does. (We could also continue with the next
+ // overload, but this seems highly likely to be a caller mistake rather than a
+ // legitimate overload).
+ if (kwargs_in && args_copied < it->args.size()) {
+ handle value = PyDict_GetItemString(kwargs_in, it->args[args_copied].name);
if (value)
- kwargs_consumed++;
- else if (it2.value)
- value = it2.value;
-
- if (value) {
- PyTuple_SET_ITEM(args_.ptr(), index, value.inc_ref().ptr());
- } else {
- kwargs_consumed = (size_t) -1; /* definite failure */
- break;
- }
+ throw type_error(std::string(it->name) + "(): got multiple values for argument '" +
+ std::string(it->args[args_copied].name) + "'");
}
+
+ pass_args.push_back(PyTuple_GET_ITEM(args_in, args_copied));
}
+ // We'll need to copy this if we steal some kwargs for defaults
+ dict kwargs = reinterpret_borrow<dict>(kwargs_in);
+
+ // 2. Check kwargs and, failing that, defaults that may help complete the list
+ if (args_copied < pos_args) {
+ bool copied_kwargs = false;
+
+ for (; args_copied < pos_args; ++args_copied) {
+ const auto &arg = it->args[args_copied];
+
+ handle value;
+ if (kwargs_in)
+ value = PyDict_GetItemString(kwargs.ptr(), arg.name);
+
+ if (value) {
+ // Consume a kwargs value
+ if (!copied_kwargs) {
+ kwargs = reinterpret_steal<dict>(PyDict_Copy(kwargs.ptr()));
+ copied_kwargs = true;
+ }
+ PyDict_DelItemString(kwargs.ptr(), arg.name);
+ }
+ else if (arg.value) {
+ value = arg.value;
+ }
+
+ if (value)
+ pass_args.push_back(value);
+ else
+ break;
+ }
+
+ if (args_copied < pos_args)
+ continue; // Not enough arguments, defaults, or kwargs to fill the positional arguments
+ }
+
+ // 3. Check everything was consumed (unless we have a kwargs arg)
+ if (kwargs && kwargs.size() > 0 && !it->has_kwargs)
+ continue; // Unconsumed kwargs, but no py::kwargs argument to accept them
+
+ // 4a. If we have a py::args argument, create a new tuple with leftovers
+ tuple extra_args;
+ if (it->has_args) {
+ if (args_to_copy == 0) {
+ // We didn't copy out any position arguments from the args_in tuple, so we
+ // can reuse it directly without copying:
+ extra_args = reinterpret_borrow<tuple>(args_in);
+ }
+ else if (args_copied >= n_args_in) {
+ extra_args = tuple(0);
+ }
+ else {
+ size_t args_size = n_args_in - args_copied;
+ extra_args = tuple(args_size);
+ for (size_t i = 0; i < args_size; ++i) {
+ handle item = PyTuple_GET_ITEM(args_in, args_copied + i);
+ extra_args[i] = item.inc_ref().ptr();
+ }
+ }
+ pass_args.push_back(extra_args);
+ }
+
+ // 4b. If we have a py::kwargs, pass on any remaining kwargs
+ if (it->has_kwargs) {
+ if (!kwargs.ptr())
+ kwargs = dict(); // If we didn't get one, send an empty one
+ pass_args.push_back(kwargs);
+ }
+
+ // 5. Put everything in a big tuple. Not technically step 5, we've been building it
+ // in `pass_args` all along.
+
+ // 6. Call the function.
try {
- if ((kwargs_consumed == nkwargs || it->has_kwargs) &&
- (nargs_ == it->nargs || it->has_args))
- result = it->impl(it, args_, kwargs, parent);
+ result = it->impl(it, pass_args, parent);
} catch (reference_cast_error &) {
result = PYBIND11_TRY_NEXT_OVERLOAD;
}
@@ -512,7 +591,7 @@
msg += "\n";
}
msg += "\nInvoked with: ";
- auto args_ = reinterpret_borrow<tuple>(args);
+ auto args_ = reinterpret_borrow<tuple>(args_in);
for (size_t ti = overloads->is_constructor ? 1 : 0; ti < args_.size(); ++ti) {
msg += pybind11::repr(args_[ti]);
if ((ti + 1) != args_.size() )
@@ -530,9 +609,8 @@
if (overloads->is_constructor) {
/* When a constructor ran successfully, the corresponding
holder type (e.g. std::unique_ptr) must still be initialized. */
- PyObject *inst = PyTuple_GET_ITEM(args, 0);
- auto tinfo = detail::get_type_info(Py_TYPE(inst));
- tinfo->init_holder(inst, nullptr);
+ auto tinfo = detail::get_type_info(Py_TYPE(parent.ptr()));
+ tinfo->init_holder(parent.ptr(), nullptr);
}
return result.ptr();
}
@@ -1416,11 +1494,11 @@
(void) wr.release();
}
-PYBIND11_NOINLINE inline void keep_alive_impl(int Nurse, int Patient, handle args, handle ret) {
- handle nurse (Nurse > 0 ? PyTuple_GetItem(args.ptr(), Nurse - 1) : ret.ptr());
- handle patient(Patient > 0 ? PyTuple_GetItem(args.ptr(), Patient - 1) : ret.ptr());
-
- keep_alive_impl(nurse, patient);
+PYBIND11_NOINLINE inline void keep_alive_impl(int Nurse, int Patient, function_arguments args, handle ret) {
+ keep_alive_impl(
+ Nurse == 0 ? ret : Nurse > 0 && (size_t) Nurse <= args.size() ? args[Nurse - 1] : handle(),
+ Patient == 0 ? ret : Patient > 0 && (size_t) Patient <= args.size() ? args[Patient - 1] : handle()
+ );
}
template <typename Iterator, typename Sentinel, bool KeyIterator, return_value_policy Policy>
diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index 0090e66..80b802f 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -149,14 +149,14 @@
preferable to use the `object` class which derives from `handle` and calls
this function automatically. Returns a reference to itself.
\endrst */
- const handle& inc_ref() const { Py_XINCREF(m_ptr); return *this; }
+ const handle& inc_ref() const & { Py_XINCREF(m_ptr); return *this; }
/** \rst
Manually decrease the reference count of the Python object. Usually, it is
preferable to use the `object` class which derives from `handle` and calls
this function automatically. Returns a reference to itself.
\endrst */
- const handle& dec_ref() const { Py_XDECREF(m_ptr); return *this; }
+ const handle& dec_ref() const & { Py_XDECREF(m_ptr); return *this; }
/** \rst
Attempt to cast the Python object into the given C++ type. A `cast_error`
diff --git a/tests/test_kwargs_and_defaults.cpp b/tests/test_kwargs_and_defaults.cpp
index 24fc0cd..3180123 100644
--- a/tests/test_kwargs_and_defaults.cpp
+++ b/tests/test_kwargs_and_defaults.cpp
@@ -28,6 +28,27 @@
return py::make_tuple(args, kwargs);
}
+py::tuple mixed_plus_args(int i, double j, py::args args) {
+ return py::make_tuple(i, j, args);
+}
+
+py::tuple mixed_plus_kwargs(int i, double j, py::kwargs kwargs) {
+ return py::make_tuple(i, j, kwargs);
+}
+
+py::tuple mixed_plus_args_kwargs(int i, double j, py::args args, py::kwargs kwargs) {
+ return py::make_tuple(i, j, args, kwargs);
+}
+
+// pybind11 won't allow these to be bound: args and kwargs, if present, must be at the end.
+void bad_args1(py::args, int) {}
+void bad_args2(py::kwargs, int) {}
+void bad_args3(py::kwargs, py::args) {}
+void bad_args4(py::args, int, py::kwargs) {}
+void bad_args5(py::args, py::kwargs, int) {}
+void bad_args6(py::args, py::args) {}
+void bad_args7(py::kwargs, py::kwargs) {}
+
struct KWClass {
void foo(int, float) {}
};
@@ -53,4 +74,20 @@
py::class_<KWClass>(m, "KWClass")
.def("foo0", &KWClass::foo)
.def("foo1", &KWClass::foo, "x"_a, "y"_a);
+
+ m.def("mixed_plus_args", &mixed_plus_args);
+ m.def("mixed_plus_kwargs", &mixed_plus_kwargs);
+ m.def("mixed_plus_args_kwargs", &mixed_plus_args_kwargs);
+
+ m.def("mixed_plus_args_kwargs_defaults", &mixed_plus_args_kwargs,
+ py::arg("i") = 1, py::arg("j") = 3.14159);
+
+ // Uncomment these to test that the static_assert is indeed working:
+// m.def("bad_args1", &bad_args1);
+// m.def("bad_args2", &bad_args2);
+// m.def("bad_args3", &bad_args3);
+// m.def("bad_args4", &bad_args4);
+// m.def("bad_args5", &bad_args5);
+// m.def("bad_args6", &bad_args6);
+// m.def("bad_args7", &bad_args7);
});
diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py
index 852d03c..d1777a4 100644
--- a/tests/test_kwargs_and_defaults.py
+++ b/tests/test_kwargs_and_defaults.py
@@ -55,3 +55,52 @@
args = 'a1', 'a2'
kwargs = dict(arg3='a3', arg4=4)
assert args_kwargs_function(*args, **kwargs) == (args, kwargs)
+
+
+def test_mixed_args_and_kwargs(msg):
+ from pybind11_tests import (mixed_plus_args, mixed_plus_kwargs, mixed_plus_args_kwargs,
+ mixed_plus_args_kwargs_defaults)
+ mpa = mixed_plus_args
+ mpk = mixed_plus_kwargs
+ mpak = mixed_plus_args_kwargs
+ mpakd = mixed_plus_args_kwargs_defaults
+
+ assert mpa(1, 2.5, 4, 99.5, None) == (1, 2.5, (4, 99.5, None))
+ assert mpa(1, 2.5) == (1, 2.5, ())
+ with pytest.raises(TypeError) as excinfo:
+ assert mpa(1)
+ assert msg(excinfo.value) == """
+ mixed_plus_args(): incompatible function arguments. The following argument types are supported:
+ 1. (arg0: int, arg1: float, *args) -> tuple
+
+ Invoked with: 1
+ """ # noqa: E501
+ with pytest.raises(TypeError) as excinfo:
+ assert mpa()
+ assert msg(excinfo.value) == """
+ mixed_plus_args(): incompatible function arguments. The following argument types are supported:
+ 1. (arg0: int, arg1: float, *args) -> tuple
+
+ Invoked with:
+ """ # noqa: E501
+
+ assert mpk(-2, 3.5, pi=3.14159, e=2.71828) == (-2, 3.5, {'e': 2.71828, 'pi': 3.14159})
+ assert mpak(7, 7.7, 7.77, 7.777, 7.7777, minusseven=-7) == (
+ 7, 7.7, (7.77, 7.777, 7.7777), {'minusseven': -7})
+ assert mpakd() == (1, 3.14159, (), {})
+ assert mpakd(3) == (3, 3.14159, (), {})
+ assert mpakd(j=2.71828) == (1, 2.71828, (), {})
+ assert mpakd(k=42) == (1, 3.14159, (), {'k': 42})
+ assert mpakd(1, 1, 2, 3, 5, 8, then=13, followedby=21) == (
+ 1, 1, (2, 3, 5, 8), {'then': 13, 'followedby': 21})
+ # Arguments specified both positionally and via kwargs is an error:
+ with pytest.raises(TypeError) as excinfo:
+ assert mpakd(1, i=1)
+ assert msg(excinfo.value) == """
+ mixed_plus_args_kwargs_defaults(): got multiple values for argument 'i'
+ """
+ with pytest.raises(TypeError) as excinfo:
+ assert mpakd(1, 2, j=1)
+ assert msg(excinfo.value) == """
+ mixed_plus_args_kwargs_defaults(): got multiple values for argument 'j'
+ """