Commits

Greg Ward  committed dcb6742

add barebones plugins package, just enough to embed Python

All it can do right now is run a bit of Python code: there's no access
to data back-and-forth, i.e. Fubsy cannot access Python code nor can
Python code access Fubsy internals. But hey, it's a start!

  • Participants
  • Parent commits a2620d4

Comments (0)

Files changed (9)

     #   tagflag = "-tags='${buildtags.join(\' \')}'"
     # but there's a bit more work to do before Fubsy supports
     # that syntax, so for now we have to put up with
-    tagflag = "-tags=kyotodb"
+    tagflag = "-tags=kyotodb python"
     # ...and you'll just have to edit it manually to modify tags
 
     # some tools needed to build/test
     ActionNode("test/fubsy/log"): <$src/log/*.go> {
         "go test $tagflag fubsy/log"
     }
+    ActionNode("test/fubsy/plugins"): <$src/plugins/*.go> {
+        "go test $tagflag fubsy/plugins"
+    }
     ActionNode("test/fubsy/runtime"): <$src/runtime/*.go> {
         "go test $tagflag fubsy/runtime"
     }

File src/fubsy/dsl/ast.go

 	return self.eof
 }
 
-func (self *ASTRoot) ListPlugins() [][]string {
+func (self *ASTRoot) FindImports() [][]string {
 	result := make([][]string, 0)
 	for _, node_ := range self.children {
 		if node, ok := node_.(*ASTImport); ok {
 	return result
 }
 
+func (self *ASTRoot) FindInlinePlugins() []*ASTInline {
+	var result []*ASTInline
+	for _, node := range self.children {
+		if node, ok := node.(*ASTInline); ok {
+			result = append(result, node)
+		}
+	}
+	return result
+}
+
 func (self *ASTRoot) FindPhase(name string) *ASTPhase {
 	for _, node_ := range self.children {
 		if node, ok := node_.(*ASTPhase); ok && node.name == name {
 	return false
 }
 
+func (self *ASTInline) Language() string {
+	return self.lang
+}
+
+func (self *ASTInline) Content() string {
+	return self.content
+}
+
 func NewASTPhase(name string, block *ASTBlock, location ...Locatable) *ASTPhase {
 	return &ASTPhase{
 		astbase:  astLocation(location),

File src/fubsy/dsl/ast_test.go

 	}
 }
 
-func Test_ASTRoot_ListPlugins(t *testing.T) {
+func Test_ASTRoot_FindImports(t *testing.T) {
 	root := &ASTRoot{
 		children: []ASTNode{
 			&ASTPhase{},
 			&ASTImport{plugin: []string{"meep", "beep"}},
 		}}
 	expect := [][]string{{"ding"}, {"meep", "beep"}}
-	actual := root.ListPlugins()
+	actual := root.FindImports()
 	assert.True(t, reflect.DeepEqual(expect, actual),
 		"expected\n%v\nbut got\n%v", expect, actual)
 }

File src/fubsy/plugins/dummypython.go

+// Copyright © 2013, Greg Ward. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can
+// be found in the LICENSE.txt file.
+
+// +build !python
+
+package plugins
+
+import (
+	"fubsy/types"
+)
+
+// Dummy version of PythonPlugin, used when the build host does not
+// have Python.h etc.
+
+type PythonPlugin struct {
+}
+
+func NewPythonPlugin() (MetaPlugin, error) {
+	return nil, NotAvailableError{"Python"}
+}
+
+func (self PythonPlugin) Run(content string) (types.ValueMap, error) {
+	panic("dummy implementation")
+}
+
+func (self PythonPlugin) Close() {
+	panic("dummy implementation")
+}

File src/fubsy/plugins/meta.go

+// Copyright © 2013, Greg Ward. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can
+// be found in the LICENSE.txt file.
+
+package plugins
+
+import (
+	"errors"
+	"fmt"
+
+	"fubsy/types"
+)
+
+// Finding and using meta-plugins, i.e. plugins that interface with
+// other languages.
+
+type MetaPlugin interface {
+	// Execute the code in content. Return a map of stuff defined by
+	// the code, e.g. functions the user can call from Fubsy code.
+	Run(content string) (types.ValueMap, error)
+
+	// Release any resources held by this metaplugin
+	Close()
+}
+
+// for use by dummy MetaPlugin implementations
+type NotAvailableError struct {
+	lang string
+}
+
+type factoryFunc func() (MetaPlugin, error)
+
+var metaFactory map[string]factoryFunc
+var metaCache map[string]MetaPlugin
+
+func init() {
+	// this just declares which languages we support -- don't actually
+	// create the required metaplugins until we know they are needed
+	metaFactory = make(map[string]factoryFunc)
+	metaFactory["python2"] = NewPythonPlugin
+
+	metaCache = make(map[string]MetaPlugin)
+}
+
+func LoadMetaPlugin(language string) (MetaPlugin, error) {
+	meta, ok := metaCache[language]
+	if ok && meta != nil {
+		return meta, nil
+	}
+
+	factory := metaFactory[language]
+	if factory == nil {
+		return nil, errors.New("unsupported language for inline plugins: " + language)
+	}
+
+	meta, err := factory()
+	if err != nil {
+		return nil, err
+	}
+	metaCache[language] = meta
+	return meta, nil
+}
+
+// Close() all metaplugins that have been created in this process and
+// empty the cache of metaplugins.
+func CloseAll() {
+	for lang, meta := range metaCache {
+		meta.Close()
+		delete(metaCache, lang)
+	}
+}
+
+func (err NotAvailableError) Error() string {
+	return fmt.Sprintf(
+		"cannot run plugin: support for %s not available",
+		err.lang)
+}

File src/fubsy/plugins/python.go

+// Copyright © 2013, Greg Ward. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can
+// be found in the LICENSE.txt file.
+
+// +build python
+
+package plugins
+
+import (
+	"errors"
+
+	py "github.com/sbinet/go-python/pkg/python"
+
+	"fubsy/types"
+)
+
+type PythonPlugin struct {
+}
+
+func NewPythonPlugin() (MetaPlugin, error) {
+	py.Initialize()
+	return PythonPlugin{}, nil
+}
+
+func (self PythonPlugin) Run(content string) (types.ValueMap, error) {
+	result := py.PyRun_SimpleString(content)
+	if result < 0 {
+		// there's no way to get the traceback info... but it doesn't
+		// really matter, since Python prints the traceback to stderr
+		return nil, errors.New("inline Python plugin raised an exception")
+	}
+	return nil, nil
+}
+
+func (self PythonPlugin) Close() {
+	// argh, go-python doesn't wrap this
+	//py.Py_Finalize()
+}

File src/fubsy/plugins/python_test.go

+// Copyright © 2013, Greg Ward. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can
+// be found in the LICENSE.txt file.
+
+// +build python
+
+package plugins
+
+import (
+	"testing"
+
+	"github.com/stretchrcom/testify/assert"
+)
+
+func Test_PythonPlugin_Run(t *testing.T) {
+	pp, err := NewPythonPlugin()
+	assert.Nil(t, err)
+	values, err := pp.Run(`
+foo = ["abc", "def"]
+bar = "!".join(foo)`)
+
+	// PythonPlugin doesn't yet harvest Python values, so we cannot do
+	// anything to test values
+	_ = values
+	assert.Nil(t, err)
+
+	values, err = pp.Run("foo = 1/0")
+	assert.Equal(t, "inline Python plugin raised an exception", err.Error())
+}

File src/fubsy/runtime/runtime.go

 	"fubsy/db"
 	"fubsy/dsl"
 	"fubsy/log"
+	"fubsy/plugins"
 	"fubsy/types"
 )
 
 
 func (self *Runtime) RunScript() []error {
 	var errors []error
-	for _, plugin := range self.ast.ListPlugins() {
+	for _, plugin := range self.ast.FindImports() {
 		log.Debug(log.PLUGINS, "loading plugin '%s'", strings.Join(plugin, "."))
 	}
 
+	errors = self.runInlinePlugins()
+	if len(errors) > 0 {
+		return errors
+	}
+
 	errors = self.runMainPhase()
 	if len(errors) > 0 {
 		return errors
 	return errors
 }
 
+func (self *Runtime) runInlinePlugins() []error {
+	var errs []error
+	var err error
+	var meta plugins.MetaPlugin
+
+	inlines := self.ast.FindInlinePlugins()
+	ns := self.stack.Inner()
+	for _, inline := range inlines {
+		meta, err = plugins.LoadMetaPlugin(inline.Language())
+		if err != nil {
+			errs = append(errs, MakeLocationError(inline, err))
+			continue
+		}
+		log.Debug(log.PLUGINS, "running %s inline plugin", inline.Language())
+		values, err := meta.Run(inline.Content())
+		if err != nil {
+			errs = append(errs, MakeLocationError(inline, err))
+		}
+		for name, val := range values {
+			// warn on shadowing?
+			ns.Assign(name, val)
+		}
+	}
+	return errs
+}
+
 // Run all the statements in the main phase of this build script.
 // Update self with the results: variable assignments, build rules,
 // etc. Most importantly, on return self.dag will contain the

File src/fubsy/types/namespace.go

 	return ValueStack(ns)
 }
 
+// return the innermost namespace of this stack; panic if the stack is empty
+func (self *ValueStack) Inner() Namespace {
+	stack := ([]Namespace)(*self)
+	return stack[len(stack)-1]
+}
+
 func (self *ValueStack) Push(ns Namespace) {
 	*self = append(*self, ns)
 }