Repository: google/starlark-go
Pull request #452 (2023-01-17 21:11:38)

Added a ParamDefault() method to Function that returns parameter default values.
#450

vtsao: @adonovan not sure where the best place is to add a test for this? It doesn't look like any of the other `starlark.Function` methods have tests.
adonovan: > @adonovan not sure where the best place is to add a test for this? It doesn't look like any of the other `starlark.Function` methods have tests.

Yeah, there isn't much there yet. See the TODO in testdata/function.star:
```
# TODO(adonovan):
# - add some introspection functions for looking at function values
#   and test that functions have correct position, free vars, names of locals, etc.
```

If you're feeling diligent, you could add a built-in function following the paradigm of hasFields or struct that returns a dict of function attributes (e.g. name, pos, defaults) and then write assertions in Starlark. But if you don't feel like it that's fine too.
vtsao: > If you're feeling diligent, you could add a built-in function following the paradigm of hasFields or struct that returns a dict of function attributes (e.g. name, pos, defaults) and then write assertions in Starlark. But if you don't feel like it that's fine too.

I added a test in `value_test.go` for this that I think is more direct as it tests `ParamDefault()` directly. Hope this is okay, even though it's a bit different than the other tests.

Patch:
diff --git a/go.mod b/go.mod
index 99a5585..719cff6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
 module go.starlark.net
 
-go 1.13
+go 1.16
 
 require (
 	github.com/chzyer/logex v1.1.10 // indirect
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
 	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
-	github.com/google/go-cmp v0.5.1 // indirect
+	github.com/google/go-cmp v0.5.1
 	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
diff --git a/starlark/value.go b/starlark/value.go
index b51d4ca..e63c5d4 100644
--- a/starlark/value.go
+++ b/starlark/value.go
@@ -711,6 +711,34 @@ func (fn *Function) Param(i int) (string, syntax.Position) {
 	id := fn.funcode.Locals[i]
 	return id.Name, id.Pos
 }
+
+// ParamDefault returns the default value of the specified parameter
+// (0 <= i < NumParams()), or nil if the parameter is not optional.
+func (fn *Function) ParamDefault(i int) Value {
+	if i < 0 || i >= fn.NumParams() {
+		panic(i)
+	}
+
+	// fn.defaults omits all required params up to the first optional param. It
+	// also does not include *args or **kwargs at the end.
+	firstOptIdx := fn.NumParams() - len(fn.defaults)
+	if fn.HasVarargs() {
+		firstOptIdx--
+	}
+	if fn.HasKwargs() {
+		firstOptIdx--
+	}
+	if i < firstOptIdx || i >= firstOptIdx+len(fn.defaults) {
+		return nil
+	}
+
+	dflt := fn.defaults[i-firstOptIdx]
+	if _, ok := dflt.(mandatory); ok {
+		return nil
+	}
+	return dflt
+}
+
 func (fn *Function) HasVarargs() bool { return fn.funcode.HasVarargs }
 func (fn *Function) HasKwargs() bool  { return fn.funcode.HasKwargs }
 


Test patch:
diff --git a/starlark/profile_test.go b/starlark/profile_test.go
index 2781833..515d7d4 100644
--- a/starlark/profile_test.go
+++ b/starlark/profile_test.go
@@ -31,10 +31,10 @@ func TestProfile(t *testing.T) {
 
 	const src = `
 def fibonacci(n):
-	res = list(range(n))
-	for i in res[2:]:
-		res[i] = res[i-2] + res[i-1]
-	return res
+	x, y = 1, 1
+	for i in range(n):
+		x, y = y, x+y
+	return y
 
 fibonacci(100000)
 `
diff --git a/starlark/testdata/function_param.star b/starlark/testdata/function_param.star
new file mode 100644
index 0000000..6c94961
--- /dev/null
+++ b/starlark/testdata/function_param.star
@@ -0,0 +1,10 @@
+# Functions used in starlark_test.TestParamDefault().
+
+def all_required(a, b, c): pass
+def all_opt(a="a", b=None, c=""): pass
+def mix_required_opt(a, b, c="c", d="d"): pass
+def with_varargs(a, b="b", *args): pass
+def with_varargs_kwonly(a, b="b", *args, c="c", d): pass
+def with_kwonly(a, b="b", *, c="c", d): pass
+def with_kwargs(a, b="b", c="c", **kwargs): pass
+def with_varargs_kwonly_kwargs(a, b="b", *args, c="c", d, e="e", **kwargs): pass
diff --git a/starlark/value_test.go b/starlark/value_test.go
index 6420a95..25d6003 100644
--- a/starlark/value_test.go
+++ b/starlark/value_test.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"go.starlark.net/starlark"
 )
 
@@ -44,3 +45,113 @@ func TestListAppend(t *testing.T) {
 		t.Errorf("failed list.Append() got: %+v, want: hello", res)
 	}
 }
