From d5be40c49f6e16b4d3b40cbee8ec8d338fea2398 Mon Sep 17 00:00:00 2001 From: wetor Date: Wed, 7 Jun 2023 19:24:55 +0800 Subject: [PATCH 01/24] py: fix iterable object --- py/arithmetic.go | 18 ------------- py/dict.go | 8 +++--- py/exception.go | 2 ++ py/gen.go | 1 - py/internal.go | 17 +++++++++++++ py/iterator.go | 36 ++++++++++++++++++-------- py/list.go | 8 ++++-- py/object.go | 53 ++++++++++++++++++++++++++++++--------- py/tests/iter.py | 12 +++++++++ stdlib/builtin/builtin.go | 12 +++++++-- 10 files changed, 118 insertions(+), 49 deletions(-) diff --git a/py/arithmetic.go b/py/arithmetic.go index 199fceee..768764b8 100644 --- a/py/arithmetic.go +++ b/py/arithmetic.go @@ -147,24 +147,6 @@ func MakeFloat(a Object) (Object, error) { return nil, ExceptionNewf(TypeError, "unsupported operand type(s) for float: '%s'", a.Type().Name) } -// Iter the python Object returning an Object -// -// Will raise TypeError if Iter can't be run on this object -func Iter(a Object) (Object, error) { - - if A, ok := a.(I__iter__); ok { - res, err := A.M__iter__() - if err != nil { - return nil, err - } - if res != NotImplemented { - return res, nil - } - } - - return nil, ExceptionNewf(TypeError, "unsupported operand type(s) for iter: '%s'", a.Type().Name) -} - // Add two python objects together returning an Object // // Will raise TypeError if can't be add can't be run on these objects diff --git a/py/dict.go b/py/dict.go index 9aac7631..497da3a7 100644 --- a/py/dict.go +++ b/py/dict.go @@ -36,7 +36,7 @@ func init() { return nil, err } sMap := self.(StringDict) - o := make([]Object, 0, len(sMap)) + o := make(Tuple, 0, len(sMap)) for k, v := range sMap { o = append(o, Tuple{String(k), v}) } @@ -49,7 +49,7 @@ func init() { return nil, err } sMap := self.(StringDict) - o := make([]Object, 0, len(sMap)) + o := make(Tuple, 0, len(sMap)) for k := range sMap { o = append(o, String(k)) } @@ -62,7 +62,7 @@ func init() { return nil, err } sMap := self.(StringDict) - o := make([]Object, 0, len(sMap)) + o := make(Tuple, 0, len(sMap)) for _, v := range sMap { o = append(o, v) } @@ -204,7 +204,7 @@ func (a StringDict) M__repr__() (Object, error) { // Returns a list of keys from the dict func (d StringDict) M__iter__() (Object, error) { - o := make([]Object, 0, len(d)) + o := make(Tuple, 0, len(d)) for k := range d { o = append(o, String(k)) } diff --git a/py/exception.go b/py/exception.go index 2e8f91a2..73f92747 100644 --- a/py/exception.go +++ b/py/exception.go @@ -336,6 +336,8 @@ func ExceptionGivenMatches(err, exc Object) bool { func IsException(exception *Type, r interface{}) bool { var t *Type switch ex := r.(type) { + case ExceptionInfo: + t = ex.Type case *Exception: t = ex.Type() case *Type: diff --git a/py/gen.go b/py/gen.go index 16db6ad8..671b0831 100644 --- a/py/gen.go +++ b/py/gen.go @@ -45,7 +45,6 @@ var data = Data{ {Name: "complex", Title: "MakeComplex", Operator: "complex", Unary: true, Conversion: "Complex"}, {Name: "int", Title: "MakeInt", Operator: "int", Unary: true, Conversion: "Int"}, {Name: "float", Title: "MakeFloat", Operator: "float", Unary: true, Conversion: "Float"}, - {Name: "iter", Title: "Iter", Operator: "iter", Unary: true}, }, BinaryOps: Ops{ {Name: "add", Title: "Add", Operator: "+", Binary: true}, diff --git a/py/internal.go b/py/internal.go index df0e285c..e649b299 100644 --- a/py/internal.go +++ b/py/internal.go @@ -430,3 +430,20 @@ func ReprAsString(self Object) (string, error) { } return string(str), nil } + +// Returns an iterator object +// +// Call __Iter__ Returns an iterator object +// +// If object is sequence object, create an iterator +func Iter(self Object) (res Object, err error) { + if I, ok := self.(I__iter__); ok { + return I.M__iter__() + } else if res, ok, err = TypeCall0(self, "__iter__"); ok { + return res, err + } + if ObjectIsSequence(self) { + return NewIterator(self), nil + } + return nil, ExceptionNewf(TypeError, "'%s' object is not iterable", self.Type().Name) +} diff --git a/py/iterator.go b/py/iterator.go index a2368da8..350700a9 100644 --- a/py/iterator.go +++ b/py/iterator.go @@ -8,8 +8,8 @@ package py // A python Iterator object type Iterator struct { - Pos int - Objs []Object + Pos int + Seq Object } var IteratorType = NewType("iterator", "iterator type") @@ -20,10 +20,10 @@ func (o *Iterator) Type() *Type { } // Define a new iterator -func NewIterator(Objs []Object) *Iterator { +func NewIterator(Seq Object) *Iterator { m := &Iterator{ - Pos: 0, - Objs: Objs, + Pos: 0, + Seq: Seq, } return m } @@ -33,13 +33,29 @@ func (it *Iterator) M__iter__() (Object, error) { } // Get next one from the iteration -func (it *Iterator) M__next__() (Object, error) { - if it.Pos >= len(it.Objs) { - return nil, StopIteration +func (it *Iterator) M__next__() (res Object, err error) { + if tuple, ok := it.Seq.(Tuple); ok { + if it.Pos >= len(tuple) { + return nil, StopIteration + } + res = tuple[it.Pos] + it.Pos++ + return res, nil + } + index := Int(it.Pos) + if I, ok := it.Seq.(I__getitem__); ok { + res, err = I.M__getitem__(index) + } else if res, ok, err = TypeCall1(it.Seq, "__getitem__", index); !ok { + return nil, ExceptionNewf(TypeError, "'%s' object is not iterable", it.Type().Name) + } + if err != nil { + if IsException(IndexError, err) { + return nil, StopIteration + } + return nil, err } - r := it.Objs[it.Pos] it.Pos++ - return r, nil + return res, nil } // Check interface is satisfied diff --git a/py/list.go b/py/list.go index 28a118a1..9f6f62b0 100644 --- a/py/list.go +++ b/py/list.go @@ -186,7 +186,7 @@ func (l *List) M__bool__() (Object, error) { } func (l *List) M__iter__() (Object, error) { - return NewIterator(l.Items), nil + return NewIterator(Tuple(l.Items)), nil } func (l *List) M__getitem__(key Object) (Object, error) { @@ -496,7 +496,11 @@ func SortInPlace(l *List, kwargs StringDict, funcName string) error { reverse = False } // FIXME: requires the same bool-check like CPython (or better "|$Op" that doesn't panic on nil). - s := ptrSortable{&sortable{l, keyFunc, ObjectIsTrue(reverse), nil}} + ok, err := ObjectIsTrue(reverse) + if err != nil { + return err + } + s := ptrSortable{&sortable{l, keyFunc, ok, nil}} sort.Stable(s) return s.s.firstErr } diff --git a/py/object.go b/py/object.go index 55141cec..540ea0b6 100644 --- a/py/object.go +++ b/py/object.go @@ -23,24 +23,53 @@ func ObjectRepr(o Object) Object { } // Return whether the object is True or not -func ObjectIsTrue(o Object) bool { - if o == True { - return true +func ObjectIsTrue(o Object) (cmp bool, err error) { + switch o { + case True: + return true, nil + case False: + return false, nil + case None: + return false, nil } - if o == False { - return false + + var res Object + switch t := o.(type) { + case I__bool__: + res, err = t.M__bool__() + case I__len__: + res, err = t.M__len__() + case *Type: + var ok bool + if res, ok, err = TypeCall0(o, "__bool__"); ok { + break + } + if res, ok, err = TypeCall0(o, "__len__"); ok { + break + } + _ = ok // pass static-check + } + if err != nil { + return false, err } - if o == None { - return false + switch t := res.(type) { + case Bool: + return t == True, nil + case Int: + return t > 0, nil } + return true, nil +} - if I, ok := o.(I__bool__); ok { - cmp, err := I.M__bool__() - if err == nil && cmp == True { +// Return whether the object is a sequence +func ObjectIsSequence(o Object) bool { + switch t := o.(type) { + case I__getitem__: + return true + case *Type: + if t.GetAttrOrNil("__getitem__") != nil { return true - } else if err == nil && cmp == False { - return false } } return false diff --git a/py/tests/iter.py b/py/tests/iter.py index 53422b79..4eda19c9 100644 --- a/py/tests/iter.py +++ b/py/tests/iter.py @@ -18,4 +18,16 @@ def f(): words2 = list(iter(words1)) for w1, w2 in zip(words1, words2): assert w1 == w2 + +class SequenceClass: + def __init__(self, n): + self.n = n + def __getitem__(self, i): + if 0 <= i < self.n: + return i + else: + raise IndexError + +assert list(iter(SequenceClass(5))) == [0, 1, 2, 3, 4] + doc="finished" \ No newline at end of file diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 83502849..134d8511 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -292,7 +292,11 @@ func builtin_all(self, seq py.Object) (py.Object, error) { } return nil, err } - if !py.ObjectIsTrue(item) { + ok, err := py.ObjectIsTrue(item) + if err != nil { + return nil, err + } + if !ok { return py.False, nil } } @@ -317,7 +321,11 @@ func builtin_any(self, seq py.Object) (py.Object, error) { } return nil, err } - if py.ObjectIsTrue(item) { + ok, err := py.ObjectIsTrue(item) + if err != nil { + return nil, err + } + if ok { return py.True, nil } } From 17dddcd8f647cc11142842523c6846540743dab1 Mon Sep 17 00:00:00 2001 From: wetor Date: Wed, 7 Jun 2023 19:28:05 +0800 Subject: [PATCH 02/24] py: implement `filter` --- py/filter.go | 72 +++++++++++++++++++++++++++++++++++++++ py/tests/filter.py | 65 +++++++++++++++++++++++++++++++++++ stdlib/builtin/builtin.go | 6 ++-- 3 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 py/filter.go create mode 100644 py/tests/filter.py diff --git a/py/filter.go b/py/filter.go new file mode 100644 index 00000000..447c4829 --- /dev/null +++ b/py/filter.go @@ -0,0 +1,72 @@ +// Copyright 2023 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 py + +// A python Filter object +type Filter struct { + it Object + fun Object +} + +var FilterType = NewTypeX("filter", `filter(function or None, iterable) --> filter object + +Return an iterator yielding those items of iterable for which function(item) +is true. If function is None, return the items that are true.`, + FilterTypeNew, nil) + +// Type of this object +func (f *Filter) Type() *Type { + return FilterType +} + +// FilterTypeNew +func FilterTypeNew(metatype *Type, args Tuple, kwargs StringDict) (res Object, err error) { + var fun, seq Object + var it Object + err = UnpackTuple(args, kwargs, "filter", 2, 2, &fun, &seq) + if err != nil { + return nil, err + } + it, err = Iter(seq) + if err != nil { + return nil, err + } + return &Filter{it: it, fun: fun}, nil +} + +func (f *Filter) M__iter__() (Object, error) { + return f, nil +} + +func (f *Filter) M__next__() (Object, error) { + var ok bool + for { + item, err := Next(f.it) + if err != nil { + return nil, err + } + // if (lz->func == Py_None || lz->func == (PyObject *)&PyBool_Type) + if _, _ok := f.fun.(Bool); _ok || f.fun == None { + ok, err = ObjectIsTrue(item) + } else { + var good Object + good, err = Call(f.fun, Tuple{item}, nil) + if err != nil { + return nil, err + } + ok, err = ObjectIsTrue(good) + } + if ok { + return item, nil + } + if err != nil { + return nil, err + } + } +} + +// Check interface is satisfied +var _ I__iter__ = (*Filter)(nil) +var _ I__next__ = (*Filter)(nil) diff --git a/py/tests/filter.py b/py/tests/filter.py new file mode 100644 index 00000000..c42b2b63 --- /dev/null +++ b/py/tests/filter.py @@ -0,0 +1,65 @@ +# test_builtin.py:BuiltinTest.test_filter() +from libtest import assertRaises + +doc="filter" +class T0: + def __bool__(self): + return True +class T1: + def __len__(self): + return 1 +class T2: + def __bool__(self): + return False +class T3: + pass +t0, t1, t2, t3 = T0(), T1(), T2(), T3() +assert list(filter(None, [t0, t1, t2, t3])) == [t0, t1, t3] +assert list(filter(None, [1, [], 2, ''])) == [1, 2] + +class T3: + def __len__(self): + raise ValueError +t3 = T3() +assertRaises(ValueError, list, filter(None, [t3])) + +class Squares: + def __init__(self, max): + self.max = max + self.sofar = [] + + def __len__(self): return len(self.sofar) + + def __getitem__(self, i): + if not 0 <= i < self.max: raise IndexError + n = len(self.sofar) + while n <= i: + self.sofar.append(n*n) + n += 1 + return self.sofar[i] + +assert list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')) == list('elloorld') +assert list(filter(None, [1, 'hello', [], [3], '', None, 9, 0])) == [1, 'hello', [3], 9] +assert list(filter(lambda x: x > 0, [1, -3, 9, 0, 2])) == [1, 9, 2] +assert list(filter(None, Squares(10))) == [1, 4, 9, 16, 25, 36, 49, 64, 81] +assert list(filter(lambda x: x%2, Squares(10))) == [1, 9, 25, 49, 81] +def identity(item): + return 1 +filter(identity, Squares(5)) +assertRaises(TypeError, filter) +class BadSeq(object): + def __getitem__(self, index): + if index<4: + return 42 + raise ValueError +assertRaises(ValueError, list, filter(lambda x: x, BadSeq())) +def badfunc(): + pass +assertRaises(TypeError, list, filter(badfunc, range(5))) + +# test bltinmodule.c::filtertuple() +assert list(filter(None, (1, 2))) == [1, 2] +assert list(filter(lambda x: x>=3, (1, 2, 3, 4))) == [3, 4] +assertRaises(TypeError, list, filter(42, (1, 2))) + +doc="finished" diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 134d8511..88d6cffe 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -77,9 +77,9 @@ func init() { "complex": py.ComplexType, "dict": py.StringDictType, // FIXME "enumerate": py.EnumerateType, - // "filter": py.FilterType, - "float": py.FloatType, - "frozenset": py.FrozenSetType, + "filter": py.FilterType, + "float": py.FloatType, + "frozenset": py.FrozenSetType, // "property": py.PropertyType, "int": py.IntType, // FIXME LongType? "list": py.ListType, From 32f9086bff2e8b2fb368759162b9012848fa54cd Mon Sep 17 00:00:00 2001 From: wetor Date: Wed, 7 Jun 2023 19:28:25 +0800 Subject: [PATCH 03/24] py: implement `map` --- py/map.go | 60 +++++++++++++++++++++++++++++++++++++++ py/tests/map.py | 54 +++++++++++++++++++++++++++++++++++ stdlib/builtin/builtin.go | 6 ++-- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 py/map.go create mode 100644 py/tests/map.py diff --git a/py/map.go b/py/map.go new file mode 100644 index 00000000..1c343538 --- /dev/null +++ b/py/map.go @@ -0,0 +1,60 @@ +// Copyright 2023 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 py + +// A python Map object +type Map struct { + iters Tuple + fun Object +} + +var MapType = NewTypeX("filter", `map(func, *iterables) --> map object + +Make an iterator that computes the function using arguments from +each of the iterables. Stops when the shortest iterable is exhausted.`, + MapTypeNew, nil) + +// Type of this object +func (m *Map) Type() *Type { + return FilterType +} + +// MapType +func MapTypeNew(metatype *Type, args Tuple, kwargs StringDict) (res Object, err error) { + numargs := len(args) + if numargs < 2 { + return nil, ExceptionNewf(TypeError, "map() must have at least two arguments.") + } + iters := make(Tuple, numargs-1) + for i := 1; i < numargs; i++ { + iters[i-1], err = Iter(args[i]) + if err != nil { + return nil, err + } + } + return &Map{iters: iters, fun: args[0]}, nil +} + +func (m *Map) M__iter__() (Object, error) { + return m, nil +} + +func (m *Map) M__next__() (Object, error) { + numargs := len(m.iters) + argtuple := make(Tuple, numargs) + + for i := 0; i < numargs; i++ { + val, err := Next(m.iters[i]) + if err != nil { + return nil, err + } + argtuple[i] = val + } + return Call(m.fun, argtuple, nil) +} + +// Check interface is satisfied +var _ I__iter__ = (*Map)(nil) +var _ I__next__ = (*Map)(nil) diff --git a/py/tests/map.py b/py/tests/map.py new file mode 100644 index 00000000..3e5a4e1e --- /dev/null +++ b/py/tests/map.py @@ -0,0 +1,54 @@ +# test_builtin.py:BuiltinTest.test_map() +from libtest import assertRaises + +doc="map" +class Squares: + def __init__(self, max): + self.max = max + self.sofar = [] + + def __len__(self): return len(self.sofar) + + def __getitem__(self, i): + if not 0 <= i < self.max: raise IndexError + n = len(self.sofar) + while n <= i: + self.sofar.append(n*n) + n += 1 + return self.sofar[i] + +assert list(map(lambda x: x*x, range(1,4))) == [1, 4, 9] +try: + from math import sqrt +except ImportError: + def sqrt(x): + return pow(x, 0.5) +assert list(map(lambda x: list(map(sqrt, x)), [[16, 4], [81, 9]])) == [[4.0, 2.0], [9.0, 3.0]] +assert list(map(lambda x, y: x+y, [1,3,2], [9,1,4])) == [10, 4, 6] + +def plus(*v): + accu = 0 + for i in v: accu = accu + i + return accu +assert list(map(plus, [1, 3, 7])) == [1, 3, 7] +assert list(map(plus, [1, 3, 7], [4, 9, 2])) == [1+4, 3+9, 7+2] +assert list(map(plus, [1, 3, 7], [4, 9, 2], [1, 1, 0])) == [1+4+1, 3+9+1, 7+2+0] +assert list(map(int, Squares(10))) == [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +def Max(a, b): + if a is None: + return b + if b is None: + return a + return max(a, b) +assert list(map(Max, Squares(3), Squares(2))) == [0, 1] +assertRaises(TypeError, map) +assertRaises(TypeError, map, lambda x: x, 42) +class BadSeq: + def __iter__(self): + raise ValueError + yield None +assertRaises(ValueError, list, map(lambda x: x, BadSeq())) +def badfunc(x): + raise RuntimeError +assertRaises(RuntimeError, list, map(badfunc, range(5))) +doc="finished" diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 88d6cffe..243f26a3 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -81,9 +81,9 @@ func init() { "float": py.FloatType, "frozenset": py.FrozenSetType, // "property": py.PropertyType, - "int": py.IntType, // FIXME LongType? - "list": py.ListType, - // "map": py.MapType, + "int": py.IntType, // FIXME LongType? + "list": py.ListType, + "map": py.MapType, "object": py.ObjectType, "range": py.RangeType, // "reversed": py.ReversedType, From bd13450143541a222b7428febecda13a717aa0c7 Mon Sep 17 00:00:00 2001 From: wetor Date: Wed, 7 Jun 2023 19:29:15 +0800 Subject: [PATCH 04/24] builtin: implement `oct` and optimise `hex` --- stdlib/builtin/builtin.go | 71 +++++++++++++++++++++++++++++---- stdlib/builtin/tests/builtin.py | 6 +++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 243f26a3..290cb939 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -8,6 +8,7 @@ package builtin import ( "fmt" "math/big" + "strconv" "unicode/utf8" "github.com/go-python/gpython/compile" @@ -53,7 +54,7 @@ func init() { 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("oct", builtin_oct, 0, oct_doc), py.MustNewMethod("ord", builtin_ord, 0, ord_doc), py.MustNewMethod("pow", builtin_pow, 0, pow_doc), py.MustNewMethod("print", builtin_print, 0, print_doc), @@ -592,6 +593,56 @@ func builtin_open(self py.Object, args py.Tuple, kwargs py.StringDict) (py.Objec int(buffering.(py.Int))) } +const oct_doc = `oct(number) -> string + +Return the octal representation of an integer. + + >>> oct(342391) + '0o1234567' +` + +func builtin_oct(self, v py.Object) (py.Object, error) { + var ( + i int64 + err error + ) + switch v := v.(type) { + case *py.BigInt: + vv := (*big.Int)(v) + neg := false + if vv.Cmp(big.NewInt(0)) == -1 { + neg = true + } + str := vv.Text(8) + if neg { + str = "-0o" + str[1:] + } else { + str = "0o" + str + } + return py.String(str), nil + case py.IGoInt64: + i, err = v.GoInt64() + case py.IGoInt: + var vv int + vv, err = v.GoInt() + i = int64(vv) + default: + return nil, py.ExceptionNewf(py.TypeError, "'%s' object cannot be interpreted as an integer", v.Type().Name) + } + + if err != nil { + return nil, err + } + + str := strconv.FormatInt(i, 8) + if i < 0 { + str = "-0o" + str[1:] + } else { + str = "0o" + str + } + return py.String(str), nil +} + const ord_doc = `ord(c) -> integer Return the integer ordinal of a one-character string.` @@ -865,11 +916,16 @@ func builtin_hex(self, v py.Object) (py.Object, error) { // test bigint first to make sure we correctly handle the case // where int64 isn't large enough. vv := (*big.Int)(v) - format := "%#x" + neg := false if vv.Cmp(big.NewInt(0)) == -1 { - format = "%+#x" + neg = true + } + str := vv.Text(16) + if neg { + str = "-0x" + str[1:] + } else { + str = "0x" + str } - str := fmt.Sprintf(format, vv) return py.String(str), nil case py.IGoInt64: i, err = v.GoInt64() @@ -885,11 +941,12 @@ func builtin_hex(self, v py.Object) (py.Object, error) { return nil, err } - format := "%#x" + str := strconv.FormatInt(i, 16) if i < 0 { - format = "%+#x" + str = "-0x" + str[1:] + } else { + str = "0x" + str } - str := fmt.Sprintf(format, i) return py.String(str), nil } diff --git a/stdlib/builtin/tests/builtin.py b/stdlib/builtin/tests/builtin.py index 07f1704a..ae4e8a5f 100644 --- a/stdlib/builtin/tests/builtin.py +++ b/stdlib/builtin/tests/builtin.py @@ -275,6 +275,12 @@ def gen2(): ok = True assert ok, "ValueError not raised" +doc="oct" +assert oct(0) == '0o0' +assert oct(100) == '0o144' +assert oct(-100) == '-0o144' +assertRaises(TypeError, oct, ()) + doc="ord" assert 65 == ord("A") assert 163 == ord("£") From 7102b79c9edeff6c73ca3f57553e0959496d08ca Mon Sep 17 00:00:00 2001 From: wetor Date: Thu, 8 Jun 2023 16:46:47 +0800 Subject: [PATCH 05/24] py: fix basic type not run `Ready()` --- py/type.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/type.go b/py/type.go index be6d19af..509a7628 100644 --- a/py/type.go +++ b/py/type.go @@ -306,7 +306,7 @@ func (t *Type) NewTypeFlags(Name string, Doc string, New NewFunc, Init InitFunc, Dict: StringDict{}, Bases: Tuple{t}, } - TypeDelayReady(t) + TypeDelayReady(tt) return tt } From e9cde5fcf8e89407c50c12534c1b14bb1f840ffa Mon Sep 17 00:00:00 2001 From: wetor Date: Sat, 14 Oct 2023 01:31:15 +0800 Subject: [PATCH 06/24] compile,py: fix closure and decorator --- compile/compile.go | 15 +- py/code.go | 61 ++++---- vm/tests/class.py | 23 ++- vm/tests/decorators.py | 327 +++++++++++++++++++++++++++++++++++++++++ vm/tests/functions.py | 38 +++-- vm/tests/libtest.py | 57 +++++++ 6 files changed, 466 insertions(+), 55 deletions(-) create mode 100644 vm/tests/decorators.py create mode 100644 vm/tests/libtest.py diff --git a/compile/compile.go b/compile/compile.go index 76d46c1a..fd8d7a2b 100644 --- a/compile/compile.go +++ b/compile/compile.go @@ -213,6 +213,8 @@ func (c *compiler) compileAst(Ast ast.Ast, filename string, futureFlags int, don case *ast.Suite: panic("suite should not be possible") case *ast.Lambda: + code.Argcount = int32(len(node.Args.Args)) + code.Kwonlyargcount = int32(len(node.Args.Kwonlyargs)) // Make None the first constant as lambda can't have a docstring c.Const(py.None) code.Name = "" @@ -220,6 +222,8 @@ func (c *compiler) compileAst(Ast ast.Ast, filename string, futureFlags int, don c.Expr(node.Body) valueOnStack = true case *ast.FunctionDef: + code.Argcount = int32(len(node.Args.Args)) + code.Kwonlyargcount = int32(len(node.Args.Kwonlyargs)) code.Name = string(node.Name) c.setQualname() c.Stmts(c.docString(node.Body, true)) @@ -299,6 +303,7 @@ func (c *compiler) compileAst(Ast ast.Ast, filename string, futureFlags int, don code.Stacksize = int32(c.OpCodes.StackDepth()) code.Nlocals = int32(len(code.Varnames)) code.Lnotab = string(c.OpCodes.Lnotab()) + code.InitCell2arg() return nil } @@ -479,7 +484,8 @@ func (c *compiler) makeClosure(code *py.Code, args uint32, child *compiler, qual if reftype == symtable.ScopeCell { arg = c.FindId(name, c.Code.Cellvars) } else { /* (reftype == FREE) */ - arg = c.FindId(name, c.Code.Freevars) + // using CellAndFreeVars in closures requires skipping Cellvars + arg = len(c.Code.Cellvars) + c.FindId(name, c.Code.Freevars) } if arg < 0 { panic(fmt.Sprintf("compile: makeClosure: lookup %q in %q %v %v\nfreevars of %q: %v\n", name, c.SymTable.Name, reftype, arg, code.Name, code.Freevars)) @@ -1363,7 +1369,12 @@ func (c *compiler) NameOp(name string, ctx ast.ExprContext) { if op == 0 { panic("NameOp: Op not set") } - c.OpArg(op, c.Index(mangled, dict)) + i := c.Index(mangled, dict) + // using CellAndFreeVars in closures requires skipping Cellvars + if scope == symtable.ScopeFree { + i += uint32(len(c.Code.Cellvars)) + } + c.OpArg(op, i) } // Call a function which is already on the stack with n arguments already on the stack diff --git a/py/code.go b/py/code.go index 09027497..ad9a42af 100644 --- a/py/code.go +++ b/py/code.go @@ -112,8 +112,6 @@ func NewCode(argcount int32, kwonlyargcount int32, filename_ Object, name_ Object, firstlineno int32, lnotab_ Object) *Code { - var cell2arg []byte - // Type assert the objects consts := consts_.(Tuple) namesTuple := names_.(Tuple) @@ -154,7 +152,6 @@ func NewCode(argcount int32, kwonlyargcount int32, // return nil; // } - n_cellvars := len(cellvars) intern_strings(namesTuple) intern_strings(varnamesTuple) intern_strings(freevarsTuple) @@ -167,13 +164,40 @@ func NewCode(argcount int32, kwonlyargcount int32, } } } + + co := &Code{ + Argcount: argcount, + Kwonlyargcount: kwonlyargcount, + Nlocals: nlocals, + Stacksize: stacksize, + Flags: flags, + Code: code, + Consts: consts, + Names: names, + Varnames: varnames, + Freevars: freevars, + Cellvars: cellvars, + Filename: filename, + Name: name, + Firstlineno: firstlineno, + Lnotab: lnotab, + Weakreflist: nil, + } + co.InitCell2arg() + return co +} + +// Create mapping between cells and arguments if needed. +func (co *Code) InitCell2arg() { + var cell2arg []byte + n_cellvars := len(co.Cellvars) /* Create mapping between cells and arguments if needed. */ if n_cellvars != 0 { - total_args := argcount + kwonlyargcount - if flags&CO_VARARGS != 0 { + total_args := co.Argcount + co.Kwonlyargcount + if co.Flags&CO_VARARGS != 0 { total_args++ } - if flags&CO_VARKEYWORDS != 0 { + if co.Flags&CO_VARKEYWORDS != 0 { total_args++ } used_cell2arg := false @@ -182,9 +206,9 @@ func NewCode(argcount int32, kwonlyargcount int32, cell2arg[i] = CO_CELL_NOT_AN_ARG } // Find cells which are also arguments. - for i, cell := range cellvars { + for i, cell := range co.Cellvars { for j := int32(0); j < total_args; j++ { - arg := varnames[j] + arg := co.Varnames[j] if cell == arg { cell2arg[i] = byte(j) used_cell2arg = true @@ -196,26 +220,7 @@ func NewCode(argcount int32, kwonlyargcount int32, cell2arg = nil } } - - return &Code{ - Argcount: argcount, - Kwonlyargcount: kwonlyargcount, - Nlocals: nlocals, - Stacksize: stacksize, - Flags: flags, - Code: code, - Consts: consts, - Names: names, - Varnames: varnames, - Freevars: freevars, - Cellvars: cellvars, - Cell2arg: cell2arg, - Filename: filename, - Name: name, - Firstlineno: firstlineno, - Lnotab: lnotab, - Weakreflist: nil, - } + co.Cell2arg = cell2arg } // Return number of free variables diff --git a/vm/tests/class.py b/vm/tests/class.py index 2c8fd70b..f9781cd7 100644 --- a/vm/tests/class.py +++ b/vm/tests/class.py @@ -47,17 +47,16 @@ def method1(self, x): c = x() assert c.method1(1) == 2 -# FIXME doesn't work -# doc="CLASS_DEREF2" -# def classderef2(x): -# class DeRefTest: -# VAR = x -# def method1(self, x): -# "method1" -# return self.VAR+x -# return DeRefTest -# x = classderef2(1) -# c = x() -# assert c.method1(1) == 2 +doc="CLASS_DEREF2" +def classderef2(x): + class DeRefTest: + VAR = x + def method1(self, x): + "method1" + return self.VAR+x + return DeRefTest +x = classderef2(1) +c = x() +assert c.method1(1) == 2 doc="finished" diff --git a/vm/tests/decorators.py b/vm/tests/decorators.py new file mode 100644 index 00000000..b7e2d703 --- /dev/null +++ b/vm/tests/decorators.py @@ -0,0 +1,327 @@ +# Copyright 2023 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. + +# Copied from Python-3.4.9\Lib\test\test_decorators.py + +import libtest as self + +def funcattrs(**kwds): + def decorate(func): + # FIXME func.__dict__.update(kwds) + for k, v in kwds.items(): + func.__dict__[k] = v + return func + return decorate + +class MiscDecorators (object): + @staticmethod + def author(name): + def decorate(func): + func.__dict__['author'] = name + return func + return decorate + +# ----------------------------------------------- + +class DbcheckError (Exception): + def __init__(self, exprstr, func, args, kwds): + # A real version of this would set attributes here + Exception.__init__(self, "dbcheck %r failed (func=%s args=%s kwds=%s)" % + (exprstr, func, args, kwds)) + + +def dbcheck(exprstr, globals=None, locals=None): + "Decorator to implement debugging assertions" + def decorate(func): + expr = compile(exprstr, "dbcheck-%s" % func.__name__, "eval") + def check(*args, **kwds): + if not eval(expr, globals, locals): + raise DbcheckError(exprstr, func, args, kwds) + return func(*args, **kwds) + return check + return decorate + +# ----------------------------------------------- + +def countcalls(counts): + "Decorator to count calls to a function" + def decorate(func): + func_name = func.__name__ + counts[func_name] = 0 + def call(*args, **kwds): + counts[func_name] += 1 + return func(*args, **kwds) + call.__name__ = func_name + return call + return decorate + +# ----------------------------------------------- + +# FIXME: dict can only have string keys +# def memoize(func): +# saved = {} +# def call(*args): +# try: +# return saved[args] +# except KeyError: +# res = func(*args) +# saved[args] = res +# return res +# except TypeError: +# # Unhashable argument +# return func(*args) +# call.__name__ = func.__name__ +# return call +def memoize(func): + saved = {} + def call(*args): + try: + if isinstance(args[0], list): + raise TypeError + return saved[str(args)] + except KeyError: + res = func(*args) + saved[str(args)] = res + return res + except TypeError: + # Unhashable argument + return func(*args) + call.__name__ = func.__name__ + return call + +# ----------------------------------------------- + +doc="test_single" +# FIXME staticmethod +# class C(object): +# @staticmethod +# def foo(): return 42 +# self.assertEqual(C.foo(), 42) +# self.assertEqual(C().foo(), 42) + +doc="test_staticmethod_function" +@staticmethod +def notamethod(x): + return x +self.assertRaises(TypeError, notamethod, 1) + +doc="test_dotted" +# FIXME class decorator +# decorators = MiscDecorators() +# @decorators.author('Cleese') +# def foo(): return 42 +# self.assertEqual(foo(), 42) +# self.assertEqual(foo.author, 'Cleese') + +doc="test_argforms" +def noteargs(*args, **kwds): + def decorate(func): + setattr(func, 'dbval', (args, kwds)) + return func + return decorate + +args = ( 'Now', 'is', 'the', 'time' ) +kwds = dict(one=1, two=2) +@noteargs(*args, **kwds) +def f1(): return 42 +self.assertEqual(f1(), 42) +self.assertEqual(f1.dbval, (args, kwds)) + +@noteargs('terry', 'gilliam', eric='idle', john='cleese') +def f2(): return 84 +self.assertEqual(f2(), 84) +self.assertEqual(f2.dbval, (('terry', 'gilliam'), + dict(eric='idle', john='cleese'))) + +@noteargs(1, 2,) +def f3(): pass +self.assertEqual(f3.dbval, ((1, 2), {})) + +doc="test_dbcheck" +# FIXME TypeError: "catching 'BaseException' that does not inherit from BaseException is not allowed" +# @dbcheck('args[1] is not None') +# def f(a, b): +# return a + b +# self.assertEqual(f(1, 2), 3) +# self.assertRaises(DbcheckError, f, 1, None) + +doc="test_memoize" +counts = {} + +@memoize +@countcalls(counts) +def double(x): + return x * 2 +self.assertEqual(double.__name__, 'double') + +self.assertEqual(counts, dict(double=0)) + +# Only the first call with a given argument bumps the call count: +# +# Only the first call with a given argument bumps the call count: +# +self.assertEqual(double(2), 4) +self.assertEqual(counts['double'], 1) +self.assertEqual(double(2), 4) +self.assertEqual(counts['double'], 1) +self.assertEqual(double(3), 6) +self.assertEqual(counts['double'], 2) + +# Unhashable arguments do not get memoized: +# +self.assertEqual(double([10]), [10, 10]) +self.assertEqual(counts['double'], 3) +self.assertEqual(double([10]), [10, 10]) +self.assertEqual(counts['double'], 4) + +doc="test_errors" +# Test syntax restrictions - these are all compile-time errors: +# +for expr in [ "1+2", "x[3]", "(1, 2)" ]: + # Sanity check: is expr is a valid expression by itself? + compile(expr, "testexpr", "exec") + + codestr = "@%s\ndef f(): pass" % expr + self.assertRaises(SyntaxError, compile, codestr, "test", "exec") + +# You can't put multiple decorators on a single line: +# +self.assertRaises(SyntaxError, compile, + "@f1 @f2\ndef f(): pass", "test", "exec") + +# Test runtime errors + +def unimp(func): + raise NotImplementedError +context = dict(nullval=None, unimp=unimp) + +for expr, exc in [ ("undef", NameError), + ("nullval", TypeError), + ("nullval.attr", NameError), # FIXME ("nullval.attr", AttributeError), + ("unimp", NotImplementedError)]: + codestr = "@%s\ndef f(): pass\nassert f() is None" % expr + code = compile(codestr, "test", "exec") + self.assertRaises(exc, eval, code, context) + +doc="test_double" +class C(object): + @funcattrs(abc=1, xyz="haha") + @funcattrs(booh=42) + def foo(self): return 42 +self.assertEqual(C().foo(), 42) +self.assertEqual(C.foo.abc, 1) +self.assertEqual(C.foo.xyz, "haha") +self.assertEqual(C.foo.booh, 42) + + +doc="test_order" +# Test that decorators are applied in the proper order to the function +# they are decorating. +def callnum(num): + """Decorator factory that returns a decorator that replaces the + passed-in function with one that returns the value of 'num'""" + def deco(func): + return lambda: num + return deco +@callnum(2) +@callnum(1) +def foo(): return 42 +self.assertEqual(foo(), 2, + "Application order of decorators is incorrect") + + +doc="test_eval_order" +# Evaluating a decorated function involves four steps for each +# decorator-maker (the function that returns a decorator): +# +# 1: Evaluate the decorator-maker name +# 2: Evaluate the decorator-maker arguments (if any) +# 3: Call the decorator-maker to make a decorator +# 4: Call the decorator +# +# When there are multiple decorators, these steps should be +# performed in the above order for each decorator, but we should +# iterate through the decorators in the reverse of the order they +# appear in the source. +# FIXME class decorator +# actions = [] +# +# def make_decorator(tag): +# actions.append('makedec' + tag) +# def decorate(func): +# actions.append('calldec' + tag) +# return func +# return decorate +# +# class NameLookupTracer (object): +# def __init__(self, index): +# self.index = index +# +# def __getattr__(self, fname): +# if fname == 'make_decorator': +# opname, res = ('evalname', make_decorator) +# elif fname == 'arg': +# opname, res = ('evalargs', str(self.index)) +# else: +# assert False, "Unknown attrname %s" % fname +# actions.append('%s%d' % (opname, self.index)) +# return res +# +# c1, c2, c3 = map(NameLookupTracer, [ 1, 2, 3 ]) +# +# expected_actions = [ 'evalname1', 'evalargs1', 'makedec1', +# 'evalname2', 'evalargs2', 'makedec2', +# 'evalname3', 'evalargs3', 'makedec3', +# 'calldec3', 'calldec2', 'calldec1' ] +# +# actions = [] +# @c1.make_decorator(c1.arg) +# @c2.make_decorator(c2.arg) +# @c3.make_decorator(c3.arg) +# def foo(): return 42 +# self.assertEqual(foo(), 42) +# +# self.assertEqual(actions, expected_actions) +# +# # Test the equivalence claim in chapter 7 of the reference manual. +# # +# actions = [] +# def bar(): return 42 +# bar = c1.make_decorator(c1.arg)(c2.make_decorator(c2.arg)(c3.make_decorator(c3.arg)(bar))) +# self.assertEqual(bar(), 42) +# self.assertEqual(actions, expected_actions) + +doc="test_simple" +def plain(x): + x.extra = 'Hello' + return x +@plain +class C(object): pass +self.assertEqual(C.extra, 'Hello') + +doc="test_double" +def ten(x): + x.extra = 10 + return x +def add_five(x): + x.extra += 5 + return x + +@add_five +@ten +class C(object): pass +self.assertEqual(C.extra, 15) + +doc="test_order" +def applied_first(x): + x.extra = 'first' + return x +def applied_second(x): + x.extra = 'second' + return x +@applied_second +@applied_first +class C(object): pass +self.assertEqual(C.extra, 'second') +doc="finished" diff --git a/vm/tests/functions.py b/vm/tests/functions.py index da4bf924..aab079f9 100644 --- a/vm/tests/functions.py +++ b/vm/tests/functions.py @@ -21,18 +21,32 @@ def fn2(x,y=1): assert fn2(1,y=4) == 5 # Closure +doc="closure1" +closure1 = lambda x: lambda y: x+y +cf1 = closure1(1) +assert cf1(1) == 2 +assert cf1(2) == 3 + +doc="closure2" +def closure2(*args, **kwargs): + def inc(): + kwargs['x'] += 1 + return kwargs['x'] + return inc +cf2 = closure2(x=1) +assert cf2() == 2 +assert cf2() == 3 -# FIXME something wrong with closures over function arguments... -# doc="counter3" -# def counter3(x): -# def inc(): -# nonlocal x -# x += 1 -# return x -# return inc -# fn3 = counter3(1) -# assert fn3() == 2 -# assert fn3() == 3 +doc="counter3" +def counter3(x): + def inc(): + nonlocal x + x += 1 + return x + return inc +fn3 = counter3(1) +assert fn3() == 2 +assert fn3() == 3 doc="counter4" def counter4(initial): @@ -238,6 +252,4 @@ def fn16_6(*,a,b,c): ck(fn16_5, "fn16_5() missing 2 required keyword-only arguments: 'a' and 'b'") ck(fn16_6, "fn16_6() missing 3 required keyword-only arguments: 'a', 'b', and 'c'") -#FIXME decorators - doc="finished" diff --git a/vm/tests/libtest.py b/vm/tests/libtest.py new file mode 100644 index 00000000..8038556d --- /dev/null +++ b/vm/tests/libtest.py @@ -0,0 +1,57 @@ +# Copyright 2023 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. + +# Imitate the calling method of unittest + +def assertRaises(expecting, fn, *args, **kwargs): + """Check the exception was raised - don't check the text""" + try: + fn(*args, **kwargs) + except expecting as e: + pass + else: + assert False, "%s not raised" % (expecting,) + +def assertEqual(first, second, msg=None): + if msg: + assert first == second, "%s not equal" % (msg,) + else: + assert first == second + +def assertIs(expr1, expr2, msg=None): + if msg: + assert expr1 is expr2, "%s is not None" % (msg,) + else: + assert expr1 is expr2 + +def assertIsNone(obj, msg=None): + if msg: + assert obj is None, "%s is not None" % (msg,) + else: + assert obj is None + +def assertTrue(obj, msg=None): + if msg: + assert obj, "%s is not True" % (msg,) + else: + assert obj + +def assertRaisesText(expecting, text, fn, *args, **kwargs): + """Check the exception with text in is raised""" + try: + fn(*args, **kwargs) + except expecting as e: + assert text in e.args[0], "'%s' not found in '%s'" % (text, e.args[0]) + else: + assert False, "%s not raised" % (expecting,) + +def assertTypedEqual(actual, expect, msg=None): + assertEqual(actual, expect, msg) + def recurse(actual, expect): + if isinstance(expect, (tuple, list)): + for x, y in zip(actual, expect): + recurse(x, y) + else: + assertIs(type(actual), type(expect)) + recurse(actual, expect) From ed47ed361fcbdb6fe2ed87579e569fe86e9d5755 Mon Sep 17 00:00:00 2001 From: vasilev Date: Wed, 22 Nov 2023 19:35:45 +0600 Subject: [PATCH 07/24] Fixed WASM compatibility by avoiding panic due to absence of `os.Executable()` in "js" and "wasip1" targets. --- stdlib/sys/sys.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/stdlib/sys/sys.go b/stdlib/sys/sys.go index 52d733b6..3a2318eb 100644 --- a/stdlib/sys/sys.go +++ b/stdlib/sys/sys.go @@ -19,6 +19,7 @@ package sys import ( "os" + "runtime" "github.com/go-python/gpython/py" ) @@ -659,7 +660,14 @@ func init() { executable, err := os.Executable() if err != nil { - panic(err) + switch runtime.GOOS { + case "js", "wasip1": + // These platforms don't implement os.Executable (at least as of Go + // 1.21), see https://github.com/tailscale/tailscale/pull/8325 + executable = "gpython" + default: + panic(err) + } } globals := py.StringDict{ From 53252dd563c98158a0c88e360727e30e69981822 Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Tue, 28 Nov 2023 12:34:04 +0100 Subject: [PATCH 08/24] stdlib/array: first import Signed-off-by: Sebastien Binet --- stdlib/array/array.go | 710 ++++++++++++++++++++++++++ stdlib/array/array_test.go | 15 + stdlib/array/testdata/test.py | 162 ++++++ stdlib/array/testdata/test_golden.txt | 298 +++++++++++ stdlib/stdlib.go | 1 + 5 files changed, 1186 insertions(+) create mode 100644 stdlib/array/array.go create mode 100644 stdlib/array/array_test.go create mode 100644 stdlib/array/testdata/test.py create mode 100644 stdlib/array/testdata/test_golden.txt diff --git a/stdlib/array/array.go b/stdlib/array/array.go new file mode 100644 index 00000000..f28d05ac --- /dev/null +++ b/stdlib/array/array.go @@ -0,0 +1,710 @@ +// Copyright 2023 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 array provides the implementation of the python's 'array' module. +package array + +import ( + "fmt" + "reflect" + "strings" + + "github.com/go-python/gpython/py" +) + +type array struct { + descr byte // typecode of elements + esize int // element size in bytes + data any + + append func(v py.Object) (py.Object, error) + extend func(seq py.Object) (py.Object, error) +} + +// Type of this StringDict object +func (*array) Type() *py.Type { + return ArrayType +} + +var ( + _ py.Object = (*array)(nil) + _ py.I__getitem__ = (*array)(nil) + _ py.I__setitem__ = (*array)(nil) + _ py.I__len__ = (*array)(nil) + _ py.I__repr__ = (*array)(nil) + _ py.I__str__ = (*array)(nil) +) + +var ( + typecodes = py.String("bBuhHiIlLqQfd") + ArrayType = py.ObjectType.NewType("array.array", array_doc, array_new, nil) + + descr2esize = map[byte]int{ + 'b': 1, + 'B': 1, + 'u': 2, + 'h': 2, + 'H': 2, + 'i': 2, + 'I': 2, + 'l': 8, + 'L': 8, + 'q': 8, + 'Q': 8, + 'f': 4, + 'd': 8, + } +) + +func init() { + py.RegisterModule(&py.ModuleImpl{ + Info: py.ModuleInfo{ + Name: "array", + Doc: "This module defines an object type which can efficiently represent\n" + + "an array of basic values: characters, integers, floating point\n" + + "numbers. Arrays are sequence types and behave very much like lists,\n" + + "except that the type of objects stored in them is constrained.\n", + }, + Methods: []*py.Method{}, + Globals: py.StringDict{ + "typecodes": typecodes, + "array": ArrayType, + "ArrayType": ArrayType, + }, + }) + + ArrayType.Dict["itemsize"] = &py.Property{ + Fget: func(self py.Object) (py.Object, error) { + arr := self.(*array) + return py.Int(arr.esize), nil + }, + Doc: "the size, in bytes, of one array item", + } + + ArrayType.Dict["typecode"] = &py.Property{ + Fget: func(self py.Object) (py.Object, error) { + arr := self.(*array) + return py.String(arr.descr), nil + }, + Doc: "the typecode character used to create the array", + } + + ArrayType.Dict["append"] = py.MustNewMethod("append", array_append, 0, array_append_doc) + ArrayType.Dict["extend"] = py.MustNewMethod("extend", array_extend, 0, array_extend_doc) +} + +const array_doc = `array(typecode [, initializer]) -> array + +Return a new array whose items are restricted by typecode, and +initialized from the optional initializer value, which must be a list, +string or iterable over elements of the appropriate type. + +Arrays represent basic values and behave very much like lists, except +the type of objects stored in them is constrained. The type is specified +at object creation time by using a type code, which is a single character. +The following type codes are defined: + + Type code C Type Minimum size in bytes + 'b' signed integer 1 + 'B' unsigned integer 1 + 'u' Unicode character 2 (see note) + 'h' signed integer 2 + 'H' unsigned integer 2 + 'i' signed integer 2 + 'I' unsigned integer 2 + 'l' signed integer 4 + 'L' unsigned integer 4 + 'q' signed integer 8 (see note) + 'Q' unsigned integer 8 (see note) + 'f' floating point 4 + 'd' floating point 8 + +NOTE: The 'u' typecode corresponds to Python's unicode character. On +narrow builds this is 2-bytes on wide builds this is 4-bytes. + +NOTE: The 'q' and 'Q' type codes are only available if the platform +C compiler used to build Python supports 'long long', or, on Windows, +'__int64'. + +Methods: + +append() -- append a new item to the end of the array +buffer_info() -- return information giving the current memory info +byteswap() -- byteswap all the items of the array +count() -- return number of occurrences of an object +extend() -- extend array by appending multiple elements from an iterable +fromfile() -- read items from a file object +fromlist() -- append items from the list +frombytes() -- append items from the string +index() -- return index of first occurrence of an object +insert() -- insert a new item into the array at a provided position +pop() -- remove and return item (default last) +remove() -- remove first occurrence of an object +reverse() -- reverse the order of the items in the array +tofile() -- write all items to a file object +tolist() -- return the array converted to an ordinary list +tobytes() -- return the array converted to a string + +Attributes: + +typecode -- the typecode character used to create the array +itemsize -- the length in bytes of one array item + +` + +func array_new(metatype *py.Type, args py.Tuple, kwargs py.StringDict) (py.Object, error) { + switch n := len(args); n { + case 0: + return nil, py.ExceptionNewf(py.TypeError, "array() takes at least 1 argument (0 given)") + case 1, 2: + // ok + default: + return nil, py.ExceptionNewf(py.TypeError, "array() takes at most 2 arguments (%d given)", n) + } + + if len(kwargs) != 0 { + return nil, py.ExceptionNewf(py.TypeError, "array.array() takes no keyword arguments") + } + + descr, ok := args[0].(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "array() argument 1 must be a unicode character, not %s", args[0].Type().Name) + } + + if len(descr) != 1 { + return nil, py.ExceptionNewf(py.TypeError, "array() argument 1 must be a unicode character, not str") + } + + if !strings.ContainsAny(string(descr), string(typecodes)) { + ts := new(strings.Builder) + for i, v := range typecodes { + if i > 0 { + switch { + case i == len(typecodes)-1: + ts.WriteString(" or ") + default: + ts.WriteString(", ") + } + } + ts.WriteString(string(v)) + } + return nil, py.ExceptionNewf(py.ValueError, "bad typecode (must be %s)", ts) + } + + arr := &array{ + descr: descr[0], + esize: descr2esize[descr[0]], + } + + if descr[0] == 'u' { + // FIXME(sbinet) + return nil, py.NotImplementedError + } + + switch descr[0] { + case 'b': + var data []int8 + arr.data = data + arr.append = arr.appendI8 + arr.extend = arr.extendI8 + case 'h': + var data []int16 + arr.data = data + arr.append = arr.appendI16 + arr.extend = arr.extendI16 + case 'i': + var data []int32 + arr.data = data + arr.append = arr.appendI32 + arr.extend = arr.extendI32 + case 'l', 'q': + var data []int64 + arr.data = data + arr.append = arr.appendI64 + arr.extend = arr.extendI64 + case 'B': + var data []uint8 + arr.data = data + arr.append = arr.appendU8 + arr.extend = arr.extendU8 + case 'H': + var data []uint16 + arr.data = data + arr.append = arr.appendU16 + arr.extend = arr.extendU16 + case 'I': + var data []uint32 + arr.data = data + arr.append = arr.appendU32 + arr.extend = arr.extendU32 + case 'L', 'Q': + var data []uint64 + arr.data = data + arr.append = arr.appendU64 + arr.extend = arr.extendU64 + case 'f': + var data []float32 + arr.data = data + arr.append = arr.appendF32 + arr.extend = arr.extendF32 + case 'd': + var data []float64 + arr.data = data + arr.append = arr.appendF64 + arr.extend = arr.extendF64 + } + + if len(args) == 2 { + _, err := arr.extend(args[1]) + if err != nil { + return nil, err + } + } + + return arr, nil +} + +const array_append_doc = `Append new value v to the end of the array.` + +func array_append(self py.Object, args py.Tuple) (py.Object, error) { + arr, ok := self.(*array) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "expected an array, got '%s'", self.Type().Name) + } + if len(args) != 1 { + return nil, py.ExceptionNewf(py.TypeError, "array.append() takes exactly one argument (%d given)", len(args)) + } + + return arr.append(args[0]) +} + +const array_extend_doc = `Append items to the end of the array.` + +func array_extend(self py.Object, args py.Tuple) (py.Object, error) { + arr, ok := self.(*array) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "expected an array, got '%s'", self.Type().Name) + } + if len(args) == 0 { + return nil, py.ExceptionNewf(py.TypeError, "extend() takes exactly 1 positional argument (%d given)", len(args)) + } + if len(args) != 1 { + return nil, py.ExceptionNewf(py.TypeError, "extend() takes at most 1 argument (%d given)", len(args)) + } + + return arr.extend(args[0]) +} + +func (arr *array) M__repr__() (py.Object, error) { + o := new(strings.Builder) + o.WriteString("array('" + string(arr.descr) + "'") + if data := reflect.ValueOf(arr.data); arr.data != nil && data.Len() > 0 { + o.WriteString(", [") + for i := 0; i < data.Len(); i++ { + if i > 0 { + o.WriteString(", ") + } + fmt.Fprintf(o, "%v", data.Index(i)) + } + o.WriteString("]") + } + o.WriteString(")") + return py.String(o.String()), nil +} + +func (arr *array) M__str__() (py.Object, error) { + return arr.M__repr__() +} + +func (arr *array) M__len__() (py.Object, error) { + if arr.data == nil { + return py.Int(0), nil + } + sli := reflect.ValueOf(arr.data) + return py.Int(sli.Len()), nil +} + +func (arr *array) M__getitem__(k py.Object) (py.Object, error) { + switch k := k.(type) { + case py.Int: + var ( + sli = reflect.ValueOf(arr.data) + i = int(k) + ) + if i < 0 { + i = sli.Len() + i + } + if i < 0 || sli.Len() <= i { + return nil, py.ExceptionNewf(py.IndexError, "array index out of range") + } + switch arr.descr { + case 'b', 'h', 'i', 'l', 'q': + return py.Int(sli.Index(i).Int()), nil + case 'B', 'H', 'I', 'L', 'Q': + return py.Int(sli.Index(i).Uint()), nil + case 'u': + // FIXME(sbinet) + return nil, py.NotImplementedError + case 'f', 'd': + return py.Float(sli.Index(i).Float()), nil + } + case *py.Slice: + return nil, py.NotImplementedError + default: + return nil, py.ExceptionNewf(py.TypeError, "array indices must be integers") + } + panic("impossible") +} + +func (arr *array) M__setitem__(k, v py.Object) (py.Object, error) { + switch k := k.(type) { + case py.Int: + var ( + sli = reflect.ValueOf(arr.data) + i = int(k) + ) + if i < 0 { + i = sli.Len() + i + } + if i < 0 || sli.Len() <= i { + return nil, py.ExceptionNewf(py.IndexError, "array index out of range") + } + switch arr.descr { + case 'b', 'h', 'i', 'l', 'q': + vv := v.(py.Int) + sli.Index(i).SetInt(int64(vv)) + case 'B', 'H', 'I', 'L', 'Q': + vv := v.(py.Int) + sli.Index(i).SetUint(uint64(vv)) + case 'u': + // FIXME(sbinet) + return nil, py.NotImplementedError + case 'f', 'd': + var vv float64 + switch v := v.(type) { + case py.Int: + vv = float64(v) + case py.Float: + vv = float64(v) + default: + return nil, py.ExceptionNewf(py.TypeError, "must be real number, not %s", v.Type().Name) + } + sli.Index(i).SetFloat(vv) + } + return py.None, nil + case *py.Slice: + return nil, py.NotImplementedError + default: + return nil, py.ExceptionNewf(py.TypeError, "array indices must be integers") + } + panic("impossible") +} + +func (arr *array) appendI8(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]int8), int8(vv)) + return py.None, nil +} + +func (arr *array) appendI16(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]int16), int16(vv)) + return py.None, nil +} + +func (arr *array) appendI32(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]int32), int32(vv)) + return py.None, nil +} + +func (arr *array) appendI64(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]int64), int64(vv)) + return py.None, nil +} + +func (arr *array) appendU8(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]uint8), uint8(vv)) + return py.None, nil +} + +func (arr *array) appendU16(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]uint16), uint16(vv)) + return py.None, nil +} + +func (arr *array) appendU32(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]uint32), uint32(vv)) + return py.None, nil +} + +func (arr *array) appendU64(v py.Object) (py.Object, error) { + vv, err := asInt(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]uint64), uint64(vv)) + return py.None, nil +} + +func (arr *array) appendF32(v py.Object) (py.Object, error) { + vv, err := py.FloatAsFloat64(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]float32), float32(vv)) + return py.None, nil +} + +func (arr *array) appendF64(v py.Object) (py.Object, error) { + vv, err := py.FloatAsFloat64(v) + if err != nil { + return nil, err + } + arr.data = append(arr.data.([]float64), float64(vv)) + return py.None, nil +} + +func (arr *array) extendI8(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendI8(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendI16(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendI16(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendI32(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendI32(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendI64(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendI64(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendU8(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendU8(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendU16(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendU16(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendU32(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendU32(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendU64(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendU64(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendF32(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendF32(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func (arr *array) extendF64(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendF64(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + +func asInt(o py.Object) (int64, error) { + v, ok := o.(py.Int) + if !ok { + return 0, py.ExceptionNewf(py.TypeError, "unsupported operand type(s) for int: '%s'", o.Type().Name) + } + return int64(v), nil +} diff --git a/stdlib/array/array_test.go b/stdlib/array/array_test.go new file mode 100644 index 00000000..a34ed842 --- /dev/null +++ b/stdlib/array/array_test.go @@ -0,0 +1,15 @@ +// Copyright 2023 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 array_test + +import ( + "testing" + + "github.com/go-python/gpython/pytest" +) + +func TestArray(t *testing.T) { + pytest.RunScript(t, "./testdata/test.py") +} diff --git a/stdlib/array/testdata/test.py b/stdlib/array/testdata/test.py new file mode 100644 index 00000000..26f26166 --- /dev/null +++ b/stdlib/array/testdata/test.py @@ -0,0 +1,162 @@ +# Copyright 2023 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 array + +print("globals:") +for name in ("typecodes", "array"): + v = getattr(array, name) + print("\narray.%s:\n%s" % (name,repr(v))) + pass + +def assertEqual(x, y): + assert x == y, "got: %s, want: %s" % (repr(x), repr(y)) + +assertEqual(array.typecodes, 'bBuhHiIlLqQfd') + +for i, typ in enumerate(array.typecodes): + print("") + print("typecode '%s'" % (typ,)) + if typ == 'u': + # FIXME(sbinet): implement + print(" SKIP: NotImplemented") + continue + if typ in "bhilqfd": + arr = array.array(typ, [-1, -2, -3, -4]) + if typ in "BHILQ": + arr = array.array(typ, [+1, +2, +3, +4]) + print(" array: %s" % (repr(arr),)) + print(" itemsize: %s" % (arr.itemsize,)) + print(" typecode: %s" % (arr.typecode,)) + print(" len: %s" % (len(arr),)) + print(" arr[0]: %s" % (arr[0],)) + print(" arr[-1]: %s" % (arr[-1],)) + try: + arr[-10] + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + + try: + arr[10] + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + arr[-2] = 33 + print(" arr[-2]: %s" % (arr[-2],)) + + try: + arr[-10] = 2 + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + + if typ in "bhilqfd": + arr.extend([-5,-6]) + if typ in "BHILQ": + arr.extend([5,6]) + print(" array: %s" % (repr(arr),)) + print(" len: %s" % (len(arr),)) + + if typ in "bhilqfd": + arr.append(-7) + if typ in "BHILQ": + arr.append(7) + print(" array: %s" % (repr(arr),)) + print(" len: %s" % (len(arr),)) + + try: + arr.append() + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.append([]) + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.append(1, 2) + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.append(None) + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + + try: + arr.extend() + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.extend(None) + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.extend([1,None]) + print(" ERROR: expected an exception") + except: + print(" caught an exception [ok]") + pass + +print("\n") +print("## testing array.array(...)") +try: + arr = array.array() + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array(b"d") + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("?") + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("dd") + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("d", initializer=[1,2]) + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("d", [1], []) + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("d", 1) + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + arr = array.array("d", ["a","b"]) + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") + +try: + ## FIXME(sbinet): implement it at some point. + arr = array.array("u") + print("ERROR: expected an exception") +except: + print("caught an exception [ok]") diff --git a/stdlib/array/testdata/test_golden.txt b/stdlib/array/testdata/test_golden.txt new file mode 100644 index 00000000..b55665ad --- /dev/null +++ b/stdlib/array/testdata/test_golden.txt @@ -0,0 +1,298 @@ +globals: + +array.typecodes: +'bBuhHiIlLqQfd' + +array.array: + + +typecode 'b' + array: array('b', [-1, -2, -3, -4]) + itemsize: 1 + typecode: b + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('b', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('b', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'B' + array: array('B', [1, 2, 3, 4]) + itemsize: 1 + typecode: B + len: 4 + arr[0]: 1 + arr[-1]: 4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('B', [1, 2, 33, 4, 5, 6]) + len: 6 + array: array('B', [1, 2, 33, 4, 5, 6, 7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'u' + SKIP: NotImplemented + +typecode 'h' + array: array('h', [-1, -2, -3, -4]) + itemsize: 2 + typecode: h + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('h', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('h', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'H' + array: array('H', [1, 2, 3, 4]) + itemsize: 2 + typecode: H + len: 4 + arr[0]: 1 + arr[-1]: 4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('H', [1, 2, 33, 4, 5, 6]) + len: 6 + array: array('H', [1, 2, 33, 4, 5, 6, 7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'i' + array: array('i', [-1, -2, -3, -4]) + itemsize: 2 + typecode: i + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('i', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('i', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'I' + array: array('I', [1, 2, 3, 4]) + itemsize: 2 + typecode: I + len: 4 + arr[0]: 1 + arr[-1]: 4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('I', [1, 2, 33, 4, 5, 6]) + len: 6 + array: array('I', [1, 2, 33, 4, 5, 6, 7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'l' + array: array('l', [-1, -2, -3, -4]) + itemsize: 8 + typecode: l + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('l', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('l', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'L' + array: array('L', [1, 2, 3, 4]) + itemsize: 8 + typecode: L + len: 4 + arr[0]: 1 + arr[-1]: 4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('L', [1, 2, 33, 4, 5, 6]) + len: 6 + array: array('L', [1, 2, 33, 4, 5, 6, 7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'q' + array: array('q', [-1, -2, -3, -4]) + itemsize: 8 + typecode: q + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('q', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('q', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'Q' + array: array('Q', [1, 2, 3, 4]) + itemsize: 8 + typecode: Q + len: 4 + arr[0]: 1 + arr[-1]: 4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('Q', [1, 2, 33, 4, 5, 6]) + len: 6 + array: array('Q', [1, 2, 33, 4, 5, 6, 7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'f' + array: array('f', [-1, -2, -3, -4]) + itemsize: 4 + typecode: f + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('f', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('f', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + +typecode 'd' + array: array('d', [-1, -2, -3, -4]) + itemsize: 8 + typecode: d + len: 4 + arr[0]: -1 + arr[-1]: -4 + caught an exception [ok] + caught an exception [ok] + arr[-2]: 33 + caught an exception [ok] + array: array('d', [-1, -2, 33, -4, -5, -6]) + len: 6 + array: array('d', [-1, -2, 33, -4, -5, -6, -7]) + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + + +## testing array.array(...) +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] +caught an exception [ok] diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index d945c382..7d1fb811 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -18,6 +18,7 @@ import ( "github.com/go-python/gpython/stdlib/marshal" "github.com/go-python/gpython/vm" + _ "github.com/go-python/gpython/stdlib/array" _ "github.com/go-python/gpython/stdlib/binascii" _ "github.com/go-python/gpython/stdlib/builtin" _ "github.com/go-python/gpython/stdlib/glob" From 95c8e39d73c6fb0e1b6e47cefbe5d4790e1d6c0f Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Wed, 29 Nov 2023 10:00:28 +0100 Subject: [PATCH 09/24] stdlib/array: add support for 'u' arrays Signed-off-by: Sebastien Binet --- stdlib/array/array.go | 86 ++++++++++++++++++----- stdlib/array/testdata/test.py | 71 +++++++++++-------- stdlib/array/testdata/test_golden.txt | 98 +++++++++++++++++++++------ 3 files changed, 188 insertions(+), 67 deletions(-) diff --git a/stdlib/array/array.go b/stdlib/array/array.go index f28d05ac..b9e682ae 100644 --- a/stdlib/array/array.go +++ b/stdlib/array/array.go @@ -13,6 +13,11 @@ import ( "github.com/go-python/gpython/py" ) +// FIXME(sbinet): consider creating an "array handler" type for each of the typecodes +// and make the handler a field of the "array" type. +// or make "array" an interface ? + +// array provides efficient manipulation of C-arrays (as Go slices). type array struct { descr byte // typecode of elements esize int // element size in bytes @@ -197,12 +202,12 @@ func array_new(metatype *py.Type, args py.Tuple, kwargs py.StringDict) (py.Objec esize: descr2esize[descr[0]], } - if descr[0] == 'u' { - // FIXME(sbinet) - return nil, py.NotImplementedError - } - switch descr[0] { + case 'u': + var data []rune + arr.data = data + arr.append = arr.appendRune + arr.extend = arr.extendRune case 'b': var data []int8 arr.data = data @@ -300,14 +305,22 @@ func (arr *array) M__repr__() (py.Object, error) { o := new(strings.Builder) o.WriteString("array('" + string(arr.descr) + "'") if data := reflect.ValueOf(arr.data); arr.data != nil && data.Len() > 0 { - o.WriteString(", [") - for i := 0; i < data.Len(); i++ { - if i > 0 { - o.WriteString(", ") + switch arr.descr { + case 'u': + o.WriteString(", '") + o.WriteString(string(arr.data.([]rune))) + o.WriteString("'") + default: + o.WriteString(", [") + for i := 0; i < data.Len(); i++ { + if i > 0 { + o.WriteString(", ") + } + // FIXME(sbinet): we don't get exactly the same display wrt CPython for float32 + fmt.Fprintf(o, "%v", data.Index(i)) } - fmt.Fprintf(o, "%v", data.Index(i)) + o.WriteString("]") } - o.WriteString("]") } o.WriteString(")") return py.String(o.String()), nil @@ -344,8 +357,7 @@ func (arr *array) M__getitem__(k py.Object) (py.Object, error) { case 'B', 'H', 'I', 'L', 'Q': return py.Int(sli.Index(i).Uint()), nil case 'u': - // FIXME(sbinet) - return nil, py.NotImplementedError + return py.String([]rune{rune(sli.Index(i).Int())}), nil case 'f', 'd': return py.Float(sli.Index(i).Float()), nil } @@ -372,14 +384,23 @@ func (arr *array) M__setitem__(k, v py.Object) (py.Object, error) { } switch arr.descr { case 'b', 'h', 'i', 'l', 'q': - vv := v.(py.Int) + vv, ok := v.(py.Int) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "'%s' object cannot be interpreted as an integer", v.Type().Name) + } sli.Index(i).SetInt(int64(vv)) case 'B', 'H', 'I', 'L', 'Q': - vv := v.(py.Int) + vv, ok := v.(py.Int) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "'%s' object cannot be interpreted as an integer", v.Type().Name) + } sli.Index(i).SetUint(uint64(vv)) case 'u': - // FIXME(sbinet) - return nil, py.NotImplementedError + vv, ok := v.(py.Int) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "array item must be unicode character") + } + sli.Index(i).SetInt(int64(vv)) case 'f', 'd': var vv float64 switch v := v.(type) { @@ -401,6 +422,16 @@ func (arr *array) M__setitem__(k, v py.Object) (py.Object, error) { panic("impossible") } +func (arr *array) appendRune(v py.Object) (py.Object, error) { + str, ok := v.(py.String) + if !ok { + return nil, py.ExceptionNewf(py.TypeError, "array item must be unicode character") + } + + arr.data = append(arr.data.([]rune), []rune(str)...) + return py.None, nil +} + func (arr *array) appendI8(v py.Object) (py.Object, error) { vv, err := asInt(v) if err != nil { @@ -491,6 +522,27 @@ func (arr *array) appendF64(v py.Object) (py.Object, error) { return py.None, nil } +func (arr *array) extendRune(arg py.Object) (py.Object, error) { + itr, err := py.Iter(arg) + if err != nil { + return nil, err + } + + nxt := itr.(py.I__next__) + + for { + o, err := nxt.M__next__() + if err == py.StopIteration { + break + } + _, err = arr.appendRune(o) + if err != nil { + return nil, err + } + } + return py.None, nil +} + func (arr *array) extendI8(arg py.Object) (py.Object, error) { itr, err := py.Iter(arg) if err != nil { diff --git a/stdlib/array/testdata/test.py b/stdlib/array/testdata/test.py index 26f26166..906ed898 100644 --- a/stdlib/array/testdata/test.py +++ b/stdlib/array/testdata/test.py @@ -19,14 +19,15 @@ def assertEqual(x, y): print("") print("typecode '%s'" % (typ,)) if typ == 'u': - # FIXME(sbinet): implement - print(" SKIP: NotImplemented") - continue - if typ in "bhilqfd": + arr = array.array(typ, "?世界!") + if typ in "bhilq": arr = array.array(typ, [-1, -2, -3, -4]) if typ in "BHILQ": arr = array.array(typ, [+1, +2, +3, +4]) - print(" array: %s" % (repr(arr),)) + if typ in "fd": + arr = array.array(typ, [-1.0, -2.0, -3.0, -4.0]) + print(" array: %s ## repr" % (repr(arr),)) + print(" array: %s ## str" % (str(arr),)) print(" itemsize: %s" % (arr.itemsize,)) print(" typecode: %s" % (arr.typecode,)) print(" len: %s" % (len(arr),)) @@ -34,21 +35,23 @@ def assertEqual(x, y): print(" arr[-1]: %s" % (arr[-1],)) try: arr[-10] - print(" ERROR: expected an exception") + print(" ERROR1: expected an exception") except: print(" caught an exception [ok]") try: arr[10] - print(" ERROR: expected an exception") + print(" ERROR2: expected an exception") except: print(" caught an exception [ok]") arr[-2] = 33 + if typ in "fd": + arr[-2] = 0.3 print(" arr[-2]: %s" % (arr[-2],)) try: arr[-10] = 2 - print(" ERROR: expected an exception") + print(" ERROR3: expected an exception") except: print(" caught an exception [ok]") @@ -56,6 +59,8 @@ def assertEqual(x, y): arr.extend([-5,-6]) if typ in "BHILQ": arr.extend([5,6]) + if typ == 'u': + arr.extend("he") print(" array: %s" % (repr(arr),)) print(" len: %s" % (len(arr),)) @@ -63,43 +68,56 @@ def assertEqual(x, y): arr.append(-7) if typ in "BHILQ": arr.append(7) + if typ == 'u': + arr.append("l") print(" array: %s" % (repr(arr),)) print(" len: %s" % (len(arr),)) try: arr.append() - print(" ERROR: expected an exception") + print(" ERROR4: expected an exception") except: print(" caught an exception [ok]") try: arr.append([]) - print(" ERROR: expected an exception") + print(" ERROR5: expected an exception") except: print(" caught an exception [ok]") try: arr.append(1, 2) - print(" ERROR: expected an exception") + print(" ERROR6: expected an exception") except: print(" caught an exception [ok]") try: arr.append(None) - print(" ERROR: expected an exception") + print(" ERROR7: expected an exception") except: print(" caught an exception [ok]") try: arr.extend() - print(" ERROR: expected an exception") + print(" ERROR8: expected an exception") except: print(" caught an exception [ok]") try: arr.extend(None) - print(" ERROR: expected an exception") + print(" ERROR9: expected an exception") except: print(" caught an exception [ok]") try: arr.extend([1,None]) - print(" ERROR: expected an exception") + print(" ERROR10: expected an exception") + except: + print(" caught an exception [ok]") + try: + arr.extend(1,None) + print(" ERROR11: expected an exception") + except: + print(" caught an exception [ok]") + + try: + arr[0] = object() + print(" ERROR12: expected an exception") except: print(" caught an exception [ok]") pass @@ -108,55 +126,48 @@ def assertEqual(x, y): print("## testing array.array(...)") try: arr = array.array() - print("ERROR: expected an exception") + print("ERROR1: expected an exception") except: print("caught an exception [ok]") try: arr = array.array(b"d") - print("ERROR: expected an exception") + print("ERROR2: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("?") - print("ERROR: expected an exception") + print("ERROR3: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("dd") - print("ERROR: expected an exception") + print("ERROR4: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("d", initializer=[1,2]) - print("ERROR: expected an exception") + print("ERROR5: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("d", [1], []) - print("ERROR: expected an exception") + print("ERROR6: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("d", 1) - print("ERROR: expected an exception") + print("ERROR7: expected an exception") except: print("caught an exception [ok]") try: arr = array.array("d", ["a","b"]) - print("ERROR: expected an exception") -except: - print("caught an exception [ok]") - -try: - ## FIXME(sbinet): implement it at some point. - arr = array.array("u") - print("ERROR: expected an exception") + print("ERROR8: expected an exception") except: print("caught an exception [ok]") diff --git a/stdlib/array/testdata/test_golden.txt b/stdlib/array/testdata/test_golden.txt index b55665ad..09e9db3d 100644 --- a/stdlib/array/testdata/test_golden.txt +++ b/stdlib/array/testdata/test_golden.txt @@ -7,7 +7,8 @@ array.array: typecode 'b' - array: array('b', [-1, -2, -3, -4]) + array: array('b', [-1, -2, -3, -4]) ## repr + array: array('b', [-1, -2, -3, -4]) ## str itemsize: 1 typecode: b len: 4 @@ -28,9 +29,12 @@ typecode 'b' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'B' - array: array('B', [1, 2, 3, 4]) + array: array('B', [1, 2, 3, 4]) ## repr + array: array('B', [1, 2, 3, 4]) ## str itemsize: 1 typecode: B len: 4 @@ -51,12 +55,38 @@ typecode 'B' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'u' - SKIP: NotImplemented + array: array('u', '?世界!') ## repr + array: array('u', '?世界!') ## str + itemsize: 2 + typecode: u + len: 4 + arr[0]: ? + arr[-1]: ! + caught an exception [ok] + caught an exception [ok] + arr[-2]: ! + caught an exception [ok] + array: array('u', '?世!!he') + len: 6 + array: array('u', '?世!!hel') + len: 7 + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'h' - array: array('h', [-1, -2, -3, -4]) + array: array('h', [-1, -2, -3, -4]) ## repr + array: array('h', [-1, -2, -3, -4]) ## str itemsize: 2 typecode: h len: 4 @@ -77,9 +107,12 @@ typecode 'h' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'H' - array: array('H', [1, 2, 3, 4]) + array: array('H', [1, 2, 3, 4]) ## repr + array: array('H', [1, 2, 3, 4]) ## str itemsize: 2 typecode: H len: 4 @@ -100,9 +133,12 @@ typecode 'H' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'i' - array: array('i', [-1, -2, -3, -4]) + array: array('i', [-1, -2, -3, -4]) ## repr + array: array('i', [-1, -2, -3, -4]) ## str itemsize: 2 typecode: i len: 4 @@ -123,9 +159,12 @@ typecode 'i' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'I' - array: array('I', [1, 2, 3, 4]) + array: array('I', [1, 2, 3, 4]) ## repr + array: array('I', [1, 2, 3, 4]) ## str itemsize: 2 typecode: I len: 4 @@ -146,9 +185,12 @@ typecode 'I' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'l' - array: array('l', [-1, -2, -3, -4]) + array: array('l', [-1, -2, -3, -4]) ## repr + array: array('l', [-1, -2, -3, -4]) ## str itemsize: 8 typecode: l len: 4 @@ -169,9 +211,12 @@ typecode 'l' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'L' - array: array('L', [1, 2, 3, 4]) + array: array('L', [1, 2, 3, 4]) ## repr + array: array('L', [1, 2, 3, 4]) ## str itemsize: 8 typecode: L len: 4 @@ -192,9 +237,12 @@ typecode 'L' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'q' - array: array('q', [-1, -2, -3, -4]) + array: array('q', [-1, -2, -3, -4]) ## repr + array: array('q', [-1, -2, -3, -4]) ## str itemsize: 8 typecode: q len: 4 @@ -215,9 +263,12 @@ typecode 'q' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'Q' - array: array('Q', [1, 2, 3, 4]) + array: array('Q', [1, 2, 3, 4]) ## repr + array: array('Q', [1, 2, 3, 4]) ## str itemsize: 8 typecode: Q len: 4 @@ -238,9 +289,12 @@ typecode 'Q' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'f' - array: array('f', [-1, -2, -3, -4]) + array: array('f', [-1, -2, -3, -4]) ## repr + array: array('f', [-1, -2, -3, -4]) ## str itemsize: 4 typecode: f len: 4 @@ -248,11 +302,11 @@ typecode 'f' arr[-1]: -4 caught an exception [ok] caught an exception [ok] - arr[-2]: 33 + arr[-2]: 0.30000001192092896 caught an exception [ok] - array: array('f', [-1, -2, 33, -4, -5, -6]) + array: array('f', [-1, -2, 0.3, -4, -5, -6]) len: 6 - array: array('f', [-1, -2, 33, -4, -5, -6, -7]) + array: array('f', [-1, -2, 0.3, -4, -5, -6, -7]) len: 7 caught an exception [ok] caught an exception [ok] @@ -261,9 +315,12 @@ typecode 'f' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] typecode 'd' - array: array('d', [-1, -2, -3, -4]) + array: array('d', [-1, -2, -3, -4]) ## repr + array: array('d', [-1, -2, -3, -4]) ## str itemsize: 8 typecode: d len: 4 @@ -271,11 +328,11 @@ typecode 'd' arr[-1]: -4 caught an exception [ok] caught an exception [ok] - arr[-2]: 33 + arr[-2]: 0.3 caught an exception [ok] - array: array('d', [-1, -2, 33, -4, -5, -6]) + array: array('d', [-1, -2, 0.3, -4, -5, -6]) len: 6 - array: array('d', [-1, -2, 33, -4, -5, -6, -7]) + array: array('d', [-1, -2, 0.3, -4, -5, -6, -7]) len: 7 caught an exception [ok] caught an exception [ok] @@ -284,6 +341,8 @@ typecode 'd' caught an exception [ok] caught an exception [ok] caught an exception [ok] + caught an exception [ok] + caught an exception [ok] ## testing array.array(...) @@ -295,4 +354,3 @@ caught an exception [ok] caught an exception [ok] caught an exception [ok] caught an exception [ok] -caught an exception [ok] From 23c0aa29cc22afb51a5985f2b74ff6464675d1e8 Mon Sep 17 00:00:00 2001 From: Natanael dos Santos Feitosa <52074821+natanfeitosa@users.noreply.github.com> Date: Tue, 26 Dec 2023 20:06:07 -0300 Subject: [PATCH 10/24] py: fix automatic addition of __doc__ to dict object Fixes #229. --- py/type.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/type.go b/py/type.go index 509a7628..db31ba61 100644 --- a/py/type.go +++ b/py/type.go @@ -1093,7 +1093,7 @@ func (t *Type) Ready() error { // if the type dictionary doesn't contain a __doc__, set it from // the tp_doc slot. - if _, ok := t.Dict["__doc__"]; ok { + if _, ok := t.Dict["__doc__"]; !ok { if t.Doc != "" { t.Dict["__doc__"] = String(t.Doc) } else { From 149d52cd50c5a375e92a3c6a7274df7469bdcdea Mon Sep 17 00:00:00 2001 From: wdq <105555429+wdq112@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:11:48 +0800 Subject: [PATCH 11/24] py: implement str.lower and str.upper Updates #232 --- py/string.go | 16 ++++++++++++++++ py/string_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ py/tests/string.py | 8 ++++++++ 3 files changed, 70 insertions(+) diff --git a/py/string.go b/py/string.go index a28e6e74..e470c01d 100644 --- a/py/string.go +++ b/py/string.go @@ -218,6 +218,14 @@ replaced.`) return self.(String).LStrip(args) }, 0, "lstrip(chars) -> replace chars from begining of string") + StringType.Dict["upper"] = MustNewMethod("upper", func(self Object, args Tuple, kwargs StringDict) (Object, error) { + return self.(String).Upper() + }, 0, "upper() -> a copy of the string converted to uppercase") + + StringType.Dict["lower"] = MustNewMethod("lower", func(self Object, args Tuple, kwargs StringDict) (Object, error) { + return self.(String).Lower() + }, 0, "lower() -> a copy of the string converted to lowercase") + } // Type of this object @@ -739,6 +747,14 @@ func (s String) RStrip(args Tuple) (Object, error) { return String(strings.TrimRightFunc(string(s), f)), nil } +func (s String) Upper() (Object, error) { + return String(strings.ToUpper(string(s))), nil +} + +func (s String) Lower() (Object, error) { + return String(strings.ToLower(string(s))), nil +} + // Check stringerface is satisfied var ( _ richComparison = String("") diff --git a/py/string_test.go b/py/string_test.go index 7f6e0c34..053cd781 100644 --- a/py/string_test.go +++ b/py/string_test.go @@ -98,3 +98,49 @@ func TestStringFind(t *testing.T) { }) } } + +func TestStringUpper(t *testing.T) { + tests := []struct { + name string + s String + want Object + }{{ + name: "abc", + s: String("abc"), + want: String("ABC")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.Upper() + if err != nil { + t.Fatalf("Upper() error = %v", err) + } + if got.(String) != tt.want.(String) { + t.Fatalf("Upper() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStringLower(t *testing.T) { + tests := []struct { + name string + s String + want Object + }{{ + name: "ABC", + s: String("ABC"), + want: String("abc")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.Lower() + if err != nil { + t.Fatalf("Lower() error = %v", err) + } + if got.(String) != tt.want.(String) { + t.Fatalf("Lower() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/py/tests/string.py b/py/tests/string.py index 8af36ca6..f2ad6e9b 100644 --- a/py/tests/string.py +++ b/py/tests/string.py @@ -897,6 +897,14 @@ def index(s, i): assert a.lstrip("a ") == "bada a" assert a.strip("a ") == "bad" +doc="upper" +a = "abc" +assert a.upper() == "ABC" + +doc="lower" +a = "ABC" +assert a.lower() == "abc" + class Index: def __index__(self): return 1 From 79bb9256ae58ef20f65fd6909672a4c7bd008525 Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Fri, 7 Mar 2025 11:07:32 +0100 Subject: [PATCH 12/24] ci: update GitHub actions Signed-off-by: Sebastien Binet --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49657da8..86a5f633 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@v5 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@v4 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 From a0c052992576917aac53235396b525632e16f9ec Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 28 Jun 2025 00:18:04 +0900 Subject: [PATCH 13/24] py: implement str.join Fixes #232 --- py/string.go | 27 +++++++++++++++++++++++++++ py/tests/string.py | 9 +++++++++ 2 files changed, 36 insertions(+) diff --git a/py/string.go b/py/string.go index e470c01d..a987a11a 100644 --- a/py/string.go +++ b/py/string.go @@ -226,6 +226,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 @@ -755,6 +758,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/string.py b/py/tests/string.py index f2ad6e9b..d1d16cfd 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 From cff1e72e91fb927255adcdbb6d2fbc19a29ed052 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 28 Jun 2025 00:18:42 +0900 Subject: [PATCH 14/24] py: remove dead code --- py/int.go | 6 ------ 1 file changed, 6 deletions(-) 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 From 53b7b8e01d998914a7f4179e3e051274f94b8990 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 28 Jun 2025 18:11:55 +0900 Subject: [PATCH 15/24] py: fix import on Windows --- py/import.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py/import.go b/py/import.go index 709a4468..5ca6598a 100644 --- a/py/import.go +++ b/py/import.go @@ -7,7 +7,7 @@ package py import ( - "path" + "path/filepath" "strings" ) @@ -101,14 +101,14 @@ 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))) + opts.CurDir = filepath.Dir(string(fromFile.(String))) } module, err := RunFile(ctx, srcPathname, opts, name) From 43d2d6169f9e96cfba63434c4d5086e462a351e0 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 28 Jun 2025 18:52:37 +0900 Subject: [PATCH 16/24] py: fix exception repr --- py/exception.go | 11 +++++++++-- vm/tests/exceptions.py | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) 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/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" From 285aad11f2d09742709dbd5aca1b73a216bea090 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 30 Jun 2025 22:27:50 +0900 Subject: [PATCH 17/24] py: Implement str.count --- py/string.go | 41 +++++++++++++++++++++++++++++++++++++++++ py/tests/string.py | 9 +++++++++ 2 files changed, 50 insertions(+) diff --git a/py/string.go b/py/string.go index a987a11a..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(...) @@ -612,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 diff --git a/py/tests/string.py b/py/tests/string.py index d1d16cfd..d71e8645 100644 --- a/py/tests/string.py +++ b/py/tests/string.py @@ -944,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" From 8b4dffbc7c4082793da9a0ba6c4de0e59281c458 Mon Sep 17 00:00:00 2001 From: AN Long Date: Fri, 4 Jul 2025 03:50:12 +0900 Subject: [PATCH 18/24] all: handle SystemExit --- main.go | 42 ++++++++++++++++++++++++++++++++++++++---- repl/cli/cli.go | 8 ++++++-- repl/repl.go | 14 +++++++++----- stdlib/sys/sys.go | 6 +++++- 4 files changed, 58 insertions(+), 12 deletions(-) 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/repl/cli/cli.go b/repl/cli/cli.go index 6648094a..6f7e3966 100644 --- a/repl/cli/cli.go +++ b/repl/cli/cli.go @@ -117,7 +117,7 @@ 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) } @@ -144,6 +144,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/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 From bbe47265af95f921cb863781f1db38d8c51b17f2 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 5 Jul 2025 00:04:12 +0900 Subject: [PATCH 19/24] pytest: ensure expected string's line sep is \n --- main_test.go | 1 + pytest/pytest.go | 1 + 2 files changed, 2 insertions(+) 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/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" From e20a7a44caad86725015a2a3c756197b05c8ae34 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 13 Oct 2025 15:10:51 +0200 Subject: [PATCH 20/24] ci: update GitHub actions --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a5f633..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@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} @@ -34,7 +34,7 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 From 759eb83ea8f2ddcfc846bc6b539cdbc4bc14e1df Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 3 Feb 2026 00:56:59 +0900 Subject: [PATCH 21/24] py: implement builtin vars --- py/method.go | 1 + stdlib/builtin/builtin.go | 7 ++++++- stdlib/builtin/tests/builtin.py | 33 +++++++++++++++++++++++++++++++++ vm/eval.go | 17 +++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) 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/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 290cb939..96715321 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -63,7 +63,7 @@ func init() { 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, @@ -1189,6 +1189,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..fc6bcdf8 100644 --- a/stdlib/builtin/tests/builtin.py +++ b/stdlib/builtin/tests/builtin.py @@ -107,6 +107,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] 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) } From 530fdbdddbf3320321ca1b6630a061adbf79c66c Mon Sep 17 00:00:00 2001 From: AN Long Date: Wed, 25 Feb 2026 18:11:23 +0900 Subject: [PATCH 22/24] py,repl/cli,stdlib/builtin: implement input - py: implement input - add missing getline method on py.File - add parameters to readfile and add test --- py/file.go | 41 ++++++++++++++++ py/run.go | 5 ++ py/tests/file.py | 6 +++ repl/cli/cli.go | 8 ++++ stdlib/builtin/builtin.go | 83 ++++++++++++++++++++++++++++++++- stdlib/builtin/tests/builtin.py | 12 +++++ 6 files changed, 154 insertions(+), 1 deletion(-) 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/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 96715321..870813ce 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" @@ -44,7 +47,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), @@ -1181,6 +1184,84 @@ 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 locals_doc = `locals() -> dictionary Update and return a dictionary containing the current scope's local variables.` diff --git a/stdlib/builtin/tests/builtin.py b/stdlib/builtin/tests/builtin.py index fc6bcdf8..77a831a2 100644 --- a/stdlib/builtin/tests/builtin.py +++ b/stdlib/builtin/tests/builtin.py @@ -502,4 +502,16 @@ class C: pass assert lib.libvar == 43 assert lib.libclass().method() == 44 +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" From c324a9a85dc022b9a765125767c338fd061b6f39 Mon Sep 17 00:00:00 2001 From: AN Long Date: Fri, 27 Feb 2026 18:34:12 +0900 Subject: [PATCH 23/24] py: harden __import__ argument handling Fixes #204. Co-authored-by: Sebastien Binet --- py/import.go | 38 +++++++++++++++++++++++++++++---- stdlib/builtin/tests/builtin.py | 20 +++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) 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/stdlib/builtin/tests/builtin.py b/stdlib/builtin/tests/builtin.py index 77a831a2..36cb8ff2 100644 --- a/stdlib/builtin/tests/builtin.py +++ b/stdlib/builtin/tests/builtin.py @@ -501,6 +501,26 @@ 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 From 444ae5ed29cfbc767b6592a3f27d5d05ca24b609 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 2 Mar 2026 22:02:41 +0900 Subject: [PATCH 24/24] stdlib/builtin: add quit and exit In CPython's REPL, there are other ways to `quit` or `exit` the REPL without calling a function (by simply typing `quit` or `exit`). These names are injected via `site.py`. Since GPython does not have a site.py, this REPL usage is not implemented. Fixes #140. --- stdlib/builtin/builtin.go | 31 +++++++++++++++++++++++++++++++ stdlib/builtin/tests/builtin.py | 9 +++++++++ 2 files changed, 40 insertions(+) diff --git a/stdlib/builtin/builtin.go b/stdlib/builtin/builtin.go index 870813ce..dae29324 100644 --- a/stdlib/builtin/builtin.go +++ b/stdlib/builtin/builtin.go @@ -40,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), @@ -61,6 +62,7 @@ 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), @@ -1262,6 +1264,35 @@ func builtin_input(self py.Object, args py.Tuple) (py.Object, error) { 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.` diff --git a/stdlib/builtin/tests/builtin.py b/stdlib/builtin/tests/builtin.py index 36cb8ff2..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):