Skip to content

Commit 9969791

Browse files
author
Egor
authored
Merge pull request #9 from medegor44/PythonNet_Integration
Pythonnet runner integration
2 parents 3937171 + cea5a14 commit 9969791

File tree

8 files changed

+304
-0
lines changed

8 files changed

+304
-0
lines changed

EmbeddedScripts.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedScripts.Shared", "S
4545
EndProject
4646
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedScripts.JS.Common.Tests", "Tests\EmbeddedScripts.JS.Common.Tests\EmbeddedScripts.JS.Common.Tests.csproj", "{AAFED3B1-E319-45BB-B213-653DC4F6819F}"
4747
EndProject
48+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Python", "Python", "{8EE291DB-ACE3-40EE-8FD9-1A2C918DD5F2}"
49+
EndProject
50+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedScripts.Python.PythonNet", "Src\EmbeddedScripts.Python.PythonNet\EmbeddedScripts.Python.PythonNet.csproj", "{B1F9D200-DE38-4A05-B573-07264709DDFB}"
51+
EndProject
52+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedScripts.Python.PythonNet.Tests", "Tests\EmbeddedScripts.Python.PythonNet.Tests\EmbeddedScripts.Python.PythonNet.Tests.csproj", "{02D102CC-1058-431E-BEFB-BEF71E8ACE6C}"
53+
EndProject
4854
Global
4955
GlobalSection(SharedMSBuildProjectFiles) = preSolution
5056
Src\EmbeddedScripts.CSharp.Shared\EmbeddedScripts.CSharp.Shared.projitems*{2e1ca5c1-3fbf-4bec-9db2-8e78b3bc8169}*SharedItemsImports = 13
@@ -129,6 +135,14 @@ Global
129135
{AAFED3B1-E319-45BB-B213-653DC4F6819F}.Debug|Any CPU.Build.0 = Debug|Any CPU
130136
{AAFED3B1-E319-45BB-B213-653DC4F6819F}.Release|Any CPU.ActiveCfg = Release|Any CPU
131137
{AAFED3B1-E319-45BB-B213-653DC4F6819F}.Release|Any CPU.Build.0 = Release|Any CPU
138+
{B1F9D200-DE38-4A05-B573-07264709DDFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
139+
{B1F9D200-DE38-4A05-B573-07264709DDFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
140+
{B1F9D200-DE38-4A05-B573-07264709DDFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
141+
{B1F9D200-DE38-4A05-B573-07264709DDFB}.Release|Any CPU.Build.0 = Release|Any CPU
142+
{02D102CC-1058-431E-BEFB-BEF71E8ACE6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
143+
{02D102CC-1058-431E-BEFB-BEF71E8ACE6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
144+
{02D102CC-1058-431E-BEFB-BEF71E8ACE6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
145+
{02D102CC-1058-431E-BEFB-BEF71E8ACE6C}.Release|Any CPU.Build.0 = Release|Any CPU
132146
EndGlobalSection
133147
GlobalSection(SolutionProperties) = preSolution
134148
HideSolutionNode = FALSE
@@ -150,6 +164,8 @@ Global
150164
{BB1850EA-C8D7-4B30-8AD1-BF699B9FA658} = {15F562A4-676E-4C31-A0C7-02FB8471FE8E}
151165
{6C138061-CE5E-4EFC-AB6C-6DBB2CC0A752} = {DB199BCF-967A-46EE-89E5-BBC0B6D59BE5}
152166
{AAFED3B1-E319-45BB-B213-653DC4F6819F} = {DB199BCF-967A-46EE-89E5-BBC0B6D59BE5}
167+
{B1F9D200-DE38-4A05-B573-07264709DDFB} = {8EE291DB-ACE3-40EE-8FD9-1A2C918DD5F2}
168+
{02D102CC-1058-431E-BEFB-BEF71E8ACE6C} = {DB199BCF-967A-46EE-89E5-BBC0B6D59BE5}
153169
EndGlobalSection
154170
GlobalSection(ExtensibilityGlobals) = postSolution
155171
SolutionGuid = {9C12123A-C2E9-46DF-AC21-40D0640056AE}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<RootNamespace>EmbeddedScripts.Python.PythonNet</RootNamespace>
6+
<LangVersion>latest</LangVersion>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="pythonnet" Version="3.0.0-preview2021-08-03" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\EmbeddedScripts.Shared\EmbeddedScripts.Shared.csproj" />
15+
</ItemGroup>
16+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using EmbeddedScripts.Shared;
2+
using Python.Runtime;
3+
4+
namespace EmbeddedScripts.Python.PythonNet
5+
{
6+
public static class PyScopeExtensions
7+
{
8+
public static PyScope AddHostObjectsFromContainer(this PyScope scope, Container container)
9+
{
10+
foreach (var name in container.VariableAliases)
11+
{
12+
var pyObj = container.Resolve(name).ToPython();
13+
scope.Set(name, pyObj);
14+
}
15+
16+
return scope;
17+
}
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using Python.Runtime;
3+
4+
namespace EmbeddedScripts.Python.PythonNet
5+
{
6+
public class PythonMultithreadingScope : IDisposable
7+
{
8+
private readonly IntPtr _threadState;
9+
10+
public PythonMultithreadingScope()
11+
{
12+
PythonEngine.Initialize();
13+
_threadState = PythonEngine.BeginAllowThreads();
14+
}
15+
16+
public void Dispose()
17+
{
18+
PythonEngine.EndAllowThreads(_threadState);
19+
PythonEngine.Shutdown();
20+
}
21+
}
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Threading.Tasks;
2+
using EmbeddedScripts.Shared;
3+
using Python.Runtime;
4+
5+
namespace EmbeddedScripts.Python.PythonNet
6+
{
7+
public class PythonNetRunner : ICodeRunner
8+
{
9+
private readonly Container _container = new();
10+
11+
public static string PythonDll
12+
{
13+
get => Runtime.PythonDLL;
14+
set => Runtime.PythonDLL = value;
15+
}
16+
17+
public Task RunAsync(string code)
18+
{
19+
using (new PythonMultithreadingScope())
20+
using (Py.GIL())
21+
using (var scope = Py.CreateScope())
22+
scope
23+
.AddHostObjectsFromContainer(_container)
24+
.Exec(code);
25+
26+
return Task.CompletedTask;
27+
}
28+
29+
public ICodeRunner Register<T>(T obj, string alias)
30+
{
31+
_container.Register(obj, alias);
32+
33+
return this;
34+
}
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
11+
<PackageReference Include="xunit" Version="2.4.1" />
12+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
13+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
14+
<PrivateAssets>all</PrivateAssets>
15+
</PackageReference>
16+
<PackageReference Include="coverlet.collector" Version="3.0.2">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\..\Src\EmbeddedScripts.Python.PythonNet\EmbeddedScripts.Python.PythonNet.csproj" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace HelperObjects
2+
{
3+
public class HelperObject
4+
{
5+
public int x = 0;
6+
}
7+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using System.Threading.Tasks;
4+
using HelperObjects;
5+
using Xunit;
6+
using Python.Runtime;
7+
8+
namespace EmbeddedScripts.Python.PythonNet.Tests
9+
{
10+
public class PythonNetRunnerTests
11+
{
12+
public PythonNetRunnerTests()
13+
{
14+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
15+
PythonNetRunner.PythonDll = "/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so";
16+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
17+
PythonNetRunner.PythonDll = "python38.dll";
18+
}
19+
20+
[Fact]
21+
public async void RunValidCode_Succeed()
22+
{
23+
var code = @"
24+
a = 1
25+
b = 2
26+
c = a + b";
27+
28+
var runner = new PythonNetRunner();
29+
30+
await runner.RunAsync(code);
31+
}
32+
33+
[Fact]
34+
public async void RunWithGlobalVariables_Succeed()
35+
{
36+
var t = new HelperObject();
37+
var code = "t.x += 1;";
38+
39+
var runner = new PythonNetRunner();
40+
runner.Register(t, "t");
41+
42+
await runner.RunAsync(code);
43+
44+
Assert.Equal(1, t.x);
45+
}
46+
47+
[Fact]
48+
public async Task AddConfigOnce_SetsConfig_Succeed()
49+
{
50+
var t = new HelperObject();
51+
var code = "t.x += 1;";
52+
53+
var runner = new PythonNetRunner();
54+
runner.Register(t, "t");
55+
56+
await runner.RunAsync(code);
57+
}
58+
59+
[Fact]
60+
public async Task AddConfigTwice_AddsNewConfig_Succeed()
61+
{
62+
var s = "abc";
63+
var t = new HelperObject();
64+
var code = "t.x += len(s)";
65+
66+
var runner = new PythonNetRunner();
67+
runner.Register(s, "s");
68+
69+
await Assert.ThrowsAsync<PythonException>(() => runner.RunAsync(code));
70+
71+
runner.Register(t, "t");
72+
73+
await runner.RunAsync(code);
74+
}
75+
76+
[Fact]
77+
public async Task RunWithTwoGlobalVariables_Succeed()
78+
{
79+
var runner = new PythonNetRunner();
80+
runner
81+
.Register(1, "a")
82+
.Register(2, "b");
83+
84+
await runner.RunAsync("c = a + b;");
85+
}
86+
87+
[Fact]
88+
public async Task RunWithGlobalFunc_Succeed()
89+
{
90+
int x = 0;
91+
var code = "t();";
92+
93+
var runner = new PythonNetRunner();
94+
runner.Register<Action>(() => x++, "t");
95+
96+
await runner.RunAsync(code);
97+
98+
Assert.Equal(1, x);
99+
}
100+
101+
[Fact]
102+
public async Task RunInvalidCode_ThrowsException()
103+
{
104+
var code = "1a = 1;";
105+
106+
var runner = new PythonNetRunner();
107+
108+
await Assert.ThrowsAsync<PythonException>(() => runner.RunAsync(code));
109+
}
110+
111+
[Fact]
112+
public async void CodeThrowsAnException_SameExceptionIsThrowingFromRunner()
113+
{
114+
var exceptionMessage = "Exception from user code";
115+
var code = $"raise Exception('{exceptionMessage}');";
116+
117+
var runner = new PythonNetRunner();
118+
var exception = await Assert.ThrowsAsync<PythonException>(() =>
119+
runner.RunAsync(code));
120+
121+
Assert.Equal(exceptionMessage, exception.Message);
122+
}
123+
124+
[Fact]
125+
public async Task ExposeFuncWithReturnValue_Succeed()
126+
{
127+
var code = @"
128+
c = add(1, 2)
129+
assert(c == 3)";
130+
131+
var runner = new PythonNetRunner();
132+
await runner
133+
.Register<Func<int, int, int>>((a, b) => a + b, "add")
134+
.RunAsync(code);
135+
}
136+
137+
[Fact]
138+
public async Task CallFunctionThrowingException_RunnerThrowsSameException()
139+
{
140+
var runner = new PythonNetRunner();
141+
var message = "Message from exception";
142+
runner.Register<Action>(() => throw new ArgumentException(message), "f");
143+
144+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => runner.RunAsync("f()"));
145+
Assert.Equal(message, exception?.Message);
146+
}
147+
148+
public class A
149+
{
150+
public int X { get; set; }
151+
}
152+
153+
[Fact]
154+
public async void AddStructAsGlobals_Succeed()
155+
{
156+
var runner = new PythonNetRunner();
157+
await runner
158+
.Register(new A(), "a")
159+
.RunAsync("a.X += 1;");
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)