diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 60c94dbeb..521a6cfdc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python uses: astral-sh/setup-uv@v7 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b1bee82c..bae02377f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,31 +36,14 @@ jobs: instance: ubuntu-22.04-arm suffix: "" - - category: macos - platform: x64 - instance: macos-15 - suffix: -macos-x86_64-none - - category: macos platform: arm64 instance: macos-15 - suffix: -macos-aarch64-none + suffix: "" - python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"] exclude: - # Fails with initfs_encoding error - - os: - category: windows - platform: x86 - python: "3.10" - - # Fails to find pytest - - os: - category: windows - platform: x64 - python: '3.10' - # fails to call mono methods - os: category: windows @@ -71,7 +54,7 @@ jobs: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 # Use main until support for architecture lands - name: Setup .NET diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml index 6de97d50e..ec39c8d8a 100644 --- a/.github/workflows/nuget-preview.yml +++ b/.github/workflows/nuget-preview.yml @@ -10,7 +10,7 @@ jobs: name: Release Preview runs-on: ubuntu-latest environment: NuGet - timeout-minutes: 10 + timeout-minutes: 30 env: PYTHONNET_SHUTDOWN_MODE: Normal @@ -21,30 +21,25 @@ jobs: echo "DATE_VER=$(date "+%Y-%m-%d")" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: - dotnet-version: '6.0.x' + dotnet-version: '10.0.x' - - name: Set up Python 3.8 - uses: actions/setup-python@v6 + - name: Set up Python 3.11 + uses: astral-sh/setup-uv@v7 with: - python-version: 3.8 - architecture: x64 + python-version: cpython-3.11 + cache-python: true + activate-environment: true + enable-cache: true - name: Install dependencies - run: | - pip install --upgrade -r requirements.txt - pip install numpy # for tests - - - name: Build and Install - run: | - pip install -v . + run: uv sync --managed-python - - name: Set Python DLL path (non Windows) - if: ${{ matrix.os != 'windows' }} + - name: Set Python DLL path run: | echo PYTHONNET_PYDLL=$(python -m find_libpython) >> $GITHUB_ENV diff --git a/CHANGELOG.md b/CHANGELOG.md index 546413849..737472caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. +## 3.2.0 - 2026-09-29 + +### Added + +- Support for Python 3.15 (#2729) +- Support for free-threaded Python 3.14 (`Py_GIL_DISABLED`) (#2721) +- Threading documentation covering GIL ownership, managed threads, and + free-threaded Python (#2721) + +### Changed + +- Raise the minimum supported Python version from 3.10 to 3.11 (#2729) +- Make runtime caches and CLR type binding safe for concurrent access (#2721) +- Migrate the Visual Studio solution to the `.slnx` format (#2733) +- Update GitHub Actions dependencies (#2739) + +### Fixed + +- Fix races in finalization, buffer disposal, assembly path updates, and + Python subclass creation (#2721) +- Fix method binding initialization when methods are first accessed (#2721) +- Fix wheel and NuGet preview package builds +- Python exception raised on conversion failure (#2741) +- Introduce additional pre-checks in `PyBuffer_GetPointer` to prevent memory + corruption (#2745) + ## 3.1.0 - 2026-05-23 ### Added diff --git a/MANIFEST.in b/MANIFEST.in index 1d4889e25..3a6c29860 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,5 @@ prune src/runtime/bin graft src/compat include src/pythonnet.snk include Directory.Build.* -include pythonnet.sln +include pythonnet.slnx include version.txt diff --git a/doc/source/index.rst b/doc/source/index.rst index 161f4483e..d906f363e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -23,8 +23,9 @@ speeds for Python code. If you are interested in a pure managed-code implementation of the Python language, you should check out the `IronPython`_ project. -Python.NET is currently compatible and tested with Python releases from 3.7 -onwards. +Python.NET is currently compatible and tested with Python releases 3.11 +through 3.15. Free-threaded CPython is supported starting with Python 3.14; +see :doc:`threading` for the threading and GIL guidance. Current releases are available on `PyPi `_ and `Nuget.org `_. @@ -45,6 +46,7 @@ page. Use the `Python.NET issue tracker`_ to report issues. python dotnet codecs + threading pyreference reference diff --git a/doc/source/python.rst b/doc/source/python.rst index d39081eba..2fb01ad92 100644 --- a/doc/source/python.rst +++ b/doc/source/python.rst @@ -481,7 +481,7 @@ Python idioms: name = item.GetName() Using Context Managers (IDisposable) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .NET types that implement ``IDisposable`` can be used with Python's context manager protocol using the standard ``with`` statement. This automatically calls the object's diff --git a/doc/source/threading.rst b/doc/source/threading.rst new file mode 100644 index 000000000..02f7bc91a --- /dev/null +++ b/doc/source/threading.rst @@ -0,0 +1,217 @@ +Threading +========= + +This page explains how Python.NET interacts with the Python Global Interpreter +Lock (GIL) and with managed threads, and what guarantees the runtime makes +when your code is multi-threaded. It covers classic CPython builds for Python +3.11 through 3.15 and the free-threaded builds introduced in CPython 3.13 +(``Py_GIL_DISABLED``). Python.NET supports free-threaded CPython starting with +Python 3.14. + +The model in one paragraph +-------------------------- + +Python.NET embeds CPython, so every interaction with a Python object — +including reading a ``PyObject``'s attributes, calling a Python callable, +constructing a Python value, or letting a ``PyObject`` go out of scope — must +happen while the calling thread is *attached* to the interpreter. On a +classic (GIL-enabled) CPython build "attached" means "holds the GIL"; on a +free-threaded build it means "has an active thread state". In both cases the +attachment API is the same: ``Py.GIL()`` on the C# side and +``threading.Thread`` / ``_thread`` on the Python side. Forgetting to attach +will crash the process or corrupt memory. + +Acquiring the GIL from C# +------------------------- + +When .NET code calls into Python it must hold the GIL. Use the ``Py.GIL()`` +disposable to acquire and release it:: + + using (Py.GIL()) + { + dynamic np = Py.Import("numpy"); + var arr = np.array(new[] { 1, 2, 3 }); + // ... interact with arr ... + } + +``Py.GIL()`` is re-entrant: nesting calls on the same thread is harmless and +cheap. Always pair acquisition with disposal — the ``using`` form does this +automatically, and you must release the GIL on the same thread that acquired +it. + +If you need a Python object to outlive the ``using`` block, copy what you +need (e.g. ``.As()`` or ``new PyObject(value)``) before releasing the +GIL. + +Releasing the GIL for long-running .NET work +-------------------------------------------- + +If a managed call holds the GIL but then does long-running work that does not +touch Python (heavy CPU, blocking I/O, native interop), release the GIL so +other Python threads can run:: + + IntPtr threadState = PythonEngine.BeginAllowThreads(); + try + { + DoCpuHeavyWork(); // safe: no Python C API calls + } + finally + { + PythonEngine.EndAllowThreads(threadState); + } + +Inside the ``BeginAllowThreads``/``EndAllowThreads`` block you must not touch +any Python object. If you need to talk to Python from worker threads spawned +in this region, those threads must acquire the GIL themselves with +``Py.GIL()``. + +Calling .NET from Python threads +-------------------------------- + +Calling a managed method from a Python ``threading.Thread`` works +transparently — Python.NET handles GIL acquisition/release around the +managed call. The managed code sees the GIL held on entry and is free to +release it via ``BeginAllowThreads`` if it does its own blocking work. + +Calling Python from CLR threads +------------------------------- + +A CLR thread that was *not* spawned by Python (a thread-pool task, a +``Thread`` started in C#, an ``async`` continuation that resumed on a +different thread, etc.) must acquire the GIL before touching any +``PyObject``:: + + Task.Run(() => + { + using (Py.GIL()) + { + // safe to use PyObjects here + } + }); + +Forgetting this is the most common pythonnet threading bug. Symptoms range +from immediate segfaults to subtle refcount corruption that crashes much +later. + +Reference counting and finalizers +--------------------------------- + +``PyObject`` follows the .NET ``IDisposable`` pattern. ``Dispose()`` (or the +end of a ``using`` block) drops the underlying Python reference; the GC +finalizer queues the same release for the next time Python.NET is on the GIL. + +Two practical consequences: + +* **Don't share a single ``PyObject`` instance across threads without + serialising access.** ``PyObject`` is not internally locked. If multiple + threads concurrently dispose the same instance, the underlying refcount can + go negative. + +* **Don't rely on the GC finalizer running promptly.** The PyObject is only + freed when a Python.NET API later reacquires the GIL. If your application + shuts down without that happening, finalizable PyObjects can be reported as + leaked. + +Free-threaded Python (PEP 703) +------------------------------ + +Starting with the free-threaded CPython 3.13+ build (``Py_GIL_DISABLED``), +the GIL is no longer the serialisation point for Python C API calls. +Python.NET is tested against the ``3.14t`` (free-threaded) interpreter and +behaves as follows under that build: + +* ``Py.GIL()`` still acquires a thread state. It is functionally a no-op + for mutual exclusion but is still required for thread-state attachment. + Existing code that uses ``using (Py.GIL())`` continues to work without + changes. +* ``PythonEngine.BeginAllowThreads`` / ``EndAllowThreads`` similarly + manage the thread state and are still needed if you want the GC and + other Python threads to run while you're in long-running unmanaged code. +* Internal Python.NET caches (the reflection cache, generic-type binding + cache, dynamic-dispatch cache, module attribute cache, the interned- + string table, etc.) are thread-safe. You may read and call CLR types + concurrently from any number of threads without external locking. +* The reference-counting protocol uses CPython's ``Py_REFCNT`` symbol on + 3.14+, which returns the merged biased + shared refcount; values you read + from ``PyObject.Refcount`` are correct under free-threading. + +Behaviour that is *unchanged* between GIL and free-threaded builds: + +* A managed object exposed to Python (e.g. via ``System.Object`` or a + Python subclass of a CLR type) is still owned by a single CLR side: you + must not mutate its plain CLR fields from multiple threads without your + own locking. Python.NET only protects its own bookkeeping, not your + domain data. +* Operations on a single ``PyObject`` instance still require external + serialisation — see "Reference counting" above. + +Patterns +-------- + +Concurrent CLR access from Python +""""""""""""""""""""""""""""""""" + +Hammering CLR attributes / generic types from many threads is supported:: + + from threading import Thread + import System + from System.Collections.Generic import List + + def worker(): + for _ in range(1000): + _ = System.String.Empty + _ = List[int]() + + threads = [Thread(target=worker) for _ in range(8)] + for t in threads: t.start() + for t in threads: t.join() + +This works on both GIL and free-threaded builds. + +Python callback invoked from a managed thread +""""""""""""""""""""""""""""""""""""""""""""" + +If a managed component calls back into a Python delegate from a thread it +spawned, that callback path acquires the GIL internally — you do not need to +add ``Py.GIL()`` around the Python code in the delegate. + +Spawning a managed thread from inside ``Py.GIL()`` +"""""""""""""""""""""""""""""""""""""""""""""""""" + +If you start a managed thread while holding the GIL and the thread needs to +call back into Python, release the GIL first so the new thread can acquire +it:: + + IntPtr threadState; + using (Py.GIL()) + { + var pyCallback = scope.Get("on_done"); + threadState = PythonEngine.BeginAllowThreads(); // let workers acquire the GIL + try + { + // spawn workers, wait for them... + } + finally + { + PythonEngine.EndAllowThreads(threadState); + } + } + +Without the ``BeginAllowThreads`` the spawned thread blocks forever waiting +for the GIL the parent thread is still holding. + +Common pitfalls +--------------- + +* Holding ``Py.GIL()`` across ``Task.Run`` / ``await`` boundaries. Async + continuations can resume on a different thread; the GIL handle is + thread-bound and must be released on the same thread that acquired it. +* Passing a ``PyObject`` to a managed worker without taking ownership. If + the producer disposes its handle while the consumer is still using it, + the worker will operate on a freed object. Wrap the producer's + ``PyObject`` with ``new PyObject(value)`` before handing it off, or use + ``NewReference()``. +* Calling a Python callable that does CPU-bound work without releasing the + GIL. Other Python threads cannot make progress in that case, even on a + free-threaded build where the GIL is otherwise a no-op (the callable + itself may still touch contended Python state). diff --git a/pyproject.toml b/pyproject.toml index 0966038cd..8bb80460d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,18 +13,18 @@ dependencies = [ "clr_loader>=0.3.1,<0.4.0" ] -requires-python = ">=3.10, <3.15" +requires-python = ">=3.11, <3.16" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: C#", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", @@ -36,8 +36,7 @@ dynamic = ["version"] dev = [ "pytest >= 6", "find_libpython >= 0.3", - "numpy >=2 ; python_version >= '3.10'", - "numpy <2 ; python_version < '3.10'", + "numpy >=2", ] doc = [ "sphinx", diff --git a/pythonnet.sln b/pythonnet.sln deleted file mode 100644 index 9dfeb44b1..000000000 --- a/pythonnet.sln +++ /dev/null @@ -1,194 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31912.275 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Runtime", "src\runtime\Python.Runtime.csproj", "{4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.EmbeddingTest", "src\embed_tests\Python.EmbeddingTest.csproj", "{819E089B-4770-400E-93C6-4F7A35F0EA12}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Test", "src\testing\Python.Test.csproj", "{14EF9518-5BB7-4F83-8686-015BD2CC788E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.DomainReloadTests", "tests\domain_tests\Python.DomainReloadTests.csproj", "{F2FB6DA3-318E-4F30-9A1F-932C667E38C5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo", "Repo", "{441A0123-F4C6-4EE4-9AEE-315FD79BE2D5}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitignore = .gitignore - CHANGELOG.md = CHANGELOG.md - LICENSE = LICENSE - README.rst = README.rst - version.txt = version.txt - shell.nix = shell.nix - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{D301657F-5EAF-4534-B280-B858D651B2E5}" - ProjectSection(SolutionItems) = preProject - .github\workflows\ARM.yml = .github\workflows\ARM.yml - .github\workflows\main.yml = .github\workflows\main.yml - .github\workflows\nuget-preview.yml = .github\workflows\nuget-preview.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{57F5D701-F265-4736-A5A2-07249E7A4DA3}" - ProjectSection(SolutionItems) = preProject - setup.py = setup.py - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{BC426F42-8494-4AA5-82C9-5109ACD97BD1}" - ProjectSection(SolutionItems) = preProject - 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 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{142A6752-C2C2-4F95-B982-193418001B65}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - TraceAlloc|Any CPU = TraceAlloc|Any CPU - TraceAlloc|x64 = TraceAlloc|x64 - TraceAlloc|x86 = TraceAlloc|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|x64.ActiveCfg = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|x64.Build.0 = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|x86.ActiveCfg = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Debug|x86.Build.0 = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|Any CPU.Build.0 = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|x64.ActiveCfg = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|x64.Build.0 = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|x86.ActiveCfg = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.Release|x86.Build.0 = Release|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|Any CPU.ActiveCfg = TraceAlloc|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|Any CPU.Build.0 = TraceAlloc|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|x64.ActiveCfg = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|x64.Build.0 = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|x86.ActiveCfg = Debug|Any CPU - {4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}.TraceAlloc|x86.Build.0 = Debug|Any CPU - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|Any CPU.ActiveCfg = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|Any CPU.Build.0 = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|x64.ActiveCfg = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|x64.Build.0 = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|x86.ActiveCfg = Debug|x86 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Debug|x86.Build.0 = Debug|x86 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|Any CPU.ActiveCfg = Release|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|Any CPU.Build.0 = Release|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|x64.ActiveCfg = Release|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|x64.Build.0 = Release|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|x86.ActiveCfg = Release|x86 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.Release|x86.Build.0 = Release|x86 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|Any CPU.ActiveCfg = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|Any CPU.Build.0 = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|x64.ActiveCfg = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|x64.Build.0 = Debug|x64 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|x86.ActiveCfg = Debug|x86 - {E6B01706-00BA-4144-9029-186AC42FBE9A}.TraceAlloc|x86.Build.0 = Debug|x86 - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|x64.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|x64.Build.0 = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|x86.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Debug|x86.Build.0 = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|Any CPU.Build.0 = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|x64.ActiveCfg = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|x64.Build.0 = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|x86.ActiveCfg = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.Release|x86.Build.0 = Release|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|Any CPU.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|Any CPU.Build.0 = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|x64.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|x64.Build.0 = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|x86.ActiveCfg = Debug|Any CPU - {819E089B-4770-400E-93C6-4F7A35F0EA12}.TraceAlloc|x86.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|x64.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|x64.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|x86.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Debug|x86.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|Any CPU.Build.0 = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|x64.ActiveCfg = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|x64.Build.0 = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|x86.ActiveCfg = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.Release|x86.Build.0 = Release|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|Any CPU.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|Any CPU.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|x64.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|x64.Build.0 = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|x86.ActiveCfg = Debug|Any CPU - {14EF9518-5BB7-4F83-8686-015BD2CC788E}.TraceAlloc|x86.Build.0 = Debug|Any CPU - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|Any CPU.ActiveCfg = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|Any CPU.Build.0 = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|x64.ActiveCfg = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|x64.Build.0 = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|x86.ActiveCfg = Debug|x86 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Debug|x86.Build.0 = Debug|x86 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|Any CPU.ActiveCfg = Release|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|Any CPU.Build.0 = Release|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.Release|x64.ActiveCfg = Release|x64 - {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 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|Any CPU.ActiveCfg = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|Any CPU.Build.0 = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|x64.ActiveCfg = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|x64.Build.0 = Debug|x64 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|x86.ActiveCfg = Debug|x86 - {4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}.TraceAlloc|x86.Build.0 = Debug|x86 - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.Build.0 = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.Build.0 = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|Any CPU.Build.0 = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.ActiveCfg = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.Build.0 = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.ActiveCfg = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.Build.0 = Release|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|Any CPU.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|Any CPU.Build.0 = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|x64.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|x64.Build.0 = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|x86.ActiveCfg = Debug|Any CPU - {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.TraceAlloc|x86.Build.0 = Debug|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 - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|Any CPU.ActiveCfg = Debug|Any CPU - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|Any CPU.Build.0 = Debug|Any CPU - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|x64.ActiveCfg = Debug|Any CPU - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|x64.Build.0 = Debug|Any CPU - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|x86.ActiveCfg = Debug|Any CPU - {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.TraceAlloc|x86.Build.0 = Debug|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C8845072-C642-4858-8627-27E862AD21BB} - EndGlobalSection -EndGlobal diff --git a/pythonnet.slnx b/pythonnet.slnx new file mode 100644 index 000000000..0434ecd6d --- /dev/null +++ b/pythonnet.slnx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/embed_tests/Codecs.cs b/src/embed_tests/Codecs.cs index 6159060b3..f9ea2e14f 100644 --- a/src/embed_tests/Codecs.cs +++ b/src/embed_tests/Codecs.cs @@ -25,15 +25,13 @@ static void TupleConversionsGeneric() TupleCodec.Register(); var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object()); T restored = default; - using (var scope = Py.CreateScope()) - { - void Accept(T value) => restored = value; - using var accept = new Action(Accept).ToPython(); - scope.Set(nameof(tuple), tuple); - scope.Set(nameof(accept), accept); - scope.Exec($"{nameof(accept)}({nameof(tuple)})"); - Assert.That(actual: restored, Is.EqualTo(expected: tuple)); - } + using var scope = Py.CreateScope(); + void Accept(T value) => restored = value; + using var accept = new Action(Accept).ToPython(); + scope.Set(nameof(tuple), tuple); + scope.Set(nameof(accept), accept); + scope.Exec($"{nameof(accept)}({nameof(tuple)})"); + Assert.That(actual: restored, Is.EqualTo(expected: tuple)); } [Test] @@ -46,15 +44,13 @@ static void TupleConversionsObject() TupleCodec.Register(); var tuple = Activator.CreateInstance(typeof(T), 42.0, "42", new object()); T restored = default; - using (var scope = Py.CreateScope()) - { - void Accept(object value) => restored = (T)value; - using var accept = new Action(Accept).ToPython(); - scope.Set(nameof(tuple), tuple); - scope.Set(nameof(accept), accept); - scope.Exec($"{nameof(accept)}({nameof(tuple)})"); - Assert.That(actual: restored, Is.EqualTo(expected: tuple)); - } + using var scope = Py.CreateScope(); + void Accept(object value) => restored = (T)value; + using var accept = new Action(Accept).ToPython(); + scope.Set(nameof(tuple), tuple); + scope.Set(nameof(accept), accept); + scope.Exec($"{nameof(accept)}({nameof(tuple)})"); + Assert.That(actual: restored, Is.EqualTo(expected: tuple)); } [Test] @@ -92,7 +88,7 @@ public void ListDecoderTest() var codec = ListDecoder.Instance; var items = new List() { new PyInt(1), new PyInt(2), new PyInt(3) }; - using var pyList = new PyList(items.ToArray()); + using var pyList = new PyList([.. items]); using var pyListType = pyList.GetPythonType(); Assert.IsTrue(codec.CanDecode(pyListType, typeof(IList))); @@ -119,7 +115,7 @@ public void ListDecoderTest() IList stringList = null; Assert.DoesNotThrow(() => { codec.TryDecode(pyList, out stringList); }); Assert.That(3, Is.EqualTo(stringList.Count)); - Assert.Throws(typeof(InvalidCastException), () => { var x = stringList[0]; }); + Assert.Throws(() => { var x = stringList[0]; }); //can't convert python iterable to list (this will require a copy which isn't lossless) using var foo = GetPythonIterable(); @@ -134,7 +130,7 @@ public void SequenceDecoderTest() var items = new List() { new PyInt(1), new PyInt(2), new PyInt(3) }; //SequenceConverter can only convert to any ICollection - using var pyList = new PyList(items.ToArray()); + using var pyList = new PyList([.. items]); using var listType = pyList.GetPythonType(); //it can convert a PyList, since PyList satisfies the python sequence protocol @@ -162,7 +158,7 @@ public void SequenceDecoderTest() //the IList will report a Count of 3. ICollection stringCollection = null; Assert.DoesNotThrow(() => { codec.TryDecode(pyList, out stringCollection); }); - Assert.That(stringCollection.Count(), Is.EqualTo(3)); + Assert.That(stringCollection.Count, Is.EqualTo(3)); Assert.Throws(() => { string[] array = new string[3]; stringCollection.CopyTo(array, 0); @@ -177,7 +173,7 @@ public void SequenceDecoderTest() Assert.IsFalse(codec.CanDecode(fooType, typeof(ICollection))); //python tuples do satisfy the python sequence protocol - var pyTuple = new PyTuple(items.ToArray()); + var pyTuple = new PyTuple([.. items]); var pyTupleType = pyTuple.GetPythonType(); Assert.IsTrue(codec.CanDecode(pyTupleType, typeof(ICollection))); @@ -215,7 +211,7 @@ public void IterableDecoderTest() var codec = IterableDecoder.Instance; var items = new List() { new PyInt(1), new PyInt(2), new PyInt(3) }; - var pyList = new PyList(items.ToArray()); + var pyList = new PyList([.. items]); var pyListType = pyList.GetPythonType(); Assert.IsFalse(codec.CanDecode(pyListType, typeof(IList))); Assert.IsTrue(codec.CanDecode(pyListType, typeof(System.Collections.IEnumerable))); @@ -249,13 +245,13 @@ public void IterableDecoderTest() IEnumerable stringEnumerable = null; Assert.DoesNotThrow(() => { codec.TryDecode(pyList, out stringEnumerable); }); - Assert.Throws(typeof(InvalidCastException), () => { + Assert.Throws(() => { foreach (string item in stringEnumerable) { var x = item; } }); - Assert.Throws(typeof(InvalidCastException), () => { + Assert.Throws(() => { stringEnumerable.Count(); }); @@ -309,7 +305,7 @@ def CanEncode(self, clr_type): public void ExceptionEncoded() { PyObjectConversions.RegisterEncoder(new ValueErrorCodec()); - void CallMe() => throw new ValueErrorWrapper(TestExceptionMessage); + static void CallMe() => throw new ValueErrorWrapper(TestExceptionMessage); var callMeAction = new Action(CallMe); using var scope = Py.CreateScope(); scope.Exec(@" @@ -385,7 +381,9 @@ public void ExceptionDecodedNoInstance() } } +#pragma warning disable IDE0060 // Remove unused parameter public static void AcceptsDateTime(DateTime v) {} +#pragma warning restore IDE0060 // Remove unused parameter [Test] public void As_Object_AffectedByDecoders() @@ -412,15 +410,14 @@ public bool TryDecode(PyObject pyObj, out T value) } } - class ValueErrorWrapper : Exception + class ValueErrorWrapper(string message) : Exception(message) { - public ValueErrorWrapper(string message) : base(message) { } } class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder { public bool CanDecode(PyType objectType, Type targetType) - => this.CanEncode(targetType) + => CanEncode(targetType) && PythonReferenceComparer.Instance.Equals(objectType, PythonEngine.Eval("ValueError")); public bool CanEncode(Type type) => type == typeof(ValueErrorWrapper) @@ -480,16 +477,10 @@ class ObjectToEncoderInstanceEncoder : IPyObjectEncoder /// Decodes object of specified Python type to the predefined value /// /// Type of the - class DecoderReturningPredefinedValue : IPyObjectDecoder + class DecoderReturningPredefinedValue(PyObject objectType, TTarget decodeResult) : IPyObjectDecoder { - public PyObject TheOnlySupportedSourceType { get; } - public TTarget DecodeResult { get; } - - public DecoderReturningPredefinedValue(PyObject objectType, TTarget decodeResult) - { - this.TheOnlySupportedSourceType = objectType; - this.DecodeResult = decodeResult; - } + public PyObject TheOnlySupportedSourceType { get; } = objectType; + public TTarget DecodeResult { get; } = decodeResult; public bool CanDecode(PyType objectType, Type targetType) => PythonReferenceComparer.Instance.Equals(objectType, TheOnlySupportedSourceType) @@ -497,7 +488,7 @@ public bool CanDecode(PyType objectType, Type targetType) public bool TryDecode(PyObject pyObj, out T value) { if (typeof(T) != typeof(TTarget)) - throw new ArgumentException(nameof(T)); + throw new ArgumentException(null, nameof(T)); value = (T)(object)DecodeResult; return true; } diff --git a/src/embed_tests/Dynamic.cs b/src/embed_tests/Dynamic.cs index 9d35bfacc..b5b13f1a7 100644 --- a/src/embed_tests/Dynamic.cs +++ b/src/embed_tests/Dynamic.cs @@ -35,10 +35,10 @@ public void AssignNone() { dynamic sys = Py.Import("sys"); sys.testattr = new StringBuilder(); - Assert.IsNotNull(sys.testattr); + Assert.That(sys.testattr, Is.Not.Null); sys.testattr = null; - Assert.IsNull(sys.testattr); + Assert.That(sys.testattr, Is.Null); } /// @@ -80,7 +80,7 @@ public void PassObjectInPython() Assert.That(sys.testattr3.ToString(), Is.EqualTo("True")); // Compare in .NET - Assert.IsTrue(sys.testattr1.Equals(sys.testattr2)); + Assert.That(sys.testattr1, Is.EqualTo(sys.testattr2)); } /// @@ -103,7 +103,7 @@ public void PassPyObjectInNet() Assert.That(sys.testattr3.ToString(), Is.EqualTo("True")); // Compare in .NET - Assert.IsTrue(sys.testattr1.Equals(sys.testattr2)); + Assert.That(sys.testattr1, Is.EqualTo(sys.testattr2)); } // regression test for https://github.com/pythonnet/pythonnet/issues/1848 @@ -119,13 +119,13 @@ class MyEnum(enum.IntEnum): ERROR = 2 def get_status(): - return MyEnum.OK + return MyEnum.OK " ); dynamic MyEnum = scope.Get("MyEnum"); dynamic status = scope.Get("get_status").Invoke(); - Assert.IsTrue(status == MyEnum.OK); + Assert.That(status, Is.EqualTo(MyEnum.OK)); } // regression test for https://github.com/pythonnet/pythonnet/issues/1680 diff --git a/src/embed_tests/NumPyTests.cs b/src/embed_tests/NumPyTests.cs index 57ff11b2b..4bd7356d7 100644 --- a/src/embed_tests/NumPyTests.cs +++ b/src/embed_tests/NumPyTests.cs @@ -67,12 +67,12 @@ public void VarArg() { dynamic zX = np.array(new[,] { { 1, 2, 3 }, { 4, 5, 6 }, { 8, 9, 0 } }); dynamic grad = np.gradient(zX, 4.0, 5.0); - dynamic grad2 = np.InvokeMethod("gradient", new PyObject[] {zX, new PyFloat(4.0), new PyFloat(5.0)}); + dynamic grad2 = np.InvokeMethod("gradient", new PyObject[] { zX, new PyFloat(4.0), new PyFloat(5.0) }); - Assert.AreEqual(4.125, grad[0].sum().__float__().As(), 0.001); - Assert.AreEqual(-1.2, grad[1].sum().__float__().As(), 0.001); - Assert.AreEqual(4.125, grad2[0].sum().__float__().As(), 0.001); - Assert.AreEqual(-1.2, grad2[1].sum().__float__().As(), 0.001); + Assert.That(grad[0].sum().__float__().As(), Is.EqualTo(4.125).Within(0.001)); + Assert.That(grad[1].sum().__float__().As(), Is.EqualTo(-1.2).Within(0.001)); + Assert.That(grad2[0].sum().__float__().As(), Is.EqualTo(4.125).Within(0.001)); + Assert.That(grad2[1].sum().__float__().As(), Is.EqualTo(-1.2).Within(0.001)); } #pragma warning disable IDE1006 diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 15258fc83..8697e7aaf 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -24,7 +24,7 @@ - +