diff --git a/addons/source-python/packages/source-python/core/cache.py b/addons/source-python/packages/source-python/core/cache.py index 54d20ed9c..70361dbe3 100644 --- a/addons/source-python/packages/source-python/core/cache.py +++ b/addons/source-python/packages/source-python/core/cache.py @@ -1,11 +1,6 @@ # ../core/cache.py -"""Provides caching functionality. - -.. data:: cached_property - An alias of :class:`core.cache.CachedProperty`. -""" - +"""Provides caching functionality.""" # ============================================================================= # >> IMPORTS @@ -13,6 +8,8 @@ # Python Imports # FuncTools from functools import wraps +# Inspect +from inspect import isdatadescriptor # Types from types import MethodType @@ -23,7 +20,6 @@ # Source.Python Imports # Core from _core._cache import CachedProperty -from _core._cache import cached_property # ============================================================================= @@ -31,7 +27,7 @@ # ============================================================================= __all__ = [ 'CachedProperty', - 'cached_property' + 'cached_property', 'cached_result' ] @@ -39,39 +35,17 @@ # ============================================================================= # >> FUNCTIONS # ============================================================================= -def cached_result(fget): - """Decorator used to register a cached method. - - :param function fget: - Method that is only called once and its result cached for subsequent - calls. - :rtype: CachedProperty - """ - # Get a dummy object as default cache, so that we can cache None - NONE = object() - - def getter(self): - """Getter function that generates the cached method.""" - # Set our cache to the default value - cache = NONE - - # Wrap the decorated method as the inner function - @wraps(fget) - def wrapper(self, *args, **kwargs): - """Calls the decorated method and cache the result.""" - nonlocal cache - - # Did we cache a result already? - if cache is NONE: - - # No cache, let's call the wrapped method and cache the result - cache = fget(self, *args, **kwargs) - - # Return the cached result - return cache +def cached_property(fget=None, *args, **kwargs): + """Decorator used to register or wrap a cached property.""" + return (CachedProperty + if not isdatadescriptor(fget) + else CachedProperty.wrap_descriptor + )(fget, *args, **kwargs) - # Bind the wrapper function to the passed instance and return it - return MethodType(wrapper, self) - # Return a cached property bound to the getter function - return CachedProperty(getter, doc=fget.__doc__, unbound=True) +def cached_result(fget, *args, **kwargs): + """Decorator used to register a cached method.""" + return CachedProperty( + lambda self: MethodType(fget, self), + *args, **kwargs + ) diff --git a/addons/source-python/packages/source-python/entities/_base.py b/addons/source-python/packages/source-python/entities/_base.py index e2bb09c92..07b2505f3 100755 --- a/addons/source-python/packages/source-python/entities/_base.py +++ b/addons/source-python/packages/source-python/entities/_base.py @@ -163,7 +163,7 @@ def __setattr__(cls, attr, value): # assigned attribute is a descriptor defined by a subclass. del cls.attributes - @cached_property(unbound=True) + @cached_property def attributes(cls): """Returns all the attributes available for this class. diff --git a/addons/source-python/packages/source-python/entities/helpers.py b/addons/source-python/packages/source-python/entities/helpers.py index 1d67fbf7b..250d06018 100644 --- a/addons/source-python/packages/source-python/entities/helpers.py +++ b/addons/source-python/packages/source-python/entities/helpers.py @@ -5,10 +5,6 @@ # ============================================================================= # >> IMPORTS # ============================================================================= -# Python Imports -# WeakRef -from weakref import proxy - # Source.Python Imports # Core from core.cache import CachedProperty @@ -101,7 +97,7 @@ def __init__(self, wrapped_self, wrapper): func = wrapped_self.__getattr__(wrapper.__name__) super().__init__(func._manager, func._type_name, func, func._this) self.wrapper = wrapper - self.wrapped_self = proxy(wrapped_self) + self.wrapped_self = wrapped_self def __call__(self, *args, **kwargs): return super().__call__( diff --git a/src/core/modules/core/core_cache.cpp b/src/core/modules/core/core_cache.cpp index da40bc2cd..ac92534fa 100644 --- a/src/core/modules/core/core_cache.cpp +++ b/src/core/modules/core/core_cache.cpp @@ -36,14 +36,13 @@ //----------------------------------------------------------------------------- CCachedProperty::CCachedProperty( object fget=object(), object fset=object(), object fdel=object(), object doc=object(), - bool unbound=false, boost::python::tuple args=boost::python::tuple(), object kwargs=object()) + boost::python::tuple args=boost::python::tuple(), object kwargs=object()) { set_getter(fget); set_setter(fset); set_deleter(fdel); m_doc = doc; - m_bUnbound = unbound; m_args = args; @@ -65,52 +64,6 @@ object CCachedProperty::_callable_check(object function, const char *szName) return function; } -object CCachedProperty::_prepare_value(object value) -{ - if (!PyGen_Check(value.ptr())) - return value; - - if (getattr(value, "gi_frame").is_none()) - BOOST_RAISE_EXCEPTION( - PyExc_ValueError, - "The given generator is exhausted." - ); - - list values; - while (true) - { - try - { - values.append(value.attr("__next__")()); - } - catch(...) - { - if (!PyErr_ExceptionMatches(PyExc_StopIteration)) - throw_error_already_set(); - - PyErr_Clear(); - break; - } - } - - return values; -} - -void CCachedProperty::_invalidate_cache(PyObject *pRef) -{ - try - { - m_cache[handle<>(pRef)].del(); - } - catch (...) - { - if (!PyErr_ExceptionMatches(PyExc_KeyError)) - throw_error_already_set(); - - PyErr_Clear(); - } -} - object CCachedProperty::get_getter() { @@ -155,81 +108,88 @@ str CCachedProperty::get_name() object CCachedProperty::get_owner() { - return m_owner(); + return m_owner; } object CCachedProperty::get_cached_value(object instance) { - if (!m_name) + if (!m_name || m_owner.is_none()) BOOST_RAISE_EXCEPTION( PyExc_AttributeError, "Unable to retrieve the value of an unbound property." ); - object value; + PyObject *pValue = NULL; + PyObject **ppDict = _PyObject_GetDictPtr(instance.ptr()); - if (m_bUnbound) - value = m_cache[ - handle<>( - PyWeakref_NewRef(instance.ptr(), NULL) - ) - ]; - else - { - dict cache = extract(instance.attr("__dict__")); - value = cache[m_name]; + + if (ppDict && *ppDict) { + pValue = PyDict_GetItem(*ppDict, m_name.ptr()); } - return value; + if (!pValue) { + const char *szName = extract(m_name); + BOOST_RAISE_EXCEPTION( + PyExc_KeyError, + "No cached value found for '%s'.", + szName + ) + } + + return object(handle<>(borrowed(pValue))); } void CCachedProperty::set_cached_value(object instance, object value) { - if (!m_name) + if (!m_name || m_owner.is_none()) BOOST_RAISE_EXCEPTION( PyExc_AttributeError, "Unable to assign the value of an unbound property." ); - if (m_bUnbound) - m_cache[handle<>( - PyWeakref_NewRef( - instance.ptr(), - make_function( - boost::bind(&CCachedProperty::_invalidate_cache, this, _1), - default_call_policies(), - boost::mpl::vector2() - ).ptr() - ) - )] = _prepare_value(value); - else - { - dict cache = extract(instance.attr("__dict__")); - cache[m_name] = _prepare_value(value); + if (!PyObject_IsInstance(instance.ptr(), m_owner.ptr())) { + const char *szOwner = extract(m_owner.attr("__qualname__")); + BOOST_RAISE_EXCEPTION( + PyExc_TypeError, + "Given instance is not of type '%s'.", + szOwner + ) } + + if (PyGen_Check(value.ptr())) { + return; + } + + PyObject *pDict = PyObject_GenericGetDict(instance.ptr(), NULL); + + if (!pDict) { + const char *szOwner = extract(m_owner.attr("__qualname__")); + BOOST_RAISE_EXCEPTION( + PyExc_AttributeError, + "'%s' object has no attribute '__dict__'", + szOwner + ) + } + + PyDict_SetItem(pDict, m_name.ptr(), value.ptr()); + Py_XDECREF(pDict); } void CCachedProperty::delete_cached_value(object instance) { - try - { - if (m_bUnbound) - m_cache[ - handle<>( - PyWeakref_NewRef(instance.ptr(), NULL) - ) - ].del(); - else - { - dict cache = extract(instance.attr("__dict__")); - cache[m_name].del(); - } + PyObject **ppDict = _PyObject_GetDictPtr(instance.ptr()); + + if (!ppDict && !*ppDict) { + return; } - catch (...) - { - if (!PyErr_ExceptionMatches(PyExc_KeyError)) + + PyDict_DelItem(*ppDict, m_name.ptr()); + + if (PyErr_Occurred()) { + if (!PyErr_ExceptionMatches(PyExc_KeyError)) { throw_error_already_set(); + } PyErr_Clear(); } @@ -239,6 +199,14 @@ void CCachedProperty::delete_cached_value(object instance) object CCachedProperty::bind(object self, object owner, str name) { CCachedProperty &pSelf = extract(self); + + if (owner.is_none() && !name) { + BOOST_RAISE_EXCEPTION( + PyExc_ValueError, + "Must provide a name and an owner." + ) + } + owner.attr(name) = self; pSelf.__set_name__(owner, name); return self; @@ -247,9 +215,9 @@ object CCachedProperty::bind(object self, object owner, str name) void CCachedProperty::__set_name__(object owner, str name) { - if (m_name && !m_owner.is_none()) + if (m_name || !m_owner.is_none()) { - const char *szName = extract(str(".").join(make_tuple(m_owner().attr("__qualname__"), m_name))); + const char *szName = extract(str(".").join(make_tuple(m_owner.attr("__qualname__"), m_name))); BOOST_RAISE_EXCEPTION( PyExc_RuntimeError, "This property was already bound as \"%s\".", @@ -258,7 +226,7 @@ void CCachedProperty::__set_name__(object owner, str name) } m_name = name; - m_owner = object(handle<>(PyWeakref_NewRef(owner.ptr(), NULL))); + m_owner = owner; } object CCachedProperty::__get__(object self, object instance, object owner=object()) @@ -349,11 +317,11 @@ void CCachedProperty::__setitem__(str item, object value) CCachedProperty *CCachedProperty::wrap_descriptor( object descriptor, object owner, str name, - bool unbound, boost::python::tuple args, object kwargs) + boost::python::tuple args, object kwargs) { CCachedProperty *pProperty = new CCachedProperty( descriptor.attr("__get__"), descriptor.attr("__set__"), descriptor.attr("__delete__"), - descriptor.attr("__doc__"), unbound, args, kwargs + descriptor.attr("__doc__"), args, kwargs ); pProperty->__set_name__(owner, name); diff --git a/src/core/modules/core/core_cache.h b/src/core/modules/core/core_cache.h index c784b26e3..8ba32f85b 100644 --- a/src/core/modules/core/core_cache.h +++ b/src/core/modules/core/core_cache.h @@ -41,13 +41,11 @@ class CCachedProperty { public: CCachedProperty( - object fget, object fset, object fdel, object doc, bool unbound, + object fget, object fset, object fdel, object doc, boost::python::tuple args, object kwargs ); static object _callable_check(object function, const char *szName); - static object _prepare_value(object value); - void _invalidate_cache(PyObject *pRef); object get_getter(); object set_getter(object fget); @@ -77,7 +75,7 @@ class CCachedProperty static CCachedProperty *wrap_descriptor( object descriptor, object owner=object(), str name=str(), - bool unbound=false, boost::python::tuple args=boost::python::tuple(), object kwargs=object() + boost::python::tuple args=boost::python::tuple(), object kwargs=object() ); private: @@ -88,9 +86,6 @@ class CCachedProperty str m_name; object m_owner; - bool m_bUnbound; - dict m_cache; - public: object m_doc; diff --git a/src/core/modules/core/core_cache_wrap.cpp b/src/core/modules/core/core_cache_wrap.cpp index 358b15921..c2c9a27bf 100644 --- a/src/core/modules/core/core_cache_wrap.cpp +++ b/src/core/modules/core/core_cache_wrap.cpp @@ -54,10 +54,10 @@ void export_cached_property(scope _cache) { class_ CachedProperty( "CachedProperty", - init( + init( ( arg("self"), arg("fget")=object(), arg("fset")=object(), arg("fdel")=object(), arg("doc")=object(), - arg("unbound")=false, arg("args")=boost::python::tuple(), arg("kwargs")=object() + arg("args")=boost::python::tuple(), arg("kwargs")=object() ), "Represents a property attribute that is only" " computed once and cached.\n" @@ -83,10 +83,6 @@ void export_cached_property(scope _cache) " Deleter signature: self, *args, **kwargs\n" ":param str doc:\n" " Documentation string for this property.\n" - ":param bool unbound:\n" - " Whether the cached objects should be independently maintained rather than bound to" - " the instance they belong to. The cache will be slightly slower to lookup, but this can" - " be required for instances that do not have a `__dict__` attribute.\n" ":param tuple args:\n" " Extra arguments passed to the getter, setter and deleter functions.\n" ":param dict kwargs:\n" @@ -95,9 +91,8 @@ void export_cached_property(scope _cache) ":raises TypeError:\n" " If the given getter, setter or deleter is not callable.\n" "\n" - ".. warning ::\n" - " If a cached object hold a strong reference of the instance it belongs to," - " this will result in a circular reference preventing their garbage collection." + ".. note ::\n" + " Generator values cannot be cached." "\n" "Example:\n" "\n" @@ -431,10 +426,8 @@ void export_cached_property(scope _cache) " If the getter, setter or deleter are not callable.", ( "descriptor", arg("owner")=object(), arg("name")=str(), - arg("unbound")=false, arg("args")=boost::python::tuple(), arg("kwargs")=object() + arg("args")=boost::python::tuple(), arg("kwargs")=object() ) ) .staticmethod("wrap_descriptor"); - - scope().attr("cached_property") = scope().attr("CachedProperty"); } diff --git a/src/core/sp_python.cpp b/src/core/sp_python.cpp index 5512e1c63..eae79463e 100644 --- a/src/core/sp_python.cpp +++ b/src/core/sp_python.cpp @@ -56,6 +56,7 @@ CPythonManager g_PythonManager; // Forward declarations. //--------------------------------------------------------------------------------- void InitConverters(); +void EnableDictTraversal(); //--------------------------------------------------------------------------------- @@ -168,6 +169,9 @@ bool CPythonManager::Initialize( void ) // And of course, the plugins directory for script imports. AddToSysPath("/plugins"); + // Enable circular references traversal + EnableDictTraversal(); + // Initialize all converters InitConverters(); @@ -262,6 +266,39 @@ bool CPythonManager::Shutdown( void ) } +//--------------------------------------------------------------------------------- +// Circular references traversal +//--------------------------------------------------------------------------------- +struct dict_traversal +{ + static int is_gc(PyObject *self) + { + return !!downcast >(self)->dict; + } + + static int traverse(PyObject *self, visitproc visit, void *arg) + { + Py_VISIT(downcast >(self)->dict); + return 0; + } + + static int clear(PyObject *self) + { + Py_CLEAR(downcast >(self)->dict); + return 0; + } +}; + +void EnableDictTraversal() +{ + PyTypeObject *type = objects::class_type().get(); + type->tp_flags |= Py_TPFLAGS_HAVE_GC; + type->tp_is_gc = dict_traversal::is_gc; + type->tp_traverse = dict_traversal::traverse; + type->tp_clear = dict_traversal::clear; +} + + //--------------------------------------------------------------------------------- // Converters //---------------------------------------------------------------------------------