From 4213ccfe1fc0da0c707b920d3eb7442a52660066 Mon Sep 17 00:00:00 2001 From: Raffaele Sena Date: Tue, 28 Aug 2018 18:23:42 -0700 Subject: [PATCH] Initial work at implementing file methods: - open (builtin) - File.read - File.write - File.close --- builtin/builtin.go | 63 +++++++++++ builtin/tests/builtin.py | 3 + py/args.go | 11 ++ py/file.go | 232 ++++++++++++++++++++++++++++++++++++++- py/tests/file.py | 43 ++++++++ pytest/pytest.go | 2 + sys/sys.go | 4 +- 7 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 py/tests/file.py 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,