view CodeAnalysis/GenerateOrthancSDK.py @ 180:ddf3e987827f java-code-model

created Python documentation for the Orthanc interface
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 02 Jul 2024 12:30:16 +0200
parents f49864df6f1f
children faaa3fec799a
line wrap: on
line source

#!/usr/bin/env python3

##
## Python plugin for Orthanc
## Copyright (C) 2020-2023 Osimis S.A., Belgium
## Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
## Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
##
## This program is free software: you can redistribute it and/or
## modify it under the terms of the GNU Affero General Public License
## as published by the Free Software Foundation, either version 3 of
## the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
## Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
##


import argparse
import json
import os
import sys
import pystache

ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))


##
## Parse the command-line arguments
##

parser = argparse.ArgumentParser(description = 'Generate Python code to wrap the Orthanc SDK.')
parser.add_argument('--model',
                    default = os.path.join(os.path.dirname(__file__),
                                           '../Resources/Orthanc/Sdk-1.10.0/CodeModel.json'),
                    help = 'Input code model, as generated by the orthanc-java project')
parser.add_argument('--target',
                    default = os.path.join(os.path.dirname(__file__),
                                           '../Sources/Autogenerated'),
                    help = 'Target folder')

args = parser.parse_args()



##
## Configuration of the custom primitives that are manually
## implemented (not autogenerated)
##

CUSTOM_FUNCTIONS = [
    {
        'c_function' : 'OrthancPluginCreateMemoryBuffer',
        'args' : [
            {
                'name' : 'arg0',
                'sdk_name' : 'size',
                'sdk_type' : 'uint32_t',
            }
        ],
        'documentation' : {
            'args' : {
                'size' : 'Size of the memory buffer to be created',
            },
            'return' : 'The newly allocated memory buffer',
            'description' : [ 'Create a new memory buffer managed by the Orthanc core' ],
        },
        'return_sdk_type' : 'OrthancPluginMemoryBuffer *',
    }
]


CUSTOM_METHODS = {
    'OrthancPluginFindQuery' : [
        {
            'method_name' : 'GetFindQueryTagGroup',
            'implementation' : 'GetFindQueryTagGroup',
            'sdk_function' : 'OrthancPluginGetFindQueryTag',
        },
        {
            'method_name' : 'GetFindQueryTagElement',
            'implementation' : 'GetFindQueryTagElement',
            'sdk_function' : 'OrthancPluginGetFindQueryTag',
        },
    ],

    'OrthancPluginDicomInstance' : [
        {
            'method_name' : 'GetInstanceData',
            'implementation' : 'GetInstanceData',
            'sdk_function' : 'OrthancPluginGetInstanceData',
        },
    ],

    'OrthancPluginImage' : [
        {
            'method_name' : 'GetImageBuffer',
            'implementation' : 'GetImageBuffer',
            'sdk_function' : 'OrthancPluginGetImageBuffer',
        }
    ],
}



TARGET = os.path.realpath(args.target)


partials = {}

with open(os.path.join(ROOT, 'FunctionBody.mustache'), 'r') as f:
    partials['function_body'] = f.read()

renderer = pystache.Renderer(
    escape = lambda u: u,  # No escaping
    partials = partials,
)



with open(args.model, 'r') as f:
    model = json.loads(f.read())


def ToUpperCase(name):
    s = ''
    for i in range(len(name)):
        if name[i].isupper():
            if len(s) == 0:
                s += name[i]
            elif name[i - 1].islower():
                s += '_' + name[i]
            elif (i + 1 < len(name) and
                  name[i - 1].islower() and
                  name[i + 1].isupper()):
                s += '_' + name[i]
            else:
                s += name[i]
        else:
            s += name[i].upper()
    return s


def ToLowerCase(name):
    s = ''
    for i in range(len(name)):
        if (name[i].isupper() and
            len(s) != 0):
            s += '_'
        s += name[i].lower()
    return s


def GetShortName(name):
    if not name.startswith('OrthancPlugin'):
        raise Exception()
    else:
        return name[len('OrthancPlugin'):]



ORTHANC_TO_PYTHON_NUMERIC_TYPES = {
    # https://docs.python.org/3/c-api/arg.html#numbers
    # https://en.wikipedia.org/wiki/C_data_types
    'uint8_t' : {
        'type' : 'unsigned char',
        'format' : 'b',
        },
    'int32_t' : {
        'type' : 'long int',
        'format' : 'l',
        },
    'uint16_t' : {
        'type' : 'unsigned short',
        'format' : 'H',
        },
    'uint32_t' : {
        'type' : 'unsigned long',
        'format' : 'k',
        },
    'uint64_t' : {
        'type' : 'unsigned long long',
        'format' : 'K',
        },
    'float' : {
        'type' : 'float',
        'format' : 'f',
        }
    }


