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