Commenting spree no.2
This commit is contained in:
parent
bfa88384c7
commit
fb41d7bf4e
@ -1,4 +1,4 @@
|
|||||||
#/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
def substitute(build, output_dir):
|
def substitute(build, output_dir):
|
||||||
|
|
||||||
@ -18,12 +18,32 @@ def substitute(build, output_dir):
|
|||||||
if not os.path.isdir(output_dir):
|
if not os.path.isdir(output_dir):
|
||||||
os.mkdir(output_dir)
|
os.mkdir(output_dir)
|
||||||
|
|
||||||
# populate templates
|
# populate template
|
||||||
populated = template.render(build=build)
|
populated = template.render(build=build)
|
||||||
with open(os.path.join(output_dir, 'buildInformation.m'), 'wb') as f:
|
with open(os.path.join(output_dir, 'buildInformation.m'), 'wb') as f:
|
||||||
f.write(populated)
|
f.write(populated)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Usage: python build_info.py --os os_version_string
|
||||||
|
--arch [bitness processor]
|
||||||
|
--compiler [id version]
|
||||||
|
--mex_arch arch_string
|
||||||
|
--mex_script /path/to/mex/script
|
||||||
|
--cxx_flags [-list -of -flags -to -passthrough]
|
||||||
|
--opencv_version version_string
|
||||||
|
--commit commit_hash_if_using_git
|
||||||
|
--modules [core imgproc highgui etc]
|
||||||
|
--configuration Debug/Release
|
||||||
|
--outdir /path/to/write/build/info
|
||||||
|
|
||||||
|
build_info.py generates a Matlab function that can be invoked with a call to
|
||||||
|
>> cv.buildInformation();
|
||||||
|
|
||||||
|
This function prints a summary of the user's OS, OpenCV and Matlab build
|
||||||
|
given the information passed to this module. build_info.py invokes Jinja2
|
||||||
|
on the template_build_info.m template.
|
||||||
|
"""
|
||||||
|
|
||||||
# parse the input options
|
# parse the input options
|
||||||
import sys, re, os
|
import sys, re, os
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
from string import split, join
|
from string import split, join
|
||||||
import re, os
|
import re, os
|
||||||
|
# precompile a URL matching regular expression
|
||||||
urlexpr = re.compile(r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE|re.UNICODE)
|
urlexpr = re.compile(r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE|re.UNICODE)
|
||||||
|
|
||||||
def inputs(args):
|
def inputs(args):
|
||||||
@ -35,9 +36,11 @@ def only(args):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def void(arg):
|
def void(arg):
|
||||||
|
'''Is the input 'void' '''
|
||||||
return arg == 'void'
|
return arg == 'void'
|
||||||
|
|
||||||
def flip(arg):
|
def flip(arg):
|
||||||
|
'''flip the sign of the input'''
|
||||||
return not arg
|
return not arg
|
||||||
|
|
||||||
def noutputs(fun):
|
def noutputs(fun):
|
||||||
@ -45,6 +48,7 @@ def noutputs(fun):
|
|||||||
return int(not void(fun.rtp)) + len(outputs(fun.req)) + len(outputs(fun.opt))
|
return int(not void(fun.rtp)) + len(outputs(fun.req)) + len(outputs(fun.opt))
|
||||||
|
|
||||||
def convertibleToInt(string):
|
def convertibleToInt(string):
|
||||||
|
'''Can the input string be evaluated to an integer?'''
|
||||||
salt = '1+'
|
salt = '1+'
|
||||||
try:
|
try:
|
||||||
exec(salt+string)
|
exec(salt+string)
|
||||||
@ -53,12 +57,21 @@ def convertibleToInt(string):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def binaryToDecimal(string):
|
def binaryToDecimal(string):
|
||||||
|
'''Attempt to convert the input string to floating point representation'''
|
||||||
try:
|
try:
|
||||||
return str(eval(string))
|
return str(eval(string))
|
||||||
except:
|
except:
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def formatMatlabConstant(string, table):
|
def formatMatlabConstant(string, table):
|
||||||
|
'''
|
||||||
|
Given a string representing a Constant, and a table of all Constants,
|
||||||
|
attempt to resolve the Constant into a valid Matlab expression
|
||||||
|
For example, the input
|
||||||
|
DEPENDENT_VALUE = 1 << FIXED_VALUE
|
||||||
|
needs to be converted to
|
||||||
|
DEPENDENT_VALUE = bitshift(1, cv.FIXED_VALUE);
|
||||||
|
'''
|
||||||
# split the string into expressions
|
# split the string into expressions
|
||||||
words = re.split('(\W+)', string)
|
words = re.split('(\W+)', string)
|
||||||
# add a 'cv' prefix if an expression is also a key in the lookup table
|
# add a 'cv' prefix if an expression is also a key in the lookup table
|
||||||
@ -76,20 +89,33 @@ def matlabURL(string):
|
|||||||
return re.sub(urlexpr, '<a href="matlab: web(\'\\1\', \'-browser\')">\\1</a>', string)
|
return re.sub(urlexpr, '<a href="matlab: web(\'\\1\', \'-browser\')">\\1</a>', string)
|
||||||
|
|
||||||
def capitalizeFirst(text):
|
def capitalizeFirst(text):
|
||||||
|
'''Capitalize only the first character of the text string'''
|
||||||
return text[0].upper() + text[1:]
|
return text[0].upper() + text[1:]
|
||||||
|
|
||||||
def toUpperCamelCase(text):
|
def toUpperCamelCase(text):
|
||||||
|
'''variable_name --> VariableName'''
|
||||||
return ''.join([capitalizeFirst(word) for word in text.split('_')])
|
return ''.join([capitalizeFirst(word) for word in text.split('_')])
|
||||||
|
|
||||||
def toLowerCamelCase(text):
|
def toLowerCamelCase(text):
|
||||||
|
'''variable_name --> variableName'''
|
||||||
upper_camel = toUpperCamelCase(test)
|
upper_camel = toUpperCamelCase(test)
|
||||||
return upper_camel[0].lower() + upper_camel[1:]
|
return upper_camel[0].lower() + upper_camel[1:]
|
||||||
|
|
||||||
def toUnderCase(text):
|
def toUnderCase(text):
|
||||||
|
'''VariableName --> variable_name'''
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
|
||||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||||
|
|
||||||
def stripTags(text):
|
def stripTags(text):
|
||||||
|
'''
|
||||||
|
strip or convert html tags from a text string
|
||||||
|
<code>content</code> --> content
|
||||||
|
<anything> --> ''
|
||||||
|
< --> <
|
||||||
|
> --> >
|
||||||
|
&le --> <=
|
||||||
|
&ge --> >=
|
||||||
|
'''
|
||||||
upper = lambda pattern: pattern.group(1).upper()
|
upper = lambda pattern: pattern.group(1).upper()
|
||||||
text = re.sub('<code>(.*?)</code>', upper, text)
|
text = re.sub('<code>(.*?)</code>', upper, text)
|
||||||
text = re.sub('<([^=\s].*?)>', '', text)
|
text = re.sub('<([^=\s].*?)>', '', text)
|
||||||
@ -100,18 +126,26 @@ def stripTags(text):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
def qualify(text, name):
|
def qualify(text, name):
|
||||||
|
'''Adds uppercase 'CV.' qualification to any occurrences of name in text'''
|
||||||
return re.sub(name.upper(), 'CV.'+name.upper(), text)
|
return re.sub(name.upper(), 'CV.'+name.upper(), text)
|
||||||
|
|
||||||
def slugify(text):
|
def slugify(text):
|
||||||
|
'''A_Function_name --> a-function-name'''
|
||||||
return text.lower().replace('_', '-')
|
return text.lower().replace('_', '-')
|
||||||
|
|
||||||
def filename(fullpath):
|
def filename(fullpath):
|
||||||
|
'''Returns only the filename without an extension from a file path
|
||||||
|
eg. /path/to/file.txt --> file
|
||||||
|
'''
|
||||||
return os.path.splitext(os.path.basename(fullpath))[0]
|
return os.path.splitext(os.path.basename(fullpath))[0]
|
||||||
|
|
||||||
def csv(items, sep=', '):
|
def csv(items, sep=', '):
|
||||||
|
'''format a list with a separator (comma if not specified)'''
|
||||||
return sep.join(item for item in items)
|
return sep.join(item for item in items)
|
||||||
|
|
||||||
def stripExtraSpaces(text):
|
def stripExtraSpaces(text):
|
||||||
|
'''Removes superfluous whitespace from a string, including the removal
|
||||||
|
of all leading and trailing whitespace'''
|
||||||
return ' '.join(text.split())
|
return ' '.join(text.split())
|
||||||
|
|
||||||
def comment(text, wrap=80, escape='% ', escape_first='', escape_last=''):
|
def comment(text, wrap=80, escape='% ', escape_first='', escape_last=''):
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
#/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
class MatlabWrapperGenerator(object):
|
class MatlabWrapperGenerator(object):
|
||||||
|
"""
|
||||||
|
MatlabWrapperGenerator is a class for generating Matlab mex sources from
|
||||||
|
a set of C++ headers. MatlabWrapperGenerator objects can be default
|
||||||
|
constructed. Given an instance, the gen() method performs the translation.
|
||||||
|
"""
|
||||||
|
|
||||||
def gen(self, module_root, modules, extras, output_dir):
|
def gen(self, module_root, modules, extras, output_dir):
|
||||||
|
"""
|
||||||
|
Generate a set of Matlab mex source files by parsing exported symbols
|
||||||
|
in a set of C++ headers. The headers can be input in one (or both) of
|
||||||
|
two methods:
|
||||||
|
1. specify module_root and modules
|
||||||
|
Given a path to the OpenCV module root and a list of module names,
|
||||||
|
the headers to parse are implicitly constructed.
|
||||||
|
2. specifiy header locations explicitly in extras
|
||||||
|
Each element in the list of extras must be of the form:
|
||||||
|
'namespace=/full/path/to/extra/header.hpp' where 'namespace' is
|
||||||
|
the namespace in which the definitions should be added.
|
||||||
|
The output_dir specifies the directory to write the generated sources
|
||||||
|
to.
|
||||||
|
"""
|
||||||
# parse each of the files and store in a dictionary
|
# parse each of the files and store in a dictionary
|
||||||
# as a separate "namespace"
|
# as a separate "namespace"
|
||||||
parser = CppHeaderParser()
|
parser = CppHeaderParser()
|
||||||
@ -109,8 +128,41 @@ class MatlabWrapperGenerator(object):
|
|||||||
f.write(populated)
|
f.write(populated)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Usage: python gen_matlab.py --hdrparser /path/to/hdr_parser/dir
|
||||||
|
--rstparser /path/to/rst_parser/dir
|
||||||
|
--moduleroot /path/to/opencv/modules
|
||||||
|
--modules [core imgproc objdetect etc]
|
||||||
|
--extra namespace=/path/to/extra/header.hpp
|
||||||
|
--outdir /path/to/output/generated/srcs
|
||||||
|
|
||||||
|
gen_matlab.py is the main control script for generating matlab source
|
||||||
|
files from given set of headers. Internally, gen_matlab:
|
||||||
|
1. constructs the headers to parse from the module root and list of modules
|
||||||
|
2. parses the headers using CppHeaderParser
|
||||||
|
3. refactors the definitions using ParseTree
|
||||||
|
4. parses .rst docs using RstParser
|
||||||
|
5. populates the templates for classes, function, enums and docs from the
|
||||||
|
definitions
|
||||||
|
|
||||||
|
gen_matlab.py requires the following inputs:
|
||||||
|
--hdrparser the path to the header parser directory
|
||||||
|
(opencv/modules/python/src2)
|
||||||
|
--rstparser the path to the rst parser directory
|
||||||
|
(opencv/modules/java/generator)
|
||||||
|
--moduleroot (optional) path to the opencv directory containing the modules
|
||||||
|
--modules (optional - required if --moduleroot specified) the modules
|
||||||
|
to produce bindings for. The path to the include directories
|
||||||
|
as well as the namespaces are constructed from the modules
|
||||||
|
and the moduleroot
|
||||||
|
--extra extra headers explicitly defined to parse. This must be in
|
||||||
|
the format "namepsace=/path/to/extra/header.hpp". For example,
|
||||||
|
the core module requires the extra header:
|
||||||
|
"core=/opencv/modules/core/include/opencv2/core/core/base.hpp"
|
||||||
|
--outdir the output directory to put the generated matlab sources. In
|
||||||
|
the OpenCV build this is "${CMAKE_CURRENT_BUILD_DIR}/src"
|
||||||
|
"""
|
||||||
|
|
||||||
# parse the input options
|
# parse the input options
|
||||||
import sys, re, os, time
|
import sys, re, os, time
|
||||||
|
@ -3,6 +3,73 @@ from textwrap import fill
|
|||||||
from filters import *
|
from filters import *
|
||||||
|
|
||||||
class ParseTree(object):
|
class ParseTree(object):
|
||||||
|
"""
|
||||||
|
The ParseTree class produces a semantic tree of C++ definitions given
|
||||||
|
the output of the CppHeaderParser (from opencv/modules/python/src2/hdr_parser.py)
|
||||||
|
|
||||||
|
The full hierarchy is as follows:
|
||||||
|
|
||||||
|
Namespaces
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- Classes
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- Methods
|
||||||
|
|- Constants
|
||||||
|
|- Methods
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- static (T/F)
|
||||||
|
|- return type
|
||||||
|
|- required Arguments
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- const (T/F)
|
||||||
|
|- reference ('&'/'*')
|
||||||
|
|- type
|
||||||
|
|- input
|
||||||
|
|- output (pass return by reference)
|
||||||
|
|- default value
|
||||||
|
|- optional Arguments
|
||||||
|
|- Constants
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- const (T/F)
|
||||||
|
|- reference ('&'/'*')
|
||||||
|
|- type
|
||||||
|
|- value
|
||||||
|
|
||||||
|
The semantic tree contains substantial information for easily introspecting
|
||||||
|
information about objects. How many methods does the 'core' namespace have?
|
||||||
|
Does the 'randn' method have any return by reference (output) arguments?
|
||||||
|
How many required and optional arguments does the 'add' method have? Is the
|
||||||
|
variable passed by reference or raw pointer?
|
||||||
|
|
||||||
|
Individual definitions from the parse tree (Classes, Functions, Constants)
|
||||||
|
are passed to the Jinja2 template engine where they are manipulated to
|
||||||
|
produce Matlab mex sources.
|
||||||
|
|
||||||
|
A common call tree for constructing and using a ParseTree object is:
|
||||||
|
|
||||||
|
# parse a set of definitions into a dictionary of namespaces
|
||||||
|
parser = CppHeaderParser()
|
||||||
|
ns['core'] = parser.parse('path/to/opencv/core.hpp')
|
||||||
|
|
||||||
|
# refactor into a semantic tree
|
||||||
|
parse_tree = ParseTree()
|
||||||
|
parse_tree.build(ns)
|
||||||
|
|
||||||
|
# iterate over the tree
|
||||||
|
for namespace in parse_tree.namespaces:
|
||||||
|
for clss in namespace.classes:
|
||||||
|
# do stuff
|
||||||
|
for method in namespace.methods:
|
||||||
|
# do stuff
|
||||||
|
|
||||||
|
Calling 'print' on a ParseTree object will reconstruct the definitions
|
||||||
|
to produce an output resembling the original C++ code.
|
||||||
|
"""
|
||||||
def __init__(self, namespaces=None):
|
def __init__(self, namespaces=None):
|
||||||
self.namespaces = namespaces if namespaces else []
|
self.namespaces = namespaces if namespaces else []
|
||||||
|
|
||||||
@ -48,6 +115,15 @@ class ParseTree(object):
|
|||||||
|
|
||||||
|
|
||||||
class Translator(object):
|
class Translator(object):
|
||||||
|
"""
|
||||||
|
The Translator class does the heavy lifting of translating the nested
|
||||||
|
list representation of the hdr_parser into individual definitions that
|
||||||
|
are inserted into the ParseTree.
|
||||||
|
Translator consists of a top-level method: translate()
|
||||||
|
along with a number of helper methods: translateClass(), translateMethod(),
|
||||||
|
translateArgument(), translateConstant(), translateName(), and
|
||||||
|
translateClassName()
|
||||||
|
"""
|
||||||
def translate(self, defn):
|
def translate(self, defn):
|
||||||
# --- class ---
|
# --- class ---
|
||||||
# classes have 'class' prefixed on their name
|
# classes have 'class' prefixed on their name
|
||||||
@ -116,6 +192,14 @@ class Translator(object):
|
|||||||
|
|
||||||
|
|
||||||
class Namespace(object):
|
class Namespace(object):
|
||||||
|
"""
|
||||||
|
Namespace
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- Constants
|
||||||
|
|- Methods
|
||||||
|
|- Constants
|
||||||
|
"""
|
||||||
def __init__(self, name='', constants=None, classes=None, methods=None):
|
def __init__(self, name='', constants=None, classes=None, methods=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.constants = constants if constants else []
|
self.constants = constants if constants else []
|
||||||
@ -129,6 +213,13 @@ class Namespace(object):
|
|||||||
(join((o.__str__() for o in self.classes), '\n\n') if self.classes else '')+'\n};'
|
(join((o.__str__() for o in self.classes), '\n\n') if self.classes else '')+'\n};'
|
||||||
|
|
||||||
class Class(object):
|
class Class(object):
|
||||||
|
"""
|
||||||
|
Class
|
||||||
|
|
|
||||||
|
|- name
|
||||||
|
|- Methods
|
||||||
|
|- Constants
|
||||||
|
"""
|
||||||
def __init__(self, name='', namespace='', constants=None, methods=None):
|
def __init__(self, name='', namespace='', constants=None, methods=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
@ -141,6 +232,21 @@ class Class(object):
|
|||||||
(join((f.__str__() for f in self.methods), '\n\t') if self.methods else '')+'\n};'
|
(join((f.__str__() for f in self.methods), '\n\t') if self.methods else '')+'\n};'
|
||||||
|
|
||||||
class Method(object):
|
class Method(object):
|
||||||
|
"""
|
||||||
|
Method
|
||||||
|
int VideoWriter::read( cv::Mat& frame, const cv::Mat& mask=cv::Mat() );
|
||||||
|
--- ----- ---- -------- ----------------
|
||||||
|
rtp class name required optional
|
||||||
|
|
||||||
|
name the method name
|
||||||
|
clss the class the method belongs to ('' if free)
|
||||||
|
static static?
|
||||||
|
namespace the namespace the method belongs to ('' if free)
|
||||||
|
rtp the return type
|
||||||
|
const const?
|
||||||
|
req list of required arguments
|
||||||
|
opt list of optional arguments
|
||||||
|
"""
|
||||||
def __init__(self, name='', clss='', static=False, namespace='', rtp='', const=False, req=None, opt=None):
|
def __init__(self, name='', clss='', static=False, namespace='', rtp='', const=False, req=None, opt=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.clss = clss
|
self.clss = clss
|
||||||
@ -158,6 +264,20 @@ class Method(object):
|
|||||||
')'+(' const' if self.const else '')+';'
|
')'+(' const' if self.const else '')+';'
|
||||||
|
|
||||||
class Argument(object):
|
class Argument(object):
|
||||||
|
"""
|
||||||
|
Argument
|
||||||
|
const cv::Mat& mask=cv::Mat()
|
||||||
|
----- ---- --- ---- -------
|
||||||
|
const tp ref name default
|
||||||
|
|
||||||
|
name the argument name
|
||||||
|
tp the argument type
|
||||||
|
const const?
|
||||||
|
I is the argument treated as an input?
|
||||||
|
O is the argument treated as an output (return by reference)
|
||||||
|
ref is the argument passed by reference? ('*'/'&')
|
||||||
|
default the default value of the argument ('' if required)
|
||||||
|
"""
|
||||||
def __init__(self, name='', tp='', const=False, I=True, O=False, ref='', default=''):
|
def __init__(self, name='', tp='', const=False, I=True, O=False, ref='', default=''):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.tp = tp
|
self.tp = tp
|
||||||
@ -172,6 +292,19 @@ class Argument(object):
|
|||||||
' '+self.name+('='+self.default if self.default else '')
|
' '+self.name+('='+self.default if self.default else '')
|
||||||
|
|
||||||
class Constant(object):
|
class Constant(object):
|
||||||
|
"""
|
||||||
|
Constant
|
||||||
|
DFT_COMPLEX_OUTPUT = 12;
|
||||||
|
---- -------
|
||||||
|
name default
|
||||||
|
|
||||||
|
name the name of the constant
|
||||||
|
clss the class that the constant belongs to ('' if free)
|
||||||
|
tp the type of the constant ('' if int)
|
||||||
|
const const?
|
||||||
|
ref is the constant a reference? ('*'/'&')
|
||||||
|
default default value, required for constants
|
||||||
|
"""
|
||||||
def __init__(self, name='', clss='', tp='', const=False, ref='', default=''):
|
def __init__(self, name='', clss='', tp='', const=False, ref='', default=''):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.clss = clss
|
self.clss = clss
|
||||||
@ -185,6 +318,10 @@ class Constant(object):
|
|||||||
' '+self.name+('='+self.default if self.default else '')+';'
|
' '+self.name+('='+self.default if self.default else '')+';'
|
||||||
|
|
||||||
def constants(tree):
|
def constants(tree):
|
||||||
|
"""
|
||||||
|
recursive generator to strip all Constant objects from the ParseTree
|
||||||
|
and place them into a flat dictionary of { name, value (default) }
|
||||||
|
"""
|
||||||
if isinstance(tree, dict) and 'constants' in tree and isinstance(tree['constants'], list):
|
if isinstance(tree, dict) and 'constants' in tree and isinstance(tree['constants'], list):
|
||||||
for node in tree['constants']:
|
for node in tree['constants']:
|
||||||
yield (node['name'], node['default'])
|
yield (node['name'], node['default'])
|
||||||
@ -198,6 +335,10 @@ def constants(tree):
|
|||||||
yield gen
|
yield gen
|
||||||
|
|
||||||
def todict(obj, classkey=None):
|
def todict(obj, classkey=None):
|
||||||
|
"""
|
||||||
|
Convert the ParseTree to a dictionary, stripping all objects of their
|
||||||
|
methods and converting class names to strings
|
||||||
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
for k in obj.keys():
|
for k in obj.keys():
|
||||||
obj[k] = todict(obj[k], classkey)
|
obj[k] = todict(obj[k], classkey)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user