Skip to content

Commit dfcace5

Browse files
committed
Add PythonCoroutine.AsTask() and GetAwaiter() for C# async interop
Allows C# code to directly await IronPython coroutines: object result = await coroutine;
1 parent 3998f8a commit dfcace5

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

src/core/IronPython/Runtime/Coroutine.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
#nullable enable
66

7+
using System.Runtime.CompilerServices;
8+
using System.Threading.Tasks;
9+
710
using Microsoft.Scripting.Runtime;
811

912
using IronPython.Runtime.Exceptions;
@@ -62,6 +65,38 @@ public string __qualname__ {
6265
get => _generator.__name__;
6366
}
6467

68+
/// <summary>
69+
/// Converts this coroutine into a .NET <see cref="Task{Object}"/>,
70+
/// allowing C# code to <c>await</c> an IronPython async method.
71+
/// The coroutine is driven on a single thread to avoid issues with
72+
/// thread-local state in the Python generator runtime.
73+
/// </summary>
74+
public Task<object?> AsTask() {
75+
return Task.Run(() => {
76+
while (true) {
77+
object result = send(null);
78+
79+
if (LightExceptions.IsLightException(result)) {
80+
var clrExc = LightExceptions.GetLightException(result);
81+
if (clrExc is StopIterationException) {
82+
var pyExc = ((IPythonAwareException)clrExc).PythonException;
83+
return pyExc is PythonExceptions._StopIteration si ? si.value : null;
84+
}
85+
throw clrExc;
86+
}
87+
88+
if (result is Task task) {
89+
task.Wait();
90+
}
91+
}
92+
});
93+
}
94+
95+
/// <summary>
96+
/// Enables <c>await coroutine</c> from C# code.
97+
/// </summary>
98+
public TaskAwaiter<object?> GetAwaiter() => AsTask().GetAwaiter();
99+
65100
internal PythonGenerator Generator => _generator;
66101

67102
#region ICodeFormattable Members
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if NET
6+
7+
using System;
8+
using System.Threading.Tasks;
9+
10+
using IronPython.Hosting;
11+
using IronPython.Runtime;
12+
13+
using Microsoft.Scripting.Hosting;
14+
15+
using NUnit.Framework;
16+
17+
namespace IronPythonTest {
18+
public class CoroutineAsTaskTest {
19+
private readonly ScriptEngine _engine;
20+
private readonly ScriptScope _scope;
21+
22+
public CoroutineAsTaskTest() {
23+
_engine = Python.CreateEngine();
24+
_scope = _engine.CreateScope();
25+
}
26+
27+
[Test]
28+
public void AsTask_SimpleReturn() {
29+
_engine.Execute(@"
30+
async def foo():
31+
return 42
32+
coro = foo()
33+
", _scope);
34+
35+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
36+
var result = coro.AsTask().GetAwaiter().GetResult();
37+
Assert.That(result, Is.EqualTo(42));
38+
}
39+
40+
[Test]
41+
public void AsTask_StringReturn() {
42+
_engine.Execute(@"
43+
async def foo():
44+
return 'hello'
45+
coro = foo()
46+
", _scope);
47+
48+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
49+
var result = coro.AsTask().GetAwaiter().GetResult();
50+
Assert.That(result, Is.EqualTo("hello"));
51+
}
52+
53+
[Test]
54+
public void AsTask_AwaitCompletedTask() {
55+
_engine.Execute(@"
56+
from System.Threading.Tasks import Task
57+
async def foo():
58+
val = await Task.FromResult(10)
59+
return val + 5
60+
coro = foo()
61+
", _scope);
62+
63+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
64+
var result = coro.AsTask().GetAwaiter().GetResult();
65+
Assert.That(result, Is.EqualTo(15));
66+
}
67+
68+
[Test]
69+
public void AsTask_AwaitRealAsync() {
70+
_engine.Execute(@"
71+
from System.Threading.Tasks import Task
72+
async def foo():
73+
await Task.Delay(50)
74+
return 'delayed'
75+
coro = foo()
76+
", _scope);
77+
78+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
79+
var result = coro.AsTask().GetAwaiter().GetResult();
80+
Assert.That(result, Is.EqualTo("delayed"));
81+
}
82+
83+
[Test]
84+
public async Task AsTask_CanBeAwaited() {
85+
_engine.Execute(@"
86+
from System.Threading.Tasks import Task
87+
async def foo():
88+
await Task.Delay(50)
89+
return 99
90+
coro = foo()
91+
", _scope);
92+
93+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
94+
var result = await coro.AsTask();
95+
Assert.That(result, Is.EqualTo(99));
96+
}
97+
98+
[Test]
99+
public void AsTask_PropagatesException() {
100+
_engine.Execute(@"
101+
async def foo():
102+
raise ValueError('boom')
103+
coro = foo()
104+
", _scope);
105+
106+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
107+
Assert.Throws<AggregateException>(() => coro.AsTask().Wait());
108+
}
109+
110+
[Test]
111+
public void AsTask_MultipleAwaits() {
112+
_engine.Execute(@"
113+
from System.Threading.Tasks import Task
114+
async def foo():
115+
a = await Task.FromResult(10)
116+
b = await Task.FromResult(20)
117+
return a + b
118+
coro = foo()
119+
", _scope);
120+
121+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
122+
var result = coro.AsTask().GetAwaiter().GetResult();
123+
Assert.That(result, Is.EqualTo(30));
124+
}
125+
126+
[Test]
127+
public void AsTask_NoneReturn() {
128+
_engine.Execute(@"
129+
async def foo():
130+
pass
131+
coro = foo()
132+
", _scope);
133+
134+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
135+
var result = coro.AsTask().GetAwaiter().GetResult();
136+
Assert.That(result, Is.Null);
137+
}
138+
139+
[Test]
140+
public async Task DirectAwait_Simple() {
141+
_engine.Execute(@"
142+
async def foo():
143+
return 42
144+
coro = foo()
145+
", _scope);
146+
147+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
148+
var result = await coro;
149+
Assert.That(result, Is.EqualTo(42));
150+
}
151+
152+
[Test]
153+
public async Task DirectAwait_WithRealAsync() {
154+
_engine.Execute(@"
155+
from System.Threading.Tasks import Task
156+
async def foo():
157+
await Task.Delay(50)
158+
return 'done'
159+
coro = foo()
160+
", _scope);
161+
162+
var coro = (PythonCoroutine)_scope.GetVariable("coro");
163+
var result = await coro;
164+
Assert.That(result, Is.EqualTo("done"));
165+
}
166+
}
167+
}
168+
169+
#endif

0 commit comments

Comments
 (0)