@@ -64,6 +64,8 @@ def __repr__(self) -> str:
64
64
65
65
class Debug :
66
66
output_class = DebugOutput
67
+ # 50 lines should be enough to make sure we always get the entire function definition
68
+ frame_context_length = 50
67
69
68
70
def __call__ (self , * args , ** kwargs ):
69
71
print (self ._process (args , kwargs , r'debug *\(' ), flush = True )
@@ -73,7 +75,7 @@ def format(self, *args, **kwargs):
73
75
74
76
def _process (self , args , kwargs , func_regex ):
75
77
curframe = inspect .currentframe ()
76
- frames = inspect .getouterframes (curframe , context = 20 )
78
+ frames = inspect .getouterframes (curframe , context = self . frame_context_length )
77
79
# BEWARE: this must be call by a method which in turn is called "directly" for the frame to be correct
78
80
call_frame = frames [2 ]
79
81
@@ -87,24 +89,63 @@ def _process(self, args, kwargs, func_regex):
87
89
pass
88
90
89
91
call_lines = []
90
- for line in range (call_frame .index , 0 , - 1 ):
91
- new_line = call_frame .code_context [line ]
92
- call_lines .append (new_line )
93
- if re .search (func_regex , new_line ):
94
- break
95
- call_lines .reverse ()
92
+ # print(call_frame)
93
+ # from pprint import pprint
94
+ # pprint(call_frame.code_context)
95
+ 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
103
+ else :
104
+ lineno = call_frame .lineno - len (call_lines )
96
105
97
106
return self .output_class (
98
107
filename = filename ,
99
- lineno = call_frame . lineno - len ( call_lines ) + 1 ,
108
+ lineno = lineno ,
100
109
frame = call_frame .function ,
101
- arguments = list (self ._process_args (call_lines , args , kwargs ))
110
+ arguments = list (self ._process_args (call_lines , args , kwargs , call_frame ))
102
111
)
103
112
104
- def _process_args (self , call_lines , args , kwargs ) -> Generator [DebugArgument , None , None ]: # noqa: C901
113
+ def _args_inspection_failed (self , args , kwargs ):
114
+ for arg in args :
115
+ yield self .output_class .arg_class (arg )
116
+ for name , value in kwargs .items ():
117
+ yield self .output_class .arg_class (value , name = name )
118
+
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
+
105
125
code = dedent ('' .join (call_lines ))
106
126
# print(code)
107
- func_ast = ast .parse (code ).body [0 ].value
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 ]
108
149
109
150
arg_offsets = list (self ._get_offsets (func_ast ))
110
151
for arg , ast_node , i in zip (args , func_ast .args , range (1000 )):
@@ -114,17 +155,22 @@ def _process_args(self, call_lines, args, kwargs) -> Generator[DebugArgument, No
114
155
yield self .output_class .arg_class (arg )
115
156
elif isinstance (ast_node , (ast .Call , ast .Compare )):
116
157
# TODO replace this hack with astor when it get's round to a new release
117
- end = - 2
118
- try :
119
- next_line , next_offset = arg_offsets [i + 1 ]
120
- if next_line == ast_node .lineno :
121
- end = next_offset
122
- except IndexError :
123
- pass
124
- name = call_lines [ast_node .lineno - 1 ][ast_node .col_offset :end ]
125
- yield self .output_class .arg_class (arg , name = name .strip (' ,' ))
158
+ start_line , start_col = ast_node .lineno - 1 , ast_node .col_offset
159
+ end_line , end_col = len (code_lines ) - 1 , None
160
+
161
+ if i < len (arg_offsets ) - 1 :
162
+ end_line , end_col = arg_offsets [i + 1 ]
163
+
164
+ name_lines = []
165
+ for l in range (start_line , end_line + 1 ):
166
+ start_ = start_col if l == start_line else 0
167
+ end_ = end_col if l == end_line else None
168
+ name_lines .append (
169
+ code_lines [l ][start_ :end_ ].strip (' ' )
170
+ )
171
+ yield self .output_class .arg_class (arg , name = ' ' .join (name_lines ).strip (' ,' ))
126
172
else :
127
- warnings .warn ('Unknown type: {}' .format (ast .dump (ast_node )), category = RuntimeError )
173
+ warnings .warn ('Unknown type: {}' .format (ast .dump (ast_node )), RuntimeWarning )
128
174
yield self .output_class .arg_class (arg )
129
175
130
176
kw_arg_names = {}
@@ -137,9 +183,9 @@ def _process_args(self, call_lines, args, kwargs) -> Generator[DebugArgument, No
137
183
@classmethod
138
184
def _get_offsets (cls , func_ast ):
139
185
for arg in func_ast .args :
140
- yield arg .lineno , arg .col_offset
186
+ yield arg .lineno - 1 , arg .col_offset
141
187
for kw in func_ast .keywords :
142
- yield kw .value .lineno , kw .value .col_offset - len (kw .arg ) - 1
188
+ yield kw .value .lineno - 1 , kw .value .col_offset - len (kw .arg ) - 1
143
189
144
190
145
191
debug = Debug ()
0 commit comments