在阅读 Odoo 的后台 Python 代码中,经常看到 AST 相关的代码,每次遇到这些代码都不自主跳过;但就在昨天在 Conda Python 3.8 的新环境中,Odoo 12 竟然没有跑起来,抛出的异常在 AST 相关的位置上。
odoo.addons.base.models.qweb.QWebException: required field "posonlyargs" missing from arguments
Traceback (most recent call last):
File "odoo/odoo/addons/base/models/qweb.py", line 332, in compile
unsafe_eval(compile(astmod, '', 'exec'), ns)
TypeError: required field "posonlyargs" missing from arguments
Error when compiling AST
TypeError: required field "posonlyargs" missing from arguments
Template: 173
Path: /templates/t/t/form/input[2]
Node:
从异常的信息来看是 AST 在处理
Node:
这个 Node 的时候出了问题。狠了狠心借着这个机会看看 Python AST 究竟是啥。
什么是 AST
AST 即抽象语法树,百度一个定义:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有两个分支的节点来表示。
Python 官方文档 — Abstract Syntax Trees
AST 模块可帮助 Python 应用程序处理 Python 抽象语法语法的树。 每个Python 版本都可能更改抽象语法。 该模块有助于以编程方式找出当前语法的外观。
通过将ast.PyCF_ONLY_AST作为标志传递给compile()内置函数,或使用此模块中提供的parse()帮助器,可以生成抽象语法树。 结果将是一棵对象树,其所有类均继承自ast.AST。 可以使用内置的compile()函数将抽象语法树编译为Python代码对象。
Odoo 借助 Python AST 在抽象语法树的层次动态构造 Python 程序,从而能够将 QWeb Template 中的语法映射成 Python 代码。通过直接构造 AST,然后再 Compile,再求值。从而能够在 QWeb 中使用 Python 的语法,在模板文件中直接使用Python代码逻辑。
AST 中的 Node 递归起来就是一个 Tree,每个节点就是 Node;ast.parse(code_string) 能把 Python 代码分析成 AST,Python 本身没有提供把 AST 变成 Python 代码的功能,有一些第三方库可以做这个事情。举个简单的例子:
>>> import ast
>>> ast.dump(ast.parse('-5'))
'Module(body=[Expr(value=UnaryOp(op=USub(), operand=Constant(value=5, kind=None)))], type_ignores=[])'
>>>
上面的例子是给 ‘-5’ 这个常数构造 AST,其body 是一个 Expr,value 是一个 UnaryOp,operand 是 Constant。
再看一个:
>>> ast.dump(ast.parse('print("Hello World")'))
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Constant(value='Hello World', kind=None)], keywords=[]))], type_ignores=[])"
>>>
这个Expr 就是一个 Call,Call 需要的参数是 func,args。
>>> node = ast.UnaryOp()
>>> node.op = ast.USub()
>>> node.operand = ast.Constant()
>>> node.operand.value = 5
>>> node.operand.lineno = 0
>>> node.operand.col_offset = 0
>>> node.lineno = 0
>>> node.col_offset = 0
>>>
>>> ast.dump(node)
'UnaryOp(op=USub(), operand=Constant(value=5))'
>>>
上面就是手动构造一个 ast node。同样再看一个例子:
>>> x = """
... def p(s):
... print(s)
... """
>>> x
'\ndef p(s):\n\tprint(s)\n'
>>> ast.dump(ast.parse(x))
"Module(body=[FunctionDef(name='p', args=arguments(posonlyargs=[], args=[arg(arg='s', annotation=None, type_comment=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Name(id='s', ctx=Load())], keywords=[]))], decorator_list=[], returns=None, type_comment=None)], type_ignores=[])"
>>>
这个例子是定义一个函数 FunctionDef,能看出来 AST 如何描述定义函数。
再回到 Odoo 的 qweb.py 都有对应的函数,如根据 QWeb 模板生成函数定义 FunctionDef:
def _create_def(self, options, body, prefix='fn', lineno=None):
""" Generate (and globally store) a rendering function definition AST
and return its name. The function takes parameters ``self``, ``append``,
``values``, ``options``, and ``log``. If ``body`` is empty, the function
simply returns ``None``.
"""
#assert body, "To create a compiled function 'body' ast list can't be empty"
name = self._make_name(prefix)
# def $name(self, append, values, options, log)
fn = ast.FunctionDef(
name=name,
args=arguments(args=[
arg(arg='self', annotation=None),
arg(arg='append', annotation=None),
arg(arg='values', annotation=None),
arg(arg='options', annotation=None),
arg(arg='log', annotation=None),
], defaults=[], vararg=None, kwarg=None, kwonlyargs=[], kw_defaults=[]),
body=body or [ast.Return()],
decorator_list=[])
if lineno is not None:
fn.lineno = lineno
options['ast_calls'].append(fn)
return name
有没有点感觉,少了点什么, arguments 中少了 posonlyargs,就是这里引起的异常,因为 Python 3.8 的 AST 中需要这个 posonlyargs,而 Odoo 在构造的时候没有指定。
AST Node
Python 的 AST 的 Node 有很多,可以分成 Literal (字面量),Variable(变量),Expression (表达式),Statement (语句),Control Flow(控制流),Function and Class (函数和类定义),Async await(异步)。
Literal Node
如一个数,True False, 字符串用 Constant 表示,Bytes,List,Tuple,Dict;一个 Dict 在代码中可以写成 {"a":1,**d},在 AST 中就是 ast.Dict。
>>> ast.dump(ast.parse('{"a":1, **d}'))
"Module(body=[Expr(value=Dict(keys=[Constant(value='a', kind=None), None], values=[Constant(value=1, kind=None), Name(id='d', ctx=Load())]))], type_ignores=[])"
写段简单的 Python Code,parse之后再把 Tree dump 出来;用这种方法很容易找到代码对应的 AST Node Class 是什么。
Variable Node
Name 用来表达变量的名字,id 表示以字符串表示的变量名,ctx可以是 Store,Load,Del,就是存储,加载,删除。看看对应的代码很容易理解:
>>> ast.dump(ast.parse("a"))
"Module(body=[Expr(value=Name(id='a', ctx=Load()))], type_ignores=[])"
>>> ast.dump(ast.parse("a = 2"))
"Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=2, kind=None), type_comment=None)], type_ignores=[])"
>>> ast.dump(ast.parse("del a"))
"Module(body=[Delete(targets=[Name(id='a', ctx=Del())])], type_ignores=[])"
>>>
所有的代码都是在 Python 的交互模式下运行,目前版本是 Python 3.8.0。
Python 3.8.0 (default, Nov 6 2019, 21:49:08)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ast
>>>
Expression (表达式)
Expr 类来表示一个表达式,前面已经看到好多 Expr 了。它更像一个容器。
当表达式(例如函数调用)本身作为语句(表达式语句)出现而未使用或存储其返回值时,它将包装在此容器中。 比如 -a,f(),a > 0 等等都是表达式。-a 已经在前文中提到了是用 Unary 来表示,再写个简单的:
>>> ast.dump(ast.parse("0 < a < 10"))
"Module(body=[Expr(value=Compare(left=Constant(value=0, kind=None), ops=[Lt(), Lt()], comparators=[Name(id='a', ctx=Load()), Constant(value=10, kind=None)]))], type_ignores=[])"
>>>
Expr 中的参数 value,可以是 Compare,Eq,NotEq 之类比较大小,位操作,取成员变量,函数调用等等。
>>> ast.dump(ast.parse("print('Hello World')"))
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Constant(value='Hello World', kind=None)], keywords=[]))], type_ignores=[])"
>>>
Statement (语句)
赋值语句是最基础的了:
>>> ast.dump(ast.parse("b = 1"))
"Module(body=[Assign(targets=[Name(id='b', ctx=Store())], value=Constant(value=1, kind=None), type_comment=None)], type_ignores=[])"
还有很多赋值语句,比如 a += 1 是 Augmented assignment,扩充赋值。
raise assert del pass 这些语句都专门对应 AST 的节点类 Raise,Assert,Delete,Pass。还有专门的导入类 对应 import 语句。
Control Flow (控制流)
就是代码块,也是结构化程序的基础了。if else for break continue try catch 等等都有直接对应的 Node Class。
写个小李子:
>>> x = """
... for a in b:
... if a > 1:
... continue
... else:
... break
... """
>>> ast.dump(ast.parse(x))
"Module(body=[For(target=Name(id='a', ctx=Store()), iter=Name(id='b', ctx=Load()), body=[If(test=Compare(left=Name(id='a', ctx=Load()), ops=[Gt()], comparators=[Constant(value=1, kind=None)]), body=[Continue()], orelse=[Break()])], orelse=[], type_comment=None)], type_ignores=[])"
>>>
Function 和 Class def
使用 FunctionDef 节点类。需要下面几个参数。
-
name
is a raw string of the function name. -
args
is aarguments
node. -
body
is the list of nodes inside the function. -
decorator_list
is the list of decorators to be applied, stored outermost first (i.e. the first in the list will be applied last). -
returns
is the return annotation (Python 3 only).
还是直接写个小李子:
>>> x = """
... def pp(s):
... print(s)
... return 0
... """
>>> ast.dump(ast.parse(x))
"Module(body=[FunctionDef(name='pp', args=arguments(posonlyargs=[], args=[arg(arg='s', annotation=None, type_comment=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Name(id='s', ctx=Load())], keywords=[])), Return(value=Constant(value=0, kind=None))], decorator_list=[], returns=None, type_comment=None)], type_ignores=[])"
>>>
Odoo 12 中不能适应 Python 3.8 就是构造 AST 有问题。当然还有 Lamba 和 Class 的构造方法,不一一列举。
Async 和 Await 异步支持
还是直接上个例子吧:
>>> x = """
... async def aa():
... await bb()
... """
>>> ast.dump(ast.parse(x))
"Module(body=[AsyncFunctionDef(name='aa', args=arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Await(value=Call(func=Name(id='bb', ctx=Load()), args=[], keywords=[])))], decorator_list=[], returns=None, type_comment=None)], type_ignores=[])"
>>>
可见是一个独特的 AsyncFunctionDef 来处理的。
遍历或者修改 AST Tree
面对一个已经存在 Tree,遍历它或者修改它或者显示它。前文中我们大量的使用 dump,就是能够以文本形式显示这个 Tree。
遍历的方法是继承 ast 的 NodeVisitor 或者直接使用它。
class FuncLister(ast.NodeVisitor):
def visit_FunctionDef(self, node):
print(node.name)
self.generic_visit(node)
FuncLister().visit(tree)
上面 overload 了 FunctionDef 的visit,同理可以重载其他的 Node。但是别忘了使用 generic_visit() 来执行基本的 visit 过程。
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(node.name)
比较直观的方法直接 walk 整个 Tree,然后挑出你感兴趣的部分。
如果你想修改 Tree,可以用 NodeTransformer。
class RewriteName(ast.NodeTransformer):
def visit_Name(self, node):
return ast.copy_location(ast.Subscript(
value=ast.Name(id='data', ctx=ast.Load()),
slice=ast.Index(value=ast.Str(s=node.id)),
ctx=node.ctx
), node)
tree = RewriteName().visit(tree)
编译执行 AST
AST 构造了程序,可以编译,然后执行。还是直接上例子:
>>> ast.dump(ast.parse("print('Hello World')"))
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Constant(value='Hello World', kind=None)], keywords=[]))], type_ignores=[])"
>>> am = ast.parse("print('Hello World')")
>>> exec(compile(am,'','exec'))
Hello World
>>>
exec 和 compile 都是 Python 的内置语句。
AST 用在何处
许多自动化测试工具,代码覆盖率工具依靠抽象语法树的功能来解析源代码,并发现代码中可能存在的缺陷和错误。 除此之外,AST还用于:
- 使IDE变得智能并使其成为众所周知的功能。
- 像Pylint这样的工具使用AST执行静态代码分析
- 自定义Python解释器
最重要的是 Odoo 引用了,通过简单了解 Python 的 AST,就不用担心看到 ast 相关的代码了。
本文参考引用了:
百度百科的抽象语法树定义
Python 官方网站对 AST 的描述
Green Tree Snakes - the missing Python AST docs
Python AST – Abstract Syntax Tree