details:   https://code.tryton.org/tryton/commit/d7e0dcf4ac45
branch:    default
user:      Cédric Krier <[email protected]>
date:      Mon Mar 30 11:29:57 2026 +0200
description:
        Fallback to Compact Syntax if RelaxNG files are not present

        lxml can load compact syntax if rnc2rng package is installed.
        This avoid the need to generate the RelaxNG files in development.
diffstat:

 trytond/trytond/ir/action.py            |   2 +-
 trytond/trytond/ir/ui/view.py           |  52 +++++++++++++++++---------------
 trytond/trytond/model/modelview.py      |   2 +-
 trytond/trytond/tests/test_modelview.py |   2 +-
 trytond/trytond/tests/test_tryton.py    |  22 +++++++------
 5 files changed, 43 insertions(+), 37 deletions(-)

diffs (194 lines):

diff -r 515ce1acffef -r d7e0dcf4ac45 trytond/trytond/ir/action.py
--- a/trytond/trytond/ir/action.py      Thu Mar 26 22:39:31 2026 +0100
+++ b/trytond/trytond/ir/action.py      Mon Mar 30 11:29:57 2026 +0200
@@ -935,7 +935,7 @@
                                 action=action.rec_name)) from exception
 
     def get_views(self, name):
-        return [(view.view.id, view.view.rng_type)
+        return [(view.view.id, view.view.real_type)
             for view in self.act_window_views]
 
     def get_domains(self, name):
diff -r 515ce1acffef -r d7e0dcf4ac45 trytond/trytond/ir/ui/view.py
--- a/trytond/trytond/ir/ui/view.py     Thu Mar 26 22:39:31 2026 +0100
+++ b/trytond/trytond/ir/ui/view.py     Mon Mar 30 11:29:57 2026 +0200
@@ -6,6 +6,7 @@
 import logging
 import os
 from collections import defaultdict
+from pathlib import Path
 
 from lxml import etree
 from sql import Literal, Null
@@ -113,7 +114,7 @@
     domain = fields.Char('Domain', states={
             'invisible': ~Eval('inherit'),
             }, depends=['inherit'])
-    _get_rng_cache = MemoryCache('ir_ui_view.get_rng', context=False)
+    _get_validator_cache = MemoryCache('ir_ui_view.validator', context=False)
     _view_get_cache = Cache('ir_ui_view.view_get')
     __module_index = None
 
@@ -196,23 +197,27 @@
     def show(cls, views):
         pass
 
+    def validator(self):
+        return self._validator(self.real_type)
+
     @classmethod
-    def get_rng(cls, type_):
-        key = (cls.__name__, type_)
-        rng = cls._get_rng_cache.get(key)
-        if rng is None:
-            if type_ == 'list-form':
-                type_ = 'form'
-            rng_name = os.path.join(os.path.dirname(__file__), type_ + '.rng')
-            with open(rng_name, 'rb') as fp:
-                rng = etree.fromstring(fp.read())
-            rng = cls._get_rng_cache.set(key, rng)
-        return rng
+    def _validator(cls, type):
+        key = (cls.__name__, type)
+        validator = cls._get_validator_cache.get(key)
+        if validator is None:
+            if type == 'list-form':
+                type = 'form'
+            filepath = Path(os.path.dirname(__file__), f'{type}.rng')
+            if not filepath.exists():
+                filepath = filepath.with_suffix('.rnc')
+            validator = etree.RelaxNG(file=str(filepath))
+            validator = cls._get_validator_cache.set(key, validator)
+        return validator
 
     @property
-    def rng_type(self):
+    def real_type(self):
         if self.inherit:
-            return self.inherit.rng_type
+            return self.inherit.real_type
         return self.type
 
     @classmethod
@@ -231,14 +236,13 @@
                 continue
             tree = etree.fromstring(xml)
 
-            if hasattr(etree, 'RelaxNG'):
-                validator = etree.RelaxNG(etree=cls.get_rng(view.rng_type))
-                if not validator.validate(tree):
-                    error_log = '\n'.join(map(str,
-                            validator.error_log.filter_from_errors()))
-                    raise XMLError(
-                        gettext('ir.msg_view_invalid_xml', name=view.rec_name),
-                        error_log)
+            validator = view.validator()
+            if not validator.validate(tree):
+                error_log = '\n'.join(map(str,
+                        validator.error_log.filter_from_errors()))
+                raise XMLError(
+                    gettext('ir.msg_view_invalid_xml', name=view.rec_name),
+                    error_log)
             root_element = tree.getroottree().getroot()
 
             # validate pyson attributes
@@ -340,7 +344,7 @@
             self._translate(root, model, Transaction().language)
         arch = etree.tostring(tree, encoding='utf-8').decode('utf-8')
         result = {
-            'type': self.rng_type,
+            'type': self.real_type,
             'view_id': self.id,
             'arch': arch,
             'field_childs': self.field_childs,
@@ -712,7 +716,7 @@
         if fields_names and 'view' not in fields_names:
             return
         for record in records:
-            if record.view and record.view.rng_type != 'tree':
+            if record.view and record.view.real_type != 'tree':
                 raise ViewError(gettext(
                         'ir.msg_view_tree_optional_type',
                         view=record.view.rec_name))
diff -r 515ce1acffef -r d7e0dcf4ac45 trytond/trytond/model/modelview.py
--- a/trytond/trytond/model/modelview.py        Thu Mar 26 22:39:31 2026 +0100
+++ b/trytond/trytond/model/modelview.py        Mon Mar 30 11:29:57 2026 +0200
@@ -288,7 +288,7 @@
                     ],
                 ]
             views = View.search(domain)
