diff --git a/utils/build_swift/build_swift/cache_utils.py b/utils/build_swift/build_swift/cache_utils.py new file mode 100644 index 0000000000000..ede762d7d2c78 --- /dev/null +++ b/utils/build_swift/build_swift/cache_utils.py @@ -0,0 +1,69 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors + + +""" +Cache related utitlity functions and decorators. +""" + + +from __future__ import absolute_import, unicode_literals + +import functools + + +__all__ = [ + 'cache', + 'reify', +] + + +def cache(func): + """Decorator that caches result of a function call. + + NOTE: This decorator does not play nice with methods as the created cache + is not instance-local, rather it lives in the decorator. + NOTE: When running in Python 3.2 or newer this decorator is replaced with + the standard `functools.lru_cache` using a maxsize of None. + """ + + # Use the standard functools.lru_cache decorator for Python 3.2 and newer. + if hasattr(functools, 'lru_cache'): + return functools.lru_cache(maxsize=None)(func) + + # Otherwise use a naive caching strategy. + _cache = {} + + @functools.wraps(func) + def wrapper(*args, **kwargs): + key = tuple(args) + tuple(kwargs.items()) + + if key not in _cache: + result = func(*args, **kwargs) + _cache[key] = result + return result + + return _cache[key] + return wrapper + + +def reify(func): + """Decorator that replaces the wrapped method with the result after the + first call. Used to wrap property-like methods with no arguments. + """ + + class wrapper(object): + def __get__(self, obj, type=None): + if obj is None: + return self + + result = func(obj) + setattr(obj, func.__name__, result) + return result + + return functools.update_wrapper(wrapper(), func) diff --git a/utils/build_swift/tests/build_swift/test_cache_utils.py b/utils/build_swift/tests/build_swift/test_cache_utils.py new file mode 100644 index 0000000000000..8dba0d434c49c --- /dev/null +++ b/utils/build_swift/tests/build_swift/test_cache_utils.py @@ -0,0 +1,121 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors + + +from __future__ import absolute_import, unicode_literals + +import unittest + +from build_swift import cache_utils + +from .. import utils + + +try: + # Python 3.3 + from unittest import mock +except ImportError: + mock = None + + +class _CallCounter(object): + """Callable helper class used to count and return the number of times an + instance has been called. + """ + + def __init__(self): + self._counter = 0 + + def __call__(self, *args, **kwargs): + count = self._counter + self._counter += 1 + return count + + +class TestCache(unittest.TestCase): + """Unit tests for the cache decorator in the cache_utils module. + """ + + @utils.requires_module('unittest.mock') + @utils.requires_python('3.2') # functools.lru_cache + def test_replaced_with_functools_lru_cache_python_3_2(self): + with mock.patch('functools.lru_cache') as mock_lru_cache: + @cache_utils.cache + def func(): + return None + + mock_lru_cache.assert_called() + + def test_call_with_no_args(self): + # Increments the counter once per unique call. + counter = _CallCounter() + + @cache_utils.cache + def func(*args, **kwargs): + return counter(*args, **kwargs) + + self.assertEqual(func(), 0) + self.assertEqual(func(), 0) + + def test_call_with_args(self): + # Increments the counter once per unique call. + counter = _CallCounter() + + @cache_utils.cache + def func(*args, **kwargs): + return counter(*args, **kwargs) + + self.assertEqual(func(0), 0) + self.assertEqual(func(0), 0) + + self.assertEqual(func(1), 1) + self.assertEqual(func(1), 1) + + self.assertEqual(func(2), 2) + self.assertEqual(func(2), 2) + + def test_call_with_args_and_kwargs(self): + # Increments the counter once per unique call. + counter = _CallCounter() + + @cache_utils.cache + def func(*args, **kwargs): + return counter(*args, **kwargs) + + self.assertEqual(func(n=0), 0) + self.assertEqual(func(n=0), 0) + + self.assertEqual(func(a=1, b='b'), 1) + self.assertEqual(func(a=1, b='b'), 1) + + self.assertEqual(func(0, x=1, y=2.0), 2) + self.assertEqual(func(0, x=1, y=2.0), 2) + + +class TestReify(unittest.TestCase): + """Unit tests for the reify decorator in the cache_utils module. + """ + + def test_replaces_attr_after_first_call(self): + class Counter(object): + def __init__(self): + self._counter = 0 + + @cache_utils.reify + def count(self): + count = self._counter + self._counter += 1 + return count + + counter = Counter() + + self.assertEqual(counter.count, 0) + self.assertEqual(counter.count, 0) + + # Assert that the count property has been replaced with the constant. + self.assertEqual(getattr(counter, 'count'), 0) diff --git a/utils/build_swift/tests/utils.py b/utils/build_swift/tests/utils.py index c21d8361bb8fd..85fc24350e9ff 100644 --- a/utils/build_swift/tests/utils.py +++ b/utils/build_swift/tests/utils.py @@ -15,9 +15,11 @@ import sys import unittest -from six import StringIO +from build_swift import cache_utils +from build_swift.versions import Version -from swift_build_support.swift_build_support import cache_util +import six +from six import StringIO __all__ = [ @@ -27,6 +29,7 @@ 'requires_attr', 'requires_module', 'requires_platform', + 'requires_python', 'BUILD_SCRIPT_IMPL_PATH', 'BUILD_SWIFT_PATH', @@ -38,6 +41,8 @@ # ----------------------------------------------------------------------------- # Constants +_PYTHON_VERSION = Version(platform.python_version()) + TESTS_PATH = os.path.abspath(os.path.dirname(__file__)) BUILD_SWIFT_PATH = os.path.abspath(os.path.join(TESTS_PATH, os.pardir)) UTILS_PATH = os.path.abspath(os.path.join(BUILD_SWIFT_PATH, os.pardir)) @@ -124,9 +129,10 @@ def __exit__(self, exc_type, exc_value, traceback): sys.stderr = self._old_stdout -@cache_util.cached +@cache_utils.cache def requires_attr(obj, attr): - """ + """Decorator used to skip tests if an object does not have the required + attribute. """ try: @@ -137,7 +143,7 @@ def requires_attr(obj, attr): attr, obj)) -@cache_util.cached +@cache_utils.cache def requires_module(fullname): """Decorator used to skip tests if a module is not imported. """ @@ -148,7 +154,7 @@ def requires_module(fullname): return unittest.skip('Unable to import "{}"'.format(fullname)) -@cache_util.cached +@cache_utils.cache def requires_platform(name): """Decorator used to skip tests if not running on the given platform. """ @@ -157,4 +163,20 @@ def requires_platform(name): return lambda func: func return unittest.skip( - 'Required platform "{}"" does not match system'.format(name)) + 'Required platform "{}" does not match system'.format(name)) + + +@cache_utils.cache +def requires_python(version): + """Decorator used to skip tests if the running Python version is not + greater or equal to the required version. + """ + + if isinstance(version, six.string_types): + version = Version(version) + + if _PYTHON_VERSION >= version: + return lambda func: func + + return unittest.skip( + 'Requires Python version {} or greater'.format(version)) diff --git a/utils/swift_build_support/swift_build_support/cache_util.py b/utils/swift_build_support/swift_build_support/cache_util.py deleted file mode 100644 index 493b558ea9084..0000000000000 --- a/utils/swift_build_support/swift_build_support/cache_util.py +++ /dev/null @@ -1,58 +0,0 @@ -# swift_build_support/cache_util.py -----------------------------*- python -*- -# -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See https://swift.org/LICENSE.txt for license information -# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -# -# ---------------------------------------------------------------------------- -""" -Cache related utilities -""" -# ---------------------------------------------------------------------------- - -from functools import update_wrapper - -__all__ = [ - 'cached', - 'reify' -] - - -def cached(func): - """Decorator that caches result of method or function. - - Note: Support method or function. - """ - cache = {} - - def wrapper(*args, **kwargs): - key = tuple(args) + tuple(kwargs.items()) - if key not in cache: - result = func(*args, **kwargs) - cache[key] = result - return result - else: - return cache[key] - - return update_wrapper(wrapper, func) - - -def reify(func): - """Decorator that replaces the wrapped method with the result after the - first call. - - Note: Support method that takes no arguments. - """ - class Wrapper(object): - def __get__(self, obj, objtype=None): - if obj is None: - return self - result = func(obj) - setattr(obj, func.__name__, result) - return result - - return update_wrapper(Wrapper(), func) diff --git a/utils/swift_build_support/swift_build_support/products/ninja.py b/utils/swift_build_support/swift_build_support/products/ninja.py index 0ffd054ccafc4..53ebb15c6e32e 100644 --- a/utils/swift_build_support/swift_build_support/products/ninja.py +++ b/utils/swift_build_support/swift_build_support/products/ninja.py @@ -18,10 +18,10 @@ import platform import sys +from build_swift.build_swift import cache_utils from build_swift.build_swift.wrappers import xcrun from . import product -from .. import cache_util from .. import shell @@ -44,7 +44,7 @@ def __init__(self, product_class, args, toolchain, workspace): self.args = args self.toolchain = toolchain - @cache_util.reify + @cache_utils.reify def ninja_bin_path(self): return os.path.join(self.build_dir, 'ninja') diff --git a/utils/swift_build_support/swift_build_support/toolchain.py b/utils/swift_build_support/swift_build_support/toolchain.py index 1395397a80332..b7c8247374e2a 100644 --- a/utils/swift_build_support/swift_build_support/toolchain.py +++ b/utils/swift_build_support/swift_build_support/toolchain.py @@ -18,10 +18,10 @@ import platform +from build_swift.build_swift import cache_utils from build_swift.build_swift.shell import which from build_swift.build_swift.wrappers import xcrun -from . import cache_util from . import shell @@ -44,7 +44,7 @@ def _register(name, *tool): def _getter(self): return self.find_tool(*tool) _getter.__name__ = name - setattr(Toolchain, name, cache_util.reify(_getter)) + setattr(Toolchain, name, cache_utils.reify(_getter)) if platform.system() == 'Windows': @@ -162,7 +162,7 @@ def __init__(self): suffixes = ['38', '37', '36', '35'] super(FreeBSD, self).__init__(suffixes) - @cache_util.reify + @cache_utils.reify def _release_date(self): """Return the release date for FreeBSD operating system on this host. If the release date cannot be ascertained, return None. diff --git a/utils/swift_build_support/tests/test_cache_util.py b/utils/swift_build_support/tests/test_cache_util.py deleted file mode 100644 index c65f308b48295..0000000000000 --- a/utils/swift_build_support/tests/test_cache_util.py +++ /dev/null @@ -1,99 +0,0 @@ -# tests/test_cache_util.py --------------------------------------*- python -*- -# -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See https://swift.org/LICENSE.txt for license information -# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -# -# ---------------------------------------------------------------------------- - -import unittest - -from swift_build_support import cache_util - - -my_func_called = 0 -my_kfunc_called = 0 - - -@cache_util.cached -def my_func(arg1, arg2): - global my_func_called - my_func_called += 1 - return "my_func_result(%s, %s)" % (arg1, arg2) - - -@cache_util.cached -def my_kfunc(arg1, arg2): - global my_kfunc_called - my_kfunc_called += 1 - return "my_kfunc_result(%s, %s)" % (arg1, arg2) - - -class MyClass(object): - def __init__(self, prop=None): - self.my_method_called = 0 - self.my_prop_called = 0 - self.prop_value = prop - - @cache_util.cached - def my_method(self, arg1, arg2): - self.my_method_called += 1 - return "my_meth_result(%s, %s)" % (arg1, arg2) - - @cache_util.reify - def my_prop(self): - self.my_prop_called += 1 - return "==%s==" % (self.prop_value) - - -class CacheUtilTestCase(unittest.TestCase): - def test_cached_func(self): - self.assertEqual(my_func("foo", 42), "my_func_result(foo, 42)") - self.assertEqual(my_func_called, 1) - self.assertEqual(my_func("foo", 42), "my_func_result(foo, 42)") - self.assertEqual(my_func_called, 1) - self.assertEqual(my_func("bar", 42), "my_func_result(bar, 42)") - self.assertEqual(my_func_called, 2) - self.assertEqual(my_func("foo", 42), "my_func_result(foo, 42)") - self.assertEqual(my_func_called, 2) - - def test_cached_kwfunc(self): - self.assertEqual(my_kfunc("foo", arg2=42), "my_kfunc_result(foo, 42)") - self.assertEqual(my_kfunc_called, 1) - self.assertEqual(my_kfunc("foo", arg2=42), "my_kfunc_result(foo, 42)") - self.assertEqual(my_kfunc_called, 1) - self.assertEqual(my_kfunc("bar", arg2=42), "my_kfunc_result(bar, 42)") - self.assertEqual(my_kfunc_called, 2) - self.assertEqual(my_kfunc("foo", arg2=42), "my_kfunc_result(foo, 42)") - self.assertEqual(my_kfunc_called, 2) - - def test_cached_method(self): - obj1 = MyClass() - self.assertEqual(obj1.my_method("foo", 42), "my_meth_result(foo, 42)") - self.assertEqual(obj1.my_method_called, 1) - self.assertEqual(obj1.my_method("foo", 42), "my_meth_result(foo, 42)") - self.assertEqual(obj1.my_method_called, 1) - self.assertEqual(obj1.my_method("bar", 12), "my_meth_result(bar, 12)") - self.assertEqual(obj1.my_method_called, 2) - - # Test for instance independency. - obj2 = MyClass() - self.assertEqual(obj2.my_method("foo", 42), "my_meth_result(foo, 42)") - self.assertEqual(obj2.my_method_called, 1) - self.assertEqual(obj1.my_method_called, 2) - - def test_reify(self): - obj1 = MyClass(prop='foo') - self.assertEqual(obj1.my_prop, '==foo==') - self.assertEqual(obj1.my_prop_called, 1) - self.assertEqual(obj1.my_prop, '==foo==') - self.assertEqual(obj1.my_prop_called, 1) - - # Test for instance independency. - obj2 = MyClass(prop='bar') - self.assertEqual(obj2.my_prop, '==bar==') - self.assertEqual(obj1.my_prop, '==foo==')