[PATCH 10/10] py: gen-py-controls: Convert to jinja2 templates

Laurent Pinchart laurent.pinchart at ideasonboard.com
Fri Aug 9 02:59:14 CEST 2024


Jinja2 templates help separating the logic related to the template from
the generation of the data. The python code gets 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 at ideasonboard.com>
---
 src/py/libcamera/gen-py-controls.py           | 114 ++++++++----------
 src/py/libcamera/meson.build                  |   8 +-
 src/py/libcamera/py_controls_generated.cpp.in |  35 ++++--
 .../libcamera/py_properties_generated.cpp.in  |  30 -----
 4 files changed, 78 insertions(+), 109 deletions(-)
 delete mode 100644 src/py/libcamera/py_properties_generated.cpp.in

diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py
index a18dc5337090..cf09c146084d 100755
--- a/src/py/libcamera/gen-py-controls.py
+++ b/src/py/libcamera/gen-py-controls.py
@@ -4,7 +4,7 @@
 # Generate Python bindings controls from YAML
 
 import argparse
-import string
+import jinja2
 import sys
 import yaml
 
@@ -23,67 +23,39 @@ def find_common_prefix(strings):
     return prefix
 
 
-def generate_py(controls, mode):
-    out = ''
+def extend_control(ctrl, mode):
+    if ctrl.vendor != 'libcamera':
+        ctrl.klass = ctrl.vendor
+        ctrl.namespace = f'{ctrl.vendor}::'
+    else:
+        ctrl.klass = mode
+        ctrl.namespace = ''
 
-    vendors_class_def = []
-    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 not ctrl.is_enum:
+        return ctrl
 
-            if vendor != 'libcamera':
-                ns = 'libcamera::{}::{}::'.format(mode, vendor)
-                container = vendor
-            else:
-                ns = 'libcamera::{}::'.format(mode)
-                container = 'controls'
+    if mode == 'controls':
+        # Adjustments for controls
+        if ctrl.name == 'LensShadingMapMode':
+            prefix = 'LensShadingMapMode'
+        else:
+            prefix = find_common_prefix([e.name for e in ctrl.enum_values])
+    else:
+        # Adjustments for properties
+        prefix = find_common_prefix([e.name for e in ctrl.enum_values])
 
-            out += f'\t{container}.def_readonly_static("{ctrl.name}", static_cast<const libcamera::ControlId *>(&{ns}{ctrl.name}));\n\n'
+    for enum in ctrl.enum_values:
+        enum.py_name = enum.name[len(prefix):]
 
-            if not ctrl.is_enum:
-                continue
-
-            cpp_enum = ctrl.name + 'Enum'
-
-            out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
-
-            if mode == 'controls':
-                # Adjustments for controls
-                if ctrl.name == 'LensShadingMapMode':
-                    prefix = 'LensShadingMapMode'
-                else:
-                    prefix = find_common_prefix([e.name for e in ctrl.enum_values])
-            else:
-                # Adjustments for properties
-                prefix = find_common_prefix([e.name for e in ctrl.enum_values])
-
-            for entry in ctrl.enum_values:
-                cpp_enum = entry.name
-                py_enum = entry.name[len(prefix):]
-
-                out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum)
-
-            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)
+    return ctrl
 
 
 def main(argv):
+    headers = {
+        'controls': 'control_ids.h',
+        'properties': 'property_ids.h',
+    }
+
     # Parse command line arguments
     parser = argparse.ArgumentParser()
     parser.add_argument('--mode', '-m', type=str, required=True,
@@ -96,27 +68,41 @@ def main(argv):
                         help='Input file name.')
     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)
         return -1
 
-    controls = {}
+    controls = []
+    vendors = []
+
     for input in args.input:
         data = yaml.safe_load(open(input, 'rb').read())
+
         vendor = data['vendor']
-        ctrls = data['controls']
-        controls[vendor] = [Control(*ctrl.popitem(), vendor) for ctrl in ctrls]
+        if vendor != 'libcamera':
+            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:
-        output = open(args.output, 'wb')
-        output.write(data.encode('utf-8'))
+        output = open(args.output, 'w', encoding='utf-8')
+        output.write(string)
         output.close()
     else:
-        sys.stdout.write(data)
+        sys.stdout.write(string)
 
     return 0
 
diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build
index 6ad2d7713e4d..596a203ca4cc 100644
--- a/src/py/libcamera/meson.build
+++ b/src/py/libcamera/meson.build
@@ -26,7 +26,7 @@ pycamera_sources = files([
     'py_transform.cpp',
 ])
 
-# Generate controls
+# Generate controls and properties
 
 gen_py_controls_template = files('py_controls_generated.cpp.in')
 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@'],
                                   env : py_build_env)
 
-# Generate properties
-
-gen_py_properties_template = files('py_properties_generated.cpp.in')
-
 pycamera_sources += custom_target('py_gen_properties',
                                   input : properties_files,
                                   output : ['py_properties_generated.cpp'],
                                   command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@',
-                                             '-t', gen_py_properties_template, '@INPUT@'],
+                                             '-t', gen_py_controls_template, '@INPUT@'],
                                   env : py_build_env)
 
 # Generate formats
diff --git a/src/py/libcamera/py_controls_generated.cpp.in b/src/py/libcamera/py_controls_generated.cpp.in
index 26d5a104f209..22a132d19ea9 100644
--- a/src/py/libcamera/py_controls_generated.cpp.in
+++ b/src/py/libcamera/py_controls_generated.cpp.in
@@ -2,12 +2,12 @@
 /*
  * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
  *
- * Python bindings - Auto-generated controls
+ * Python bindings - Auto-generated {{mode}}
  *
  * This file is auto-generated. Do not edit.
  */
 
-#include <libcamera/control_ids.h>
+#include <libcamera/{{header}}>
 
 #include <pybind11/pybind11.h>
 
@@ -15,16 +15,33 @@
 
 namespace py = pybind11;
 
-class PyControls
+class Py{{mode|capitalize}}
 {
 };
 
-${vendors_class_def}
-
-void init_py_controls_generated(py::module& m)
+{% for vendor in vendors -%}
+class Py{{vendor|capitalize}}{{mode|capitalize}}
 {
-	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 -%}
 }
diff --git a/src/py/libcamera/py_properties_generated.cpp.in b/src/py/libcamera/py_properties_generated.cpp.in
deleted file mode 100644
index d28f1ab8b61a..000000000000
--- a/src/py/libcamera/py_properties_generated.cpp.in
+++ /dev/null
@@ -1,30 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at 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}
-}
-- 
Regards,

Laurent Pinchart



More information about the libcamera-devel mailing list