Heim >Backend-Entwicklung >Python-Tutorial >Eine Ersatzlösung für fettfingerfreundliches Tippen
Mein Python-Interpreter Memphis hat eine REPL (Read-Eval-Print-Schleife)!
Das sind alte Nachrichten. Solange Sie bei der Interaktion mit der weisen alten Eule keine Fehler gemacht haben, konnten Sie nach Herzenslust dolmetschen. Angenommen, Sie wollten nie die gleiche Aussage zweimal bewerten, oder wenn ja, hätten Sie nichts dagegen, sie noch einmal einzugeben. Auch mit null Fehlern.
Ich war mit diesem REPL vollkommen zufrieden. Sogar begeistert. Ich hatte über diesen REPL nach Hause geschrieben. Aber die Bosse meiner Vorgesetzten forderten, dass wir die REPL verbessern, um das Endergebnis für die Menschen zu erreichen. Sie riefen mich in ihr Büro und setzten mich auf den Stuhl gegenüber ihrem Mahagonischreibtisch. „Einige Benutzer erwarten, dass die Rücktaste funktioniert.“ Zur Hölle mit den Benutzern! „Der Aufwärtspfeil sollte ihren letzten Befehl anzeigen.“ PFEILTASTENUNTERSTÜTZUNG IST SO NEUNZIG! „Können Sie das bis zum Ende des fünften Quartals erledigen?“ Ich gebe auf!
Also ging ich zurück an meinen Schreibtisch und verbesserte die REPL.
Ich habe es so sehr verbessert, dass alle Tasten funktionierten. Die Rücktaste, der Aufwärtspfeil, der Abwärtspfeil, der Linkspfeil und nicht zuletzt der Rücktaste. Ein Buchhalter könnte mit der neuen REPL viel Spaß haben. tick tick tick tick tick tick tick tick tick tick. Das ist der Buchhalter, der Zahlen eintippt, und keine Bombe, die sich langsam ausbreitet.
Ich habe die REPL ins Labor geschickt und meinem Hauptmaschinisten gesagt, er solle diese Bestellung schnellstmöglich erledigen. Es ist die REPL, die ich gesagt habe, und an dem Blick in ihren Augen konnte ich erkennen, dass sie es verstanden haben. 750 ms später war der Build abgeschlossen und wir hatten Pfeiltastenunterstützung. Ich brachte das Produkt zu den großen Perücken zurück, bettelte um meinen Job zurück und fragte sie, was sie davon hielten. Sie führten ein paar Befehle aus, druckten einige Ausdrucke und fügten einige Ergänzungen hinzu. Sie haben einen Fehler gemacht und die Rücktaste gedrückt. Ich verdrehte die Augen, denn im Ernst, wer macht Fehler, aber sie schienen zufrieden zu sein. Sie erkannten, dass sie keinen langen Befehl ausführen wollten, den sie bereits getippt hatten, und hier ging mein Leben in einem Handkorb zur Hölle. Sie. Schlag. Strg. C. Im Ernst, wer macht das?! Sie wissen, dass damit der aktuelle Prozess beendet ist, oder? RICHTIG???
„Wir brauchen bis Ende nächsten Jahres Strg-C-Unterstützung.“ Diese Menschen und ihre Ansprüche. Ich würde Strg-C-Unterstützung hinzufügen. Aber das würde auf keinen Fall innerhalb der nächsten zwei Jahre der Fall sein.
Also ging ich zurück an meinen Schreibtisch und fügte Strg-C-Unterstützung hinzu.
Ich habe meine gesamte berufliche und finanzielle Zukunft darauf gesetzt, Dinge „von Grund auf neu“ aufzubauen, und so stand ich am ersten Tag dieses Projekts vor einer Zwickmühle. Ich habe mich vor allem wegen der plattformübergreifenden Unterstützung für die Verwendung von Crossterm für die Schlüsselerkennung entschieden. Ehrlich gesagt war Crossterm sehr, sehr gut. Die API ist intuitiv und ich war besonders zufrieden mit KeyModifiers (die wir brauchten, um Strg-C zu verarbeiten, was ich für unnötig hielt, siehe oben).
Wir brauchten es, damit das Terminal keine Sonderschlüssel für uns verarbeiten konnte. Aber verdammt, ich hätte nicht gedacht, dass es unseren Bildschirm in eine defekte Schreibmaschine verwandeln würde. Wie auch immer, ich musste alle Zeichenfolgen normalisieren, um vor allen Zeilenumbrüchen einen Wagenrücklauf einzufügen. Was gut funktioniert hat und ich bin BEGEISTERT darüber.
/// 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"); }
Auch! Wenn Sie den Raw-Modus bei einer unerwarteten Panik nicht deaktivieren, wird Ihre Terminalsitzung unbrauchbar. Ich habe einen benutzerdefinierten Panik-Handler installiert, um dies abzufangen und nett zu spielen.
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); }));
Unter meiner alten REPL (die ich bevorzugt habe, siehe oben) konnte ich die Integration testen, indem ich einfach die Binärdatei ausführte und etwas Python-Code an stdin übergab. Ich glaube, das hat bei der Verwendung von Crossterm aufgrund eines Vertragsstreits nicht mehr funktioniert. Ich kann es ehrlich gesagt nicht vollständig erklären, aber event::read() würde eine Zeitüberschreitung verursachen und im Integrationstest, der mit der stdin-Eingabe bereitgestellt wird, fehlschlagen. Also habe ich mich darüber lustig gemacht.
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(()) } }
Was dazu führte, dass das Ganze zu einem Unit-Test wurde? Ehrlich gesagt weiß ich es nicht. An dieser Stelle nenne ich es einen Integrationstest, wenn ich entweder a) eine Binärdatei innerhalb einer anderen Binärdatei aufrufe oder 2) einen Server starte / einen Port öffne / einen Socket innerhalb eines Tests abhöre. Wenn Sie eine andere Definition haben, die Sie in den Kommentaren hinterlassen möchten, hinterlassen Sie diese bitte nicht, denn das klingt ehrlich gesagt nervig.
Ich habe zwei Hilfsfunktionen erstellt, um loszulegen.
/// 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() }
Damit können wir diese gängigen Szenarien nun mit relativ wenig Standardaufwand testen.
#[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"); }
Eine meiner Motivationen, überhaupt eine REPL hinzuzufügen, war, dass ich glaube, dass Sie Ihren Code verbessern, wenn Sie einen zweiten Einstiegspunkt hinzufügen. Sie werden im Wesentlichen zum zweiten Benutzer Ihrer Bibliothek, was Ihnen hilft, die Eine Perfekte Abstraktion, nach der wir alle auf unserer Tastatur suchen, besser zu verstehen. Ich meine diesen Punkt ernst.
Die REPL steckt jetzt hinter einem Feature-Flag, um wieder an die Kontrolle zu kommen. Ich halte die Fähigkeit aufrecht, Python-Code mit Hilfe von Null-Crates von Drittanbietern zu interpretieren, was bedeutet, dass Crossterm entweder eine Ausnahme sein müsste oder ich ein Feature-Flag einführen würde. Wenn Sie nun kompilieren, ohne dass REPL aktiviert ist, und „memphis“ ausführen, wird Ihnen höflich mitgeteilt: „Falscher Build, Idiot.“
Die REPL ist da. Sie können es so ausführen. Wenn Sie es kaufen möchten, klingt das nach Betrug. Seien Sie gesund und reden Sie bald.
Wenn Sie weitere Beiträge dieser Art direkt in Ihrem Posteingang erhalten möchten, können Sie sich hier anmelden!
Neben der Betreuung von Softwareentwicklern schreibe ich auch über meine Erfahrungen als Erwachsener, bei dem Autismus diagnostiziert wurde. Weniger Code und die gleiche Anzahl an Witzen.
Das obige ist der detaillierte Inhalt vonEine Ersatzlösung für fettfingerfreundliches Tippen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!