Repository: pythonnet/pythonnet (language: python)

Issue:
RecursionError when substracting System Decimal from Python Decimal
### Environment

-   Pythonnet version: Version: 3.0.1
-   Python version: 3.10
-   Operating System: Any (Windows/Linux)
-   .NET Runtime: 6.0.0

### Details

-   Trying to substract System Decimal from Python Decimal (the other way works fine)

    _TODO_

- import both system decimal and python decimal
- create a separate variable from each type
- subtract System Decimal from Python Decimal

```python
    from System import Decimal as SysDecimal
    from decimal import Decimal as PyDecimal
    sys_dec = SysDecimal(1)
    py_dec = PyDecimal(1)
    print(str(sys_dec - py_dec )) # works fine
    print(str(py_dec - sys_dec)) # crashes
```

-   error thrown
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RecursionError: maximum recursion depth exceeded while calling a Python object



Discussion / hints:
I have the same issue:

same setup, here is my case:
```
var offers = new [] { new {Discount = 2.0m}};
Runtime.PythonDLL = @"C:\Python311\python311.dll";
PythonEngine.Initialize();
PythonEngine.BeginAllowThreads();
using (Py.GIL())
{
    File.WriteAllText("script.py", @"
def execute(offers):
    total_discount = 0
    for offer in offers:
        total_discount += offer.Discount
    average_discount = total_discount / len(offers)
    return average_discount
");
    var script = Py.Import("script");
    var result = script.InvokeMethod("execute", offers.ToPython());
}
```

maximum recursion depth exceeded while calling a Python object
  File [...], line 4, in execute
    total_discount += offer.Discount
    
hope it helps
I'll have a look into the recursion (which I can reproduce and is a bug), but we don't actually by default provide conversions from/to Python decimals. You must be running a non-upstream version of Python.NET or have more code that you are not showing.
I have the same issue. 

Pythonnet version: 3.0.3
Pythonnet version: 3.11
Dotnet 6.0: Windows and Linux

For a codec I am using a naive string based conversion of cs System.Decimal to py decimal.Decimal and the inverse, in our use case the possible loss of precision should not be a concern. 

I have traced it down to the `__rsub__` operation  on the cs type. 
| Operation | Status |
| --- | --- |
| `cs - py` | Working |
| `cs.__sub__(py)` | Working |
| | |
| `py - cs` |  RecursionError |
| `py.__sub__(cs)` |  NotImplemented |
| `cs.__rsub__(py)` |  RecursionError |

