From ccebbbc0acf3d0c3666eb1b2f79753f33ec8b374 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Thu, 20 Jan 2022 15:21:47 +0300 Subject: [PATCH] feature: submodule or a class scope for exported classes All classes are registered in the scope that corresponds to C++ namespace or exported class. Example: `cv::ml::Boost` is exported as `cv.ml.Boost` `cv::SimpleBlobDetector::Params` is exported as `cv.SimpleBlobDetector.Params` For backward compatibility all classes are registered in the global module with their mangling name containing scope information. Example: `cv::ml::Boost` has `cv.ml_Boost` alias to `cv.ml.Boost` type --- .../include/opencv2/core/bindings_utils.hpp | 47 +++++ modules/python/src2/cv2.cpp | 191 +++++++++++++++++- modules/python/src2/gen2.py | 110 +++++++--- modules/python/src2/pycompat.hpp | 144 +++++++------ modules/python/test/test_misc.py | 85 ++++++++ 5 files changed, 484 insertions(+), 93 deletions(-) diff --git a/modules/core/include/opencv2/core/bindings_utils.hpp b/modules/core/include/opencv2/core/bindings_utils.hpp index f091606c4a..6e825ec816 100644 --- a/modules/core/include/opencv2/core/bindings_utils.hpp +++ b/modules/core/include/opencv2/core/bindings_utils.hpp @@ -217,6 +217,53 @@ namespace nested { CV_WRAP static inline bool testEchoBooleanFunction(bool flag) { return flag; } + +class CV_EXPORTS_W CV_WRAP_AS(ExportClassName) OriginalClassName +{ +public: + struct CV_EXPORTS_W_SIMPLE Params + { + CV_PROP_RW int int_value; + CV_PROP_RW float float_value; + + CV_WRAP explicit Params(int int_param = 123, float float_param = 3.5f) + { + int_value = int_param; + float_value = float_param; + } + }; + + explicit OriginalClassName(const OriginalClassName::Params& params = OriginalClassName::Params()) + { + params_ = params; + } + + CV_WRAP int getIntParam() const + { + return params_.int_value; + } + + CV_WRAP float getFloatParam() const + { + return params_.float_value; + } + + CV_WRAP static std::string originalName() + { + return "OriginalClassName"; + } + + CV_WRAP static Ptr + create(const OriginalClassName::Params& params = OriginalClassName::Params()) + { + return makePtr(params); + } + +private: + OriginalClassName::Params params_; +}; + +typedef OriginalClassName::Params OriginalClassName_Params; } // namespace nested //! @} // core_utils diff --git a/modules/python/src2/cv2.cpp b/modules/python/src2/cv2.cpp index a82086f315..f41cd6f389 100644 --- a/modules/python/src2/cv2.cpp +++ b/modules/python/src2/cv2.cpp @@ -2066,9 +2066,9 @@ static int convert_to_char(PyObject *o, char *dst, const ArgInfo& info) #include "pyopencv_custom_headers.h" #ifdef CVPY_DYNAMIC_INIT -#define CVPY_TYPE(WNAME, NAME, STORAGE, SNAME, _1, _2) CVPY_TYPE_DECLARE_DYNAMIC(WNAME, NAME, STORAGE, SNAME) +#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, _1, _2, SCOPE) CVPY_TYPE_DECLARE_DYNAMIC(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) #else -#define CVPY_TYPE(WNAME, NAME, STORAGE, SNAME, _1, _2) CVPY_TYPE_DECLARE(WNAME, NAME, STORAGE, SNAME) +#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, _1, _2, SCOPE) CVPY_TYPE_DECLARE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) #endif #include "pyopencv_generated_types.h" #undef CVPY_TYPE @@ -2251,6 +2251,189 @@ static bool init_submodule(PyObject * root, const char * name, PyMethodDef * met return true; } +static inline +bool registerTypeInModuleScope(PyObject* module, const char* type_name, PyObject* type_obj) +{ + if (PyModule_AddObject(module, type_name, type_obj) < 0) + { + PyErr_Format(PyExc_ImportError, + "Failed to register type '%s' in module scope '%s'", + type_name, PyModule_GetName(module) + ); + Py_DECREF(type_obj); + return false; + } + return true; +} + +static inline +bool registerTypeInClassScope(PyObject* cls, const char* type_name, PyObject* type_obj) +{ + if (!PyType_CheckExact(cls)) { + PyErr_Format(PyExc_ImportError, + "Failed to register type '%s' in class scope. " + "Scope class object has a wrong type", type_name + ); + return false; + } + if (PyObject_SetAttrString(cls, type_name, type_obj) < 0) + { + #ifndef Py_LIMITED_API + PyObject* cls_dict = reinterpret_cast(cls)->tp_dict; + if (PyDict_SetItemString(cls_dict, type_name, type_obj) >= 0) { + /// Clearing the error set by PyObject_SetAttrString: + /// TypeError: can't set attributes of built-in/extension type NAME + PyErr_Clear(); + return true; + } + #endif + const std::string cls_name = getPyObjectNameAttr(cls); + PyErr_Format(PyExc_ImportError, + "Failed to register type '%s' in '%s' class scope. Can't update scope dictionary", + type_name, cls_name.c_str() + ); + return false; + } + return true; +} + +static inline +PyObject* getScopeFromTypeObject(PyObject* obj, const std::string& scope_name) +{ + if (!PyType_CheckExact(obj)) { + const std::string type_name = getPyObjectNameAttr(obj); + return PyErr_Format(PyExc_ImportError, + "Failed to get scope from type '%s' " + "Scope class object has a wrong type", type_name.c_str() + ); + } + /// When using LIMITED API all classes are registered in the heap +#if defined(Py_LIMITED_API) + return PyObject_GetAttrString(obj, scope_name.c_str()); +#else + /// Otherwise classes may be registed on the stack or heap + PyObject* type_dict = reinterpret_cast(obj)->tp_dict; + if (!type_dict) { + const std::string type_name = getPyObjectNameAttr(obj); + return PyErr_Format(PyExc_ImportError, + "Failed to get scope from type '%s' " + "Type dictionary is not available", type_name.c_str() + ); + } + return PyDict_GetItemString(type_dict, scope_name.c_str()); +#endif // Py_LIMITED_API +} + +static inline +PyObject* findTypeScope(PyObject* root_module, const std::string& scope_name) +{ + PyObject* scope = root_module; + if (scope_name.empty()) + { + return scope; + } + /// Starting with 1 to omit leading dot in the scope name + size_t name_end = scope_name.find('.', 1); + if (name_end == std::string::npos) + { + name_end = scope_name.size(); + } + for (size_t name_start = 1; name_start < scope_name.size() && scope; ) + { + const std::string current_scope_name = scope_name.substr(name_start, + name_end - name_start); + + if (PyModule_CheckExact(scope)) + { + PyObject* scope_dict = PyModule_GetDict(scope); + if (!scope_dict) + { + return PyErr_Format(PyExc_ImportError, + "Scope '%s' dictionary is not available during the search for " + " the '%s' scope object", current_scope_name.c_str(), + scope_name.c_str() + ); + } + + scope = PyDict_GetItemString(scope_dict, current_scope_name.c_str()); + } + else if (PyType_CheckExact(scope)) + { + scope = getScopeFromTypeObject(scope, current_scope_name); + } + else + { + return PyErr_Format(PyExc_ImportError, + "Can't find scope '%s'. '%s' doesn't reference a module or a class", + scope_name.c_str(), current_scope_name.c_str() + ); + } + + + name_start = name_end + 1; + name_end = scope_name.find('.', name_start); + if (name_end == std::string::npos) + { + name_end = scope_name.size(); + } + } + if (!scope) + { + return PyErr_Format(PyExc_ImportError, + "Module or class with name '%s' can't be found in '%s' module", + scope_name.c_str(), PyModule_GetName(root_module) + ); + } + return scope; +} + +static bool registerNewType(PyObject* root_module, const char* type_name, + PyObject* type_obj, const std::string& scope_name) +{ + PyObject* scope = findTypeScope(root_module, scope_name); + + /// If scope can't be found it means that there is an error during + /// bindings generation + if (!scope) { + return false; + } + + if (PyModule_CheckExact(scope)) + { + if (!registerTypeInModuleScope(scope, type_name, type_obj)) + { + return false; + } + } + else + { + /// In Python 2 it is disallowed to register an inner classes + /// via modifing dictionary of the built-in type. + if (!registerTypeInClassScope(scope, type_name, type_obj)) + { + return false; + } + } + + /// Expose all classes that are defined in the submodules as aliases in the + /// root module for backward compatibility + /// If submodule and root module are same than no aliases registration are + /// required + if (scope != root_module) + { + std::string type_name_str(type_name); + + std::string alias_name; + alias_name.reserve(scope_name.size() + type_name_str.size()); + std::replace_copy(scope_name.begin() + 1, scope_name.end(), std::back_inserter(alias_name), '.', '_'); + alias_name += '_'; + alias_name += type_name_str; + + return registerTypeInModuleScope(root_module, alias_name.c_str(), type_obj); + } + return true; +} + #include "pyopencv_generated_modules_content.h" static bool init_body(PyObject * m) @@ -2264,10 +2447,10 @@ static bool init_body(PyObject * m) #undef CVPY_MODULE #ifdef CVPY_DYNAMIC_INIT -#define CVPY_TYPE(WNAME, NAME, _1, _2, BASE, CONSTRUCTOR) CVPY_TYPE_INIT_DYNAMIC(WNAME, NAME, return false, BASE, CONSTRUCTOR) +#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, _1, _2, BASE, CONSTRUCTOR, SCOPE) CVPY_TYPE_INIT_DYNAMIC(EXPORT_NAME, CLASS_ID, return false, BASE, CONSTRUCTOR, SCOPE) PyObject * pyopencv_NoBase_TypePtr = NULL; #else -#define CVPY_TYPE(WNAME, NAME, _1, _2, BASE, CONSTRUCTOR) CVPY_TYPE_INIT_STATIC(WNAME, NAME, return false, BASE, CONSTRUCTOR) +#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, _1, _2, BASE, CONSTRUCTOR, SCOPE) CVPY_TYPE_INIT_STATIC(EXPORT_NAME, CLASS_ID, return false, BASE, CONSTRUCTOR, SCOPE) PyTypeObject * pyopencv_NoBase_TypePtr = NULL; #endif #include "pyopencv_generated_types.h" diff --git a/modules/python/src2/gen2.py b/modules/python/src2/gen2.py index 51566fc248..79853648c5 100755 --- a/modules/python/src2/gen2.py +++ b/modules/python/src2/gen2.py @@ -243,10 +243,20 @@ class ClassProp(object): self.readonly = False class ClassInfo(object): - def __init__(self, name, decl=None): + def __init__(self, name, decl=None, codegen=None): + # Scope name can be a module or other class e.g. cv::SimpleBlobDetector::Params + scope_name, self.original_name = name.rsplit(".", 1) + + # In case scope refer the outer class exported with different name + if codegen: + scope_name = codegen.get_export_scope_name(scope_name) + self.scope_name = re.sub(r"^cv\.?", "", scope_name) + + self.export_name = self.original_name + + self.class_id = normalize_class_name(name) + self.cname = name.replace(".", "::") - self.name = self.wname = normalize_class_name(name) - self.sname = name[name.rfind('.') + 1:] self.ismap = False self.issimple = False self.isalgorithm = False @@ -261,7 +271,7 @@ class ClassInfo(object): if decl: bases = decl[1].split()[1:] if len(bases) > 1: - print("Note: Class %s has more than 1 base class (not supported by Python C extensions)" % (self.name,)) + print("Note: Class %s has more than 1 base class (not supported by Python C extensions)" % (self.cname,)) print(" Bases: ", " ".join(bases)) print(" Only the first base class will be used") #return sys.exit(-1) @@ -275,22 +285,47 @@ class ClassInfo(object): for m in decl[2]: if m.startswith("="): - wname = m[1:] - npos = name.rfind('.') - if npos >= 0: - self.wname = normalize_class_name(name[:npos] + '.' + wname) - else: - self.wname = wname - customname = True + # Aliasing only affects the exported class name, not class identifier + self.export_name = m[1:] elif m == "/Map": self.ismap = True elif m == "/Simple": self.issimple = True self.props = [ClassProp(p) for p in decl[3]] + if not self.has_export_alias and self.original_name.startswith("Cv"): + self.export_name = self.export_name[2:] + if not customname and self.wname.startswith("Cv"): self.wname = self.wname[2:] + @property + def wname(self): + if len(self.scope_name) > 0: + return self.scope_name.replace(".", "_") + "_" + self.export_name + + return self.export_name + + @property + def name(self): + return self.class_id + + @property + def full_scope_name(self): + return "cv." + self.scope_name if len(self.scope_name) else "cv" + + @property + def full_export_name(self): + return self.full_scope_name + "." + self.export_name + + @property + def full_original_name(self): + return self.full_scope_name + "." + self.original_name + + @property + def has_export_alias(self): + return self.export_name != self.original_name + def gen_map_code(self, codegen): all_classes = codegen.classes code = "static bool pyopencv_to(PyObject* src, %s& dst, const ArgInfo& info)\n{\n PyObject* tmp;\n bool ok;\n" % (self.cname) @@ -343,9 +378,11 @@ class ClassInfo(object): methods_code.write(m.gen_code(codegen)) methods_inits.write(m.get_tab_entry()) - code = gen_template_type_impl.substitute(name=self.name, wname=self.wname, cname=self.cname, - getset_code=getset_code.getvalue(), getset_inits=getset_inits.getvalue(), - methods_code=methods_code.getvalue(), methods_inits=methods_inits.getvalue()) + code = gen_template_type_impl.substitute(name=self.name, + getset_code=getset_code.getvalue(), + getset_inits=getset_inits.getvalue(), + methods_code=methods_code.getvalue(), + methods_inits=methods_inits.getvalue()) return code @@ -359,13 +396,15 @@ class ClassInfo(object): if self.constructor is not None: constructor_name = self.constructor.get_wrapper_name() - return "CVPY_TYPE({}, {}, {}, {}, {}, {});\n".format( - self.wname, - self.name, + return 'CVPY_TYPE({}, {}, {}, {}, {}, {}, "{}");\n'.format( + self.export_name, + self.class_id, self.cname if self.issimple else "Ptr<{}>".format(self.cname), - self.sname if self.issimple else "Ptr", + self.original_name if self.issimple else "Ptr", baseptr, - constructor_name + constructor_name, + # Leading dot is required to provide correct class naming + "." + self.scope_name if len(self.scope_name) > 0 else self.scope_name ) @@ -815,12 +854,12 @@ class FuncInfo(object): classinfo = all_classes[self.classname] #if dump: pprint(vars(classinfo)) if self.isconstructor: - py_name = 'cv.' + classinfo.wname - elif self.is_static: - py_name = '.'.join([self.namespace, classinfo.sname + '_' + self.variants[0].wname]) + py_name = classinfo.full_export_name else: + py_name = classinfo.full_export_name + "." + self.variants[0].wname + + if not self.is_static: cname = classinfo.cname + '::' + cname - py_name = 'cv.' + classinfo.wname + '.' + self.variants[0].wname else: py_name = '.'.join([self.namespace, self.variants[0].wname]) #if dump: print(cname + " => " + py_name) @@ -862,7 +901,7 @@ class PythonWrapperGenerator(object): self.class_idx = 0 def add_class(self, stype, name, decl): - classinfo = ClassInfo(name, decl) + classinfo = ClassInfo(name, decl, self) classinfo.decl_idx = self.class_idx self.class_idx += 1 @@ -872,16 +911,30 @@ class PythonWrapperGenerator(object): sys.exit(-1) self.classes[classinfo.name] = classinfo - # Add Class to json file. - namespace, classes, name = self.split_decl_name(name) + namespace, _, _ = self.split_decl_name(name) namespace = '.'.join(namespace) - name = '_'.join(classes+[name]) + # Registering a namespace if it is not already handled or + # doesn't have anything except classes defined in it + self.namespaces.setdefault(namespace, Namespace()) - py_name = 'cv.' + classinfo.wname # use wrapper name + # Add Class to json file. + py_name = classinfo.full_export_name # use wrapper name py_signatures = self.py_signatures.setdefault(classinfo.cname, []) py_signatures.append(dict(name=py_name)) #print('class: ' + classinfo.cname + " => " + py_name) + def get_export_scope_name(self, original_scope_name): + # Outer classes should be registered before their content - inner classes in this case + class_scope = self.classes.get(normalize_class_name(original_scope_name), None) + + if class_scope: + return class_scope.full_export_name + + # Otherwise it is a namespace. + # If something is messed up at this point - it will be revelead during + # library import + return original_scope_name + def split_decl_name(self, name): chunks = name.split('.') namespace = chunks[:-1] @@ -971,6 +1024,7 @@ class PythonWrapperGenerator(object): w_classes.append(w_classname) g_wname = "_".join(w_classes+[name]) func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs + # Exports static function with internal name (backward compatibility) func = func_map.setdefault(g_name, FuncInfo("", g_name, cname, isconstructor, namespace_str, False)) func.add_variant(decl, isphantom) if g_wname != g_name: # TODO OpenCV 5.0 diff --git a/modules/python/src2/pycompat.hpp b/modules/python/src2/pycompat.hpp index 5c1bc2354f..b9eca7bc18 100644 --- a/modules/python/src2/pycompat.hpp +++ b/modules/python/src2/pycompat.hpp @@ -60,6 +60,10 @@ #endif // PY_MAJOR >=3 +#ifndef PyType_CheckExact +#define PyType_CheckExact(obj) (Py_TYPE(op) == &PyType_Type) +#endif // !PyType_CheckExact + static inline bool getUnicodeString(PyObject * obj, std::string &str) { bool res = false; @@ -91,6 +95,26 @@ static inline bool getUnicodeString(PyObject * obj, std::string &str) return res; } +static inline +std::string getPyObjectNameAttr(PyObject* obj) +{ + std::string obj_name; + PyObject* cls_name_obj = PyObject_GetAttrString(obj, "__name__"); + if (cls_name_obj && !getUnicodeString(cls_name_obj, obj_name)) { + obj_name.clear(); + } + #ifndef Py_LIMITED_API + if (PyType_CheckExact(obj) && obj_name.empty()) + { + obj_name = reinterpret_cast(obj)->tp_name; + } + #endif + if (obj_name.empty()) { + obj_name = ""; + } + return obj_name; +} + //================================================================================================== #define CV_PY_FN_WITH_KW_(fn, flags) (PyCFunction)(void*)(PyCFunctionWithKeywords)(fn), (flags) | METH_VARARGS | METH_KEYWORDS @@ -172,107 +196,106 @@ PyObject* pyopencv_from(const TYPE& src) #endif -#define CVPY_TYPE_DECLARE(WNAME, NAME, STORAGE, SNAME) \ - struct pyopencv_##NAME##_t \ +#define CVPY_TYPE_DECLARE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) \ + struct pyopencv_##CLASS_ID##_t \ { \ PyObject_HEAD \ STORAGE v; \ }; \ - static PyTypeObject pyopencv_##NAME##_TypeXXX = \ + static PyTypeObject pyopencv_##CLASS_ID##_TypeXXX = \ { \ CVPY_TYPE_HEAD \ - MODULESTR"."#WNAME, \ - sizeof(pyopencv_##NAME##_t), \ + MODULESTR SCOPE"."#EXPORT_NAME, \ + sizeof(pyopencv_##CLASS_ID##_t), \ }; \ - static PyTypeObject * pyopencv_##NAME##_TypePtr = &pyopencv_##NAME##_TypeXXX; \ - static bool pyopencv_##NAME##_getp(PyObject * self, STORAGE * & dst) \ + static PyTypeObject * pyopencv_##CLASS_ID##_TypePtr = &pyopencv_##CLASS_ID##_TypeXXX; \ + static bool pyopencv_##CLASS_ID##_getp(PyObject * self, STORAGE * & dst) \ { \ - if (PyObject_TypeCheck(self, pyopencv_##NAME##_TypePtr)) \ + if (PyObject_TypeCheck(self, pyopencv_##CLASS_ID##_TypePtr)) \ { \ - dst = &(((pyopencv_##NAME##_t*)self)->v); \ + dst = &(((pyopencv_##CLASS_ID##_t*)self)->v); \ return true; \ } \ return false; \ } \ - static PyObject * pyopencv_##NAME##_Instance(const STORAGE &r) \ + static PyObject * pyopencv_##CLASS_ID##_Instance(const STORAGE &r) \ { \ - pyopencv_##NAME##_t *m = PyObject_NEW(pyopencv_##NAME##_t, pyopencv_##NAME##_TypePtr); \ + pyopencv_##CLASS_ID##_t *m = PyObject_NEW(pyopencv_##CLASS_ID##_t, pyopencv_##CLASS_ID##_TypePtr); \ new (&(m->v)) STORAGE(r); \ return (PyObject*)m; \ } \ - static void pyopencv_##NAME##_dealloc(PyObject* self) \ + static void pyopencv_##CLASS_ID##_dealloc(PyObject* self) \ { \ - ((pyopencv_##NAME##_t*)self)->v.STORAGE::~SNAME(); \ + ((pyopencv_##CLASS_ID##_t*)self)->v.STORAGE::~SNAME(); \ PyObject_Del(self); \ } \ - static PyObject* pyopencv_##NAME##_repr(PyObject* self) \ + static PyObject* pyopencv_##CLASS_ID##_repr(PyObject* self) \ { \ char str[1000]; \ - sprintf(str, "<"#WNAME" %p>", self); \ + sprintf(str, "< " MODULESTR SCOPE"."#EXPORT_NAME" %p>", self); \ return PyString_FromString(str); \ } -#define CVPY_TYPE_INIT_STATIC(WNAME, NAME, ERROR_HANDLER, BASE, CONSTRUCTOR) \ +#define CVPY_TYPE_INIT_STATIC(EXPORT_NAME, CLASS_ID, ERROR_HANDLER, BASE, CONSTRUCTOR, SCOPE) \ { \ - pyopencv_##NAME##_TypePtr->tp_base = pyopencv_##BASE##_TypePtr; \ - pyopencv_##NAME##_TypePtr->tp_dealloc = pyopencv_##NAME##_dealloc; \ - pyopencv_##NAME##_TypePtr->tp_repr = pyopencv_##NAME##_repr; \ - pyopencv_##NAME##_TypePtr->tp_getset = pyopencv_##NAME##_getseters; \ - pyopencv_##NAME##_TypePtr->tp_init = (initproc) CONSTRUCTOR; \ - pyopencv_##NAME##_TypePtr->tp_methods = pyopencv_##NAME##_methods; \ - pyopencv_##NAME##_TypePtr->tp_alloc = PyType_GenericAlloc; \ - pyopencv_##NAME##_TypePtr->tp_new = PyType_GenericNew; \ - pyopencv_##NAME##_TypePtr->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; \ - if (PyType_Ready(pyopencv_##NAME##_TypePtr) != 0) \ + pyopencv_##CLASS_ID##_TypePtr->tp_base = pyopencv_##BASE##_TypePtr; \ + pyopencv_##CLASS_ID##_TypePtr->tp_dealloc = pyopencv_##CLASS_ID##_dealloc; \ + pyopencv_##CLASS_ID##_TypePtr->tp_repr = pyopencv_##CLASS_ID##_repr; \ + pyopencv_##CLASS_ID##_TypePtr->tp_getset = pyopencv_##CLASS_ID##_getseters; \ + pyopencv_##CLASS_ID##_TypePtr->tp_init = (initproc) CONSTRUCTOR; \ + pyopencv_##CLASS_ID##_TypePtr->tp_methods = pyopencv_##CLASS_ID##_methods; \ + pyopencv_##CLASS_ID##_TypePtr->tp_alloc = PyType_GenericAlloc; \ + pyopencv_##CLASS_ID##_TypePtr->tp_new = PyType_GenericNew; \ + pyopencv_##CLASS_ID##_TypePtr->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; \ + if (PyType_Ready(pyopencv_##CLASS_ID##_TypePtr) != 0) \ { \ ERROR_HANDLER; \ } \ - CVPY_TYPE_INCREF(pyopencv_##NAME##_TypePtr); \ - if (PyModule_AddObject(m, #WNAME, (PyObject *)pyopencv_##NAME##_TypePtr) < 0) \ + CVPY_TYPE_INCREF(pyopencv_##CLASS_ID##_TypePtr); \ + if (!registerNewType(m, #EXPORT_NAME, (PyObject*)pyopencv_##CLASS_ID##_TypePtr, SCOPE)) \ { \ - printf("Failed to register a new type: " #WNAME ", base (" #BASE ")\n"); \ - Py_DECREF(pyopencv_##NAME##_TypePtr); \ + printf("Failed to register a new type: " #EXPORT_NAME ", base (" #BASE ") in " SCOPE " \n"); \ ERROR_HANDLER; \ } \ } //================================================================================================== -#define CVPY_TYPE_DECLARE_DYNAMIC(WNAME, NAME, STORAGE, SNAME) \ - struct pyopencv_##NAME##_t \ +#define CVPY_TYPE_DECLARE_DYNAMIC(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) \ + struct pyopencv_##CLASS_ID##_t \ { \ PyObject_HEAD \ STORAGE v; \ }; \ - static PyObject * pyopencv_##NAME##_TypePtr = 0; \ - static bool pyopencv_##NAME##_getp(PyObject * self, STORAGE * & dst) \ + static PyObject * pyopencv_##CLASS_ID##_TypePtr = 0; \ + static bool pyopencv_##CLASS_ID##_getp(PyObject * self, STORAGE * & dst) \ { \ - if (PyObject_TypeCheck(self, (PyTypeObject*)pyopencv_##NAME##_TypePtr)) \ + if (PyObject_TypeCheck(self, (PyTypeObject*)pyopencv_##CLASS_ID##_TypePtr)) \ { \ - dst = &(((pyopencv_##NAME##_t*)self)->v); \ + dst = &(((pyopencv_##CLASS_ID##_t*)self)->v); \ return true; \ } \ return false; \ } \ - static PyObject * pyopencv_##NAME##_Instance(const STORAGE &r) \ + static PyObject * pyopencv_##CLASS_ID##_Instance(const STORAGE &r) \ { \ - pyopencv_##NAME##_t *m = PyObject_New(pyopencv_##NAME##_t, (PyTypeObject*)pyopencv_##NAME##_TypePtr); \ + pyopencv_##CLASS_ID##_t *m = PyObject_New(pyopencv_##CLASS_ID##_t, (PyTypeObject*)pyopencv_##CLASS_ID##_TypePtr); \ new (&(m->v)) STORAGE(r); \ return (PyObject*)m; \ } \ - static void pyopencv_##NAME##_dealloc(PyObject* self) \ + static void pyopencv_##CLASS_ID##_dealloc(PyObject* self) \ { \ - ((pyopencv_##NAME##_t*)self)->v.STORAGE::~SNAME(); \ + ((pyopencv_##CLASS_ID##_t*)self)->v.STORAGE::~SNAME(); \ PyObject_Del(self); \ } \ - static PyObject* pyopencv_##NAME##_repr(PyObject* self) \ + static PyObject* pyopencv_##CLASS_ID##_repr(PyObject* self) \ { \ char str[1000]; \ - sprintf(str, "<"#WNAME" %p>", self); \ + sprintf(str, "< " MODULESTR SCOPE"."#EXPORT_NAME" %p>", self); \ return PyString_FromString(str); \ } \ - static PyType_Slot pyopencv_##NAME##_Slots[] = \ + static PyType_Slot pyopencv_##CLASS_ID##_Slots[] = \ { \ {Py_tp_dealloc, 0}, \ {Py_tp_repr, 0}, \ @@ -283,37 +306,36 @@ PyObject* pyopencv_from(const TYPE& src) {Py_tp_new, 0}, \ {0, 0} \ }; \ - static PyType_Spec pyopencv_##NAME##_Spec = \ + static PyType_Spec pyopencv_##CLASS_ID##_Spec = \ { \ - MODULESTR"."#WNAME, \ - sizeof(pyopencv_##NAME##_t), \ + MODULESTR SCOPE"."#EXPORT_NAME, \ + sizeof(pyopencv_##CLASS_ID##_t), \ 0, \ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, \ - pyopencv_##NAME##_Slots \ + pyopencv_##CLASS_ID##_Slots \ }; -#define CVPY_TYPE_INIT_DYNAMIC(WNAME, NAME, ERROR_HANDLER, BASE, CONSTRUCTOR) \ +#define CVPY_TYPE_INIT_DYNAMIC(EXPORT_NAME, CLASS_ID, ERROR_HANDLER, BASE, CONSTRUCTOR, SCOPE) \ { \ - pyopencv_##NAME##_Slots[0].pfunc /*tp_dealloc*/ = (void*)pyopencv_##NAME##_dealloc; \ - pyopencv_##NAME##_Slots[1].pfunc /*tp_repr*/ = (void*)pyopencv_##NAME##_repr; \ - pyopencv_##NAME##_Slots[2].pfunc /*tp_getset*/ = (void*)pyopencv_##NAME##_getseters; \ - pyopencv_##NAME##_Slots[3].pfunc /*tp_init*/ = (void*) CONSTRUCTOR; \ - pyopencv_##NAME##_Slots[4].pfunc /*tp_methods*/ = pyopencv_##NAME##_methods; \ - pyopencv_##NAME##_Slots[5].pfunc /*tp_alloc*/ = (void*)PyType_GenericAlloc; \ - pyopencv_##NAME##_Slots[6].pfunc /*tp_new*/ = (void*)PyType_GenericNew; \ + pyopencv_##CLASS_ID##_Slots[0].pfunc /*tp_dealloc*/ = (void*)pyopencv_##CLASS_ID##_dealloc; \ + pyopencv_##CLASS_ID##_Slots[1].pfunc /*tp_repr*/ = (void*)pyopencv_##CLASS_ID##_repr; \ + pyopencv_##CLASS_ID##_Slots[2].pfunc /*tp_getset*/ = (void*)pyopencv_##CLASS_ID##_getseters; \ + pyopencv_##CLASS_ID##_Slots[3].pfunc /*tp_init*/ = (void*) CONSTRUCTOR; \ + pyopencv_##CLASS_ID##_Slots[4].pfunc /*tp_methods*/ = pyopencv_##CLASS_ID##_methods; \ + pyopencv_##CLASS_ID##_Slots[5].pfunc /*tp_alloc*/ = (void*)PyType_GenericAlloc; \ + pyopencv_##CLASS_ID##_Slots[6].pfunc /*tp_new*/ = (void*)PyType_GenericNew; \ PyObject * bases = 0; \ if (pyopencv_##BASE##_TypePtr) \ bases = PyTuple_Pack(1, pyopencv_##BASE##_TypePtr); \ - pyopencv_##NAME##_TypePtr = PyType_FromSpecWithBases(&pyopencv_##NAME##_Spec, bases); \ - if (!pyopencv_##NAME##_TypePtr) \ + pyopencv_##CLASS_ID##_TypePtr = PyType_FromSpecWithBases(&pyopencv_##CLASS_ID##_Spec, bases); \ + if (!pyopencv_##CLASS_ID##_TypePtr) \ { \ - printf("Failed to create type from spec: " #WNAME ", base (" #BASE ")\n"); \ + printf("Failed to create type from spec: " #CLASS_ID ", base (" #BASE ")\n"); \ ERROR_HANDLER; \ } \ - if (PyModule_AddObject(m, #WNAME, (PyObject *)pyopencv_##NAME##_TypePtr) < 0) \ + if (!registerNewType(m, #EXPORT_NAME, (PyObject*)pyopencv_##CLASS_ID##_TypePtr, SCOPE)) \ { \ - printf("Failed to register a new type: " #WNAME ", base (" #BASE ")\n"); \ - Py_DECREF(pyopencv_##NAME##_TypePtr); \ + printf("Failed to register a new type: " #EXPORT_NAME ", base (" #BASE ") in " SCOPE " \n"); \ ERROR_HANDLER; \ } \ } diff --git a/modules/python/test/test_misc.py b/modules/python/test/test_misc.py index 41e5c6ba4b..ec56585ace 100644 --- a/modules/python/test/test_misc.py +++ b/modules/python/test/test_misc.py @@ -618,6 +618,91 @@ class Arguments(NewOpenCVTests): self.assertEqual(flag, cv.utils.nested.testEchoBooleanFunction(flag), msg="Function in nested module returns wrong result") + def test_class_from_submodule_has_global_alias(self): + self.assertTrue(hasattr(cv.ml, "Boost"), + msg="Class is not registered in the submodule") + self.assertTrue(hasattr(cv, "ml_Boost"), + msg="Class from submodule doesn't have alias in the " + "global module") + self.assertEqual(cv.ml.Boost, cv.ml_Boost, + msg="Classes from submodules and global module don't refer " + "to the same type") + + def test_inner_class_has_global_alias(self): + self.assertTrue(hasattr(cv.SimpleBlobDetector, "Params"), + msg="Class is not registered as inner class") + self.assertEqual(cv.SimpleBlobDetector.Params, cv.SimpleBlobDetector_Params, + msg="Inner class and class in global module don't refer " + "to the same type") + self.assertTrue(hasattr(cv, "SimpleBlobDetector_Params"), + msg="Inner class doesn't have alias in the global module") + + def test_class_from_submodule_has_global_alias(self): + self.assertTrue(hasattr(cv.ml, "Boost"), + msg="Class is not registered in the submodule") + self.assertTrue(hasattr(cv, "ml_Boost"), + msg="Class from submodule doesn't have alias in the " + "global module") + self.assertEqual(cv.ml.Boost, cv.ml_Boost, + msg="Classes from submodules and global module don't refer " + "to the same type") + + def test_inner_class_has_global_alias(self): + self.assertTrue(hasattr(cv.SimpleBlobDetector, "Params"), + msg="Class is not registered as inner class") + self.assertTrue(hasattr(cv, "SimpleBlobDetector_Params"), + msg="Inner class doesn't have alias in the global module") + self.assertEqual(cv.SimpleBlobDetector.Params, cv.SimpleBlobDetector_Params, + msg="Inner class and class in global module don't refer " + "to the same type") + self.assertTrue(hasattr(cv, "SimpleBlobDetector_Params"), + msg="Inner class doesn't have alias in the global module") + + def test_export_class_with_different_name(self): + self.assertTrue(hasattr(cv.utils.nested, "ExportClassName"), + msg="Class with export alias is not registered in the submodule") + self.assertTrue(hasattr(cv, "utils_nested_ExportClassName"), + msg="Class with export alias doesn't have alias in the " + "global module") + self.assertEqual(cv.utils.nested.ExportClassName.originalName(), "OriginalClassName") + + instance = cv.utils.nested.ExportClassName.create() + self.assertTrue(isinstance(instance, cv.utils.nested.ExportClassName), + msg="Factory function returns wrong class instance: {}".format(type(instance))) + self.assertTrue(hasattr(cv.utils.nested, "ExportClassName_create"), + msg="Factory function should have alias in the same module as the class") + # self.assertFalse(hasattr(cv.utils.nested, "OriginalClassName_create"), + # msg="Factory function should not be registered with original class name, "\ + # "when class has different export name") + + def test_export_inner_class_of_class_exported_with_different_name(self): + if not hasattr(cv.utils.nested, "ExportClassName"): + raise unittest.SkipTest("Outer class with export alias is not registered in the submodule") + + self.assertTrue(hasattr(cv.utils.nested.ExportClassName, "Params"), + msg="Inner class with export alias is not registered in " + "the outer class") + self.assertTrue(hasattr(cv, "utils_nested_ExportClassName_Params"), + msg="Inner class with export alias is not registered in " + "global module") + params = cv.utils.nested.ExportClassName.Params() + params.int_value = 45 + params.float_value = 4.5 + + instance = cv.utils.nested.ExportClassName.create(params) + self.assertTrue(isinstance(instance, cv.utils.nested.ExportClassName), + msg="Factory function returns wrong class instance: {}".format(type(instance))) + self.assertEqual( + params.int_value, instance.getIntParam(), + msg="Class initialized with wrong integer parameter. Expected: {}. Actual: {}".format( + params.int_value, instance.getIntParam() + )) + self.assertEqual( + params.float_value, instance.getFloatParam(), + msg="Class initialized with wrong integer parameter. Expected: {}. Actual: {}".format( + params.float_value, instance.getFloatParam() + )) + class SamplesFindFile(NewOpenCVTests):