Maison >développement back-end >Tutoriel Python >Un REPL pour une saisie conviviale avec les gros doigts

Un REPL pour une saisie conviviale avec les gros doigts

Barbara Streisand
Barbara Streisandoriginal
2024-10-22 06:12:30497parcourir

A REPL for Fat-Finger Friendly Typing

Mon interprète Python, Memphis, a un REPL (boucle lecture-évaluation-impression) !

C'est une vieille nouvelle. Tant que vous n’avez commis aucune erreur en interagissant avec le vieux hibou sage ?, vous pouvez interpréter à votre guise. En supposant que vous n’ayez jamais voulu évaluer deux fois la même déclaration, ou si vous l’avez fait, cela ne vous dérangeait pas de la retaper. Également sans erreur.

J'étais parfaitement content de ce REPL. Ravie même. J'avais écrit à la maison à propos de ce REPL. Mais les patrons de mes patrons ont exigé que nous améliorions le REPL pour le bénéfice du peuple. Ils m'ont appelé dans leur bureau et m'ont fait signe de m'asseoir sur la chaise en face de leur bureau en acajou. "Certains utilisateurs s'attendent à ce que la touche Retour arrière fonctionne." AU ENFER AVEC LES UTILISATEURS ! "La flèche vers le haut devrait faire apparaître leur dernière commande." LE SUPPORT DES FLÈCHES EST TELLEMENT ANNÉES 90 ! « Pouvez-vous avoir terminé cela d'ici la fin du cinquième trimestre ? » J'ARRÊTE !

Je suis donc retourné à mon bureau et j'ai amélioré le REPL.

Je l'ai tellement amélioré que toutes les touches ont fonctionné. La touche retour arrière, la flèche vers le haut, la flèche vers le bas, la flèche gauche et enfin, mais non des moindres, la flèche retour arrière. Un comptable pourrait s’en donner à cœur joie avec le nouveau REPL. tique tique tique tique tique tique tique tique tique tique. C’est le comptable qui tape des chiffres, pas une bombe qui se diffuse lentement.

J'ai envoyé le REPL au laboratoire et j'ai dit à mon machiniste principal de faire un travail urgent sur cette commande. C’est le REPL que j’ai dit et d’après leur regard, je peux dire qu’ils ont compris. 750 ms plus tard, la construction était terminée et nous bénéficiions du support des touches fléchées. J'ai ramené le produit aux grosses perruques, j'ai demandé mon travail et je leur ai demandé ce qu'ils en pensaient. Ils ont exécuté quelques commandes, imprimé des impressions et ajouté quelques ajouts. Ils ont commis une erreur et ont appuyé sur la touche Retour arrière. J'ai roulé des yeux car sérieusement qui fait des erreurs mais ils semblaient satisfaits. Ils ont réalisé qu’ils ne voulaient pas exécuter une longue commande qu’ils avaient déjà tapée et c’est là que ma vie s’est transformée en enfer dans un panier à main. Ils. frapper. Ctrl. C. Sérieusement, qui fait ça ?! Vous savez que cela met fin au processus actuel, n'est-ce pas ? N'EST-CE PAS ???

"Nous avons besoin du support Ctrl-C d'ici la fin de l'année prochaine." Ces gens et leurs revendications. J'ajouterais le support Ctrl-C. Mais ce ne serait absolument pas dans les deux prochaines années.

Je suis donc retourné à mon bureau et j'ai ajouté le support Ctrl-C.

Qu’est-ce qui a rendu ce REPL digne des personnes ayant des tendances aux gros doigts ?

Serais-je un outil ?

J'ai misé tout mon avenir professionnel et financier sur la construction de choses « à partir de zéro », j'ai donc été confronté à un dilemme dès le premier jour de ce projet. J'ai choisi d'utiliser crossterm pour la détection des clés principalement en raison de la prise en charge multiplateforme. Honnêtement, le crossterm était très, très bon. L'API est intuitive et j'ai été particulièrement satisfait des KeyModifiers (dont nous avions besoin pour gérer Ctrl-C, ce que je pensais inutile, voir ci-dessus).

Le mode brut est pénible

Nous en avions besoin pour que le terminal ne gère pas de clés spéciales à notre place. Mais bon sang, je n'avais pas réalisé que cela transformerait notre écran en une machine à écrire défectueuse. Quoi qu'il en soit, j'ai dû normaliser toutes les chaînes pour ajouter un retour chariot avant tout caractère de nouvelle ligne. Ce qui a bien fonctionné et j'en suis ravi.

/// 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");
}

Aussi ! Si vous ne désactivez pas le mode brut en cas de panique inattendue, votre session de terminal devient inutilisable. J'ai installé un gestionnaire de panique personnalisé pour détecter cela et jouer gentiment.

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);
}));

Les tests d'intégration étaient amusants

Sous mon ancien REPL (que je préférais, voir ci-dessus), je pouvais le tester en termes d'intégration en exécutant simplement le binaire et en transmettant du code Python à stdin. Cela a cessé de fonctionner lors de l'utilisation de crossterm, je pense, à cause d'un différend contractuel. Honnêtement, je ne peux pas l'expliquer complètement, mais event::read() expirerait et échouerait dans le test d'intégration fourni avec l'entrée stdin. Alors je m'en suis moqué.

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(())
    }
}

Ce qui a fait que le tout est devenu un test unitaire ? Honnêtement, je ne sais pas. À ce stade, j'appelle cela un test d'intégration si j'ai) appelle un binaire dans un autre binaire, ou 2) lance un serveur / ouvre un port / écoute sur un socket dans un test. Si vous avez une autre définition que vous aimeriez laisser dans les commentaires, ne le faites pas car cela semble ennuyeux TBH.

J'ai créé deux fonctions utilitaires pour commencer.

/// 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()
}

Grâce à ceux-ci, nous pouvons désormais tester ces scénarios courants avec assez peu de passe-partout.

#[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");
}

Les points d'entrée du code me font sortir du lit le matin

L'une de mes motivations en ajoutant un REPL était parce que je pense que vous améliorez votre code lorsque vous ajoutez un deuxième point d'entrée. Vous devenez essentiellement le deuxième utilisateur de votre bibliothèque, ce qui vous aide à vous rapprocher de la compréhension de The One Perfect Abstraction que nous recherchons tous sur nos claviers. Je pense ce point avec sérieux.

« Zéro dépendances » ?

Le REPL se cache désormais derrière un indicateur de fonctionnalité comme moyen de se venger de la direction. Je garde en vie la possibilité d'interpréter le code Python à l'aide de zéro caisse tierce, ce qui signifie que crossterm devrait soit être une exception, soit introduire un indicateur de fonctionnalité. Maintenant, si vous compilez sans REPL activé et exécutez « memphis », il vous dira poliment « mauvaise version, idiot ».

Au revoir

Le REPL est ici. Vous pouvez l'exécuter comme ça. Si vous voulez l'acheter, cela ressemble à une arnaque. Portez-vous bien et parlez bientôt.


Si vous souhaitez recevoir plus de messages comme celui-ci directement dans votre boîte de réception, vous pouvez vous abonner ici !

Autre part

En plus d'encadrer des ingénieurs logiciels, j'écris également sur mon expérience en tant que personne autiste diagnostiquée chez l'adulte. Moins de code et le même nombre de blagues.

  • La Grande-Bretagne avec modération - From Scratch dot org

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Types de données en PythonArticle suivant:Types de données en Python