Skip to content

Commit dfee8ae

Browse files
committed
[IMP] util/misc: dynamic literal_replace
Allow to match in a glob-like fashion and produce the replacement node dynamically. Improvements: * Do not replace if no update happens (allows to write less to the DB.) * Do not buble syntax errors, log instead. * Most valid nodes for Python expressions in the ORM are covered. * Implement node matching in same order as the reference below. * Much higher test coverage. * Adapt tests for Python3.6 (Odoo 13 and 14) See: https://greentreesnakes.readthedocs.io/en/latest/nodes.html Part-of: #231 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent 534cec0 commit dfee8ae

File tree

2 files changed

+327
-38
lines changed

2 files changed

+327
-38
lines changed

src/base/tests/test_util.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ast
22
import operator
33
import re
4+
import sys
45
import threading
56
import unittest
67
import uuid
@@ -1731,8 +1732,9 @@ def test_SelfPrint_failure(self, value):
17311732
"[('company_id', 'in', (companies.active_ids or [False]))]",
17321733
),
17331734
(
1734-
"[('company_id','in', user.other.allowed_company_ids)]",
1735-
"[('company_id', 'in', user.other.allowed_company_ids)]",
1735+
"[('company_id','in', user.other.allowed_company_ids)]",
1736+
# note it keeps the original spacing since no match should happen!
1737+
"[('company_id','in', user.other.allowed_company_ids)]",
17361738
),
17371739
(
17381740
"[('group_id','in', user.groups_id.ids)]",
@@ -1764,6 +1766,135 @@ def test_literal_replace(self, orig, expected, old_unparse_fallback=None):
17641766
else:
17651767
self.assertEqual(repl, expected)
17661768

1769+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1770+
@mute_logger("odoo.upgrade.util.misc")
1771+
def test_literal_replace_error(self):
1772+
# this shouldn't raise a syntax error
1773+
res = util.literal_replace("[1,2", {"1": "3"})
1774+
self.assertEqual(res, "[1,2")
1775+
1776+
@parametrize(
1777+
[
1778+
("x[1 ]", "x[1]"),
1779+
("{x for x in y }, [ {1 }, {1 : 2},x[1],y[ 1:2:3 ] ]", "{x for x in y},[{1},{1:2},x[1],y[1:2:3]]"),
1780+
(
1781+
"[1 if True else 2, * z] + [x for x in y if w], {x:x for x in y} ",
1782+
"[1 if True else 2,*z]+[x for x in y if w],{x:x for x in y}",
1783+
),
1784+
("1 < 2 <3, x or z and y or x", "1<2<3,x or z and y or x"),
1785+
]
1786+
)
1787+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1788+
def test_literal_replace_full(self, text, orig):
1789+
# check each grammar piece
1790+
repl = util.literal_replace(text, {orig: "gone"})
1791+
self.assertEqual(repl, "gone")
1792+
1793+
# minimal change should invalidate the replace
1794+
text = text.replace("x", "xx")
1795+
repl = util.literal_replace(text, {orig: "gone"})
1796+
self.assertEqual(text, repl)
1797+
1798+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1799+
def test_literal_replace_callable(self):
1800+
def adapter(node):
1801+
return ast.parse("this.get('{}')".format(node.attr), mode="eval").body
1802+
1803+
repl = util.literal_replace(
1804+
"result = this. x if that . y == 2 else this .z",
1805+
{ast.Attribute(ast.Name("this", None), util.literal_replace.WILDCARD, None): adapter},
1806+
)
1807+
self.assertIn(
1808+
repl,
1809+
[
1810+
"result = this.get('x') if that.y == 2 else this.get('z')",
1811+
# fallback for older unparse
1812+
"result = (this.get('x') if (that.y == 2) else this.get('z'))",
1813+
],
1814+
)
1815+
1816+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1817+
@unittest.skipUnless(hasattr(ast, "Constant"), "`ast.Constant` available from Python3.6")
1818+
def test_literal_replace_callable2(self):
1819+
def adapter2(node):
1820+
return ast.parse(
1821+
"{} / 42".format(node.left.value if hasattr(node.left, "value") else node.left.n), mode="eval"
1822+
).body
1823+
1824+
def adapter3(node):
1825+
return ast.parse("y == 42", mode="eval").body
1826+
1827+
repl = util.literal_replace(
1828+
"16 * w or y == 2",
1829+
{
1830+
ast.BinOp(ast.Constant(16), ast.Mult(), ast.Name(util.literal_replace.WILDCARD, None)): adapter2,
1831+
ast.Compare(ast.Name("y", None), [ast.Eq()], [ast.Constant(util.literal_replace.WILDCARD)]): adapter3,
1832+
},
1833+
)
1834+
# Check with fallback for older unparse
1835+
self.assertIn(repl, ["16 / 42 or y == 42", "((16 / 42) or (y == 42))"])
1836+
1837+
def adapter4(node):
1838+
return ast.parse(
1839+
"{}.get({})".format(util.ast_unparse(node.value), util.ast_unparse(node.slice)), mode="eval"
1840+
).body
1841+
1842+
repl = util.literal_replace(
1843+
" x[ 'a' ]+y[b ] - z[None]",
1844+
{
1845+
ast.Subscript(
1846+
ast.Name(util.literal_replace.WILDCARD, None), util.literal_replace.WILDCARD, None
1847+
): adapter4
1848+
},
1849+
)
1850+
# Check with fallback for older unparse
1851+
self.assertIn(repl, ["x.get('a') + y.get(b) - z.get(None)", "((x.get('a') + y.get(b)) - z.get(None))"])
1852+
1853+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1854+
@unittest.skipUnless(hasattr(ast, "Constant"), "`ast.Constant` available from Python3.6")
1855+
def test_literal_replace_wildcards(self):
1856+
repl = util.literal_replace(
1857+
"x+1 - z* 18 + [1,2,3]",
1858+
{
1859+
ast.Name(util.literal_replace.WILDCARD, None): "y",
1860+
(ast.Constant if sys.version_info > (3, 9) else ast.Num)(util.literal_replace.WILDCARD): "2",
1861+
ast.List([util.literal_replace.WILDCARD], None): "[4,5]",
1862+
},
1863+
)
1864+
self.assertIn(repl, ["y + 2 - y * 2 + [4, 5]", "(((y + 2) - (y * 2)) + [4, 5])"])
1865+
1866+
@parametrize(
1867+
[
1868+
(ast.Constant(util.literal_replace.WILDCARD, kind=None), "*"),
1869+
(ast.Name(util.literal_replace.WILDCARD, None), "*"),
1870+
(ast.Attribute(ast.Name(util.literal_replace.WILDCARD, None), util.literal_replace.WILDCARD, None), "*.*"),
1871+
(
1872+
ast.Subscript(
1873+
ast.Name(util.literal_replace.WILDCARD, None), ast.Name(util.literal_replace.WILDCARD, None), None
1874+
),
1875+
"*[*]",
1876+
),
1877+
(
1878+
ast.BinOp(
1879+
ast.Name(util.literal_replace.WILDCARD, None),
1880+
ast.Add(),
1881+
ast.Name(util.literal_replace.WILDCARD, None),
1882+
),
1883+
"* + *",
1884+
"(* + *)",
1885+
),
1886+
(ast.List([util.literal_replace.WILDCARD], None), "[*]"),
1887+
(ast.Tuple([util.literal_replace.WILDCARD], None), "(*,)"),
1888+
]
1889+
)
1890+
@unittest.skipUnless(util.ast_unparse is not None, "`ast.unparse` available from Python3.9")
1891+
def test_literal_replace_wildcard_unparse(self, orig, expected, old_unparse_fallback=None):
1892+
res = util.ast_unparse(orig)
1893+
if old_unparse_fallback:
1894+
self.assertIn(res, [expected, old_unparse_fallback])
1895+
else:
1896+
self.assertEqual(res, expected)
1897+
17671898

17681899
def not_doing_anything_converter(el):
17691900
return True

0 commit comments

Comments
 (0)