summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author0dminnimda <0dminnimda@gmail.com>2021-10-28 10:14:32 +0300
committerGitHub <noreply@github.com>2021-10-28 09:14:32 +0200
commitcfb8879c04b9bb7281760200600b6e9e3d9010bc (patch)
tree17a8433ac10969ad9a12b1cca8a40e593287f499
parentdb19667d81066aa20cf13ab3ea9cba8d5ab9c948 (diff)
downloadcython-cfb8879c04b9bb7281760200600b6e9e3d9010bc.tar.gz
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
-rw-r--r--.github/workflows/ci.yml4
-rw-r--r--Cython/Tests/TestCythonUtils.py88
-rw-r--r--Cython/Utils.py27
3 files changed, 115 insertions, 4 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca0f43a88..7a70f8517 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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:
diff --git a/Cython/Tests/TestCythonUtils.py b/Cython/Tests/TestCythonUtils.py
index 878562512..e4882acc0 100644
--- a/Cython/Tests/TestCythonUtils.py
+++ b/Cython/Tests/TestCythonUtils.py
@@ -1,6 +1,18 @@
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})
diff --git a/Cython/Utils.py b/Cython/Utils.py
index e6c98f583..d20972e5b 100644
--- a/Cython/Utils.py
+++ b/Cython/Utils.py
@@ -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)