Skip to content

Commit

Permalink
Add clear_method_caches to Utils.py (cython#4338)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
0dminnimda authored Oct 28, 2021
1 parent db19667 commit cfb8879
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
88 changes: 87 additions & 1 deletion Cython/Tests/TestCythonUtils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,96 @@
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):
self.assertEqual('0x001D00A1', build_hex_version('0.29a1'))
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})
27 changes: 26 additions & 1 deletion Cython/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

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 = []
Expand All @@ -54,8 +57,30 @@ def wrapper(*args):
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)
Expand Down

0 comments on commit cfb8879

Please sign in to comment.