diff --git a/edgedb/datatypes/datatypes.pxd b/edgedb/datatypes/datatypes.pxd index 12fd0769..fdb5109b 100644 --- a/edgedb/datatypes/datatypes.pxd +++ b/edgedb/datatypes/datatypes.pxd @@ -67,3 +67,5 @@ cdef namedtuple_new(object namedtuple_type) cdef namedtuple_type_new(object desc) cdef object_new(object desc) cdef object_set(object tuple, Py_ssize_t pos, object elem) + +cdef extern cpython.PyObject* at_sign_ptr diff --git a/edgedb/datatypes/datatypes.pyx b/edgedb/datatypes/datatypes.pyx index 4faa20ec..98b8f833 100644 --- a/edgedb/datatypes/datatypes.pyx +++ b/edgedb/datatypes/datatypes.pyx @@ -35,6 +35,8 @@ Array = list Link = EdgeLink_InitType() LinkSet = EdgeLinkSet_InitType() +cdef str at_sign = "@" +at_sign_ptr = at_sign _EDGE_POINTER_IS_IMPLICIT = EDGE_POINTER_IS_IMPLICIT _EDGE_POINTER_IS_LINKPROP = EDGE_POINTER_IS_LINKPROP diff --git a/edgedb/datatypes/object.c b/edgedb/datatypes/object.c index b8202efa..03268efe 100644 --- a/edgedb/datatypes/object.c +++ b/edgedb/datatypes/object.c @@ -25,6 +25,7 @@ static int init_type_called = 0; +PyObject* at_sign_ptr; EDGE_SETUP_FREELIST( @@ -186,7 +187,7 @@ object_getattr(EdgeObject *o, PyObject *name) case L_ERROR: return NULL; - case L_NOT_FOUND: + case L_NOT_FOUND: { // Used in `dataclasses.as_dict()` if ( PyUnicode_CompareWithASCIIString( @@ -195,7 +196,46 @@ object_getattr(EdgeObject *o, PyObject *name) ) { return EdgeRecordDesc_GetDataclassFields((PyObject *)o->desc); } + + // getattr(obj, "@...") for link property + int prefixed = PyUnicode_Tailmatch( + name, at_sign_ptr, 0, PY_SSIZE_T_MAX, -1 + ); + if (prefixed == -1) { + return NULL; + } + if (prefixed) { + PyObject *stripped = PyUnicode_Substring( + name, 1, PyUnicode_GET_LENGTH(name) + ); + if (stripped == NULL) { + return NULL; + } + ret = EdgeRecordDesc_Lookup( + (PyObject *)o->desc, stripped, &pos); + Py_DECREF(stripped); + switch (ret) { + case L_ERROR: + return NULL; + + case L_NOT_FOUND: + case L_LINK: + case L_PROPERTY: + return PyObject_GenericGetAttr((PyObject *)o, name); + + case L_LINKPROP: { + PyObject *val = EdgeObject_GET_ITEM(o, pos); + Py_INCREF(val); + return val; + } + + default: + abort(); + } + } + return PyObject_GenericGetAttr((PyObject *)o, name); + } case L_LINKPROP: return PyObject_GenericGetAttr((PyObject *)o, name); @@ -216,28 +256,87 @@ static PyObject * object_getitem(EdgeObject *o, PyObject *name) { Py_ssize_t pos; + int prefixed = 0; + PyObject *stripped = name; + if (PyUnicode_Check(name)) { + prefixed = PyUnicode_Tailmatch( + name, at_sign_ptr, 0, PY_SSIZE_T_MAX, -1 + ); + if (prefixed == -1) { + return NULL; + } + if (prefixed) { + stripped = PyUnicode_Substring( + name, 1, PyUnicode_GET_LENGTH(name) + ); + if (stripped == NULL) { + return NULL; + } + } + } + edge_attr_lookup_t ret = EdgeRecordDesc_Lookup( - (PyObject *)o->desc, name, &pos); + (PyObject *)o->desc, stripped, &pos + ); + if (prefixed) { + Py_DECREF(stripped); + } switch (ret) { case L_ERROR: return NULL; case L_PROPERTY: - PyErr_Format( - PyExc_TypeError, - "property %R should be accessed via dot notation", - name); + if (prefixed) { + PyErr_Format( + PyExc_KeyError, + "link property %R does not exist", + name); + } else { + PyErr_Format( + PyExc_TypeError, + "property %R should be accessed via dot notation", + name); + } return NULL; case L_LINKPROP: + if (prefixed) { + PyObject *val = EdgeObject_GET_ITEM(o, pos); + Py_INCREF(val); + return val; + } else { + PyErr_Format( + PyExc_TypeError, + "link property %R should be accessed with '@' prefix", + name); + return NULL; + } + case L_NOT_FOUND: PyErr_Format( PyExc_KeyError, - "link %R does not exist", + "link property %R does not exist", name); return NULL; case L_LINK: { + if (prefixed) { + PyErr_Format( + PyExc_KeyError, + "link property %R does not exist", + name); + return NULL; + } + int res = PyErr_WarnEx( + PyExc_DeprecationWarning, + "getting link on object is deprecated since 1.0, " + "please use dot notation to access linked objects, " + "and a following ['@...'] for the link properties.", + 1 + ); + if (res != 0) { + return NULL; + } PyObject *val = EdgeObject_GET_ITEM(o, pos); if (PyList_Check(val)) { diff --git a/tests/datatypes/test_datatypes.py b/tests/datatypes/test_datatypes.py index 8f612427..1dc579b5 100644 --- a/tests/datatypes/test_datatypes.py +++ b/tests/datatypes/test_datatypes.py @@ -23,6 +23,7 @@ import random import string import unittest +import warnings import weakref import edgedb @@ -30,6 +31,14 @@ from edgedb import introspect +def test_deprecated(f): + def wrapper(*args, **kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return f(*args, **kwargs) + return wrapper + + class TestRecordDesc(unittest.TestCase): def test_recorddesc_1(self): @@ -568,6 +577,7 @@ def test_object_6(self): "accessed via dot notation"): u['name'] + @test_deprecated def test_object_links_1(self): O2 = private.create_object_factory( id='property', @@ -616,6 +626,7 @@ def test_object_links_1(self): with self.assertRaises(AttributeError): link2.aaaa + @test_deprecated def test_object_links_2(self): User = private.create_object_factory( id='property', @@ -634,6 +645,7 @@ def test_object_links_2(self): self.assertEqual(set(dir(u1)), {'id', 'friends', 'enemies'}) + @test_deprecated def test_object_links_3(self): User = private.create_object_factory( id='property', @@ -659,6 +671,7 @@ def test_object_links_3(self): repr(u2['friend']), "Link(name='friend', source_id=2, target_id=1)") + @test_deprecated def test_object_links_4(self): User = private.create_object_factory( id='property', @@ -667,10 +680,63 @@ def test_object_links_4(self): u = User(1, None) - with self.assertRaisesRegex(KeyError, - "link 'error_key' does not exist"): + with self.assertRaisesRegex( + KeyError, "link property 'error_key' does not exist" + ): u['error_key'] + def test_object_link_property_1(self): + O2 = private.create_object_factory( + id='property', + lb='link-property', + c='property' + ) + + O1 = private.create_object_factory( + id='property', + o2s='link' + ) + + o2_1 = O2(1, 'linkprop o2 1', 3) + o2_2 = O2(4, 'linkprop o2 2', 6) + o1 = O1(2, edgedb.Set((o2_1, o2_2))) + + o2s = o1.o2s + self.assertEqual(len(o2s), 2) + self.assertEqual(o2s, o1.o2s) + self.assertEqual( + repr(o2s), + "[Object{id := 1, @lb := 'linkprop o2 1', c := 3}," + " Object{id := 4, @lb := 'linkprop o2 2', c := 6}]" + ) + + self.assertEqual(o2s[0]['@lb'], 'linkprop o2 1') + self.assertEqual(o2s[1]['@lb'], 'linkprop o2 2') + self.assertEqual(getattr(o2s[0], '@lb'), 'linkprop o2 1') + self.assertEqual(getattr(o2s[1], '@lb'), 'linkprop o2 2') + + with self.assertRaises(AttributeError): + o2s[0].lb + + with self.assertRaises(AttributeError): + getattr(o2s[0], "@lb2") + + with self.assertRaisesRegex( + TypeError, + "link property 'lb' should be accessed with '@' prefix", + ): + o2s[0]['lb'] + + with self.assertRaisesRegex( + TypeError, "property 'c' should be accessed via dot notation" + ): + o2s[0]['c'] + + with self.assertRaisesRegex( + KeyError, "link property '@c' does not exist" + ): + o2s[0]['@c'] + def test_object_dataclass_1(self): User = private.create_object_factory( id='property',