diff --git a/py/util.go b/py/util.go index 56558cac..e2904187 100644 --- a/py/util.go +++ b/py/util.go @@ -7,6 +7,7 @@ package py import ( "errors" "strconv" + "strings" ) var ( @@ -204,3 +205,32 @@ func loadValue(src Object, data interface{}) error { } return nil } + +// Println prints the provided strings to gpython's stdout. +func Println(self Object, args ...string) bool { + sysModule, err := self.(*Module).Context.GetModule("sys") + if err != nil { + return false + } + stdout := sysModule.Globals["stdout"] + write, err := GetAttrString(stdout, "write") + if err != nil { + return false + } + call, ok := write.(I__call__) + if !ok { + return false + } + for _, v := range args { + if !strings.Contains(v, "\n") { + v += " " + } + _, err := call.M__call__(Tuple{String(v)}, nil) + if err != nil { + return false + } + + } + _, err = call.M__call__(Tuple{String("\n")}, nil) // newline + return err == nil +} diff --git a/stdlib/os/os.go b/stdlib/os/os.go new file mode 100644 index 00000000..d863575f --- /dev/null +++ b/stdlib/os/os.go @@ -0,0 +1,228 @@ +// Copyright 2022 The go-python Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package os implements the Python os module. +package os + +import ( + "os" + "os/exec" + "runtime" + "strings" + + "github.com/go-python/gpython/py" +) + +var ( + osSep = py.String("/") + osName = py.String("posix") + osPathsep = py.String(":") + osLinesep = py.String("\n") + osDefpath = py.String(":/bin:/usr/bin") + osDevnull = py.String("/dev/null") + + osAltsep py.Object = py.None +) + +func initGlobals() { + switch runtime.GOOS { + case "android": + osName = py.String("java") + case "windows": + osSep = py.String(`\`) + osName = py.String("nt") + osPathsep = py.String(";") + osLinesep = py.String("\r\n") + osDefpath = py.String(`C:\bin`) + osDevnull = py.String("nul") + osAltsep = py.String("/") + } +} + +func init() { + initGlobals() + + methods := []*py.Method{ + py.MustNewMethod("getcwd", getCwd, 0, "Get the current working directory"), + py.MustNewMethod("getcwdb", getCwdb, 0, "Get the current working directory in a byte slice"), + py.MustNewMethod("chdir", chdir, 0, "Change the current working directory"), + py.MustNewMethod("getenv", getenv, 0, "Return the value of the environment variable key if it exists, or default if it doesn’t. key, default and the result are str."), + py.MustNewMethod("getpid", getpid, 0, "Return the current process id."), + py.MustNewMethod("putenv", putenv, 0, "Set the environment variable named key to the string value."), + py.MustNewMethod("unsetenv", unsetenv, 0, "Unset (delete) the environment variable named key."), + py.MustNewMethod("_exit", _exit, 0, "Immediate program termination."), + py.MustNewMethod("system", system, 0, "Run shell commands, prints stdout directly to deault"), + } + globals := py.StringDict{ + "error": py.OSError, + "environ": getEnvVariables(), + "sep": osSep, + "name": osName, + "curdir": py.String("."), + "pardir": py.String(".."), + "extsep": py.String("."), + "altsep": osAltsep, + "pathsep": osPathsep, + "linesep": osLinesep, + "defpath": osDefpath, + "devnull": osDevnull, + } + + py.RegisterModule(&py.ModuleImpl{ + Info: py.ModuleInfo{ + Name: "os", + Doc: "Miscellaneous operating system interfaces", + }, + Methods: methods, + Globals: globals, + }) +} + +// getEnvVariables returns the dictionary of environment variables. +func getEnvVariables() py.StringDict { + vs := os.Environ() + dict := py.NewStringDictSized(len(vs)) + for _, evar := range vs { + key_value := strings.SplitN(evar, "=", 2) // returns a []string containing [key,value] + dict.M__setitem__(py.String(key_value[0]), py.String(key_value[1])) + } + + return dict +} + +// getCwd returns the current working directory. +func getCwd(self py.Object, args py.Tuple) (py.Object, error) { + dir, err := os.Getwd() + if err != nil { + return nil, py.ExceptionNewf(py.OSError, "Unable to get current working directory.") + } + return py.String(dir), nil +} + +// getCwdb returns the current working directory as a byte list. +func getCwdb(self py.Object, args py.Tuple) (py.Object, error) { + dir, err := os.Getwd() + if err != nil { + return nil, py.ExceptionNewf(py.OSError, "Unable to get current working directory.") + } + return py.Bytes(dir), nil +} + +// chdir changes the current working directory to the provided path. +func chdir(self py.Object, args py.Tuple) (py.Object, error) { + if len(args) == 0 { + return nil, py.ExceptionNewf(py.TypeError, "Missing required argument 'path' (pos 1)") + } + dir, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected, not "+args[0].Type().Name) + } + err := os.Chdir(string(dir)) + if err != nil { + return nil, py.ExceptionNewf(py.NotADirectoryError, "Couldn't change cwd; "+err.Error()) + } + return py.None, nil +} + +// getenv returns the value of the environment variable key. +// If no such environment variable exists and a default value was provided, that value is returned. +func getenv(self py.Object, args py.Tuple) (py.Object, error) { + if len(args) < 1 { + return nil, py.ExceptionNewf(py.TypeError, "missing one required argument: 'name:str'") + } + k, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected (pos 1), not "+args[0].Type().Name) + } + v, ok := os.LookupEnv(string(k)) + if ok { + return py.String(v), nil + } + if len(args) == 2 { + return args[1], nil + } + return py.None, nil +} + +// getpid returns the current process id. +func getpid(self py.Object, args py.Tuple) (py.Object, error) { + return py.Int(os.Getpid()), nil +} + +// putenv sets the value of an environment variable named by the key. +func putenv(self py.Object, args py.Tuple) (py.Object, error) { + if len(args) != 2 { + return nil, py.ExceptionNewf(py.TypeError, "missing required arguments: 'key:str' and 'value:str'") + } + k, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected (pos 1), not "+args[0].Type().Name) + } + v, ok := args[1].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected (pos 2), not "+args[1].Type().Name) + } + err := os.Setenv(string(k), string(v)) + if err != nil { + return nil, py.ExceptionNewf(py.OSError, "Unable to set enviroment variable") + } + return py.None, nil +} + +// Unset (delete) the environment variable named key. +func unsetenv(self py.Object, args py.Tuple) (py.Object, error) { + if len(args) != 1 { + return nil, py.ExceptionNewf(py.TypeError, "missing one required argument: 'key:str'") + } + k, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected (pos 1), not "+args[0].Type().Name) + } + err := os.Unsetenv(string(k)) + if err != nil { + return nil, py.ExceptionNewf(py.OSError, "Unable to unset enviroment variable") + } + return py.None, nil +} + +// os._exit() immediate program termination; unlike sys.exit(), which raises a SystemExit, this function will termninate the program immediately. +func _exit(self py.Object, args py.Tuple) (py.Object, error) { // can never return + if len(args) == 0 { + os.Exit(0) + } + arg, ok := args[0].(py.Int) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "expected int (pos 1), not "+args[0].Type().Name) + } + os.Exit(int(arg)) + return nil, nil +} + +// os.system(command string) this function runs a shell command and directs the output to standard output. +func system(self py.Object, args py.Tuple) (py.Object, error) { + if len(args) != 1 { + return nil, py.ExceptionNewf(py.TypeError, "missing one required argument: 'command:str'") + } + arg, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "str expected (pos 1), not "+args[0].Type().Name) + } + + var command *exec.Cmd + if runtime.GOOS != "windows" { + command = exec.Command("/bin/sh", "-c", string(arg)) + } else { + command = exec.Command("cmd.exe", string(arg)) + } + outb, err := command.CombinedOutput() // - commbinedoutput to get both stderr and stdout - + if err != nil { + return nil, py.ExceptionNewf(py.OSError, err.Error()) + } + ok = py.Println(self, string(outb)) + if !ok { + return py.Int(1), nil + } + + return py.Int(0), nil +} diff --git a/stdlib/os/os_test.go b/stdlib/os/os_test.go new file mode 100644 index 00000000..8ae63d09 --- /dev/null +++ b/stdlib/os/os_test.go @@ -0,0 +1,15 @@ +// Copyright 2022 The go-python Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os_test + +import ( + "testing" + + "github.com/go-python/gpython/pytest" +) + +func TestOs(t *testing.T) { + pytest.RunScript(t, "./testdata/test.py") +} diff --git a/stdlib/os/testdata/test.py b/stdlib/os/testdata/test.py new file mode 100644 index 00000000..100f48df --- /dev/null +++ b/stdlib/os/testdata/test.py @@ -0,0 +1,121 @@ +# Copyright 2022 The go-python Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +import os + +print("test os") +print("os.error: ", os.error) +print("os.getenv($GPYTHON_TEST_HOME)=", os.getenv("GPYTHON_TEST_HOME")) +os.putenv("GPYTHON_TEST_HOME", "/home/go") +print("os.environ($GPYTHON_TEST_HOME)=", os.environ.get("GPYTHON_TEST_HOME")) +print("os.getenv($GPYTHON_TEST_HOME)=", os.getenv("GPYTHON_TEST_HOME")) +os.unsetenv("GPYTHON_TEST_HOME") +print("os.unsetenv($GPYTHON_TEST_HOME)=", os.getenv("GPYTHON_TEST_HOME")) + +if not os.error is OSError: + print("os.error is not OSError!") +else: + print("os.error is OSError [OK]") + +## FIXME(sbinet): check returned value with a known one +## (ie: when os.mkdir is implemented) +if os.getcwd() == None: + print("os.getcwd() == None !") +else: + print("os.getcwd() != None [OK]") + +## FIXME(sbinet): check returned value with a known one +## (ie: when os.mkdir is implemented) +if os.getcwdb() == None: + print("os.getcwdb() == None !") +else: + print("os.getcwdb() != None [OK]") + +print("os.system('echo hello')...") +if os.name != "nt": + os.system('echo hello') +else: ## FIXME(sbinet): find a way to test this nicely + print("hello\n") + +if os.getpid() > 1: + print("os.getpid is greater than 1 [OK]") +else: + print("invalid os.getpid: ", os.getpid()) + +orig = os.getcwd() +testdir = "/" +if os.name == "nt": + testdir = "C:\\" +os.chdir(testdir) +if os.getcwd() != testdir: + print("invalid getcwd() after os.chdir:",os.getcwd()) +else: + print("os.chdir(testdir) [OK]") +os.chdir(orig) + +try: + os.chdir(1) + print("expected an error with os.chdir(1)") +except TypeError: + print("os.chdir(1) failed [OK]") + +try: + os.environ.get(15) + print("expected an error with os.environ.get(15)") +except KeyError: + print("os.environ.get(15) failed [OK]") + +try: + os.putenv() + print("expected an error with os.putenv()") +except TypeError: + print("os.putenv() failed [OK]") + +try: + os.unsetenv() + print("expected an error with os.unsetenv()") +except TypeError: + print("os.unsetenv() failed [OK]") + +try: + os.getenv() + print("expected an error with os.getenv()") +except TypeError: + print("os.getenv() failed [OK]") + +try: + os.unsetenv("FOO", "BAR") + print("expected an error with os.unsetenv(\"FOO\", \"BAR\")") +except TypeError: + print("os.unsetenv(\"FOO\", \"BAR\") failed [OK]") + +if bytes(os.getcwd(), "utf-8") == os.getcwdb(): + print('bytes(os.getcwd(), "utf-8") == os.getcwdb() [OK]') +else: + print('expected: bytes(os.getcwd(), "utf-8") == os.getcwdb()') + +golden = { + "posix": { + "sep": "/", + "pathsep": ":", + "linesep": "\n", + "devnull": "/dev/null", + "altsep": None + }, + "nt": { + "sep": "\\", + "pathsep": ";", + "linesep": "\r\n", + "devnull": "nul", + "altsep": "/" + }, +}[os.name] + +for k in ("sep", "pathsep", "linesep", "devnull", "altsep"): + if getattr(os, k) != golden[k]: + print("invalid os."+k+": got=",getattr(os,k),", want=", golden[k]) + else: + print("os."+k+": [OK]") + +print("OK") diff --git a/stdlib/os/testdata/test_golden.txt b/stdlib/os/testdata/test_golden.txt new file mode 100644 index 00000000..8aae577d --- /dev/null +++ b/stdlib/os/testdata/test_golden.txt @@ -0,0 +1,27 @@ +test os +os.error: +os.getenv($GPYTHON_TEST_HOME)= None +os.environ($GPYTHON_TEST_HOME)= None +os.getenv($GPYTHON_TEST_HOME)= /home/go +os.unsetenv($GPYTHON_TEST_HOME)= None +os.error is OSError [OK] +os.getcwd() != None [OK] +os.getcwdb() != None [OK] +os.system('echo hello')... +hello + +os.getpid is greater than 1 [OK] +os.chdir(testdir) [OK] +os.chdir(1) failed [OK] +os.environ.get(15) failed [OK] +os.putenv() failed [OK] +os.unsetenv() failed [OK] +os.getenv() failed [OK] +os.unsetenv("FOO", "BAR") failed [OK] +bytes(os.getcwd(), "utf-8") == os.getcwdb() [OK] +os.sep: [OK] +os.pathsep: [OK] +os.linesep: [OK] +os.devnull: [OK] +os.altsep: [OK] +OK diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 8182d462..d69268d3 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -21,6 +21,7 @@ import ( _ "github.com/go-python/gpython/stdlib/binascii" _ "github.com/go-python/gpython/stdlib/builtin" _ "github.com/go-python/gpython/stdlib/math" + _ "github.com/go-python/gpython/stdlib/os" _ "github.com/go-python/gpython/stdlib/string" _ "github.com/go-python/gpython/stdlib/sys" _ "github.com/go-python/gpython/stdlib/time"