4
4
from .ansi import sformat
5
5
from .prettier import PrettyFormat
6
6
from .timer import Timer
7
- from .utils import env_bool , env_true , use_highlight
7
+ from .utils import env_bool , env_true , is_literal , use_highlight
8
8
9
9
__all__ = 'Debug' , 'debug'
10
10
MYPY = False
11
11
if MYPY :
12
- import ast
13
12
from types import FrameType
14
- from typing import Generator , List , Optional , Tuple
13
+ from typing import Generator , List , Optional
15
14
16
15
17
16
pformat = PrettyFormat (
22
21
)
23
22
24
23
25
- class IntrospectionError (ValueError ):
26
- pass
27
-
28
-
29
24
class DebugArgument :
30
25
__slots__ = 'value' , 'name' , 'extra'
31
26
@@ -43,7 +38,7 @@ def __init__(self, value, *, name=None, **extra):
43
38
44
39
def str (self , highlight = False ) -> str :
45
40
s = ''
46
- if self .name :
41
+ if self .name and not is_literal ( self . name ) :
47
42
s = sformat (self .name , sformat .blue , apply = highlight ) + ': '
48
43
49
44
suffix = sformat (
@@ -108,21 +103,17 @@ def __repr__(self) -> str:
108
103
class Debug :
109
104
output_class = DebugOutput
110
105
111
- def __init__ (
112
- self , * , warnings : 'Optional[bool]' = None , highlight : 'Optional[bool]' = None , frame_context_length : int = 50
113
- ):
106
+ def __init__ (self , * , warnings : 'Optional[bool]' = None , highlight : 'Optional[bool]' = None ):
114
107
self ._show_warnings = env_bool (warnings , 'PY_DEVTOOLS_WARNINGS' , True )
115
108
self ._highlight = highlight
116
- # 50 lines should be enough to make sure we always get the entire function definition
117
- self ._frame_context_length = frame_context_length
118
109
119
110
def __call__ (self , * args , file_ = None , flush_ = True , ** kwargs ) -> None :
120
- d_out = self ._process (args , kwargs , 'debug' )
111
+ d_out = self ._process (args , kwargs )
121
112
s = d_out .str (use_highlight (self ._highlight , file_ ))
122
113
print (s , file = file_ , flush = flush_ )
123
114
124
115
def format (self , * args , ** kwargs ) -> DebugOutput :
125
- return self ._process (args , kwargs , 'format' )
116
+ return self ._process (args , kwargs )
126
117
127
118
def breakpoint (self ):
128
119
import pdb
@@ -132,7 +123,7 @@ def breakpoint(self):
132
123
def timer (self , name = None , * , verbose = True , file = None , dp = 3 ) -> Timer :
133
124
return Timer (name = name , verbose = verbose , file = file , dp = dp )
134
125
135
- def _process (self , args , kwargs , func_name : str ) -> DebugOutput :
126
+ def _process (self , args , kwargs ) -> DebugOutput :
136
127
"""
137
128
BEWARE: this must be called from a function exactly 2 levels below the top of the stack.
138
129
"""
@@ -165,23 +156,20 @@ def _process(self, args, kwargs, func_name: str) -> DebugOutput:
165
156
lineno = call_frame .f_lineno
166
157
warning = None
167
158
168
- import inspect
159
+ import executing
169
160
170
- try :
171
- file_lines , _ = inspect .findsource (call_frame )
172
- except OSError :
161
+ source = executing .Source .for_frame (call_frame )
162
+ if not source .text :
173
163
warning = 'no code context for debug call, code inspection impossible'
174
164
arguments = list (self ._args_inspection_failed (args , kwargs ))
175
165
else :
176
- try :
177
- first_line , last_line = self ._statement_range (call_frame , func_name )
178
- func_ast , code_lines = self ._parse_code (filename , file_lines , first_line , last_line )
179
- except IntrospectionError as e :
180
- # parsing failed
181
- warning = e .args [0 ]
166
+ ex = source .executing (call_frame )
167
+ # function = ex.code_qualname()
168
+ if not ex .node :
169
+ warning = "executing failed to find the calling node"
182
170
arguments = list (self ._args_inspection_failed (args , kwargs ))
183
171
else :
184
- arguments = list (self ._process_args (func_ast , code_lines , args , kwargs ))
172
+ arguments = list (self ._process_args (ex , args , kwargs ))
185
173
186
174
return self .output_class (
187
175
filename = filename ,
@@ -197,174 +185,25 @@ def _args_inspection_failed(self, args, kwargs):
197
185
for name , value in kwargs .items ():
198
186
yield self .output_class .arg_class (value , name = name )
199
187
200
- def _process_args (self , func_ast , code_lines , args , kwargs ) -> 'Generator[DebugArgument, None, None]' : # noqa: C901
188
+ def _process_args (self , ex , args , kwargs ) -> 'Generator[DebugArgument, None, None]' :
201
189
import ast
202
190
203
- complex_nodes = (
204
- ast .Call ,
205
- ast .Attribute ,
206
- ast .Subscript ,
207
- ast .IfExp ,
208
- ast .BoolOp ,
209
- ast .BinOp ,
210
- ast .Compare ,
211
- ast .DictComp ,
212
- ast .ListComp ,
213
- ast .SetComp ,
214
- ast .GeneratorExp ,
215
- )
216
-
217
- arg_offsets = list (self ._get_offsets (func_ast ))
218
- for i , arg in enumerate (args ):
219
- try :
220
- ast_node = func_ast .args [i ]
221
- except IndexError : # pragma: no cover
222
- # happens when code has been commented out and there are fewer func_ast args than real args
223
- yield self .output_class .arg_class (arg )
224
- continue
225
-
226
- if isinstance (ast_node , ast .Name ):
227
- yield self .output_class .arg_class (arg , name = ast_node .id )
228
- elif isinstance (ast_node , complex_nodes ):
229
- # TODO replace this hack with astor when it get's round to a new release
230
- start_line , start_col = arg_offsets [i ]
231
-
232
- if i + 1 < len (arg_offsets ):
233
- end_line , end_col = arg_offsets [i + 1 ]
234
- else :
235
- end_line , end_col = len (code_lines ) - 1 , None
236
-
237
- name_lines = []
238
- for l_ in range (start_line , end_line + 1 ):
239
- start_ = start_col if l_ == start_line else 0
240
- end_ = end_col if l_ == end_line else None
241
- name_lines .append (code_lines [l_ ][start_ :end_ ].strip (' ' ))
242
- yield self .output_class .arg_class (arg , name = ' ' .join (name_lines ).strip (' ,' ))
191
+ func_ast = ex .node
192
+ atok = ex .source .asttokens ()
193
+ for arg , ast_arg in zip (args , func_ast .args ):
194
+ if isinstance (ast_arg , ast .Name ):
195
+ yield self .output_class .arg_class (arg , name = ast_arg .id )
243
196
else :
244
- yield self .output_class .arg_class (arg )
197
+ name = ' ' .join (map (str .strip , atok .get_text (ast_arg ).splitlines ()))
198
+ yield self .output_class .arg_class (arg , name = name )
245
199
246
200
kw_arg_names = {}
247
201
for kw in func_ast .keywords :
248
202
if isinstance (kw .value , ast .Name ):
249
203
kw_arg_names [kw .arg ] = kw .value .id
204
+
250
205
for name , value in kwargs .items ():
251
206
yield self .output_class .arg_class (value , name = name , variable = kw_arg_names .get (name ))
252
207
253
- def _parse_code (
254
- self , filename : str , file_lines : 'List[str]' , first_line : int , last_line : int
255
- ) -> 'Tuple[ast.AST, List[str]]' :
256
- """
257
- All we're trying to do here is build an AST of the function call statement. However numerous ugly interfaces,
258
- lack on introspection support and changes between python versions make this extremely hard.
259
- """
260
- import ast
261
- from textwrap import dedent
262
-
263
- def get_code (_last_line : int ) -> str :
264
- lines = file_lines [first_line - 1 : _last_line ]
265
- return dedent ('' .join (ln for ln in lines if ln .strip ('\n ' ) and not ln .lstrip (' ' ).startswith ('#' )))
266
-
267
- code = get_code (last_line )
268
- func_ast = None
269
- try :
270
- func_ast = self ._wrap_parse (code , filename )
271
- except (SyntaxError , AttributeError ) as e1 :
272
- # if the trailing bracket(s) of the function is/are on a new line e.g.:
273
- # debug(
274
- # foo, bar,
275
- # )
276
- # inspect ignores it when setting index and we have to add it back
277
- for extra in range (1 , 6 ):
278
- code = get_code (last_line + extra )
279
- try :
280
- func_ast = self ._wrap_parse (code , filename )
281
- except (SyntaxError , AttributeError ):
282
- pass
283
- else :
284
- break
285
-
286
- if not func_ast :
287
- raise IntrospectionError ('error parsing code, {0.__class__.__name__}: {0}' .format (e1 ))
288
-
289
- if not isinstance (func_ast , ast .Call ):
290
- raise IntrospectionError ('error parsing code, found {0.__class__} not Call' .format (func_ast ))
291
-
292
- code_lines = [line for line in code .split ('\n ' ) if line ]
293
- # this removes the trailing bracket from the lines of code meaning it doesn't appear in the
294
- # representation of the last argument
295
- code_lines [- 1 ] = code_lines [- 1 ][:- 1 ]
296
- return func_ast , code_lines
297
-
298
- @staticmethod # noqa: C901
299
- def _statement_range (call_frame : 'FrameType' , func_name : str ) -> 'Tuple[int, int]' : # noqa: C901
300
- """
301
- Try to find the start and end of a frame statement.
302
- """
303
- import dis
304
-
305
- # dis.disassemble(call_frame.f_code, call_frame.f_lasti)
306
- # pprint([i for i in dis.get_instructions(call_frame.f_code)])
307
-
308
- instructions = iter (dis .get_instructions (call_frame .f_code ))
309
- first_line = None
310
- last_line = None
311
-
312
- for instr in instructions : # pragma: no branch
313
- if (
314
- instr .starts_line
315
- and instr .opname in {'LOAD_GLOBAL' , 'LOAD_NAME' }
316
- and (instr .argval == func_name or (instr .argval == 'debug' and next (instructions ).argval == func_name ))
317
- ):
318
- first_line = instr .starts_line
319
- if instr .offset == call_frame .f_lasti :
320
- break
321
-
322
- if first_line is None :
323
- raise IntrospectionError ('error parsing code, unable to find "{}" function statement' .format (func_name ))
324
-
325
- for instr in instructions :
326
- if instr .starts_line :
327
- last_line = instr .starts_line - 1
328
- break
329
-
330
- if last_line is None :
331
- if sys .version_info >= (3 , 8 ):
332
- # absolutely no reliable way of getting the last line of the statement, complete hack is to
333
- # get the last line of the last statement of the whole code block and go from there
334
- # this assumes (perhaps wrongly?) that the reason we couldn't find last_line is that the statement
335
- # in question was the last of the block
336
- last_line = max (i .starts_line for i in dis .get_instructions (call_frame .f_code ) if i .starts_line )
337
- else :
338
- # in older version of python f_lineno is the end of the statement, not the beginning
339
- # so this is a reasonable guess
340
- last_line = call_frame .f_lineno
341
-
342
- return first_line , last_line
343
-
344
- @staticmethod
345
- def _wrap_parse (code : str , filename : str ) -> 'ast.Call' :
346
- """
347
- async wrapper is required to avoid await calls raising a SyntaxError
348
- """
349
- import ast
350
- from textwrap import indent
351
-
352
- code = 'async def wrapper():\n ' + indent (code , ' ' )
353
- return ast .parse (code , filename = filename ).body [0 ].body [0 ].value
354
-
355
- @staticmethod
356
- def _get_offsets (func_ast ):
357
- import ast
358
-
359
- for arg in func_ast .args :
360
- start_line , start_col = arg .lineno - 2 , arg .col_offset - 1
361
-
362
- # horrible hack for http://bugs.python.org/issue31241
363
- if isinstance (arg , (ast .ListComp , ast .GeneratorExp )):
364
- start_col -= 1
365
- yield start_line , start_col
366
- for kw in func_ast .keywords :
367
- yield kw .value .lineno - 2 , kw .value .col_offset - 2 - (len (kw .arg ) if kw .arg else 0 )
368
-
369
208
370
209
debug = Debug ()
0 commit comments