diff --git a/builtin/builtin.go b/builtin/builtin.go index 52105670..6e1b41a9 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -51,6 +51,7 @@ func init() { // py.MustNewMethod("max", builtin_max, 0, max_doc), // py.MustNewMethod("min", builtin_min, 0, min_doc), py.MustNewMethod("next", builtin_next, 0, next_doc), + py.MustNewMethod("open", builtin_open, 0, open_doc), // py.MustNewMethod("oct", builtin_oct, 0, oct_doc), py.MustNewMethod("ord", builtin_ord, 0, ord_doc), py.MustNewMethod("pow", builtin_pow, 0, pow_doc), @@ -437,6 +438,68 @@ fromlist is not empty. Level is used to determine whether to perform absolute or relative imports. 0 is absolute while a positive number is the number of parent directories to search relative to the current module.` +const open_doc = `open(name[, mode[, buffering]]) -> file object + +Open a file using the file() type, returns a file object. This is the +preferred way to open a file. See file.__doc__ for further information.` + +func builtin_open(self py.Object, args py.Tuple, kwargs py.StringDict) (py.Object, error) { + kwlist := []string{ + "file", + "mode", + "buffering", + "encoding", + "errors", + "newline", + "closefd", + "opener", + } + + var ( + filename py.Object + mode py.Object = py.String("r") + buffering py.Object = py.Int(-1) + encoding py.Object = py.None + errors py.Object = py.None + newline py.Object = py.None + closefd py.Object = py.Bool(true) + opener py.Object = py.None + ) + + err := py.ParseTupleAndKeywords(args, kwargs, "s|sizzzpO:open", kwlist, + &filename, + &mode, + &buffering, + &encoding, + &errors, + &newline, + &closefd, + &opener) + if err != nil { + return nil, err + } + + if encoding != py.None && encoding.(py.String) != py.String("utf-8") { + return nil, py.ExceptionNewf(py.NotImplementedError, "encoding not implemented yet") + } + + if errors != py.None { + return nil, py.ExceptionNewf(py.NotImplementedError, "errors not implemented yet") + } + + if newline != py.None { + return nil, py.ExceptionNewf(py.NotImplementedError, "newline not implemented yet") + } + + if opener != py.None { + return nil, py.ExceptionNewf(py.NotImplementedError, "opener not implemented yet") + } + + return py.OpenFile(string(filename.(py.String)), + string(mode.(py.String)), + int(buffering.(py.Int))) +} + const ord_doc = `ord(c) -> integer Return the integer ordinal of a one-character string.` diff --git a/builtin/tests/builtin.py b/builtin/tests/builtin.py index 4b4cb09e..c7c8cd18 100644 --- a/builtin/tests/builtin.py +++ b/builtin/tests/builtin.py @@ -135,6 +135,9 @@ def gen2(): ok = True assert ok, "TypeError not raised" +doc="open" +assert open(__file__) is not None + doc="pow" assert pow(2, 10) == 1024 assert pow(2, 10, 17) == 4 diff --git a/py/args.go b/py/args.go index d32c319e..dcdfc392 100644 --- a/py/args.go +++ b/py/args.go @@ -452,6 +452,12 @@ func ParseTupleAndKeywords(args Tuple, kwargs StringDict, format string, kwlist switch op { case "O": *result = arg + case "Z", "z": + if _, ok := arg.(NoneType); ok { + *result = arg + break + } + fallthrough case "U", "s": if _, ok := arg.(String); !ok { return ExceptionNewf(TypeError, "%s() argument %d must be str, not %s", name, i+1, arg.Type().Name) @@ -462,6 +468,11 @@ func ParseTupleAndKeywords(args Tuple, kwargs StringDict, format string, kwlist return ExceptionNewf(TypeError, "%s() argument %d must be int, not %s", name, i+1, arg.Type().Name) } *result = arg + case "p": + if _, ok := arg.(Bool); !ok { + return ExceptionNewf(TypeError, "%s() argument %d must be bool, not %s", name, i+1, arg.Type().Name) + } + *result = arg case "d": switch x := arg.(type) { case Int: diff --git a/py/file.go b/py/file.go index 18ac1e6a..31ac6766 100644 --- a/py/file.go +++ b/py/file.go @@ -10,17 +10,243 @@ package py import ( + "io" + "io/ioutil" "os" ) -var FileType = NewTypeX("file", `represents an open file`, - nil, nil) +var FileType = NewType("file", `represents an open file`) -type File os.File +func init() { + FileType.Dict["write"] = MustNewMethod("write", func(self Object, value Object) (Object, error) { + return self.(*File).Write(value) + }, 0, "write(arg) -> writes the contents of arg to the file, returning the number of characters written.") + + FileType.Dict["read"] = MustNewMethod("read", func(self Object, args Tuple, kwargs StringDict) (Object, error) { + return self.(*File).Read(args, kwargs) + }, 0, "read([size]) -> read at most size bytes, returned as a string.\n\nIf the size argument is negative or omitted, read until EOF is reached.\nNotice that when in non-blocking mode, less data than what was requested\nmay be returned, even if no size parameter was given.") + FileType.Dict["close"] = MustNewMethod("close", func(self Object) (Object, error) { + return self.(*File).Close() + }, 0, "close() -> None or (perhaps) an integer. Close the file.\n\nSets data attribute .closed to True. A closed file cannot be used for\nfurther I/O operations. close() may be called more than once without\nerror. Some kinds of file objects (for example, opened by popen())\nmay return an exit status upon closing.") +} + +type FileMode int + +const ( + FileRead FileMode = 0x01 + FileWrite FileMode = 0x02 + FileText FileMode = 0x4000 + FileBinary FileMode = 0x8000 + + FileReadWrite = FileRead + FileWrite +) + +type File struct { + *os.File + FileMode +} // Type of this object func (o *File) Type() *Type { return FileType } +func (o *File) Can(mode FileMode) bool { + return o.FileMode&mode == mode +} + +func (o *File) Write(value Object) (Object, error) { + var b []byte + + switch v := value.(type) { + // FIXME Bytearray + case Bytes: + b = v + + case String: + b = []byte(v) + + default: + return nil, ExceptionNewf(TypeError, "expected a string or other character buffer object") + } + + n, err := o.File.Write(b) + return Int(n), err +} + +func (o *File) readResult(b []byte) (Object, error) { + if o.Can(FileBinary) { + if b != nil { + return Bytes(b), nil + } + + return Bytes{}, nil + } + + if b != nil { + return String(b), nil + } + + return String(""), nil +} + +func (o *File) Read(args Tuple, kwargs StringDict) (Object, error) { + var arg Object = None + + err := UnpackTuple(args, kwargs, "read", 0, 1, &arg) + if err != nil { + return nil, err + } + + var r io.Reader = o.File + + switch pyN, ok := arg.(Int); { + case arg == None: + // read all + + case ok: + // number of bytes to read + // 0: read nothing + // < 0: read all + // > 0: read n + n, _ := pyN.GoInt64() + if n == 0 { + return o.readResult(nil) + } + if n > 0 { + r = io.LimitReader(r, n) + } + + default: + // invalid type + return nil, ExceptionNewf(TypeError, "read() argument 1 must be int, not %s", arg.Type().Name) + } + + b, err := ioutil.ReadAll(r) + if err == io.EOF { + return o.readResult(nil) + } + if err != nil { + return nil, err + } + + return o.readResult(b) +} + +func (o *File) Close() (Object, error) { + _ = o.File.Close() + return None, nil +} + +func OpenFile(filename, mode string, buffering int) (Object, error) { + var fileMode FileMode + var truncate bool + var exclusive bool + + for _, m := range mode { + switch m { + case 'r': + if fileMode&FileReadWrite != 0 { + return nil, ExceptionNewf(ValueError, "must have exactly one of create/read/write/append mode") + } + fileMode |= FileRead + + case 'w': + if fileMode&FileReadWrite != 0 { + return nil, ExceptionNewf(ValueError, "must have exactly one of create/read/write/append mode") + } + fileMode |= FileWrite + truncate = true + + case 'x': + if fileMode&FileReadWrite != 0 { + return nil, ExceptionNewf(ValueError, "must have exactly one of create/read/write/append mode") + } + fileMode |= FileWrite + exclusive = true + + case 'a': + if fileMode&FileReadWrite != 0 { + return nil, ExceptionNewf(ValueError, "must have exactly one of create/read/write/append mode") + } + fileMode |= FileWrite + truncate = false + + case '+': + if fileMode&FileReadWrite == 0 { + return nil, ExceptionNewf(ValueError, "Must have exactly one of create/read/write/append mode and at most one plus") + } + + fileMode |= FileReadWrite + truncate = false + + case 'b': + if fileMode&FileReadWrite == 0 { + return nil, ExceptionNewf(ValueError, "Must have exactly one of create/read/write/append mode and at most one plus") + } + + if fileMode&FileText != 0 { + return nil, ExceptionNewf(ValueError, "can't have text and binary mode at once") + } + + fileMode |= FileBinary + + case 't': + if fileMode&FileReadWrite == 0 { + return nil, ExceptionNewf(ValueError, "Must have exactly one of create/read/write/append mode and at most one plus") + } + + if fileMode&FileBinary != 0 { + return nil, ExceptionNewf(ValueError, "can't have text and binary mode at once") + } + + fileMode |= FileText + } + } + + var fmode int + + switch fileMode & FileReadWrite { + case FileReadWrite: + fmode = os.O_RDWR + + case FileRead: + fmode = os.O_RDONLY + + case FileWrite: + fmode = os.O_WRONLY + } + + if exclusive { + fmode |= os.O_EXCL + } + + if truncate { + fmode |= os.O_CREATE | os.O_TRUNC + } else { + fmode |= os.O_APPEND + } + + f, err := os.OpenFile(filename, fmode, 0666) + if err != nil { + // XXX: should check for different types of errors + switch { + case os.IsExist(err): + return nil, ExceptionNewf(FileExistsError, err.Error()) + + case os.IsNotExist(err): + return nil, ExceptionNewf(FileNotFoundError, err.Error()) + } + } + + if finfo, err := f.Stat(); err == nil { + if finfo.IsDir() { + f.Close() + return nil, ExceptionNewf(IsADirectoryError, "Is a directory: '%s'", filename) + } + } + + return &File{f, fileMode}, nil +} + // Check interface is satisfied diff --git a/py/tests/file.py b/py/tests/file.py new file mode 100644 index 00000000..c2a12c85 --- /dev/null +++ b/py/tests/file.py @@ -0,0 +1,43 @@ +# Copyright 2018 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. + +from libtest import assertRaises + +doc = "open" +assertRaises(FileNotFoundError, open, "not-existent.file") + +assertRaises(IsADirectoryError, open, ".") + +f = open(__file__) +assert f is not None + +doc = "read" +b = f.read(12) +assert b == '# Copyright ' + +b = f.read(4) +assert b == '2018' + +b = f.read() +assert b != '' + +b = f.read() +assert b == '' + +doc = "write" +assertRaises(TypeError, f.write, 42) + +# assertRaises(io.UnsupportedOperation, f.write, 'hello') + +import sys +n = sys.stdout.write('hello') +assert n == 5 + +doc = "close" +f.close() + +# closing a closed file should not throw an error +f.close() + +doc = "finished" diff --git a/pytest/pytest.go b/pytest/pytest.go index 36128b70..44c8979d 100644 --- a/pytest/pytest.go +++ b/pytest/pytest.go @@ -12,8 +12,10 @@ import ( "testing" _ "github.com/go-python/gpython/builtin" + _ "github.com/go-python/gpython/sys" "github.com/go-python/gpython/compile" "github.com/go-python/gpython/py" + _ "github.com/go-python/gpython/sys" "github.com/go-python/gpython/vm" ) diff --git a/sys/sys.go b/sys/sys.go index 2cd4dc5e..fcd1f0df 100644 --- a/sys/sys.go +++ b/sys/sys.go @@ -653,7 +653,9 @@ func init() { py.MustNewMethod("_debugmallocstats", sys_debugmallocstats, 0, debugmallocstats_doc), } argv := MakeArgv(os.Args[1:]) - stdin, stdout, stderr := (*py.File)(os.Stdin), (*py.File)(os.Stdout), (*py.File)(os.Stderr) + stdin, stdout, stderr := &py.File{os.Stdin, py.FileRead}, + &py.File{os.Stdout, py.FileWrite}, + &py.File{os.Stderr, py.FileWrite} globals := py.StringDict{ "argv": argv, "stdin": stdin,
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: