[Build System: build-script] Adds a new cache_utils module to build_swift which replaces the existing module from swift_build_support.

This commit is contained in:
Ross Bayer
2020-01-30 23:05:29 -08:00
parent a4dbe676a9
commit a7d6cd8126
7 changed files with 224 additions and 169 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -15,9 +15,11 @@ import platform
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 @@ __all__ = [
'requires_attr',
'requires_module',
'requires_platform',
'requires_python',
'BUILD_SCRIPT_IMPL_PATH',
'BUILD_SWIFT_PATH',
@@ -38,6 +41,8 @@ __all__ = [
# -----------------------------------------------------------------------------
# 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 @@ class redirect_stdout():
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))

View File

@@ -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)

View File

@@ -18,10 +18,10 @@ import os.path
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 @@ class NinjaBuilder(product.ProductBuilder):
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')

View File

@@ -18,10 +18,10 @@ from __future__ import absolute_import
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 @@ class FreeBSD(GenericUnix):
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.

View File

@@ -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==')