Repository: cython/cython

Issue:
Single-argument functions cannot be called using keyword
```
In [1]: %load_ext cython

In [2]: %%cython
   ...: def f(k): return k

In [3]: f(k=1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-2f40ef43e783> in <module>()
----> 1 f(k=1)

TypeError: f() takes no keyword arguments
```


Discussion:
The problem is very obvious in retrospect: single-argument functions use `METH_O` which do not accept keywords. The question is what to do with this, given that nobody complained about this so far. I only found this out when trying some edge-cases for #3021.
There is a compiler setting to always allow keywords, also for METH_O functions.

Hmm, on second thought, I wonder if it's worth changing the default setting, now that we have all that fast-calling integration?
I now think that Cython 3.0 is a good time to change the default setting. Let's just do that.

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -3663,8 +3663,8 @@ def generate_stararg_copy_code(self, code):
             code.globalstate.use_utility_code(
                 UtilityCode.load_cached("RaiseArgTupleInvalid", "FunctionArguments.c"))
             code.putln("if (unlikely(%s > 0)) {" % Naming.nargs_cname)
-            code.put('__Pyx_RaiseArgtupleInvalid("%s", 1, 0, 0, %s); return %s;' % (
-                self.name, Naming.nargs_cname, self.error_value()))
+            code.put('__Pyx_RaiseArgtupleInvalid(%s, 1, 0, 0, %s); return %s;' % (
+                self.name.as_c_string_literal(), Naming.nargs_cname, self.error_value()))
             code.putln("}")
 
         if self.starstar_arg:
