py: gen-py-controls: Convert to jinja2 templates

Jinja2 templates help separate the logic related to the template from
the generation of the data. The python code becomes much clearer as a
result.

As an added bonus, we can use a single template file for both controls
and properties.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
Acked-by: Paul Elder <paul.elder@ideasonboard.com>
This commit is contained in:
Laurent Pinchart 2024-08-08 03:41:32 +03:00
parent 5c1cb5e5bc
commit 39f01e9185
4 changed files with 78 additions and 109 deletions

View file

@ -4,7 +4,7 @@
# Generate Python bindings controls from YAML # Generate Python bindings controls from YAML
import argparse import argparse
import string import jinja2
import sys import sys
import yaml import yaml
@ -23,35 +23,16 @@ def find_common_prefix(strings):
return prefix return prefix
def generate_py(controls, mode): def extend_control(ctrl, mode):
out = '' if ctrl.vendor != 'libcamera':
ctrl.klass = ctrl.vendor
vendors_class_def = [] ctrl.namespace = f'{ctrl.vendor}::'
vendor_defs = []
vendors = []
for vendor, ctrl_list in controls.items():
for ctrl in ctrl_list:
if vendor not in vendors and vendor != 'libcamera':
vendor_mode_str = f'{vendor.capitalize()}{mode.capitalize()}'
vendors_class_def.append('class Py{}\n{{\n}};\n'.format(vendor_mode_str))
vendor_defs.append('\tauto {} = py::class_<Py{}>(controls, \"{}\");'.format(vendor, vendor_mode_str, vendor))
vendors.append(vendor)
if vendor != 'libcamera':
ns = 'libcamera::{}::{}::'.format(mode, vendor)
container = vendor
else: else:
ns = 'libcamera::{}::'.format(mode) ctrl.klass = mode
container = 'controls' ctrl.namespace = ''
out += f'\t{container}.def_readonly_static("{ctrl.name}", static_cast<const libcamera::ControlId *>(&{ns}{ctrl.name}));\n\n'
if not ctrl.is_enum: if not ctrl.is_enum:
continue return ctrl
cpp_enum = ctrl.name + 'Enum'
out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
if mode == 'controls': if mode == 'controls':
# Adjustments for controls # Adjustments for controls
@ -63,27 +44,18 @@ def generate_py(controls, mode):
# Adjustments for properties # Adjustments for properties
prefix = find_common_prefix([e.name for e in ctrl.enum_values]) prefix = find_common_prefix([e.name for e in ctrl.enum_values])
for entry in ctrl.enum_values: for enum in ctrl.enum_values:
cpp_enum = entry.name enum.py_name = enum.name[len(prefix):]
py_enum = entry.name[len(prefix):]
out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum) return ctrl
out += '\t;\n\n'
return {'controls': out,
'vendors_class_def': '\n'.join(vendors_class_def),
'vendors_defs': '\n'.join(vendor_defs)}
def fill_template(template, data):
template = open(template, 'rb').read()
template = template.decode('utf-8')
template = string.Template(template)
return template.substitute(data)
def main(argv): def main(argv):
headers = {
'controls': 'control_ids.h',
'properties': 'property_ids.h',
}
# Parse command line arguments # Parse command line arguments
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--mode', '-m', type=str, required=True, parser.add_argument('--mode', '-m', type=str, required=True,
@ -96,27 +68,41 @@ def main(argv):
help='Input file name.') help='Input file name.')
args = parser.parse_args(argv[1:]) args = parser.parse_args(argv[1:])
if args.mode not in ['controls', 'properties']: if not headers.get(args.mode):
print(f'Invalid mode option "{args.mode}"', file=sys.stderr) print(f'Invalid mode option "{args.mode}"', file=sys.stderr)
return -1 return -1
controls = {} controls = []
vendors = []
for input in args.input: for input in args.input:
data = yaml.safe_load(open(input, 'rb').read()) data = yaml.safe_load(open(input, 'rb').read())
vendor = data['vendor'] vendor = data['vendor']
ctrls = data['controls'] if vendor != 'libcamera':
controls[vendor] = [Control(*ctrl.popitem(), vendor) for ctrl in ctrls] vendors.append(vendor)
data = generate_py(controls, args.mode) for ctrl in data['controls']:
ctrl = Control(*ctrl.popitem(), vendor)
controls.append(extend_control(ctrl, args.mode))
data = fill_template(args.template, data) data = {
'mode': args.mode,
'header': headers[args.mode],
'vendors': vendors,
'controls': controls,
}
env = jinja2.Environment()
template = env.from_string(open(args.template, 'r', encoding='utf-8').read())
string = template.render(data)
if args.output: if args.output:
output = open(args.output, 'wb') output = open(args.output, 'w', encoding='utf-8')
output.write(data.encode('utf-8')) output.write(string)
output.close() output.close()
else: else:
sys.stdout.write(data) sys.stdout.write(string)
return 0 return 0

