Skip to content

List conversion fails on new version of pythonnet compared to 3.0.0.post1 #2004

Open

Description

@screig

Environment

  • Pythonnet version:
    Works with pythonnet==3.0.0.post1
    Failes with pythonnet>=3.0.0.post1

  • Python version:
    Python 3.8.0

  • Operating System:
    Windows

  • .NET Runtime:
    netstandard2.0;net471

Details

I followed the notes to write a codec to convert a C# List to a Python list, here it is:

  /// <summary>
   /// This class will convert a C# list of strings to a Python list [] of strings.
   /// </summary>
   public class ListStringConversions : IPyObjectEncoder
   {
       bool IPyObjectEncoder.CanEncode(Type type)
       {
           return type == typeof(IList<string>);
       }

       PyObject IPyObjectEncoder.TryEncode(object value)
       {
           using (var scope = Py.CreateScope())
           {
               scope.Exec($"a = []");
               var input = (List<string>)value;
               foreach (var item in input)
                   scope.Exec($"a.append('{item}')");  // {item}
               return scope.Get("a");
           }
       }
   }

I have a simple test which calls in to a C# assembly and returns a list. With pythonnet version 3.0.0.post1, the C# list is successfully converted to a python list.

In my requirements file, I had this
pythonnet>=3.0.0.post1

Now on the CICD process it has updated the pythonnet package and the same test fails:

>       assert isinstance(codes, list)
E       assert False
E        +  where False = isinstance(<System.Collections.Generic.IList[String] object at 0x0000023C1D9D5180>, list)

The code no longer is returning a python list. I switched the requirements file to
pythonnet==3.0.0.post1

and the test passes. As such I think there is a breaking change in the latest release that prevents the conversions been executed.

Activity

  1. filmor commented on Nov 7, 2022

    @filmor
    Member

    What is the actual test that you are running here?

  2. screig commented on Nov 7, 2022

    @screig
    Author
    def test_eq_get_meta_marketcodes(self):
        logger.info("Get all the meta data codes")
        codes = eqg.eq_get_meta_marketcodes()
        assert isinstance(codes, list)
        assert len(codes) > 40
    
  3. filmor commented on Nov 7, 2022

    @filmor
    Member

    Can you please boil this down to a reproducible example? The potentially relevant change is #1986, but I can't verify that like this.

    (As a side note, this way of implementing the conversion is a recipe for disaster. Imagine someone gets the string "'); import os; os.system('rm -rf /'); (')" into any list that is returned from C#).

  4. screig commented on Nov 7, 2022

    @screig
    Author

    Well I would love to see more examples on conversions, but there seem to be very thin on the ground

  5. lostmsu commented on Nov 7, 2022

    @lostmsu
    Member
    return type == typeof(IList<string>);

    This never could have worked because no concrete instance has type of IList<string>.

  6. screig commented on Nov 7, 2022

    @screig
    Author

    No that still fails, even with this change, I will make you a stand alone test later (am at work)

  7. screig commented on Nov 8, 2022

    @screig
    Author

    I made you a sample project

    https://github.com/screig/pythonnet_test

    It runs the tests under Github actions, there are two jobs. Which are identical apart from, one does this

       pip install pythonnet==3.0.0.post1
    

    and the other, does this:

       pip install pythonnet==3.0.1
    

    https://github.com/screig/pythonnet_test/actions/runs/3418546969

    As you can see 3.0.1 fails

  8. filmor commented on Nov 8, 2022

    @filmor
    Member

    Great, thank you for the example, I'll look into it.

  9. reopened this on Nov 8, 2022
  10. filmor commented on Nov 9, 2022

    @filmor
    Member

    Wow, this is a weird one:

    The issue is due to the fact that you reference Python.NET 3.0.0 in your project explicitly. This leads to your RegisterEncoder call being run in the 3.0.0 instance, while the version loaded by Python (3.0.1) looks up the encoders in its own instance of the respective variable. We have to prevent loading of multiple instances of Python.Runtime.dll, I'll see if I can implement that.

  11. filmor commented on Nov 9, 2022

    @filmor
    Member

    It's actually enough to just delete the Python.Runtime.dll from your lib directory. Instead of doing a manual copy, dotnet publish the project to a lib directory and reference pythonnet like this:

    <PackageReference Include="pythonnet" Version="3.0.0" ExcludeAssets="runtime" />

    I'll update the documentation accordingly. @lostmsu, are you aware of a way to prevent loading multiple versions of a DLL?

  12. lostmsu commented on Nov 9, 2022

    @lostmsu
    Member

    @filmor can always scan the loaded assemblies like we do for reflecting types. Just skip the assemblies loaded in reflection only context.

  13. filmor commented on Nov 10, 2022

    @filmor
    Member

    I don't know what you mean. User assemblies are just loaded using Assembly.Load, which resolves the dependencies automatically and (to my knowledge) without an option to modify behaviour. We could consider splitting up into a "loader" DLL and the actual Runtime, ensuring that all globals are registered in the loader if available (this is also relevant for our instance tracking). The simplest solution would of course be to just prevent the .NET runtime from loading the second version. We could also build separate nuget packages for both scenarios.

    (By the way, I just tested it out, this is only a problem for .NET Framework/Mono, .NET Core only loads the first version it finds)

  14. lostmsu commented on Nov 10, 2022

    @lostmsu
    Member

    @filmor we also scan already loaded assemblies. E.g. AppDomain.CurrentDomain.GetAssemblies()

  15. filmor commented on Nov 10, 2022

    @filmor
    Member

    How does that help?

  16. lostmsu commented on Nov 10, 2022

    @lostmsu
    Member

    @filmor upon initialization of Python.NET you can call that method and ensure there is only one Python.Runtime loaded, or throw otherwise.

  17. filmor commented on Nov 10, 2022

    @filmor
    Member

    Python.NET is already initialized at that point.

    import clr  # => Python.Runtime.dll is loaded from site-packages
    
    clr.AddReference("SomeDll")  # => SomeDll has a reference to Python.Runtime.dll *and* has another instance next to it
    
    SomeDll.SomeFunctionThatCallsAPythonRuntimeFunction()  # => Calls into the second instance of the Python.Runtime assembly => registration is "ignored" in the main one
  18. lostmsu commented on Nov 10, 2022

    @lostmsu
    Member

    This check can go in many places. For instance, you could use PyObject static constructor. Or you could add this check to AssemblyManager.ScanAssembly

  19. filmor commented on Nov 10, 2022

    @filmor
    Member

    The only thing we could do at that point would be to throw an exception, though. I don't think there is a way to fix the situation once the second instance is loaded.

  20. lostmsu commented on Nov 10, 2022

    @lostmsu
    Member

    I don't think there's anything for us to fix here. This is an unsupported situation. Detect and raise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions