Skip to content

Commit 88bec54

Browse files
committed
Added "unbound" parameter to CachedProperty as a workaround for circular references.
Entity.__hash__ now hashes its inthandle rather than its index (the later can be reused and match a new entity later on, while the former doesn't seems to be ever re-used from my testing).
1 parent 7747961 commit 88bec54

File tree

5 files changed

+101
-40
lines changed

5 files changed

+101
-40
lines changed

addons/source-python/packages/source-python/entities/_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def __init__(self, index, caching=True):
169169
def __hash__(self):
170170
"""Return a hash value based on the entity index."""
171171
# Required for sets, because we have implemented __eq__
172-
return hash(self.index)
172+
return hash(self.inthandle)
173173

174174
def __getattr__(self, attr):
175175
"""Find if the attribute is valid and returns the appropriate value."""

addons/source-python/packages/source-python/entities/helpers.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
# =============================================================================
66
# >> IMPORTS
77
# =============================================================================
8-
# Python Imports
9-
# WeakRef
10-
from weakref import proxy
11-
128
# Source.Python Imports
139
# Core
1410
from core.cache import CachedProperty
@@ -101,11 +97,7 @@ def __init__(self, wrapped_self, wrapper):
10197
func = wrapped_self.__getattr__(wrapper.__name__)
10298
super().__init__(func._manager, func._type_name, func, func._this)
10399
self.wrapper = wrapper
104-
105-
# Don't store a strong reference to the wrapped instance.
106-
# If we do, we will ends with a circular reference preventing itself,
107-
# along with everything it refers, to ever be garbage collected.
108-
self.wrapped_self = proxy(wrapped_self)
100+
self.wrapped_self = wrapped_self
109101

110102
def __call__(self, *args, **kwargs):
111103
return super().__call__(
@@ -128,5 +120,6 @@ def wrap_entity_mem_func(wrapper):
128120

129121
return CachedProperty(
130122
lambda self: EntityMemFuncWrapper(self, wrapper),
131-
doc=wrapper.__doc__
123+
doc=wrapper.__doc__,
124+
unbound=True
132125
)

src/core/modules/core/core_cache.cpp

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@
3636
//-----------------------------------------------------------------------------
3737
CCachedProperty::CCachedProperty(
3838
object fget=object(), object fset=object(), object fdel=object(), const char *doc=NULL,
39-
boost::python::tuple args=boost::python::tuple(), dict kwargs=dict())
39+
bool unbound=false, boost::python::tuple args=boost::python::tuple(), dict kwargs=dict())
4040
{
4141
set_getter(fget);
4242
set_setter(fset);
4343
set_deleter(fdel);
4444

4545
m_szDocString = doc;
46+
m_bUnbound = unbound;
4647

4748
m_args = args;
4849
m_kwargs = kwargs;
@@ -88,6 +89,66 @@ object CCachedProperty::_prepare_value(object value)
8889
return values;
8990
}
9091

92+
void CCachedProperty::_invalidate_cache(PyObject *pRef)
93+
{
94+
try
95+
{
96+
m_cache[handle<>(pRef)].del();
97+
}
98+
catch (...)
99+
{
100+
if (!PyErr_ExceptionMatches(PyExc_KeyError))
101+
throw_error_already_set();
102+
103+
PyErr_Clear();
104+
}
105+
}
106+
107+
void CCachedProperty::_update_cache(object instance, object value)
108+
{
109+
if (m_bUnbound)
110+
m_cache[handle<>(
111+
PyWeakref_NewRef(
112+
instance.ptr(),
113+
make_function(
114+
boost::bind(&CCachedProperty::_invalidate_cache, this, _1),
115+
default_call_policies(),
116+
boost::mpl::vector2<void, PyObject *>()
117+
).ptr()
118+
)
119+
)] = value;
120+
else
121+
{
122+
dict cache = extract<dict>(instance.attr("__dict__"));
123+
cache[m_name] = value;
124+
}
125+
}
126+
127+
void CCachedProperty::_delete_cache(object instance)
128+
{
129+
try
130+
{
131+
if (m_bUnbound)
132+
m_cache[
133+
handle<>(
134+
PyWeakref_NewRef(instance.ptr(), NULL)
135+
)
136+
].del();
137+
else
138+
{
139+
dict cache = extract<dict>(instance.attr("__dict__"));
140+
cache[m_name].del();
141+
}
142+
}
143+
catch (...)
144+
{
145+
if (!PyErr_ExceptionMatches(PyExc_KeyError))
146+
throw_error_already_set();
147+
148+
PyErr_Clear();
149+
}
150+
}
151+
91152

92153
object CCachedProperty::get_getter()
93154
{
@@ -149,11 +210,21 @@ object CCachedProperty::__get__(object self, object instance, object owner=objec
149210
"Unable to retrieve the value of an unbound property."
150211
);
151212

152-
object cache = extract<dict>(instance.attr("__dict__"));
213+
object value;
153214

154215
try
155216
{
156-
return cache[name];
217+
if (pSelf.m_bUnbound)
218+
return pSelf.m_cache[
219+
handle<>(
220+
PyWeakref_NewRef(instance.ptr(), NULL)
221+
)
222+
];
223+
else
224+
{
225+
dict cache = extract<dict>(instance.attr("__dict__"));
226+
return cache[name];
227+
}
157228
}
158229
catch (...)
159230
{
@@ -169,15 +240,17 @@ object CCachedProperty::__get__(object self, object instance, object owner=objec
169240
"Unable to retrieve the value of a property that have no getter function."
170241
);
171242

172-
cache[name] = pSelf._prepare_value(
243+
value = pSelf._prepare_value(
173244
getter(
174245
*(make_tuple(handle<>(borrowed(instance.ptr()))) + pSelf.m_args),
175246
**pSelf.m_kwargs
176247
)
177248
);
249+
250+
pSelf._update_cache(instance, value);
178251
}
179252

180-
return cache[name];
253+
return value;
181254
}
182255

183256

@@ -194,20 +267,10 @@ void CCachedProperty::__set__(object instance, object value)
194267
**m_kwargs
195268
);
196269

197-
dict cache = extract<dict>(instance.attr("__dict__"));
198270
if (!result.is_none())
199-
cache[m_name] = _prepare_value(result);
271+
_update_cache(instance, _prepare_value(result));
200272
else
201-
{
202-
try
203-
{
204-
cache[m_name].del();
205-
}
206-
catch (...)
207-
{
208-
PyErr_Clear();
209-
}
210-
}
273+
_delete_cache(instance);
211274
}
212275

