Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: tonybaloney/CSnakes
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.0.24
Choose a base ref
...
head repository: tonybaloney/CSnakes
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 19 commits
  • 47 files changed
  • 5 contributors

Commits on Jan 13, 2025

  1. Use system mutex in RedistributableLocator during installation (#330)

    * Use system mutex in "RedistributableLocator" during installation
    
    * Release mutex when installation goes well
    
    ---------
    
    Co-authored-by: Anthony Shaw <anthony.p.shaw@gmail.com>
    atifaziz and tonybaloney authored Jan 13, 2025
    Copy the full SHA
    527f03a View commit details
  2. reduce package installer noise in logs (#335)

    * Only write output as error if it fails.
    
    * Capture for debug logs
    
    * Update ProcessUtils.cs
    
    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
    tonybaloney and Copilot authored Jan 13, 2025
    Copy the full SHA
    abb75f0 View commit details

Commits on Jan 14, 2025

  1. Bump version

    tonybaloney committed Jan 14, 2025
    Copy the full SHA
    1a87e38 View commit details
  2. Fix PATH separator in package installers (#336)

    atifaziz authored Jan 14, 2025
    Copy the full SHA
    bced059 View commit details
  3. Add ARM64 on Windows path locators (#339)

    tonybaloney authored Jan 14, 2025
    Copy the full SHA
    de05da4 View commit details

Commits on Jan 22, 2025

  1. Optimise memory load of redistributable download (#342)

    atifaziz authored Jan 22, 2025
    Copy the full SHA
    de88333 View commit details
  2. Allow users to run Python code from a string (#340)

    * Add run string function
    
    * Add input type enumerations
    
    * Add overrides for locals and globals. Add tests
    
    * Add missing argument to test
    
    * Seperate APIs for simple expression and multiple lines. Make them extension methods.
    tonybaloney authored Jan 22, 2025
    Copy the full SHA
    3c318eb View commit details
  3. Allow trailing commas in parameter list (#344)

    * Add failing test
    
    * Allow trailing commas
    tonybaloney authored Jan 22, 2025
    Copy the full SHA
    67a11a1 View commit details

Commits on Jan 27, 2025

  1. Configure awaits in "RedistributableLocator" (#345)

    atifaziz authored Jan 27, 2025
    Copy the full SHA
    8dfadcd View commit details
  2. Update Packaging.props

    tonybaloney authored Jan 27, 2025
    Copy the full SHA
    62ab0cc View commit details
  3. Update docs

    tonybaloney committed Jan 27, 2025
    Copy the full SHA
    f86ecc5 View commit details
  4. Merge branch 'main' of https://github.com/tonybaloney/CSnakes

    tonybaloney committed Jan 27, 2025
    Copy the full SHA
    58bb70f View commit details

Commits on Feb 10, 2025

  1. Update versions for python static build (#349)

    * Update versions for python static build
    
    * Update test version
    tonybaloney authored Feb 10, 2025
    Copy the full SHA
    0231e6b View commit details

Commits on Feb 14, 2025

  1. Add support for async functions (#313)

    * Add async enumerator
    
    * Add a test, move the task up to the generator iterator
    
    * Add integration test
    
    * Basic coroutine bridge between the Python event loop and the C# Task AP
    
    * Create and call task outside of the GIL of the main thread to avoid deadlock
    
    * Add a harder test
    
    * Make the current event loop a thread static as they're per thread state
    
    * Refactor event loops into a specific type and handle lifetimes.
    
    * Add remark
    
    * Refactor finalize into a method
    
    * Run initialization and finalization in the same thread as a background task
    
    * Test that exceptions are handled gracefully
    
    * Reuse event loops and close them all at shutdown.
    
    * Ensure disposal can only happen once
    
    * Update tests
    
    * Use ManualResetEventSlim instead of loop
    
    * Raise disposed exceptions
    
    * Support Coroutine[None] add tests and a benchmark
    
    * Update benchmark and add a timer test
    
    * update spec
    
    * Add docs for the changes
    
    * Update reference table
    
    * Start to hook up cancellation tokens for future usage
    tonybaloney authored Feb 14, 2025
    Copy the full SHA
    3cdcfe4 View commit details
  2. Update Packaging.props

    tonybaloney authored Feb 14, 2025
    Copy the full SHA
    de9f054 View commit details
  3. Update mkdocs.yml

    tonybaloney authored Feb 14, 2025
    Copy the full SHA
    02a7449 View commit details
  4. Update async_support.md

    tonybaloney authored Feb 14, 2025
    Copy the full SHA
    502f37d View commit details

Commits on Feb 28, 2025

  1. Fix warning

    AaronRobinsonMSFT committed Feb 28, 2025
    Copy the full SHA
    8e46250 View commit details
  2. Merge pull request #356 from AaronRobinsonMSFT/fix_warning

    Fix warning
    aaronpowell authored Feb 28, 2025
    Copy the full SHA
    d72a219 View commit details
Showing with 1,051 additions and 123 deletions.
  1. +1 −1 docs/advanced.md
  2. +52 −0 docs/async_support.md
  3. +39 −1 docs/reference.md
  4. +1 −0 mkdocs.yml
  5. +63 −0 src/CSnakes.Runtime.Tests/Python/RunTests.cs
  6. +2 −1 src/CSnakes.Runtime.Tests/RuntimeTestBase.cs
  7. +94 −0 src/CSnakes.Runtime/CPython/Coroutine.cs
  8. +78 −17 src/CSnakes.Runtime/CPython/Init.cs
  9. +19 −0 src/CSnakes.Runtime/CPython/Run.cs
  10. +1 −0 src/CSnakes.Runtime/IPythonEnvironment.cs
  11. +1 −1 src/CSnakes.Runtime/Locators/NuGetLocator.cs
  12. +42 −33 src/CSnakes.Runtime/Locators/RedistributableLocator.cs
  13. +7 −1 src/CSnakes.Runtime/Locators/WindowsStoreLocator.cs
  14. +2 −2 src/CSnakes.Runtime/PackageManagement/PipInstaller.cs
  15. +3 −3 src/CSnakes.Runtime/PackageManagement/UVInstaller.cs
  16. +9 −3 src/CSnakes.Runtime/ProcessUtils.cs
  17. +31 −0 src/CSnakes.Runtime/PyObjectTypeConverter.Coroutine.cs
  18. +44 −0 src/CSnakes.Runtime/Python/Coroutine.cs
  19. +4 −1 src/CSnakes.Runtime/Python/GeneratorIterator.cs
  20. +8 −0 src/CSnakes.Runtime/Python/ICoroutine.cs
  21. +3 −1 src/CSnakes.Runtime/Python/IGeneratorIterator.cs
  22. +8 −4 src/CSnakes.Runtime/Python/Import.cs
  23. +0 −1 src/CSnakes.Runtime/Python/PyObject.Imortals.cs
  24. +2 −1 src/CSnakes.Runtime/Python/PyObject.cs
  25. +1 −0 src/CSnakes.Runtime/PythonEnvironment.cs
  26. +75 −0 src/CSnakes.Runtime/PythonRunString.cs
  27. +3 −2 src/CSnakes.SourceGeneration/Parser/PythonParser.Function.cs
  28. +3 −3 src/CSnakes.SourceGeneration/Parser/PythonParser.Parameters.cs
  29. +3 −4 src/CSnakes.SourceGeneration/Parser/Types/PythonFunctionDefinition.cs
  30. +1 −1 src/CSnakes.SourceGeneration/PythonStaticGenerator.cs
  31. +135 −31 src/CSnakes.SourceGeneration/Reflection/MethodReflection.cs
  32. +7 −1 src/CSnakes.SourceGeneration/Reflection/TypeReflection.cs
  33. +156 −0 src/CSnakes.Tests/PythonStaticGeneratorTests/FormatClassFromMethods.test_coroutines.approved.txt
  34. +25 −1 src/CSnakes.Tests/PythonStaticGeneratorTests/FormatClassFromMethods.test_generators.approved.txt
  35. +15 −0 src/CSnakes.Tests/TokenizerTests.cs
  36. +2 −2 src/Directory.Packages.props
  37. +62 −0 src/Integration.Tests/CoroutineTests.cs
  38. +2 −2 src/Integration.Tests/GeneratorTests.cs
  39. +2 −1 src/Integration.Tests/IntegrationTestBase.cs
  40. +15 −0 src/Integration.Tests/python/test_coroutines.py
  41. +7 −0 src/Integration.Tests/python/test_generators.py
  42. +1 −1 src/Packaging.props
  43. +1 −1 src/Profile/BaseBenchmark.cs
  44. +6 −0 src/Profile/MarshallingBenchmarks.cs
  45. +1 −1 src/Profile/Program.cs
  46. +8 −0 src/Profile/Properties/launchSettings.json
  47. +6 −1 src/Profile/marshalling_benchmarks.py
2 changes: 1 addition & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
@@ -157,4 +157,4 @@ Beyond the C# [limitations](https://learn.microsoft.com/visualstudio/debugger/su
- Changing the name of a function
- Changing the name of a module

The Hot Reload feature is useful for iterating on the __body__ of a Python function, without having to restart the debugger or application.
The Hot Reload feature is useful for iterating on the __body__ of a Python function, without having to restart the debugger or application.
52 changes: 52 additions & 0 deletions docs/async_support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Async support

Python async functions will be generated into async C# methods. The generated C# method will return a `Task<T>` depending on the return type of the Python function.

CSnakes requires strict typing for async functions. This means that the return type of the Python function must be annotated with the `typing.Coroutine[TYield, TSend, TReturn]` type hint:

```python
import asyncio
from typing import Coroutine

async def async_function() -> Coroutine[int, None, None]:
await asyncio.sleep(1)
return 42
```

The generated C# method will have this signature:


```csharp
public async Task<int> AsyncFunction();
```

Python async functions can be awaited in C# code.

## TSend and TResult

CSnakes only supports `Task<TYield>` where `TYield` is `Coroutine[TYield, ..., ...]`. You cannot send values back into the coroutine object.

## Implementation Details

The [C# Async model](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming) and the Python Async models have some important differences:

- C# creates a task pool and tasks are scheduled on this pool. Python uses a single-threaded event loop.
- Python event loops belong to the thread that created them. C# tasks can be scheduled on any thread.
- Python async functions are coroutines that are scheduled on the event loop. C# async functions are tasks that are scheduled on the task pool.

To converge these two models, CSnakes creates a Python event-loop for each C# thread that calls into Python. This event loop is created when the first Python function is called and is destroyed when the thread is disposed. This event loop is used to schedule the Python async functions.
Because C# reuses threads in the Task pool, the event loop is reused and kept as a thread-local variable.

The behavior is abstracted away from the user, but it is important to understand that the Python event loop is created and destroyed for each C# thread that calls into Python. This is important to understand when debugging or profiling your application.

## Parallelism considerations

Event though C# uses a thread-pool to schedule tasks, the Python Global Interpreter Lock (GIL) will prevent multiple Python threads from running in parallel.
This means that even if you use parallel LINQ or other parallel constructs in C#, CPU-bound Python code will mostly run in a single thread at a time.

Python 3.13 and above have a feature called "free-threading mode" which allows the Python interpreter to run in a multi-threaded environment without the Global Interpreter Lock (GIL). This is a significant change to the Python runtime and can have a big impact on the performance of Python code running in a multi-threaded environment.
See [Free-Threading Mode](advanced.md#free-threading-mode) for more information.

## Cancellation Tokens

Coroutine objects have an `AsTask<TYield>(CancellationToken?)` API, but the source generator does not yet propagate the cancellation token argument to the generated interfaces, if you want to use cancellation tokens, please raise an issue with your use case.
40 changes: 39 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
@@ -21,9 +21,9 @@ CSnakes supports the following typed scenarios:
| `typing.Optional[T]` | `T?` |
| `typing.Generator[TYield, TSend, TReturn]` | `IGeneratorIterator<TYield, TSend, TReturn>` [1](#generators) |
| `typing.Buffer` | `IPyBuffer` [2](buffers.md) |
| `typing.Coroutine[TYield, TSend, TReturn]` | `Task<TYield>` [3](async_support.md) |
| `None` (Return) | `void` |


### Return types

The same type conversion applies for the return type of the Python function, with the additional feature that functions which explicitly return type `None` are declared as `void` in C#.
@@ -294,3 +294,41 @@ The type of `.Send` is the `TSend` type parameter of the `Generator` type annota
var generator = env.ExampleGenerator(5);
string nextValue= generator.Send(10);
```


## Running any Python code

Sometimes you may want to run Python code that doesn't have type annotations or is not part of a module. You can use the `Run` method on the `IPythonEnvironment` to execute any Python code.

Python has two ways to do, this "expressions" and "statements". Expressions return a value and are a single line. You can use the `ExecuteExpression` method to run an expression and get the result back:

```csharp
env.ExecuteExpression("1+1").As<int>() ; // 2
```

You can pass a dictionary of local and/or global variables:

```csharp
var locals = new Dictionary<string, PyObject>
{
["a"] = PyObject.From(101)
};
using var result = env.ExecuteExpression("a+1", locals); // 102
```

To execute a series of statements, you can use the `Execute` method, which also takes globals and locals:
```csharp
var c = """
a = 101
b = c + a
""";
var locals = new Dictionary<string, PyObject>
{
["c"] = PyObject.From(101)
};
var globals = new Dictionary<string, PyObject>
{
["d"] = PyObject.From(100)
};
using var result = env.Execute(c, locals, globals);
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ nav:
- Getting Started: getting-started.md
- Reference: reference.md
- Environment and Package Management: environments.md
- Asynchronous Functions: async_support.md
- Buffer Protocol and NumPy Arrays: buffers.md
- Advanced Usage: advanced.md
- Limitations: limitations.md
63 changes: 63 additions & 0 deletions src/CSnakes.Runtime.Tests/Python/RunTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using CSnakes.Runtime.Python;

namespace CSnakes.Runtime.Tests.Python;
public class RunTests : RuntimeTestBase
{
[Fact]
public void TestSimpleString()
{
using var result = env.ExecuteExpression("1+1");
Assert.Equal("2", result.ToString());
}

[Fact]
public void TestBadString()
{
Assert.Throws<PythonInvocationException>(() => env.ExecuteExpression("1+"));
}

[Fact]
public void TestSimpleStringWithLocals()
{
var locals = new Dictionary<string, PyObject>
{
["a"] = PyObject.From(101)
};
using var result = env.ExecuteExpression("a+1", locals);
Assert.Equal("102", result.ToString());
}

[Fact]
public void TestSimpleStringWithLocalsAndGlobals()
{
var locals = new Dictionary<string, PyObject>
{
["a"] = PyObject.From(101)
};
var globals = new Dictionary<string, PyObject>
{
["b"] = PyObject.From(100)
};
using var result = env.ExecuteExpression("a+b+1", locals, globals);
Assert.Equal("202", result.ToString());
}

[Fact]
public void TestMultilineInput()
{
var c = """
a = 101
b = c + a
""";
var locals = new Dictionary<string, PyObject>
{
["c"] = PyObject.From(101)
};
var globals = new Dictionary<string, PyObject>
{
["d"] = PyObject.From(100)
};
using var result = env.Execute(c, locals, globals);
Assert.Equal("None", result.ToString());
}
}
3 changes: 2 additions & 1 deletion src/CSnakes.Runtime.Tests/RuntimeTestBase.cs
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ public class RuntimeTestBase : IDisposable

public RuntimeTestBase()
{
string pythonVersionWindows = Environment.GetEnvironmentVariable("PYTHON_VERSION") ?? "3.12.4";
string pythonVersionWindows = Environment.GetEnvironmentVariable("PYTHON_VERSION") ?? "3.12.9";
string pythonVersionMacOS = Environment.GetEnvironmentVariable("PYTHON_VERSION") ?? "3.12";
string pythonVersionLinux = Environment.GetEnvironmentVariable("PYTHON_VERSION") ?? "3.12";
bool freeThreaded = Environment.GetEnvironmentVariable("PYTHON_FREETHREADED") == "true";
@@ -25,6 +25,7 @@ public RuntimeTestBase()
pb
.FromNuGet(pythonVersionWindows)
.FromMacOSInstallerLocator(pythonVersionMacOS, freeThreaded)
.FromWindowsStore("3.12")
.FromEnvironmentVariable("Python3_ROOT_DIR", pythonVersionLinux); // This last one is for GitHub Actions

services.AddLogging(builder => builder.AddXUnit());
94 changes: 94 additions & 0 deletions src/CSnakes.Runtime/CPython/Coroutine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using CSnakes.Runtime.Python;
using System.Collections.Concurrent;

namespace CSnakes.Runtime.CPython;
internal unsafe partial class CPythonAPI
{
internal static bool IsPyCoroutine(PyObject p)
{
return HasAttr(p, "__await__");
}

internal class EventLoop : IDisposable
{
private readonly PyObject? loop = null;

public EventLoop()
{
if (NewEventLoopFactory is null)
{
throw new InvalidOperationException("NewEventLoopFactory not initialized");
}
loop = NewEventLoopFactory.Call();
}

public bool IsDisposed { get; private set; }

private void Close()
{
if (loop is null)
{
throw new InvalidOperationException("Event loop not initialized");
}
using var close = loop.GetAttr("close");
close?.Call();
}

public void Dispose()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
Close();
loop?.Dispose();
IsDisposed = true;
}

public PyObject RunTaskUntilComplete(PyObject coroutine, CancellationToken? cancellationToken = null)
{
if (loop is null)
{
throw new InvalidOperationException("Event loop not initialized");
}
using PyObject taskFunc = loop.GetAttr("create_task");
using PyObject task = taskFunc.Call(coroutine);
using PyObject runUntilComplete = loop.GetAttr("run_until_complete");

// On cancellation, call task cancellation in Python
cancellationToken?.Register(() =>
{
using PyObject cancel = task.GetAttr("cancel");
// TODO : send optional message
cancel.Call();
});

return runUntilComplete.Call(task);
}
}

private static ConcurrentBag<EventLoop> eventLoops = [];
[ThreadStatic] private static EventLoop? currentEventLoop = null;
private static PyObject? AsyncioModule = null;
private static PyObject? NewEventLoopFactory = null;

internal static EventLoop GetEventLoop()
{
if (AsyncioModule is null)
{
throw new InvalidOperationException("Asyncio module not initialized");
}
if (currentEventLoop is null || currentEventLoop.IsDisposed)
{
currentEventLoop = new EventLoop();
eventLoops.Add(currentEventLoop);
}
return currentEventLoop!;
}

internal static void CloseEventLoops()
{
foreach (var eventLoop in eventLoops)
{
eventLoop.Dispose();
}
eventLoops.Clear();
}
}
Loading