8000 Enable user classes to override how Python.NET processes parameters of their functions by lostmsu · Pull Request #835 · pythonnet/pythonnet · GitHub
[go: up one dir, main page]

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 1 commit
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
Prev Previous commit
Next Next commit
enabled an ability to override Python to .NET argument coversion usin…
…g PyArgConverter attribute
  • Loading branch information
lostmsu committed Jan 31, 2020
commit b5cae6cd361d2357474527761103b25222140744
55 changes: 55 additions & 0 deletions src/embed_tests/TestCustomArgMarshal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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);
}
}

[PyArgConverter(typeof(CustomArgConverter))]
class CustomArgMarshaling {
public object LastArgument { get; private set; }
public void CallWithInt(int value) => this.LastArgument = 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;
}
}
}
39 changes: 36 additions & 3 deletions src/runtime/methodbinder.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using System.Linq;

namespace Python.Runtime
{
using System.Linq;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to the top.


/// <summary>
/// A MethodBinder encapsulates information about a (possibly overloaded)
/// managed method, and is responsible for selecting the right method given
Expand All @@ -19,16 +22,16 @@ internal class MethodBinder
public MethodBase[] methods;
public bool init = false;
public bool allow_threads = true;
readonly IPyArgumentConverter pyArgumentConverter = DefaultPyArgumentConverter.Instance;
IPyArgumentConverter pyArgumentConverter;

internal MethodBinder()
{
list = new ArrayList();
}

internal MethodBinder(MethodInfo mi)
internal MethodBinder(MethodInfo mi): this()
{
list = new ArrayList { mi };
this.AddMethod(mi);
}

public int Count
Expand All @@ -38,6 +41,7 @@ public int Count

internal void AddMethod(MethodBase m)
{
Debug.Assert(!init);
list.Add(m);
}

Expand Down Expand Up @@ -165,11 +169,40 @@ internal MethodBase[] GetMethods()
// I'm sure this could be made more efficient.
list.Sort(new MethodSorter());
methods = (MethodBase[])list.ToArray(typeof(MethodBase));
pyArgumentConverter = this.GetArgumentConverter();
init = true;
}
return methods;
}

IPyArgumentConverter GetArgumentConverter() {
IPyArgumentConverter converter = null;
Type converterType = null;
foreach (MethodBase method in this.methods)
{
var attribute = method.DeclaringType?
.GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: false)
.OfType<PyArgConverterAttribute>()
.SingleOrDefault()
?? method.DeclaringType?.Assembly
.GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: false)
.OfType<PyArgConverterAttribute>()
.SingleOrDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you could additionally introduce a "global" (maybe per engine) lookup table (class -> converter) into which one could register the marshalers even if one can't modify the declaring assembly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not want to overoptimize at first. Weren't there some issues with AppDomains for global caches?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, there is plenty of brokenness in that general direction, that's why I put the "(maybe per engine)" there. We have other global data though already, and for the one usecase of multiple AppDomains that I know 8000 of (reloading code in Unity) it works because they take down the PythonEngine and restart it, s.t. always exactly one is running. The comment was also more of a "fyi", not a requirement for merging :)

if (converterType == null)
{
if (attribute == null) continue;

converterType = attribute.ConverterType;
converter = attribute.Converter;
} else if (converterType != attribute?.ConverterType)
{
throw new NotSupportedException("All methods must have the same IPyArgumentConverter");
}
}

return converter ?? DefaultPyArgumentConverter.Instance;
}

/// <summary>
/// Precedence algorithm largely lifted from Jython - the concerns are
/// generally the same so we'll start with this and tweak as necessary.
Expand Down
44 changes: 43 additions & 1 deletion src/runtime/pyargconverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ bool TryConvertArgument(IntPtr pyarg, Type parameterType,
bool needsResolution, out object arg, out bool isOut);
}

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

/// <summary>
/// Attempts to convert an argument passed by Python to the specified parameter type.
Expand All @@ -44,4 +50,40 @@ public virtual bool TryConvertArgument(
out arg, out isOut);
}
}

/// <summary>
/// Specifies an argument converter to be used, when methods in this class/assembly are called from Python.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct)]
public class PyArgConverterAttribute : Attribute
{
static readonly Type[] EmptyArgTypeList = new Type[0];
static readonly object[] EmptyArgList = new object[0];

/// <summary>
/// Gets the instance of the converter, that will be used when calling methods
/// of this class/assembly from Python
/// </summary>
public IPyArgumentConverter Converter { get; }
/// <summary>
/// Gets the type of the converter, that will be used when calling methods
/// of this class/assembly from Python
/// </su 6167 mmary>
public Type ConverterType { get; }

/// <summary>
/// Specifies an argument converter to be used, when methods
/// in this class/assembly are called from Python.
/// </summary>
/// <param name="converterType">Type of the converter to use.
/// Must implement <see cref="IPyArgumentConverter"/>.</param>
public PyArgConverterAttribute(Type converterType)
{
if (converterType == null) throw new ArgumentNullException(nameof(converterType));
var ctor = converterType.GetConstructor(EmptyArgTypeList);
if (ctor == null) throw new ArgumentException("Specified converter must have public parameterless constructor");
this.Converter = (IPyArgumentConverter)ctor.Invoke(EmptyArgList);
this.ConverterType = converterType;
}
}
}
0