-            views = [v for v in views if v.rng_type == view_type]
+            views = [v for v in views if v.real_type == view_type]
             if views:
                 view = views[0]
                 view_id = view.id
diff -r 515ce1acffef -r d7e0dcf4ac45 trytond/trytond/tests/test_modelview.py
--- a/trytond/trytond/tests/test_modelview.py   Thu Mar 26 22:39:31 2026 +0100
+++ b/trytond/trytond/tests/test_modelview.py   Mon Mar 30 11:29:57 2026 +0200
@@ -707,7 +707,7 @@
         arch = TestModel.fields_view_get()['arch']
         parser = etree.XMLParser()
         tree = etree.fromstring(arch, parser=parser)
-        validator = etree.RelaxNG(etree=UIView.get_rng('form'))
+        validator = UIView._validator('form')
 
         self.assertTrue(validator.validate(tree))
 
diff -r 515ce1acffef -r d7e0dcf4ac45 trytond/trytond/tests/test_tryton.py
--- a/trytond/trytond/tests/test_tryton.py      Thu Mar 26 22:39:31 2026 +0100
+++ b/trytond/trytond/tests/test_tryton.py      Mon Mar 30 11:29:57 2026 +0200
@@ -37,7 +37,7 @@
 from trytond.pool import Pool, isregisteredby
 from trytond.protocols.wrappers import Response
 from trytond.pyson import PYSON, PYSONDecoder, PYSONEncoder
-from trytond.tools import file_open, find_dir, is_instance_method
+from trytond.tools import file_open, find_dir, find_path, is_instance_method
 from trytond.transaction import Transaction, TransactionError
 from trytond.wizard import StateAction, StateView
 from trytond.wsgi import app
@@ -471,11 +471,11 @@
                     view_id = view.id
                 model = view.model
                 Model = pool.get(model)
-                view = Model.fields_view_get(view_id)
-                self.assertEqual(view['model'], model)
-                tree = etree.fromstring(view['arch'])
+                fields_view = Model.fields_view_get(view_id)
+                self.assertEqual(fields_view['model'], model)
+                tree = etree.fromstring(fields_view['arch'])
 
-                validator = etree.RelaxNG(etree=View.get_rng(view['type']))
+                validator = view.validator()
                 validator.assertValid(tree)
 
                 tree_root = tree.getroottree().getroot()
@@ -520,11 +520,11 @@
                                     button_name, Model.__name__))
 
                         for field in fields_to_check:
-                            self.assertIn(field, view['fields'].keys(),
+                            self.assertIn(field, fields_view['fields'].keys(),
                                 msg="Missing field %r in %r" % (
                                     field, Model.__name__))
                         for field, t_fields in target_fields_to_check.items():
-                            for t_view in view['fields'][field].get(
+                            for t_view in fields_view['fields'][field].get(
                                     'views', {}).values():
                                 for t_field in t_fields:
                                     self.assertIn(
@@ -1186,9 +1186,11 @@
             config.read_file(fp)
         if not config.has_option('tryton', 'xml'):
             return
-        with file_open('tryton.rng', subdir='', mode='rb') as fp:
-            rng = etree.parse(fp)
-        validator = etree.RelaxNG(etree=rng)
+        try:
+            filepath = find_path('tryton.rng', subdir='')
+        except FileNotFoundError:
+            filepath = find_path('tryton.rnc', subdir='')
+        validator = etree.RelaxNG(file=filepath)
         for xml_file in filter(None, config.get('tryton', 'xml').splitlines()):
             with self.subTest(xml=xml_file):
                 with file_open('%s/%s' % (self.module, xml_file),

Reply via email to