diff --git a/src/Bonsai.Scripting.Python/Args.cs b/src/Bonsai.Scripting.Python/Args.cs
new file mode 100644
index 0000000..81661ab
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/Args.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Reflection;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that takes a sequence of python objects and converts them to a type of PyTuple to pass as arguments to a function.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class Args
+ {
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ if (!(obj is ITuple || obj is IList || obj is Array))
+ {
+ if (obj is Pythonnet.PyObject pyObj)
+ {
+ return new Pythonnet.PyTuple(new Pythonnet.PyObject[] {pyObj});
+ }
+
+ return new Pythonnet.PyTuple(new Pythonnet.PyObject[] {Pythonnet.PyObject.FromManagedObject(obj)});
+ }
+
+ PropertyInfo[] properties = obj.GetType().GetProperties();
+ Pythonnet.PyObject[] pyObjects = new Pythonnet.PyObject[properties.Length];
+
+ for (int i = 0; i < properties.Length; i++)
+ {
+ object value = properties[i].GetValue(obj, null);
+
+ if (!(value is Pythonnet.PyObject))
+ {
+ throw new ArgumentException($"All elements of the tuple must be of type PyObject. Instead, found {value.GetType()} for Item{i+1}.");
+ }
+
+ pyObjects[i] = (Pythonnet.PyObject)value;
+ }
+ return new Pythonnet.PyTuple(pyObjects);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/EnvironmentConfig.cs b/src/Bonsai.Scripting.Python/EnvironmentConfig.cs
index 9cadcb7..c1107a3 100644
--- a/src/Bonsai.Scripting.Python/EnvironmentConfig.cs
+++ b/src/Bonsai.Scripting.Python/EnvironmentConfig.cs
@@ -26,17 +26,14 @@ public EnvironmentConfig(string pythonHome, string pythonVersion)
public static EnvironmentConfig FromConfigFile(string configFileName)
{
- var config = new EnvironmentConfig();
- config.Path = System.IO.Path.GetDirectoryName(configFileName);
+ var config = new EnvironmentConfig
+ {
+ Path = System.IO.Path.GetDirectoryName(configFileName)
+ };
using var configReader = new StreamReader(File.OpenRead(configFileName));
while (!configReader.EndOfStream)
{
var line = configReader.ReadLine();
- static string GetConfigValue(string line)
- {
- var parts = line.Split('=');
- return parts.Length > 1 ? parts[1].Trim() : string.Empty;
- }
if (line.StartsWith("home"))
{
@@ -59,5 +56,11 @@ static string GetConfigValue(string line)
return config;
}
+
+ private static string GetConfigValue(string line)
+ {
+ var parts = line.Split('=');
+ return parts.Length > 1 ? parts[1].Trim() : string.Empty;
+ }
}
}
diff --git a/src/Bonsai.Scripting.Python/GetAttr.cs b/src/Bonsai.Scripting.Python/GetAttr.cs
new file mode 100644
index 0000000..c6a33ff
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/GetAttr.cs
@@ -0,0 +1,40 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that gets an attribute from a python object if the attribute exists.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class GetAttr
+ {
+ [Description("The name of the attribute to get.")]
+ public string Attribute { get; set; }
+
+ public IObservable Process(IObservable source)
+ {
+ if (string.IsNullOrEmpty(Attribute))
+ {
+ throw new Exception("Attribute cannot be null or empty.");
+ }
+ return source.Select(obj =>
+ {
+ using (Py.GIL())
+ {
+ if (!obj.HasAttr(Attribute))
+ {
+ throw new Exception($"PyObject does not have attribute {Attribute}.");
+ }
+ return obj.GetAttr(Attribute);
+ }
+ });
+ }
+ }
+}
diff --git a/src/Bonsai.Scripting.Python/GetItem.cs b/src/Bonsai.Scripting.Python/GetItem.cs
new file mode 100644
index 0000000..9e47fa3
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/GetItem.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that gets an item from a generic python iterable using either a numbered index, or a string key.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class GetItem
+ {
+ [Description("The index to get.")]
+ public int? Index { get; set; } = null;
+
+ [Description("The key to get.")]
+ public string Key { get; set; } = null;
+
+ public IObservable Process(IObservable source)
+ {
+ if (!(Index.HasValue ^ !string.IsNullOrEmpty(Key)))
+ {
+ throw new Exception("Either an index or a key must be provided.");
+ }
+
+ return source.Select(obj =>
+ {
+ using (Py.GIL())
+ {
+ if (!obj.IsIterable())
+ {
+ throw new Exception($"PyObject is not iterable.");
+ }
+ return Index != null ? obj.GetItem(Index.Value) : obj.GetItem(Key);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/GetPythonType.cs b/src/Bonsai.Scripting.Python/GetPythonType.cs
new file mode 100644
index 0000000..8bbd3b4
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/GetPythonType.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Reactive.Linq;
+using Python.Runtime;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that gets the type of a python object. Equivalent to calling type(obj) in python.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class GetPythonType
+ {
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Py.GIL())
+ {
+ return obj.GetPythonType();
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/Import.cs b/src/Bonsai.Scripting.Python/Import.cs
new file mode 100644
index 0000000..4de6290
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/Import.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that will import the name of a package as a user-defined named package into the input module.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class Import
+ {
+ [Description("The name of the package.")]
+ public string Package { get; set; }
+
+ [Description("The \"as name\" of the package. For example, in \"import numpy as np\", np is the as name.")]
+ public string AsName { get; set; }
+
+ public IObservable Process(IObservable source)
+ {
+ if (string.IsNullOrEmpty(Package))
+ {
+ throw new ArgumentException(nameof(Package), "A package must be specified.");
+ }
+ return source.Select(module =>
+ {
+ using (Py.GIL())
+ {
+ if (!string.IsNullOrEmpty(AsName))
+ {
+ return module.Import(Package, AsName);
+ }
+
+ if (!Package.Contains('.'))
+ {
+ return module.Import(Package);
+ }
+
+ var packages = Package.Split('.');
+ var packagePath = string.Empty;
+
+ PyObject obj = null;
+ for (int i = 0; i < packages.Length; i++)
+ {
+ packagePath = string.IsNullOrEmpty(packagePath) ? packages[i] : $"{packagePath}.{packages[i]}";
+ obj = module.Import(packagePath);
+ }
+
+ return obj;
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/Invoke.cs b/src/Bonsai.Scripting.Python/Invoke.cs
new file mode 100644
index 0000000..8fba858
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/Invoke.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that will invoke a callable object and pass the given arguments to the call. The input can either be the callable object itself, or can be an object which has a named callable attribute.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class Invoke
+ {
+ [Description("The name of the callable object to invoke.")]
+ public string Callable { get; set; }
+
+ [XmlIgnore]
+ [Description("The args to pass to the callable.")]
+ public Pythonnet.PyTuple Args { get; set; } = null;
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ var callable = string.IsNullOrEmpty(Callable) ? obj : obj.GetAttr(Callable);
+ if (!callable.IsCallable())
+ {
+ throw new Exception($"Cannot invoke callable: {callable} because it is not callable.");
+ }
+ var args = Args == null ? new Pythonnet.PyTuple() : Args;
+ return callable.Invoke(args);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/InvokeMethod.cs b/src/Bonsai.Scripting.Python/InvokeMethod.cs
new file mode 100644
index 0000000..aaf4eb4
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/InvokeMethod.cs
@@ -0,0 +1,43 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that will invoke a method of the object and pass the given arguments to the method.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class InvokeMethod
+ {
+ [Description("The name of the method to invoke.")]
+ public string Method { get; set; }
+
+ [XmlIgnore]
+ [Description("The args to pass to the callable.")]
+ public Pythonnet.PyTuple Args { get; set; } = null;
+
+ public IObservable Process(IObservable source)
+ {
+ if (string.IsNullOrEmpty(Method))
+ {
+ throw new Exception("Method name cannot be null or empty.");
+ }
+
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ var args = Args == null ? new Pythonnet.PyTuple() : Args;
+ return obj.InvokeMethod(Method, args);
+ }
+ });
+ }
+
+ }
+}
diff --git a/src/Bonsai.Scripting.Python/PyFloat.cs b/src/Bonsai.Scripting.Python/PyFloat.cs
new file mode 100644
index 0000000..6a7a456
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/PyFloat.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents the creation of a float python data type.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Source)]
+ public class PyFloat
+ {
+
+ [Description("The value of the float.")]
+ public double Value { get; set; }
+
+ public IObservable Process()
+ {
+ return Observable.Defer(() =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return Observable.Return(new Pythonnet.PyFloat(Value));
+ }
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return new Pythonnet.PyFloat(Value);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/PyInt.cs b/src/Bonsai.Scripting.Python/PyInt.cs
new file mode 100644
index 0000000..2cf22c9
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/PyInt.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents the creation of an int python data type.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Source)]
+ public class PyInt
+ {
+
+ [Description("The value of the integer.")]
+ public int Value { get; set; }
+
+ public IObservable Process()
+ {
+ return Observable.Defer(() =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return Observable.Return(new Pythonnet.PyInt(Value));
+ }
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return new Pythonnet.PyInt(Value);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/PyString.cs b/src/Bonsai.Scripting.Python/PyString.cs
new file mode 100644
index 0000000..219c79c
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/PyString.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents the creation of a string python data type.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Source)]
+ public class PyString
+ {
+
+ [Description("The value of the string.")]
+ public string Value { get; set; }
+
+ public IObservable Process()
+ {
+ return Observable.Defer(() =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return Observable.Return(new Pythonnet.PyString(Value));
+ }
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ return new Pythonnet.PyString(Value);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/PythonInterpreterLock.cs b/src/Bonsai.Scripting.Python/PythonInterpreterLock.cs
new file mode 100644
index 0000000..f2ebf3b
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/PythonInterpreterLock.cs
@@ -0,0 +1,67 @@
+using Bonsai.Expressions;
+using System.Linq.Expressions;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System;
+using System.Reflection;
+using System.Linq;
+using Python.Runtime;
+using System.Reactive;
+
+namespace Bonsai.Scripting.Python
+{
+ public class PythonInterpreterLock : WorkflowExpressionBuilder
+ {
+ static readonly Range argumentRange = Range.Create(lowerBound: 1, upperBound: 1);
+ private static readonly object _lock = new object();
+
+ public PythonInterpreterLock()
+ : this(new ExpressionBuilderGraph())
+ {
+ }
+
+ public PythonInterpreterLock(ExpressionBuilderGraph workflow)
+ : base(workflow)
+ {
+ }
+
+ public override Range ArgumentRange => argumentRange;
+
+ public override Expression Build(IEnumerable arguments)
+ {
+ var source = arguments.FirstOrDefault();
+ if (source == null)
+ {
+ throw new InvalidOperationException("There must be at least one input.");
+ }
+ var sourceType = source.Type.GetGenericArguments()[0];
+ var factoryParameter = Expression.Parameter(typeof(IObservable<>).MakeGenericType(sourceType), "factoryParam");
+
+ return BuildWorkflow(arguments, factoryParameter, selectorBody =>
+ {
+ var selector = Expression.Lambda(selectorBody, factoryParameter);
+ var resultType = selectorBody.Type.GetGenericArguments()[0];
+ return Expression.Call(GetType(), nameof(Process), new Type[] {sourceType, resultType}, source, selector);
+ });
+ }
+
+ static IObservable Process(IObservable source, Func, IObservable> selector)
+ {
+ return RuntimeManager.RuntimeSource.SelectMany(runtime =>
+ {
+ return selector(Observable.Create(observer =>
+ {
+ return source.SubscribeSafe(Observer.Create(value =>
+ {
+ using (Py.GIL())
+ {
+ observer.OnNext(value); //locking around downstream effects
+ }
+ },
+ observer.OnError,
+ observer.OnCompleted));
+ }));
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bonsai.Scripting.Python/SetAttr.cs b/src/Bonsai.Scripting.Python/SetAttr.cs
new file mode 100644
index 0000000..a4e7c3c
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/SetAttr.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that sets the named attribute of an object to a value.
+ ///
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Sink)]
+ public class SetAttr
+ {
+ [Description("The name of the attribute to get.")]
+ public string Attribute { get; set; }
+
+ [XmlIgnore]
+ [Description("The object to assign to the attribute.")]
+ public PyObject Value { get; set; } = null;
+
+ public IObservable Process(IObservable source)
+ {
+ if (string.IsNullOrEmpty(Attribute))
+ {
+ throw new Exception("Attribute cannot be null or empty.");
+ }
+ return source.Do(obj =>
+ {
+ using (Py.GIL())
+ {
+ if (Value == null)
+ {
+ throw new Exception("Value cannot be null.");
+ }
+ obj.SetAttr(Attribute, Value);
+ }
+ });
+ }
+ }
+}
diff --git a/src/Bonsai.Scripting.Python/SetItem.cs b/src/Bonsai.Scripting.Python/SetItem.cs
new file mode 100644
index 0000000..0fc74e9
--- /dev/null
+++ b/src/Bonsai.Scripting.Python/SetItem.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+using Pythonnet = Python.Runtime;
+using System.Xml.Serialization;
+using System.Linq;
+
+namespace Bonsai.Scripting.Python
+{
+ ///
+ /// Represents an operator that sets the named attribute of an object to a value.
+ ///
+ [WorkflowElementCategory(ElementCategory.Sink)]
+ [Combinator]
+ public class SetItem
+ {
+ [Description("The index to set.")]
+ public int Index { get; set; } = 0;
+
+ [XmlIgnore]
+ [Description("The value to assign to the index.")]
+ public Pythonnet.PyObject Value { get; set; } = null;
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Do(obj =>
+ {
+ using (Pythonnet.Py.GIL())
+ {
+ if (!Pythonnet.PySequence.IsSequenceType(obj))
+ {
+ throw new Exception($"PyObject is not a type of sequence.");
+ }
+ if (Value == null)
+ {
+ throw new Exception("Value cannot be null.");
+ }
+ obj.SetItem(Index, Value);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file