summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Behnel <stefan_ml@behnel.de>2020-04-20 09:02:34 +0200
committerStefan Behnel <stefan_ml@behnel.de>2020-04-21 06:35:06 +0200
commit10f91539a5d81a3a77a02ce95be49ae3f5d5a547 (patch)
treef2283f6e8da6164cd3face1af50f600831acc1db
parent4900109cb691cfa8efe2b2f674ee3dcee68b878d (diff)
downloadcython-10f91539a5d81a3a77a02ce95be49ae3f5d5a547.tar.gz
Implement PEP-487: simpler customisation of class creation.
Closes GH-2781.
-rw-r--r--CHANGES.rst3
-rw-r--r--Cython/Utility/ObjectHandling.c106
-rw-r--r--tests/run/test_subclassinit.py316
3 files changed, 418 insertions, 7 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 66ffc5022..a1cf525bb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -11,6 +11,9 @@ Features added
* Cython functions now use the PEP-590 vectorcall protocol in Py3.7+.
Patch by Jeroen Demeyer. (Github issue #2263)
+* The simplified Py3.6 customisation of class creation is implemented (PEP-487).
+ (Github issue #2781)
+
* Unicode identifiers are supported in Cython code (PEP 3131).
Patch by David Woods. (Github issue #2601)
diff --git a/Cython/Utility/ObjectHandling.c b/Cython/Utility/ObjectHandling.c
index 2bc38a3a0..f6dbf1a23 100644
--- a/Cython/Utility/ObjectHandling.c
+++ b/Cython/Utility/ObjectHandling.c
@@ -1020,8 +1020,11 @@ static PyObject *__Pyx_Py3ClassCreate(PyObject *metaclass, PyObject *name, PyObj
PyObject *mkw, int calculate_metaclass, int allow_py2_metaclass); /*proto*/
/////////////// Py3ClassCreate ///////////////
+//@substitute: naming
//@requires: PyObjectGetAttrStrNoError
//@requires: CalculateMetaclass
+//@requires: PyObjectCall2Args
+//@requires: PyObjectLookupSpecial
static PyObject *__Pyx_Py3MetaclassPrepare(PyObject *metaclass, PyObject *bases, PyObject *name,
PyObject *qualname, PyObject *mkw, PyObject *modname, PyObject *doc) {
@@ -1063,6 +1066,78 @@ bad:
return NULL;
}
+#if PY_VERSION_HEX < 0x030600A4
+// https://www.python.org/dev/peps/pep-0487/
+static int __Pyx_SetNamesPEP487(PyObject *type_obj) {
+ PyTypeObject *type = (PyTypeObject*) type_obj;
+ PyObject *names_to_set, *key, *value, *set_name, *tmp;
+ Py_ssize_t i = 0;
+
+#if CYTHON_USE_TYPE_SLOTS
+ names_to_set = PyDict_Copy(type->tp_dict);
+#else
+ names_to_set = PyObject_GetAttr(type_obj, PYIDENT("__dict__"));
+#endif
+ if (unlikely(names_to_set == NULL))
+ goto bad;
+
+ while (PyDict_Next(names_to_set, &i, &key, &value)) {
+ set_name = __Pyx_PyObject_LookupSpecialNoError(value, PYIDENT("__set_name__"));
+ if (unlikely(set_name != NULL)) {
+ tmp = __Pyx_PyObject_Call2Args(set_name, type_obj, key);
+ Py_DECREF(set_name);
+ if (unlikely(tmp == NULL)) {
+ PyErr_Format(PyExc_RuntimeError,
+#if PY_MAJOR_VERSION >= 3
+ "Error calling __set_name__ on '%.100s' instance %R "
+ "in '%.100s'",
+ Py_TYPE(value)->tp_name, key, type->tp_name);
+#else
+ "Error calling __set_name__ on '%.100s' instance %s "
+ "in '%.100s'",
+ Py_TYPE(value)->tp_name, PyString_Check(key) ? PyString_AS_STRING(key) : "?", type->tp_name);
+#endif
+ goto bad;
+ } else {
+ Py_DECREF(tmp);
+ }
+ }
+ else if (unlikely(PyErr_Occurred())) {
+ goto bad;
+ }
+ }
+
+ Py_DECREF(names_to_set);
+ return 0;
+bad:
+ Py_XDECREF(names_to_set);
+ return -1;
+}
+
+static PyObject *__Pyx_InitSubclassPEP487(PyObject *type_obj, PyObject *mkw) {
+ PyObject *super, *func, *res;
+ super = __Pyx_PyObject_Call2Args((PyObject*) &PySuper_Type, type_obj, type_obj);
+ if (unlikely(!super)) {
+ Py_CLEAR(type_obj);
+ goto done;
+ }
+ func = __Pyx_PyObject_GetAttrStrNoError(super, PYIDENT("__init_subclass__"));
+ Py_DECREF(super);
+ if (likely(!func)) {
+ if (unlikely(PyErr_Occurred()))
+ Py_CLEAR(type_obj);
+ goto done;
+ }
+ res = __Pyx_PyObject_Call(func, $empty_tuple, mkw);
+ Py_DECREF(func);
+ if (unlikely(!res))
+ Py_CLEAR(type_obj);
+ Py_XDECREF(res);
+done:
+ return type_obj;
+}
+#endif
+
static PyObject *__Pyx_Py3ClassCreate(PyObject *metaclass, PyObject *name, PyObject *bases,
PyObject *dict, PyObject *mkw,
int calculate_metaclass, int allow_py2_metaclass) {
@@ -1086,14 +1161,26 @@ static PyObject *__Pyx_Py3ClassCreate(PyObject *metaclass, PyObject *name, PyObj
return NULL;
owned_metaclass = metaclass;
}
+ result = NULL;
margs = PyTuple_Pack(3, name, bases, dict);
- if (unlikely(!margs)) {
- result = NULL;
- } else {
- result = PyObject_Call(metaclass, margs, mkw);
+ if (likely(margs)) {
+ // Before PEP-487, type(a,b,c) did not accept any keyword arguments, so guard at least against that case.
+ PyObject *mc_kwargs = (PY_VERSION_HEX >= 0x030600A4) ? mkw : (
+ (metaclass == (PyObject*)&PyType_Type) ? NULL : mkw);
+
+ result = PyObject_Call(metaclass, margs, mc_kwargs);
Py_DECREF(margs);
}
Py_XDECREF(owned_metaclass);
+#if PY_VERSION_HEX < 0x030600A4
+ if (likely(result) && likely(PyType_Check(result))) {
+ if (unlikely(__Pyx_SetNamesPEP487(result) < 0)) {
+ Py_CLEAR(result);
+ } else {
+ result = __Pyx_InitSubclassPEP487(result, mkw);
+ }
+ }
+#endif
return result;
}
@@ -1357,14 +1444,18 @@ static CYTHON_INLINE PyObject *__Pyx_GetAttr(PyObject *o, PyObject *n) {
/////////////// PyObjectLookupSpecial.proto ///////////////
//@requires: PyObjectGetAttrStr
+//@requires: PyObjectGetAttrStrNoError
#if CYTHON_USE_PYTYPE_LOOKUP && CYTHON_USE_TYPE_SLOTS
-static CYTHON_INLINE PyObject* __Pyx_PyObject_LookupSpecial(PyObject* obj, PyObject* attr_name) {
+#define __Pyx_PyObject_LookupSpecialNoError(obj, attr_name) __Pyx__PyObject_LookupSpecial(obj, attr_name, 0)
+#define __Pyx_PyObject_LookupSpecial(obj, attr_name) __Pyx__PyObject_LookupSpecial(obj, attr_name, 1)
+
+static CYTHON_INLINE PyObject* __Pyx__PyObject_LookupSpecial(PyObject* obj, PyObject* attr_name, int with_error) {
PyObject *res;
PyTypeObject *tp = Py_TYPE(obj);
#if PY_MAJOR_VERSION < 3
if (unlikely(PyInstance_Check(obj)))
- return __Pyx_PyObject_GetAttrStr(obj, attr_name);
+ return with_error ? __Pyx_PyObject_GetAttrStr(obj, attr_name) : __Pyx_PyObject_GetAttrStrNoError(obj, attr_name);
#endif
// adapted from CPython's special_lookup() in ceval.c
res = _PyType_Lookup(tp, attr_name);
@@ -1375,12 +1466,13 @@ static CYTHON_INLINE PyObject* __Pyx_PyObject_LookupSpecial(PyObject* obj, PyObj
} else {
res = f(res, obj, (PyObject *)tp);
}
- } else {
+ } else if (with_error) {
PyErr_SetObject(PyExc_AttributeError, attr_name);
}
return res;
}
#else
+#define __Pyx_PyObject_LookupSpecialNoError(o,n) __Pyx_PyObject_GetAttrStrNoError(o,n)
#define __Pyx_PyObject_LookupSpecial(o,n) __Pyx_PyObject_GetAttrStr(o,n)
#endif
diff --git a/tests/run/test_subclassinit.py b/tests/run/test_subclassinit.py
new file mode 100644
index 000000000..7661c6062
--- /dev/null
+++ b/tests/run/test_subclassinit.py
@@ -0,0 +1,316 @@
+# mode: run
+# tag: pure3.6
+# cython: language_level=3str
+
+import sys
+HAS_NATIVE_SUPPORT = sys.version_info >= (3, 6)
+IS_PY2 = sys.version_info[0] == 2
+
+import re
+import types
+import unittest
+
+ZERO = 0
+
+skip_if_not_native = unittest.skipIf(not HAS_NATIVE_SUPPORT, "currently requires Python 3.6+")
+
+
+class Test(unittest.TestCase):
+ if not hasattr(unittest.TestCase, 'assertRegex'):
+ def assertRegex(self, value, regex):
+ self.assertTrue(re.search(regex, str(value)),
+ "'%s' did not match '%s'" % (value, regex))
+
+ if not hasattr(unittest.TestCase, 'assertCountEqual'):
+ def assertCountEqual(self, first, second):
+ self.assertEqual(set(first), set(second))
+ self.assertEqual(len(first), len(second))
+
+ def test_init_subclass(self):
+ class A:
+ initialized = False
+
+ def __init_subclass__(cls):
+ if HAS_NATIVE_SUPPORT:
+ super().__init_subclass__()
+ cls.initialized = True
+
+ class B(A):
+ pass
+
+ self.assertFalse(A.initialized)
+ self.assertTrue(B.initialized)
+
+ def test_init_subclass_dict(self):
+ class A(dict):
+ initialized = False
+
+ def __init_subclass__(cls):
+ if HAS_NATIVE_SUPPORT:
+ super().__init_subclass__()
+ cls.initialized = True
+
+ class B(A):
+ pass
+
+ self.assertFalse(A.initialized)
+ self.assertTrue(B.initialized)
+
+ def test_init_subclass_kwargs(self):
+ class A:
+ def __init_subclass__(cls, **kwargs):
+ cls.kwargs = kwargs
+
+ class B(A, x=3):
+ pass
+
+ self.assertEqual(B.kwargs, dict(x=3))
+
+ def test_init_subclass_error(self):
+ class A:
+ def __init_subclass__(cls):
+ raise RuntimeError
+
+ with self.assertRaises(RuntimeError):
+ class B(A):
+ pass
+
+ def test_init_subclass_wrong(self):
+ class A:
+ def __init_subclass__(cls, whatever):
+ pass
+
+ with self.assertRaises(TypeError):
+ class B(A):
+ pass
+
+ def test_init_subclass_skipped(self):
+ class BaseWithInit:
+ def __init_subclass__(cls, **kwargs):
+ if HAS_NATIVE_SUPPORT:
+ super().__init_subclass__(**kwargs)
+ cls.initialized = cls
+
+ class BaseWithoutInit(BaseWithInit):
+ pass
+
+ class A(BaseWithoutInit):
+ pass
+
+ self.assertIs(A.initialized, A)
+ self.assertIs(BaseWithoutInit.initialized, BaseWithoutInit)
+
+ def test_init_subclass_diamond(self):
+ class Base:
+ def __init_subclass__(cls, **kwargs):
+ if HAS_NATIVE_SUPPORT:
+ super().__init_subclass__(**kwargs)
+ cls.calls = []
+
+ class Left(Base):
+ pass
+
+ class Middle:
+ def __init_subclass__(cls, middle, **kwargs):
+ super().__init_subclass__(**kwargs)
+ cls.calls += [middle]
+
+ class Right(Base):
+ def __init_subclass__(cls, right="right", **kwargs):
+ super().__init_subclass__(**kwargs)
+ cls.calls += [right]
+
+ class A(Left, Middle, Right, middle="middle"):
+ pass
+
+ self.assertEqual(A.calls, ["right", "middle"])
+ self.assertEqual(Left.calls, [])
+ self.assertEqual(Right.calls, [])
+
+ def test_set_name(self):
+ class Descriptor:
+ def __set_name__(self, owner, name):
+ self.owner = owner
+ self.name = name
+
+ class A:
+ d = Descriptor()
+
+ self.assertEqual(A.d.name, "d")
+ self.assertIs(A.d.owner, A)
+
+ @skip_if_not_native
+ def test_set_name_metaclass(self):
+ class Meta(type):
+ def __new__(cls, name, bases, ns):
+ ret = super().__new__(cls, name, bases, ns)
+ self.assertEqual(ret.d.name, "d")
+ self.assertIs(ret.d.owner, ret)
+ return 0
+
+ class Descriptor:
+ def __set_name__(self, owner, name):
+ self.owner = owner
+ self.name = name
+
+ class A(metaclass=Meta):
+ d = Descriptor()
+ self.assertEqual(A, 0)
+
+ def test_set_name_error(self):
+ class Descriptor:
+ def __set_name__(self, owner, name):
+ 1 / ZERO
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NotGoingToWork:
+ attr = Descriptor()
+
+ exc = cm.exception
+ self.assertRegex(str(exc), r'\bNotGoingToWork\b')
+ self.assertRegex(str(exc), r'\battr\b')
+ self.assertRegex(str(exc), r'\bDescriptor\b')
+ if HAS_NATIVE_SUPPORT:
+ self.assertIsInstance(exc.__cause__, ZeroDivisionError)
+
+ def test_set_name_wrong(self):
+ class Descriptor:
+ def __set_name__(self):
+ pass
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NotGoingToWork:
+ attr = Descriptor()
+
+ exc = cm.exception
+ self.assertRegex(str(exc), r'\bNotGoingToWork\b')
+ self.assertRegex(str(exc), r'\battr\b')
+ self.assertRegex(str(exc), r'\bDescriptor\b')
+ if HAS_NATIVE_SUPPORT:
+ self.assertIsInstance(exc.__cause__, TypeError)
+
+ def test_set_name_lookup(self):
+ resolved = []
+ class NonDescriptor:
+ def __getattr__(self, name):
+ resolved.append(name)
+
+ class A:
+ d = NonDescriptor()
+
+ self.assertNotIn('__set_name__', resolved,
+ '__set_name__ is looked up in instance dict')
+
+ @skip_if_not_native
+ def test_set_name_init_subclass(self):
+ class Descriptor:
+ def __set_name__(self, owner, name):
+ self.owner = owner
+ self.name = name
+
+ class Meta(type):
+ def __new__(cls, name, bases, ns):
+ self = super().__new__(cls, name, bases, ns)
+ self.meta_owner = self.owner
+ self.meta_name = self.name
+ return self
+
+ class A:
+ def __init_subclass__(cls):
+ cls.owner = cls.d.owner
+ cls.name = cls.d.name
+
+ class B(A, metaclass=Meta):
+ d = Descriptor()
+
+ self.assertIs(B.owner, B)
+ self.assertEqual(B.name, 'd')
+ self.assertIs(B.meta_owner, B)
+ self.assertEqual(B.name, 'd')
+
+ def test_set_name_modifying_dict(self):
+ notified = []
+ class Descriptor:
+ def __set_name__(self, owner, name):
+ setattr(owner, name + 'x', None)
+ notified.append(name)
+
+ class A:
+ a = Descriptor()
+ b = Descriptor()
+ c = Descriptor()
+ d = Descriptor()
+ e = Descriptor()
+
+ self.assertCountEqual(notified, ['a', 'b', 'c', 'd', 'e'])
+
+ def test_errors(self):
+ class MyMeta(type):
+ pass
+
+ with self.assertRaises(TypeError):
+ class MyClass(metaclass=MyMeta, otherarg=1):
+ pass
+
+ if not IS_PY2:
+ with self.assertRaises(TypeError):
+ types.new_class("MyClass", (object,),
+ dict(metaclass=MyMeta, otherarg=1))
+ types.prepare_class("MyClass", (object,),
+ dict(metaclass=MyMeta, otherarg=1))
+
+ class MyMeta(type):
+ def __init__(self, name, bases, namespace, otherarg):
+ super().__init__(name, bases, namespace)
+
+ with self.assertRaises(TypeError):
+ class MyClass(metaclass=MyMeta, otherarg=1):
+ pass
+
+ class MyMeta(type):
+ def __new__(cls, name, bases, namespace, otherarg):
+ return super().__new__(cls, name, bases, namespace)
+
+ def __init__(self, name, bases, namespace, otherarg):
+ super().__init__(name, bases, namespace)
+ self.otherarg = otherarg
+
+ class MyClass(metaclass=MyMeta, otherarg=1):
+ pass
+
+ self.assertEqual(MyClass.otherarg, 1)
+
+ @skip_if_not_native
+ def test_errors_changed_pep487(self):
+ # These tests failed before Python 3.6, PEP 487
+ class MyMeta(type):
+ def __new__(cls, name, bases, namespace):
+ return super().__new__(cls, name=name, bases=bases,
+ dict=namespace)
+
+ with self.assertRaises(TypeError):
+ class MyClass(metaclass=MyMeta):
+ pass
+
+ class MyMeta(type):
+ def __new__(cls, name, bases, namespace, otherarg):
+ self = super().__new__(cls, name, bases, namespace)
+ self.otherarg = otherarg
+ return self
+
+ class MyClass(metaclass=MyMeta, otherarg=1):
+ pass
+
+ self.assertEqual(MyClass.otherarg, 1)
+
+ def test_type(self):
+ t = type('NewClass', (object,), {})
+ self.assertIsInstance(t, type)
+ self.assertEqual(t.__name__, 'NewClass')
+
+ with self.assertRaises(TypeError):
+ type(name='NewClass', bases=(object,), dict={})
+
+
+if __name__ == "__main__":
+ unittest.main()