Skip to content

Enable user classes to override how Python.NET processes parameters of their functions #835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- Added function that sets Py_NoSiteFlag to 1.
- Added support for Jetson Nano.
- Added support for __len__ for .NET classes that implement ICollection
- Added `IPyArgumentConverter` interface and `PyArgConverter` attribute, that control custom argument marshaling from Python to .NET (#835)

### Changed

Expand Down
88 changes: 88 additions & 0 deletions src/embed_tests/TestCustomArgMarshal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using NUnit.Framework;
using Python.Runtime;

namespace Python.EmbeddingTest
{
class TestCustomArgMarshal
{
[OneTimeSetUp]
public void SetUp()
{
PythonEngine.Initialize();
}

[OneTimeTearDown]
public void Dispose()
{
PythonEngine.Shutdown();
}

[Test]
public void CustomArgMarshaller()
{
var obj = new CustomArgMarshaling();
using (Py.GIL()) {
dynamic callWithInt = PythonEngine.Eval("lambda o: o.CallWithInt('42')");
callWithInt(obj.ToPython());
}
Assert.AreEqual(expected: 42, actual: obj.LastArgument);
}

[Test]
public void MarshallerOverride() {
var obj = new DerivedMarshaling();
using (Py.GIL()) {
dynamic callWithInt = PythonEngine.Eval("lambda o: o.CallWithInt({ 'value': 42 })");
callWithInt(obj.ToPython());
}
Assert.AreEqual(expected: 42, actual: obj.LastArgument);
}
}

[PyArgConverter(typeof(CustomArgConverter))]
class CustomArgMarshaling {
public object LastArgument { get; protected set; }
public virtual void CallWithInt(int value) => this.LastArgument = value;
}

// this should override original custom marshaling behavior for any new methods
[PyArgConverter(typeof(CustomArgConverter2))]
class DerivedMarshaling : CustomArgMarshaling {
public override void CallWithInt(int value) {
base.CallWithInt(value);
}
}

class CustomArgConverter : DefaultPyArgumentConverter {
public override bool TryConvertArgument(IntPtr pyarg, Type parameterType, bool needsResolution,
out object arg, out bool isOut) {
if (parameterType != typeof(int))
return base.TryConvertArgument(pyarg, parameterType, needsResolution, out arg, out isOut);

bool isString = base.TryConvertArgument(pyarg, typeof(string), needsResolution,
out arg, out isOut);
if (!isString) return false;

int number;
if (!int.TryParse((string)arg, out number)) return false;
arg = number;
return true;
}
}

class CustomArgConverter2 : DefaultPyArgumentConverter {
public override bool TryConvertArgument(IntPtr pyarg, Type parameterType, bool needsResolution,
out object arg, out bool isOut) {
if (parameterType != typeof(int))
return base.TryConvertArgument(pyarg, parameterType, needsResolution, out arg, out isOut);
bool isPyObject = base.TryConvertArgument(pyarg, typeof(PyObject), needsResolution,
out arg, out isOut);
if (!isPyObject) return false;
var dict = new PyDict((PyObject)arg);
int number = (dynamic)dict["value"];
arg = number;
return true;
}
}
}
2 changes: 2 additions & 0 deletions src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
<Compile Include="converter.cs" />
<Compile Include="CustomMarshaler.cs" />
<Compile Include="debughelper.cs" />
<Compile Include="defaultpyargconverter.cs" />
<Compile Include="delegatemanager.cs" />
<Compile Include="delegateobject.cs" />
<Compile Include="eventbinding.cs" />
Expand Down Expand Up @@ -122,6 +123,7 @@
<Compile Include="overload.cs" />
<Compile Include="propertyobject.cs" />
<Compile Include="pyansistring.cs" />
<Compile Include="pyargconverter.cs" />
<Compile Include="pydict.cs" />
<Compile Include="pyfloat.cs" />
<Compile Include="pyint.cs" />
Expand Down
111 changes: 111 additions & 0 deletions src/runtime/defaultpyargconverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
namespace Python.Runtime {
using System;

/// <summary>
/// The implementation of <see cref="T:Python.Runtime.IPyArgumentConverter" /> used by default
/// </summary>
public class DefaultPyArgumentConverter: IPyArgumentConverter
{
/// <summary>
/// Gets the singleton instance.
/// </summary>
public static DefaultPyArgumentConverter Instance { get; } = new DefaultPyArgumentConverter();

/// <inheritdoc />
/// <summary>
/// Attempts to convert an argument passed by Python to the specified parameter type.
/// </summary>
/// <param name="pyarg">Unmanaged pointer to the Python argument value</param>
/// <param name="parameterType">The expected type of the parameter</param>
/// <param name="needsResolution"><c>true</c> if the method is overloaded</param>
/// <param name="arg">This parameter will receive the converted value, matching the specified type</param>
/// <param name="isOut">This parameter will be set to <c>true</c>,
/// if the final type needs to be marshaled as an out argument.</param>
/// <returns><c>true</c>, if the object matches requested type,
/// and conversion was successful, otherwise <c>false</c></returns>
public virtual bool TryConvertArgument(
IntPtr pyarg, Type parameterType, bool needsResolution,
out object arg, out bool isOut)
{
arg = null;
isOut = false;
Type clrType = TryComputeClrArgumentType(parameterType, pyarg, needsResolution: needsResolution);
if (clrType == null)
{
return false;
}

if (!Converter.ToManaged(pyarg, clrType, out arg, false))
{
Exceptions.Clear();
return false;
}

isOut = clrType.IsByRef;
return true;
}

static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool needsResolution)
{
// this logic below handles cases when multiple overloading methods
// are ambiguous, hence comparison between Python and CLR types
// is necessary
Type clrType = null;
IntPtr pyArgType;
if (needsResolution)
{
// HACK: each overload should be weighted in some way instead
pyArgType = Runtime.PyObject_Type(argument);
Exceptions.Clear();
if (pyArgType != IntPtr.Zero)
{
clrType = Converter.GetTypeByAlias(pyArgType);
}
Runtime.XDecref(pyArgType);
}

if (clrType != null)
{
if ((parameterType != typeof(object)) && (parameterType != clrType))
{
IntPtr pyParamType = Converter.GetPythonTypeByAlias(parameterType);
pyArgType = Runtime.PyObject_Type(argument);
Exceptions.Clear();

bool typeMatch = false;
if (pyArgType != IntPtr.Zero && pyParamType == pyArgType)
{
typeMatch = true;
clrType = parameterType;
}
if (!typeMatch)
{
// this takes care of enum values
TypeCode argTypeCode = Type.GetTypeCode(parameterType);
TypeCode paramTypeCode = Type.GetTypeCode(clrType);
if (argTypeCode == paramTypeCode)
{
typeMatch = true;
clrType = parameterType;
}
}
Runtime.XDecref(pyArgType);
if (!typeMatch)
{
return null;
}
}
else
{
clrType = parameterType;
}
}
else
{
clrType = parameterType;
}

return clrType;
}
}
}
Loading