77import sys
88import tokenize
99import traceback
10+ import types
1011from token import tok_name
12+ from typing import Any , Iterator
1113
1214from _testinternalcapi import compiler_codegen , optimize_cfg
1315
1416
15- def view_tokens (code : str ) -> str :
16- out = []
17+ def _as_view (rows : list [tuple [str , int | None ]]) -> dict [str , Any ]:
18+ text_lines = [row [0 ] for row in rows ]
19+ src_lines = [row [1 ] for row in rows ]
20+ return {"text" : "\n " .join (text_lines ), "lines" : src_lines }
21+
22+
23+ def view_tokens (code : str ) -> dict [str , Any ]:
24+ rows = []
1725 toks = tokenize .tokenize (io .BytesIO (code .encode ("utf-8" )).readline )
1826 current_line = 0
1927 for t in toks :
@@ -24,14 +32,77 @@ def view_tokens(code: str) -> str:
2432 marker = f"{ line :4d} : "
2533 else :
2634 marker = " "
27- out .append (f"{ marker } { tok_name [t .exact_type ]:10} { t .string !r} " )
35+ rows .append (
36+ (
37+ f"{ marker } { tok_name [t .exact_type ]:10} { t .string !r} " ,
38+ line if line > 0 else None ,
39+ )
40+ )
2841 current_line = line
29- return "\n " .join (out )
42+ return _as_view (rows )
43+
44+
45+ def _has_ast_children (node : ast .AST ) -> bool :
46+ if isinstance (node , ast .Name ):
47+ return False
48+ SENTINEL = object ()
49+ for name in node ._fields :
50+ value = getattr (node , name , SENTINEL )
51+ if isinstance (value , (list , ast .AST )):
52+ return True
53+ return False
54+
55+
56+ def _ast_attr_repr (node : ast .AST , attr : str ) -> str :
57+ value = getattr (node , attr , ...)
58+ if isinstance (value , (ast .Load , ast .Store , ast .Del )):
59+ return value .__class__ .__name__
60+ return repr (value )
61+
62+
63+ def _dump_ast (tree : ast .AST ) -> Iterator [tuple [str , int | None ]]:
64+ SENTINEL = object ()
65+ indent = " "
66+
67+ def walk (
68+ node : Any , level : int = 0 , last_line : int = 0 , prepend : str = ""
69+ ) -> Iterator [tuple [str , int ]]:
70+ prefix = f"{ indent * level } { prepend } "
71+ if isinstance (node , ast .AST ):
72+ fields = node ._fields
73+ start = getattr (node , "lineno" , last_line ) or last_line
74+ if not _has_ast_children (node ):
75+ args = ", " .join (f"{ n } ={ _ast_attr_repr (node , n )} " for n in fields )
76+ yield f"{ prefix } { node .__class__ .__name__ } ({ args } )" , start
77+ else :
78+ yield f"{ prefix } { node .__class__ .__name__ } ()" , start
79+ for name in fields :
80+ value = getattr (node , name , SENTINEL )
81+ if value is SENTINEL :
82+ continue
83+ yield from walk (value , level + 1 , start , f"{ name } =" )
84+ elif isinstance (node , list ):
85+ if len (node ) == 1 and not _has_ast_children (node [0 ]):
86+ inner = list (walk (node [0 ], level , last_line , prepend + "[" ))
87+ if len (inner ) == 1 :
88+ text , line = inner [0 ]
89+ yield text + "]" , line
90+ return
91+ yield from inner
92+ else :
93+ yield f"{ prefix } []" , last_line
94+ for value in node :
95+ yield from walk (value , level + 1 , last_line )
96+ else :
97+ yield f"{ prefix } { node !r} " , last_line
3098
99+ for text , line in walk (tree ):
100+ yield text , (line if line and line > 0 else None )
31101
32- def view_ast (code : str , * , optimize : bool = False ) -> str :
102+
103+ def view_ast (code : str , * , optimize : bool = False ) -> dict [str , Any ]:
33104 tree = ast .parse (code , optimize = 1 ) if optimize else ast .parse (code )
34- return ast . dump ( tree , indent = 4 )
105+ return _as_view ( list ( _dump_ast ( tree )) )
35106
36107
37108class _PseudoArgResolver (dis .ArgResolver ):
@@ -44,10 +115,13 @@ def offset_from_jump_arg(self, op, arg, offset):
44115class _CaptureStream :
45116 def __init__ (self ):
46117 self .lines = []
118+ self .src_lines = []
119+ self .current_line = None
47120
48121 def write (self , line ):
49122 if line .strip ():
50123 self .lines .append (line )
124+ self .src_lines .append (self .current_line )
51125
52126
53127def _iter_instructions (insts , resolver ):
@@ -78,20 +152,28 @@ def _iter_instructions(insts, resolver):
78152 )
79153
80154
81- def _disassemble (insts_list , co_consts ) -> str :
155+ class _LineTrackingFormatter (dis .Formatter ):
156+ def print_instruction (self , instr , mark_as_current = False ):
157+ line = getattr (instr , "line_number" , None )
158+ if line :
159+ self .file .current_line = line
160+ super ().print_instruction (instr , mark_as_current = mark_as_current )
161+
162+
163+ def _disassemble (insts_list , co_consts ) -> dict [str , Any ]:
82164 stream = _CaptureStream ()
83165 jump_targets = [
84166 t for op , t , * _ in insts_list if op in dis .hasjump or op in dis .hasexc
85167 ]
86168 labels_map = {o : i for i , o in enumerate (jump_targets , start = 1 )}
87169 resolver = _PseudoArgResolver (co_consts = co_consts , labels_map = labels_map )
88- fmt = dis . Formatter (
170+ fmt = _LineTrackingFormatter (
89171 file = stream ,
90172 lineno_width = 4 ,
91173 label_width = 4 + len (str (len (labels_map ))),
92174 )
93175 dis .print_instructions (_iter_instructions (insts_list , resolver ), None , fmt )
94- return " \n " .join (stream .lines )
176+ return { "text" : " \n " .join (stream .lines ), "lines" : list ( stream . src_lines )}
95177
96178
97179class _ConstPlaceholder :
@@ -216,7 +298,35 @@ def _instruction_items(insts):
216298 return list (insts )
217299
218300
219- def view_pseudo (code : str , * , optimize : bool = False ) -> str :
301+ def _iter_nested_code_objects (co : types .CodeType ) -> Iterator [types .CodeType ]:
302+ for const in co .co_consts :
303+ if isinstance (const , types .CodeType ):
304+ yield const
305+ yield from _iter_nested_code_objects (const )
306+
307+
308+ def _heading_view (co : types .CodeType ) -> dict [str , Any ]:
309+ name = getattr (co , "co_qualname" , None ) or co .co_name
310+ text = f"\n Disassembly of <code object { name } at line { co .co_firstlineno } >:"
311+ return {"text" : text , "lines" : [None ] * (text .count ("\n " ) + 1 )}
312+
313+
314+ def _combine_views (* parts : dict [str , Any ]) -> dict [str , Any ]:
315+ text_segs = []
316+ src_lines = []
317+ for p in parts :
318+ text_segs .append (p ["text" ])
319+ src_lines .extend (p ["lines" ])
320+ return {"text" : "\n " .join (text_segs ), "lines" : src_lines }
321+
322+
323+ def _nested_compiled_views (co : types .CodeType ) -> Iterator [dict [str , Any ]]:
324+ for nested in _iter_nested_code_objects (co ):
325+ yield _heading_view (nested )
326+ yield _disassemble (list (dis .Bytecode (nested )), list (nested .co_consts ))
327+
328+
329+ def view_pseudo (code : str , * , optimize : bool = False ) -> dict [str , Any ]:
220330 insts , metadata = compiler_codegen (ast .parse (code , optimize = 1 ), "<source>" , 0 )
221331 co_consts = _merge_co_consts (
222332 _co_consts_from_metadata (metadata ), _compiled_co_consts (code )
@@ -228,16 +338,18 @@ def view_pseudo(code: str, *, optimize: bool = False) -> str:
228338 insts = optimize_cfg (insts , co_consts , 0 )
229339 items = _instruction_items (insts )
230340 adjusted_consts = _apply_annotations_const_workaround (items , co_consts )
231- return _disassemble (items , _fit_co_consts (items , adjusted_consts ))
341+ top = _disassemble (items , _fit_co_consts (items , adjusted_consts ))
342+ co = compile (code , "<source>" , "exec" , optimize = 1 )
343+ return _combine_views (top , * _nested_compiled_views (co ))
232344
233345
234- def view_compiled (code : str ) -> str :
346+ def view_compiled (code : str ) -> dict [ str , Any ] :
235347 # assemble_code_object requires metadata["consts"] that compiler_codegen
236348 # no longer emits. Fall back to the public compile() API which yields an
237349 # equivalent final code object (with real consts).
238350 co = compile (code , "<source>" , "exec" , optimize = 1 )
239- items = list (dis .Bytecode (co ))
240- return _disassemble ( items , list (co . co_consts ))
351+ top = _disassemble ( list (dis .Bytecode (co )), list ( co . co_consts ))
352+ return _combine_views ( top , * _nested_compiled_views (co ))
241353
242354
243355VIEWS = {
@@ -258,9 +370,11 @@ def main() -> int:
258370 result = {"python_version" : sys .version .split ()[0 ]}
259371 for name , fn in VIEWS .items ():
260372 try :
261- result [ name ] = fn (code )
373+ view = fn (code )
262374 except Exception :
263- result [name ] = traceback .format_exc ()
375+ text = traceback .format_exc ()
376+ view = {"text" : text , "lines" : [None ] * len (text .splitlines ())}
377+ result [name ] = view
264378 json .dump (result , sys .stdout )
265379 return 0
266380
0 commit comments