def FormatFunction(f):
    answer = {
        'c_function' : f['c_function'],
        'short_name' : GetShortName(f['c_function']),
        'has_args' : len(f['args']) > 0,
        'count_args' : len(f['args']),
    }

    allow_threads = True
    tuple_format = ''
    tuple_target = []
    call_args = []
    args = []

    for arg in f['args']:
        # https://docs.python.org/3/c-api/arg.html
        if arg['sdk_type'] in [ 'const void *', 'const_void_pointer_with_size' ]:
            args.append({
                'name' : arg['name'],
                'python_type' : 'Py_buffer',
                'release' : 'PyBuffer_Release(&%s);' % arg['name'],
            })
            tuple_format += 's*'
            allow_threads = False
        elif arg['sdk_type'] == 'const char *':
            args.append({
                'name' : arg['name'],
                'python_type' : 'const char*',
                'initialization' : ' = NULL',
            })
            tuple_format += 's'
            allow_threads = False
        elif arg['sdk_type'] == 'enumeration':
            args.append({
                'name' : arg['name'],
                'python_type' : 'long int',
                'initialization' : ' = 0',
            })
            tuple_format += 'l'
        elif arg['sdk_type'] == 'const_object':
            args.append({
                'name' : arg['name'],
                'python_type' : 'PyObject*',
                'initialization' : ' = NULL',
                'check_object_type' : arg['sdk_class'],
            })
            tuple_format += 'O'
            allow_threads = False
        elif arg['sdk_type'] in ORTHANC_TO_PYTHON_NUMERIC_TYPES:
            t = ORTHANC_TO_PYTHON_NUMERIC_TYPES[arg['sdk_type']]
            args.append({
                'name' : arg['name'],
                'python_type' : t['type'],
                'initialization' : ' = 0',
            })
            tuple_format += t['format']
        else:
            print('Ignoring function with unsupported argument type: %s(), type = %s' % (f['c_function'], arg['sdk_type']))
            return None

        tuple_target.append('&' + arg['name'])

        if arg['sdk_type'] == 'const void *':
            call_args.append(arg['name'] + '.buf')
        elif arg['sdk_type'] == 'const_void_pointer_with_size':
            call_args.append(arg['name'] + '.buf')
            call_args.append(arg['name'] + '.len')
        elif arg['sdk_type'] == 'enumeration':
            call_args.append('static_cast<%s>(%s)' % (arg['sdk_enumeration'], arg['name']))
        elif arg['sdk_type'] == 'const_object':
            call_args.append('%s == Py_None ? NULL : reinterpret_cast<sdk_%s_Object*>(%s)->object_' % (
                arg['name'], arg['sdk_class'], arg['name']))
        else:
            call_args.append(arg['name'])

    answer['args'] = args

    if f['return_sdk_type'] == 'void':
        answer['return_void'] = True
    elif f['return_sdk_type'] in [ 'int32_t', 'uint32_t', 'int64_t' ]:
        answer['return_long'] = True
    elif f['return_sdk_type'] == 'OrthancPluginMemoryBuffer *':
        answer['return_bytes'] = True
    elif f['return_sdk_type'] == 'enumeration':
        if f['return_sdk_enumeration'] == 'OrthancPluginErrorCode':
            answer['return_error'] = True
        else:
            answer['return_enumeration'] = f['return_sdk_enumeration']
    elif f['return_sdk_type'] == 'char *':
        answer['return_dynamic_string'] = True
    elif f['return_sdk_type'] == 'const char *':
        answer['return_static_string'] = True
    elif f['return_sdk_type'] == 'object':
        answer['return_object'] = f['return_sdk_class']
    else:
        print('Ignoring function with unsupported return type: %s(), type = %s' % (f['c_function'], f['return_sdk_type']))
        return None

    allow_threads = False   # TODO

    answer['tuple_format'] = ', '.join([ '"' + tuple_format + '"' ] + tuple_target)
    answer['allow_threads'] = allow_threads

    if 'documentation' in f:
        documentation = {}
        description = f['documentation'].get('description', [])
        if len(description) > 0:
            documentation['short_description'] = description[0].split('.') [0]
        documentation['description'] = map(lambda x: { 'text' : x }, description)

        args_declaration = []
        args_documentation = []
        for a in f['args']:
            arg_name = ToLowerCase(a['sdk_name'])
            if a['sdk_type'] == 'const char *':
                arg_type = 'str'
            elif a['sdk_type'] == 'float':
                arg_type = 'float'
            elif a['sdk_type'] in [ 'const_void_pointer_with_size', 'const void *' ]:
                arg_type = 'bytes'
            elif a['sdk_type'] == 'enumeration':
                arg_type = GetShortName(a['sdk_enumeration'])
            elif a['sdk_type'] == 'const_object':
                arg_type = GetShortName(a['sdk_class'])
            elif a['sdk_type'] in [ 'int32_t', 'uint32_t', 'uint8_t', 'uint16_t', 'uint64_t' ]:
                arg_type = 'int'
            else:
                raise Exception('Argument type not implemented: %s' % a['sdk_type'])
            args_declaration.append('%s: %s' % (arg_name, arg_type))
            args_documentation.append({
                'name' : arg_name,
                'type' : arg_type,
                'text' : f['documentation']['args'] [a['sdk_name']],
            })

        documentation['args_declaration'] = ', '.join(args_declaration)
        documentation['args'] = args_documentation
        documentation['has_args'] = len(args_documentation) > 0
        documentation['has_return'] = True
        documentation['return_text'] = f['documentation'].get('return', None)

        if f['return_sdk_type'] == 'enumeration':
            if f['return_sdk_enumeration'] == 'OrthancPluginErrorCode':
                documentation['has_return'] = False
                documentation['return_type'] = 'None'
            else:
                documentation['return_type'] = GetShortName(f['return_sdk_enumeration'])
        elif f['return_sdk_type'] == 'object':
            documentation['return_type'] = GetShortName(f['return_sdk_class'])
        elif f['return_sdk_type'] == 'void':
            documentation['has_return'] = False
            documentation['return_type'] = 'None'
        elif f['return_sdk_type'] == 'OrthancPluginMemoryBuffer *':
            documentation['return_type'] = 'bytes'
        elif f['return_sdk_type'] in [ 'char *', 'const char *' ]:
            documentation['return_type'] = 'str'
        elif f['return_sdk_type'] in [ 'int32_t', 'uint32_t', 'int64_t' ]:
            documentation['return_type'] = 'int'
        else:
            raise Exception('Return type not implemented: %s' % f['return_sdk_type'])

        answer['documentation'] = documentation

    if len(call_args) > 0:
        answer['call_args'] = ', ' + ', '.join(call_args)

    if not allow_threads:
        print('Threads are not allowed in function: %s()' % f['c_function'])

    return answer



