Skip to content

Commit

Permalink
Add a non-raising API to clear cached properties
Browse files Browse the repository at this point in the history
Summary: Adds an API for clearing `cached_property` objects.  This can be simulated with a `__set__` and then a delete against it (to avoid raising an exception on the delete, which would de-optimize the calling method in the JIT), but that's very convoluted way of doing it.  This still ends up being a little weird - you need to access the `cached_property` via the class, and then pass in the instance as the first argument.  But it's a lot better then calling a dunder method.

Reviewed By: sinancepel

Differential Revision: D36319182

fbshipit-source-id: 6292ff6
  • Loading branch information
DinoV authored and facebook-github-bot committed May 16, 2022
1 parent e45880b commit a92d979
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 0 deletions.
82 changes: 82 additions & 0 deletions Lib/test/test_cinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,30 @@ def f(self):
a.f = 42
self.assertEqual(a.f, 42)

def test_cached_property_clear(self):
value = 42

class C:
@cached_property
def f(self):
return value

a = C()
self.assertEqual(a.f, 42)
C.f.clear(a)
value = 100
self.assertEqual(a.f, 100)

def test_cached_property_clear_not_set(self):
class C:
@cached_property
def f(self):
return 42

a = C()
C.f.clear(a)
self.assertEqual(a.f, 42)

def test_cached_property_no_dict(self):
class C:
__slots__ = ()
Expand All @@ -536,6 +560,17 @@ def f(self):
with self.assertRaises(AttributeError):
C().f = 42

def test_cached_property_clear_no_dict(self):
class C:
__slots__ = ()

@cached_property
def f(self):
return 42

with self.assertRaises(AttributeError):
a = C.f.clear(C())

def test_cached_property_name(self):
class C:
@cached_property
Expand Down Expand Up @@ -592,6 +627,53 @@ def f(self):
self.assertEqual(a.f, 42)
self.assertEqual(a.calls, 1)

def test_cached_property_clear_slot(self):
value = 42

class C:

__slots__ = "f"

def f(self):
return value

C.f = cached_property(f, C.f)
a = C()
self.assertEqual(a.f, 42)
C.f.clear(a)
value = 100
self.assertEqual(a.f, 100)

def test_cached_property_clear_slot_not_set(self):
class C:
__slots__ = "f"

def f(self):
return 42

C.f = cached_property(f, C.f)
a = C()
C.f.clear(a)
self.assertEqual(a.f, 42)

def test_cached_property_clear_slot_bad_value(self):
value = 42

class C:

__slots__ = "f"

def f(self):
return value

C.f = cached_property(f, C.f)
a = C()
self.assertEqual(a.f, 42)
with self.assertRaisesRegex(
TypeError, "descriptor 'f' for 'C' objects doesn't apply to a 'int' object"
):
C.f.clear(42)

def test_cached_property_slot_set_del(self):
class C:
__slots__ = ("f", "calls")
Expand Down
42 changes: 42 additions & 0 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* Descriptors -- a new, flexible way to describe attributes */

#include "Python.h"
#include "object.h"
#include "pycore_object.h"
#include "pycore_pystate.h"
#include "pycore_pyerrors.h"
#include "pycore_tupleobject.h"
#include "pyerrors.h"
#include "structmember.h" /* Why is this not included in Python.h? */
#include "classloader.h"

Expand Down Expand Up @@ -2375,6 +2377,41 @@ cached_property_get_slot(PyCachedPropertyDescrObject *cp, void *closure)
Py_RETURN_NONE;
}

static PyObject *
cached_property_clear(PyCachedPropertyDescrObject *self, PyObject *obj)
{
PyCachedPropertyDescrObject *cp = (PyCachedPropertyDescrObject *)self;
PyObject **dictptr;

if (Py_TYPE(cp->name_or_descr) == &PyMemberDescr_Type) {
if (Py_TYPE(cp->name_or_descr)->tp_descr_set(cp->name_or_descr, obj, NULL) < 0) {
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
Py_RETURN_NONE;
}
return NULL;
}
Py_RETURN_NONE;
}

dictptr = _PyObject_GetDictPtr(obj);

if (dictptr == NULL) {
PyErr_SetString(PyExc_AttributeError,
"This object has no __dict__");
return NULL;
}

if (_PyObjectDict_SetItem(Py_TYPE(obj), dictptr, cp->name_or_descr, NULL) < 0) {
if (PyErr_ExceptionMatches(PyExc_KeyError)) {
PyErr_Clear();
Py_RETURN_NONE;
}
return NULL;
}
Py_RETURN_NONE;
}

static PyGetSetDef cached_property_getsetlist[] = {
{"__doc__", (getter)cached_property_get___doc__, NULL, NULL, NULL},
{"__name__", (getter)cached_property_get_name, NULL, NULL, NULL},
Expand All @@ -2390,6 +2427,10 @@ static PyMemberDef cached_property_members[] = {
{0}
};

static PyMethodDef cached_property_methods[] = {
{"clear", (PyCFunction)cached_property_clear, METH_O, NULL},
{NULL, NULL}
};

PyTypeObject PyCachedProperty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
Expand All @@ -2407,6 +2448,7 @@ PyTypeObject PyCachedProperty_Type = {
.tp_init = cached_property_init,
.tp_alloc = PyType_GenericAlloc,
.tp_free = PyObject_GC_Del,
.tp_methods = cached_property_methods,
};

PyTypeObject PyCachedPropertyWithDescr_Type = {
Expand Down

0 comments on commit a92d979

Please sign in to comment.