The same happens with multiply and addition right-hand methods as well. As per the python rules for operators the forward operators are tried first and if they are NotImplemented  for a type the right hand ones are tried where the RecursionError occurs. The codec conversion does not get called for the python decimal foward operators which results in  the python [decimal conversion code](https://github.com/python/cpython/blob/dae7341da27a6004d985f1fd696f755f634bf61e/Lib/_pydecimal.py#L6027) being called which throws the NotImplemented exception

Codec:
```cs
public class DecimalCodec : IPyObjectDecoder, IPyObjectEncoder
    {
        public bool CanDecode(PyType objectType, Type targetType)
        {
            return objectType.Name == "decimal.Decimal" && targetType == typeof(decimal);
        }

        public bool CanEncode(Type type)
        {
            return typeof(decimal) == type;
        }

        public bool TryDecode<T>(PyObject pyObj, out T? value)
        {
            if (typeof(T) != typeof(decimal))
            {
                value = default(T);
                return false;
            }
            try
            {
                string decstr = (pyObj as dynamic).__str__();
                bool parseSuccess = decimal.TryParse(decstr, out decimal csdeci);
                value = (T)Convert.ChangeType(csdeci, typeof(decimal));
                return parseSuccess;
            }
            catch (Exception ex)
            {
                value = default(T);
                return false;
            }
        }

        public PyObject? TryEncode(object value)
        {
            if (typeof(decimal) != value.GetType())
            {
                return null;
            };
            try
            {
                dynamic pydeci = PyModule.Import("decimal");
                dynamic thedecimal = pydeci.Decimal(value.ToString());
                return thedecimal;
            }
            catch (Exception ex)
            {
                return null;
            }
        }
    }
```

Fix patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 577e898aa..18435671c 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -86,3 +86,4 @@
 -   ([@gpetrou](https://github.com/gpetrou))
 -   Ehsan Iran-Nejad ([@eirannejad](https://github.com/eirannejad))
 -   ([@legomanww](https://github.com/legomanww))
+-   ([@gertdreyer](https://github.com/gertdreyer))
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdab9bf64..5b545045f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
 
 ### Fixed
 
+-   Fixed RecursionError for reverse operators on C# operable types from python. See #2240
 
 ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11
 
diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs
index 79ab20e82..ecb6055a8 100644
--- a/src/runtime/ClassManager.cs
+++ b/src/runtime/ClassManager.cs
@@ -546,7 +546,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
                         ci.members[pyName] = new MethodObject(type, name, forwardMethods).AllocObject();
                     // Only methods where only the right operand is the declaring type.
                     if (reverseMethods.Length > 0)
-                        ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods).AllocObject();
+                        ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods, argsReversed: true).AllocObject();
                 }
             }
 
diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs
index 07ed4fe22..9a5515c8e 100644
--- a/src/runtime/MethodBinder.cs
+++ b/src/runtime/MethodBinder.cs
@@ -28,9 +28,12 @@ internal class MethodBinder
 
         [NonSerialized]
         public bool init = false;
+
         public const bool DefaultAllowThreads = true;
         public bool allow_threads = DefaultAllowThreads;
 
+        public bool argsReversed = false;
+
         internal MethodBinder()
         {
             list = new List<MaybeMethodBase>();
@@ -363,10 +366,10 @@ public MismatchedMethod(Exception exception, MethodBase mb)
                 _methods = GetMethods();
             }
 
-            return Bind(inst, args, kwargDict, _methods, matchGenerics: true);
+            return Bind(inst, args, kwargDict, _methods, matchGenerics: true, argsReversed);
         }
 
-        static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary<string, PyObject> kwargDict, MethodBase[] methods, bool matchGenerics)
+        private static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary<string, PyObject> kwargDict, MethodBase[] methods, bool matchGenerics, bool argsReversed = false)
         {
             var pynargs = (int)Runtime.PyTuple_Size(args);
             var isGeneric = false;
@@ -386,7 +389,7 @@ public MismatchedMethod(Exception exception, MethodBase mb)
                 // Binary operator methods will have 2 CLR args but only one Python arg
                 // (unary operators will have 1 less each), since Python operator methods are bound.
                 isOperator = isOperator && pynargs == pi.Length - 1;
-                bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi);  // Only cast if isOperator.
+                bool isReverse = isOperator && argsReversed;  // Only cast if isOperator.
                 if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi))
                     continue;  // Comparison operators in Python have no reverse mode.
                 if (!MatchesArgumentCount(pynargs, pi, kwargDict, out bool paramsArray, out ArrayList? defaultArgList, out int kwargsMatched, out int defaultsNeeded) && !isOperator)
@@ -394,12 +397,14 @@ public MismatchedMethod(Exception exception, MethodBase mb)
                     continue;
                 }
                 // Preprocessing pi to remove either the first or second argument.
-                if (isOperator && !isReverse) {
+                if (isOperator && !isReverse)
+                {
                     // The first Python arg is the right operand, while the bound instance is the left.
                     // We need to skip the first (left operand) CLR argument.
                     pi = pi.Skip(1).ToArray();
                 }
-                else if (isOperator && isReverse) {
+                else if (isOperator && isReverse)
+                {
                     // The first Python arg is the left operand.
                     // We need to take the first CLR argument.
                     pi = pi.Take(1).ToArray();
diff --git a/src/runtime/Types/MethodBinding.cs b/src/runtime/Types/MethodBinding.cs
index 334d705a6..bfe22b0f3 100644
--- a/src/runtime/Types/MethodBinding.cs
+++ b/src/runtime/Types/MethodBinding.cs
@@ -237,6 +237,7 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args,
                         }
                     }
                 }
+
                 return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue);
             }
             finally
