From 5bb2566f7985162c74ea88af7e19f259f1c72436 Mon Sep 17 00:00:00 2001
From: Tom Minka <8955276+tminka@users.noreply.github.com>
Date: Thu, 31 Dec 2020 22:02:24 +0000
Subject: [PATCH 1/4] Python tests can now be debugged by running them as
embedded tests within NUnit. Added PythonTestsRunner project and extra build
actions.
---
.github/workflows/main.yml | 6 +-
pythonnet.sln | 26 ++++++
.../Python.PythonTestsRunner.csproj | 25 ++++++
src/python_tests_runner/PythonTestRunner.cs | 82 +++++++++++++++++++
4 files changed, 138 insertions(+), 1 deletion(-)
create mode 100644 src/python_tests_runner/Python.PythonTestsRunner.csproj
create mode 100644 src/python_tests_runner/PythonTestRunner.cs
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8137d0b0a..3ca6a27bb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -49,9 +49,13 @@ jobs:
- name: Python Tests
run: pytest
- - name: Run Embedding tests
+ - name: Embedding tests
run: dotnet test --runtime any-${{ matrix.platform }} src/embed_tests/
if: ${{ matrix.os != 'macos' }} # Not working right now, doesn't find libpython
+ - name: Python tests runner
+ run: dotnet test --runtime any-${{ matrix.platform }} src/python_tests_runner/
+ if: ${{ matrix.os != 'macos' }} # Not working right now, doesn't find libpython
+
# TODO: Run perf tests
# TODO: Run mono tests on Windows?
diff --git a/pythonnet.sln b/pythonnet.sln
index fcad97d5c..e0fbeede7 100644
--- a/pythonnet.sln
+++ b/pythonnet.sln
@@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{BC426F42
tools\geninterop\geninterop.py = tools\geninterop\geninterop.py
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.PythonTestsRunner", "src\python_tests_runner\Python.PythonTestsRunner.csproj", "{35CBBDEB-FC07-4D04-9D3E-F88FC180110B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -129,6 +131,30 @@ Global
{4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|x64.Build.0 = Release|x64
{4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|x86.ActiveCfg = Release|x86
{4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|x86.Build.0 = Release|x86
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|x64.Build.0 = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Debug|x86.Build.0 = Debug|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|x64.ActiveCfg = Release|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|x64.Build.0 = Release|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|x86.ActiveCfg = Release|Any CPU
+ {6CF9EEA0-F865-4536-AABA-739AE3DA971E}.Release|x86.Build.0 = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|x64.Build.0 = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Debug|x86.Build.0 = Debug|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x64.ActiveCfg = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x64.Build.0 = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x86.ActiveCfg = Release|Any CPU
+ {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj
new file mode 100644
index 000000000..2d6544614
--- /dev/null
+++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net472;netcoreapp3.1
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ 1.0.0
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
diff --git a/src/python_tests_runner/PythonTestRunner.cs b/src/python_tests_runner/PythonTestRunner.cs
new file mode 100644
index 000000000..79b15700e
--- /dev/null
+++ b/src/python_tests_runner/PythonTestRunner.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+using NUnit.Framework;
+
+using Python.Runtime;
+
+namespace Python.PythonTestsRunner
+{
+ public class PythonTestRunner
+ {
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ PythonEngine.Initialize();
+ }
+
+ [OneTimeTearDown]
+ public void Dispose()
+ {
+ PythonEngine.Shutdown();
+ }
+
+ ///
+ /// Selects the Python tests to be run as embedded tests.
+ ///
+ ///
+ static IEnumerable PythonTestCases()
+ {
+ // Add the test that you want to debug here.
+ yield return new[] { "test_enum", "test_enum_standard_attrs" };
+ yield return new[] { "test_generic", "test_missing_generic_type" };
+ }
+
+ ///
+ /// Runs a test in src/tests/*.py as an embedded test. This facilitates debugging.
+ ///
+ /// The file name without extension
+ /// The name of the test method
+ [TestCaseSource(nameof(PythonTestCases))]
+ public void RunPythonTest(string testFile, string testName)
+ {
+ // Find the tests directory
+ string folder = typeof(PythonTestRunner).Assembly.Location;
+ while (Path.GetFileName(folder) != "src")
+ {
+ folder = Path.GetDirectoryName(folder);
+ }
+ folder = Path.Combine(folder, "tests");
+ string path = Path.Combine(folder, testFile + ".py");
+ if (!File.Exists(path)) throw new FileNotFoundException("Cannot find test file", path);
+
+ // We could use 'import' below, but importlib gives more helpful error messages than 'import'
+ // https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
+ // Because the Python tests sometimes have relative imports, the module name must be inside the tests package
+ PythonEngine.Exec($@"
+import sys
+import os
+sys.path.append(os.path.dirname(r'{folder}'))
+sys.path.append(os.path.join(r'{folder}', 'fixtures'))
+import clr
+clr.AddReference('Python.Test')
+import tests
+module_name = 'tests.{testFile}'
+file_path = r'{path}'
+import importlib.util
+spec = importlib.util.spec_from_file_location(module_name, file_path)
+module = importlib.util.module_from_spec(spec)
+sys.modules[module_name] = module
+try:
+ spec.loader.exec_module(module)
+except ImportError as error:
+ raise ImportError(str(error) + ' when sys.path=' + os.pathsep.join(sys.path))
+module.{testName}()
+");
+ }
+ }
+}
From 0ccc443d905afa56f04ec48ffdfddf61b3a13955 Mon Sep 17 00:00:00 2001
From: Tom Minka <8955276+tminka@users.noreply.github.com>
Date: Mon, 4 Jan 2021 23:06:48 +0000
Subject: [PATCH 2/4] ImportHook preserves the original exception message when
an import fails
---
.github/workflows/main.yml | 4 ++--
src/runtime/importhook.cs | 5 ++++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3ca6a27bb..7c53d7522 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -53,9 +53,9 @@ jobs:
run: dotnet test --runtime any-${{ matrix.platform }} src/embed_tests/
if: ${{ matrix.os != 'macos' }} # Not working right now, doesn't find libpython
- - name: Python tests runner
+ - name: Python tests run from .NET
run: dotnet test --runtime any-${{ matrix.platform }} src/python_tests_runner/
- if: ${{ matrix.os != 'macos' }} # Not working right now, doesn't find libpython
+ if: ${{ matrix.os == 'windows' }} # Not working for others right now
# TODO: Run perf tests
# TODO: Run mono tests on Windows?
diff --git a/src/runtime/importhook.cs b/src/runtime/importhook.cs
index d8f7e4dcc..c5c81e0c3 100644
--- a/src/runtime/importhook.cs
+++ b/src/runtime/importhook.cs
@@ -291,6 +291,9 @@ public static IntPtr __import__(IntPtr self, IntPtr args, IntPtr kw)
// We don't support them anyway
return IntPtr.Zero;
}
+ // Save the exception
+ var originalException = new PythonException();
+ var originalExceptionMessage = originalException.ToString();
// Otherwise, just clear the it.
Exceptions.Clear();
@@ -342,7 +345,7 @@ public static IntPtr __import__(IntPtr self, IntPtr args, IntPtr kw)
ManagedType mt = tail.GetAttribute(name, true);
if (!(mt is ModuleObject))
{
- Exceptions.SetError(Exceptions.ImportError, $"No module named {name}");
+ Exceptions.SetError(Exceptions.ImportError, originalExceptionMessage);
return IntPtr.Zero;
}
if (head == null)
From 0a88f27fa19959a6cf0c7f0ca43e5c1720839a39 Mon Sep 17 00:00:00 2001
From: Tom Minka <8955276+tminka@users.noreply.github.com>
Date: Tue, 5 Jan 2021 00:27:36 +0000
Subject: [PATCH 3/4] Use PythonException.Restore
---
src/runtime/importhook.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/runtime/importhook.cs b/src/runtime/importhook.cs
index c5c81e0c3..af6174188 100644
--- a/src/runtime/importhook.cs
+++ b/src/runtime/importhook.cs
@@ -293,7 +293,6 @@ public static IntPtr __import__(IntPtr self, IntPtr args, IntPtr kw)
}
// Save the exception
var originalException = new PythonException();
- var originalExceptionMessage = originalException.ToString();
// Otherwise, just clear the it.
Exceptions.Clear();
@@ -345,7 +344,7 @@ public static IntPtr __import__(IntPtr self, IntPtr args, IntPtr kw)
ManagedType mt = tail.GetAttribute(name, true);
if (!(mt is ModuleObject))
{
- Exceptions.SetError(Exceptions.ImportError, originalExceptionMessage);
+ originalException.Restore();
return IntPtr.Zero;
}
if (head == null)
From 4ac44fee0edaa5da94c18266bc5126921a2e60a6 Mon Sep 17 00:00:00 2001
From: Tom Minka <8955276+tminka@users.noreply.github.com>
Date: Tue, 5 Jan 2021 00:31:53 +0000
Subject: [PATCH 4/4] Updated CHANGELOG
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1442075ef..53c45f419 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,7 @@ details about the cause of the failure
- Made it possible to call `ToString`, `GetHashCode`, and `GetType` on inteface objects
- Fixed objects returned by enumerating `PyObject` being disposed too soon
- Incorrectly using a non-generic type with type parameters now produces a helpful Python error instead of throwing NullReferenceException
+- `import` may now raise errors with more detail than "No module named X"
### Removed