From 709675a7aaa3e8f60ab6b9a03e93daa14f3b2372 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 3 Feb 2017 20:16:14 -0500 Subject: [PATCH 1/2] Made arithmetic and complex casters respect `convert` Arithmetic and complex casters now only do a converting cast when `convert=true`; previously they would convert always (e.g. when passing an int to a float-accepting function, or a float to complex-accepting function). --- include/pybind11/cast.h | 20 ++++++++++++-------- include/pybind11/complex.h | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index af85f9c50..4dc3082fc 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -473,18 +473,22 @@ public: template struct type_caster::value>> { - typedef typename std::conditional::type _py_type_0; - typedef typename std::conditional::value, _py_type_0, typename std::make_unsigned<_py_type_0>::type>::type _py_type_1; - typedef typename std::conditional::value, double, _py_type_1>::type py_type; + using _py_type_0 = conditional_t; + using _py_type_1 = conditional_t::value, _py_type_0, typename std::make_unsigned<_py_type_0>::type>; + using py_type = conditional_t::value, double, _py_type_1>; public: - bool load(handle src, bool) { + bool load(handle src, bool convert) { py_type py_value; - if (!src) { + if (!src) return false; - } if (std::is_floating_point::value) { - py_value = (py_type) PyFloat_AsDouble(src.ptr()); + + if (std::is_floating_point::value) { + if (convert || PyFloat_Check(src.ptr())) + py_value = (py_type) PyFloat_AsDouble(src.ptr()); + else + return false; } else if (sizeof(T) <= sizeof(long)) { if (PyFloat_Check(src.ptr())) return false; @@ -511,7 +515,7 @@ public: bool type_error = PyErr_ExceptionMatches(PyExc_TypeError); #endif PyErr_Clear(); - if (type_error && PyNumber_Check(src.ptr())) { + if (type_error && convert && PyNumber_Check(src.ptr())) { auto tmp = reinterpret_borrow(std::is_floating_point::value ? PyNumber_Float(src.ptr()) : PyNumber_Long(src.ptr())); diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index a6b7b23cd..945ca0710 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -28,9 +28,11 @@ template struct is_fmt_numeric> { template class type_caster> { public: - bool load(handle src, bool) { + bool load(handle src, bool convert) { if (!src) return false; + if (!convert && !PyComplex_Check(src.ptr())) + return false; Py_complex result = PyComplex_AsCComplex(src.ptr()); if (result.real == -1.0 && PyErr_Occurred()) { PyErr_Clear(); From abc29cad020bb035f698fdc3e5d33c5c551ed772 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 23 Jan 2017 03:50:00 -0500 Subject: [PATCH 2/2] Add support for non-converting arguments This adds support for controlling the `convert` flag of arguments through the py::arg annotation. This then allows arguments to be flagged as non-converting, which the type_caster is able to use to request different behaviour. Currently, AFAICS `convert` is only used for type converters of regular pybind11-registered types; all of the other core type_casters ignore it. We can, however, repurpose it to control internal conversion of converters like Eigen and `array`: most usefully to give callers a way to disable the conversion that would otherwise occur when a `Eigen::Ref` argument is passed a numpy array that requires conversion (either because it has an incompatible stride or the wrong dtype). Specifying a noconvert looks like one of these: m.def("f1", &f, "a"_a.noconvert() = "default"); // Named, default, noconvert m.def("f2", &f, "a"_a.noconvert()); // Named, no default, no converting m.def("f3", &f, py::arg().noconvert()); // Unnamed, no default, no converting (The last part--being able to declare a py::arg without a name--is new: previous py::arg() only accepted named keyword arguments). Such an non-convert argument is then passed `convert = false` by the type caster when loading the argument. Whether this has an effect is up to the type caster itself, but as mentioned above, this would be extremely helpful for the Eigen support to give a nicer way to specify a "no-copy" mode than the custom wrapper in the current PR, and moreover isn't an Eigen-specific hack. --- docs/advanced/classes.rst | 2 + docs/advanced/functions.rst | 54 ++++++++++++++++++ include/pybind11/attr.h | 36 +++++------- include/pybind11/cast.h | 82 ++++++++++++++++++++++----- include/pybind11/pybind11.h | 38 ++++++++----- tests/test_methods_and_attributes.cpp | 57 +++++++++++++++++++ tests/test_methods_and_attributes.py | 44 ++++++++++++++ 7 files changed, 261 insertions(+), 52 deletions(-) diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 9d17364b3..e0463b451 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -388,6 +388,8 @@ crucial that instances are deallocated on the C++ side to avoid memory leaks. py::class_>(m, "MyClass") .def(py::init<>()) +.. _implicit_conversions: + Implicit conversions ==================== diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index 7ffdfaa80..e67d7bc37 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -318,3 +318,57 @@ like so: py::class_("MyClass") .def("myFunction", py::arg("arg") = (SomeType *) nullptr); + +Non-converting arguments +======================== + +Certain argument types may support conversion from one type to another. Some +examples of conversions are: + +* :ref:`implicit_conversions` declared using ``py::implicitly_convertible()`` +* Calling a method accepting a double with an integer argument +* Calling a ``std::complex`` argument with a non-complex python type + (for example, with a float). (Requires the optional ``pybind11/complex.h`` + header). +* Calling a function taking an Eigen matrix reference with a numpy array of the + wrong type or of an incompatible data layout. (Requires the optional + ``pybind11/eigen.h`` header). + +This behaviour is sometimes undesirable: the binding code may prefer to raise +an error rather than convert the argument. This behaviour can be obtained +through ``py::arg`` by calling the ``.noconvert()`` method of the ``py::arg`` +object, such as: + +.. code-block:: cpp + + m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert()); + m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f")); + +Attempting the call the second function (the one without ``.noconvert()``) with +an integer will succeed, but attempting to call the ``.noconvert()`` version +will fail with a ``TypeError``: + +.. code-block:: pycon + + >>> floats_preferred(4) + 2.0 + >>> floats_only(4) + Traceback (most recent call last): + File "", line 1, in + TypeError: floats_only(): incompatible function arguments. The following argument types are supported: + 1. (f: float) -> float + + Invoked with: 4 + +You may, of course, combine this with the :var:`_a` shorthand notation (see +:ref:`keyword_args`) and/or :ref:`default_args`. It is also permitted to omit +the argument name by using the ``py::arg()`` constructor without an argument +name, i.e. by specifying ``py::arg().noconvert()``. + +.. note:: + + When specifying ``py::arg`` options it is necessary to provide the same + number of options as the bound function has arguments. Thus if you want to + enable no-convert behaviour for just one of several arguments, you will + need to specify a ``py::arg()`` annotation for each argument with the + no-convert argument modified to ``py::arg().noconvert()``. diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index e9468b856..129db0548 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -69,7 +69,6 @@ struct undefined_t; template struct op_; template struct init; template struct init_alias; -struct function_call; inline void keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret); /// Internal data structure which holds metadata about a keyword argument @@ -77,9 +76,10 @@ struct argument_record { const char *name; ///< Argument name const char *descr; ///< Human-readable version of the argument value handle value; ///< Associated Python object + bool convert : 1; ///< True if the argument is allowed to convert when loading - argument_record(const char *name, const char *descr, handle value) - : name(name), descr(descr), value(value) { } + argument_record(const char *name, const char *descr, handle value, bool convert) + : name(name), descr(descr), value(value), convert(convert) { } }; /// Internal data structure which holds metadata about a bound function (signature, overloads, etc.) @@ -131,7 +131,7 @@ struct function_record { bool is_method : 1; /// Number of arguments (including py::args and/or py::kwargs, if present) - uint16_t nargs; + std::uint16_t nargs; /// Python method object PyMethodDef *def = nullptr; @@ -222,21 +222,11 @@ struct type_record { } }; -/// Internal data associated with a single function call -struct function_call { - function_call(function_record &f, handle p) : func(f), parent(p) { - args.reserve(f.nargs); - } - - /// The function data: - const function_record &func; - - /// Arguments passed to the function: - std::vector args; - - /// The parent, if any - handle parent; -}; +inline function_call::function_call(function_record &f, handle p) : + func(f), parent(p) { + args.reserve(f.nargs); + args_convert.reserve(f.nargs); +} /** * Partial template specializations to process custom attributes provided to @@ -300,8 +290,8 @@ template <> struct process_attribute : process_attribute_default struct process_attribute : process_attribute_default { static void init(const arg &a, function_record *r) { if (r->is_method && r->args.empty()) - r->args.emplace_back("self", nullptr, handle()); - r->args.emplace_back(a.name, nullptr, handle()); + r->args.emplace_back("self", nullptr, handle(), true /*convert*/); + r->args.emplace_back(a.name, nullptr, handle(), !a.flag_noconvert); } }; @@ -309,7 +299,7 @@ template <> struct process_attribute : process_attribute_default { template <> struct process_attribute : process_attribute_default { static void init(const arg_v &a, function_record *r) { if (r->is_method && r->args.empty()) - r->args.emplace_back("self", nullptr, handle()); + r->args.emplace_back("self", nullptr /*descr*/, handle() /*parent*/, true /*convert*/); if (!a.value) { #if !defined(NDEBUG) @@ -330,7 +320,7 @@ template <> struct process_attribute : process_attribute_default { "Compile in debug mode for more information."); #endif } - r->args.emplace_back(a.name, a.descr, a.value.inc_ref()); + r->args.emplace_back(a.name, a.descr, a.value.inc_ref(), !a.flag_noconvert); } }; diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4dc3082fc..67ab2169b 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1202,22 +1202,26 @@ template arg_v operator=(T &&value) const; + /// Indicate that the type should not be converted in the type caster + arg &noconvert(bool flag = true) { flag_noconvert = flag; return *this; } - const char *name; + const char *name; ///< If non-null, this is a named kwargs argument + bool flag_noconvert : 1; ///< If set, do not allow conversion (requires a supporting type caster!) }; /// \ingroup annotations -/// Annotation for keyword arguments with values +/// Annotation for arguments with values struct arg_v : arg { +private: template - arg_v(const char *name, T &&x, const char *descr = nullptr) - : arg(name), + arg_v(arg &&base, T &&x, const char *descr = nullptr) + : arg(base), value(reinterpret_steal( detail::make_caster::cast(x, return_value_policy::automatic, {}) )), @@ -1227,15 +1231,32 @@ struct arg_v : arg { #endif { } +public: + /// Direct construction with name, default, and description + template + arg_v(const char *name, T &&x, const char *descr = nullptr) + : arg_v(arg(name), std::forward(x), descr) { } + + /// Called internally when invoking `py::arg("a") = value` + template + arg_v(const arg &base, T &&x, const char *descr = nullptr) + : arg_v(arg(base), std::forward(x), descr) { } + + /// Same as `arg::noconvert()`, but returns *this as arg_v&, not arg& + arg_v &noconvert(bool flag = true) { arg::noconvert(flag); return *this; } + + /// The default value object value; + /// The (optional) description of the default value const char *descr; #if !defined(NDEBUG) + /// The C++ type name of the default value (only available when compiled in debug mode) std::string type; #endif }; template -arg_v arg::operator=(T &&value) const { return {name, std::forward(value)}; } +arg_v arg::operator=(T &&value) const { return {std::move(*this), std::forward(value)}; } /// Alias for backward compatibility -- to be removed in version 2.0 template using arg_t = arg_v; @@ -1252,11 +1273,28 @@ NAMESPACE_BEGIN(detail) // forward declaration struct function_record; +/// Internal data associated with a single function call +struct function_call { + function_call(function_record &f, handle p); // Implementation in attr.h + + /// The function data: + const function_record &func; + + /// Arguments passed to the function: + std::vector args; + + /// The `convert` value the arguments should be loaded with + std::vector args_convert; + + /// The parent, if any + handle parent; +}; + + /// Helper class which loads arguments for C++ functions called from Python template class argument_loader { using indices = make_index_sequence; - using function_arguments = const std::vector &; template using argument_is_args = std::is_same, args>; template using argument_is_kwargs = std::is_same, kwargs>; @@ -1274,8 +1312,8 @@ public: static PYBIND11_DESCR arg_names() { return detail::concat(make_caster::name()...); } - bool load_args(function_arguments args) { - return load_impl_sequence(args, indices{}); + bool load_args(function_call &call) { + return load_impl_sequence(call, indices{}); } template @@ -1291,11 +1329,11 @@ public: private: - static bool load_impl_sequence(function_arguments, index_sequence<>) { return true; } + static bool load_impl_sequence(function_call &, index_sequence<>) { return true; } template - bool load_impl_sequence(function_arguments args, index_sequence) { - for (bool r : {std::get(value).load(args[Is], true)...}) + bool load_impl_sequence(function_call &call, index_sequence) { + for (bool r : {std::get(value).load(call.args[Is], call.args_convert[Is])...}) if (!r) return false; return true; @@ -1384,6 +1422,13 @@ private: } void process(list &/*args_list*/, arg_v a) { + if (!a.name) +#if defined(NDEBUG) + nameless_argument_error(); +#else + nameless_argument_error(a.type); +#endif + if (m_kwargs.contains(a.name)) { #if defined(NDEBUG) multiple_values_error(); @@ -1416,6 +1461,15 @@ private: } } + [[noreturn]] static void nameless_argument_error() { + throw type_error("Got kwargs without a name; only named arguments " + "may be passed via py::arg() to a python function call. " + "(compile in debug mode for details)"); + } + [[noreturn]] static void nameless_argument_error(std::string type) { + throw type_error("Got kwargs without a name of type '" + type + "'; only named " + "arguments may be passed via py::arg() to a python function call. "); + } [[noreturn]] static void multiple_values_error() { throw type_error("Got multiple values for keyword argument " "(compile in debug mode for details)"); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 14dc56ca6..fc283704e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -122,7 +122,7 @@ protected: cast_in args_converter; /* Try to cast the function arguments into the C++ domain */ - if (!args_converter.load_args(call.args)) + if (!args_converter.load_args(call)) return PYBIND11_TRY_NEXT_OVERLOAD; /* Invoke call policy pre-call hook */ @@ -198,7 +198,7 @@ protected: if (c == '{') { // Write arg name for everything except *args, **kwargs and return type. if (type_depth == 0 && text[char_index] != '*' && arg_index < args) { - if (!rec->args.empty()) { + if (!rec->args.empty() && rec->args[arg_index].name) { signature += rec->args[arg_index].name; } else if (arg_index == 0 && rec->is_method) { signature += "self"; @@ -257,7 +257,7 @@ protected: rec->signature = strdup(signature.c_str()); rec->args.shrink_to_fit(); rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__"); - rec->nargs = (uint16_t) args; + rec->nargs = (std::uint16_t) args; #if PY_MAJOR_VERSION < 3 if (rec->sibling && PyMethod_Check(rec->sibling.ptr())) @@ -392,8 +392,10 @@ protected: 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) { + /* For each overload: 1. Copy all positional arguments we were given, also checking to make sure that named positional arguments weren't *also* specified via kwarg. @@ -435,14 +437,15 @@ protected: // 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 (kwargs_in && args_copied < func.args.size() && func.args[args_copied].name) { + handle value = PyDict_GetItemString(kwargs_in, func.args[args_copied].name); if (value) - throw type_error(std::string(it->name) + "(): got multiple values for argument '" + - std::string(it->args[args_copied].name) + "'"); + throw type_error(std::string(func.name) + "(): got multiple values for argument '" + + std::string(func.args[args_copied].name) + "'"); } call.args.push_back(PyTuple_GET_ITEM(args_in, args_copied)); + call.args_convert.push_back(args_copied < func.args.size() ? func.args[args_copied].convert : true); } // We'll need to copy this if we steal some kwargs for defaults @@ -453,10 +456,10 @@ protected: bool copied_kwargs = false; for (; args_copied < pos_args; ++args_copied) { - const auto &arg = it->args[args_copied]; + const auto &arg = func.args[args_copied]; handle value; - if (kwargs_in) + if (kwargs_in && arg.name) value = PyDict_GetItemString(kwargs.ptr(), arg.name); if (value) { @@ -470,8 +473,10 @@ protected: value = arg.value; } - if (value) + if (value) { call.args.push_back(value); + call.args_convert.push_back(arg.convert); + } else break; } @@ -481,12 +486,12 @@ protected: } // 3. Check everything was consumed (unless we have a kwargs arg) - if (kwargs && kwargs.size() > 0 && !it->has_kwargs) + if (kwargs && kwargs.size() > 0 && !func.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 (func.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: @@ -502,31 +507,34 @@ protected: } } call.args.push_back(extra_args); + call.args_convert.push_back(false); } // 4b. If we have a py::kwargs, pass on any remaining kwargs - if (it->has_kwargs) { + if (func.has_kwargs) { if (!kwargs.ptr()) kwargs = dict(); // If we didn't get one, send an empty one call.args.push_back(kwargs); + call.args_convert.push_back(false); } // 5. Put everything in a vector. Not technically step 5, we've been building it // in `call.args` all along. #if !defined(NDEBUG) - if (call.args.size() != call.func.nargs) + if (call.args.size() != func.nargs || call.args_convert.size() != func.nargs) pybind11_fail("Internal error: function call dispatcher inserted wrong number of arguments!"); #endif // 6. Call the function. try { - result = it->impl(call); + result = func.impl(call); } catch (reference_cast_error &) { result = PYBIND11_TRY_NEXT_OVERLOAD; } if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD) break; + } } catch (error_already_set &e) { e.restore(); diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index f7d6d6855..6f5e5ef2f 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -97,6 +97,42 @@ public: class CppDerivedDynamicClass : public DynamicClass { }; +// py::arg/py::arg_v testing: these arguments just record their argument when invoked +class ArgInspector1 { public: std::string arg = "(default arg inspector 1)"; }; +class ArgInspector2 { public: std::string arg = "(default arg inspector 2)"; }; +namespace pybind11 { namespace detail { +template <> struct type_caster { +public: + PYBIND11_TYPE_CASTER(ArgInspector1, _("ArgInspector1")); + + bool load(handle src, bool convert) { + value.arg = "loading ArgInspector1 argument " + + std::string(convert ? "WITH" : "WITHOUT") + " conversion allowed. " + "Argument value = " + (std::string) str(src); + return true; + } + + static handle cast(const ArgInspector1 &src, return_value_policy, handle) { + return str(src.arg).release(); + } +}; +template <> struct type_caster { +public: + PYBIND11_TYPE_CASTER(ArgInspector2, _("ArgInspector2")); + + bool load(handle src, bool convert) { + value.arg = "loading ArgInspector2 argument " + + std::string(convert ? "WITH" : "WITHOUT") + " conversion allowed. " + "Argument value = " + (std::string) str(src); + return true; + } + + static handle cast(const ArgInspector2 &src, return_value_policy, handle) { + return str(src.arg).release(); + } +}; +}} + test_initializer methods_and_attributes([](py::module &m) { py::class_(m, "ExampleMandA") .def(py::init<>()) @@ -183,4 +219,25 @@ test_initializer methods_and_attributes([](py::module &m) { py::class_(m, "CppDerivedDynamicClass") .def(py::init()); #endif + + class ArgInspector { + public: + ArgInspector1 f(ArgInspector1 a) { return a; } + std::string g(ArgInspector1 a, const ArgInspector1 &b, int c, ArgInspector2 *d) { + return a.arg + "\n" + b.arg + "\n" + std::to_string(c) + "\n" + d->arg; + } + static ArgInspector2 h(ArgInspector2 a) { return a; } + }; + py::class_(m, "ArgInspector") + .def(py::init<>()) + .def("f", &ArgInspector::f) + .def("g", &ArgInspector::g, "a"_a.noconvert(), "b"_a, "c"_a.noconvert()=13, "d"_a=ArgInspector2()) + .def_static("h", &ArgInspector::h, py::arg().noconvert()) + ; + m.def("arg_inspect_func", [](ArgInspector2 a, ArgInspector1 b) { return a.arg + "\n" + b.arg; }, + py::arg().noconvert(false), py::arg_v(nullptr, ArgInspector1()).noconvert(true)); + + m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f")); + m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert()); + }); diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 840ee707b..f890c6aaa 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -203,3 +203,47 @@ def test_cyclic_gc(): assert cstats.alive() == 2 del i1, i2 assert cstats.alive() == 0 + + +def test_noconvert_args(msg): + from pybind11_tests import ArgInspector, arg_inspect_func, floats_only, floats_preferred + + a = ArgInspector() + assert msg(a.f("hi")) == """ + loading ArgInspector1 argument WITH conversion allowed. Argument value = hi + """ + assert msg(a.g("this is a", "this is b")) == """ + loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a + loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b + 13 + loading ArgInspector2 argument WITH conversion allowed. Argument value = (default arg inspector 2) + """ # noqa: E501 line too long + assert msg(a.g("this is a", "this is b", 42)) == """ + loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a + loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b + 42 + loading ArgInspector2 argument WITH conversion allowed. Argument value = (default arg inspector 2) + """ # noqa: E501 line too long + assert msg(a.g("this is a", "this is b", 42, "this is d")) == """ + loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a + loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b + 42 + loading ArgInspector2 argument WITH conversion allowed. Argument value = this is d + """ + assert (a.h("arg 1") == + "loading ArgInspector2 argument WITHOUT conversion allowed. Argument value = arg 1") + assert msg(arg_inspect_func("A1", "A2")) == """ + loading ArgInspector2 argument WITH conversion allowed. Argument value = A1 + loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = A2 + """ + + assert floats_preferred(4) == 2.0 + assert floats_only(4.0) == 2.0 + with pytest.raises(TypeError) as excinfo: + floats_only(4) + assert msg(excinfo.value) == """ + floats_only(): incompatible function arguments. The following argument types are supported: + 1. (f: float) -> float + + Invoked with: 4 + """