[10/10] py: gen-py-controls: Convert to jinja2 templates
diff mbox series

Message ID 20240809005914.20662-11-laurent.pinchart@ideasonboard.com
State Accepted
Headers show
Series
  • libcamera: Improve code generation for controls
Related show

Commit Message

Laurent Pinchart Aug. 9, 2024, 12:59 a.m. UTC
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@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

Comments

Tomi Valkeinen Aug. 12, 2024, 2:45 p.m. UTC | #1
On 09/08/2024 03:59, Laurent Pinchart wrote:
> 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@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@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@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}
> -}

I'm not familiar with jinja2, but looks fine to me and works fine.

Reviewed-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>

  Tomi
Paul Elder Aug. 15, 2024, 5:08 a.m. UTC | #2
On Fri, Aug 09, 2024 at 03:59:14AM +0300, Laurent Pinchart wrote:
> 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@ideasonboard.com>

Looks good to me.

Acked-by: Paul Elder <paul.elder@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@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@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}
> -}

Patch
diff mbox series

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@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@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}
-}