From e19797603db3e2d4f8bea67d142318c9107f9c83 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Mon, 12 Jan 2026 10:00:05 -0500 Subject: [PATCH 01/14] add tests --- src/embed_tests/TestConverter.cs | 124 +++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index a59b9c97b..a14908516 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -209,6 +209,90 @@ class PyGetListImpl(test.GetListImpl): List result = inst.GetList(); CollectionAssert.AreEqual(new[] { "testing" }, result); } + + /// + /// Test that when a method returns a concrete type implementing IDisposable, + /// the object is wrapped as the concrete type (not IDisposable interface), + /// preserving access to concrete type members and supporting 'with' statements. + /// + [Test] + public void ConcreteTypeImplementingIDisposable_IsWrappedAsConcreteType() + { + using var scope = Py.CreateScope(); + scope.Import(typeof(ConcreteDisposableResource).Namespace, asname: "test"); + + // Reset static state + ConcreteDisposableResource.IsDisposed = false; + ConcreteDisposableResource.InstanceCount = 0; + + // Test that a method returning IDisposable but actually returning a concrete type + // wraps the object as the concrete type, not the interface + scope.Exec(@" +import clr +clr.AddReference('Python.EmbeddingTest') +from Python.EmbeddingTest import ConcreteDisposableResource + +# Get a resource through a method that declares IDisposable return type +resource = ConcreteDisposableResource.GetResource() + +# Verify it's wrapped as the concrete type, not IDisposable +# The concrete type has a GetValue() method that IDisposable doesn't have +value = resource.GetValue() +assert value == 42, f'Expected 42, got {value}' + +# Verify the concrete type name is accessible +type_name = resource.GetType().Name +assert type_name == 'ConcreteDisposableResource', f'Expected ConcreteDisposableResource, got {type_name}' + +# Verify 'with' statement still works (IDisposable support) +with resource: + inside_value = resource.GetValue() + assert inside_value == 42 + assert ConcreteDisposableResource.IsDisposed == False + +# After 'with' block, should be disposed +assert ConcreteDisposableResource.IsDisposed == True +"); + + // Verify the resource was actually disposed + Assert.IsTrue(ConcreteDisposableResource.IsDisposed, "Resource should be disposed after 'with' statement"); + } + + /// + /// Test that Converter.ToPython wraps concrete types implementing interfaces + /// as the concrete type, not the interface, when the declared type is an interface. + /// + [Test] + public void Converter_ToPython_ConcreteTypeOverInterface() + { + using (Py.GIL()) + { + // Create a concrete type that implements IDisposable + var concreteResource = new ConcreteDisposableResource(100); + + // Convert using IDisposable as the declared type (simulating method return type) + var pyObject = Converter.ToPython(concreteResource, typeof(IDisposable)); + + // Verify it's wrapped as the concrete type, not IDisposable + var wrappedObject = ManagedType.GetManagedObject(pyObject.BorrowOrThrow()); + Assert.IsInstanceOf(wrappedObject); + + var clrObject = (CLRObject)wrappedObject; + var wrappedType = clrObject.inst.GetType(); + + // Should be the concrete type, not IDisposable + Assert.AreEqual(typeof(ConcreteDisposableResource), wrappedType); + Assert.AreNotEqual(typeof(IDisposable), wrappedType); + + // Verify we can access concrete type members from Python + using var scope = Py.CreateScope(); + scope.Set("resource", pyObject); + var result = scope.Eval("resource.GetValue()"); + Assert.AreEqual(100, result.As()); + + pyObject.Dispose(); + } + } } public interface IGetList @@ -220,4 +304,44 @@ public class GetListImpl : IGetList { public List GetList() => new() { "testing" }; } + + /// + /// A concrete class implementing IDisposable with additional members. + /// Used to test that methods returning IDisposable but actually returning + /// concrete types are wrapped as the concrete type, not the interface. + /// + public class ConcreteDisposableResource : IDisposable + { + public static bool IsDisposed { get; set; } + public static int InstanceCount { get; set; } + + private readonly int _value; + + public ConcreteDisposableResource(int value = 42) + { + _value = value; + InstanceCount++; + IsDisposed = false; + } + + /// + /// A method that exists only on the concrete type, not on IDisposable. + /// This verifies that the object is wrapped as the concrete type. + /// + public int GetValue() => _value; + + public void Dispose() + { + IsDisposed = true; + } + + /// + /// A method that declares IDisposable return type but actually returns + /// the concrete type. This is the scenario we're testing. + /// + public static IDisposable GetResource() + { + return new ConcreteDisposableResource(); + } + } } From 2444244122b790410df4785d435437fa02e9486d Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 16:33:43 -0500 Subject: [PATCH 02/14] fix build --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e45c16f6a..b3d933914 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,9 +16,9 @@ all runtime; build; native; contentfiles; analyzers - + From a912c29c8164ce90c91758cdc114d6516c819d30 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Mon, 12 Jan 2026 23:59:50 -0500 Subject: [PATCH 03/14] fix build errors --- src/embed_tests/Python.EmbeddingTest.csproj | 2 +- src/embed_tests/TestConverter.cs | 2 +- src/module_tests/Python.ModuleTest.csproj | 2 +- src/perf_tests/Python.PerformanceTests.csproj | 8 +------- src/python_tests_runner/Python.PythonTestsRunner.csproj | 2 +- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 4993994d3..4ecb689d7 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -24,7 +24,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + 1.0.0 all diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index a14908516..83540a16a 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -286,7 +286,7 @@ public void Converter_ToPython_ConcreteTypeOverInterface() // Verify we can access concrete type members from Python using var scope = Py.CreateScope(); - scope.Set("resource", pyObject); + scope.Set("resource", pyObject.MoveToPyObject()); var result = scope.Eval("resource.GetValue()"); Assert.AreEqual(100, result.As()); diff --git a/src/module_tests/Python.ModuleTest.csproj b/src/module_tests/Python.ModuleTest.csproj index 7a8aa9ac3..081b8d871 100644 --- a/src/module_tests/Python.ModuleTest.csproj +++ b/src/module_tests/Python.ModuleTest.csproj @@ -20,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + 1.0.0 all diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index b526183cc..5a5ab5455 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -5,16 +5,10 @@ false x64 x64 - - - - ..\..\pythonnet\runtime\Python.Runtime.dll - true - + - diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index 63981c424..d5cf106d1 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From aa0494e3a2c47fdd4c23ff23af79f28d3f1fdf55 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 16:28:16 -0500 Subject: [PATCH 04/14] resolve cherry-pick conflicts --- src/runtime/Converter.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index c7154ce36..3aca907e7 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -142,8 +142,20 @@ internal static NewReference ToPython(object? value, Type type) if (type.IsInterface) { - var ifaceObj = (InterfaceObject)ClassManager.GetClassImpl(type); - return ifaceObj.TryWrapObject(value); + Type actualType = value.GetType(); + // This fixes issues where methods return concrete types that implement + // interfaces (e.g., IDisposable) but should be exposed as their + // concrete type for proper member access and Python 'with' statement support. + if (!actualType.IsInterface && type == typeof(IDisposable) && typeof(IDisposable).IsAssignableFrom(actualType)) + { + type = actualType; + } + else + { + // Runtime type is also an interface (rare: proxy/dynamic case), wrap as interface + var ifaceObj = (InterfaceObject)ClassManager.GetClassImpl(type); + return ifaceObj.TryWrapObject(value); + } } if (type.IsArray || type.IsEnum) From 9443ef8deed56eb887cd0e69134ee52b8e8e0179 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 18:03:38 -0500 Subject: [PATCH 05/14] update github action to install .net 6.0 for x86 platform --- .github/workflows/main.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a4a74f11..59effcc3a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,10 +45,17 @@ jobs: uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: '6.0.x' + - name: Install .NET 6.0 x86 Runtime (Windows x86 only) + if: ${{ matrix.os.category == 'windows' && matrix.os.platform == 'x86' }} + run: | + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "$env:TEMP\dotnet-install.ps1" + & "$env:TEMP\dotnet-install.ps1" -Architecture x86 -Runtime dotnet -Version 6.0.0 -InstallDir "$env:ProgramFiles(x86)\dotnet" + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: From eb94ece8194cadb037f6e3b296c34b63711cb4c3 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 18:21:07 -0500 Subject: [PATCH 06/14] remove macos build --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59effcc3a..532113349 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,10 +28,6 @@ jobs: platform: x64 instance: ubuntu-22.04 - - category: macos - platform: x64 - instance: macos-13 - python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: From 00d2083374bdd5aed03efe6c9602ac7fc04b6d33 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 23:33:56 -0500 Subject: [PATCH 07/14] undo Install .NET 6.0 x86 Runtime in GH action --- .github/workflows/main.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 532113349..efb9b0822 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,13 +45,6 @@ jobs: with: dotnet-version: '6.0.x' - - name: Install .NET 6.0 x86 Runtime (Windows x86 only) - if: ${{ matrix.os.category == 'windows' && matrix.os.platform == 'x86' }} - run: | - $ProgressPreference = 'SilentlyContinue' - Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "$env:TEMP\dotnet-install.ps1" - & "$env:TEMP\dotnet-install.ps1" -Architecture x86 -Runtime dotnet -Version 6.0.0 -InstallDir "$env:ProgramFiles(x86)\dotnet" - - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: From 58b5eee2e4eae6e059258ed36fe7a3590dda9cf9 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 23:41:53 -0500 Subject: [PATCH 08/14] revert dotnet version in GH action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index efb9b0822..3af81269d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v1 with: dotnet-version: '6.0.x' From 5a6260ea9ab71c16707c3c3fba6a521103bff737 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Wed, 14 Jan 2026 23:51:57 -0500 Subject: [PATCH 09/14] remove windows x86 build from GH action --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3af81269d..7dc94812c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,10 +16,6 @@ jobs: fail-fast: false matrix: os: - - category: windows - platform: x86 - instance: windows-latest - - category: windows platform: x64 instance: windows-latest From 9519794a476e85220f7bfdacb75f050ee3e3889a Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Thu, 15 Jan 2026 00:07:09 -0500 Subject: [PATCH 10/14] try restoring x86 build --- .github/workflows/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7dc94812c..3009dfe04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,10 @@ jobs: fail-fast: false matrix: os: + - category: windows + platform: x86 + instance: windows-latest + - category: windows platform: x64 instance: windows-latest @@ -68,6 +72,12 @@ jobs: Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(python -m find_libpython)" Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')" + - name: Install .NET 6.0 x86 runtime + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '6.0.x' + architecture: ${{ matrix.os.platform }} + - name: Embedding tests run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ env: From 79860400b7d09135b3b5df7458eaf40272c6cce8 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Thu, 15 Jan 2026 10:00:54 -0500 Subject: [PATCH 11/14] install .net 6 runtime for only x86 arch --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3009dfe04..633838956 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,10 +73,11 @@ jobs: Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')" - name: Install .NET 6.0 x86 runtime + if: ${{ matrix.os.category == 'windows' && matrix.os.platform == 'x86' }} uses: actions/setup-dotnet@v4 with: dotnet-version: '6.0.x' - architecture: ${{ matrix.os.platform }} + architecture: x86 - name: Embedding tests run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ From 1de6e4da2a76b2495170d2733df1b787cd25bbe1 Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Thu, 15 Jan 2026 13:41:55 -0500 Subject: [PATCH 12/14] update gh action to fix failing x86 build --- .github/workflows/main.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 633838956..3c71d4cf9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,10 +74,11 @@ jobs: - name: Install .NET 6.0 x86 runtime if: ${{ matrix.os.category == 'windows' && matrix.os.platform == 'x86' }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '6.0.x' - architecture: x86 + run: | + $installScript = "$env:TEMP\dotnet-install.ps1" + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript + & $installScript -Channel "6.0" -Architecture "x86" -InstallDir "${env:ProgramFiles(x86)}\dotnet" -Runtime "dotnet" + Remove-Item $installScript - name: Embedding tests run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ From 8ffa05377dfe3c205345f4c1b59815d88166443b Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Thu, 15 Jan 2026 14:03:34 -0500 Subject: [PATCH 13/14] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b5a0f99..ace09add3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ## [Unreleased][] +### Fixed + +- Fixed an issue where a concrete type object implementing an IDisposable when returned from a method within a `with` statement, for example, was incorrectly resolved as the IDisposable interface type instead of the concrete type. + ### Added - Added support for hiding members from inherited classes. From 6ac8a7b301d10d49af9b336a15472c114663559a Mon Sep 17 00:00:00 2001 From: Aparajit Pratap Date: Thu, 15 Jan 2026 14:05:06 -0500 Subject: [PATCH 14/14] resore macos builds in GH action --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c71d4cf9..e454bb8f3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,10 @@ jobs: platform: x64 instance: ubuntu-22.04 + - category: macos + platform: x64 + instance: macos-13 + python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: