Skip to content

Commit 5404930

Browse files
committed
Merge pull request opencv#9260 from Cartucho:add_python_signatures
2 parents e49febb + 768f4cb commit 5404930

File tree

10 files changed

+402
-91
lines changed

10 files changed

+402
-91
lines changed

cmake/OpenCVModule.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ macro(ocv_add_dependencies full_modname)
104104
list(FIND OPENCV_MODULE_${full_modname}_WRAPPERS "python" __python_idx)
105105
if (NOT __python_idx EQUAL -1)
106106
list(REMOVE_ITEM OPENCV_MODULE_${full_modname}_WRAPPERS "python")
107-
list(APPEND OPENCV_MODULE_${full_modname}_WRAPPERS "python2" "python3")
107+
list(APPEND OPENCV_MODULE_${full_modname}_WRAPPERS "python_bindings_generator" "python2" "python3")
108108
endif()
109109
unset(__python_idx)
110110

doc/CMakeLists.txt

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ endif(HAVE_DOC_GENERATOR)
3333

3434
if(BUILD_DOCS AND DOXYGEN_FOUND)
3535
# not documented modules list
36-
list(APPEND blacklist "ts" "java" "python2" "python3" "js" "world")
36+
list(APPEND blacklist "ts" "java" "python_bindings_generator" "python2" "python3" "js" "world")
3737
unset(CMAKE_DOXYGEN_TUTORIAL_CONTRIB_ROOT)
3838
unset(CMAKE_DOXYGEN_TUTORIAL_JS_ROOT)
3939

@@ -104,7 +104,6 @@ if(BUILD_DOCS AND DOXYGEN_FOUND)
104104
list(APPEND deps ${bib_file})
105105
endif()
106106
# Reference entry
107-
# set(one_ref "@ref ${m} | ${m}\n")
108107
set(one_ref "\t- ${m}. @ref ${m}\n")
109108
list(FIND EXTRA_MODULES ${m} _pos)
110109
if(${_pos} EQUAL -1)
@@ -205,15 +204,49 @@ if(BUILD_DOCS AND DOXYGEN_FOUND)
205204
list(APPEND js_tutorials_assets_deps "${f}" "${opencv_tutorial_html_dir}/${fname}")
206205
endforeach()
207206

208-
add_custom_target(doxygen
207+
add_custom_target(
208+
doxygen_cpp
209209
COMMAND ${DOXYGEN_EXECUTABLE} ${doxyfile}
210210
DEPENDS ${doxyfile} ${rootfile} ${bibfile} ${deps} ${js_tutorials_assets_deps}
211+
COMMENT "Generate Doxygen documentation"
211212
)
213+
212214
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doxygen/html
213215
DESTINATION "${OPENCV_DOC_INSTALL_PATH}"
214216
COMPONENT "docs" OPTIONAL
215217
)
216218

