11import code
22import contextlib
33import errno
4+ import functools
45import greenlet
56import logging
67import os
1011import sys
1112import tempfile
1213import threading
14+ import time
1315import unicodedata
1416
1517from bpython import autocomplete
3941from bpython .curtsiesfrontend import sitefix ; sitefix .monkeypatch_quit ()
4042import bpython .curtsiesfrontend .replpainter as paint
4143from bpython .curtsiesfrontend .coderunner import CodeRunner , FakeOutput
44+ from bpython .curtsiesfrontend .filewatch import ModuleChangedEventHandler
4245
4346#TODO other autocomplete modes (also fix in other bpython implementations)
4447
@@ -193,15 +196,16 @@ class Repl(BpythonRepl):
193196 """
194197
195198 ## initialization, cleanup
196- def __init__ (self , locals_ = None , config = None ,
197- request_refresh = lambda : None , get_term_hw = lambda :(50 , 10 ),
198- get_cursor_vertical_diff = lambda : 0 , banner = None , interp = None , interactive = True ,
199- orig_tcattrs = None ):
199+ def __init__ (self , locals_ = None , config = None , request_refresh = lambda : None ,
200+ request_reload = lambda desc : None , get_term_hw = lambda :(50 , 10 ),
201+ get_cursor_vertical_diff = lambda : 0 , banner = None , interp = None ,
202+ interactive = True , orig_tcattrs = None ):
200203 """
201204 locals_ is a mapping of locals to pass into the interpreter
202205 config is a bpython config.Struct with config attributes
203206 request_refresh is a function that will be called when the Repl
204207 wants to refresh the display, but wants control returned to it afterwards
208+ Takes as a kwarg when= which is when to fire
205209 get_term_hw is a function that returns the current width and height
206210 of the terminal
207211 get_cursor_vertical_diff is a function that returns how the cursor moved
@@ -229,18 +233,26 @@ def __init__(self, locals_=None, config=None,
229233
230234 self .reevaluating = False
231235 self .fake_refresh_requested = False
232- def smarter_request_refresh ():
236+ def smarter_request_refresh (when = 'now' ):
233237 if self .reevaluating or self .paste_mode :
234238 self .fake_refresh_requested = True
235239 else :
236- request_refresh ()
240+ request_refresh (when = when )
237241 self .request_refresh = smarter_request_refresh
242+ def smarter_request_reload (desc ):
243+ if self .watching_files :
244+ request_reload (desc )
245+ else :
246+ pass
247+ self .request_reload = smarter_request_reload
238248 self .get_term_hw = get_term_hw
239249 self .get_cursor_vertical_diff = get_cursor_vertical_diff
240250
241- self .status_bar = StatusBar (banner , _ (
242- " <%s> Rewind <%s> Save <%s> Pastebin <%s> Editor"
243- ) % (config .undo_key , config .save_key , config .pastebin_key , config .external_editor_key ),
251+ self .status_bar = StatusBar (
252+ banner ,
253+ (_ (" <%s> Rewind <%s> Save <%s> Pastebin <%s> Editor" )
254+ % (config .undo_key , config .save_key , config .pastebin_key , config .external_editor_key )
255+ if config .curtsies_fill_terminal else '' ),
244256 refresh_request = self .request_refresh
245257 )
246258 self .rl_char_sequences = get_updated_char_sequences (key_dispatch , config )
@@ -280,12 +292,15 @@ def smarter_request_refresh():
280292 self .paste_mode = False
281293 self .current_match = None
282294 self .list_win_visible = False
295+ self .watching_files = False
283296
284297 self .original_modules = sys .modules .keys ()
285298
286299 self .width = None # will both be set by a window resize event
287300 self .height = None
288301
302+ self .watcher = ModuleChangedEventHandler ([], smarter_request_reload )
303+
289304 def __enter__ (self ):
290305 self .orig_stdout = sys .stdout
291306 self .orig_stderr = sys .stderr
@@ -295,13 +310,27 @@ def __enter__(self):
295310 sys .stdin = self .stdin
296311 self .orig_sigwinch_handler = signal .getsignal (signal .SIGWINCH )
297312 signal .signal (signal .SIGWINCH , self .sigwinch_handler )
313+
314+ self .orig_import = __builtins__ ['__import__' ]
315+ @functools .wraps (self .orig_import )
316+ def new_import (name , globals = {}, locals = {}, fromlist = [], level = - 1 ):
317+ m = self .orig_import (name , globals = globals , locals = locals , fromlist = fromlist )
318+ if hasattr (m , "__file__" ):
319+ if self .watching_files :
320+ self .watcher .add_module (m .__file__ )
321+ else :
322+ self .watcher .add_module_later (m .__file__ )
323+ return m
324+ __builtins__ ['__import__' ] = new_import
325+
298326 return self
299327
300328 def __exit__ (self , * args ):
301329 sys .stdin = self .orig_stdin
302330 sys .stdout = self .orig_stdout
303331 sys .stderr = self .orig_stderr
304332 signal .signal (signal .SIGWINCH , self .orig_sigwinch_handler )
333+ __builtins__ ['__import__' ] = self .orig_import
305334
306335 def sigwinch_handler (self , signum , frame ):
307336 old_rows , old_columns = self .height , self .width
@@ -347,7 +376,9 @@ def process_event(self, e):
347376
348377 logger .debug ("processing event %r" , e )
349378 if isinstance (e , events .RefreshRequestEvent ):
350- if self .status_bar .has_focus :
379+ if e .when != 'now' :
380+ pass # This is a scheduled refresh - it's really just a refresh (so nop)
381+ elif self .status_bar .has_focus :
351382 self .status_bar .process_event (e )
352383 else :
353384 assert self .coderunner .code_is_waiting
@@ -377,6 +408,28 @@ def process_event(self, e):
377408 self .update_completion ()
378409 return
379410
411+ elif isinstance (e , events .ReloadEvent ):
412+ if self .watching_files :
413+ self .clear_modules_and_reevaluate ()
414+ self .update_completion ()
415+ self .status_bar .message ('Reloaded at ' + time .strftime ('%H:%M:%S' ) + ' because ' + ' & ' .join (e .files_modified ) + ' modified' )
416+
417+ elif e in key_dispatch [self .config .toggle_file_watch_key ]:
418+ msg = "Auto-reloading active, watching for file changes..."
419+ if self .watching_files :
420+ self .watcher .deactivate ()
421+ self .watching_files = False
422+ self .status_bar .pop_permanent_message (msg )
423+ else :
424+ self .watching_files = True
425+ self .status_bar .push_permanent_message (msg )
426+ self .watcher .activate ()
427+
428+ elif e in key_dispatch [self .config .reimport_key ]:
429+ self .clear_modules_and_reevaluate ()
430+ self .update_completion ()
431+ self .status_bar .message ('Reloaded at ' + time .strftime ('%H:%M:%S' ) + ' by user' )
432+
380433 elif (e in ("<RIGHT>" , '<Ctrl-f>' ) and self .config .curtsies_right_arrow_completion
381434 and self .cursor_offset == len (self .current_line )):
382435 self .current_line += self .current_suggestion
@@ -441,9 +494,6 @@ def process_event(self, e):
441494 elif e in ("<Shift-TAB>" ,):
442495 self .on_tab (back = True )
443496 self .rl_history .reset ()
444- elif e in key_dispatch [self .config .reimport_key ]:
445- self .clear_modules_and_reevaluate ()
446- self .update_completion ()
447497 elif e in key_dispatch [self .config .undo_key ]: #ctrl-r for undo
448498 self .undo ()
449499 self .update_completion ()
@@ -554,6 +604,7 @@ def send_session_to_external_editor(self, filename=None):
554604 self .cursor_offset = len (self .current_line )
555605
556606 def clear_modules_and_reevaluate (self ):
607+ self .watcher .reset ()
557608 cursor , line = self .cursor_offset , self .current_line
558609 for modname in sys .modules .keys ():
559610 if modname not in self .original_modules :
@@ -804,7 +855,7 @@ def paint(self, about_to_exit=False, user_quit=False):
804855 self .clean_up_current_line_for_exit () # exception to not changing state!
805856
806857 width , min_height = self .width , self .height
807- show_status_bar = bool (self .status_bar ._message ) or (self .config .curtsies_fill_terminal or self .status_bar .has_focus )
858+ show_status_bar = bool (self .status_bar .should_show_message ) or (self .config .curtsies_fill_terminal or self .status_bar .has_focus )
808859 if show_status_bar :
809860 min_height -= 1
810861
@@ -992,6 +1043,7 @@ def reprint_line(self, lineno, tokens):
9921043 self .display_buffer [lineno ] = bpythonparse (format (tokens , self .formatter ))
9931044 def reevaluate (self , insert_into_history = False ):
9941045 """bpython.Repl.undo calls this"""
1046+ self .watcher .reset ()
9951047 old_logical_lines = self .history
9961048 self .history = []
9971049 self .display_lines = []
0 commit comments