diff --git a/py/file.go b/py/file.go index 3d9f0185..4335abdb 100644 --- a/py/file.go +++ b/py/file.go @@ -31,6 +31,9 @@ func init() { FileType.Dict["flush"] = MustNewMethod("flush", func(self Object) (Object, error) { return self.(*File).Flush() }, 0, "flush() -> Flush the write buffers of the stream if applicable. This does nothing for read-only and non-blocking streams.") + FileType.Dict["readline"] = MustNewMethod("readline", func(self Object, args Tuple, kwargs StringDict) (Object, error) { + return self.(*File).ReadLine(args, kwargs) + }, 0, "readline(size=-1, /) -> Read and return one line from the stream. If size is specified, at most size bytes will be read.\n\nThe line terminator is always b'\\n' for binary files; for text files, the newline argument to open can be used to select the line terminator(s) recognized.") } type FileMode int @@ -143,6 +146,44 @@ func (o *File) Read(args Tuple, kwargs StringDict) (Object, error) { return o.readResult(b) } +func (o *File) ReadLine(args Tuple, kwargs StringDict) (Object, error) { + var size Object = None + err := UnpackTuple(args, kwargs, "readline", 0, 1, &size) + if err != nil { + return nil, err + } + limit := int64(-1) + if size != None { + pyN, ok := size.(Int) + if !ok { + return nil, ExceptionNewf(TypeError, "integer argument expected, got '%s'", size.Type().Name) + } + limit, _ = pyN.GoInt64() + } + + var buf []byte + b := make([]byte, 1) + for { + if limit >= 0 && int64(len(buf)) >= limit { + break + } + n, err := o.File.Read(b) + if n > 0 { + buf = append(buf, b[0]) + if b[0] == '\n' { + break + } + } + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + } + return o.readResult(buf) +} + func (o *File) Close() (Object, error) { _ = o.File.Close() return None, nil diff --git a/py/import.go b/py/import.go index 5ca6598a..8adc8568 100644 --- a/py/import.go +++ b/py/import.go @@ -108,7 +108,9 @@ func ImportModuleLevelObject(ctx Context, name string, globals, locals StringDic } if fromFile, ok := globals["__file__"]; ok { - opts.CurDir = filepath.Dir(string(fromFile.(String))) + if fromFileStr, ok := fromFile.(String); ok { + opts.CurDir = filepath.Dir(string(fromFileStr)) + } } module, err := RunFile(ctx, srcPathname, opts, name) @@ -344,14 +346,42 @@ func BuiltinImport(ctx Context, self Object, args Tuple, kwargs StringDict, curr var globals Object = currentGlobal var locals Object = NewStringDict() var fromlist Object = Tuple{} + var fromlistTuple Tuple var level Object = Int(0) err := ParseTupleAndKeywords(args, kwargs, "U|OOOi:__import__", kwlist, &name, &globals, &locals, &fromlist, &level) if err != nil { return nil, err } - if fromlist == None { - fromlist = Tuple{} + levelObj, ok := level.(Int) + if !ok { + return nil, ExceptionNewf(TypeError, "__import__() argument 5 must be int, not %s", level.Type().Name) + } + levelInt, err := levelObj.GoInt() + if err != nil { + return nil, err + } + + globalsDict, ok := globals.(StringDict) + if !ok { + if levelInt > 0 { + return nil, ExceptionNewf(TypeError, "globals must be a dict") + } + globalsDict = StringDict{} } - return ImportModuleLevelObject(ctx, string(name.(String)), globals.(StringDict), locals.(StringDict), fromlist.(Tuple), int(level.(Int))) + + localsDict, ok := locals.(StringDict) + if !ok { + localsDict = StringDict{} + } + + fromlistTuple = Tuple{} + if fromlist != None { + fromlistTuple, err = SequenceTuple(fromlist) + if err != nil { + return nil, err + } + } + + return ImportModuleLevelObject(ctx, string(name.(String)), globalsDict, localsDict, fromlistTuple, levelInt) } diff --git a/py/method.go b/py/method.go index c8b0ab03..438ad5f4 100644 --- a/py/method.go +++ b/py/method.go @@ -84,6 +84,7 @@ const ( InternalMethodImport InternalMethodEval InternalMethodExec + InternalMethodVars ) var MethodType = NewType("method", "method object") 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/py/tests/file.py b/py/tests/file.py index 898bc1bd..9c1d4e01 100644 --- a/py/tests/file.py +++ b/py/tests/file.py @@ -25,6 +25,12 @@ b = f.read() assert b == '' +doc = "readline" +f2 = open(__file__) +line = f2.readline() +assert line == '# Copyright 2018 The go-python Authors. All rights reserved.\n' +f2.close() + doc = "write" assertRaises(TypeError, f.write, 42) 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..dae29324 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -6,9 +6,12 @@ package builtin import ( + "errors" "fmt" + "io" "math/big" "strconv" + "strings" "unicode/utf8" "github.com/go-python/gpython/compile" @@ -37,6 +40,7 @@ func init() { py.MustNewMethod("divmod", builtin_divmod, 0, divmod_doc), py.MustNewMethod("eval", py.InternalMethodEval, 0, eval_doc), py.MustNewMethod("exec", py.InternalMethodExec, 0, exec_doc), + py.MustNewMethod("exit", builtin_exit, 0, exit_doc), // py.MustNewMethod("format", builtin_format, 0, format_doc), py.MustNewMethod("getattr", builtin_getattr, 0, getattr_doc), py.MustNewMethod("globals", py.InternalMethodGlobals, 0, globals_doc), @@ -44,7 +48,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), @@ -58,12 +62,13 @@ func init() { py.MustNewMethod("ord", builtin_ord, 0, ord_doc), py.MustNewMethod("pow", builtin_pow, 0, pow_doc), py.MustNewMethod("print", builtin_print, 0, print_doc), + py.MustNewMethod("quit", builtin_quit, 0, quit_doc), py.MustNewMethod("repr", builtin_repr, 0, repr_doc), py.MustNewMethod("round", builtin_round, 0, round_doc), py.MustNewMethod("setattr", builtin_setattr, 0, setattr_doc), py.MustNewMethod("sorted", builtin_sorted, 0, sorted_doc), py.MustNewMethod("sum", builtin_sum, 0, sum_doc), - // py.MustNewMethod("vars", builtin_vars, 0, vars_doc), + py.MustNewMethod("vars", py.InternalMethodVars, 0, vars_doc), } globals := py.StringDict{ "None": py.None, @@ -1181,6 +1186,113 @@ 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.` + +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 { + s, ok := prompt.(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "input() prompt must be a string") + } + promptStr = string(s) + } + line, err := py.InputHook(promptStr) + if err != nil { + if errors.Is(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) + } + } + + readline, err := py.GetAttrString(stdin, "readline") + if err != nil { + return nil, err + } + result, err := py.Call(readline, nil, nil) + if err != nil { + return nil, err + } + line, ok := result.(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "object.readline() should return a str object, got %s", result.Type().Name) + } + if line == "" { + return nil, py.ExceptionNewf(py.EOFError, "EOF when reading a line") + } + line = py.String(strings.TrimRight(string(line), "\r\n")) + return line, nil +} + +const exit_doc = `exit([status]) + +Exit the interpreter by raising SystemExit(status).` + +const quit_doc = `quit([status]) + +Alias for exit().` + +func builtin_exit(self py.Object, args py.Tuple) (py.Object, error) { + return builtinExit("exit", args) +} + +func builtin_quit(self py.Object, args py.Tuple) (py.Object, error) { + return builtinExit("quit", args) +} + +func builtinExit(name string, args py.Tuple) (py.Object, error) { + var exitCode py.Object + err := py.UnpackTuple(args, nil, name, 0, 1, &exitCode) + if err != nil { + return nil, err + } + exc, err := py.ExceptionNew(py.SystemExit, args, nil) + if err != nil { + return nil, err + } + return nil, exc.(*py.Exception) +} + const locals_doc = `locals() -> dictionary Update and return a dictionary containing the current scope's local variables.` @@ -1189,6 +1301,11 @@ const globals_doc = `globals() -> dictionary Return the dictionary containing the current scope's global variables.` +const vars_doc = `vars([object]) -> dictionary + +Without an argument, equivalent to locals(). +With an argument, equivalent to object.__dict__.` + const sum_doc = `sum($module, iterable, start=0, /) -- Return the sum of a \'start\' value (default: 0) plus an iterable of numbers diff --git a/stdlib/builtin/tests/builtin.py b/stdlib/builtin/tests/builtin.py index ae4e8a5f..0fef8d23 100644 --- a/stdlib/builtin/tests/builtin.py +++ b/stdlib/builtin/tests/builtin.py @@ -79,6 +79,15 @@ assert exec("b = a+100", glob) == None assert glob["b"] == 200 +doc="exit/quit" +assertRaises(SystemExit, exit) +assertRaises(SystemExit, exit, 0) +assertRaises(SystemExit, exit, 3) +assertRaises(SystemExit, quit) +assertRaises(SystemExit, quit, "bye") +assertRaises(TypeError, exit, 1, 2) +assertRaises(TypeError, quit, 1, 2) + doc="getattr" class C: def __init__(self): @@ -107,6 +116,39 @@ def fn(x): assert locals()["x"] == 1 fn(1) +doc="vars" +def fn(x): + assert vars()["x"] == 1 +fn(1) + +# Test vars() with an object that has __dict__ (function objects have __dict__) +def test_func(): + pass + +assert vars(test_func) == test_func.__dict__ +assert isinstance(vars(test_func), dict) + +ok = False +try: + vars(test_func, test_func) +except TypeError: + ok = True +assert ok, "TypeError not raised for too many arguments" + +ok = False +try: + vars(x=1) +except TypeError: + ok = True +assert ok, "TypeError not raised for keyword arguments" + +ok = False +try: + vars(test_func, y=1) +except TypeError: + ok = True +assert ok, "TypeError not raised for keyword arguments with object" + def func(p): return p[1] @@ -468,5 +510,37 @@ class C: pass assert lib.libfn() == 42 assert lib.libvar == 43 assert lib.libclass().method() == 44 +lib = __import__("lib", {}, {}, [""]) +assert lib.libfn() == 42 +ok = False +try: + __import__("lib", {}, {}, 1) +except TypeError: + ok = True +assert ok, "TypeError not raised" +lib = __import__("lib", 1, {}, [""]) +assert lib.libfn() == 42 +ok = False +try: + __import__("lib", 1, {}, [""], 1) +except TypeError as e: + if e.args[0] != "globals must be a dict": + raise + ok = True +assert ok, "TypeError not raised" +lib = __import__("lib", {"__file__": 1}, {}, [""]) +assert lib.libfn() == 42 + +doc="input" +import sys +class MockStdin: + def __init__(self, line): + self._line = line + def readline(self): + return self._line +old_stdin = sys.stdin +sys.stdin = MockStdin("hello\n") +assert input() == "hello" +sys.stdin = old_stdin doc="finished" diff --git a/vm/eval.go b/vm/eval.go index d32cf734..9db0fae9 100644 --- a/vm/eval.go +++ b/vm/eval.go @@ -1599,6 +1599,23 @@ func callInternal(fn py.Object, args py.Tuple, kwargs py.StringDict, f *py.Frame case py.InternalMethodExec: f.FastToLocals() return builtinExec(f.Context, args, kwargs, f.Locals, f.Globals, f.Builtins) + case py.InternalMethodVars: + if len(kwargs) > 0 { + return nil, py.ExceptionNewf(py.TypeError, "vars() takes no keyword arguments") + } + switch len(args) { + case 0: + f.FastToLocals() + return f.Locals, nil + case 1: + attr, err := py.GetAttrString(args[0], "__dict__") + if err != nil { + return nil, err + } + return attr, nil + default: + return nil, py.ExceptionNewf(py.TypeError, "vars() takes at most 1 argument (%d given)", len(args)) + } default: return nil, py.ExceptionNewf(py.SystemError, "Internal method %v not found", x) }