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