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

Conversation

lostmsu
Copy link
Member

@lostmsu lostmsu commented Mar 28, 2019

What does this implement/fix? Explain your changes.

This change introduces IPyArgumentConverter, that enables hooking into the
process of converting Python argument values into CLR types when invoking
.NET functions from Python. See an example on the new wiki page.

If a user class needs custom conversion for the parameters of its functions, it can specify
the desired implementation of IPyArgumentConverter using PyArgConverter attribute
either on the class/struct, or for the entire assembly.

Does this close any currently open issues?

This implements #823

Any other comments?

This is but one option on how to achieve #823 . I am concerned, that it does not let users specify the converter per instance, and does not allow specifying converter for 3rd-party classes.

This change has an additional performance overhead on the first Bind call on each instance of MethodBinder due to the need to look the attribute up.

Checklist

Check all those that are applicable and complete.

  • Make sure to include one or more tests for your change
  • If an enhancement PR, please create docs and at best an example (see an example)
  • Add yourself to AUTHORS
  • Updated the CHANGELOG

@codecov
Copy link

codecov bot commented Mar 28, 2019

Codecov Report

❗ No coverage uploaded for pull request base (master@33db56d). Click here to learn what that means.
The diff coverage is 86.58%.

Impacted file tree graph

@@           Coverage Diff            @@
##             master    #835   +/-   ##
========================================
  Coverage          ?   65.2%           
========================================
  Files             ?      66           
  Lines             ?    5970           
  Branches          ?     994           
========================================
  Hits              ?    3893           
  Misses            ?    1719           
  Partials          ?     358
Flag Coverage Δ
#setup_linux 65.3% <ø> (?)
#setup_windows 64.43% <86.58%> (?)
Impacted Files Coverage Δ
src/runtime/pyargconverter.cs 81.81% <81.81%> (ø)
src/runtime/methodbinder.cs 64.54% <87.09%> (ø)
src/runtime/defaultpyargconverter.cs 87.5% <87.5%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 33db56d...8448fe6. Read the comment docs.

@lostmsu lostmsu marked this pull request as ready for review March 28, 2019 23:39
Copy link
Member

@filmor filmor left a comment

Choose a reason for hiding this comment

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

This is already a huge step forward, thank you very much :)

Regarding your concerns, I can't think of a case where one would want per-instance conversion. In that case I'd rather extend the interface to pass the instance object along s.t. the converter can branch out on its own.

Third party conversion should be quite simple to add, I commented on the respective section.

using System.Reflection;

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.

?? 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 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 :)

@filmor filmor added this to the 2.4.1 milestone Apr 12, 2019
@lostmsu
Copy link
Member Author

lostmsu commented Apr 17, 2019

The build is failing, but does not seem to be related to the changes in this PR.

@filmor
Copy link
Member

filmor commented Apr 23, 2019

I've retriggered the build and will have a look at your changes :)

@lostmsu
Copy link
Member Author

lostmsu commented May 8, 2019

@filmor I've discovered, that this does not entirely cover my own use case as-is. Perhaps it needs more refining before upstreaming.

The use case I have is .NET wrapper classes for Python classes. E.g. classes in .NET, that have interface, that copies some Python class, and a single PyObject field pointing to the actual Python object.

Problem I faced is that it is unclear what to do when a wrapper class is inherited.

// using default arg converter
class Wrapper {
   PyObject impl;
   public virtual void Overridable(CustomType o) => impl.overridable(o);
   public void NonOverridable(CustomType o) => impl.non_overridable(o);
}

[PyArgConverter(typeof(CustomArgConverter))]
class UserDerived: Wrapper {
  public override void Overridable(CustomType o) => o.DoSomething();
}

Now I send a wrapped instance of UserDerived to Python code. While it is clear, that if Python calls Overridable on it, the argument should be converted using CustomArgConverter, it is unclear what converter should be used in case Python calls NonOverridable.

In my use case, CustomArgConverter must be applied to NonOverridable too. However, the implementation in this PR will not do this, as NonOverridable.DeclaringType is Wrapper, which does not have PyArgConverter attribute.

I can also see an opposite case, when the base class actually relies on using a specific converter.

@lostmsu
Copy link
Member Author

lostmsu commented May 12, 2019

Nevermind, I figured it out. It works as is.

@lostmsu
Copy link
Member Author

lostmsu commented Jun 17, 2019

@filmor do not rush this change. After playing a bit with it, I think we might need to change inherit: false into inherit: true, and add a couple of tests. The point being that the custom converter should be applied to derived classes automatically (which would be a breaking change if made later on).

Also, looks like it reformats .csproj file, which is unnecessary.

@filmor
Copy link
Member

filmor commented Jun 17, 2019

Yeah, this one I saw as well. I just wanted to see how this fares test-wise against master :)

@lostmsu
Copy link
Member Author

lostmsu commented Jun 20, 2019

Hm, the .csproj change seems to be a result of fda9499

@lostmsu lostmsu force-pushed the PR/PyArgConverter branch from 07c096e to cfc9336 Compare August 22, 2019 21:09
@lostmsu
Copy link
Member Author

lostmsu commented Aug 23, 2019

Alrightie. Caught a bug in the original implementation, caused by partial initialization of MethodBinder when MethodInfo to be invoked is explicitly passed to Bind. Should now be mergeable.

@codecov-io
Copy link

codecov-io commented Aug 23, 2019

Codecov Report

Merging #835 into master will increase coverage by 0.04%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #835      +/-   ##
==========================================
+ Coverage   86.71%   86.75%   +0.04%     
==========================================
  Files           1        1              
  Lines         301      302       +1     
==========================================
+ Hits          261      262       +1     
  Misses         40       40
Flag Coverage Δ
#setup_linux 65.56% <100%> (+0.11%) ⬆️
#setup_windows 71.52% <100%> (+0.09%) ⬆️
Impacted Files Coverage Δ
setup.py 86.75% <100%> (+0.04%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update aaafea8...3804438. Read the comment docs.

@lostmsu lostmsu requested a review from filmor August 23, 2019 00:38
@lostmsu
Copy link
Member Author

lostmsu commented Oct 8, 2019

@filmor , any chance you could review this? It is ready for merging.

@filmor
Copy link
Member

filmor commented Oct 9, 2019

Could you rebase it on the current master? There was a change in the MethodBinder in between.

@lostmsu
Copy link
Member Author

lostmsu commented Oct 21, 2019

AppVeyor seems to have succeeded, but for some reason not being picked up: https://ci.appveyor.com/project/pythonnet/pythonnet/builds/28269629

@filmor rebased on master

@lostmsu
Copy link
Member Author

lostmsu commented Oct 24, 2019

@filmor review?

@lostmsu lostmsu removed this from the 2.4.1 milestone Feb 27, 2020
@lostmsu
Copy link
Member Author

lostmsu commented Feb 27, 2020

Removed from 2.x, will review for 3.x. Might have been superseded by codecs #1022

@lostmsu lostmsu closed this Mar 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants