Commit cfb8879c authored by 0dminnimda's avatar 0dminnimda Committed by GitHub

Add `clear_method_caches` to Utils.py (#4338)

* Utils.py: add _find_cache_attributes, clear_method_caches

* TestCythonUtils.py: add tests for Cached Methods

* Utils.py: add constants

* Utils.py: update comment

* TestCythonUtils.py: remove excess blank line

* Change names to `_CACHE_NAME` and `_CACHE_NAME_PATTERN`

* ci.yml: extend timeout to 40 minutes

* _CACHE_NAME -> _build_cache_name
parent db19667d
......@@ -205,9 +205,9 @@ jobs:
env: { MACOSX_DEPLOYMENT_TARGET: 10.14 }
# This defaults to 360 minutes (6h) which is way too long and if a test gets stuck, it can block other pipelines.
# From testing, the runs tend to take ~20 minutes, so a limit of 30 minutes should be enough. This can always be
# From testing, the runs tend to take ~20/~30 minutes, so a limit of 40 minutes should be enough. This can always be
# changed in the future if needed.
timeout-minutes: 30
timeout-minutes: 40
runs-on: ${{ matrix.os }}
env:
......
import unittest
from ..Utils import build_hex_version
from Cython.Utils import (
_CACHE_NAME_PATTERN, _build_cache_name, _find_cache_attributes,
build_hex_version, cached_method, clear_method_caches)
METHOD_NAME = "cached_next"
CACHE_NAME = _build_cache_name(METHOD_NAME)
NAMES = CACHE_NAME, METHOD_NAME
class Cached(object):
@cached_method
def cached_next(self, x):
return next(x)
class TestCythonUtils(unittest.TestCase):
def test_build_hex_version(self):
......@@ -8,3 +20,77 @@ class TestCythonUtils(unittest.TestCase):
self.assertEqual('0x001D03C4', build_hex_version('0.29.3rc4'))
self.assertEqual('0x001D00F0', build_hex_version('0.29'))
self.assertEqual('0x040000F0', build_hex_version('4.0'))
############################## Cached Methods ##############################
def test_cache_method_name(self):
method_name = "foo"
cache_name = _build_cache_name(method_name)
match = _CACHE_NAME_PATTERN.match(cache_name)
self.assertIsNot(match, None)
self.assertEqual(match.group(1), method_name)
def test_requirements_for_Cached(self):
obj = Cached()
self.assertFalse(hasattr(obj, CACHE_NAME))
self.assertTrue(hasattr(obj, METHOD_NAME))
self.set_of_names_equal(obj, set())
def set_of_names_equal(self, obj, value):
self.assertEqual(set(_find_cache_attributes(obj)), value)
def test_find_cache_attributes(self):
obj = Cached()
method_name = "bar"
cache_name = _build_cache_name(method_name)
setattr(obj, CACHE_NAME, {})
setattr(obj, cache_name, {})
self.assertFalse(hasattr(obj, method_name))
self.set_of_names_equal(obj, {NAMES, (cache_name, method_name)})
def test_cached_method(self):
obj = Cached()
value = iter(range(3)) # iter for Py2
cache = {(value,): 0}
# cache args
self.assertEqual(obj.cached_next(value), 0)
self.set_of_names_equal(obj, {NAMES})
self.assertEqual(getattr(obj, CACHE_NAME), cache)
# use cache
self.assertEqual(obj.cached_next(value), 0)
self.set_of_names_equal(obj, {NAMES})
self.assertEqual(getattr(obj, CACHE_NAME), cache)
def test_clear_method_caches(self):
obj = Cached()
value = iter(range(3)) # iter for Py2
cache = {(value,): 1}
obj.cached_next(value) # cache args
clear_method_caches(obj)
self.set_of_names_equal(obj, set())
self.assertEqual(obj.cached_next(value), 1)
self.set_of_names_equal(obj, {NAMES})
self.assertEqual(getattr(obj, CACHE_NAME), cache)
def test_clear_method_caches_with_missing_method(self):
obj = Cached()
method_name = "bar"
cache_name = _build_cache_name(method_name)
names = cache_name, method_name
setattr(obj, cache_name, object())
self.assertFalse(hasattr(obj, method_name))
self.set_of_names_equal(obj, {names})
clear_method_caches(obj)
self.set_of_names_equal(obj, {names})
......@@ -29,6 +29,9 @@ from . import __version__ as cython_version
PACKAGE_FILES = ("__init__.py", "__init__.pyc", "__init__.pyx", "__init__.pxd")
_build_cache_name = "__{0}_cache".format
_CACHE_NAME_PATTERN = re.compile(r"^__(.+)_cache$")
modification_time = os.path.getmtime
_function_caches = []
......@@ -54,8 +57,30 @@ def cached_function(f):
return wrapper
def _find_cache_attributes(obj):
"""The function iterates over the attributes of the object and,
if it finds the name of the cache, it returns it and the corresponding method name.
The method may not be present in the object.
"""
for attr_name in dir(obj):
match = _CACHE_NAME_PATTERN.match(attr_name)
if match is not None:
yield attr_name, match.group(1)
def clear_method_caches(obj):
"""Removes every cache found in the object,
if a corresponding method exists for that cache.
"""
for cache_name, method_name in _find_cache_attributes(obj):
if hasattr(obj, method_name):
delattr(obj, cache_name)
# if there is no corresponding method, then we assume
# that this attribute was not created by our cached method
def cached_method(f):
cache_name = '__%s_cache' % f.__name__
cache_name = _build_cache_name(f.__name__)
def wrapper(self, *args):
cache = getattr(self, cache_name, None)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment