Skip to content

Commit 6ce9bb6

Browse files
committed
Merge pull request opencv#19312 from VadimLevin:dev/vlevin/clear-msg-for-failed-overload-resolution
2 parents 104e64d + a0bdb78 commit 6ce9bb6

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

modules/core/include/opencv2/core/bindings_utils.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ String dumpString(const String& argument)
6464
return cv::format("String: %s", argument.c_str());
6565
}
6666

67+
CV_WRAP static inline
68+
String testOverloadResolution(int value, const Point& point = Point(42, 24))
69+
{
70+
return format("overload (int=%d, point=(x=%d, y=%d))", value, point.x,
71+
point.y);
72+
}
73+
74+
CV_WRAP static inline
75+
String testOverloadResolution(const Rect& rect)
76+
{
77+
return format("overload (rect=(x=%d, y=%d, w=%d, h=%d))", rect.x, rect.y,
78+
rect.width, rect.height);
79+
}
80+
6781
CV_WRAP static inline
6882
AsyncArray testAsyncArray(InputArray argument)
6983
{

modules/python/src2/cv2.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include "opencv2/core/utils/configuration.private.hpp"
3535
#include "opencv2/core/utils/logger.hpp"
36+
#include "opencv2/core/utils/tls.hpp"
3637

3738
#include "pyopencv_generated_include.h"
3839
#include "opencv2/core/types_c.h"
@@ -138,6 +139,51 @@ class PyEnsureGIL
138139
PyGILState_STATE _state;
139140
};
140141

142+
/**
143+
* Light weight RAII wrapper for `PyObject*` owning references.
144+
* In comparisson to C++11 `std::unique_ptr` with custom deleter, it provides
145+
* implicit conversion functions that might be useful to initialize it with
146+
* Python functions those returns owning references through the `PyObject**`
147+
* e.g. `PyErr_Fetch` or directly pass it to functions those want to borrow
148+
* reference to object (doesn't extend object lifetime) e.g. `PyObject_Str`.
149+
*/
150+
class PySafeObject
151+
{
152+
public:
153+
PySafeObject() : obj_(NULL) {}
154+
155+
explicit PySafeObject(PyObject* obj) : obj_(obj) {}
156+
157+
~PySafeObject()
158+
{
159+
Py_CLEAR(obj_);
160+
}
161+
162+
operator PyObject*()
163+
{
164+
return obj_;
165+
}
166+
167+
operator PyObject**()
168+
{
169+
return &obj_;
170+
}
171+
172+
PyObject* release()
173+
{
174+
PyObject* obj = obj_;
175+
obj_ = NULL;
176+
return obj;
177+
}
178+
179+
private:
180+
PyObject* obj_;
181+
182+
// Explicitly disable copy operations
183+
PySafeObject(const PySafeObject*); // = delete
184+
PySafeObject& operator=(const PySafeObject&); // = delete
185+
};
186+
141187
static void pyRaiseCVException(const cv::Exception &e)
142188
{
143189
PyObject_SetAttrString(opencv_error, "file", PyString_FromString(e.file.c_str()));
@@ -290,6 +336,74 @@ bool parseNumpyScalar(PyObject* obj, T& value)
290336
return false;
291337
}
292338

339+
TLSData<std::vector<std::string> > conversionErrorsTLS;
340+
341+
inline void pyPrepareArgumentConversionErrorsStorage(std::size_t size)
342+
{
343+
std::vector<std::string>& conversionErrors = conversionErrorsTLS.getRef();
344+
conversionErrors.clear();
345+
conversionErrors.reserve(size);
346+
}
347+
348+
void pyRaiseCVOverloadException(const std::string& functionName)
349+
{
350+
const std::vector<std::string>& conversionErrors = conversionErrorsTLS.getRef();
351+
const std::size_t conversionErrorsCount = conversionErrors.size();
352+
if (conversionErrorsCount > 0)
353+
{
354+
// In modern std libraries small string optimization is used = no dynamic memory allocations,
355+
// but it can be applied only for string with length < 18 symbols (in GCC)
356+
const std::string bullet = "\n - ";
357+
358+
// Estimate required buffer size - save dynamic memory allocations = faster
359+
std::size_t requiredBufferSize = bullet.size() * conversionErrorsCount;
360+
for (std::size_t i = 0; i < conversionErrorsCount; ++i)
361+
{
362+
requiredBufferSize += conversionErrors[i].size();
363+
}
364+
365+
// Only string concatenation is required so std::string is way faster than
366+
// std::ostringstream
367+
std::string errorMessage("Overload resolution failed:");
368+
errorMessage.reserve(errorMessage.size() + requiredBufferSize);
369+
for (std::size_t i = 0; i < conversionErrorsCount; ++i)
370+
{
371+
errorMessage += bullet;
372+
errorMessage += conversionErrors[i];
373+
}
374+
cv::Exception exception(CV_StsBadArg, errorMessage, functionName, "", -1);
375+
pyRaiseCVException(exception);
376+
}
377+
else
378+
{
379+
cv::Exception exception(CV_StsInternal, "Overload resolution failed, but no errors reported",
380+
functionName, "", -1);
381+
pyRaiseCVException(exception);
382+
}
383+
}
384+
385+
void pyPopulateArgumentConversionErrors()
386+
{
387+
if (PyErr_Occurred())
388+
{
389+
PySafeObject exception_type;
390+
PySafeObject exception_value;
391+
PySafeObject exception_traceback;
392+
PyErr_Fetch(exception_type, exception_value, exception_traceback);
393+
PyErr_NormalizeException(exception_type, exception_value,
394+
exception_traceback);
395+
396+
PySafeObject exception_message(PyObject_Str(exception_value));
397+
std::string message;
398+
getUnicodeString(exception_message, message);
399+
#ifdef CV_CXX11
400+
conversionErrorsTLS.getRef().push_back(std::move(message));
401+
#else
402+
conversionErrorsTLS.getRef().push_back(message);
403+
#endif
404+
}
405+
}
406+
293407
} // namespace
294408

