##// END OF EJS Templates
[merge] Merging with upstream trunk.
Fernando Perez -
r2097:6aff330d merge
parent child Browse files
Show More
@@ -1,7 +1,7 b''
1 """
1 """
2 IPython extension: autoreload modules before executing the next line
2 IPython extension: autoreload modules before executing the next line
3
3
4 Try::
4 Try::
5
5
6 %autoreload?
6 %autoreload?
7
7
@@ -32,7 +32,7 b' PY_COMPILED_EXT = _get_compiled_ext()'
32 class ModuleReloader(object):
32 class ModuleReloader(object):
33 failed = {}
33 failed = {}
34 """Modules that failed to reload: {module: mtime-on-failed-reload, ...}"""
34 """Modules that failed to reload: {module: mtime-on-failed-reload, ...}"""
35
35
36 modules = {}
36 modules = {}
37 """Modules specially marked as autoreloadable."""
37 """Modules specially marked as autoreloadable."""
38
38
@@ -44,39 +44,39 b' class ModuleReloader(object):'
44
44
45 old_objects = {}
45 old_objects = {}
46 """(module-name, name) -> weakref, for replacing old code objects"""
46 """(module-name, name) -> weakref, for replacing old code objects"""
47
47
48 def check(self, check_all=False):
48 def check(self, check_all=False):
49 """Check whether some modules need to be reloaded."""
49 """Check whether some modules need to be reloaded."""
50
50
51 if check_all or self.check_all:
51 if check_all or self.check_all:
52 modules = sys.modules.keys()
52 modules = sys.modules.keys()
53 else:
53 else:
54 modules = self.modules.keys()
54 modules = self.modules.keys()
55
55
56 for modname in modules:
56 for modname in modules:
57 m = sys.modules.get(modname, None)
57 m = sys.modules.get(modname, None)
58
58
59 if modname in self.skip_modules:
59 if modname in self.skip_modules:
60 continue
60 continue
61
61
62 if not hasattr(m, '__file__'):
62 if not hasattr(m, '__file__'):
63 continue
63 continue
64
64
65 if m.__name__ == '__main__':
65 if m.__name__ == '__main__':
66 # we cannot reload(__main__)
66 # we cannot reload(__main__)
67 continue
67 continue
68
68
69 filename = m.__file__
69 filename = m.__file__
70 dirname = os.path.dirname(filename)
70 dirname = os.path.dirname(filename)
71 path, ext = os.path.splitext(filename)
71 path, ext = os.path.splitext(filename)
72
72
73 if ext.lower() == '.py':
73 if ext.lower() == '.py':
74 ext = PY_COMPILED_EXT
74 ext = PY_COMPILED_EXT
75 filename = os.path.join(dirname, path + PY_COMPILED_EXT)
75 filename = os.path.join(dirname, path + PY_COMPILED_EXT)
76
76
77 if ext != PY_COMPILED_EXT:
77 if ext != PY_COMPILED_EXT:
78 continue
78 continue
79
79
80 try:
80 try:
81 pymtime = os.stat(filename[:-1]).st_mtime
81 pymtime = os.stat(filename[:-1]).st_mtime
82 if pymtime <= os.stat(filename).st_mtime:
82 if pymtime <= os.stat(filename).st_mtime:
@@ -85,7 +85,7 b' class ModuleReloader(object):'
85 continue
85 continue
86 except OSError:
86 except OSError:
87 continue
87 continue
88
88
89 try:
89 try:
90 superreload(m, reload, self.old_objects)
90 superreload(m, reload, self.old_objects)
91 if filename[:-1] in self.failed:
91 if filename[:-1] in self.failed:
@@ -118,12 +118,12 b' def update_class(old, new):'
118 new_obj = getattr(new, key)
118 new_obj = getattr(new, key)
119 except AttributeError:
119 except AttributeError:
120 # obsolete attribute: remove it
120 # obsolete attribute: remove it
121 try:
121 try:
122 delattr(old, key)
122 delattr(old, key)
123 except (AttributeError, TypeError):
123 except (AttributeError, TypeError):
124 pass
124 pass
125 continue
125 continue
126
126
127 if update_generic(old_obj, new_obj): continue
127 if update_generic(old_obj, new_obj): continue
128
128
129 try:
129 try:
@@ -146,9 +146,9 b' UPDATE_RULES = ['
146 (lambda a, b: isinstance2(a, b, types.TypeType),
146 (lambda a, b: isinstance2(a, b, types.TypeType),
147 update_class),
147 update_class),
148 (lambda a, b: isinstance2(a, b, types.FunctionType),
148 (lambda a, b: isinstance2(a, b, types.FunctionType),
149 update_function),
149 update_function),
150 (lambda a, b: isinstance2(a, b, property),
150 (lambda a, b: isinstance2(a, b, property),
151 update_property),
151 update_property),
152 (lambda a, b: isinstance2(a, b, types.MethodType),
152 (lambda a, b: isinstance2(a, b, types.MethodType),
153 lambda a, b: update_function(a.im_func, b.im_func)),
153 lambda a, b: update_function(a.im_func, b.im_func)),
154 ]
154 ]
@@ -168,15 +168,15 b' class StrongRef(object):'
168
168
169 def superreload(module, reload=reload, old_objects={}):
169 def superreload(module, reload=reload, old_objects={}):
170 """Enhanced version of the builtin reload function.
170 """Enhanced version of the builtin reload function.
171
171
172 superreload remembers objects previously in the module, and
172 superreload remembers objects previously in the module, and
173
173
174 - upgrades the class dictionary of every old class in the module
174 - upgrades the class dictionary of every old class in the module
175 - upgrades the code object of every old function and method
175 - upgrades the code object of every old function and method
176 - clears the module's namespace before reloading
176 - clears the module's namespace before reloading
177
177
178 """
178 """
179
179
180 # collect old objects in the module
180 # collect old objects in the module
181 for name, obj in module.__dict__.items():
181 for name, obj in module.__dict__.items():
182 if not hasattr(obj, '__module__') or obj.__module__ != module.__name__:
182 if not hasattr(obj, '__module__') or obj.__module__ != module.__name__:
@@ -199,7 +199,7 b' def superreload(module, reload=reload, old_objects={}):'
199 except (TypeError, AttributeError, KeyError):
199 except (TypeError, AttributeError, KeyError):
200 pass
200 pass
201 module = reload(module)
201 module = reload(module)
202
202
203 # iterate over all objects and update functions & classes
203 # iterate over all objects and update functions & classes
204 for name, new_obj in module.__dict__.items():
204 for name, new_obj in module.__dict__.items():
205 key = (module.__name__, name)
205 key = (module.__name__, name)
@@ -248,40 +248,46 b' def disable_autoreload():'
248
248
249 def autoreload_f(self, parameter_s=''):
249 def autoreload_f(self, parameter_s=''):
250 r""" %autoreload => Reload modules automatically
250 r""" %autoreload => Reload modules automatically
251
251
252 %autoreload
252 %autoreload
253 Reload all modules (except those excluded by %aimport) automatically now.
253 Reload all modules (except those excluded by %aimport) automatically now.
254
254
255 %autoreload 0
256 Disable automatic reloading.
257
255 %autoreload 1
258 %autoreload 1
256 Reload all modules imported with %aimport every time before executing
259 Reload all modules imported with %aimport every time before executing
257 the Python code typed.
260 the Python code typed.
258
261
259 %autoreload 2
262 %autoreload 2
260 Reload all modules (except those excluded by %aimport) every time
263 Reload all modules (except those excluded by %aimport) every time
261 before executing the Python code typed.
264 before executing the Python code typed.
262
265
263 Reloading Python modules in a reliable way is in general difficult,
266 Reloading Python modules in a reliable way is in general
264 and unexpected things may occur. %autoreload tries to work
267 difficult, and unexpected things may occur. %autoreload tries to
265 around common pitfalls by replacing code objects of functions
268 work around common pitfalls by replacing function code objects and
266 previously in the module with new versions. This makes the following
269 parts of classes previously in the module with new versions. This
267 things to work:
270 makes the following things to work:
268
271
269 - Functions and classes imported via 'from xxx import foo' are upgraded
272 - Functions and classes imported via 'from xxx import foo' are upgraded
270 to new versions when 'xxx' is reloaded.
273 to new versions when 'xxx' is reloaded.
274
271 - Methods and properties of classes are upgraded on reload, so that
275 - Methods and properties of classes are upgraded on reload, so that
272 calling 'c.foo()' on an object 'c' created before the reload causes
276 calling 'c.foo()' on an object 'c' created before the reload causes
273 the new code for 'foo' to be executed.
277 the new code for 'foo' to be executed.
274
278
275 Some of the known remaining caveats are:
279 Some of the known remaining caveats are:
276
280
277 - Replacing code objects does not always succeed: changing a @property
281 - Replacing code objects does not always succeed: changing a @property
278 in a class to an ordinary method or a method to a member variable
282 in a class to an ordinary method or a method to a member variable
279 can cause problems (but in old objects only).
283 can cause problems (but in old objects only).
284
280 - Functions that are removed (eg. via monkey-patching) from a module
285 - Functions that are removed (eg. via monkey-patching) from a module
281 before it is reloaded are not upgraded.
286 before it is reloaded are not upgraded.
287
282 - C extension modules cannot be reloaded, and so cannot be
288 - C extension modules cannot be reloaded, and so cannot be
283 autoreloaded.
289 autoreloaded.
284
290
285 """
291 """
286 if parameter_s == '':
292 if parameter_s == '':
287 reloader.check(True)
293 reloader.check(True)
@@ -307,7 +313,7 b" def aimport_f(self, parameter_s=''):"
307 Mark module 'foo' to not be autoreloaded for %autoreload 1
313 Mark module 'foo' to not be autoreloaded for %autoreload 1
308
314
309 """
315 """
310
316
311 modname = parameter_s
317 modname = parameter_s
312 if not modname:
318 if not modname:
313 to_reload = reloader.modules.keys()
319 to_reload = reloader.modules.keys()
@@ -329,12 +335,15 b" def aimport_f(self, parameter_s=''):"
329 except KeyError: pass
335 except KeyError: pass
330 reloader.modules[modname] = True
336 reloader.modules[modname] = True
331
337
332 mod = __import__(modname)
338 # Inject module to user namespace; handle also submodules properly
333 ip.to_user_ns({modname: mod})
339 __import__(modname)
340 basename = modname.split('.')[0]
341 mod = sys.modules[basename]
342 ip.to_user_ns({basename: mod})
334
343
335 def init():
344 def init():
336 ip.expose_magic('autoreload', autoreload_f)
345 ip.expose_magic('autoreload', autoreload_f)
337 ip.expose_magic('aimport', aimport_f)
346 ip.expose_magic('aimport', aimport_f)
338 ip.set_hook('pre_runcode_hook', runcode_hook)
347 ip.set_hook('pre_runcode_hook', runcode_hook)
339
348
340 init()
349 init()
@@ -447,29 +447,30 b' class ConsoleWidget(editwindow.EditWindow):'
447 # different callbacks share local variables?
447 # different callbacks share local variables?
448
448
449 # Intercept some specific keys.
449 # Intercept some specific keys.
450 if event.KeyCode == ord('L') and event.ControlDown() :
450 key_code = event.GetKeyCode()
451 if key_code == ord('L') and event.ControlDown() :
451 self.scroll_to_bottom()
452 self.scroll_to_bottom()
452 elif event.KeyCode == ord('K') and event.ControlDown() :
453 elif key_code == ord('K') and event.ControlDown() :
453 self.input_buffer = ''
454 self.input_buffer = ''
454 elif event.KeyCode == ord('A') and event.ControlDown() :
455 elif key_code == ord('A') and event.ControlDown() :
455 self.GotoPos(self.GetLength())
456 self.GotoPos(self.GetLength())
456 self.SetSelectionStart(self.current_prompt_pos)
457 self.SetSelectionStart(self.current_prompt_pos)
457 self.SetSelectionEnd(self.GetCurrentPos())
458 self.SetSelectionEnd(self.GetCurrentPos())
458 catched = True
459 catched = True
459 elif event.KeyCode == ord('E') and event.ControlDown() :
460 elif key_code == ord('E') and event.ControlDown() :
460 self.GotoPos(self.GetLength())
461 self.GotoPos(self.GetLength())
461 catched = True
462 catched = True
462 elif event.KeyCode == wx.WXK_PAGEUP:
463 elif key_code == wx.WXK_PAGEUP:
463 self.ScrollPages(-1)
464 self.ScrollPages(-1)
464 elif event.KeyCode == wx.WXK_PAGEDOWN:
465 elif key_code == wx.WXK_PAGEDOWN:
465 self.ScrollPages(1)
466 self.ScrollPages(1)
466 elif event.KeyCode == wx.WXK_HOME:
467 elif key_code == wx.WXK_HOME:
467 self.GotoPos(self.GetLength())
468 self.GotoPos(self.GetLength())
468 elif event.KeyCode == wx.WXK_END:
469 elif key_code == wx.WXK_END:
469 self.GotoPos(self.GetLength())
470 self.GotoPos(self.GetLength())
470 elif event.KeyCode == wx.WXK_UP and event.ShiftDown():
471 elif key_code == wx.WXK_UP and event.ShiftDown():
471 self.ScrollLines(-1)
472 self.ScrollLines(-1)
472 elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown():
473 elif key_code == wx.WXK_DOWN and event.ShiftDown():
473 self.ScrollLines(1)
474 self.ScrollLines(1)
474 else:
475 else:
475 catched = False
476 catched = False
@@ -477,13 +478,12 b' class ConsoleWidget(editwindow.EditWindow):'
477 if self.AutoCompActive():
478 if self.AutoCompActive():
478 event.Skip()
479 event.Skip()
479 else:
480 else:
480 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
481 if key_code in (13, wx.WXK_NUMPAD_ENTER):
481 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN,
482 # XXX: not catching modifiers, to be wx2.6-compatible
482 wx.MOD_SHIFT):
483 catched = True
483 catched = True
484 if not self.enter_catched:
484 if not self.enter_catched:
485 self.CallTipCancel()
485 self.CallTipCancel()
486 if event.Modifiers == wx.MOD_SHIFT:
486 if event.ShiftDown():
487 # Try to force execution
487 # Try to force execution
488 self.GotoPos(self.GetLength())
488 self.GotoPos(self.GetLength())
489 self.write('\n' + self.continuation_prompt(),
489 self.write('\n' + self.continuation_prompt(),
@@ -493,19 +493,18 b' class ConsoleWidget(editwindow.EditWindow):'
493 self._on_enter()
493 self._on_enter()
494 self.enter_catched = True
494 self.enter_catched = True
495
495
496 elif event.KeyCode == wx.WXK_HOME:
496 elif key_code == wx.WXK_HOME:
497 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
497 if not event.ShiftDown():
498 self.GotoPos(self.current_prompt_pos)
498 self.GotoPos(self.current_prompt_pos)
499 catched = True
499 catched = True
500
500 else:
501 elif event.Modifiers == wx.MOD_SHIFT:
502 # FIXME: This behavior is not ideal: if the selection
501 # FIXME: This behavior is not ideal: if the selection
503 # is already started, it will jump.
502 # is already started, it will jump.
504 self.SetSelectionStart(self.current_prompt_pos)
503 self.SetSelectionStart(self.current_prompt_pos)
505 self.SetSelectionEnd(self.GetCurrentPos())
504 self.SetSelectionEnd(self.GetCurrentPos())
506 catched = True
505 catched = True
507
506
508 elif event.KeyCode == wx.WXK_UP:
507 elif key_code == wx.WXK_UP:
509 if self.GetCurrentLine() > self.current_prompt_line:
508 if self.GetCurrentLine() > self.current_prompt_line:
510 if self.GetCurrentLine() == self.current_prompt_line + 1 \
509 if self.GetCurrentLine() == self.current_prompt_line + 1 \
511 and self.GetColumn(self.GetCurrentPos()) < \
510 and self.GetColumn(self.GetCurrentPos()) < \
@@ -515,18 +514,18 b' class ConsoleWidget(editwindow.EditWindow):'
515 event.Skip()
514 event.Skip()
516 catched = True
515 catched = True
517
516
518 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
517 elif key_code in (wx.WXK_LEFT, wx.WXK_BACK):
519 if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1):
518 if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1):
520 event.Skip()
519 event.Skip()
521 catched = True
520 catched = True
522
521
523 elif event.KeyCode == wx.WXK_RIGHT:
522 elif key_code == wx.WXK_RIGHT:
524 if not self._keep_cursor_in_buffer(self.GetCurrentPos() + 1):
523 if not self._keep_cursor_in_buffer(self.GetCurrentPos() + 1):
525 event.Skip()
524 event.Skip()
526 catched = True
525 catched = True
527
526
528
527
529 elif event.KeyCode == wx.WXK_DELETE:
528 elif key_code == wx.WXK_DELETE:
530 if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1):
529 if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1):
531 event.Skip()
530 event.Skip()
532 catched = True
531 catched = True
@@ -535,7 +534,7 b' class ConsoleWidget(editwindow.EditWindow):'
535 # Put the cursor back in the edit region
534 # Put the cursor back in the edit region
536 if not self._keep_cursor_in_buffer():
535 if not self._keep_cursor_in_buffer():
537 if not (self.GetCurrentPos() == self.GetLength()
536 if not (self.GetCurrentPos() == self.GetLength()
538 and event.KeyCode == wx.WXK_DELETE):
537 and key_code == wx.WXK_DELETE):
539 event.Skip()
538 event.Skip()
540 catched = True
539 catched = True
541
540
@@ -389,7 +389,8 b' class WxController(ConsoleWidget, PrefilterFrontEnd):'
389 """
389 """
390 # FIXME: This method needs to be broken down in smaller ones.
390 # FIXME: This method needs to be broken down in smaller ones.
391 current_line_num = self.GetCurrentLine()
391 current_line_num = self.GetCurrentLine()
392 if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
392 key_code = event.GetKeyCode()
393 if key_code in (ord('c'), ord('C')) and event.ControlDown():
393 # Capture Control-C
394 # Capture Control-C
394 if self._input_state == 'subprocess':
395 if self._input_state == 'subprocess':
395 if self.debug:
396 if self.debug:
@@ -403,40 +404,39 b' class WxController(ConsoleWidget, PrefilterFrontEnd):'
403 # XXX: We need to make really sure we
404 # XXX: We need to make really sure we
404 # get back to a prompt.
405 # get back to a prompt.
405 elif self._input_state == 'subprocess' and (
406 elif self._input_state == 'subprocess' and (
406 ( event.KeyCode<256 and
407 ( key_code <256 and not event.ControlDown() )
407 not event.ControlDown() )
408 or
408 or
409 ( event.KeyCode in (ord('d'), ord('D')) and
409 ( key_code in (ord('d'), ord('D')) and
410 event.ControlDown())):
410 event.ControlDown())):
411 # We are running a process, we redirect keys.
411 # We are running a process, we redirect keys.
412 ConsoleWidget._on_key_down(self, event, skip=skip)
412 ConsoleWidget._on_key_down(self, event, skip=skip)
413 char = chr(event.KeyCode)
413 char = chr(key_code)
414 # Deal with some inconsistency in wx keycodes:
414 # Deal with some inconsistency in wx keycodes:
415 if char == '\r':
415 if char == '\r':
416 char = '\n'
416 char = '\n'
417 elif not event.ShiftDown():
417 elif not event.ShiftDown():
418 char = char.lower()
418 char = char.lower()
419 if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')):
419 if event.ControlDown() and key_code in (ord('d'), ord('D')):
420 char = '\04'
420 char = '\04'
421 self._running_process.process.stdin.write(char)
421 self._running_process.process.stdin.write(char)
422 self._running_process.process.stdin.flush()
422 self._running_process.process.stdin.flush()
423 elif event.KeyCode in (ord('('), 57, 53):
423 elif key_code in (ord('('), 57, 53):
424 # Calltips
424 # Calltips
425 event.Skip()
425 event.Skip()
426 self.do_calltip()
426 self.do_calltip()
427 elif self.AutoCompActive() and not event.KeyCode == ord('\t'):
427 elif self.AutoCompActive() and not key_code == ord('\t'):
428 event.Skip()
428 event.Skip()
429 if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE):
429 if key_code in (wx.WXK_BACK, wx.WXK_DELETE):
430 wx.CallAfter(self._popup_completion, create=True)
430 wx.CallAfter(self._popup_completion, create=True)
431 elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
431 elif not key_code in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
432 wx.WXK_RIGHT, wx.WXK_ESCAPE):
432 wx.WXK_RIGHT, wx.WXK_ESCAPE):
433 wx.CallAfter(self._popup_completion)
433 wx.CallAfter(self._popup_completion)
434 else:
434 else:
435 # Up history
435 # Up history
436 if event.KeyCode == wx.WXK_UP and (
436 if key_code == wx.WXK_UP and (
437 ( current_line_num == self.current_prompt_line and
437 event.ControlDown() or
438 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
438 current_line_num == self.current_prompt_line
439 or event.ControlDown() ):
439 ):
440 new_buffer = self.get_history_previous(
440 new_buffer = self.get_history_previous(
441 self.input_buffer)
441 self.input_buffer)
442 if new_buffer is not None:
442 if new_buffer is not None:
@@ -445,23 +445,24 b' class WxController(ConsoleWidget, PrefilterFrontEnd):'
445 # Go to first line, for seemless history up.
445 # Go to first line, for seemless history up.
446 self.GotoPos(self.current_prompt_pos)
446 self.GotoPos(self.current_prompt_pos)
447 # Down history
447 # Down history
448 elif event.KeyCode == wx.WXK_DOWN and (
448 elif key_code == wx.WXK_DOWN and (
449 ( current_line_num == self.LineCount -1 and
449 event.ControlDown() or
450 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
450 current_line_num == self.LineCount -1
451 or event.ControlDown() ):
451 ):
452 new_buffer = self.get_history_next()
452 new_buffer = self.get_history_next()
453 if new_buffer is not None:
453 if new_buffer is not None:
454 self.input_buffer = new_buffer
454 self.input_buffer = new_buffer
455 # Tab-completion
455 # Tab-completion
456 elif event.KeyCode == ord('\t'):
456 elif key_code == ord('\t'):
457 current_line, current_line_num = self.CurLine
457 current_line, current_line_num = self.CurLine
458 if not re.match(r'^\s*$', current_line):
458 if not re.match(r'^%s\s*$' % self.continuation_prompt(),
459 current_line):
459 self.complete_current_input()
460 self.complete_current_input()
460 if self.AutoCompActive():
461 if self.AutoCompActive():
461 wx.CallAfter(self._popup_completion, create=True)
462 wx.CallAfter(self._popup_completion, create=True)
462 else:
463 else:
463 event.Skip()
464 event.Skip()
464 elif event.KeyCode == wx.WXK_BACK:
465 elif key_code == wx.WXK_BACK:
465 # If characters where erased, check if we have to
466 # If characters where erased, check if we have to
466 # remove a line.
467 # remove a line.
467 # XXX: What about DEL?
468 # XXX: What about DEL?
@@ -496,7 +497,7 b' class WxController(ConsoleWidget, PrefilterFrontEnd):'
496 def _on_key_up(self, event, skip=True):
497 def _on_key_up(self, event, skip=True):
497 """ Called when any key is released.
498 """ Called when any key is released.
498 """
499 """
499 if event.KeyCode in (59, ord('.')):
500 if event.GetKeyCode() in (59, ord('.')):
500 # Intercepting '.'
501 # Intercepting '.'
501 event.Skip()
502 event.Skip()
502 wx.CallAfter(self._popup_completion, create=True)
503 wx.CallAfter(self._popup_completion, create=True)
1 NO CONTENT: modified file chmod 100755 => 100644
NO CONTENT: modified file chmod 100755 => 100644
General Comments 0
You need to be logged in to leave comments. Login now