Skip to content

Commit 920139a

Browse files
committed
add template debugging #349
1 parent be62784 commit 920139a

File tree

3 files changed

+206
-26
lines changed

3 files changed

+206
-26
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
"label": "Python",
183183
"enableBreakpointsFor": {
184184
"languageIds": [
185-
"python"
185+
"python", "html"
186186
]
187187
},
188188
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",

pythonFiles/PythonTools/visualstudio_py_debugger.py

Lines changed: 199 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#
1414
# See the Apache Version 2.0 License for specific language governing
1515
# permissions and limitations under the License.
16+
# With number of modifications by Don Jayamanne
1617

1718
from __future__ import with_statement
1819

@@ -174,6 +175,26 @@ def __len__(self):
174175
BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2
175176
BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3
176177

178+
## Begin modification by Don Jayamanne
179+
DJANGO_VERSIONS_IDENTIFIED = False
180+
IS_DJANGO18 = False
181+
IS_DJANGO19 = False
182+
IS_DJANGO19_OR_HIGHER = False
183+
184+
try:
185+
dict_contains = dict.has_key
186+
except:
187+
try:
188+
#Py3k does not have has_key anymore, and older versions don't have __contains__
189+
dict_contains = dict.__contains__
190+
except:
191+
try:
192+
dict_contains = dict.has_key
193+
except NameError:
194+
def dict_contains(d, key):
195+
return d.has_key(key)
196+
## End modification by Don Jayamanne
197+
177198
class BreakpointInfo(object):
178199
__slots__ = [
179200
'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition',
@@ -619,19 +640,57 @@ def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True):
619640
cur_thread._block_starting_lock.release()
620641

621642
DJANGO_BREAKPOINTS = {}
643+
DJANGO_TEMPLATES = {}
622644

623645
class DjangoBreakpointInfo(object):
624646
def __init__(self, filename):
625647
self._line_locations = None
626648
self.filename = filename
627649
self.breakpoints = {}
650+
self.rangeIsPlainText = {}
628651

629652
def add_breakpoint(self, lineno, brkpt_id):
630653
self.breakpoints[lineno] = brkpt_id
631654

632655
def remove_breakpoint(self, lineno):
633-
del self.breakpoints[lineno]
634-
656+
try:
657+
del self.breakpoints[lineno]
658+
except:
659+
pass
660+
661+
def has_breakpoint_for_line(self, lineNumber):
662+
return self.breakpoints.get(lineNumber) is not None
663+
664+
def is_range_plain_text(self, start, end):
665+
key = str(start) + '-' + str(end)
666+
if self.rangeIsPlainText.get(key) is None:
667+
# we need to calculate our line number offset information
668+
try:
669+
with open(self.filename, 'rb') as contents:
670+
contents.seek(start, 0) # 0 = start of file, optional in this case
671+
data = contents.read(end - start)
672+
isPlainText = True
673+
if data.startswith('{{') and data.endswith('}}'):
674+
isPlainText = False
675+
if data.startswith('{%') and data.endswith('%}'):
676+
isPlainText = False
677+
self.rangeIsPlainText[key] = isPlainText
678+
return isPlainText
679+
except:
680+
return False
681+
else:
682+
return self.rangeIsPlainText.get(key)
683+
684+
def line_number_to_offset(self, lineNumber):
685+
line_locs = self.line_locations
686+
if line_locs is not None:
687+
low_line = line_locs[lineNumber - 1]
688+
hi_line = line_locs[lineNumber]
689+
690+
return low_line, hi_line
691+
692+
return (None, None)
693+
635694
@property
636695
def line_locations(self):
637696
if self._line_locations is None:
@@ -680,28 +739,42 @@ def should_break(self, start, end):
680739
return False, 0
681740