219+
if(NOT DEFINED HAVE_PYTHON_BS4 AND PYTHON_DEFAULT_EXECUTABLE)
220+
# Documentation post-processing tool requires BuautifulSoup Python package
221+
execute_process(COMMAND "${PYTHON_DEFAULT_EXECUTABLE}" -c "import bs4; from bs4 import BeautifulSoup; print(bs4.__version__)"
222+
RESULT_VARIABLE _result
223+
OUTPUT_VARIABLE _bs4_version
224+
OUTPUT_STRIP_TRAILING_WHITESPACE)
225+
226+
if(NOT _result EQUAL 0)
227+
set(HAVE_PYTHON_BS4 0 CACHE INTERNAL "")
228+
else()
229+
message(STATUS "Python BeautifulSoup (bs4) version: ${_bs4_version}")
230+
set(HAVE_PYTHON_BS4 1 CACHE INTERNAL "")
231+
endif()
232+
endif()
233+
234+
if(PYTHON_DEFAULT_EXECUTABLE AND HAVE_PYTHON_BS4
235+
AND OPENCV_PYTHON_SIGNATURES_FILE AND TARGET gen_opencv_python_source)
236+
add_custom_target(doxygen_python
237+
COMMAND ${PYTHON_DEFAULT_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/tools/add_signatures.py" "${CMAKE_CURRENT_BINARY_DIR}/doxygen/html/" "${OPENCV_PYTHON_SIGNATURES_FILE}" "python"
238+
DEPENDS doxygen_cpp gen_opencv_python_source
239+
COMMENT "Inject Python signatures into documentation"
240+
)
241+
add_custom_target(doxygen
242+
DEPENDS doxygen_cpp doxygen_python
243+
)
244+
else()
245+
add_custom_target(doxygen
246+
DEPENDS doxygen_cpp
247+
)
248+
endif()
249+
217250
# Alias to build/install docs only
218251
add_custom_target(install_docs
219252
DEPENDS doxygen

doc/tools/add_signatures.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
This code adds Python/Java signatures to the docs.
3+
4+
TODO: Do the same thing for Java
5+
* using javadoc/ get all the methods/classes/constants to a json file
6+
7+
TODO:
8+
* clarify when there are several C++ signatures corresponding to a single Python function.
9+
i.e: calcHist():
10+
http://docs.opencv.org/3.2.0/d6/dc7/group__imgproc__hist.html#ga4b2b5fd75503ff9e6844cc4dcdaed35d
11+
* clarify special case:
12+
http://docs.opencv.org/3.2.0/db/de0/group__core__utils.html#ga4910d7f86336cd4eff9dd05575667e41
13+
"""
14+
from __future__ import print_function
15+
import sys
16+
sys.dont_write_bytecode = True # Don't generate .pyc files / __pycache__ directories
17+
18+
import os
19+
from pprint import pprint
20+
import re
21+
import logging
22+
import json
23+
24+
import html_functions
25+
import doxygen_scan
26+
27+
loglevel=os.environ.get("LOGLEVEL", None)
28+
if loglevel:
29+
logging.basicConfig(level=loglevel)
30+
31+
ROOT_DIR = sys.argv[1]
32+
PYTHON_SIGNATURES_FILE = sys.argv[2]
33+
JAVA_OR_PYTHON = sys.argv[3]
34+
35+
ADD_JAVA = False
36+
ADD_PYTHON = False
37+
if JAVA_OR_PYTHON == "python":
38+
ADD_PYTHON = True
39+
40+
python_signatures = dict()
41+
with open(PYTHON_SIGNATURES_FILE, "rt") as f:
42+
python_signatures = json.load(f)
43+
print("Loaded Python signatures: %d" % len(python_signatures))
44+
45+
import xml.etree.ElementTree as ET
46+
root = ET.parse(ROOT_DIR + 'opencv.tag')
47+
files_dict = {}
48+
49+
# constants and function from opencv.tag
50+
namespaces = root.findall("./compound[@kind='namespace']")
51+
#print("Found {} namespaces".format(len(namespaces)))
52+
for ns in namespaces:
53+
ns_name = ns.find("./name").text
54+
#print('NS: {}'.format(ns_name))
55+
doxygen_scan.scan_namespace_constants(ns, ns_name, files_dict)
56+
doxygen_scan.scan_namespace_functions(ns, ns_name, files_dict)
57+
58+
# class methods from opencv.tag
59+
classes = root.findall("./compound[@kind='class']")
60+
#print("Found {} classes".format(len(classes)))
61+
for c in classes:
62+
c_name = c.find("./name").text
63+
file = c.find("./filename").text
64+
#print('Class: {} => {}'.format(c_name, file))
65+
doxygen_scan.scan_class_methods(c, c_name, files_dict)
66+
67+
print('Doxygen files to scan: %s' % len(files_dict))
68+
69+
files_processed = 0
70+
files_skipped = 0
71+
symbols_processed = 0
72+
73+
for file in files_dict:
74+
#if file != "dd/d9e/classcv_1_1VideoWriter.html":
75+
#if file != "d4/d86/group__imgproc__filter.html":
76+
#if file != "df/dfb/group__imgproc__object.html":
77+
# continue
78+
#print('File: ' + file)
79+
80+
anchor_list = files_dict[file]
81+
active_anchors = [a for a in anchor_list if a.cppname in python_signatures]
82+
if len(active_anchors) == 0: # no linked Python symbols
83+
#print('Skip: ' + file)
84+
files_skipped = files_skipped + 1
85+
continue
86+
87+
active_anchors_dict = {a.anchor: a for a in active_anchors}
88+
if len(active_anchors_dict) != len(active_anchors):
89+
logging.info('Duplicate entries detected: %s -> %s (%s)' % (len(active_anchors), len(active_anchors_dict), file))
90+
91+
files_processed = files_processed + 1
92+
93+
#pprint(active_anchors)
94+
symbols_processed = symbols_processed + len(active_anchors_dict)
95+
96+
logging.info('File: %r' % file)
97+
html_functions.insert_python_signatures(python_signatures, active_anchors_dict, ROOT_DIR + file)
98+
99+
print('Done (processed files %d, symbols %d, skipped %d files)' % (files_processed, symbols_processed, files_skipped))

doc/tools/doxygen_scan.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import traceback
2+
3+
class Symbol(object):
4+
def __init__(self, anchor, type, cppname):
5+
self.anchor = anchor
6+
self.type = type
7+
self.cppname = cppname
8+
#if anchor == 'ga586ebfb0a7fb604b35a23d85391329be':
9+
# print(repr(self))
10+
# traceback.print_stack()
11+
12+
def __repr__(self):
13+
return '%s:%s@%s' % (self.type, self.cppname, self.anchor)
14+
15+
def add_to_file(files_dict, file, anchor):
16+
anchors = files_dict.setdefault(file, [])
17+
anchors.append(anchor)
18+
19+
20+
def scan_namespace_constants(ns, ns_name, files_dict):
21+
constants = ns.findall("./member[@kind='enumvalue']")
22+
for c in constants:
23+
c_name = c.find("./name").text
24+
name = ns_name + '::' + c_name
25+
file = c.find("./anchorfile").text
26+
anchor = c.find("./anchor").text
27+
#print(' CONST: {} => {}#{}'.format(name, file, anchor))
28+
add_to_file(files_dict, file, Symbol(anchor, "const", name))
29+
30+
def scan_namespace_functions(ns, ns_name, files_dict):
31+
functions = ns.findall("./member[@kind='function']")
32+
for f in functions:
33+
f_name = f.find("./name").text
34+
name = ns_name + '::' + f_name
35+
file = f.find("./anchorfile").text
36+
anchor = f.find("./anchor").text
37+
#print(' FN: {} => {}#{}'.format(name, file, anchor))
38+
add_to_file(files_dict, file, Symbol(anchor, "fn", name))
39+
40+
def scan_class_methods(c, c_name, files_dict):
41+
methods = c.findall("./member[@kind='function']")
42+
for m in methods:
43+
m_name = m.find("./name").text
44+
name = c_name + '::' + m_name
45+
file = m.find("./anchorfile").text
46+
anchor = m.find("./anchor").text
47+
#print(' Method: {} => {}#{}'.format(name, file, anchor))
48+
add_to_file(files_dict, file, Symbol(anchor, "method", name))

doc/tools/html_functions.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import print_function
2+
import sys
3+
4+
import logging
5+
import os
6+
from pprint import pprint
7+
import traceback
8+
9+
try:
10+
import bs4
11+
from bs4 import BeautifulSoup
12+
except ImportError:
13+
raise ImportError('Error: '
14+
'Install BeautifulSoup (bs4) for adding'
15+
' Python & Java signatures documentation')
16+
17+
def load_html_file(file_dir):
18+
""" Uses BeautifulSoup to load an html """
19+
with open(file_dir, 'rb') as fp:
20+
soup = BeautifulSoup(fp, 'html.parser')
21+
return soup
22+
23+
def update_html(file, soup):
24+
s = str(soup)
25+
if os.name == 'nt' or sys.version_info[0] == 3: # if Windows
26+
s = s.encode('utf-8', 'ignore')
27+
with open(file, 'wb') as f:
28+
f.write(s)
29+
30+
31+
def insert_python_signatures(python_signatures, symbols_dict, filepath):
32+
soup = load_html_file(filepath)
33+
entries = soup.find_all(lambda tag: tag.name == "a" and tag.has_attr('id'))
34+
for e in entries:
35+
anchor = e['id']
36+
if anchor in symbols_dict:
37+
s = symbols_dict[anchor]
38+
logging.info('Process: %r' % s)
39+
if s.type == 'fn' or s.type == 'method':
40+
process_fn(soup, e, python_signatures[s.cppname], s)
41+
elif s.type == 'const':
42+
process_const(soup, e, python_signatures[s.cppname], s)
43+
else:
44+
logging.error('unsupported type: %s' % s);
45+
46+
update_html(filepath, soup)
47+
48+
49+
def process_fn(soup, anchor, python_signature, symbol):
50+
try:
51+
r = anchor.find_next_sibling(class_='memitem').find(class_='memproto').find('table')
52+
insert_python_fn_signature(soup, r, python_signature, symbol)
53+
except:
54+
logging.error("Can't process: %s" % symbol)
55+
traceback.print_exc()
56+
pprint(anchor)
57+
58+
59+
def process_const(soup, anchor, python_signature, symbol):
60+
try:
61+
#pprint(anchor.parent)
62+
description = append(soup.new_tag('div', **{'class' : ['python_language']}),
63+
'Python: ' + python_signature[0]['name'])
64+
old = anchor.find_next_sibling('div', class_='python_language')
65+
if old is None:
66+
anchor.parent.append(description)
67+
else:
68+
old.replace_with(description)
69+
#pprint(anchor.parent)
70+
except:
71+
logging.error("Can't process: %s" % symbol)
72+
traceback.print_exc()
73+
pprint(anchor)
74+
75+
76+
def insert_python_fn_signature(soup, table, variants, symbol):
77+
description = create_python_fn_description(soup, variants)
78+
description['class'] = 'python_language'
79+
soup = insert_or_replace(table, description, 'table', 'python_language')
80+
return soup
81+
82+
83+
def create_python_fn_description(soup, variants):
84+
language = 'Python:'
85+
table = soup.new_tag('table')
86+
heading_row = soup.new_tag('th')
87+
table.append(
88+
append(soup.new_tag('tr'),
89+
append(soup.new_tag('th', colspan=999, style="text-align:left"), language)))
90+
for v in variants:
91+
#logging.debug(v)
92+
add_signature_to_table(soup, table, v, language, type)
93+
#print(table)
94+
return table
95+
96+
97+
def add_signature_to_table(soup, table, signature, language, type):
98+
""" Add a signature to an html table"""
99+
row = soup.new_tag('tr')
100+
row.append(soup.new_tag('td', style='width: 20px;'))
101+
102+
if 'ret' in signature:
103+
row.append(append(soup.new_tag('td'), signature['ret']))
104+
row.append(append(soup.new_tag('td'), '='))
105+
else:
106+
row.append(soup.new_tag('td')) # return values
107+
row.append(soup.new_tag('td')) # '='
108+
109+
row.append(append(soup.new_tag('td'), signature['name'] + '('))
110+
row.append(append(soup.new_tag('td', **{'class': 'paramname'}), signature['arg']))
111+
row.append(append(soup.new_tag('td'), ')'))
112+
table.append(row)
113+
114+
115+
def append(target, obj):
116+
target.append(obj)
117+
return target
118+
119+
120+
def insert_or_replace(element_before, new_element, tag, tag_class):
121+
old = element_before.find_next_sibling(tag, class_=tag_class)
122+
if old is None:
123+
element_before.insert_after(new_element)
124+
else:
125+
old.replace_with(new_element)

modules/python/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# CMake file for python support
33
# ----------------------------------------------------------------------------
44

5+
add_subdirectory(bindings)
6+
57
if(ANDROID OR APPLE_FRAMEWORK OR WINRT)
68
set(__disable_python2 ON)
79
set(__disable_python3 ON)

0 commit comments

Comments
 (0)