11import ast
22import inspect
33import os
4+ import pdb
45import re
56import warnings
67from pathlib import Path
78from textwrap import dedent
8- from typing import Generator , List , Optional , Tuple
9+ from typing import Generator , List , Optional , Tuple , Type
910
1011from .ansi import isatty , sformat
1112from .prettier import PrettyFormat
@@ -99,7 +100,7 @@ def __init__(self, *,
99100 colour : Optional [bool ]= None ,
100101 highlight : Optional [bool ]= None ,
101102 frame_context_length : int = 50 ):
102- self ._warnings = self ._env_bool (warnings , 'PY_DEVTOOLS_WARNINGS' )
103+ self ._show_warnings = self ._env_bool (warnings , 'PY_DEVTOOLS_WARNINGS' )
103104 self ._colour = self ._env_bool (colour , 'PY_DEVTOOLS_COLOUR' )
104105 self ._highlight = self ._env_bool (highlight , 'PY_DEVTOOLS_HIGHLIGHT' )
105106 # 50 lines should be enough to make sure we always get the entire function definition
@@ -121,10 +122,24 @@ def __call__(self, *args, file_=None, flush_=True, **kwargs) -> None:
121122 def format (self , * args , ** kwargs ) -> DebugOutput :
122123 return self ._process (args , kwargs , r'debug.format *\(' )
123124
125+ def breakpoint (self ):
126+ pdb .Pdb (skip = ['devtools.*' ]).set_trace ()
127+
124128 def _process (self , args , kwargs , func_regex ) -> DebugOutput :
125129 curframe = inspect .currentframe ()
126- frames = inspect .getouterframes (curframe , context = self ._frame_context_length )
127- # BEWARE: this must be call by a method which in turn is called "directly" for the frame to be correct
130+ try :
131+ frames = inspect .getouterframes (curframe , context = self ._frame_context_length )
132+ except IndexError as e :
133+ # NOTICE: we should really catch all conceivable errors here, if you find one please report.
134+ # IndexError happens in odd situations such as code called from within jinja templates
135+ self ._warn ('error parsing code, {0.__class__.__name__}: {0}' .format (e ), SyntaxWarning )
136+ return self .output_class (
137+ filename = '<unknown>' ,
138+ lineno = 0 ,
139+ frame = '' ,
140+ arguments = list (self ._args_inspection_failed (args , kwargs ))
141+ )
142+ # BEWARE: this must be called by a method which in turn is called "directly" for the frame to be correct
128143 call_frame = frames [2 ]
129144
130145 filename = call_frame .filename
@@ -145,8 +160,7 @@ def _process(self, args, kwargs, func_regex) -> DebugOutput:
145160 arguments = list (self ._args_inspection_failed (args , kwargs ))
146161 else :
147162 lineno = call_frame .lineno
148- if self ._warnings :
149- warnings .warn ('no code context for debug call, code inspection impossible' , RuntimeWarning )
163+ self ._warn ('no code context for debug call, code inspection impossible' )
150164 arguments = list (self ._args_inspection_failed (args , kwargs ))
151165
152166 return self .output_class (
@@ -226,13 +240,11 @@ def _parse_code(self, call_frame, func_regex, filename) -> Tuple[Optional[ast.AS
226240 break
227241
228242 if not func_ast :
229- if self ._warnings :
230- warnings .warn ('error passing code:\n "{}"\n Error: {}' .format (original_code , e1 ), SyntaxWarning )
243+ self ._warn ('error passing code:\n "{}"\n Error: {}' .format (original_code , e1 ), SyntaxWarning )
231244 return None , None , lineno
232245
233246 if not isinstance (func_ast , ast .Call ):
234- if self ._warnings :
235- warnings .warn ('error passing code, found {} not Call' .format (func_ast .__class__ ), SyntaxWarning )
247+ self ._warn ('error passing code, found {} not Call' .format (func_ast .__class__ ), SyntaxWarning )
236248 return None , None , lineno
237249
238250 code_lines = [l for l in code .split ('\n ' ) if l ]
@@ -253,5 +265,9 @@ def _get_offsets(cls, func_ast):
253265 for kw in func_ast .keywords :
254266 yield kw .value .lineno - 1 , kw .value .col_offset - len (kw .arg ) - 1
255267
268+ def _warn (self , msg , category : Type [Warning ]= RuntimeWarning ):
269+ if self ._show_warnings :
270+ warnings .warn (msg , category )
271+
256272
257273debug = Debug ()
0 commit comments