我的 Python 解釋器 Memphis 有一個 REPL(讀取-評估-列印循環)!
這是舊聞了。只要你在與聰明的老貓頭鷹互動時零失誤,就可以隨心所欲地解讀。假設您不想對同一個語句求值兩次,或者如果您這樣做,也不介意重新輸入它。而且零錯誤。
我對這個 REPL 非常滿意。甚至激動不已。我已經寫信回家談論這個 REPL。但我老闆的老闆的老闆要求我們為了人民的底線而改進 REPL。他們把我叫進辦公室,點頭讓我坐在紅木辦公桌對面的椅子上。 「有些用戶希望退格鍵起作用。」讓用戶見鬼去吧! 「向上箭頭應該會顯示他們的最後一個命令。」箭頭鍵支援是如此九十年代! 「你能在第五季結束前完成這項工作嗎?」我放棄了!
所以我回到辦公桌前改進了 REPL。
我對其進行了很大改進,所有按鍵都可以使用。退格鍵、向上箭頭、向下箭頭、向左箭頭,以及最後但並非最不重要的退格箭頭。會計師可以使用新的 REPL 進行實地考察。勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾。那是會計師在敲數字,而不是慢慢擴散的炸彈。
我將 REPL 送到實驗室,並告訴我的主要機械師對該訂單進行加急工作。這是我說的 REPL,從他們的眼神我可以看出他們明白了。 750 毫秒後,建置完成,我們獲得了箭頭鍵支援。我把產品帶回給大佬們,請求讓我恢復工作,並詢問他們的想法。他們運行了一些命令,列印了一些列印內容,並添加了一些添加內容。他們犯了一個錯誤,按下了退格鍵。我翻了個白眼,因為說真的,誰犯了錯誤,但他們似乎很滿意。他們意識到他們不想運行已經輸入的長命令,這就是我的生活陷入困境的地方。他們。打。 Ctrl。 C. 說真的,這是誰幹的? !您知道這會結束當前進程,對嗎?對嗎? ? ?
「到明年年底,我們需要 Ctrl-C 支援。」這些人和他們的要求。我會添加 Ctrl-C 支援。但兩年之內絕對不會。
所以我回到辦公桌並添加了 Ctrl-C 支援。
我把我的整個職業和財務未來都押在「從頭開始」建立東西上,所以我在這個專案的第一天就面臨著困境。我選擇使用 crossterm 進行關鍵檢測主要是因為跨平台支援。老實說,crossterm 非常非常好。 API 很直觀,我對 KeyModifiers 特別滿意(我們需要它來處理 Ctrl-C,我認為這是不必要的,請參見上文)。
我們需要它,這樣終端就不會為我們處理特殊的金鑰。但該死的,我沒有意識到它會把我們的螢幕變成一台故障的打字機。無論如何,我必須規範所有字串以在任何換行符之前添加回車符。效果很好,我對此感到非常興奮。
/// When the terminal is in raw mode, we must emit a carriage return in addition to a newline, /// because that does not happen automatically. fn normalize<T: Display>(err: T) -> String { let formatted = format!("{}", err); if terminal::is_raw_mode_enabled().expect("Failed to query terminal raw mode") { formatted.replace("\n", "\n\r") } else { formatted.to_string() } } /// Print command which will normalize newlines + carriage returns before printing. fn print_raw<T: Display>(val: T) { print!("{}", normalize(val)); io::stdout().flush().expect("Failed to flush stdout"); }
還有!如果您沒有在意外恐慌時停用原始模式,您的終端會話將變得不可用。我安裝了自訂恐慌處理程序來捕捉這個並玩得很好。
panic::set_hook(Box::new(|info| { // This line is critical!! The rest of this function is just debug info, but without this // line, your shell will become unusable on an unexpected panic. let _ = terminal::disable_raw_mode(); if let Some(s) = info.payload().downcast_ref::<&str>() { eprintln!("\nPanic: {s:?}"); } else if let Some(s) = info.payload().downcast_ref::<String>() { eprintln!("\nPanic: {s:?}"); } else { eprintln!("\nPanic occurred!"); } if let Some(location) = info.location() { eprintln!( " in file '{}' at line {}", location.file(), location.line() ); } else { eprintln!(" in an unknown location."); } process::exit(1); }));
在我的舊 REPL(我更喜歡它,見上文)下,我可以透過執行二進位檔案並將一些 Python 程式碼傳遞到 stdin 來測試它的整合。當使用 crossterm 時,它停止工作,我認為是由於合約糾紛。老實說,我無法完全解釋它,但是 event::read() 在標準輸入輸入提供的整合測試中會超時並失敗。所以我嘲笑了它。
pub trait TerminalIO { fn read_event(&mut self) -> Result<Event, io::Error>; fn write<T: Display>(&mut self, output: T) -> io::Result<()>; fn writeln<T: Display>(&mut self, output: T) -> io::Result<()>; } /// A mock for testing that doesn't use `crossterm`. struct MockTerminalIO { /// Predefined events for testing events: Vec<Event>, /// Captured output for assertions output: Vec<String>, } impl TerminalIO for MockTerminalIO { fn read_event(&mut self) -> Result<Event, io::Error> { if self.events.is_empty() { Err(io::Error::new(io::ErrorKind::Other, "No more events")) } else { // remove from the front (semantically similar to VecDequeue::pop_front). Ok(self.events.remove(0)) } } fn write<T: Display>(&mut self, output: T) -> io::Result<()> { self.output.push(format!("{}", output)); Ok(()) } fn writeln<T: Display>(&mut self, output: T) -> io::Result<()> { self.write(output)?; self.write("\n")?; Ok(()) } }
這導致整件事情變成了單元測試?老實說我不知道。此時,如果我 a) 在另一個二進位檔案中呼叫一個二進位文件,或者 2) 在測試中啟動伺服器/開啟連接埠/偵聽套接字,我將其稱為整合測試。如果您想在評論中留下其他定義,請不要留下,因為這聽起來很煩人。
我創建了兩個實用函數來開始。
/// Run the complete flow, from input code string to return value string. If you need any Ctrl /// modifiers, do not use this! fn run_and_return(input: &str) -> String { let mut terminal = MockTerminalIO::from_str(input); Repl::new().run(&mut terminal); terminal.return_val() } /// Turn an input string into a list of crossterm events so we don't have to /// hand-compile our test. fn string_to_events(input: &str) -> Vec<Event> { input .chars() .map(|c| { let key_code = match c { '\n' => KeyCode::Enter, _ => KeyCode::Char(c), }; Event::Key(KeyEvent::new(key_code, KeyModifiers::NONE)) }) .collect() }
使用這些,我們現在可以用相當少的樣板來測試這些常見場景。
#[test] fn test_repl_name_error() { let return_val = run_and_return("e\n"); assert!(return_val.contains("NameError: name 'e' is not defined")); } #[test] fn test_repl_expr() { let third_from_last = run_and_return("12345\n"); assert_eq!(third_from_last, "12345"); } #[test] fn test_repl_statement() { let return_val = run_and_return("a = 5.5\n"); // empty string because a statement does not have a return value assert_eq!(return_val, ""); } #[test] fn test_repl_function() { let code = r#" def foo(): a = 10 return 2 * a foo() "#; let return_val = run_and_return(code); assert_eq!(return_val, "20"); } #[test] fn test_repl_ctrl_c() { let mut events = string_to_events("123456789\n"); let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); events.insert(4, ctrl_c); let mut terminal = MockTerminalIO::new(events); Repl::new().run(&mut terminal); assert_eq!(terminal.return_val(), "56789"); }
我添加 REPL 的動機之一是因為我相信當您添加第二個入口點時,您的程式碼會變得更好。您實際上正在成為您的圖書館的第二個用戶,這可以幫助您更接近地理解我們都在鍵盤上尋找的完美抽象。我是認真的說這一點。
REPL 現在位於功能標誌後面,作為重新獲得管理的一種方式。我在零第三方 crate 的幫助下保持解釋 Python 程式碼的能力,這意味著 crossterm 要么需要成為例外,要么我會引入一個功能標誌。現在,如果你在沒有啟用 REPL 的情況下編譯並運行“memphis”,它會禮貌地告訴你“錯誤的構建,笨蛋。”
REPL 就在這裡。你可以像這樣運行它。如果你想買的話,那聽起來就像是個騙局。祝你好起來,盡快說話。
如果您想將更多類似的貼文直接發送到您的收件匣,您可以在這裡訂閱!
除了指導軟體工程師之外,我還寫了我作為成人診斷自閉症患者的經歷。更少的程式碼和相同數量的笑話。
以上是適合胖手指打字的 REPL的詳細內容。更多資訊請關注PHP中文網其他相關文章!