Skip to content

Commit a1c1605

Browse files
committed
Implement .NET async interop: await Task/ValueTask, async for IAsyncEnumerable, CancelledError
- Add TaskAwaitable/ValueTaskAwaitable wrappers enabling `await` on Task, Task<T>, ValueTask and ValueTask<T> from Python async code - Add AsyncEnumerableWrapper enabling `async for` over IAsyncEnumerable<T> - Map OperationCanceledException to new CancelledError Python exception - Add __await__, __aiter__, __anext__ resolvers in PythonTypeInfo - Add bridge methods in InstanceOps for the resolver pattern - ValueTask/IAsyncEnumerable support gated behind #if NET (requires .NET Core) - Handle Task<VoidTaskResult> (internal type arg) by falling back to non-generic TaskAwaitable via IsVisible check
1 parent 826dfeb commit a1c1605

10 files changed

Lines changed: 591 additions & 0 deletions

File tree

eng/scripts/generate_exceptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def MakeNewException(self):
8585
ExceptionInfo('Exception', 'IronPython.Runtime.Exceptions.PythonException', None, (), (
8686
ExceptionInfo('StopIteration', 'IronPython.Runtime.Exceptions.StopIterationException', None, ('value',), ()),
8787
ExceptionInfo('StopAsyncIteration', 'IronPython.Runtime.Exceptions.StopAsyncIterationException', None, ('value',), ()),
88+
ExceptionInfo('CancelledError', 'System.OperationCanceledException', None, (), ()),
8889
ExceptionInfo('ArithmeticError', 'System.ArithmeticException', None, (), (
8990
ExceptionInfo('FloatingPointError', 'IronPython.Runtime.Exceptions.FloatingPointException', None, (), ()),
9091
ExceptionInfo('OverflowError', 'System.OverflowException', None, (), ()),

src/core/IronPython/Modules/Builtin.Generated.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static partial class Builtin {
2020
public static PythonType Exception => PythonExceptions.Exception;
2121
public static PythonType StopIteration => PythonExceptions.StopIteration;
2222
public static PythonType StopAsyncIteration => PythonExceptions.StopAsyncIteration;
23+
public static PythonType CancelledError => PythonExceptions.CancelledError;
2324
public static PythonType ArithmeticError => PythonExceptions.ArithmeticError;
2425
public static PythonType FloatingPointError => PythonExceptions.FloatingPointError;
2526
public static PythonType OverflowError => PythonExceptions.OverflowError;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
// IAsyncEnumerable<T> / IAsyncEnumerator<T> require .NET Core 3.0+
6+
#if NET
7+
8+
#nullable enable
9+
10+
using System.Collections.Generic;
11+
using System.Threading.Tasks;
12+
13+
using Microsoft.Scripting.Runtime;
14+
15+
using IronPython.Runtime.Exceptions;
16+
using IronPython.Runtime.Operations;
17+
using IronPython.Runtime.Types;
18+
19+
namespace IronPython.Runtime {
20+
/// <summary>
21+
/// Wraps <see cref="IAsyncEnumerator{T}"/> to implement the Python
22+
/// async iterator protocol (__aiter__, __anext__).
23+
/// Returned by <see cref="InstanceOps.AsyncIterMethod{T}"/>.
24+
/// </summary>
25+
[PythonType("async_enumerator_wrapper")]
26+
public sealed class AsyncEnumeratorWrapper<T> {
27+
private readonly IAsyncEnumerator<T> _enumerator;
28+
29+
internal AsyncEnumeratorWrapper(IAsyncEnumerator<T> enumerator) {
30+
_enumerator = enumerator;
31+
}
32+
33+
public AsyncEnumeratorWrapper<T> __aiter__() => this;
34+
35+
/// <summary>
36+
/// Returns an awaitable that, when awaited, advances the async enumerator
37+
/// and returns the next value or raises StopAsyncIteration.
38+
/// </summary>
39+
public object __anext__() {
40+
return new AsyncEnumeratorAwaitable<T>(_enumerator);
41+
}
42+
}
43+
44+
/// <summary>
45+
/// The awaitable object returned by <see cref="AsyncEnumeratorWrapper{T}.__anext__"/>.
46+
/// Implements both __await__ and __iter__/__next__ (the yield-from protocol) to
47+
/// block on MoveNextAsync and return the current value via StopIteration.
48+
/// </summary>
49+
[PythonType("async_enumerator_awaitable")]
50+
public sealed class AsyncEnumeratorAwaitable<T> {
51+
private readonly IAsyncEnumerator<T> _enumerator;
52+
53+
internal AsyncEnumeratorAwaitable(IAsyncEnumerator<T> enumerator) {
54+
_enumerator = enumerator;
55+
}
56+
57+
public AsyncEnumeratorAwaitable<T> __await__() => this;
58+
59+
public AsyncEnumeratorAwaitable<T> __iter__() => this;
60+
61+
[LightThrowing]
62+
public object __next__() {
63+
bool hasNext = _enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult();
64+
if (!hasNext) {
65+
return LightExceptions.Throw(
66+
new PythonExceptions._StopAsyncIteration().InitAndGetClrException());
67+
}
68+
return LightExceptions.Throw(
69+
new PythonExceptions._StopIteration().InitAndGetClrException(_enumerator.Current!));
70+
}
71+
}
72+
}
73+
74+
#endif

src/core/IronPython/Runtime/Exceptions/PythonExceptions.Generated.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ public _StopAsyncIteration(PythonType type) : base(type) { }
114114
public object value { get; set; }
115115
}
116116

117+
[MultiRuntimeAware]
118+
private static PythonType CancelledErrorStorage;
119+
public static PythonType CancelledError {
120+
get {
121+
if (CancelledErrorStorage == null) {
122+
Interlocked.CompareExchange(ref CancelledErrorStorage, CreateSubType(Exception, "CancelledError", (msg, innerException) => new System.OperationCanceledException(msg, innerException)), null);
123+
}
124+
return CancelledErrorStorage;
125+
}
126+
}
127+
117128
[MultiRuntimeAware]
118129
private static PythonType ArithmeticErrorStorage;
119130
public static PythonType ArithmeticError {
@@ -935,6 +946,7 @@ public static PythonType ResourceWarning {
935946
if (clrException is StopAsyncIterationException) return new PythonExceptions._StopAsyncIteration();
936947
if (clrException is StopIterationException) return new PythonExceptions._StopIteration();
937948
if (clrException is SyntaxErrorException) return new PythonExceptions._SyntaxError();
949+
if (clrException is OperationCanceledException) return new PythonExceptions.BaseException(PythonExceptions.CancelledError);
938950
if (clrException is SystemException) return new PythonExceptions.BaseException(PythonExceptions.SystemError);
939951
if (clrException is SystemExitException) return new PythonExceptions._SystemExit();
940952
if (clrException is UnboundNameException) return new PythonExceptions.BaseException(PythonExceptions.NameError);

src/core/IronPython/Runtime/Operations/InstanceOps.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,47 @@ public static object NextMethod(object self) {
205205

206206
#endregion
207207

208+
#region Async Interop
209+
210+
/// <summary>
211+
/// Provides the implementation of __await__ for <see cref="System.Threading.Tasks.Task"/>.
212+
/// </summary>
213+
public static object TaskAwaitMethod(System.Threading.Tasks.Task self) {
214+
return new TaskAwaitable(self);
215+
}
216+
217+
/// <summary>
218+
/// Provides the implementation of __await__ for <see cref="System.Threading.Tasks.Task{T}"/>.
219+
/// </summary>
220+
public static object TaskAwaitMethodGeneric<T>(System.Threading.Tasks.Task<T> self) {
221+
return new TaskAwaitable<T>(self);
222+
}
223+
224+
#if NET
225+
/// <summary>
226+
/// Provides the implementation of __await__ for <see cref="System.Threading.Tasks.ValueTask"/>.
227+
/// </summary>
228+
public static object ValueTaskAwaitMethod(System.Threading.Tasks.ValueTask self) {
229+
return new ValueTaskAwaitable(self);
230+
}
231+
232+
/// <summary>
233+
/// Provides the implementation of __await__ for <see cref="System.Threading.Tasks.ValueTask{T}"/>.
234+
/// </summary>
235+
public static object ValueTaskAwaitMethodGeneric<T>(System.Threading.Tasks.ValueTask<T> self) {
236+
return new ValueTaskAwaitable<T>(self);
237+
}
238+
239+
/// <summary>
240+
/// Provides the implementation of __aiter__ for <see cref="IAsyncEnumerable{T}"/>.
241+
/// </summary>
242+
public static object AsyncIterMethod<T>(System.Collections.Generic.IAsyncEnumerable<T> self) {
243+
return new AsyncEnumeratorWrapper<T>(self.GetAsyncEnumerator());
244+
}
245+
#endif
246+
247+
#endregion
248+
208249
/// <summary>
209250
/// __dir__(self) -> Returns the list of members defined on a foreign IDynamicMetaObjectProvider.
210251
/// </summary>

src/core/IronPython/Runtime/Operations/PythonOps.Generated.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ public static partial class PythonOps {
125125
internal static Exception ModuleNotFoundError(string message) => new ModuleNotFoundException(message);
126126
public static Exception ModuleNotFoundError(string format, params object?[] args) => new ModuleNotFoundException(string.Format(format, args));
127127

128+
internal static Exception CancelledError(string message) => new System.OperationCanceledException(message);
129+
public static Exception CancelledError(string format, params object?[] args) => new System.OperationCanceledException(string.Format(format, args));
130+
128131
// *** END GENERATED CODE ***
129132

130133
#endregion
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
#nullable enable
6+
7+
using System.Threading.Tasks;
8+
9+
using Microsoft.Scripting.Runtime;
10+
11+
using IronPython.Runtime.Exceptions;
12+
using IronPython.Runtime.Types;
13+
14+
namespace IronPython.Runtime {
15+
/// <summary>
16+
/// Provides an __await__ protocol wrapper for <see cref="Task"/>,
17+
/// enabling <c>await task</c> from Python async code.
18+
/// Blocks on the task and returns None via StopIteration.
19+
/// </summary>
20+
[PythonType("task_awaitable")]
21+
public sealed class TaskAwaitable {
22+
private readonly Task _task;
23+
24+
internal TaskAwaitable(Task task) {
25+
_task = task;
26+
}
27+
28+
public TaskAwaitable __await__() => this;
29+
30+
public TaskAwaitable __iter__() => this;
31+
32+
[LightThrowing]
33+
public object __next__() {
34+
_task.GetAwaiter().GetResult();
35+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException());
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Provides an __await__ protocol wrapper for <see cref="Task{T}"/>,
41+
/// enabling <c>result = await task</c> from Python async code.
42+
/// Blocks on the task and returns the result via StopIteration.value.
43+
/// </summary>
44+
[PythonType("task_awaitable")]
45+
public sealed class TaskAwaitable<T> {
46+
private readonly Task<T> _task;
47+
48+
internal TaskAwaitable(Task<T> task) {
49+
_task = task;
50+
}
51+
52+
public TaskAwaitable<T> __await__() => this;
53+
54+
public TaskAwaitable<T> __iter__() => this;
55+
56+
[LightThrowing]
57+
public object __next__() {
58+
T result = _task.GetAwaiter().GetResult();
59+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(result!));
60+
}
61+
}
62+
63+
#if NET
64+
/// <summary>
65+
/// Provides an __await__ protocol wrapper for <see cref="ValueTask"/>,
66+
/// enabling <c>await valuetask</c> from Python async code.
67+
/// </summary>
68+
[PythonType("task_awaitable")]
69+
public sealed class ValueTaskAwaitable {
70+
private readonly ValueTask _task;
71+
72+
internal ValueTaskAwaitable(ValueTask task) {
73+
_task = task;
74+
}
75+
76+
public ValueTaskAwaitable __await__() => this;
77+
78+
public ValueTaskAwaitable __iter__() => this;
79+
80+
[LightThrowing]
81+
public object __next__() {
82+
_task.AsTask().GetAwaiter().GetResult();
83+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException());
84+
}
85+
}
86+
87+
/// <summary>
88+
/// Provides an __await__ protocol wrapper for <see cref="ValueTask{T}"/>,
89+
/// enabling <c>result = await valuetask</c> from Python async code.
90+
/// </summary>
91+
[PythonType("task_awaitable")]
92+
public sealed class ValueTaskAwaitable<T> {
93+
private readonly ValueTask<T> _task;
94+
95+
internal ValueTaskAwaitable(ValueTask<T> task) {
96+
_task = task;
97+
}
98+
99+
public ValueTaskAwaitable<T> __await__() => this;
100+
101+
public ValueTaskAwaitable<T> __iter__() => this;
102+
103+
[LightThrowing]
104+
public object __next__() {
105+
T result = _task.AsTask().GetAwaiter().GetResult();
106+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(result!));
107+
}
108+
}
109+
#endif
110+
}

src/core/IronPython/Runtime/Types/PythonTypeInfo.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Linq;
1111
using System.Numerics;
1212
using System.Reflection;
13+
using System.Threading.Tasks;
1314

1415
using Microsoft.Scripting;
1516
using Microsoft.Scripting.Actions;
@@ -672,6 +673,9 @@ private class ProtectedMemberResolver : MemberResolver {
672673
new OneOffResolver("__len__", LengthResolver),
673674
new OneOffResolver("__format__", FormatResolver),
674675
new OneOffResolver("__next__", NextResolver),
676+
new OneOffResolver("__await__", AwaitResolver),
677+
new OneOffResolver("__aiter__", AsyncIterResolver),
678+
new OneOffResolver("__anext__", AsyncNextResolver),
675679

676680
new OneOffResolver("__complex__", ComplexResolver),
677681
new OneOffResolver("__float__", FloatResolver),
@@ -965,6 +969,83 @@ internal static MemberGroup GetExtensionMemberGroup(Type type, MemberInfo[] news
965969
return MemberGroup.EmptyGroup;
966970
}
967971

972+
/// <summary>
973+
/// Provides a resolution for __await__ on Task, Task&lt;T&gt;, ValueTask and ValueTask&lt;T&gt;.
974+
/// </summary>
975+
private static MemberGroup/*!*/ AwaitResolver(MemberBinder/*!*/ binder, Type/*!*/ type) {
976+
foreach (Type t in binder.GetContributingTypes(type)) {
977+
if (t.GetMember("__await__").Length > 0) {
978+
return MemberGroup.EmptyGroup;
979+
}
980+
}
981+
982+
if (typeof(Task).IsAssignableFrom(type)) {
983+
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) {
984+
// Only use the generic TaskAwaitable<T> if the result type is visible
985+
// (e.g. Task.CompletedTask is Task<VoidTaskResult> at runtime where
986+
// VoidTaskResult is internal — fall back to non-generic TaskAwaitable)
987+
Type resultType = type.GetGenericArguments()[0];
988+
if (resultType.IsVisible) {
989+
MethodInfo genMeth = typeof(InstanceOps).GetMethod(nameof(InstanceOps.TaskAwaitMethodGeneric));
990+
return new MemberGroup(
991+
MethodTracker.FromMemberInfo(genMeth.MakeGenericMethod(type.GetGenericArguments()), type)
992+
);
993+
}
994+
}
995+
return GetInstanceOpsMethod(type, nameof(InstanceOps.TaskAwaitMethod));
996+
}
997+
998+
#if NET
999+
if (type.IsGenericType) {
1000+
Type genDef = type.GetGenericTypeDefinition();
1001+
if (genDef == typeof(ValueTask<>)) {
1002+
MethodInfo genMeth = typeof(InstanceOps).GetMethod(nameof(InstanceOps.ValueTaskAwaitMethodGeneric));
1003+
return new MemberGroup(
1004+
MethodTracker.FromMemberInfo(genMeth.MakeGenericMethod(type.GetGenericArguments()), type)
1005+
);
1006+
}
1007+
}
1008+
1009+
if (type == typeof(ValueTask)) {
1010+
return GetInstanceOpsMethod(type, nameof(InstanceOps.ValueTaskAwaitMethod));
1011+
}
1012+
#endif
1013+
1014+
return MemberGroup.EmptyGroup;
1015+
}
1016+
1017+
/// <summary>
1018+
/// Provides a resolution for __aiter__ on IAsyncEnumerable&lt;T&gt;.
1019+
/// </summary>
1020+
private static MemberGroup/*!*/ AsyncIterResolver(MemberBinder/*!*/ binder, Type/*!*/ type) {
1021+
#if NET
1022+
foreach (Type t in binder.GetContributingTypes(type)) {
1023+
if (t.GetMember("__aiter__").Length > 0) {
1024+
return MemberGroup.EmptyGroup;
1025+
}
1026+
}
1027+
1028+
foreach (Type t in binder.GetInterfaces(type)) {
1029+
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) {
1030+
MethodInfo genMeth = typeof(InstanceOps).GetMethod(nameof(InstanceOps.AsyncIterMethod));
1031+
return new MemberGroup(
1032+
MethodTracker.FromMemberInfo(genMeth.MakeGenericMethod(t.GetGenericArguments()), type)
1033+
);
1034+
}
1035+
}
1036+
#endif
1037+
1038+
return MemberGroup.EmptyGroup;
1039+
}
1040+
1041+
/// <summary>
1042+
/// Provides a resolution for __anext__ on AsyncEnumeratorWrapper&lt;T&gt;.
1043+
/// Not auto-mapped from interfaces; the wrapper class provides __anext__ directly.
1044+
/// </summary>
1045+
private static MemberGroup/*!*/ AsyncNextResolver(MemberBinder/*!*/ binder, Type/*!*/ type) {
1046+
return MemberGroup.EmptyGroup;
1047+
}
1048+
9681049
/// <summary>
9691050
/// Provides a resolution for __len__
9701051
/// </summary>

0 commit comments

Comments
 (0)