44import warnings
55from pathlib import Path
66from textwrap import dedent
7- from typing import Generator , List
7+ from typing import Generator , List , Optional , Tuple
88
99__all__ = ['Debug' , 'debug' ]
1010CWD = Path ('.' ).resolve ()
@@ -66,6 +66,11 @@ class Debug:
6666 output_class = DebugOutput
6767 # 50 lines should be enough to make sure we always get the entire function definition
6868 frame_context_length = 50
69+ complex_nodes = (
70+ ast .Call ,
71+ ast .IfExp , ast .BoolOp , ast .BinOp , ast .Compare ,
72+ ast .DictComp , ast .ListComp , ast .SetComp , ast .GeneratorExp
73+ )
6974
7075 def __call__ (self , * args , ** kwargs ):
7176 print (self ._process (args , kwargs , r'debug *\(' ), flush = True )
@@ -88,26 +93,23 @@ def _process(self, args, kwargs, func_regex):
8893 # happens if filename path is not within CWD
8994 pass
9095
91- call_lines = []
92- # print(call_frame)
93- # from pprint import pprint
94- # pprint(call_frame.code_context)
9596 if call_frame .code_context :
96- for line in range (call_frame .index , 0 , - 1 ):
97- new_line = call_frame .code_context [line ]
98- call_lines .append (new_line )
99- if re .search (func_regex , new_line ):
100- break
101- call_lines .reverse ()
102- lineno = call_frame .lineno - len (call_lines ) + 1
97+ func_ast , code_lines , lineno = self ._parse_code (call_frame , func_regex , filename )
98+ if func_ast :
99+ arguments = list (self ._process_args (func_ast , code_lines , args , kwargs ))
100+ else :
101+ # parsing failed
102+ arguments = list (self ._args_inspection_failed (args , kwargs ))
103103 else :
104- lineno = call_frame .lineno - len (call_lines )
104+ lineno = call_frame .lineno
105+ warnings .warn ('no code context for debug call, code inspection impossible' , RuntimeWarning )
106+ arguments = list (self ._args_inspection_failed (args , kwargs ))
105107
106108 return self .output_class (
107109 filename = filename ,
108110 lineno = lineno ,
109111 frame = call_frame .function ,
110- arguments = list ( self . _process_args ( call_lines , args , kwargs , call_frame ))
112+ arguments = arguments
111113 )
112114
113115 def _args_inspection_failed (self , args , kwargs ):
@@ -116,44 +118,12 @@ def _args_inspection_failed(self, args, kwargs):
116118 for name , value in kwargs .items ():
117119 yield self .output_class .arg_class (value , name = name )
118120
119- def _process_args (self , call_lines , args , kwargs , call_frame ) -> Generator [DebugArgument , None , None ]: # noqa: C901
120- if not call_lines :
121- warnings .warn ('no code context for debug call, code inspection impossible' , RuntimeWarning )
122- yield from self ._args_inspection_failed (args , kwargs )
123- return
124-
125- code = dedent ('' .join (call_lines ))
126- # print(code)
127- try :
128- func_ast = ast .parse (code ).body [0 ].value
129- except SyntaxError as e1 :
130- # if the trailing bracket of the function is on a new line eg.
131- # debug(
132- # foo, bar,
133- # )
134- # inspect ignores it with index and we have to add it back
135- code2 = code + call_frame .code_context [call_frame .index + 1 ]
136- try :
137- func_ast = ast .parse (code2 ).body [0 ].value
138- except SyntaxError :
139- warnings .warn ('error passing code:\n "{}"\n Error: {}' .format (code , e1 ), SyntaxWarning )
140- yield from self ._args_inspection_failed (args , kwargs )
141- return
142- else :
143- code = code2
144-
145- code_lines = [l for l in code .split ('\n ' ) if l ]
146- # this removes the trailing bracket from the lines of code meaning it doesn't appear in the
147- # representation of the last argument
148- code_lines [- 1 ] = code_lines [- 1 ][:- 1 ]
149-
121+ def _process_args (self , func_ast , code_lines , args , kwargs ) -> Generator [DebugArgument , None , None ]: # noqa: C901
150122 arg_offsets = list (self ._get_offsets (func_ast ))
151123 for arg , ast_node , i in zip (args , func_ast .args , range (1000 )):
152124 if isinstance (ast_node , ast .Name ):
153125 yield self .output_class .arg_class (arg , name = ast_node .id )
154- elif isinstance (ast_node , (ast .Str , ast .Bytes , ast .Num , ast .List , ast .Dict , ast .Set )):
155- yield self .output_class .arg_class (arg )
156- elif isinstance (ast_node , (ast .Call , ast .Compare )):
126+ elif isinstance (ast_node , self .complex_nodes ):
157127 # TODO replace this hack with astor when it get's round to a new release
158128 start_line , start_col = ast_node .lineno - 1 , ast_node .col_offset
159129 end_line , end_col = len (code_lines ) - 1 , None
@@ -170,7 +140,6 @@ def _process_args(self, call_lines, args, kwargs, call_frame) -> Generator[Debug
170140 )
171141 yield self .output_class .arg_class (arg , name = ' ' .join (name_lines ).strip (' ,' ))
172142 else :
173- warnings .warn ('Unknown type: {}' .format (ast .dump (ast_node )), RuntimeWarning )
174143 yield self .output_class .arg_class (arg )
175144
176145 kw_arg_names = {}
@@ -180,6 +149,47 @@ def _process_args(self, call_lines, args, kwargs, call_frame) -> Generator[Debug
180149 for name , value in kwargs .items ():
181150 yield self .output_class .arg_class (value , name = name , variable = kw_arg_names .get (name ))
182151
152+ def _parse_code (self , call_frame , func_regex , filename ) -> Tuple [Optional [ast .AST ], Optional [List [str ]], int ]:
153+ call_lines = []
154+ for line in range (call_frame .index , 0 , - 1 ):
155+ new_line = call_frame .code_context [line ]
156+ call_lines .append (new_line )
157+ if re .search (func_regex , new_line ):
158+ break
159+ call_lines .reverse ()
160+ lineno = call_frame .lineno - len (call_lines ) + 1
161+
162+ original_code = code = dedent ('' .join (call_lines ))
163+ func_ast = None
164+ tail_index = call_frame .index
165+ try :
166+ func_ast = ast .parse (code , filename = filename ).body [0 ].value
167+ except SyntaxError as e1 :
168+ # if the trailing bracket(s) of the function is/are on a new line eg.
169+ # debug(
170+ # foo, bar,
171+ # )
172+ # inspect ignores it when setting index and we have to add it back
173+ for extra in range (2 , 6 ):
174+ extra_lines = call_frame .code_context [tail_index + 1 :tail_index + extra ]
175+ code = dedent ('' .join (call_lines + extra_lines ))
176+ try :
177+ func_ast = ast .parse (code ).body [0 ].value
178+ except SyntaxError :
179+ pass
180+ else :
181+ break
182+
183+ if not func_ast :
184+ warnings .warn ('error passing code:\n "{}"\n Error: {}' .format (original_code , e1 ), SyntaxWarning )
185+ return None , None , lineno
186+
187+ code_lines = [l for l in code .split ('\n ' ) if l ]
188+ # this removes the trailing bracket from the lines of code meaning it doesn't appear in the
189+ # representation of the last argument
190+ code_lines [- 1 ] = code_lines [- 1 ][:- 1 ]
191+ return func_ast , code_lines , lineno
192+
183193 @classmethod
184194 def _get_offsets (cls , func_ast ):
185195 for arg in func_ast .args :
0 commit comments