4
4
import warnings
5
5
from pathlib import Path
6
6
from textwrap import dedent
7
- from typing import Generator , List
7
+ from typing import Generator , List , Optional , Tuple
8
8
9
9
__all__ = ['Debug' , 'debug' ]
10
10
CWD = Path ('.' ).resolve ()
@@ -66,6 +66,11 @@ class Debug:
66
66
output_class = DebugOutput
67
67
# 50 lines should be enough to make sure we always get the entire function definition
68
68
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
+ )
69
74
70
75
def __call__ (self , * args , ** kwargs ):
71
76
print (self ._process (args , kwargs , r'debug *\(' ), flush = True )
@@ -88,26 +93,23 @@ def _process(self, args, kwargs, func_regex):
88
93
# happens if filename path is not within CWD
89
94
pass
90
95
91
- call_lines = []
92
- # print(call_frame)
93
- # from pprint import pprint
94
- # pprint(call_frame.code_context)
95
96
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 ))
103
103
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 ))
105
107
106
108
return self .output_class (
107
109
filename = filename ,
108
110
lineno = lineno ,
109
111
frame = call_frame .function ,
110
- arguments = list ( self . _process_args ( call_lines , args , kwargs , call_frame ))
112
+ arguments = arguments
111
113
)
112
114
113
115
def _args_inspection_failed (self , args , kwargs ):
@@ -116,44 +118,12 @@ def _args_inspection_failed(self, args, kwargs):
116
118
for name , value in kwargs .items ():
117
119
yield self .output_class .arg_class (value , name = name )
118
120
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
150
122
arg_offsets = list (self ._get_offsets (func_ast ))
151
123
for arg , ast_node , i in zip (args , func_ast .args , range (1000 )):
152
124
if isinstance (ast_node , ast .Name ):
153
125
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 ):
157
127
# TODO replace this hack with astor when it get's round to a new release
158
128
start_line , start_col = ast_node .lineno - 1 , ast_node .col_offset
159
129
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
170
140
)
171
141
yield self .output_class .arg_class (arg , name = ' ' .join (name_lines ).strip (' ,' ))
172
142
else :
173
- warnings .warn ('Unknown type: {}' .format (ast .dump (ast_node )), RuntimeWarning )
174
143
yield self .output_class .arg_class (arg )
175
144
176
145
kw_arg_names = {}
@@ -180,6 +149,47 @@ def _process_args(self, call_lines, args, kwargs, call_frame) -> Generator[Debug
180
149
for name , value in kwargs .items ():
181
150
yield self .output_class .arg_class (value , name = name , variable = kw_arg_names .get (name ))
182
151
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
+
183
193
@classmethod
184
194
def _get_offsets (cls , func_ast ):
185
195
for arg in func_ast .args :
0 commit comments