globalFunctions = []

for f in model['global_functions']:
    g = FormatFunction(f)
    if g != None:
        globalFunctions.append(g)

for f in CUSTOM_FUNCTIONS:
    g = FormatFunction(f)
    if g != None:
        globalFunctions.append(g)


enumerations = []

with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f:
    ENUMERATION_TEMPLATE = f.read()

for e in model['enumerations']:
    values = []
    for value in e['values']:
        values.append({
            'key' : ToUpperCase(value['key']),
            'value' : value['value'],
            'documentation' : value['documentation'],
        })

    enumerations.append({
        'name' : e['name'],
        'short_name' : GetShortName(e['name']),
        'path' : 'sdk_%s.impl.h' % e['name'],
        'values' : values,
        'documentation' : e['documentation'],
    })

    path = 'sdk_%s.impl.h' % e['name']

    with open(os.path.join(TARGET, path), 'w') as f:
        f.write(pystache.render(ENUMERATION_TEMPLATE, {
            'name' : e['name'],
            'short_name' : GetShortName(e['name']),
            'values' : values,
        }))


classes = []

for c in model['classes']:
    methods = []

    for m in c['methods']:
        g = FormatFunction(m)
        if g != None:
            g['self'] = ', self->object_'
            methods.append(g)

    classes.append({
        'class_name' : c['name'],
        'short_name' : GetShortName(c['name']),
        'methods' : methods,
    })

    if c['name'] in CUSTOM_METHODS:
        classes[-1]['custom_methods'] = CUSTOM_METHODS[c['name']]

    if 'destructor' in c:
        classes[-1]['destructor'] = c['destructor']




with open(os.path.join(ROOT, 'Class.mustache'), 'r') as f:
    with open(os.path.join(ROOT, 'ClassMethods.mustache'), 'r') as g:
        classDefinition = f.read()
        classMethods = g.read()

        for c in classes:
            with open(os.path.join(TARGET, 'sdk_%s.impl.h' % c['class_name']), 'w') as h:
                h.write(renderer.render(classDefinition, c))
            with open(os.path.join(TARGET, 'sdk_%s.methods.h' % c['class_name']), 'w') as h:
                h.write(renderer.render(classMethods, c))


sortedClasses = sorted(classes, key = lambda x: x['class_name'])
sortedEnumerations = sorted(enumerations, key = lambda x: x['name'])
sortedGlobalFunctions = sorted(globalFunctions, key = lambda x: x['c_function'])

with open(os.path.join(ROOT, 'GlobalFunctions.mustache'), 'r') as f:
    with open(os.path.join(TARGET, 'sdk_GlobalFunctions.impl.h'), 'w') as h:
        h.write(renderer.render(f.read(), {
            'global_functions' : sortedGlobalFunctions,
        }))

with open(os.path.join(ROOT, 'sdk.cpp.mustache'), 'r') as f:
    with open(os.path.join(TARGET, 'sdk.cpp'), 'w') as h:
        h.write(renderer.render(f.read(), {
            'classes' : sortedClasses,
            'enumerations' : sortedEnumerations,
            'global_functions' : sortedGlobalFunctions,
        }))

with open(os.path.join(ROOT, 'sdk.h.mustache'), 'r') as f:
    with open(os.path.join(TARGET, 'sdk.h'), 'w') as h:
        h.write(renderer.render(f.read(), {
            'classes' : sortedClasses,
        }))

with open(os.path.join(ROOT, 'PythonDocumentation.mustache'), 'r') as f:
    with open(os.path.join(TARGET, 'orthanc.pyi'), 'w') as h:
        h.write(renderer.render(f.read(), {
            'classes' : sortedClasses,
            'enumerations' : sortedEnumerations,
            'global_functions' : sortedGlobalFunctions,
        }))