Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].

### Added

- Extension methods declared in non-framework assemblies are bound to the type they extend, so they can be called from the instance like in C# (e.g. `order_status.is_open()`)
- Types with an implicit conversion to a numeric type support Python arithmetic, comparisons, `float()` and `int()` through that conversion, unless they declare the operator themselves
- Ability to instantiate new .NET arrays using `Array[T](dim1, dim2, ...)` syntax
- Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]).
- Add GetPythonThreadID and Interrupt methods in PythonEngine
Expand Down
193 changes: 193 additions & 0 deletions src/embed_tests/TestExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Python.Runtime;

namespace Python.EmbeddingTest
{
public enum ExtendedStatus { Open, Closed }

public interface IExtendedShape
{
double Side { get; }
}

public class ExtendedModel
{
public string Name { get; set; } = "model";

public string Existing() => "member";
}

public class DerivedExtendedModel : ExtendedModel
{
}

public class ExtendedSquare : IExtendedShape
{
public double Side => 2;
}

public static class ExtendedStatusExtensions
{
public static bool IsOpen(this ExtendedStatus status) => status == ExtendedStatus.Open;

public static string Describe(this ExtendedStatus status, string prefix, int times = 1)
=> string.Concat(Enumerable.Repeat(prefix + status, times));
}

public static class ExtendedModelExtensions
{
public static string Describe(this ExtendedModel model, string prefix = "") => prefix + model.Name;

// collides with the real instance method, which must win
public static string Existing(this ExtendedModel model) => "extension";

public static double Area(this IExtendedShape shape) => shape.Side * shape.Side;

public static int Total(this List<int> values) => values.Sum();

public static T Identity<T>(this T value) => value;

public static string Twice(this string value) => value + value;
}

public class TestExtensionMethods
{
private static dynamic module;

private const string TestModule = @"
from clr import AddReference
AddReference(""System"")
AddReference(""Python.EmbeddingTest"")
from System.Collections.Generic import List
from Python.EmbeddingTest import *

def enum_instance():
return ExtendedStatus.Open.IsOpen(), ExtendedStatus.Open.is_open(), ExtendedStatus.Closed.is_open()

def enum_with_args():
return ExtendedStatus.Open.describe('s-', times=2)

def enum_static_forms():
return ExtendedStatusExtensions.is_open(ExtendedStatus.Open), ExtendedStatus.is_open(ExtendedStatus.Closed)

def class_instance():
return ExtendedModel().describe(), ExtendedModel().describe(prefix='p-')

def derived_inherits():
return DerivedExtendedModel().describe('d-')

def interface_target():
return ExtendedSquare().area()

def member_wins():
return ExtendedModel().existing()

def closed_generic():
values = List[int]()
values.Add(1)
values.Add(2)
return values.total()

def open_generic_not_bound():
return hasattr(ExtendedModel(), 'identity')

def python_native_not_bound():
return hasattr('ab', 'twice'), ExtendedModelExtensions.twice('ab')
";

[OneTimeSetUp]
public void OneTimeSetUp()
{
PythonEngine.Initialize();
using var _ = Py.GIL();
module = PyModule.FromString("extension_methods", TestModule);
}

[OneTimeTearDown]
public void Dispose()
{
PythonEngine.Shutdown();
}

[Test]
public void EnumExtensionIsCallableFromTheInstance()
{
using var _ = Py.GIL();
var result = module.enum_instance();
Assert.IsTrue((bool)result[0]);
Assert.IsTrue((bool)result[1]);
Assert.IsFalse((bool)result[2]);
}

[Test]
public void ExtensionArgumentsFollowTheInstance()
{
using var _ = Py.GIL();
Assert.AreEqual("s-Opens-Open", (string)module.enum_with_args());
}

[Test]
public void StaticFormsKeepWorking()
{
using var _ = Py.GIL();
var result = module.enum_static_forms();
Assert.IsTrue((bool)result[0]);
Assert.IsFalse((bool)result[1]);
}

[Test]
public void ClassExtensionIsCallableFromTheInstance()
{
using var _ = Py.GIL();
var result = module.class_instance();
Assert.AreEqual("model", (string)result[0]);
Assert.AreEqual("p-model", (string)result[1]);
}

[Test]
public void DerivedClassInheritsBaseClassExtensions()
{
using var _ = Py.GIL();
Assert.AreEqual("d-model", (string)module.derived_inherits());
}

[Test]
public void InterfaceExtensionIsBoundToImplementingClass()
{
using var _ = Py.GIL();
Assert.AreEqual(4.0, (double)module.interface_target());
}

[Test]
public void RealMemberWinsOverExtension()
{
using var _ = Py.GIL();
Assert.AreEqual("member", (string)module.member_wins());
}

[Test]
public void ClosedGenericTargetIsBound()
{
using var _ = Py.GIL();
Assert.AreEqual(3, (int)module.closed_generic());
}

[Test]
public void OpenGenericTargetIsNotBound()
{
using var _ = Py.GIL();
Assert.IsFalse((bool)module.open_generic_not_bound());
}

[Test]
public void PythonNativeTargetKeepsTheStaticForm()
{
using var _ = Py.GIL();
var result = module.python_native_not_bound();
Assert.IsFalse((bool)result[0]);
Assert.AreEqual("abab", (string)result[1]);
}
}
}
162 changes: 162 additions & 0 deletions src/embed_tests/TestImplicitNumericOperators.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using NUnit.Framework;
using Python.Runtime;