View file

@ -26,7 +26,7 @@ pycamera_sources = files([
'py_transform.cpp', 'py_transform.cpp',
]) ])
# Generate controls # Generate controls and properties
gen_py_controls_template = files('py_controls_generated.cpp.in') gen_py_controls_template = files('py_controls_generated.cpp.in')
gen_py_controls = files('gen-py-controls.py') gen_py_controls = files('gen-py-controls.py')
@ -38,15 +38,11 @@ pycamera_sources += custom_target('py_gen_controls',
'-t', gen_py_controls_template, '@INPUT@'], '-t', gen_py_controls_template, '@INPUT@'],
env : py_build_env) env : py_build_env)
# Generate properties
gen_py_properties_template = files('py_properties_generated.cpp.in')
pycamera_sources += custom_target('py_gen_properties', pycamera_sources += custom_target('py_gen_properties',
input : properties_files, input : properties_files,
output : ['py_properties_generated.cpp'], output : ['py_properties_generated.cpp'],
command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@', command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@',
'-t', gen_py_properties_template, '@INPUT@'], '-t', gen_py_controls_template, '@INPUT@'],
env : py_build_env) env : py_build_env)
# Generate formats # Generate formats

View file

@ -2,12 +2,12 @@
/* /*
* Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
* *
* Python bindings - Auto-generated controls * Python bindings - Auto-generated {{mode}}
* *
* This file is auto-generated. Do not edit. * This file is auto-generated. Do not edit.
*/ */
#include <libcamera/control_ids.h> #include <libcamera/{{header}}>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
@ -15,16 +15,33 @@
namespace py = pybind11; namespace py = pybind11;
class PyControls class Py{{mode|capitalize}}
{ {
}; };
${vendors_class_def} {% for vendor in vendors -%}
class Py{{vendor|capitalize}}{{mode|capitalize}}
void init_py_controls_generated(py::module& m)
{ {
auto controls = py::class_<PyControls>(m, "controls"); };
${vendors_defs}
${controls} {% endfor -%}
void init_py_{{mode}}_generated(py::module& m)
{
auto {{mode}} = py::class_<Py{{mode|capitalize}}>(m, "{{mode}}");
{%- for vendor in vendors %}
auto {{vendor}} = py::class_<Py{{vendor|capitalize}}{{mode|capitalize}}>({{mode}}, "{{vendor}}");
{%- endfor %}
{% for ctrl in controls %}
{{ctrl.klass}}.def_readonly_static("{{ctrl.name}}", static_cast<const libcamera::ControlId *>(&libcamera::{{mode}}::{{ctrl.namespace}}{{ctrl.name}}));
{%- if ctrl.is_enum %}
py::enum_<libcamera::{{mode}}::{{ctrl.namespace}}{{ctrl.name}}Enum>({{ctrl.klass}}, "{{ctrl.name}}Enum")
{%- for enum in ctrl.enum_values %}
.value("{{enum.py_name}}", libcamera::{{mode}}::{{ctrl.namespace}}{{enum.name}})
{%- endfor %}
;
{%- endif %}
{% endfor -%}
} }

View file

@ -1,30 +0,0 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
*
* Python bindings - Auto-generated properties
*
* This file is auto-generated. Do not edit.
*/
#include <libcamera/property_ids.h>
#include <pybind11/pybind11.h>
#include "py_main.h"
namespace py = pybind11;
class PyProperties
{
};
${vendors_class_def}
void init_py_properties_generated(py::module& m)
{
auto controls = py::class_<PyProperties>(m, "properties");
${vendors_defs}
${controls}
}