[IMP] core: prettify_domain function

This commit add a new `odoo.osv.expression.prettify_domain` function
that can be used to format a domain as a string with the correct
indentation.

Example:

    ['&', '|', ('name', 'like', 'Jack'), ('name', 'like', "O'Neill"),
     ('function', '=', 'Colonel')]

Becomes:

    ['&',
        '|',
            ('name', 'like', 'Jack'),
            ('name', 'like', "O'Neill"),
        ('function', '=', 'Colonel')]

closes odoo/odoo#118067

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
This commit is contained in:
Julien Castiaux
2023-04-27 10:12:57 +02:00
parent 5a998694a6
commit 1bbdd77f0e
2 changed files with 161 additions and 0 deletions
+81
View File
@@ -1872,6 +1872,87 @@ class TestMany2many(TransactionCase):
self.User.search([('groups_id', '=', False)], order='id')
class TestPrettifyDomain(BaseCase):
def test_prettify_domain(self):
_Case = collections.namedtuple('Case', ('name', 'dom', 'pretty'))
test_matrix = [
_Case(
name='single leaf',
dom=[('name', '=', 'Jack')],
pretty="[('name', '=', 'Jack')]"
),
_Case(
name='not',
dom=['!', ('name', '=', 'Apophis')],
pretty=textwrap.dedent("""\
['!',
('name', '=', 'Apophis')]
""").rstrip()
),
_Case(
name='single and',
dom=['&',
('name', '=', 'Jack'),
('function', '=', 'Colonel')],
pretty=textwrap.dedent("""\
['&',
('name', '=', 'Jack'),
('function', '=', 'Colonel')]
""").rstrip()
),
_Case(
name='multiple and',
dom=['&', '&',
('name', 'like', 'Jack'),
('name', 'like', "O'Neill"),
('function', '=', 'Colonel')],
pretty=textwrap.dedent("""\
['&', '&',
('name', 'like', 'Jack'),
('name', 'like', "O'Neill"),
('function', '=', 'Colonel')]
""").rstrip()
),
_Case(
name='and or',
dom=['&',
'|',
('name', 'like', 'Jack'),
('name', 'like', "O'Neill"),
('function', '=', 'Colonel')],
pretty=textwrap.dedent("""\
['&',
'|',
('name', 'like', 'Jack'),
('name', 'like', "O'Neill"),
('function', '=', 'Colonel')]
""").rstrip()
),
_Case(
name='any single',
dom=[('company', 'any', [('name', '=', 'SGC')])],
pretty="[('company', 'any', [('name', '=', 'SGC')])]"
),
_Case(
name='any or',
dom=[('company', 'any', ['|',
('name', '=', 'SGC'),
('name', '=', 'Stargate Command')])],
pretty=textwrap.dedent("""\
[('company', 'any', ['|',
('name', '=', 'SGC'),
('name', '=', 'Stargate Command')])]
""").rstrip()
)
]
for case in test_matrix:
with self.subTest(name=case.name):
pretty_domain = expression.prettify_domain(case.dom)
self.assertEqual(pretty_domain, case.pretty)
self.assertEqual(literal_eval(case.pretty), case.dom)
class TestAnyfy(TransactionCase):
def _test_combine_anies(self, domain, expected):
+80
View File
@@ -581,6 +581,86 @@ def domain_combine_anies(domain, model):
new_domain = _tree_as_domain(merged_tree)
return new_domain
def prettify_domain(domain, pre_indent=0):
"""
Pretty-format a domain into a string by separating each leaf on a
separated line and by including some indentation. Works with ``any``
and ``not any`` too. The domain must be normalized.
:param list domain: a normalized domain
:param int pre_indent: (optinal) a starting indentation level
:return: the domain prettified
:rtype: str
"""
# The ``stack`` is a stack of layers, each layer accumulates the
# ``terms`` (leaves/operators) that share a same indentation
# level (the depth of the layer inside the stack). ``left_count``
# tracks how many terms should still appear on each layer before the
# layer is considered complete.
#
# When a layer is completed, it is removed from the stack and
# commited, i.e. its terms added to the ``commits`` list along with
# the indentation for those terms.
#
# When a new operator is added to the layer terms, the current layer
# is commited (but not removed from the stack if there are still
# some terms that must be added) and a new (empty) layer is added on
# top of the stack.
#
# When the domain has been fully iterated, the commits are used to
# craft the final string. All terms are indented according to their
# commit indentation level and separated by a new line.
stack = [{'left_count': 1, 'terms': []}]
commits = []
for term in domain:
top = stack[-1]
if is_operator(term):
# when a same operator appears twice in a row, we want to
# include the second one on the same line as the former one
if (not top['terms'] and commits
and (commits[-1]['terms'] or [''])[-1].startswith(repr(term))):
commits[-1]['terms'][-1] += f", {term!r}" # hack
top['left_count'] += 0 if term == NOT_OPERATOR else 1
else:
commits.append({
'indent': len(stack) - 1,
'terms': top['terms'] + [repr(term)]
})
top['terms'] = []
top['left_count'] -= 1
stack.append({
'left_count': 1 if term == NOT_OPERATOR else 2,
'terms': [],
})
top = stack[-1]
elif term[1] in ('any', 'not any'):
top['terms'].append('({!r}, {!r}, {})'.format(
term[0], term[1], prettify_domain(term[2], pre_indent + len(stack) - 1)))
top['left_count'] -= 1
else:
top['terms'].append(repr(term))
top['left_count'] -= 1
if not top['left_count']:
commits.append({
'indent': len(stack) - 1,
'terms': top['terms']
})
stack.pop()
return '[{}]'.format(
f",\n{' ' * pre_indent}".join([
f"{' ' * commit['indent']}{term}"
for commit in commits
for term in commit['terms']
])
)
# --------------------------------------------------
# Generic leaf manipulation
# --------------------------------------------------