Repository: cython/cython
Pull request: Fixed __class__() lookup on builtin types
Fixes https://github.com/cython/cython/issues/3954.

The problem seems to be that __Pyx_CallUnboundCMethod0 optimizes
the call incorrectly. This patch avoids it getting sent to
that mechanism.

Problem statement:
Unexpected behavior of __class__ attribute for annotated python arguments
Hello, I'm kinda new to Cython so hopefully I didn't miss anything obvious, but I think I've found a weird behavior of annotated function arguments.
Seems like the ```__class__``` attribute acts unexpectedly when the arguments are annotated with pythonic 

```cython
def func(dict my_dict):
    # The following prints "<class 'dict'>" as expected
    print(my_dict.__class__)

    # I would expect the following to print "{}" since this is the equivalent of dict()
    # Instead, it once again prints "<class 'dict'>
    print(my_dict.__class__())

    # Calling the class twice would create an empty dict and print "{}"
    print(my_dict.__class__()())
```
Removing the "dict" annotation from "my_dict" would make this function work as expected
(printing "<class 'dict'> followed by "{}" followed by an exception for calling a 'dict' object)

This behavior also occurs with other python types such as strings, lists and sets.
However, when using ints, it behaved has expected.

I'm using Cython 0.29.21 with Python 3.9.1 on Windows.

Hints from issue discussion:

Commit message: Improve comment

Patch to review:
diff --git a/Cython/Compiler/Optimize.py b/Cython/Compiler/Optimize.py
index e1aea0fa406..77e54b16d16 100644
--- a/Cython/Compiler/Optimize.py
+++ b/Cython/Compiler/Optimize.py
@@ -3009,6 +3009,12 @@ def _handle_any_slot__new__(self, node, function, args,
                 utility_code=utility_code,
                 is_temp=node.is_temp
             )
+    def _handle_any_slot__class__(self, node, function, args,
+                                is_unbound_method, kwargs=None):
+        # The purpose of this function is to handle calls to instance.__class__() so that
+        # it doesn't get handled by the __Pyx_CallUnboundCMethod0 mechanism.
+        # TODO: optimizations of the instance.__class__() call might be possible in future.
+        return node
 
     ### methods of builtin types
 
diff --git a/Cython/Compiler/Visitor.py b/Cython/Compiler/Visitor.py
index 368c38058b5..2d6aaded29f 100644
--- a/Cython/Compiler/Visitor.py
+++ b/Cython/Compiler/Visitor.py
@@ -674,7 +674,7 @@ def _dispatch_to_method_handler(self, attr_name, self_arg,
             "method_%s_%s" % (type_name, attr_name), kwargs)
         if method_handler is None:
             if (attr_name in TypeSlots.method_name_to_slot
-                    or attr_name == '__new__'):
+                    or attr_name in ['__new__', '__class__']):
                 method_handler = self._find_handler(
                     "slot%s" % attr_name, kwargs)
             if method_handler is None:
diff --git a/tests/run/builtin_types_class.py b/tests/run/builtin_types_class.py
new file mode 100644
index 00000000000..f09be80f468
--- /dev/null
+++ b/tests/run/builtin_types_class.py
@@ -0,0 +1,60 @@
+# mode: run
+
+from __future__ import print_function
+
+import cython
+
+# https://github.com/cython/cython/issues/3954
+# calls to the __class__ attributes of builtin types were optimized to something invalid
+
+@cython.locals(d=dict)
+def test_dict(d):
+    """
+    >>> test_dict({})
+    dict
+    {}
+    """
+    print(d.__class__.__name__)
+    print(d.__class__())
+
+@cython.locals(i=int)
+def test_int(i):
+    """
+    >>> test_int(0)
+    int
+    0
+    """
+    print(i.__class__.__name__)
+    print(i.__class__())
+
+@cython.cclass
+class C:
+    def __str__(self):
+        return "I'm a C object"
+
+@cython.locals(c=C)
+def test_cdef_class(c):
+    """
+    # This wasn't actually broken but is worth testing anyway
+    >>> test_cdef_class(C())
+    C
+    I'm a C object
+    """
+    print(c.__class__.__name__)
+    print(c.__class__())
+
+@cython.locals(d=object)
+def test_object(o):
+    """
+    >>> test_object({})
+    dict
+    {}
+    >>> test_object(1)
+    int
+    0
+    >>> test_object(C())
+    C
+    I'm a C object
+    """
+    print(o.__class__.__name__)
+    print(o.__class__())


Code review comments:
diff_hunk: @@ -3009,6 +3009,12 @@ def _handle_any_slot__new__(self, node, function, args,
                 utility_code=utility_code,
                 is_temp=node.is_temp
             )
+    def _handle_any_slot__class__(self, node, function, args,
path: Cython/Compiler/Optimize.py
text: @user1:
```suggestion

    def _handle_any_slot__class__(self, node, function, args,
```