diff --git a/src/runtime/Types/MethodObject.cs b/src/runtime/Types/MethodObject.cs
index 05198da76..12484d301 100644
--- a/src/runtime/Types/MethodObject.cs
+++ b/src/runtime/Types/MethodObject.cs
@@ -19,20 +19,20 @@ internal class MethodObject : ExtensionType
     {
         [NonSerialized]
         private MethodBase[]? _info = null;
+
         private readonly List<MaybeMethodInfo> infoList;
         internal string name;
         internal readonly MethodBinder binder;
         internal bool is_static = false;
-
         internal PyString? doc;
         internal MaybeType type;
 
-        public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads)
+        public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads, bool argsReversed = false)
         {
             this.type = type;
             this.name = name;
             this.infoList = new List<MaybeMethodInfo>();
-            binder = new MethodBinder();
+            binder = new MethodBinder() { argsReversed = argsReversed };
             foreach (MethodBase item in info)
             {
                 this.infoList.Add(item);
@@ -45,8 +45,8 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t
             binder.allow_threads = allow_threads;
         }
 
-        public MethodObject(MaybeType type, string name, MethodBase[] info)
-            : this(type, name, info, allow_threads: AllowThreads(info))
+        public MethodObject(MaybeType type, string name, MethodBase[] info, bool argsReversed = false)
+            : this(type, name, info, allow_threads: AllowThreads(info), argsReversed)
         {
         }
 
diff --git a/src/runtime/Types/OperatorMethod.cs b/src/runtime/Types/OperatorMethod.cs
index e3cc23370..a2ca73982 100644
--- a/src/runtime/Types/OperatorMethod.cs
+++ b/src/runtime/Types/OperatorMethod.cs
@@ -177,17 +177,14 @@ public static string ReversePyMethodName(string pyName)
         }
 
         /// <summary>
-        /// Check if the method is performing a reverse operation.
+        /// Check if the method should have a reversed operation.
         /// </summary>
         /// <param name="method">The operator method.</param>
         /// <returns></returns>
-        public static bool IsReverse(MethodBase method)
+        public static bool HaveReverse(MethodBase method)
         {
-            Type primaryType = method.IsOpsHelper()
-                ? method.DeclaringType.GetGenericArguments()[0]
-                : method.DeclaringType;
-            Type leftOperandType = method.GetParameters()[0].ParameterType;
-            return leftOperandType != primaryType;
+            var pi = method.GetParameters();
+            return OpMethodMap.ContainsKey(method.Name) && pi.Length == 2;
         }
 
         public static void FilterMethods(MethodBase[] methods, out MethodBase[] forwardMethods, out MethodBase[] reverseMethods)
@@ -196,14 +193,11 @@ public static void FilterMethods(MethodBase[] methods, out MethodBase[] forwardM
             var reverseMethodsList = new List<MethodBase>();
             foreach (var method in methods)
             {
-                if (IsReverse(method))
+                forwardMethodsList.Add(method);
+                if (HaveReverse(method))
                 {
                     reverseMethodsList.Add(method);
-                } else
-                {
-                    forwardMethodsList.Add(method);
                 }
-
             }
             forwardMethods = forwardMethodsList.ToArray();
             reverseMethods = reverseMethodsList.ToArray();


Test patch:
diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs
index a5713274a..6bfb81bdb 100644
--- a/src/embed_tests/TestOperator.cs
+++ b/src/embed_tests/TestOperator.cs
@@ -15,6 +15,7 @@ public class TestOperator
         public void SetUp()
         {
             PythonEngine.Initialize();
+            OwnIntCodec.Setup();
         }
 
         [OneTimeTearDown]
@@ -23,6 +24,125 @@ public void Dispose()
             PythonEngine.Shutdown();
         }
 