213276
void CCachedProperty::__delete__(object instance)
@@ -218,15 +281,7 @@ void CCachedProperty::__delete__(object instance)
218281
**m_kwargs
219282
);
220283

221-
dict cache = extract<dict>(instance.attr("__dict__"));
222-
try
223-
{
224-
cache[m_name].del();
225-
}
226-
catch (...)
227-
{
228-
PyErr_Clear();
229-
}
284+
_delete_cache(instance);
230285
}
231286

232287
object CCachedProperty::__call__(object self, object fget)

src/core/modules/core/core_cache.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ using namespace boost::python;
4040
class CCachedProperty
4141
{
4242
public:
43-
CCachedProperty(object fget, object fset, object fdel, const char *doc, boost::python::tuple args, dict kwargs);
43+
CCachedProperty(
44+
object fget, object fset, object fdel, const char *doc, bool unbound,
45+
boost::python::tuple args, dict kwargs
46+
);
4447

4548
static object _callable_check(object function, const char *szName);
4649
static object _prepare_value(object value);
50+
void _invalidate_cache(PyObject *pRef);
51+
void _update_cache(object instance, object value);
52+
void _delete_cache(object instance);
4753

4854
object get_getter();
4955
object set_getter(object fget);
@@ -73,6 +79,9 @@ class CCachedProperty
7379

7480
str m_name;
7581

82+
bool m_bUnbound;
83+
dict m_cache;
84+
7685
public:
7786
const char *m_szDocString;
7887

src/core/modules/core/core_cache_wrap.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ void export_cached_property(scope _cache)
5454
{
5555
class_<CCachedProperty, CCachedProperty *> CachedProperty(
5656
"CachedProperty",
57-
init<object, object, object, const char *, boost::python::tuple, dict>(
57+
init<object, object, object, const char *, bool, boost::python::tuple, dict>(
5858
(
5959
arg("fget")=object(), arg("fset")=object(), arg("fdel")=object(), arg("doc")=object(),
60-
arg("args")=boost::python::tuple(), arg("kwargs")=dict()
60+
arg("unbound")=false, arg("args")=boost::python::tuple(), arg("kwargs")=dict()
6161
),
6262
"Represents a property attribute that is only"
6363
" computed once and cached.\n"
@@ -83,6 +83,10 @@ void export_cached_property(scope _cache)
8383
" Deleter signature: self, *args, **kwargs\n"
8484
":param str doc:\n"
8585
" Documentation string for this property.\n"
86+
":param bool unbound\n"
87+
" Whether the cached objects should be independently maintained rather than bound to"
88+
" the instance they belong to. The cache will be slightly slower to lookup, but this can"
89+
" be required in certain cases to prevent circular references/memory leak.\n"
8690
":param tuple args:\n"
8791
" Extra arguments passed to the getter, setter and deleter functions.\n"
8892
":param dict kwargs:\n"

0 commit comments

Comments
 (0)