682741
def get_django_frame_source(frame):
742+
global DJANGO_VERSIONS_IDENTIFIED
743+
global IS_DJANGO18
744+
global IS_DJANGO19
745+
global IS_DJANGO19_OR_HIGHER
746+
if DJANGO_VERSIONS_IDENTIFIED == False:
747+
DJANGO_VERSIONS_IDENTIFIED = True
748+
try:
749+
import django
750+
version = django.VERSION
751+
IS_DJANGO18 = version[0] == 1 and version[1] == 8
752+
IS_DJANGO19 = version[0] == 1 and version[1] == 9
753+
IS_DJANGO19_OR_HIGHER = ((version[0] == 1 and version[1] >= 9) or version[0] > 1)
754+
except:
755+
pass
683756
if frame.f_code.co_name == 'render':
757+
origin = _get_template_file_name(frame)
758+
line = _get_template_line(frame)
759+
position = None
684760
self_obj = frame.f_locals.get('self', None)
685761
if self_obj is None:
686762
return None
687-
name = type(self_obj).__name__
688-
if name in ('Template', 'TextNode'):
689-
return None
690-
source_obj = getattr(self_obj, 'source', None)
691-
if source_obj and hasattr(source_obj, '__len__') and len(source_obj) == 2:
692-
return str(source_obj[0]), source_obj[1]
693-
694-
token_obj = getattr(self_obj, 'token', None)
695-
if token_obj is None:
696-
return None
697-
template_obj = getattr(frame.f_locals.get('context', None), 'template', None)
698-
if template_obj is None:
699-
return None
700-
template_name = getattr(template_obj, 'origin', None)
701-
position = getattr(token_obj, 'position', None)
702-
if template_name and position:
703-
return str(template_name), position
704763

764+
if self_obj is not None and hasattr(self_obj, 'token') and hasattr(self_obj.token, 'position'):
765+
position = self_obj.token.position
766+
767+
if origin is not None and position is None:
768+
active_bps = DJANGO_BREAKPOINTS.get(origin.lower())
769+
if active_bps is None:
770+
active_bps = DJANGO_TEMPLATES.get(origin.lower())
771+
if active_bps is None:
772+
DJANGO_BREAKPOINTS[origin.lower()] = active_bps = DjangoBreakpointInfo(origin.lower())
773+
if active_bps is not None:
774+
if line is not None:
775+
position = active_bps.line_number_to_offset(line)
776+
if origin and position:
777+
return str(origin), position, line
705778

706779
return None
707780

@@ -901,12 +974,15 @@ def handle_call(self, frame, arg):
901974
if DJANGO_BREAKPOINTS:
902975
source_obj = get_django_frame_source(frame)
903976
if source_obj is not None:
904-
origin, (start, end) = source_obj
977+
origin, (start, end), lineNumber = source_obj
905978

906979
active_bps = DJANGO_BREAKPOINTS.get(origin.lower())
907980
should_break = False
908-
if active_bps is not None:
981+
if active_bps is not None and origin != '<unknown source>':
909982
should_break, bkpt_id = active_bps.should_break(start, end)
983+
isPlainText = active_bps.is_range_plain_text(start, end)
984+
if isPlainText:
985+
should_break = False
910986
if should_break:
911987
probe_stack()
912988
update_all_thread_stacks(self)
@@ -1464,7 +1540,7 @@ def get_frame_list(self):
14641540
frame_info = None
14651541

14661542
if source_obj is not None:
1467-
origin, (start, end) = source_obj
1543+
origin, (start, end), lineNumber = source_obj
14681544

14691545
filename = str(origin)
14701546
bp_info = DJANGO_BREAKPOINTS.get(filename.lower())
@@ -1622,6 +1698,7 @@ def __init__(self, conn):
16221698
to_bytes('brka') : self.command_break_all,
16231699
to_bytes('resa') : self.command_resume_all,
16241700
to_bytes('rest') : self.command_resume_thread,
1701+
to_bytes('thrf') : self.command_get_thread_frames,
16251702
to_bytes('ares') : self.command_auto_resume,
16261703
to_bytes('exec') : self.command_execute_code,
16271704
to_bytes('chld') : self.command_enum_children,
@@ -1805,6 +1882,13 @@ def command_break_all(self):
18051882
SEND_BREAK_COMPLETE = True
18061883
mark_all_threads_for_break()
18071884

