##// END OF EJS Templates
Use python-prompt-toolkit for ipdb....
Matthias Bussonnier -
Show More
@@ -1,602 +1,637 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 Pdb debugger class.
4 4
5 5 Modified from the standard pdb.Pdb class to avoid including readline, so that
6 6 the command line completion of other programs which include this isn't
7 7 damaged.
8 8
9 9 In the future, this class will be expanded with improvements over the standard
10 10 pdb.
11 11
12 12 The code in this file is mainly lifted out of cmd.py in Python 2.2, with minor
13 13 changes. Licensing should therefore be under the standard Python terms. For
14 14 details on the PSF (Python Software Foundation) standard license, see:
15 15
16 16 http://www.python.org/2.2.3/license.html"""
17 17
18 18 #*****************************************************************************
19 19 #
20 20 # This file is licensed under the PSF license.
21 21 #
22 22 # Copyright (C) 2001 Python Software Foundation, www.python.org
23 23 # Copyright (C) 2005-2006 Fernando Perez. <fperez@colorado.edu>
24 24 #
25 25 #
26 26 #*****************************************************************************
27 27 from __future__ import print_function
28 28
29 29 import bdb
30 30 import functools
31 31 import inspect
32 32 import sys
33 33
34 34 from IPython import get_ipython
35 35 from IPython.utils import PyColorize, ulinecache
36 36 from IPython.utils import coloransi, py3compat
37 37 from IPython.core.excolors import exception_colors
38 38 from IPython.testing.skipdoctest import skip_doctest
39 39
40 from prompt_toolkit import prompt as ptk_prompt
41
40 42 prompt = 'ipdb> '
41 43
42 44 #We have to check this directly from sys.argv, config struct not yet available
43 45 from pdb import Pdb as OldPdb
44 46
45 47 # Allow the set_trace code to operate outside of an ipython instance, even if
46 48 # it does so with some limitations. The rest of this support is implemented in
47 49 # the Tracer constructor.
48 50
49 51 def make_arrow(pad):
50 52 """generate the leading arrow in front of traceback or debugger"""
51 53 if pad >= 2:
52 54 return '-'*(pad-2) + '> '
53 55 elif pad == 1:
54 56 return '>'
55 57 return ''
56 58
57 59
58 60 def BdbQuit_excepthook(et, ev, tb, excepthook=None):
59 61 """Exception hook which handles `BdbQuit` exceptions.
60 62
61 63 All other exceptions are processed using the `excepthook`
62 64 parameter.
63 65 """
64 66 if et==bdb.BdbQuit:
65 67 print('Exiting Debugger.')
66 68 elif excepthook is not None:
67 69 excepthook(et, ev, tb)
68 70 else:
69 71 # Backwards compatibility. Raise deprecation warning?
70 72 BdbQuit_excepthook.excepthook_ori(et,ev,tb)
71 73
72 74 def BdbQuit_IPython_excepthook(self,et,ev,tb,tb_offset=None):
73 75 print('Exiting Debugger.')
74 76
75 77
76 78 class Tracer(object):
77 79 """Class for local debugging, similar to pdb.set_trace.
78 80
79 81 Instances of this class, when called, behave like pdb.set_trace, but
80 82 providing IPython's enhanced capabilities.
81 83
82 84 This is implemented as a class which must be initialized in your own code
83 85 and not as a standalone function because we need to detect at runtime
84 86 whether IPython is already active or not. That detection is done in the
85 87 constructor, ensuring that this code plays nicely with a running IPython,
86 88 while functioning acceptably (though with limitations) if outside of it.
87 89 """
88 90
89 91 @skip_doctest
90 92 def __init__(self, colors=None):
91 93 """Create a local debugger instance.
92 94
93 95 Parameters
94 96 ----------
95 97
96 98 colors : str, optional
97 99 The name of the color scheme to use, it must be one of IPython's
98 100 valid color schemes. If not given, the function will default to
99 101 the current IPython scheme when running inside IPython, and to
100 102 'NoColor' otherwise.
101 103
102 104 Examples
103 105 --------
104 106 ::
105 107
106 108 from IPython.core.debugger import Tracer; debug_here = Tracer()
107 109
108 110 Later in your code::
109 111
110 112 debug_here() # -> will open up the debugger at that point.
111 113
112 114 Once the debugger activates, you can use all of its regular commands to
113 115 step through code, set breakpoints, etc. See the pdb documentation
114 116 from the Python standard library for usage details.
115 117 """
116 118
117 119 ip = get_ipython()
118 120 if ip is None:
119 121 # Outside of ipython, we set our own exception hook manually
120 122 sys.excepthook = functools.partial(BdbQuit_excepthook,
121 123 excepthook=sys.excepthook)
122 124 def_colors = 'NoColor'
123 125 try:
124 126 # Limited tab completion support
125 127 import readline
126 128 readline.parse_and_bind('tab: complete')
127 129 except ImportError:
128 130 pass
129 131 else:
130 132 # In ipython, we use its custom exception handler mechanism
131 133 def_colors = ip.colors
132 134 ip.set_custom_exc((bdb.BdbQuit,), BdbQuit_IPython_excepthook)
133 135
134 136 if colors is None:
135 137 colors = def_colors
136 138
137 139 # The stdlib debugger internally uses a modified repr from the `repr`
138 140 # module, that limits the length of printed strings to a hardcoded
139 141 # limit of 30 characters. That much trimming is too aggressive, let's
140 142 # at least raise that limit to 80 chars, which should be enough for
141 143 # most interactive uses.
142 144 try:
143 145 try:
144 146 from reprlib import aRepr # Py 3
145 147 except ImportError:
146 148 from repr import aRepr # Py 2
147 149 aRepr.maxstring = 80
148 150 except:
149 151 # This is only a user-facing convenience, so any error we encounter
150 152 # here can be warned about but can be otherwise ignored. These
151 153 # printouts will tell us about problems if this API changes
152 154 import traceback
153 155 traceback.print_exc()
154 156
155 157 self.debugger = Pdb(colors)
156 158
157 159 def __call__(self):
158 160 """Starts an interactive debugger at the point where called.
159 161
160 162 This is similar to the pdb.set_trace() function from the std lib, but
161 163 using IPython's enhanced debugger."""
162 164
163 165 self.debugger.set_trace(sys._getframe().f_back)
164 166
165 167
166 168 def decorate_fn_with_doc(new_fn, old_fn, additional_text=""):
167 169 """Make new_fn have old_fn's doc string. This is particularly useful
168 170 for the ``do_...`` commands that hook into the help system.
169 171 Adapted from from a comp.lang.python posting
170 172 by Duncan Booth."""
171 173 def wrapper(*args, **kw):
172 174 return new_fn(*args, **kw)
173 175 if old_fn.__doc__:
174 176 wrapper.__doc__ = old_fn.__doc__ + additional_text
175 177 return wrapper
176 178
177 179
178 180 def _file_lines(fname):
179 181 """Return the contents of a named file as a list of lines.
180 182
181 183 This function never raises an IOError exception: if the file can't be
182 184 read, it simply returns an empty list."""
183 185
184 186 try:
185 187 outfile = open(fname)
186 188 except IOError:
187 189 return []
188 190 else:
189 191 out = outfile.readlines()
190 192 outfile.close()
191 193 return out
192 194
193 195
194 196 class Pdb(OldPdb, object):
195 197 """Modified Pdb class, does not load readline."""
196 198
197 199 def __init__(self,color_scheme='NoColor',completekey=None,
198 200 stdin=None, stdout=None, context=5):
199 201
200 202 # Parent constructor:
201 203 try:
202 204 self.context=int(context)
203 205 if self.context <= 0:
204 206 raise ValueError("Context must be a positive integer")
205 207 except (TypeError, ValueError):
206 208 raise ValueError("Context must be a positive integer")
207 209
208 210 OldPdb.__init__(self,completekey,stdin,stdout)
209 211
210 212 # IPython changes...
211 213 self.shell = get_ipython()
212 214
213 215 if self.shell is None:
214 216 # No IPython instance running, we must create one
215 217 from IPython.terminal.interactiveshell import \
216 218 TerminalInteractiveShell
217 219 self.shell = TerminalInteractiveShell.instance()
218 220
219 221 self.aliases = {}
220 222
221 223 # Create color table: we copy the default one from the traceback
222 224 # module and add a few attributes needed for debugging
223 225 self.color_scheme_table = exception_colors()
224 226
225 227 # shorthands
226 228 C = coloransi.TermColors
227 229 cst = self.color_scheme_table
228 230
229 231 cst['NoColor'].colors.prompt = C.NoColor
230 232 cst['NoColor'].colors.breakpoint_enabled = C.NoColor
231 233 cst['NoColor'].colors.breakpoint_disabled = C.NoColor
232 234
233 235 cst['Linux'].colors.prompt = C.Green
234 236 cst['Linux'].colors.breakpoint_enabled = C.LightRed
235 237 cst['Linux'].colors.breakpoint_disabled = C.Red
236 238
237 239 cst['LightBG'].colors.prompt = C.Blue
238 240 cst['LightBG'].colors.breakpoint_enabled = C.LightRed
239 241 cst['LightBG'].colors.breakpoint_disabled = C.Red
240 242
241 243 self.set_colors(color_scheme)
242 244
243 245 # Add a python parser so we can syntax highlight source while
244 246 # debugging.
245 247 self.parser = PyColorize.Parser()
246 248
247 249 # Set the prompt - the default prompt is '(Pdb)'
248 Colors = cst.active_colors
249 if color_scheme == 'NoColor':
250 self.prompt = prompt
251 else:
252 # The colour markers are wrapped by bytes 01 and 02 so that readline
253 # can calculate the width.
254 self.prompt = u'\x01%s\x02%s\x01%s\x02' % (Colors.prompt, prompt, Colors.Normal)
250 self.prompt = prompt
251
252
253 def cmdloop(self, intro=None):
254 """Repeatedly issue a prompt, accept input, parse an initial prefix
255 off the received input, and dispatch to action methods, passing them
256 the remainder of the line as argument.
257
258 override the same methods from cmd.Cmd to provide prompt toolkit replacement.
259 """
260 if not self.use_rawinput:
261 raise ValueError('Sorry ipdb does not support raw_input=False')
262
263 def get_prompt_tokens(cli):
264 from pygments.token import Token
265 return [(Token.Prompt, self.prompt)]
266
267 self.preloop()
268 try:
269 if intro is not None:
270 self.intro = intro
271 if self.intro:
272 self.stdout.write(str(self.intro)+"\n")
273 stop = None
274 while not stop:
275 if self.cmdqueue:
276 line = self.cmdqueue.pop(0)
277 else:
278 try:
279 line = ptk_prompt(get_prompt_tokens=get_prompt_tokens)
280 except EOFError:
281 line = 'EOF'
282 line = self.precmd(line)
283 stop = self.onecmd(line)
284 stop = self.postcmd(stop, line)
285 self.postloop()
286 except Exception:
287 pass
288
289
255 290
256 291 def set_colors(self, scheme):
257 292 """Shorthand access to the color table scheme selector method."""
258 293 self.color_scheme_table.set_active_scheme(scheme)
259 294
260 295 def interaction(self, frame, traceback):
261 296 self.shell.set_completer_frame(frame)
262 297 while True:
263 298 try:
264 299 OldPdb.interaction(self, frame, traceback)
265 300 break
266 301 except KeyboardInterrupt:
267 302 self.shell.write('\n' + self.shell.get_exception_only())
268 303 break
269 304 finally:
270 305 # Pdb sets readline delimiters, so set them back to our own
271 306 if self.shell.readline is not None:
272 307 self.shell.readline.set_completer_delims(self.shell.readline_delims)
273 308
274 309 def parseline(self, line):
275 310 if line.startswith("!!"):
276 311 # Force standard behavior.
277 312 return super(Pdb, self).parseline(line[2:])
278 313 # "Smart command mode" from pdb++: don't execute commands if a variable
279 314 # with the same name exists.
280 315 cmd, arg, newline = super(Pdb, self).parseline(line)
281 316 if cmd in self.curframe.f_globals or cmd in self.curframe.f_locals:
282 317 return super(Pdb, self).parseline("!" + line)
283 318 return super(Pdb, self).parseline(line)
284 319
285 320 def new_do_up(self, arg):
286 321 OldPdb.do_up(self, arg)
287 322 self.shell.set_completer_frame(self.curframe)
288 323 do_u = do_up = decorate_fn_with_doc(new_do_up, OldPdb.do_up)
289 324
290 325 def new_do_down(self, arg):
291 326 OldPdb.do_down(self, arg)
292 327 self.shell.set_completer_frame(self.curframe)
293 328
294 329 do_d = do_down = decorate_fn_with_doc(new_do_down, OldPdb.do_down)
295 330
296 331 def new_do_frame(self, arg):
297 332 OldPdb.do_frame(self, arg)
298 333 self.shell.set_completer_frame(self.curframe)
299 334
300 335 def new_do_quit(self, arg):
301 336
302 337 if hasattr(self, 'old_all_completions'):
303 338 self.shell.Completer.all_completions=self.old_all_completions
304 339
305 340 return OldPdb.do_quit(self, arg)
306 341
307 342 do_q = do_quit = decorate_fn_with_doc(new_do_quit, OldPdb.do_quit)
308 343
309 344 def new_do_restart(self, arg):
310 345 """Restart command. In the context of ipython this is exactly the same
311 346 thing as 'quit'."""
312 347 self.msg("Restart doesn't make sense here. Using 'quit' instead.")
313 348 return self.do_quit(arg)
314 349
315 350 def postloop(self):
316 351 self.shell.set_completer_frame(None)
317 352
318 353 def print_stack_trace(self, context=None):
319 354 if context is None:
320 355 context = self.context
321 356 try:
322 357 context=int(context)
323 358 if context <= 0:
324 359 raise ValueError("Context must be a positive integer")
325 360 except (TypeError, ValueError):
326 361 raise ValueError("Context must be a positive integer")
327 362 try:
328 363 for frame_lineno in self.stack:
329 364 self.print_stack_entry(frame_lineno, context=context)
330 365 except KeyboardInterrupt:
331 366 pass
332 367
333 368 def print_stack_entry(self,frame_lineno,prompt_prefix='\n-> ',
334 369 context=None):
335 370 if context is None:
336 371 context = self.context
337 372 try:
338 373 context=int(context)
339 374 if context <= 0:
340 375 raise ValueError("Context must be a positive integer")
341 376 except (TypeError, ValueError):
342 377 raise ValueError("Context must be a positive integer")
343 378 print(self.format_stack_entry(frame_lineno, '', context))
344 379
345 380 # vds: >>
346 381 frame, lineno = frame_lineno
347 382 filename = frame.f_code.co_filename
348 383 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
349 384 # vds: <<
350 385
351 386 def format_stack_entry(self, frame_lineno, lprefix=': ', context=None):
352 387 if context is None:
353 388 context = self.context
354 389 try:
355 390 context=int(context)
356 391 if context <= 0:
357 392 print("Context must be a positive integer")
358 393 except (TypeError, ValueError):
359 394 print("Context must be a positive integer")
360 395 try:
361 396 import reprlib # Py 3
362 397 except ImportError:
363 398 import repr as reprlib # Py 2
364 399
365 400 ret = []
366 401
367 402 Colors = self.color_scheme_table.active_colors
368 403 ColorsNormal = Colors.Normal
369 404 tpl_link = u'%s%%s%s' % (Colors.filenameEm, ColorsNormal)
370 405 tpl_call = u'%s%%s%s%%s%s' % (Colors.vName, Colors.valEm, ColorsNormal)
371 406 tpl_line = u'%%s%s%%s %s%%s' % (Colors.lineno, ColorsNormal)
372 407 tpl_line_em = u'%%s%s%%s %s%%s%s' % (Colors.linenoEm, Colors.line,
373 408 ColorsNormal)
374 409
375 410 frame, lineno = frame_lineno
376 411
377 412 return_value = ''
378 413 if '__return__' in frame.f_locals:
379 414 rv = frame.f_locals['__return__']
380 415 #return_value += '->'
381 416 return_value += reprlib.repr(rv) + '\n'
382 417 ret.append(return_value)
383 418
384 419 #s = filename + '(' + `lineno` + ')'
385 420 filename = self.canonic(frame.f_code.co_filename)
386 421 link = tpl_link % py3compat.cast_unicode(filename)
387 422
388 423 if frame.f_code.co_name:
389 424 func = frame.f_code.co_name
390 425 else:
391 426 func = "<lambda>"
392 427
393 428 call = ''
394 429 if func != '?':
395 430 if '__args__' in frame.f_locals:
396 431 args = reprlib.repr(frame.f_locals['__args__'])
397 432 else:
398 433 args = '()'
399 434 call = tpl_call % (func, args)
400 435
401 436 # The level info should be generated in the same format pdb uses, to
402 437 # avoid breaking the pdbtrack functionality of python-mode in *emacs.
403 438 if frame is self.curframe:
404 439 ret.append('> ')
405 440 else:
406 441 ret.append(' ')
407 442 ret.append(u'%s(%s)%s\n' % (link,lineno,call))
408 443
409 444 start = lineno - 1 - context//2
410 445 lines = ulinecache.getlines(filename)
411 446 start = min(start, len(lines) - context)
412 447 start = max(start, 0)
413 448 lines = lines[start : start + context]
414 449
415 450 for i,line in enumerate(lines):
416 451 show_arrow = (start + 1 + i == lineno)
417 452 linetpl = (frame is self.curframe or show_arrow) \
418 453 and tpl_line_em \
419 454 or tpl_line
420 455 ret.append(self.__format_line(linetpl, filename,
421 456 start + 1 + i, line,
422 457 arrow = show_arrow) )
423 458 return ''.join(ret)
424 459
425 460 def __format_line(self, tpl_line, filename, lineno, line, arrow = False):
426 461 bp_mark = ""
427 462 bp_mark_color = ""
428 463
429 464 scheme = self.color_scheme_table.active_scheme_name
430 465 new_line, err = self.parser.format2(line, 'str', scheme)
431 466 if not err: line = new_line
432 467
433 468 bp = None
434 469 if lineno in self.get_file_breaks(filename):
435 470 bps = self.get_breaks(filename, lineno)
436 471 bp = bps[-1]
437 472
438 473 if bp:
439 474 Colors = self.color_scheme_table.active_colors
440 475 bp_mark = str(bp.number)
441 476 bp_mark_color = Colors.breakpoint_enabled
442 477 if not bp.enabled:
443 478 bp_mark_color = Colors.breakpoint_disabled
444 479
445 480 numbers_width = 7
446 481 if arrow:
447 482 # This is the line with the error
448 483 pad = numbers_width - len(str(lineno)) - len(bp_mark)
449 484 num = '%s%s' % (make_arrow(pad), str(lineno))
450 485 else:
451 486 num = '%*s' % (numbers_width - len(bp_mark), str(lineno))
452 487
453 488 return tpl_line % (bp_mark_color + bp_mark, num, line)
454 489
455 490
456 491 def print_list_lines(self, filename, first, last):
457 492 """The printing (as opposed to the parsing part of a 'list'
458 493 command."""
459 494 try:
460 495 Colors = self.color_scheme_table.active_colors
461 496 ColorsNormal = Colors.Normal
462 497 tpl_line = '%%s%s%%s %s%%s' % (Colors.lineno, ColorsNormal)
463 498 tpl_line_em = '%%s%s%%s %s%%s%s' % (Colors.linenoEm, Colors.line, ColorsNormal)
464 499 src = []
465 500 if filename == "<string>" and hasattr(self, "_exec_filename"):
466 501 filename = self._exec_filename
467 502
468 503 for lineno in range(first, last+1):
469 504 line = ulinecache.getline(filename, lineno)
470 505 if not line:
471 506 break
472 507
473 508 if lineno == self.curframe.f_lineno:
474 509 line = self.__format_line(tpl_line_em, filename, lineno, line, arrow = True)
475 510 else:
476 511 line = self.__format_line(tpl_line, filename, lineno, line, arrow = False)
477 512
478 513 src.append(line)
479 514 self.lineno = lineno
480 515
481 516 print(''.join(src))
482 517
483 518 except KeyboardInterrupt:
484 519 pass
485 520
486 521 def do_list(self, arg):
487 522 self.lastcmd = 'list'
488 523 last = None
489 524 if arg:
490 525 try:
491 526 x = eval(arg, {}, {})
492 527 if type(x) == type(()):
493 528 first, last = x
494 529 first = int(first)
495 530 last = int(last)
496 531 if last < first:
497 532 # Assume it's a count
498 533 last = first + last
499 534 else:
500 535 first = max(1, int(x) - 5)
501 536 except:
502 537 print('*** Error in argument:', repr(arg))
503 538 return
504 539 elif self.lineno is None:
505 540 first = max(1, self.curframe.f_lineno - 5)
506 541 else:
507 542 first = self.lineno + 1
508 543 if last is None:
509 544 last = first + 10
510 545 self.print_list_lines(self.curframe.f_code.co_filename, first, last)
511 546
512 547 # vds: >>
513 548 lineno = first
514 549 filename = self.curframe.f_code.co_filename
515 550 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
516 551 # vds: <<
517 552
518 553 do_l = do_list
519 554
520 555 def getsourcelines(self, obj):
521 556 lines, lineno = inspect.findsource(obj)
522 557 if inspect.isframe(obj) and obj.f_globals is obj.f_locals:
523 558 # must be a module frame: do not try to cut a block out of it
524 559 return lines, 1
525 560 elif inspect.ismodule(obj):
526 561 return lines, 1
527 562 return inspect.getblock(lines[lineno:]), lineno+1
528 563
529 564 def do_longlist(self, arg):
530 565 self.lastcmd = 'longlist'
531 566 try:
532 567 lines, lineno = self.getsourcelines(self.curframe)
533 568 except OSError as err:
534 569 self.error(err)
535 570 return
536 571 last = lineno + len(lines)
537 572 self.print_list_lines(self.curframe.f_code.co_filename, lineno, last)
538 573 do_ll = do_longlist
539 574
540 575 def do_pdef(self, arg):
541 576 """Print the call signature for any callable object.
542 577
543 578 The debugger interface to %pdef"""
544 579 namespaces = [('Locals', self.curframe.f_locals),
545 580 ('Globals', self.curframe.f_globals)]
546 581 self.shell.find_line_magic('pdef')(arg, namespaces=namespaces)
547 582
548 583 def do_pdoc(self, arg):
549 584 """Print the docstring for an object.
550 585
551 586 The debugger interface to %pdoc."""
552 587 namespaces = [('Locals', self.curframe.f_locals),
553 588 ('Globals', self.curframe.f_globals)]
554 589 self.shell.find_line_magic('pdoc')(arg, namespaces=namespaces)
555 590
556 591 def do_pfile(self, arg):
557 592 """Print (or run through pager) the file where an object is defined.
558 593
559 594 The debugger interface to %pfile.
560 595 """
561 596 namespaces = [('Locals', self.curframe.f_locals),
562 597 ('Globals', self.curframe.f_globals)]
563 598 self.shell.find_line_magic('pfile')(arg, namespaces=namespaces)
564 599
565 600 def do_pinfo(self, arg):
566 601 """Provide detailed information about an object.
567 602
568 603 The debugger interface to %pinfo, i.e., obj?."""
569 604 namespaces = [('Locals', self.curframe.f_locals),
570 605 ('Globals', self.curframe.f_globals)]
571 606 self.shell.find_line_magic('pinfo')(arg, namespaces=namespaces)
572 607
573 608 def do_pinfo2(self, arg):
574 609 """Provide extra detailed information about an object.
575 610
576 611 The debugger interface to %pinfo2, i.e., obj??."""
577 612 namespaces = [('Locals', self.curframe.f_locals),
578 613 ('Globals', self.curframe.f_globals)]
579 614 self.shell.find_line_magic('pinfo2')(arg, namespaces=namespaces)
580 615
581 616 def do_psource(self, arg):
582 617 """Print (or run through pager) the source code for an object."""
583 618 namespaces = [('Locals', self.curframe.f_locals),
584 619 ('Globals', self.curframe.f_globals)]
585 620 self.shell.find_line_magic('psource')(arg, namespaces=namespaces)
586 621
587 622 if sys.version_info > (3, ):
588 623 def do_where(self, arg):
589 624 """w(here)
590 625 Print a stack trace, with the most recent frame at the bottom.
591 626 An arrow indicates the "current frame", which determines the
592 627 context of most commands. 'bt' is an alias for this command.
593 628
594 629 Take a number as argument as an (optional) number of context line to
595 630 print"""
596 631 if arg:
597 632 context = int(arg)
598 633 self.print_stack_trace(context)
599 634 else:
600 635 self.print_stack_trace()
601 636
602 637 do_w = do_where
General Comments 0
You need to be logged in to leave comments. Login now