diff options
author | 0dminnimda <0dminnimda@gmail.com> | 2021-10-28 10:14:32 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-28 09:14:32 +0200 |
commit | cfb8879c04b9bb7281760200600b6e9e3d9010bc (patch) | |
tree | 17a8433ac10969ad9a12b1cca8a40e593287f499 | |
parent | db19667d81066aa20cf13ab3ea9cba8d5ab9c948 (diff) | |
download | cython-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.yml | 4 | ||||
-rw-r--r-- | Cython/Tests/TestCythonUtils.py | 88 | ||||
-rw-r--r-- | Cython/Utils.py | 27 |
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) |