Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mako/ast.py: 3%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# mako/ast.py
2# Copyright 2006-2026 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
7"""utilities for analyzing expressions and blocks of Python
8code, as well as generating Python from AST nodes"""
10import re
12from mako import exceptions
13from mako import pyparser
16class PythonCode:
17 """represents information about a string containing Python code"""
19 def __init__(self, code, lineno_offset=0, **exception_kwargs):
20 self.code = code
22 # represents all identifiers which are assigned to at some point in
23 # the code
24 self.declared_identifiers = set()
26 # represents all identifiers which are referenced before their
27 # assignment, if any
28 self.undeclared_identifiers = set()
30 # note that an identifier can be in both the undeclared and declared
31 # lists.
33 # using AST to parse instead of using code.co_varnames,
34 # code.co_names has several advantages:
35 # - we can locate an identifier as "undeclared" even if
36 # its declared later in the same block of code
37 # - AST is less likely to break with version changes
38 # (for example, the behavior of co_names changed a little bit
39 # in python version 2.5)
40 if isinstance(code, str):
41 stripped = code.lstrip()
43 # the code of a <% %> block usually begins on the line after the
44 # tag; count the lines that are stripped off so that a syntax
45 # error is reported against the line it is on
46 lineno_offset += code[: len(code) - len(stripped)].count("\n")
48 expr = pyparser.parse(
49 stripped,
50 "exec",
51 lineno_offset=lineno_offset,
52 **exception_kwargs,
53 )
54 else:
55 expr = code
57 f = pyparser.FindIdentifiers(self, **exception_kwargs)
58 f.visit(expr)
61class ArgumentList:
62 """parses a fragment of code as a comma-separated list of expressions"""
64 def __init__(self, code, **exception_kwargs):
65 self.codeargs = []
66 self.args = []
67 self.declared_identifiers = set()
68 self.undeclared_identifiers = set()
69 if isinstance(code, str):
70 if re.match(r"\S", code) and not re.match(r",\s*$", code):
71 # if theres text and no trailing comma, insure its parsed
72 # as a tuple by adding a trailing comma
73 code += ","
74 expr = pyparser.parse(code, "exec", **exception_kwargs)
75 else:
76 expr = code
78 f = pyparser.FindTuple(self, PythonCode, **exception_kwargs)
79 f.visit(expr)
82class PythonFragment(PythonCode):
83 """extends PythonCode to provide identifier lookups in partial control
84 statements
86 e.g.::
88 for x in 5:
89 elif y==9:
90 except (MyException, e):
92 """
94 def __init__(self, code, **exception_kwargs):
95 m = re.match(r"^(\w+)(?:\s+(.*?))?:\s*(#|$)", code.strip(), re.S)
96 if not m:
97 raise exceptions.CompileException(
98 "Fragment '%s' is not a partial control statement" % code,
99 **exception_kwargs,
100 )
101 if m.group(3):
102 code = code[: m.start(3)]
103 keyword, expr = m.group(1, 2)
105 # a statement that is only valid as a continuation is completed by
106 # a line placed before it; the line the fragment is on is that many
107 # lines further along than the code which is parsed
108 lineno_offset = 0
110 if keyword in ["for", "if", "while"]:
111 code = code + "pass"
112 elif keyword == "try":
113 code = code + "pass\nexcept:pass"
114 elif keyword in ["elif", "else"]:
115 code = "if False:pass\n" + code + "pass"
116 lineno_offset = -1
117 elif keyword == "except":
118 code = "try:pass\n" + code + "pass"
119 lineno_offset = -1
120 elif keyword == "with":
121 code = code + "pass"
122 else:
123 raise exceptions.CompileException(
124 "Unsupported control keyword: '%s'" % keyword,
125 **exception_kwargs,
126 )
127 super().__init__(code, lineno_offset=lineno_offset, **exception_kwargs)
130class FunctionDecl:
131 """function declaration"""
133 def __init__(self, code, allow_kwargs=True, **exception_kwargs):
134 self.code = code
135 expr = pyparser.parse(code, "exec", **exception_kwargs)
137 f = pyparser.ParseFunc(self, **exception_kwargs)
138 f.visit(expr)
139 if not hasattr(self, "funcname"):
140 raise exceptions.CompileException(
141 "Code '%s' is not a function declaration" % code,
142 **exception_kwargs,
143 )
144 if not allow_kwargs and self.kwargs:
145 raise exceptions.CompileException(
146 "'**%s' keyword argument not allowed here"
147 % self.kwargnames[-1],
148 **exception_kwargs,
149 )
151 def get_argument_expressions(self, as_call=False):
152 """Return the argument declarations of this FunctionDecl as a printable
153 list.
155 By default the return value is appropriate for writing in a ``def``;
156 set `as_call` to true to build arguments to be passed to the function
157 instead (assuming locals with the same names as the arguments exist).
158 """
160 namedecls = []
162 # Build in reverse order, since defaults and slurpy args come last
163 argnames = self.argnames[::-1]
164 kwargnames = self.kwargnames[::-1]
165 defaults = self.defaults[::-1]
166 kwdefaults = self.kwdefaults[::-1]
168 # Named arguments
169 if self.kwargs:
170 namedecls.append("**" + kwargnames.pop(0))
172 for name in kwargnames:
173 # Keyword-only arguments must always be used by name, so even if
174 # this is a call, print out `foo=foo`
175 if as_call:
176 namedecls.append("%s=%s" % (name, name))
177 elif kwdefaults:
178 default = kwdefaults.pop(0)
179 if default is None:
180 # The AST always gives kwargs a default, since you can do
181 # `def foo(*, a=1, b, c=3)`
182 namedecls.append(name)
183 else:
184 namedecls.append(
185 "%s=%s"
186 % (name, pyparser.ExpressionGenerator(default).value())
187 )
188 else:
189 namedecls.append(name)
191 # Positional arguments
192 if self.varargs:
193 namedecls.append("*" + argnames.pop(0))
195 for name in argnames:
196 if as_call or not defaults:
197 namedecls.append(name)
198 else:
199 default = defaults.pop(0)
200 namedecls.append(
201 "%s=%s"
202 % (name, pyparser.ExpressionGenerator(default).value())
203 )
205 namedecls.reverse()
206 return namedecls
208 @property
209 def allargnames(self):
210 return tuple(self.argnames) + tuple(self.kwargnames)
213class FunctionArgs(FunctionDecl):
214 """the argument portion of a function declaration"""
216 def __init__(self, code, **kwargs):
217 super().__init__("def ANON(%s):pass" % code, **kwargs)