+
+func TestParamDefault(t *testing.T) {
+	tests := []struct {
+		desc         string
+		starFn       string
+		wantDefaults []starlark.Value
+	}{
+		{
+			desc:         "function with all required params",
+			starFn:       "all_required",
+			wantDefaults: []starlark.Value{nil, nil, nil},
+		},
+		{
+			desc:   "function with all optional params",
+			starFn: "all_opt",
+			wantDefaults: []starlark.Value{
+				starlark.String("a"),
+				starlark.None,
+				starlark.String(""),
+			},
+		},
+		{
+			desc:   "function with required and optional params",
+			starFn: "mix_required_opt",
+			wantDefaults: []starlark.Value{
+				nil,
+				nil,
+				starlark.String("c"),
+				starlark.String("d"),
+			},
+		},
+		{
+			desc:   "function with required, optional, and varargs params",
+			starFn: "with_varargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, varargs, and keyword-only params",
+			starFn: "with_varargs_kwonly",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, and keyword-only params",
+			starFn: "with_kwonly",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, and kwargs params",
+			starFn: "with_kwargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, varargs, kw-only, and kwargs params",
+			starFn: "with_varargs_kwonly_kwargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+				starlark.String("e"),
+				nil,
+				nil,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			thread := &starlark.Thread{}
+			filename := "testdata/function_param.star"
+			globals, err := starlark.ExecFile(thread, filename, nil, nil)
+			if err != nil {
+				t.Fatalf("ExecFile(%v, %q) failed: %v", thread, filename, err)
+			}
+
+			fn, ok := globals[tt.starFn].(*starlark.Function)
+			if !ok {
+				t.Fatalf("value %v is not a Starlark function", globals[tt.starFn])
+			}
+
+			var paramDefaults []starlark.Value
+			for i := 0; i < fn.NumParams(); i++ {
+				paramDefaults = append(paramDefaults, fn.ParamDefault(i))
+			}
+			if diff := cmp.Diff(tt.wantDefaults, paramDefaults); diff != "" {
+				t.Errorf("param defaults got diff (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/starlarktest/assert.go b/starlarktest/assert.star
similarity index 88%
rename from starlarktest/assert.go
rename to starlarktest/assert.star
index f18719c..c6e480f 100644
--- a/starlarktest/assert.go
+++ b/starlarktest/assert.star
@@ -1,9 +1,3 @@
-package starlarktest
-
-// assertStar contains the logical data file assert.star.
-// TODO(adonovan): move it back into an actual file,
-// when fs.Embed is more than two releases old.
-const assertStar = `
 # Predeclared built-ins for this module:
 #
 # error(msg): report an error in Go's test framework without halting execution.
@@ -55,4 +49,3 @@ assert = module(
     contains = _contains,
     fails = _fails,
 )
-`
diff --git a/starlarktest/starlarktest.go b/starlarktest/starlarktest.go
index afd1e90..3a19f04 100644
--- a/starlarktest/starlarktest.go
+++ b/starlarktest/starlarktest.go
@@ -13,6 +13,7 @@
 package starlarktest // import "go.starlark.net/starlarktest"
 
 import (
+	_ "embed"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -50,9 +51,11 @@ func GetReporter(thread *starlark.Thread) Reporter {
 }
 
 var (
-	once      sync.Once
-	assert    starlark.StringDict
-	assertErr error
+	once   sync.Once
+	assert starlark.StringDict
+	//go:embed assert.star
+	assertFileSrc string
+	assertErr     error
 )
 
 // LoadAssertModule loads the assert module.
@@ -66,10 +69,8 @@ func LoadAssertModule() (starlark.StringDict, error) {
 			"module":  starlark.NewBuiltin("module", starlarkstruct.MakeModule),
 			"_freeze": starlark.NewBuiltin("freeze", freeze),
 		}
-		// TODO(adonovan): embed the file using embed.FS when we can rely on go1.16,
-		// and make the apparent filename reference that file.
 		thread := new(starlark.Thread)
-		assert, assertErr = starlark.ExecFile(thread, "builtins/assert.star", assertStar, predeclared)
+		assert, assertErr = starlark.ExecFile(thread, "assert.star", assertFileSrc, predeclared)
 	})
 	return assert, assertErr
 }