namespace Python.EmbeddingTest
{
public class ConvertibleFee
{
public decimal Amount { get; }

public ConvertibleFee(decimal amount)
{
Amount = amount;
}

public static implicit operator decimal(ConvertibleFee fee) => fee.Amount;
}

public class ConvertibleRatio
{
public double Value { get; }

public ConvertibleRatio(double value)
{
Value = value;
}

public static implicit operator double(ConvertibleRatio ratio) => ratio.Value;
}

public class ConvertibleMoney
{
public decimal Amount { get; }

public ConvertibleMoney(decimal amount)
{
Amount = amount;
}

public static implicit operator decimal(ConvertibleMoney money) => money.Amount;

// the type's own operator must keep winning over the synthesized one
public static ConvertibleMoney operator +(ConvertibleMoney a, ConvertibleMoney b) => new(a.Amount + b.Amount);
}

public class NotConvertible
{
}

public class TestImplicitNumericOperators
{
private static dynamic module;

private const string TestModule = @"
from clr import AddReference
AddReference(""System"")
AddReference(""Python.EmbeddingTest"")
from Python.EmbeddingTest import *

fee = ConvertibleFee(2)
other = ConvertibleFee(3)

def arithmetic():
return (fee + other, fee + 1, 1 + fee, fee - other, fee - 1, 10 - fee,
fee * other, fee * 2, 2 * fee, other / fee, fee / 2, 10 / fee, -fee)

def aggregates():
return sum([fee, other]), max(fee, other) is other, sorted([other, fee])[0] is fee

def comparisons():
return (fee < other, fee > other, fee <= other, fee >= other,
fee > 0, fee < 2, fee <= 2, fee >= 2, 1 < fee, 2 >= fee)

def conversions():
return float(fee), int(ConvertibleRatio(1.75)), float(ConvertibleRatio(1.75))

def own_operator_wins():
total = ConvertibleMoney(2) + ConvertibleMoney(3)
return type(total).__name__, total.Amount, ConvertibleMoney(2) - ConvertibleMoney(1)

def not_convertible():
return hasattr(NotConvertible(), '__add__'), hasattr(NotConvertible(), '__float__')
";

[OneTimeSetUp]
public void OneTimeSetUp()
{
PythonEngine.Initialize();
using var _ = Py.GIL();
module = PyModule.FromString("implicit_numeric_operators", TestModule);
}

[OneTimeTearDown]
public void Dispose()
{
PythonEngine.Shutdown();
}

[Test]
public void ArithmeticDelegatesToTheImplicitConversion()
{
using var _ = Py.GIL();
var result = module.arithmetic();
var expected = new[] { 5.0, 3.0, 3.0, -1.0, 1.0, 8.0, 6.0, 4.0, 4.0, 1.5, 1.0, 5.0, -2.0 };
for (var i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], (double)result[i], $"index {i}");
}
}

[Test]
public void AggregatesWork()
{
using var _ = Py.GIL();
var result = module.aggregates();
Assert.AreEqual(5.0, (double)result[0]);
Assert.IsTrue((bool)result[1]);
Assert.IsTrue((bool)result[2]);
}

[Test]
public void ComparisonsDelegateToTheImplicitConversion()
{
using var _ = Py.GIL();
var result = module.comparisons();
var expected = new[] { true, false, true, false, true, false, true, true, true, true };
for (var i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], (bool)result[i], $"index {i}");
}
}

[Test]
public void FloatAndIntConversions()
{
using var _ = Py.GIL();
var result = module.conversions();
Assert.AreEqual(2.0, (double)result[0]);
Assert.AreEqual(1, (int)result[1]);
Assert.AreEqual(1.75, (double)result[2]);
}

[Test]
public void TheTypesOwnOperatorsKeepWinning()
{
using var _ = Py.GIL();
var result = module.own_operator_wins();
Assert.AreEqual("ConvertibleMoney", (string)result[0]);
Assert.AreEqual(5m, (decimal)result[1]);
// subtraction is not declared by the type, so the synthesized one applies
Assert.AreEqual(1.0, (double)result[2]);
}

[Test]
public void TypesWithoutAConversionAreUntouched()
{
using var _ = Py.GIL();
var result = module.not_convertible();
Assert.IsFalse((bool)result[0]);
Assert.IsFalse((bool)result[1]);
}
}
}
4 changes: 2 additions & 2 deletions src/perf_tests/Python.PerformanceTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
<PackageReference Include="quantconnect.pythonnet" Version="2.0.66" GeneratePathProperty="true">
<PackageReference Include="quantconnect.pythonnet" Version="2.0.68" GeneratePathProperty="true">
<IncludeAssets>compile</IncludeAssets>
</PackageReference>
</ItemGroup>
Expand All @@ -25,7 +25,7 @@
</Target>

<Target Name="CopyBaseline" AfterTargets="Build">
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.66\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.68\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
</Target>

<Target Name="CopyNewBuild" AfterTargets="Build">
Expand Down
Loading
Loading