diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8137d0b0a..7c53d7522 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 run from .NET
+ run: dotnet test --runtime any-${{ matrix.platform }} src/python_tests_runner/
+ if: ${{ matrix.os == 'windows' }} # Not working for others right now
+
# TODO: Run perf tests
# TODO: Run mono tests on Windows?
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
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}()
+");
+ }
+ }
+}
diff --git a/src/runtime/importhook.cs b/src/runtime/importhook.cs
index d8f7e4dcc..af6174188 100644
--- a/src/runtime/importhook.cs
+++ b/src/runtime/importhook.cs
@@ -291,6 +291,8 @@ 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();
// Otherwise, just clear the it.
Exceptions.Clear();
@@ -342,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, $"No module named {name}");
+ originalException.Restore();
return IntPtr.Zero;
}
if (head == null)