55from  subprocess  import  check_output 
66import  os .path 
77import  uuid 
8+ import  random 
9+ import  string 
810
911import  re 
1012import  signal 
@@ -25,23 +27,39 @@ class IREPLWrapper(replwrap.REPLWrapper):
2527    :param line_output_callback: a callback method to receive each batch 
2628      of incremental output. It takes one string parameter. 
2729    """ 
28-     def  __init__ (self , cmd_or_spawn , orig_prompt , prompt_change ,
30+     def  __init__ (self , cmd_or_spawn , orig_prompt , prompt_change ,  unique_prompt , 
2931                 extra_init_cmd = None , line_output_callback = None ):
32+         self .unique_prompt  =  unique_prompt 
3033        self .line_output_callback  =  line_output_callback 
34+         # The extra regex at the start of PS1 below is designed to catch the 
35+         # `(envname) ` which conda/mamba add to the start of PS1 by default. 
36+         # Obviously anything else that looks like this, including user output, 
37+         # will be eaten. 
38+         # FIXME: work out if there is a way to update these by reading PS1 
39+         # after each command and checking that it has changed. The answer is 
40+         # probably no, as we never see individual commands but rather cells 
41+         # with possibly many commands, and would need to update this half-way 
42+         # through a cell. 
43+         self .ps1_re  =  r"(\(\w+\) )?"  +  re .escape (self .unique_prompt  +  ">" )
44+         self .ps2_re  =  re .escape (self .unique_prompt  +  "+" )
3145        replwrap .REPLWrapper .__init__ (self , cmd_or_spawn , orig_prompt ,
32-                                       prompt_change , extra_init_cmd = extra_init_cmd )
46+                 prompt_change , new_prompt = self .ps1_re ,
47+                 continuation_prompt = self .ps2_re , extra_init_cmd = extra_init_cmd )
3348
3449    def  _expect_prompt (self , timeout = - 1 ):
50+         prompts  =  [self .ps1_re , self .ps2_re ]
51+ 
3552        if  timeout  ==  None :
3653            # "None" means we are executing code from a Jupyter cell by way of the run_command 
37-             # in the do_execute() code below, so do incremental output. 
54+             # in the do_execute() code below, so do incremental output, i.e. 
55+             # also look for end of line or carridge return 
56+             prompts .extend (['\r ?\n ' , '\r ' ])
3857            while  True :
39-                 pos  =  self .child .expect_exact ([self .prompt , self .continuation_prompt , u'\r \n ' , u'\n ' , u'\r ' ],
40-                                               timeout = None )
41-                 if  pos  ==  2  or  pos  ==  3 :
58+                 pos  =  self .child .expect_list ([re .compile (x ) for  x  in  prompts ], timeout = None )
59+                 if  pos  ==  2 :
4260                    # End of line received. 
4361                    self .line_output_callback (self .child .before  +  '\n ' )
44-                 elif  pos  ==  4 :
62+                 elif  pos  ==  3 :
4563                    # Carriage return ('\r') received. 
4664                    self .line_output_callback (self .child .before  +  '\r ' )
4765                else :
@@ -50,8 +68,8 @@ def _expect_prompt(self, timeout=-1):
5068                        self .line_output_callback (self .child .before )
5169                    break 
5270        else :
53-             # Otherwise, use existing non-incremental code  
54-             pos  =  replwrap . REPLWrapper . _expect_prompt ( self , timeout = timeout )
71+             # Otherwise, wait (with timeout) until the next prompt  
72+             pos  =  self . child . expect_list ([ re . compile ( x )  for   x   in   prompts ] , timeout = timeout )
5573
5674        # Prompt received, so return normally 
5775        return  pos 
@@ -79,6 +97,9 @@ def banner(self):
7997                     'file_extension' : '.sh' }
8098
8199    def  __init__ (self , ** kwargs ):
100+         # Make a random prompt, further reducing chances of accidental matches. 
101+         rand  =  '' .join (random .choices (string .ascii_uppercase , k = 12 ))
102+         self .unique_prompt  =  "PROMPT_"  +  rand 
82103        Kernel .__init__ (self , ** kwargs )
83104        self ._start_bash ()
84105        self ._known_display_ids  =  set ()
@@ -97,12 +118,16 @@ def _start_bash(self):
97118            bashrc  =  os .path .join (os .path .dirname (pexpect .__file__ ), 'bashrc.sh' )
98119            child  =  pexpect .spawn ("bash" , ['--rcfile' , bashrc ], echo = False ,
99120                                  encoding = 'utf-8' , codec_errors = 'replace' )
100-             ps1  =  replwrap .PEXPECT_PROMPT [:5 ] +  u'\[\]'  +  replwrap .PEXPECT_PROMPT [5 :]
101-             ps2  =  replwrap .PEXPECT_CONTINUATION_PROMPT [:5 ] +  u'\[\]'  +  replwrap .PEXPECT_CONTINUATION_PROMPT [5 :]
121+             # Following comment stolen from upstream's REPLWrap: 
122+             # If the user runs 'env', the value of PS1 will be in the output. To avoid 
123+             # replwrap seeing that as the next prompt, we'll embed the marker characters 
124+             # for invisible characters in the prompt; these show up when inspecting the 
125+             # environment variable, but not when bash displays the prompt. 
126+             ps1  =  self .unique_prompt  +  u'\[\]'  +  ">" 
127+             ps2  =  self .unique_prompt  +  u'\[\]'  +  "+" 
102128            prompt_change  =  u"PS1='{0}' PS2='{1}' PROMPT_COMMAND=''" .format (ps1 , ps2 )
103- 
104129            # Using IREPLWrapper to get incremental output 
105-             self .bashwrapper  =  IREPLWrapper (child , u'\$' , prompt_change ,
130+             self .bashwrapper  =  IREPLWrapper (child , u'\$' , prompt_change ,  self . unique_prompt , 
106131                                            extra_init_cmd = "export PAGER=cat" ,
107132                                            line_output_callback = self .process_output )
108133        finally :
@@ -182,8 +207,8 @@ def do_execute(self, code, silent, store_history=True,
182207            return  {'status' : 'abort' , 'execution_count' : self .execution_count }
183208
184209        try :
185-             exitcode  =  int (self .bashwrapper .run_command ('echo $?' ).rstrip ())
186-         except  Exception :
210+             exitcode  =  int (self .bashwrapper .run_command ('echo $?' ).rstrip (). split ( " \r \n " )[ 0 ] )
211+         except  Exception   as   exc :
187212            exitcode  =  1 
188213
189214        if  exitcode :
0 commit comments