Mercurial > hg > orthanc-python
view CodeAnalysis/GenerateOrthancSDK.py @ 278:02077e32cd70
manual wrapping of functions for queues
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 12 Aug 2025 16:17:57 +0200 |
parents | 919836e3fafd |
children | 66768d476400 |
line wrap: on
line source
#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2020-2023 Osimis S.A., 2024-2025 Orthanc Team SRL, 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain # SPDX-License-Identifier: AGPL-3.0-or-later ## ## Python plugin for Orthanc ## Copyright (C) 2020-2023 Osimis S.A., Belgium ## Copyright (C) 2024-2025 Orthanc Team SRL, Belgium ## Copyright (C) 2021-2025 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 pystache import re import sys ROOT = os.path.dirname(os.path.realpath(__file__)) ## ## Extract the default SDK version ## with open(os.path.join(ROOT, '..', 'CMakeLists.txt'), 'r') as f: m = re.findall('^set\(ORTHANC_SDK_DEFAULT_VERSION "([^"]+)"\)$', f.read(), re.MULTILINE) assert(len(m) == 1) ORTHANC_SDK_DEFAULT_VERSION = m[0] ## ## Parse the command-line arguments ## parser = argparse.ArgumentParser(description = 'Generate Python code to wrap the Orthanc SDK.') parser.add_argument('--sdk', default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '../Resources/Orthanc/Sdk-%s/orthanc/OrthancCPlugin.h' % ORTHANC_SDK_DEFAULT_VERSION), help = 'Path to the Orthanc SDK') parser.add_argument('--model', default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '../Resources/Orthanc/OrthancPluginCodeModel.json'), help = 'Input code model, as generated by the orthanc project') parser.add_argument('--target', default = os.path.join('/tmp/PythonAutogenerated/'), help = 'Target folder') args = parser.parse_args() TARGET = os.path.realpath(args.target) try: # "exist_ok = True" is not available on Python 2.7, which is still in use on our CIS for Ubuntu 16.04 os.makedirs(TARGET) except: pass ## ## Detect the actual version of the Orthanc SDK ## with open(args.sdk, 'r') as f: content = f.read() major = re.findall(r'#\s*define\s+ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER\s+([0-9.]+)$', content, re.MULTILINE) minor = re.findall(r'#\s*define\s+ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER\s+([0-9.]+)$', content, re.MULTILINE) revision = re.findall(r'#\s*define\s+ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER\s+([0-9.]+)$', content, re.MULTILINE) assert(len(major) == 1) assert(len(minor) == 1) assert(len(revision) == 1) SDK_VERSION = [ int(major[0]), int(minor[0]), int(revision[0]) ] def IsPrimitiveAvailable(item, key_prefix = ''): since_sdk = item.get('since_sdk') if since_sdk != None: assert(len(since_sdk) == 3) assert(len(SDK_VERSION) == 3) if since_sdk[0] < SDK_VERSION[0]: available = True elif since_sdk[0] > SDK_VERSION[0]: available = False elif since_sdk[1] < SDK_VERSION[1]: available = True elif since_sdk[1] > SDK_VERSION[1]: available = False else: available = since_sdk[2] <= SDK_VERSION[2] if not available: name = item.get('name') if name == None: name = item.get('c_function') if name == None: name = item.get('short_name') if name == None: # For enumerations key = item.get('key') if key != None: name = '%s_%s' % (key_prefix, key) print('Primitive unavailable in SDK: %s (only available since %s)' % (name, '.'.join(map(str, since_sdk)))) return available else: return True print('\n** Generating the Python wrapper for Orthanc SDK %d.%d.%d **' % (SDK_VERSION[0], SDK_VERSION[1], SDK_VERSION[2])) ## ## Configuration of the custom primitives that are manually ## implemented (not autogenerated) ## with open(os.path.join(ROOT, 'CustomMethods.json'), 'r') as f: CUSTOM_METHODS = json.loads(f.read()) with open(os.path.join(ROOT, 'CustomFunctions.json'), 'r') as f: CUSTOM_FUNCTIONS = json.loads(f.read()) partials = {} with open(os.path.join(ROOT, 'FunctionBody.mustache'), 'r') as f: partials['function_body'] = f.read() with open(os.path.join(ROOT, 'FunctionDocumentation.mustache'), 'r') as f: partials['function_documentation'] = 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()) with open(os.path.join(ROOT, 'ClassDocumentation.json'), 'r') as f: classes_documentation = 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, parent_class = None): if not name.startswith('OrthancPlugin'): raise Exception() elif parent_class != None and name.startswith(parent_class): return name[len(parent_class):] 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', }, 'int64_t' : { 'type' : 'long long', '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 DocumentFunction(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', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t' ]: arg_type = 'int' elif a['sdk_type'] == 'Callable': # This is only used to generate the documentation file "orthanc.pyi" arg_type = a['callable_type'] 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 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', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t' ]: documentation['return_type'] = 'int' elif f['return_sdk_type'] == 'Dictionary': # This is only used to generate the documentation file "orthanc.pyi" documentation['return_type'] = 'dict' elif f['return_sdk_type'] == 'Tuple': # This is only used to generate the documentation file "orthanc.pyi" documentation['return_type'] = 'tuple' elif f['return_sdk_type'] == 'bool': # This is only used to generate the documentation file "orthanc.pyi" documentation['return_type'] = 'bool' else: raise Exception('Return type not implemented: %s' % f['return_sdk_type']) if documentation['has_return']: documentation['return_text'] = f['documentation']['return'] return documentation def FormatFunction(f, parent_class = None): answer = { 'c_function' : f['c_function'], 'short_name' : GetShortName(f['c_function'], parent_class), 'has_args' : len(f['args']) > 0, 'count_args' : len(f['args']), } 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 += 'z*' elif arg['sdk_type'] == 'const char *': args.append({ 'name' : arg['name'], 'python_type' : 'const char*', 'initialization' : ' = NULL', }) tuple_format += 's' 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' 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': # NB: The cast to "const char*" allows compatibility with functions whose # signatures were incorrect at the time they were introduced, notably: # - argument "body" of "OrthancPluginSendHttpStatus()" in 1.11.1 call_args.append('reinterpret_cast<const char*>(' + arg['name'] + '.len > 0 ? ' + arg['name'] + '.buf' + ' : NULL)') call_args.append('(' + arg['name'] + '.len > 0 ? ' + arg['name'] + '.len' + ' : 0)') 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', 'int64_t', 'uint32_t', 'uint64_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 answer['tuple_format'] = ', '.join([ '"' + tuple_format + '"' ] + tuple_target) if 'documentation' in f: answer['documentation'] = DocumentFunction(f) if len(call_args) > 0: answer['call_args'] = ', ' + ', '.join(call_args) return answer globalFunctions = [] customFunctions = [] for f in model['global_functions']: if IsPrimitiveAvailable(f): g = FormatFunction(f) if g != None: globalFunctions.append(g) for f in CUSTOM_FUNCTIONS: if IsPrimitiveAvailable(f): f['documentation'] = DocumentFunction(f) customFunctions.append(f) enumerations = [] with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f: ENUMERATION_TEMPLATE = f.read() for e in model['enumerations']: if not IsPrimitiveAvailable(e): continue values = [] for value in e['values']: if IsPrimitiveAvailable(value, key_prefix = e['name']): 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 = [] countDestructors = 0 for c in model['classes']: if not IsPrimitiveAvailable(c): continue methods = [] for m in c['methods']: if IsPrimitiveAvailable(m): g = FormatFunction(m, parent_class = c['name']) if g != None: g['self'] = ', self->object_' methods.append(g) custom_methods = [] if c['name'] in CUSTOM_METHODS: for custom_method in CUSTOM_METHODS[c['name']]: if IsPrimitiveAvailable(custom_method): custom_method['self'] = True # Indicates that this is a method custom_method['documentation'] = DocumentFunction(custom_method) custom_methods.append(custom_method) classes.append({ 'description' : classes_documentation[c['name']], 'class_name' : c['name'], 'short_name' : GetShortName(c['name']), 'methods' : methods, 'custom_methods' : sorted(custom_methods, key = lambda x: x['short_name']), }) if 'destructor' in c: countDestructors += 1 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']) sortedCustomFunctions = sorted(customFunctions, key = lambda x: x['short_name']) 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, 'custom_functions' : sortedCustomFunctions, })) 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, 'custom_functions' : sortedCustomFunctions, })) 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, 'custom_functions' : sortedCustomFunctions, })) ## ## Print statistics ## countWrappedMethods = 0 countCustomMethods = 0 for c in sortedClasses: countWrappedMethods += len(c['methods']) countCustomMethods += len(c['custom_methods']) print('\nNumber of automatically wrapped global functions: %d' % len(sortedGlobalFunctions)) print('Number of automatically wrapped methods: %d' % countWrappedMethods) print('Number of automatically wrapped destructors: %d' % countDestructors) totalWrapped = (len(sortedGlobalFunctions) + countWrappedMethods + countDestructors) print('=> Total number of automatically wrapped functions (including destructors): %d\n' % totalWrapped) print('Number of manually implemented (custom) global functions: %d' % len(sortedCustomFunctions)) print('Number of manually implemented (custom) methods: %d' % countCustomMethods) total = totalWrapped + len(sortedCustomFunctions) + countCustomMethods print('=> Total number of functions or methods in the Python wrapper: %d\n' % total)