diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49657da8..5fc4227a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} @@ -34,12 +34,12 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 with: fetch-depth: 1 - name: Cache-Go - uses: actions/cache@v1 + uses: actions/cache@v4 with: # In order: # * Module download cache @@ -93,11 +93,11 @@ jobs: run: | go run ./ci/run-tests.go $TAGS -race - name: static-check - uses: dominikh/staticcheck-action@v1.2.0 + uses: dominikh/staticcheck-action@v1 with: install-go: false cache-key: ${{ matrix.platform }} version: "2022.1" - name: Upload-Coverage if: matrix.platform == 'ubuntu-latest' - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/main.go b/main.go index 8be7ea2e..8b55ab1e 100644 --- a/main.go +++ b/main.go @@ -60,6 +60,7 @@ func xmain(args []string) { defer pprof.StopCPUProfile() } + var err error // IF no args, enter REPL mode if len(args) == 0 { @@ -69,13 +70,46 @@ func xmain(args []string) { fmt.Printf("- go version: %s\n", runtime.Version()) replCtx := repl.New(ctx) - cli.RunREPL(replCtx) + err = cli.RunREPL(replCtx) + } else { + _, err = py.RunFile(ctx, args[0], py.CompileOpts{}, nil) + } + if err != nil { + if py.IsException(py.SystemExit, err) { + handleSystemExit(err.(py.ExceptionInfo).Value.(*py.Exception)) + } + py.TracebackDump(err) + os.Exit(1) + } +} +func handleSystemExit(exc *py.Exception) { + args := exc.Args.(py.Tuple) + if len(args) == 0 { + os.Exit(0) + } else if len(args) == 1 { + if code, ok := args[0].(py.Int); ok { + c, err := code.GoInt() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(c) + } + msg, err := py.ReprAsString(args[0]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } else { + fmt.Fprintln(os.Stderr, msg) + } + os.Exit(1) } else { - _, err := py.RunFile(ctx, args[0], py.CompileOpts{}, nil) + msg, err := py.ReprAsString(args) if err != nil { - py.TracebackDump(err) - os.Exit(1) + fmt.Fprintln(os.Stderr, err) + } else { + fmt.Fprintln(os.Stderr, msg) } + os.Exit(1) } } diff --git a/main_test.go b/main_test.go index e41ed869..f6347fda 100644 --- a/main_test.go +++ b/main_test.go @@ -48,6 +48,7 @@ func TestGPython(t *testing.T) { } want, err := os.ReadFile(fname) + want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n")) if err != nil { t.Fatalf("could not read golden file: %+v", err) } diff --git a/py/exception.go b/py/exception.go index 73f92747..3de2aa0d 100644 --- a/py/exception.go +++ b/py/exception.go @@ -368,9 +368,16 @@ func (e *Exception) M__str__() (Object, error) { } func (e *Exception) M__repr__() (Object, error) { - msg := e.Args.(Tuple)[0].(String) typ := e.Base.Name - return String(fmt.Sprintf("%s(%q)", typ, string(msg))), nil + args := e.Args.(Tuple) + if len(args) == 0 { + return String(fmt.Sprintf("%s()", typ)), nil + } + msg, err := args.M__repr__() + if err != nil { + return nil, err + } + return String(fmt.Sprintf("%s%s", typ, string(msg.(String)))), nil } // Check Interfaces 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 709a4468..8adc8568 100644 --- a/py/import.go +++ b/py/import.go @@ -7,7 +7,7 @@ package py import ( - "path" + "path/filepath" "strings" ) @@ -101,14 +101,16 @@ func ImportModuleLevelObject(ctx Context, name string, globals, locals StringDic // Convert import's dot separators into path seps parts := strings.Split(name, ".") - srcPathname := path.Join(parts...) + srcPathname := filepath.Join(parts...) opts := CompileOpts{ UseSysPaths: true, } if fromFile, ok := globals["__file__"]; ok { - opts.CurDir = path.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/int.go b/py/int.go index 919c28aa..511f1f88 100644 --- a/py/int.go +++ b/py/int.go @@ -411,9 +411,6 @@ func (a Int) M__truediv__(other Object) (Object, error) { return nil, err } fa := Float(a) - if err != nil { - return nil, err - } fb := b.(Float) if fb == 0 { return nil, divisionByZero @@ -427,9 +424,6 @@ func (a Int) M__rtruediv__(other Object) (Object, error) { return nil, err } fa := Float(a) - if err != nil { - return nil, err - } fb := b.(Float) if fa == 0 { return nil, divisionByZero 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/string.go b/py/string.go index e470c01d..f88d6524 100644 --- a/py/string.go +++ b/py/string.go @@ -148,6 +148,13 @@ func init() { return Bool(false), nil }, 0, "endswith(suffix[, start[, end]]) -> bool") + StringType.Dict["count"] = MustNewMethod("count", func(self Object, args Tuple) (Object, error) { + return self.(String).Count(args) + }, 0, `count(sub[, start[, end]]) -> int +Return the number of non-overlapping occurrences of substring sub in +string S[start:end]. Optional arguments start and end are +interpreted as in slice notation.`) + StringType.Dict["find"] = MustNewMethod("find", func(self Object, args Tuple) (Object, error) { return self.(String).find(args) }, 0, `find(...) @@ -226,6 +233,9 @@ replaced.`) return self.(String).Lower() }, 0, "lower() -> a copy of the string converted to lowercase") + StringType.Dict["join"] = MustNewMethod("join", func(self Object, args Tuple) (Object, error) { + return self.(String).Join(args) + }, 0, "join(iterable) -> return a string which is the concatenation of the strings in iterable") } // Type of this object @@ -609,6 +619,40 @@ func (s String) M__contains__(item Object) (Object, error) { return NewBool(strings.Contains(string(s), string(needle))), nil } +func (s String) Count(args Tuple) (Object, error) { + var ( + pysub Object + pybeg Object = Int(0) + pyend Object = Int(s.len()) + pyfmt = "s|ii:count" + ) + err := ParseTuple(args, pyfmt, &pysub, &pybeg, &pyend) + if err != nil { + return nil, err + } + + var ( + beg = int(pybeg.(Int)) + end = int(pyend.(Int)) + size = s.len() + ) + if beg > size { + beg = size + } + if end < 0 { + end = size + } + if end > size { + end = size + } + + var ( + str = string(s.slice(beg, end, s.len())) + sub = string(pysub.(String)) + ) + return Int(strings.Count(str, sub)), nil +} + func (s String) find(args Tuple) (Object, error) { var ( pysub Object @@ -755,6 +799,30 @@ func (s String) Lower() (Object, error) { return String(strings.ToLower(string(s))), nil } +func (s String) Join(args Tuple) (Object, error) { + if len(args) != 1 { + return nil, ExceptionNewf(TypeError, "join() takes exactly one argument (%d given)", len(args)) + } + var parts []string + iterable, err := Iter(args[0]) + if err != nil { + return nil, err + } + item, err := Next(iterable) + for err == nil { + str, ok := item.(String) + if !ok { + return nil, ExceptionNewf(TypeError, "sequence item %d: expected str instance, %s found", len(parts), item.Type().Name) + } + parts = append(parts, string(str)) + item, err = Next(iterable) + } + if err != StopIteration { + return nil, err + } + return String(strings.Join(parts, string(s))), nil +} + // Check stringerface is satisfied var ( _ richComparison = String("") 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/py/tests/string.py b/py/tests/string.py index f2ad6e9b..d71e8645 100644 --- a/py/tests/string.py +++ b/py/tests/string.py @@ -905,6 +905,15 @@ def index(s, i): a = "ABC" assert a.lower() == "abc" +doc="join" +assert ",".join(['a', 'b', 'c']) == "a,b,c" +assert " ".join(('a', 'b', 'c')) == "a b c" +assert " ".join("abc") == "a b c" +assert "".join(['a', 'b', 'c']) == "abc" +assert ",".join([]) == "" +assert ",".join(()) == "" +assertRaises(TypeError, lambda: ",".join([1, 2, 3])) + class Index: def __index__(self): return 1 @@ -935,5 +944,14 @@ def __index__(self): else: assert False, "TypeError not raised" +doc="count" +assert 'hello world'.count('l') == 3 +assert 'hello world'.count('l', 3) == 2 +assert 'hello world'.count('l', 3, 10) == 2 +assert 'hello world'.count('l', 3, 100) == 2 +assert 'hello world'.count('l', 3, 5) == 1 +assert 'hello world'.count('l', 3, 1) == 0 +assert 'hello world'.count('z') == 0 + doc="finished" diff --git a/pytest/pytest.go b/pytest/pytest.go index 7c331d26..d00f7fb0 100644 --- a/pytest/pytest.go +++ b/pytest/pytest.go @@ -273,6 +273,7 @@ func (task *Task) run() error { return fmt.Errorf("could not read golden output %q: %w", task.GoldFile, err) } + want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n")) diff := cmp.Diff(string(want), string(got)) if !bytes.Equal(got, want) { out := fileBase + ".txt" diff --git a/repl/cli/cli.go b/repl/cli/cli.go index 6648094a..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" ) @@ -117,13 +118,20 @@ func (rl *readline) Print(out string) { } // RunREPL starts the REPL loop -func RunREPL(replCtx *repl.REPL) { +func RunREPL(replCtx *repl.REPL) error { if replCtx == nil { replCtx = repl.New(nil) } 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) { @@ -144,6 +152,10 @@ func RunREPL(replCtx *repl.REPL) { if line != "" { rl.AppendHistory(line) } - rl.repl.Run(line) + err = rl.repl.Run(line) + if err != nil { + return err + } } + return nil } diff --git a/repl/repl.go b/repl/repl.go index f6639b25..3938a7b6 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -66,7 +66,7 @@ func (r *REPL) SetUI(term UI) { } // Run runs a single line of the REPL -func (r *REPL) Run(line string) { +func (r *REPL) Run(line string) error { // Override the PrintExpr output temporarily oldPrintExpr := vm.PrintExpr vm.PrintExpr = r.term.Print @@ -76,13 +76,13 @@ func (r *REPL) Run(line string) { if r.continuation { if line != "" { r.previous += string(line) + "\n" - return + return nil } } // need +"\n" because "single" expects \n terminated input toCompile := r.previous + string(line) if toCompile == "" { - return + return nil } code, err := py.Compile(toCompile+"\n", r.prog, py.SingleMode, 0, true) if err != nil { @@ -97,7 +97,7 @@ func (r *REPL) Run(line string) { r.previous += string(line) + "\n" r.term.SetPrompt(ContinuationPrompt) } - return + return nil } } r.continuation = false @@ -105,12 +105,16 @@ func (r *REPL) Run(line string) { r.previous = "" if err != nil { r.term.Print(fmt.Sprintf("Compile error: %v", err)) - return + return nil } _, err = r.Context.RunCode(code, r.Module.Globals, r.Module.Globals, nil) if err != nil { + if py.IsException(py.SystemExit, err) { + return err + } py.TracebackDump(err) } + return nil } // WordCompleter takes the currently edited line with the cursor 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/stdlib/sys/sys.go b/stdlib/sys/sys.go index 3a2318eb..fc6efc5a 100644 --- a/stdlib/sys/sys.go +++ b/stdlib/sys/sys.go @@ -133,7 +133,11 @@ func sys_exit(self py.Object, args py.Tuple) (py.Object, error) { return nil, err } // Raise SystemExit so callers may catch it or clean up. - return py.ExceptionNew(py.SystemExit, args, nil) + exc, err := py.ExceptionNew(py.SystemExit, args, nil) + if err != nil { + return nil, err + } + return nil, exc.(*py.Exception) } const getdefaultencoding_doc = `getdefaultencoding() -> string 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) } diff --git a/vm/tests/exceptions.py b/vm/tests/exceptions.py index 665757bf..163dd644 100644 --- a/vm/tests/exceptions.py +++ b/vm/tests/exceptions.py @@ -165,4 +165,10 @@ ok = True assert ok, "ValueError not raised" +doc = "exception repr" +repr(ValueError()) == "ValueError()" +repr(ValueError(1)) == "ValueError(1)" +repr(ValueError(1, 2, 3)) == "ValueError(1, 2, 3)" +repr(ValueError("failed")) == 'ValueError("failed")' + doc = "finished"