Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/tensorflow/python/autograph/pyct/origin_info.py: 25%
130 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 07:57 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 07:57 +0000
1# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Container for origin source code information before AutoGraph compilation."""
16import collections
17import difflib
18import io
19import os
20import tokenize
22import gast
24from tensorflow.python.autograph.pyct import anno
25from tensorflow.python.autograph.pyct import ast_util
26from tensorflow.python.autograph.pyct import parser
27from tensorflow.python.autograph.pyct import pretty_printer
28from tensorflow.python.util import tf_inspect
31class LineLocation(
32 collections.namedtuple('LineLocation', ('filename', 'lineno'))):
33 """Similar to Location, but without column information.
35 Attributes:
36 filename: Text
37 lineno: int, 1-based
38 """
39 pass
42class Location(
43 collections.namedtuple('Location', ('filename', 'lineno', 'col_offset'))):
44 """Encodes code location information.
46 Attributes:
47 filename: Text
48 lineno: int, 1-based
49 col_offset: int
50 line_loc: LineLocation
51 """
53 @property
54 def line_loc(self):
55 return LineLocation(self.filename, self.lineno)
58class OriginInfo(
59 collections.namedtuple(
60 'OriginInfo',
61 ('loc', 'function_name', 'source_code_line', 'comment'))):
62 """Container for information about the source code before conversion.
64 Attributes:
65 loc: Location
66 function_name: Optional[Text]
67 source_code_line: Text
68 comment: Optional[Text]
69 """
71 def as_frame(self):
72 """Returns a 4-tuple consistent with the return of traceback.extract_tb."""
73 return (self.loc.filename, self.loc.lineno, self.function_name,
74 self.source_code_line)
76 def __repr__(self):
77 if self.loc.filename:
78 return '{}:{}:{}'.format(
79 os.path.split(self.loc.filename)[1], self.loc.lineno,
80 self.loc.col_offset)
81 return '<no file>:{}:{}'.format(self.loc.lineno, self.loc.col_offset)
84# TODO(mdan): This source map should be a class - easier to refer to.
85def create_source_map(nodes, code, filepath):
86 """Creates a source map between an annotated AST and the code it compiles to.
88 Note: this function assumes nodes nodes, code and filepath correspond to the
89 same code.
91 Args:
92 nodes: Iterable[ast.AST, ...], one or more AST modes.
93 code: Text, the source code in which nodes are found.
94 filepath: Text
96 Returns:
97 Dict[LineLocation, OriginInfo], mapping locations in code to locations
98 indicated by origin annotations in node.
99 """
100 reparsed_nodes = parser.parse(code, preamble_len=0, single_node=False)
101 for node in reparsed_nodes:
102 resolve(node, code, filepath, node.lineno, node.col_offset)
104 source_map = {}
106 try:
107 for before, after in ast_util.parallel_walk(nodes, reparsed_nodes):
108 # Note: generated code might not be mapped back to its origin.
109 # TODO(mdan): Generated code should always be mapped to something.
110 origin_info = anno.getanno(before, anno.Basic.ORIGIN, default=None)
111 final_info = anno.getanno(after, anno.Basic.ORIGIN, default=None)
112 if origin_info is None or final_info is None:
113 continue
115 # Note: the keys are by line only, excluding the column offset.
116 line_loc = LineLocation(final_info.loc.filename, final_info.loc.lineno)
118 existing_origin = source_map.get(line_loc)
119 if existing_origin is not None:
120 # Overlaps may exist because of child nodes, but almost never to
121 # different line locations. Exception make decorated functions, where
122 # both lines are mapped to the same line in the AST.
124 # Line overlaps: keep bottom node.
125 if existing_origin.loc.line_loc == origin_info.loc.line_loc:
126 if existing_origin.loc.lineno >= origin_info.loc.lineno:
127 continue
129 # In case of column overlaps, keep the leftmost node.
130 if existing_origin.loc.col_offset <= origin_info.loc.col_offset:
131 continue
133 source_map[line_loc] = origin_info
135 except ValueError as err:
136 new_msg = 'Inconsistent ASTs detected. This is a bug. Cause: \n'
137 new_msg += str(err)
138 new_msg += 'Diff:\n'
140 for n, rn in zip(nodes, reparsed_nodes):
141 nodes_str = pretty_printer.fmt(n, color=False, noanno=True)
142 reparsed_nodes_str = pretty_printer.fmt(rn, color=False, noanno=True)
143 diff = difflib.context_diff(
144 nodes_str.split('\n'),
145 reparsed_nodes_str.split('\n'),
146 fromfile='Original nodes',
147 tofile='Reparsed nodes',
148 n=7)
149 diff = '\n'.join(diff)
150 new_msg += diff + '\n'
151 raise ValueError(new_msg)
153 return source_map
156class _Function:
158 def __init__(self, name):
159 self.name = name
162class OriginResolver(gast.NodeVisitor):
163 """Annotates an AST with additional source information like file name."""
165 def __init__(self, root_node, source_lines, comments_map,
166 context_lineno, context_col_offset,
167 filepath):
168 self._source_lines = source_lines
169 self._comments_map = comments_map
171 if (hasattr(root_node, 'decorator_list') and root_node.decorator_list and
172 hasattr(root_node.decorator_list[0], 'lineno')):
173 # Typical case: functions. The line number of the first decorator
174 # is more accurate than the line number of the function itself in
175 # 3.8+. In earier versions they coincide.
176 self._lineno_offset = context_lineno - root_node.decorator_list[0].lineno
177 else:
178 # Fall back to the line number of the root node.
179 self._lineno_offset = context_lineno - root_node.lineno
181 self._col_offset = context_col_offset - root_node.col_offset
183 self._filepath = filepath
185 self._function_stack = []
187 def _absolute_lineno(self, lineno):
188 return lineno + self._lineno_offset
190 def _absolute_col_offset(self, col_offset):
191 if col_offset is None:
192 return 0
193 return col_offset + self._col_offset
195 def _attach_origin_info(self, node):
196 lineno = getattr(node, 'lineno', None)
197 col_offset = getattr(node, 'col_offset', None)
199 if lineno is None:
200 return
202 if self._function_stack:
203 function_name = self._function_stack[-1].name
204 else:
205 function_name = None
207 source_code_line = self._source_lines[lineno - 1]
208 comment = self._comments_map.get(lineno)
210 loc = Location(self._filepath, self._absolute_lineno(lineno),
211 self._absolute_col_offset(col_offset))
212 origin = OriginInfo(loc, function_name, source_code_line, comment)
213 anno.setanno(node, 'lineno', lineno)
214 anno.setanno(node, anno.Basic.ORIGIN, origin)
216 def visit(self, node):
217 entered_function = False
218 if isinstance(node, gast.FunctionDef):
219 entered_function = True
220 self._function_stack.append(_Function(node.name))
222 self._attach_origin_info(node)
223 self.generic_visit(node)
225 if entered_function:
226 self._function_stack.pop()
229def resolve(node, source, context_filepath, context_lineno, context_col_offset):
230 """Adds origin information to an AST, based on the source it was loaded from.
232 This allows us to map the original source code line numbers to generated
233 source code.
235 Note: the AST may be a part of a larger context (e.g. a function is part of
236 a module that may contain other things). However, this function does not
237 assume the source argument contains the entire context, nor that it contains
238 only code corresponding to node itself. However, it assumes that node was
239 parsed from the given source code.
240 For this reason, two extra arguments are required, and they indicate the
241 location of the node in the original context.
243 Args:
244 node: gast.AST, the AST to annotate.
245 source: Text, the source code representing node.
246 context_filepath: Text
247 context_lineno: int
248 context_col_offset: int
249 """
250 # TODO(mdan): Pull this to a separate utility.
251 code_reader = io.StringIO(source)
252 comments_map = {}
253 try:
254 for token in tokenize.generate_tokens(code_reader.readline):
255 tok_type, tok_string, loc, _, _ = token
256 srow, _ = loc
257 if tok_type == tokenize.COMMENT:
258 comments_map[srow] = tok_string.strip()[1:].strip()
259 except tokenize.TokenError:
260 if isinstance(node, gast.Lambda):
261 # Source code resolution in older Python versions is brittle for
262 # lambda functions, and may contain garbage.
263 pass
264 else:
265 raise
267 source_lines = source.split('\n')
268 visitor = OriginResolver(node, source_lines, comments_map,
269 context_lineno, context_col_offset,
270 context_filepath)
271 visitor.visit(node)
274def resolve_entity(node, source, entity):
275 """Like resolve, but extracts the context information from an entity."""
276 lines, lineno = tf_inspect.getsourcelines(entity)
277 filepath = tf_inspect.getsourcefile(entity)
279 # Poor man's attempt at guessing the column offset: count the leading
280 # whitespace. This might not work well with tabs.
281 definition_line = lines[0]
282 col_offset = len(definition_line) - len(definition_line.lstrip())
284 resolve(node, source, filepath, lineno, col_offset)
287def copy_origin(from_node, to_node):
288 """Copies the origin info from a node to another, recursively."""
289 origin = anno.Basic.ORIGIN.of(from_node, default=None)
290 if origin is None:
291 return
292 if not isinstance(to_node, (list, tuple)):
293 to_node = (to_node,)
294 for node in to_node:
295 for n in gast.walk(node):
296 anno.setanno(n, anno.Basic.ORIGIN, origin)