Skip to content

Commit

Permalink
Merge pull request #20 from mottosso/MFnDagNode
Browse files Browse the repository at this point in the history
MFnDagNode
  • Loading branch information
mottosso committed Jun 6, 2021
2 parents b2bdf3f + 18ed4c9 commit 8345f79
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 110 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,5 @@
*.pyd
build/**
tmp/*
*.pyc
*.pyc
MFn.Types.inl
180 changes: 104 additions & 76 deletions scripts/parse_header.py
@@ -1,64 +1,65 @@
#/usr/bin/python
#!/usr/bin/python

"""Parse Header.
This script generate a Pythong binding stub from a Maya devkit header.
"""

import re
import os
import logging
import argparse
import collections
import logging
import re
import os
import sys
import textwrap

import maya.api.OpenMaya
import maya.api.OpenMayaAnim
import maya.api.OpenMayaRender
import maya.api.OpenMayaUI
from maya.api import (
OpenMaya,
OpenMayaUI,
OpenMayaAnim,
OpenMayaRender,
)

log = logging.getLogger('parse_header')
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
log.handlers.append(handler)
log.setLevel(logging.INFO)

LOG = logging.getLogger('parse_header')
LOG.setLevel(logging.INFO)
logging.basicConfig()

TEMP_DOCSTRING = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
TEMP_DOCSTRING = (
'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
)

IGNORED_METHODS = [
IGNORED_METHODS = (
'className',
]
)

IGNORED_CPP_TOKENS = [
IGNORED_CPP_TOKENS = (
'*',
'&',
'const',
]
)

TEMPLATE_STR = (
"""\
TEMPLATE_STR = """\
py::class_<M{class_name}>(m, "{class_name}")
{class_body}\
"""
)

INSTANCE_METHOD = (
"""\
INSTANCE_METHOD = """\
.def("{method_name}", []({class_name} & self{arguments}){return_type} {{
throw std::logic_error{{"Function not yet implemented."}};
}}, R"pbdoc({doctstring})pbdoc")\
"""
)

STATIC_METHOD = (
"""\
STATIC_METHOD = """\
.def_static("{method_name}", []({arguments}){return_type} {{
throw std::logic_error{{"Function not yet implemented."}};
}}, R"pbdoc({doctstring})pbdoc")\
"""
)


def parse_header(header_name):
"""Parse the given header.
Args:
header_name (str): Name of the header to parse (eg, 'MDagPath')
"""
Expand All @@ -68,8 +69,11 @@ def parse_header(header_name):
else:
class_name = header_name
header_name = '{}.h'.format(header_name)

header_path = os.path.join(os.environ['DEVKIT_LOCATION'], 'include', 'maya', header_name)

header_path = os.path.join(
os.environ['DEVKIT_LOCATION'],
'include', 'maya', header_name
)

if not os.path.exists(header_path):
raise LookupError("No '{}' header in the devkit.".format(header_name))
Expand All @@ -85,13 +89,14 @@ def parse_header(header_name):
m_class = find_maya_class(class_name)

for (signature, arguments) in filter_function_lines(lines):
method_name, arguments, return_type = parse_method(signature, arguments)
method_name, arguments, return_type = parse_method(signature,
arguments)

if method_name in IGNORED_METHODS:
continue

if not hasattr(m_class, method_name):
continue
continue

if signature.startswith('static'):
method_str_fmt = STATIC_METHOD
Expand Down Expand Up @@ -123,22 +128,27 @@ def parse_header(header_name):

code_str = (
TEMPLATE_STR.format(
class_name=class_name[1:],
class_body=class_body_str
class_name=class_name[1:],
class_body=class_body_str
)
)

tmp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'tmp')
out_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "src"
)
out_dir = os.path.abspath(out_dir)

file_name = '{}.inl'.format(class_name)
file_path = os.path.join(tmp_dir, file_name)
file_path = os.path.join(out_dir, file_name)

if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
if os.path.exists(file_path):
raise OSError("File '%s' already exist" % file_path)

with open(file_path, 'w') as fp:
fp.write(code_str)

log.info("Successfully generated '%s'" % file_path)


def parse_method(signature, arguments):
"""Parse the given method components.
Expand All @@ -156,31 +166,34 @@ def parse_method(signature, arguments):

signature_list = signature.split()

method_name = None
method_name = None
return_type = None

if len(signature_list) == 1:
method_name, = signature_list
if len(signature_list) == 1:
method_name, = signature_list

elif len(signature_list) == 2:
if signature_list[0] == 'virtual':
method_name = signature_list[-1][1:]
else:
else:
return_type, method_name = signature_list

elif len(signature_list) == 3:
__, return_type, method_name = signature_list
elif len(signature_list) == 4:
__, __, return_type, method_name = signature_list
else:
raise RuntimeError("Cannot parse signature: {}".format(signature))

in_args, out_args = parse_arguments(argument_list, has_outs=return_type=='MStatus')
in_args, out_args = parse_arguments(
argument_list, has_outs=return_type == 'MStatus')

if out_args:
if len(out_args) == 1:
return_type, = out_args
else:
return_type = 'std::tuple<{}>'.format(', '.join(out_args))

if return_type == 'MStatus':
return_type = ''
else:
Expand All @@ -194,7 +207,8 @@ def parse_arguments(argument_list, has_outs):
Args:
argument_list (list[str]): C++ argument strings.
has_outs (bool): True if the method has an out argument (mutable value passed by reference).
has_outs (bool): True if the method has an out argument
(mutable value passed by reference).
Returns:
tuple[list]
Expand All @@ -212,6 +226,7 @@ def parse_arguments(argument_list, has_outs):
'MStatus * ReturnStatus = NULL',
'MStatus *ReturnStatus = nullptr',
'MStatus *ReturnStatus = NULL',
'MStatus ReturnStatus = NULL',
]:
continue

Expand All @@ -223,7 +238,7 @@ def parse_arguments(argument_list, has_outs):
if has_outs:
if arg == 'unsigned int':
pass
else:
else:
try:
arg, __ = arg.split(' ')
except ValueError:
Expand All @@ -234,8 +249,8 @@ def parse_arguments(argument_list, has_outs):
in_args.append(arg)
else:
in_args.append(arg)
return in_args, out_args

return in_args, out_args


def sanitize_argument(arg_str):
Expand All @@ -245,23 +260,24 @@ def sanitize_argument(arg_str):
parts = [p for p in parts if p not in IGNORED_CPP_TOKENS]

for i, _ in enumerate(parts):
parts[i] = ''.join([c for c in parts[i] if c not in IGNORED_CPP_TOKENS])
parts[i] = ''.join([
c for c in parts[i]
if c not in IGNORED_CPP_TOKENS
])

return ' '.join(parts)


def find_maya_class(class_name):
"""Return the maya.api class (for docstrings)."""

for module in [
maya.api.OpenMaya,
maya.api.OpenMayaAnim,
maya.api.OpenMayaRender,
maya.api.OpenMayaUI
]:
for module in (OpenMaya,
OpenMayaAnim,
OpenMayaRender,
OpenMayaUI):
if hasattr(module, class_name):
return getattr(module, class_name)

return type('NoModule', (), {'__doc__': 'TODO: Add docstring'})


Expand Down Expand Up @@ -298,19 +314,19 @@ def filter_header_lines(class_name, lines):
line = line.strip()

if line == 'BEGIN_NO_SCRIPT_SUPPORT:':
no_script = True
continue
no_script = True
continue

if line == 'END_NO_SCRIPT_SUPPORT:':
no_script = False
continue
no_script = False
continue

if no_script:
continue

if line.startswith('OPENMAYA_DEPRECATED'):
if line.startswith('OPENMAYA_DEPRECATED'):
skip_next_statement = True
continue
continue

try:
# Remove trailing comments
Expand All @@ -319,18 +335,18 @@ def filter_header_lines(class_name, lines):
pass

if not in_class_definition:
in_class_definition = class_def_re.match(line) is not None
in_class_definition = class_def_re.match(line) is not None

if in_class_definition:
statements.append(line)

if line.endswith(','):
continue
continue

statement = ' '.join(statements)

if not _is_complete_statement(statement):
continue
continue

if skip_next_statement:
skip_next_statement = False
Expand All @@ -345,20 +361,32 @@ def filter_header_lines(class_name, lines):

def _is_complete_statement(statement):
if '(' in statement:
return '(' in statement and ')' in statement and statement.endswith(';')
else:
return True
return (
'(' in statement and
')' in statement and
statement.endswith(';')
)

return True

def main(*argv):
"""Parse the given headers."""

for name in argv:
try:
parse_header(name)
except LookupError as e:
LOG.error(str(e))
def main():
"""Parse the given header"""

parser = argparse.ArgumentParser()
parser.add_argument("header", help="Name of Maya header, e.g. MPlug.h")

opts = parser.parse_args()

try:
parse_header(opts.header)

except OSError as e:
log.error(str(e))

except LookupError as e:
log.error(str(e))


if __name__ == '__main__':
main(*sys.argv[1:])
main()

0 comments on commit 8345f79

Please sign in to comment.