295409
typedef std::vector<uchar> vector_uchar;

modules/python/src2/gen2.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@
174174
gen_template_rw_prop_init = Template("""
175175
{(char*)"${member}", (getter)pyopencv_${name}_get_${member}, (setter)pyopencv_${name}_set_${member}, (char*)"${member}", NULL},""")
176176

177+
gen_template_overloaded_function_call = Template("""
178+
{
179+
${variant}
180+
181+
pyPopulateArgumentConversionErrors();
182+
}
183+
""")
184+
177185
class FormatStrings:
178186
string = 's'
179187
unsigned_char = 'b'
@@ -768,8 +776,12 @@ def gen_code(self, codegen):
768776
# if the function/method has only 1 signature, then just put it
769777
code += all_code_variants[0]
770778
else:
771-
# try to execute each signature
772-
code += " PyErr_Clear();\n\n".join([" {\n" + v + " }\n" for v in all_code_variants])
779+
# try to execute each signature, add an interlude between function
780+
# calls to collect error from all conversions
781+
code += ' pyPrepareArgumentConversionErrorsStorage({});\n'.format(len(all_code_variants))
782+
code += ' \n'.join(gen_template_overloaded_function_call.substitute(variant=v)
783+
for v in all_code_variants)
784+
code += ' pyRaiseCVOverloadException("{}");\n'.format(self.name)
773785

774786
def_ret = "NULL"
775787
if self.isconstructor:

modules/python/test/test_misc.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,44 @@ def test_error_handler(status, func_name, err_msg, file_name, line):
7373
except cv.error as _e:
7474
pass
7575

76+
def test_overload_resolution_can_choose_correct_overload(self):
77+
val = 123
78+
point = (51, 165)
79+
self.assertEqual(cv.utils.testOverloadResolution(val, point),
80+
'overload (int={}, point=(x={}, y={}))'.format(val, *point),
81+
"Can't select first overload if all arguments are provided as positional")
82+
83+
self.assertEqual(cv.utils.testOverloadResolution(val, point=point),
84+
'overload (int={}, point=(x={}, y={}))'.format(val, *point),
85+
"Can't select first overload if one of the arguments are provided as keyword")
86+
87+
self.assertEqual(cv.utils.testOverloadResolution(val),
88+
'overload (int={}, point=(x=42, y=24))'.format(val),
89+
"Can't select first overload if one of the arguments has default value")
90+
91+
rect = (1, 5, 10, 23)
92+
self.assertEqual(cv.utils.testOverloadResolution(rect),
93+
'overload (rect=(x={}, y={}, w={}, h={}))'.format(*rect),
94+
"Can't select second overload if all arguments are provided")
95+
96+
def test_overload_resolution_fails(self):
97+
def test_overload_resolution(msg, *args, **kwargs):
98+
no_exception_msg = 'Overload resolution failed without any exception for: "{}"'.format(msg)
99+
wrong_exception_msg = 'Overload resolution failed with wrong exception type for: "{}"'.format(msg)
100+
with self.assertRaises((cv.error, Exception), msg=no_exception_msg) as cm:
101+
cv.utils.testOverloadResolution(*args, **kwargs)
102+
self.assertEqual(type(cm.exception), cv.error, wrong_exception_msg)
103+
104+
test_overload_resolution('wrong second arg type (keyword arg)', 5, point=(1, 2, 3))
105+
test_overload_resolution('wrong second arg type', 5, 2)
106+
test_overload_resolution('wrong first arg', 3.4, (12, 21))
107+
test_overload_resolution('wrong first arg, no second arg', 4.5)
108+
test_overload_resolution('wrong args number for first overload', 3, (12, 21), 123)
109+
test_overload_resolution('wrong args number for second overload', (3, 12, 12, 1), (12, 21))
110+
# One of the common problems
111+
test_overload_resolution('rect with float coordinates', (4.5, 4, 2, 1))
112+
test_overload_resolution('rect with wrong number of coordinates', (4, 4, 1))
113+
76114

77115
class Arguments(NewOpenCVTests):
78116

0 commit comments

Comments
 (0)