diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b5dd1816..9f0f212e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -67,6 +67,9 @@ or the DLL must be loaded in advance. This must be done before calling any other
- BREAKING: disabled implicit conversion from C# enums to Python `int` and back.
One must now either use enum members (e.g. `MyEnum.Option`), or use enum constructor
(e.g. `MyEnum(42)` or `MyEnum(42, True)` when `MyEnum` does not have a member with value 42).
+- BREAKING: disabled implicit conversion from Python objects implementing sequence protocol to
+.NET arrays when the target .NET type is `System.Object`. The conversion is still attempted when the
+target type is a `System.Array`.
- Sign Runtime DLL with a strong name
- Implement loading through `clr_loader` instead of the included `ClrModule`, enables
support for .NET Core
diff --git a/Directory.Build.props b/Directory.Build.props
index 8c5b53685..72a519f4f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,7 +6,7 @@
Python.NET
10.0
false
- $([System.IO.File]::ReadAllText("version.txt"))
+ $([System.IO.File]::ReadAllText("version.txt").Trim())
$(FullVersion.Split('-', 2)[0])
$(FullVersion.Split('-', 2)[1])
diff --git a/MANIFEST.in b/MANIFEST.in
index 6458d5778..71473c2c3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
graft src/runtime
prune src/runtime/obj
prune src/runtime/bin
+include src/pythonnet.snk
include Directory.Build.*
include pythonnet.sln
include version.txt
diff --git a/README.rst b/README.rst
index c2c2d53e1..ba4a56fbf 100644
--- a/README.rst
+++ b/README.rst
@@ -50,6 +50,8 @@ Embedding Python in .NET
(internal, derived from ``MissingMethodException``) upon calling ``Initialize``.
Typical values are ``python38.dll`` (Windows), ``libpython3.8.dylib`` (Mac),
``libpython3.8.so`` (most other Unix-like operating systems).
+- Then call ``PythonEngine.Initialize()``. If you plan to use Python objects from
+ multiple threads, also call ``PythonEngine.BeginAllowThreads()``.
- All calls to python should be inside a
``using (Py.GIL()) {/* Your code here */}`` block.
- Import python modules using ``dynamic mod = Py.Import("mod")``, then
diff --git a/pyproject.toml b/pyproject.toml
index 5ee89d3b7..39c7c14fb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ license = {text = "MIT"}
readme = "README.rst"
dependencies = [
- "clr_loader>=0.1.7"
+ "clr_loader>=0.2.2,<0.3.0"
]
classifiers = [
diff --git a/pythonnet.sln b/pythonnet.sln
index eb97cfbd0..d1a47892e 100644
--- a/pythonnet.sln
+++ b/pythonnet.sln
@@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo", "Repo", "{441A0123-F
CHANGELOG.md = CHANGELOG.md
LICENSE = LICENSE
README.rst = README.rst
+ version.txt = version.txt
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{D301657F-5EAF-4534-B280-B858D651B2E5}"
diff --git a/pythonnet/__init__.py b/pythonnet/__init__.py
index 9876a0bec..2cde01233 100644
--- a/pythonnet/__init__.py
+++ b/pythonnet/__init__.py
@@ -1,12 +1,12 @@
import sys
from pathlib import Path
-from typing import Dict, Optional, Union
+from typing import Dict, Optional, Union, Any
import clr_loader
__all__ = ["set_runtime", "set_runtime_from_env", "load"]
_RUNTIME: Optional[clr_loader.Runtime] = None
-_LOADER_ASSEMBLY: Optional[clr_loader.wrappers.Assembly] = None
+_LOADER_ASSEMBLY: Optional[clr_loader.Assembly] = None
_LOADED: bool = False
@@ -27,6 +27,15 @@ def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None:
_RUNTIME = runtime
+def get_runtime_info() -> Optional[clr_loader.RuntimeInfo]:
+ """Retrieve information on the configured runtime"""
+
+ if _RUNTIME is None:
+ return None
+ else:
+ return _RUNTIME.info()
+
+
def _get_params_from_env(prefix: str) -> Dict[str, str]:
from os import environ
@@ -43,9 +52,11 @@ def _get_params_from_env(prefix: str) -> Dict[str, str]:
def _create_runtime_from_spec(
- spec: str, params: Optional[Dict[str, str]] = None
+ spec: str, params: Optional[Dict[str, Any]] = None
) -> clr_loader.Runtime:
+ was_default = False
if spec == "default":
+ was_default = True
if sys.platform == "win32":
spec = "netfx"
else:
@@ -53,14 +64,29 @@ def _create_runtime_from_spec(
params = params or _get_params_from_env(spec)
- if spec == "netfx":
- return clr_loader.get_netfx(**params)
- elif spec == "mono":
- return clr_loader.get_mono(**params)
- elif spec == "coreclr":
- return clr_loader.get_coreclr(**params)
- else:
- raise RuntimeError(f"Invalid runtime name: '{spec}'")
+ try:
+ if spec == "netfx":
+ return clr_loader.get_netfx(**params)
+ elif spec == "mono":
+ return clr_loader.get_mono(**params)
+ elif spec == "coreclr":
+ return clr_loader.get_coreclr(**params)
+ else:
+ raise RuntimeError(f"Invalid runtime name: '{spec}'")
+ except Exception as exc:
+ if was_default:
+ raise RuntimeError(
+ f"""Failed to create a default .NET runtime, which would
+ have been "{spec}" on this system. Either install a
+ compatible runtime or configure it explicitly via
+ `set_runtime` or the `PYTHONNET_*` environment variables
+ (see set_runtime_from_env)."""
+ ) from exc
+ else:
+ raise RuntimeError(
+ f"""Failed to create a .NET runtime ({spec}) using the
+ parameters {params}."""
+ ) from exc
def set_runtime_from_env() -> None:
@@ -85,9 +111,7 @@ def set_runtime_from_env() -> None:
set_runtime(runtime)
-def load(
- runtime: Union[clr_loader.Runtime, str, None] = None, **params: str
-) -> None:
+def load(runtime: Union[clr_loader.Runtime, str, None] = None, **params: str) -> None:
"""Load Python.NET in the specified runtime
The same parameters as for `set_runtime` can be used. By default,
@@ -109,9 +133,9 @@ def load(
dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll"
- _LOADER_ASSEMBLY = _RUNTIME.get_assembly(str(dll_path))
+ _LOADER_ASSEMBLY = assembly = _RUNTIME.get_assembly(str(dll_path))
+ func = assembly.get_function("Python.Runtime.Loader.Initialize")
- func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"]
if func(b"") != 0:
raise RuntimeError("Failed to initialize Python.Runtime.dll")
@@ -125,12 +149,12 @@ def unload() -> None:
global _RUNTIME, _LOADER_ASSEMBLY
if _LOADER_ASSEMBLY is not None:
- func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"]
+ func = _LOADER_ASSEMBLY.get_function("Python.Runtime.Loader.Shutdown")
if func(b"full_shutdown") != 0:
raise RuntimeError("Failed to call Python.NET shutdown")
_LOADER_ASSEMBLY = None
if _RUNTIME is not None:
- # TODO: Add explicit `close` to clr_loader
+ _RUNTIME.shutdown()
_RUNTIME = None
diff --git a/src/embed_tests/Codecs.cs b/src/embed_tests/Codecs.cs
index c9e83f03a..be416bc15 100644
--- a/src/embed_tests/Codecs.cs
+++ b/src/embed_tests/Codecs.cs
@@ -285,7 +285,6 @@ public void IterableDecoderTest()
}
// regression for https://github.com/pythonnet/pythonnet/issues/1427
- [Ignore("https://github.com/pythonnet/pythonnet/issues/1256")]
[Test]
public void PythonRegisteredDecoder_NoStackOverflowOnSystemType()
{
diff --git a/src/runtime/AssemblyManager.cs b/src/runtime/AssemblyManager.cs
index d09d2d76e..a8bbd1f6c 100644
--- a/src/runtime/AssemblyManager.cs
+++ b/src/runtime/AssemblyManager.cs
@@ -334,7 +334,7 @@ public static bool IsValidNamespace(string name)
}
///
- /// Returns an IEnumerable containing the namepsaces exported
+ /// Returns an enumerable collection containing the namepsaces exported
/// by loaded assemblies in the current app domain.
///
public static IEnumerable GetNamespaces ()
diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs
index e1820f05b..2e0edc3b5 100644
--- a/src/runtime/Converter.cs
+++ b/src/runtime/Converter.cs
@@ -389,11 +389,6 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
return true;
}
- if (Runtime.PySequence_Check(value))
- {
- return ToArray(value, typeof(object[]), out result, setError);
- }
-
result = new PyObject(value);
return true;
}
diff --git a/src/runtime/Native/ITypeOffsets.cs b/src/runtime/Native/ITypeOffsets.cs
index 2c4fdf59a..7d71b4b91 100644
--- a/src/runtime/Native/ITypeOffsets.cs
+++ b/src/runtime/Native/ITypeOffsets.cs
@@ -22,6 +22,7 @@ interface ITypeOffsets
int nb_true_divide { get; }
int nb_and { get; }
int nb_int { get; }
+ int nb_float { get; }
int nb_or { get; }
int nb_xor { get; }
int nb_lshift { get; }
@@ -30,6 +31,7 @@ interface ITypeOffsets
int nb_invert { get; }
int nb_inplace_add { get; }
int nb_inplace_subtract { get; }
+ int nb_index { get; }
int ob_size { get; }
int ob_type { get; }
int qualname { get; }
diff --git a/src/runtime/Native/LibraryLoader.cs b/src/runtime/Native/LibraryLoader.cs
index 6d67bea61..05f960481 100644
--- a/src/runtime/Native/LibraryLoader.cs
+++ b/src/runtime/Native/LibraryLoader.cs
@@ -137,17 +137,17 @@ public IntPtr GetFunction(IntPtr hModule, string procedureName)
static IntPtr[] GetAllModules()
{
- var self = Process.GetCurrentProcess().Handle;
+ using var self = Process.GetCurrentProcess();
uint bytes = 0;
var result = new IntPtr[0];
- if (!EnumProcessModules(self, result, bytes, out var needsBytes))
+ if (!EnumProcessModules(self.Handle, result, bytes, out var needsBytes))
throw new Win32Exception();
while (bytes < needsBytes)
{
bytes = needsBytes;
result = new IntPtr[bytes / IntPtr.Size];
- if (!EnumProcessModules(self, result, bytes, out needsBytes))
+ if (!EnumProcessModules(self.Handle, result, bytes, out needsBytes))
throw new Win32Exception();
}
return result.Take((int)(needsBytes / IntPtr.Size)).ToArray();
diff --git a/src/runtime/Native/TypeOffset.cs b/src/runtime/Native/TypeOffset.cs
index 59c5c3ee0..803b86bfa 100644
--- a/src/runtime/Native/TypeOffset.cs
+++ b/src/runtime/Native/TypeOffset.cs
@@ -31,12 +31,14 @@ static partial class TypeOffset
internal static int nb_or { get; private set; }
internal static int nb_xor { get; private set; }
internal static int nb_int { get; private set; }
+ internal static int nb_float { get; private set; }
internal static int nb_lshift { get; private set; }
internal static int nb_rshift { get; private set; }
internal static int nb_remainder { get; private set; }
internal static int nb_invert { get; private set; }
internal static int nb_inplace_add { get; private set; }
internal static int nb_inplace_subtract { get; private set; }
+ internal static int nb_index { get; private set; }
internal static int ob_size { get; private set; }
internal static int ob_type { get; private set; }
internal static int qualname { get; private set; }
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index fad5b9da8..5072f23cd 100644
--- a/src/runtime/Python.Runtime.csproj
+++ b/src/runtime/Python.Runtime.csproj
@@ -23,6 +23,8 @@
true
snupkg
+ True
+
..\pythonnet.snk
true
diff --git a/src/runtime/README.md b/src/runtime/README.md
index ab75280ed..6c8eb2a6c 100644
--- a/src/runtime/README.md
+++ b/src/runtime/README.md
@@ -8,6 +8,9 @@ integrate Python engine and use Python libraries.
(internal, derived from `MissingMethodException`) upon calling `Initialize`.
Typical values are `python38.dll` (Windows), `libpython3.8.dylib` (Mac),
`libpython3.8.so` (most other *nix). Full path may be required.
+- Then call `PythonEngine.Initialize()`. If you plan to [use Python objects from
+ multiple threads](https://github.com/pythonnet/pythonnet/wiki/Threading),
+ also call `PythonEngine.BeginAllowThreads()`.
- All calls to Python should be inside a
`using (Py.GIL()) {/* Your code here */}` block.
- Import python modules using `dynamic mod = Py.Import("mod")`, then
diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs
index 88a8ed173..4dc904f43 100644
--- a/src/runtime/Runtime.cs
+++ b/src/runtime/Runtime.cs
@@ -99,6 +99,7 @@ internal static int GetRun()
internal static bool HostedInPython;
internal static bool ProcessIsTerminating;
+ ///
/// Initialize the runtime...
///
/// Always call this method from the Main thread. After the
diff --git a/src/runtime/Types/ArrayObject.cs b/src/runtime/Types/ArrayObject.cs
index bda717e56..b95934baf 100644
--- a/src/runtime/Types/ArrayObject.cs
+++ b/src/runtime/Types/ArrayObject.cs
@@ -520,7 +520,7 @@ static IntPtr AllocateBufferProcs()
#endregion
///
- ///
+ ///
///
public static void InitializeSlots(PyType type, ISet initialized, SlotsHolder slotsHolder)
{
diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs
index 1e3c325cc..7296a1900 100644
--- a/src/runtime/Types/ClassBase.cs
+++ b/src/runtime/Types/ClassBase.cs
@@ -529,6 +529,37 @@ static NewReference tp_call_impl(BorrowedReference ob, BorrowedReference args, B
return callBinder.Invoke(ob, args, kw);
}
+ static NewReference DoConvert(BorrowedReference ob)
+ {
+ var self = (CLRObject)GetManagedObject(ob)!;
+ using var python = self.inst.ToPython();
+ return python.NewReferenceOrNull();
+ }
+
+ static NewReference DoConvertInt(BorrowedReference ob)
+ {
+ var self = (CLRObject)GetManagedObject(ob)!;
+ return Runtime.PyLong_FromLongLong(Convert.ToInt64(self.inst));
+ }
+
+ static NewReference DoConvertUInt(BorrowedReference ob)
+ {
+ var self = (CLRObject)GetManagedObject(ob)!;
+ return Runtime.PyLong_FromUnsignedLongLong(Convert.ToUInt64(self.inst));
+ }
+
+ static NewReference DoConvertBooleanInt(BorrowedReference ob)
+ {
+ var self = (CLRObject)GetManagedObject(ob)!;
+ return Runtime.PyInt_FromInt32((bool)self.inst ? 1 : 0);
+ }
+
+ static NewReference DoConvertFloat(BorrowedReference ob)
+ {
+ var self = (CLRObject)GetManagedObject(ob)!;
+ return Runtime.PyFloat_FromDouble(Convert.ToDouble(self.inst));
+ }
+
static IEnumerable GetCallImplementations(Type type)
=> type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "__call__");
@@ -564,6 +595,32 @@ public virtual void InitializeSlots(BorrowedReference pyType, SlotsHolder slotsH
{
TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.mp_length, new Interop.B_P(MpLengthSlot.impl), slotsHolder);
}
+
+ switch (Type.GetTypeCode(type.Value))
+ {
+ case TypeCode.Boolean:
+ case TypeCode.SByte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_int, new Interop.B_N(DoConvertInt), slotsHolder);
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_index, new Interop.B_N(DoConvertInt), slotsHolder);
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_float, new Interop.B_N(DoConvertFloat), slotsHolder);
+ break;
+ case TypeCode.Byte:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_int, new Interop.B_N(DoConvertUInt), slotsHolder);
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_index, new Interop.B_N(DoConvertUInt), slotsHolder);
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_float, new Interop.B_N(DoConvertFloat), slotsHolder);
+ break;
+ case TypeCode.Double:
+ case TypeCode.Single:
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_int, new Interop.B_N(DoConvertInt), slotsHolder);
+ TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.nb_float, new Interop.B_N(DoConvertFloat), slotsHolder);
+ break;
+ }
}
public virtual bool HasCustomNew() => this.GetType().GetMethod("tp_new") is not null;
diff --git a/src/runtime/Types/EventBinding.cs b/src/runtime/Types/EventBinding.cs
index 9eb2382ec..5c47d4aab 100644
--- a/src/runtime/Types/EventBinding.cs
+++ b/src/runtime/Types/EventBinding.cs
@@ -70,7 +70,6 @@ public static NewReference nb_inplace_subtract(BorrowedReference ob, BorrowedRef
return new NewReference(ob);
}
- ///
public static int tp_descr_set(BorrowedReference ds, BorrowedReference ob, BorrowedReference val)
=> EventObject.tp_descr_set(ds, ob, val);
diff --git a/src/runtime/Types/OperatorMethod.cs b/src/runtime/Types/OperatorMethod.cs
index 0c6127f65..e3cc23370 100644
--- a/src/runtime/Types/OperatorMethod.cs
+++ b/src/runtime/Types/OperatorMethod.cs
@@ -53,6 +53,8 @@ static OperatorMethod()
["op_UnaryPlus"] = new SlotDefinition("__pos__", TypeOffset.nb_positive),
["__int__"] = new SlotDefinition("__int__", TypeOffset.nb_int),
+ ["__float__"] = new SlotDefinition("__float__", TypeOffset.nb_float),
+ ["__index__"] = new SlotDefinition("__index__", TypeOffset.nb_index),
};
ComparisonOpMap = new Dictionary
{
diff --git a/src/runtime/Types/ReflectedClrType.cs b/src/runtime/Types/ReflectedClrType.cs
index 2e8f95924..b787939be 100644
--- a/src/runtime/Types/ReflectedClrType.cs
+++ b/src/runtime/Types/ReflectedClrType.cs
@@ -22,7 +22,6 @@ internal ReflectedClrType(BorrowedReference original) : base(original) { }
///
///
/// Returned might be partially initialized.
- /// If you need fully initialized type, use
///
public static ReflectedClrType GetOrCreate(Type type)
{
diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs
index 12d0c7aa6..7c35f27eb 100644
--- a/src/runtime/Util/OpsHelper.cs
+++ b/src/runtime/Util/OpsHelper.cs
@@ -82,10 +82,14 @@ internal static class EnumOps where T : Enum
{
[ForbidPythonThreads]
#pragma warning disable IDE1006 // Naming Styles - must match Python
- public static PyInt __int__(T value)
+ public static PyInt __index__(T value)
#pragma warning restore IDE1006 // Naming Styles
=> typeof(T).GetEnumUnderlyingType() == typeof(UInt64)
? new PyInt(Convert.ToUInt64(value))
: new PyInt(Convert.ToInt64(value));
+ [ForbidPythonThreads]
+#pragma warning disable IDE1006 // Naming Styles - must match Python
+ public static PyInt __int__(T value) => __index__(value);
+#pragma warning restore IDE1006 // Naming Styles
}
}
diff --git a/tests/conftest.py b/tests/conftest.py
index fcd1d224a..e61e3680e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -53,10 +53,12 @@ def pytest_configure(config):
runtime_params = {}
if runtime_opt == "coreclr":
- fw = "net6.0"
- runtime_params["runtime_config"] = str(
- bin_path / "Python.Test.runtimeconfig.json"
- )
+ # This is optional now:
+ #
+ # fw = "net6.0"
+ # runtime_params["runtime_config"] = str(
+ # bin_path / "Python.Test.runtimeconfig.json"
+ # )
collect_ignore.append("domain_tests/test_domain_reload.py")
else:
domain_tests_dir = cwd / "domain_tests"
diff --git a/tests/test_conversion.py b/tests/test_conversion.py
index a5b4c6fd9..bb686dd52 100644
--- a/tests/test_conversion.py
+++ b/tests/test_conversion.py
@@ -577,6 +577,13 @@ class Foo(object):
ob.ObjectField = Foo
assert ob.ObjectField == Foo
+ class PseudoSeq:
+ def __getitem__(self, idx):
+ return 0
+
+ ob.ObjectField = PseudoSeq()
+ assert ob.ObjectField.__class__.__name__ == "PseudoSeq"
+
def test_null_conversion():
"""Test null conversion."""
@@ -713,3 +720,35 @@ def test_intptr_construction():
with pytest.raises(OverflowError):
UIntPtr(v)
+def test_explicit_conversion():
+ from operator import index
+ from System import (
+ Int64, UInt64, Int32, UInt32, Int16, UInt16, Byte, SByte, Boolean
+ )
+ from System import Double, Single
+
+ assert int(Boolean(False)) == 0
+ assert int(Boolean(True)) == 1
+
+ for t in [UInt64, UInt32, UInt16, Byte]:
+ assert index(t(127)) == 127
+ assert int(t(127)) == 127
+ assert float(t(127)) == 127.0
+
+ for t in [Int64, Int32, Int16, SByte]:
+ assert index(t(127)) == 127
+ assert index(t(-127)) == -127
+ assert int(t(127)) == 127
+ assert int(t(-127)) == -127
+ assert float(t(127)) == 127.0
+ assert float(t(-127)) == -127.0
+
+ assert int(Int64(Int64.MaxValue)) == 2**63 - 1
+ assert int(Int64(Int64.MinValue)) == -2**63
+ assert int(UInt64(UInt64.MaxValue)) == 2**64 - 1
+
+ for t in [Single, Double]:
+ assert float(t(0.125)) == 0.125
+ assert int(t(123.4)) == 123
+ with pytest.raises(TypeError):
+ index(t(123.4))
diff --git a/tests/test_module.py b/tests/test_module.py
index 4e1a1a1ef..ddfa7bb36 100644
--- a/tests/test_module.py
+++ b/tests/test_module.py
@@ -31,6 +31,7 @@ def test_import_clr():
def test_version_clr():
import clr
assert clr.__version__ >= "3.0.0"
+ assert clr.__version__[-1] != "\n"
def test_preload_var():
diff --git a/version.txt b/version.txt
index dc72b3783..916e2438f 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-3.0.0-rc4
+3.0.0-rc5