1885+
def command_get_thread_frames(self):
1886+
tid = read_int(self.conn)
1887+
THREADS_LOCK.acquire()
1888+
thread = THREADS[tid]
1889+
THREADS_LOCK.release()
1890+
thread.enum_thread_frames_locally()
1891+
18081892
def command_resume_all(self):
18091893
# resume all
18101894
THREADS_LOCK.acquire()
@@ -2190,7 +2274,7 @@ def attach_process(port_num, debug_id, debug_options, currentPid, report = False
21902274
## Begin modification by Don Jayamanne
21912275
# Pass current Process id to pass back to debugger
21922276
write_int(conn, currentPid) # success
2193-
## End Modification by Don Jayamanne
2277+
## End Modification by Don Jayamanne
21942278
break
21952279
except:
21962280
import time
@@ -2480,7 +2564,7 @@ def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'
24802564
# Pass current Process id to pass back to debugger
24812565
attach_process(port_num, debug_id, debug_options, currentPid, report = True)
24822566
## End Modification by Don Jayamanne
2483-
2567+
24842568
# setup the current thread
24852569
cur_thread = new_thread()
24862570
cur_thread.stepping = STEPPING_LAUNCH_BREAK
@@ -2529,3 +2613,94 @@ def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'
25292613
get_code(exec_code),
25302614
get_code(new_thread_wrapper)
25312615
))
2616+
2617+
## Begin modification by Don Jayamanne
2618+
def _read_file(filename):
2619+
f = open(filename, "r")
2620+
s = f.read()
2621+
f.close()
2622+
return s
2623+
2624+
2625+
def _offset_to_line_number(text, offset):
2626+
curLine = 1
2627+
curOffset = 0
2628+
while curOffset < offset:
2629+
if curOffset == len(text):
2630+
return -1
2631+
c = text[curOffset]
2632+
if c == '\n':
2633+
curLine += 1
2634+
elif c == '\r':
2635+
curLine += 1
2636+
if curOffset < len(text) and text[curOffset + 1] == '\n':
2637+
curOffset += 1
2638+
2639+
curOffset += 1
2640+
2641+
return curLine
2642+
2643+
2644+
def _get_source_django_18_or_lower(frame):
2645+
# This method is usable only for the Django <= 1.8
2646+
try:
2647+
node = frame.f_locals['self']
2648+
if hasattr(node, 'source'):
2649+
return node.source
2650+
else:
2651+
if IS_DJANGO18:
2652+
# The debug setting was changed since Django 1.8
2653+
print("WARNING: Template path is not available. Set the 'debug' option in the OPTIONS of a DjangoTemplates "
2654+
"backend.")
2655+
else:
2656+
# The debug setting for Django < 1.8
2657+
print("WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True in your settings.py to make "
2658+
"django template breakpoints working")
2659+
return None
2660+
2661+
except:
2662+
print(traceback.format_exc())
2663+
return None
2664+
2665+
def _get_template_file_name(frame):
2666+
try:
2667+
if IS_DJANGO19_OR_HIGHER:
2668+
# The Node source was removed since Django 1.9
2669+
if dict_contains(frame.f_locals, 'context'):
2670+
context = frame.f_locals['context']
2671+
if hasattr(context, 'template') and hasattr(context.template, 'origin') and \
2672+
hasattr(context.template.origin, 'name'):
2673+
return context.template.origin.name
2674+
return None
2675+
source = _get_source_django_18_or_lower(frame)
2676+
if source is None:
2677+
print("Source is None\n")
2678+
return None
2679+
fname = source[0].name
2680+
2681+
if fname == '<unknown source>':
2682+
print("Source name is %s\n" + fname)
2683+
return None
2684+
else:
2685+
abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_file(fname)
2686+
return abs_path_real_path_and_base[1]
2687+
except:
2688+
print(traceback.format_exc())
2689+
return None
2690+
2691+
2692+
def _get_template_line(frame):
2693+
if IS_DJANGO19_OR_HIGHER:
2694+
# The Node source was removed since Django 1.9
2695+
self = frame.f_locals['self']
2696+
if hasattr(self, 'token') and hasattr(self.token, 'lineno'):
2697+
return self.token.lineno
2698+
else:
2699+
return None
2700+
source = _get_source_django_18_or_lower(frame)
2701+
file_name = _get_template_file_name(frame)
2702+
try:
2703+
return _offset_to_line_number(_read_file(file_name), source[1][0])
2704+
except:
2705+
return None
2706+
## End modification by Don Jayamanne

src/client/debugger/PythonProcess.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,12 @@ export class PythonProcess extends EventEmitter implements IPythonProcess {
281281
this.stream.WriteInt32(brkpoint.LineNo);
282282
this.stream.WriteString(brkpoint.Filename);
283283

284-
if (!brkpoint.IsDjangoBreakpoint) {
284+
if (brkpoint.IsDjangoBreakpoint) {
285+
// Bining django breakpoints don't return any responses
286+
// Assume it worked
287+
resolve();
288+
}
289+
else {
285290
this.SendCondition(brkpoint);
286291
this.SendPassCount(brkpoint);
287292
}

0 commit comments

Comments
 (0)