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