+        // Mock Integer class to test math ops on non-native dotnet types
+        public readonly struct OwnInt
+        {
+            private readonly int _value;
+
+            public int Num => _value;
+
+            public OwnInt()
+            {
+                _value = 0;
+            }
+
+            public OwnInt(int value)
+            {
+                _value = value;
+            }
+
+            public override int GetHashCode()
+            {
+                return unchecked(65535 + _value.GetHashCode());
+            }
+
+            public override bool Equals(object obj)
+            {
+                return obj is OwnInt @object &&
+                       Num == @object.Num;
+            }
+
+            public static OwnInt operator -(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value - p2._value);
+            }
+
+            public static OwnInt operator +(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value + p2._value);
+            }
+
+            public static OwnInt operator *(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value * p2._value);
+            }
+
+            public static OwnInt operator /(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value / p2._value);
+            }
+
+            public static OwnInt operator %(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value % p2._value);
+            }
+
+            public static OwnInt operator ^(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value ^ p2._value);
+            }
+
+            public static bool operator <(OwnInt p1, OwnInt p2)
+            {
+                return p1._value < p2._value;
+            }
+
+            public static bool operator >(OwnInt p1, OwnInt p2)
+            {
+                return p1._value > p2._value;
+            }
+
+            public static bool operator ==(OwnInt p1, OwnInt p2)
+            {
+                return p1._value == p2._value;
+            }
+
+            public static bool operator !=(OwnInt p1, OwnInt p2)
+            {
+                return p1._value != p2._value;
+            }
+
+            public static OwnInt operator |(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value | p2._value);
+            }
+
+            public static OwnInt operator &(OwnInt p1, OwnInt p2)
+            {
+                return new OwnInt(p1._value & p2._value);
+            }
+
+            public static bool operator <=(OwnInt p1, OwnInt p2)
+            {
+                return p1._value <= p2._value;
+            }
+
+            public static bool operator >=(OwnInt p1, OwnInt p2)
+            {
+                return p1._value >= p2._value;
+            }
+        }
+
+        // Codec for mock class above.
+        public class OwnIntCodec : IPyObjectDecoder
+        {
+            public static void Setup()
+            {
+                PyObjectConversions.RegisterDecoder(new OwnIntCodec());
+            }
+
+            public bool CanDecode(PyType objectType, Type targetType)
+            {
+                return objectType.Name == "int" && targetType == typeof(OwnInt);
+            }
+
+            public bool TryDecode<T>(PyObject pyObj, out T value)
+            {
+                value = (T)(object)new OwnInt(pyObj.As<int>());
+                return true;
+            }
+        }
+
         public class OperableObject
         {
             public int Num { get; set; }
@@ -524,6 +644,121 @@ public void ShiftOperatorOverloads()
 
 c = a >> b.Num
 assert c.Num == a.Num >> b.Num
+");
+        }
+
+        [Test]
+        public void ReverseOperatorWithCodec()
+        {
+            string name = string.Format("{0}.{1}",
+                typeof(OwnInt).DeclaringType.Name,
+                typeof(OwnInt).Name);
+            string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace;
+
+            PythonEngine.Exec($@"
+from {module} import *
+cls = {name}
+a = 2
+b = cls(10)
+
+c = a + b
+assert c.Num == a + b.Num
+
+c = a - b
+assert c.Num == a - b.Num
+
+c = a * b
+assert c.Num == a * b.Num
+
+c = a / b
+assert c.Num == a // b.Num
+
+c = a % b
+assert c.Num == a % b.Num
+
+c = a & b
+assert c.Num == a & b.Num
+
+c = a | b
+assert c.Num == a | b.Num
+
+c = a ^ b
+assert c.Num == a ^ b.Num
+
+c = a == b
+assert c == (a == b.Num)
+
+c = a != b
+assert c == (a != b.Num)
+
+c = a <= b
+assert c == (a <= b.Num)
+
+c = a >= b
+assert c == (a >= b.Num)
+
+c = a < b
+assert c == (a < b.Num)
+
+c = a > b
+assert c == (a > b.Num)
+");
+        }
+
+        [Test]
+        public void ForwardOperatorWithCodec()
+        {
+            string name = string.Format("{0}.{1}",
+                 typeof(OwnInt).DeclaringType.Name,
+                 typeof(OwnInt).Name);
+            string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace;
+
+            PythonEngine.Exec($@"
+from {module} import *
+cls = {name}
+a = cls(2)
+b = 10
+c = a + b
+assert c.Num == a.Num + b
+
+c = a - b
+assert c.Num == a.Num - b
+
+c = a * b
+assert c.Num == a.Num * b
+
+c = a / b
+assert c.Num == a.Num // b
+
+c = a % b
+assert c.Num == a.Num % b
+
+c = a & b
+assert c.Num == a.Num & b
+
+c = a | b
+assert c.Num == a.Num | b
+
+c = a ^ b
+assert c.Num == a.Num ^ b
+
+c = a == b
+assert c == (a.Num == b)
+
+c = a != b
+assert c == (a.Num != b)
+
+c = a <= b
+assert c == (a.Num <= b)
+
+c = a >= b
+assert c == (a.Num >= b)
+
+c = a < b
+assert c == (a.Num < b)
+
+c = a > b
+assert c == (a.Num > b)
 ");
         }
     }