@@ -3678,8 +3678,8 @@ def generate_stararg_copy_code(self, code):
         code.globalstate.use_utility_code(
             UtilityCode.load_cached("KeywordStringCheck", "FunctionArguments.c"))
         code.putln(
-            "if (%s && unlikely(!__Pyx_CheckKeywordStrings(%s, \"%s\", %d))) return %s;" % (
-                kwarg_check, Naming.kwds_cname, self.name,
+            "if (%s && unlikely(!__Pyx_CheckKeywordStrings(%s, %s, %d))) return %s;" % (
+                kwarg_check, Naming.kwds_cname, self.name.as_c_string_literal(),
                 bool(self.starstar_arg), self.error_value()))
 
         if self.starstar_arg and self.starstar_arg.entry.cf_used:
diff --git a/Cython/Compiler/Options.py b/Cython/Compiler/Options.py
--- a/Cython/Compiler/Options.py
+++ b/Cython/Compiler/Options.py
@@ -180,7 +180,7 @@ def get_directive_defaults():
     'cdivision_warnings': False,
     'overflowcheck': False,
     'overflowcheck.fold': True,
-    'always_allow_keywords': False,
+    'always_allow_keywords': True,
     'allow_none_for_extension_args': True,
     'wraparound' : True,
     'ccomplex' : False,  # use C99/C++ for complex types and arith


Test patch:
diff --git a/tests/buffers/bufaccess.pyx b/tests/buffers/bufaccess.pyx
--- a/tests/buffers/bufaccess.pyx
+++ b/tests/buffers/bufaccess.pyx
@@ -960,6 +960,7 @@ def decref(*args):
     for item in args: Py_DECREF(item)
 
 @cython.binding(False)
+@cython.always_allow_keywords(False)
 def get_refcount(x):
     return (<PyObject*>x).ob_refcnt
 
diff --git a/tests/compile/fused_redeclare_T3111.pyx b/tests/compile/fused_redeclare_T3111.pyx
--- a/tests/compile/fused_redeclare_T3111.pyx
+++ b/tests/compile/fused_redeclare_T3111.pyx
@@ -22,9 +22,9 @@ def foo(dtype_t[:] a, dtype_t_out[:, :] b):
 # "__pyxutil:16:4: '___pyx_npy_uint8' redeclared".  The remaining warnings are
 # unrelated to this test.
 _WARNINGS = """
-# cpdef redeclaration bug
-22:10: 'cpdef_method' redeclared
-33:10: 'cpdef_cname_method' redeclared
+# cpdef redeclaration bug, from TestCythonScope.pyx
+25:10: 'cpdef_method' redeclared
+36:10: 'cpdef_cname_method' redeclared
 # from MemoryView.pyx
 984:29: Ambiguous exception value, same as default return value: 0
 984:29: Ambiguous exception value, same as default return value: 0
diff --git a/tests/memoryview/memoryview.pyx b/tests/memoryview/memoryview.pyx
--- a/tests/memoryview/memoryview.pyx
+++ b/tests/memoryview/memoryview.pyx
@@ -627,6 +627,7 @@ def decref(*args):
     for item in args: Py_DECREF(item)
 
 @cython.binding(False)
+@cython.always_allow_keywords(False)
 def get_refcount(x):
     return (<PyObject*>x).ob_refcnt
 
diff --git a/tests/memoryview/memslice.pyx b/tests/memoryview/memslice.pyx
--- a/tests/memoryview/memslice.pyx
+++ b/tests/memoryview/memslice.pyx
@@ -1059,6 +1059,7 @@ def decref(*args):
     for item in args: Py_DECREF(item)
 
 @cython.binding(False)
+@cython.always_allow_keywords(False)
 def get_refcount(x):
     return (<PyObject*>x).ob_refcnt
 
diff --git a/tests/run/always_allow_keywords_T295.pyx b/tests/run/always_allow_keywords_T295.pyx
--- a/tests/run/always_allow_keywords_T295.pyx
+++ b/tests/run/always_allow_keywords_T295.pyx
@@ -1,7 +1,12 @@
+# mode: run
 # ticket: 295
 
 cimport cython
 
+import sys
+IS_PY2 = sys.version_info[0] == 2
+
+
 def assert_typeerror_no_keywords(func, *args, **kwds):
     # Python 3.9 produces an slightly different error message
     # to previous versions, so doctest isn't matching the
@@ -14,13 +19,18 @@ def assert_typeerror_no_keywords(func, *args, **kwds):
         assert False, "call did not raise TypeError"
 
 
+def func0():
+    """
+    >>> func0()
+    >>> func0(**{})
+    """
+
 def func1(arg):
     """
     >>> func1(None)
     >>> func1(*[None])
-    >>> assert_typeerror_no_keywords(func1, arg=None)
+    >>> func1(arg=None)
     """
-    pass
 
 @cython.always_allow_keywords(False)
 def func2(arg):
@@ -29,7 +39,6 @@ def func2(arg):
     >>> func2(*[None])
     >>> assert_typeerror_no_keywords(func2, arg=None)
     """
-    pass
 
 @cython.always_allow_keywords(True)
 def func3(arg):
@@ -40,26 +49,132 @@ def func3(arg):
     """
     pass
 
+
 cdef class A:
     """
-    >>> A().meth1(None)
-    >>> A().meth1(*[None])
-    >>> assert_typeerror_no_keywords(A().meth1, arg=None)
-    >>> A().meth2(None)
-    >>> A().meth2(*[None])
-    >>> assert_typeerror_no_keywords(A().meth2, arg=None)
-    >>> A().meth3(None)
-    >>> A().meth3(*[None])
-    >>> A().meth3(arg=None)
+    >>> class PyA(object):
+    ...     def meth0(self): pass
+    ...     def meth1(self, arg): pass
+
+    >>> PyA().meth0()
+    >>> PyA.meth0(PyA())
+    >>> if not IS_PY2: PyA.meth0(self=PyA())
+    >>> try: PyA().meth0(self=PyA())
+    ... except TypeError as exc: assert 'multiple' in str(exc), "Unexpected message: %s" % exc
+    ... else: assert False, "No TypeError when passing 'self' argument twice"
+
+    >>> PyA().meth1(1)
+    >>> PyA.meth1(PyA(), 1)
+    >>> PyA.meth1(PyA(), arg=1)
+    >>> if not IS_PY2: PyA.meth1(self=PyA(), arg=1)
     """
 
+    @cython.always_allow_keywords(False)
+    def meth0_nokw(self):
+        """
+        >>> A().meth0_nokw()
+        >>> A().meth0_nokw(**{})
+        >>> try: pass  #A.meth0_nokw(self=A())
+        ... except TypeError as exc: assert 'needs an argument' in str(exc), "Unexpected message: %s" % exc
+        ... else: pass  #assert False, "No TypeError for missing 'self' positional argument"
+        """
+
+    @cython.always_allow_keywords(True)
+    def meth0_kw(self):
+        """
+        >>> A().meth0_kw()
+        >>> A().meth0_kw(**{})
+        >>> A.meth0_kw(A())
+        >>> #A.meth0_kw(self=A())
+        >>> try: pass  #A().meth0_kw(self=A())
+        ... except TypeError as exc: assert 'multiple' in str(exc), "Unexpected message: %s" % exc
+        ... else: pass  #assert False, "No TypeError when passing 'self' argument twice"
+        """
+
+    @cython.always_allow_keywords(True)
+    def meth1_kw(self, arg):
+        """
+        >>> A().meth1_kw(None)
+        >>> A().meth1_kw(*[None])
+        >>> A().meth1_kw(arg=None)
+        >>> A.meth1_kw(A(), arg=None)
+        >>> #A.meth1_kw(self=A(), arg=None)
+        """
+
+    @cython.always_allow_keywords(False)
+    def meth1_nokw(self, arg):
+        """
+        >>> A().meth1_nokw(None)
+        >>> A().meth1_nokw(*[None])
+        >>> assert_typeerror_no_keywords(A().meth1_nokw, arg=None)
+        >>> assert_typeerror_no_keywords(A.meth1_nokw, A(), arg=None)
+        >>> try: pass  # A.meth1_nokw(self=A(), arg=None)
+        ... except TypeError as exc: assert 'needs an argument' in str(exc), "Unexpected message: %s" % exc
+        ... else: pass  # assert False, "No TypeError for missing 'self' positional argument"
+        """
+
+    @cython.always_allow_keywords(False)
+    def meth2(self, arg):
+        """
+        >>> A().meth2(None)
+        >>> A().meth2(*[None])
+        >>> assert_typeerror_no_keywords(A().meth2, arg=None)
+        """
+
+    @cython.always_allow_keywords(True)
+    def meth3(self, arg):
+        """
+        >>> A().meth3(None)
+        >>> A().meth3(*[None])
+        >>> A().meth3(arg=None)
+        """
+
+
+class B(object):
+    @cython.always_allow_keywords(False)
+    def meth0_nokw(self):
+        """
+        >>> B().meth0_nokw()
+        >>> B().meth0_nokw(**{})
+        >>> if not IS_PY2: assert_typeerror_no_keywords(B.meth0_nokw, self=B())
+        """
+
+    @cython.always_allow_keywords(True)
+    def meth0_kw(self):
+        """
+        >>> B().meth0_kw()
+        >>> B().meth0_kw(**{})
+        >>> B.meth0_kw(B())
+        >>> if not IS_PY2: B.meth0_kw(self=B())
+        >>> try: B().meth0_kw(self=B())
+        ... except TypeError as exc: assert 'multiple' in str(exc), "Unexpected message: %s" % exc
+        ... else: assert False, "No TypeError when passing 'self' argument twice"
+        """
+
+    @cython.always_allow_keywords(True)
     def meth1(self, arg):
-        pass
+        """
+        >>> B().meth1(None)
+        >>> B().meth1(*[None])
+        >>> B().meth1(arg=None)
+        >>> B.meth1(B(), arg=None)
+        >>> if not IS_PY2: B.meth1(self=B(), arg=None)
+        """
 
     @cython.always_allow_keywords(False)
     def meth2(self, arg):
-        pass
+        """
+        >>> B().meth2(None)
+        >>> B().meth2(*[None])
+        >>> B.meth2(B(), None)
+        >>> if not IS_PY2: B.meth2(self=B(), arg=None)
+        >>> B().meth2(arg=None)  # assert_typeerror_no_keywords(B().meth2, arg=None)  -> not a cdef class!
+        """
 
     @cython.always_allow_keywords(True)
     def meth3(self, arg):
-        pass
+        """
+        >>> B().meth3(None)
+        >>> B().meth3(*[None])
+        >>> B().meth3(arg=None)
+        """
diff --git a/tests/run/cyfunction_METH_O_GH1728.pyx b/tests/run/cyfunction_METH_O_GH1728.pyx
--- a/tests/run/cyfunction_METH_O_GH1728.pyx
+++ b/tests/run/cyfunction_METH_O_GH1728.pyx
@@ -8,9 +8,9 @@ cdef class TestMethodOneArg:
 
 def call_meth(x):
     """
-    >>> call_meth(TestMethodOneArg())
+    >>> call_meth(TestMethodOneArg())  # doctest: +ELLIPSIS
     Traceback (most recent call last):
     ...
-    TypeError: meth() takes exactly one argument (0 given)
+    TypeError: meth() takes exactly ... argument (0 given)
     """
     return x.meth()
diff --git a/tests/run/exceptionrefcount.pyx b/tests/run/exceptionrefcount.pyx
--- a/tests/run/exceptionrefcount.pyx
+++ b/tests/run/exceptionrefcount.pyx
@@ -27,8 +27,11 @@ __doc__ = u"""
 >>> run_test(50, test_finally)
 """
 
+cimport cython
 from cpython.ref cimport PyObject
 
+@cython.binding(False)
+@cython.always_allow_keywords(False)
 def get_refcount(obj):
     return (<PyObject*>obj).ob_refcnt
 
diff --git a/tests/run/refcount_in_meth.pyx b/tests/run/refcount_in_meth.pyx
--- a/tests/run/refcount_in_meth.pyx
+++ b/tests/run/refcount_in_meth.pyx
@@ -12,8 +12,10 @@ True
 True
 """
 
+cimport cython
 from cpython.ref cimport PyObject
 
+@cython.always_allow_keywords(False)
 def get_refcount(obj):
     return (<PyObject*>obj).ob_refcnt
 
diff --git a/tests/run/unicode_identifiers.pyx b/tests/run/unicode_identifiers.pyx
--- a/tests/run/unicode_identifiers.pyx
+++ b/tests/run/unicode_identifiers.pyx
@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
-# cython: language_level=3
 # mode: run
 # tag: pep3131, traceback
 
+# cython: language_level=3
+
 # Code with unicode identifiers can be compiled with Cython running either Python 2 or 3.
 # However Python access to unicode identifiers is only possible in Python 3. In Python 2
 # it's only really safe to use the unicode identifiers for purely Cython interfaces
@@ -11,10 +12,13 @@
 # This is controlled by putting the Python3 only tests in the module __doc__ attribute
 # Most of the individual function and class docstrings are only present as a compile test
 
+cimport cython
+
 import sys
 
-if sys.version_info[0]>2:
-    __doc__ = """
+
+if sys.version_info[0] > 2:
+    __doc__ = u"""
     >>> f()()
     2
     >>> f().__name__
@@ -37,6 +41,9 @@ if sys.version_info[0]>2:
     >>> print(x.α)
     200
 
+    >>> B().Ƒ()
+    >>> C().Ƒ()
+
     Test generation of locals()
     >>> sorted(Γναμε2().boring_function(1,2).keys())
     ['self', 'somevalue', 'x', 'ναμε5', 'ναμε6']
@@ -45,6 +52,8 @@ if sys.version_info[0]>2:
     0
     >>> function_taking_fancy_argument(Γναμε2()).ναμε3
     1
+    >>> metho_function_taking_fancy_argument(Γναμε2()).ναμε3
+    1
     >>> NormalClassΓΓ().ναμε
     10
     >>> NormalClassΓΓ().εxciting_function(None).__qualname__
@@ -81,7 +90,7 @@ cdef class A:
     def __init__(self):
         self.ναμε = 1
     cdef Ƒ(self):
-        return self.ναμε==1
+        return self.ναμε == 1
     def regular_function(self):
         """
         Can use unicode cdef functions and (private) attributes internally
@@ -174,9 +183,16 @@ cdef class Derived(Γναμε2):
 
 cdef Γναμε2 global_ναμε3 = Γναμε2()
 
+
+@cython.always_allow_keywords(False)  # METH_O signature
+def metho_function_taking_fancy_argument(Γναμε2 αrγ):
+    return αrγ
+
+@cython.always_allow_keywords(True)
 def function_taking_fancy_argument(Γναμε2 αrγ):
     return αrγ
 
+
 class NormalClassΓΓ(Γναμε2):
     """
     docstring