Skip to content

Commit

Permalink
Deprecate object links and simplify link property access (#379)
Browse files Browse the repository at this point in the history
Accessing `edgedb.Link` or `edgedb.LinkSet` on `edgedb.Object` instances
through `__getitem__()` will now raise a `DeprecationWarning`. Accessing
link properties through links will be dropped in a future version.
Instead, link properties can be accessed now through
`edgedb.Object.__getitem__(linked_obj, '@link_prop_name')`, or in short,
`linked_obj['@link_prop_name']`.

* Also added support for `getattr(obj, "@...")`
  • Loading branch information
fantix committed Oct 19, 2022
1 parent 04b0da2 commit 2c5dcc7
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 9 deletions.
2 changes: 2 additions & 0 deletions edgedb/datatypes/datatypes.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions edgedb/datatypes/datatypes.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Array = list
Link = EdgeLink_InitType()
LinkSet = EdgeLinkSet_InitType()

cdef str at_sign = "@"
at_sign_ptr = <cpython.PyObject*>at_sign

_EDGE_POINTER_IS_IMPLICIT = EDGE_POINTER_IS_IMPLICIT
_EDGE_POINTER_IS_LINKPROP = EDGE_POINTER_IS_LINKPROP
Expand Down
113 changes: 106 additions & 7 deletions edgedb/datatypes/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@


static int init_type_called = 0;
PyObject* at_sign_ptr;


EDGE_SETUP_FREELIST(
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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)) {
Expand Down
70 changes: 68 additions & 2 deletions tests/datatypes/test_datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@
import random
import string
import unittest
import warnings
import weakref

import edgedb
from edgedb.datatypes import datatypes as private
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):
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down

0 comments on commit 2c5dcc7

Please sign in to comment.