diff --git a/py/run.go b/py/run.go index 427cdbe6..cd584fc2 100644 --- a/py/run.go +++ b/py/run.go @@ -105,6 +105,11 @@ var ( // Compiles a python buffer into a py.Code object. // Returns a py.Code object or otherwise an error. Compile func(src, srcDesc string, mode CompileMode, flags int, dont_inherit bool) (*Code, error) + + // InputHook is an optional function that can be set to provide a custom input + // mechanism for the input() builtin. If nil, input() reads from sys.stdin. + // This is used by the REPL to integrate with the liner library. + InputHook func(prompt string) (string, error) ) // RunFile resolves the given pathname, compiles as needed, executes the code in the given module, and returns the Module to indicate success. diff --git a/repl/cli/cli.go b/repl/cli/cli.go index 6f7e3966..f6f2f6c0 100644 --- a/repl/cli/cli.go +++ b/repl/cli/cli.go @@ -12,6 +12,7 @@ import ( "os/user" "path/filepath" + "github.com/go-python/gpython/py" "github.com/go-python/gpython/repl" "github.com/peterh/liner" ) @@ -124,6 +125,13 @@ func RunREPL(replCtx *repl.REPL) error { rl := newReadline(replCtx) replCtx.SetUI(rl) defer rl.Close() + + // Set up InputHook for the input() builtin function + py.InputHook = func(prompt string) (string, error) { + return rl.Prompt(prompt) + } + defer func() { py.InputHook = nil }() + err := rl.ReadHistory() if err != nil { if !os.IsNotExist(err) { diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 290cb939..406c985e 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -6,7 +6,9 @@ package builtin import ( + "bufio" "fmt" + "io" "math/big" "strconv" "unicode/utf8" @@ -44,7 +46,7 @@ func init() { // py.MustNewMethod("hash", builtin_hash, 0, hash_doc), py.MustNewMethod("hex", builtin_hex, 0, hex_doc), // py.MustNewMethod("id", builtin_id, 0, id_doc), - // py.MustNewMethod("input", builtin_input, 0, input_doc), + py.MustNewMethod("input", builtin_input, 0, input_doc), py.MustNewMethod("isinstance", builtin_isinstance, 0, isinstance_doc), // py.MustNewMethod("issubclass", builtin_issubclass, 0, issubclass_doc), py.MustNewMethod("iter", builtin_iter, 0, iter_doc), @@ -1181,6 +1183,82 @@ func builtin_chr(self py.Object, args py.Tuple) (py.Object, error) { return py.String(buf[:n]), nil } +const input_doc = `input([prompt]) -> string + +Read a string from standard input. The trailing newline is stripped. +The prompt string, if given, is printed to standard output without a +trailing newline before reading input. +If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError. +On *nix systems, GNU readline is used if enabled.` + +func builtin_input(self py.Object, args py.Tuple) (py.Object, error) { + var prompt py.Object = py.None + + err := py.UnpackTuple(args, nil, "input", 0, 1, &prompt) + if err != nil { + return nil, err + } + + // Use InputHook if available (e.g., in REPL mode) + if py.InputHook != nil { + promptStr := "" + if prompt != py.None { + promptStr = string(prompt.(py.String)) + } + line, err := py.InputHook(promptStr) + if err != nil { + if err == io.EOF { + return nil, py.ExceptionNewf(py.EOFError, "EOF when reading a line") + } + return nil, err + } + return py.String(line), nil + } + + sysModule, err := self.(*py.Module).Context.GetModule("sys") + if err != nil { + return nil, err + } + + stdin := sysModule.Globals["stdin"] + stdout := sysModule.Globals["stdout"] + + if prompt != py.None { + write, err := py.GetAttrString(stdout, "write") + if err != nil { + return nil, err + } + _, err = py.Call(write, py.Tuple{prompt}, nil) + if err != nil { + return nil, err + } + + flush, err := py.GetAttrString(stdout, "flush") + if err == nil { + py.Call(flush, nil, nil) + } + } + + file := stdin.(*py.File) + reader := bufio.NewReader(file.File) + line, err := reader.ReadString('\n') + if err != nil { + if err.Error() == "EOF" { + return nil, py.ExceptionNewf(py.EOFError, "EOF when reading a line") + } + return nil, err + } + + if len(line) > 0 && line[len(line)-1] == '\n' { + line = line[:len(line)-1] + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + } + + return py.String(line), nil +} + const locals_doc = `locals() -> dictionary Update and return a dictionary containing the current scope's local variables.`