Skip to content

Commit 8d20c1f

Browse files
authored
Merge pull request #29567 from Rostepher/cache-utils
[Build System: build-script] Adds a new cache_utils module to build_swift which replaces the existing module from swift_build_support.
2 parents c4bd50c + a7d6cd8 commit 8d20c1f

File tree

7 files changed

+224
-169
lines changed

7 files changed

+224
-169
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
10+
"""
11+
Cache related utitlity functions and decorators.
12+
"""
13+
14+
15+
from __future__ import absolute_import, unicode_literals
16+
17+
import functools
18+
19+
20+
__all__ = [
21+
'cache',
22+
'reify',
23+
]
24+
25+
26+
def cache(func):
27+
"""Decorator that caches result of a function call.
28+
29+
NOTE: This decorator does not play nice with methods as the created cache
30+
is not instance-local, rather it lives in the decorator.
31+
NOTE: When running in Python 3.2 or newer this decorator is replaced with
32+
the standard `functools.lru_cache` using a maxsize of None.
33+
"""
34+
35+
# Use the standard functools.lru_cache decorator for Python 3.2 and newer.
36+
if hasattr(functools, 'lru_cache'):
37+
return functools.lru_cache(maxsize=None)(func)
38+
39+
# Otherwise use a naive caching strategy.
40+
_cache = {}
41+
42+
@functools.wraps(func)
43+
def wrapper(*args, **kwargs):
44+
key = tuple(args) + tuple(kwargs.items())
45+
46+
if key not in _cache:
47+
result = func(*args, **kwargs)
48+
_cache[key] = result
49+
return result
50+
51+
return _cache[key]
52+
return wrapper
53+
54+
55+
def reify(func):
56+
"""Decorator that replaces the wrapped method with the result after the
57+
first call. Used to wrap property-like methods with no arguments.
58+
"""
59+
60+
class wrapper(object):
61+
def __get__(self, obj, type=None):
62+
if obj is None:
63+
return self
64+
65+
result = func(obj)
66+
setattr(obj, func.__name__, result)
67+
return result
68+
69+
return functools.update_wrapper(wrapper(), func)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
10+
from __future__ import absolute_import, unicode_literals
11+
12+
import unittest
13+
14+
from build_swift import cache_utils
15+
16+
from .. import utils
17+
18+
19+
try:
20+
# Python 3.3
21+
from unittest import mock
22+
except ImportError:
23+
mock = None
24+
25+
26+
class _CallCounter(object):
27+
"""Callable helper class used to count and return the number of times an
28+
instance has been called.
29+
"""
30+
31+
def __init__(self):
32+
self._counter = 0
33+
34+
def __call__(self, *args, **kwargs):
35+
count = self._counter
36+
self._counter += 1
37+
return count
38+
39+
40+
class TestCache(unittest.TestCase):
41+
"""Unit tests for the cache decorator in the cache_utils module.
42+
"""
43+
44+
@utils.requires_module('unittest.mock')
45+
@utils.requires_python('3.2') # functools.lru_cache
46+
def test_replaced_with_functools_lru_cache_python_3_2(self):
47+
with mock.patch('functools.lru_cache') as mock_lru_cache:
48+
@cache_utils.cache
49+
def func():
50+
return None
51+
52+
mock_lru_cache.assert_called()
53+
54+
def test_call_with_no_args(self):
55+
# Increments the counter once per unique call.
56+
counter = _CallCounter()
57+
58+
@cache_utils.cache
59+
def func(*args, **kwargs):
60+
return counter(*args, **kwargs)
61+
62+
self.assertEqual(func(), 0)
63+
self.assertEqual(func(), 0)
64+
65+
def test_call_with_args(self):
66+
# Increments the counter once per unique call.
67+
counter = _CallCounter()
68+
69+
@cache_utils.cache
70+
def func(*args, **kwargs):
71+
return counter(*args, **kwargs)
72+
73+
self.assertEqual(func(0), 0)
74+
self.assertEqual(func(0), 0)
75+
76+
self.assertEqual(func(1), 1)
77+
self.assertEqual(func(1), 1)
78+
79+
self.assertEqual(func(2), 2)
80+
self.assertEqual(func(2), 2)
81+
82+
def test_call_with_args_and_kwargs(self):
83+
# Increments the counter once per unique call.
84+
counter = _CallCounter()
85+
86+
@cache_utils.cache
87+
def func(*args, **kwargs):
88+
return counter(*args, **kwargs)
89+
90+
self.assertEqual(func(n=0), 0)
91+
self.assertEqual(func(n=0), 0)
92+
93+
self.assertEqual(func(a=1, b='b'), 1)
94+
self.assertEqual(func(a=1, b='b'), 1)
95+
96+
self.assertEqual(func(0, x=1, y=2.0), 2)
97+
self.assertEqual(func(0, x=1, y=2.0), 2)
98+
99+
100+
class TestReify(unittest.TestCase):
101+
"""Unit tests for the reify decorator in the cache_utils module.
102+
"""
103+
104+
def test_replaces_attr_after_first_call(self):
105+
class Counter(object):
106+
def __init__(self):
107+
self._counter = 0
108+
109+
@cache_utils.reify
110+
def count(self):
111+
count = self._counter
112+
self._counter += 1
113+
return count
114+
115+
counter = Counter()
116+
117+
self.assertEqual(counter.count, 0)
118+
self.assertEqual(counter.count, 0)
119+
120+
# Assert that the count property has been replaced with the constant.
121+
self.assertEqual(getattr(counter, 'count'), 0)

utils/build_swift/tests/utils.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
import sys
1616
import unittest
1717

18-
from six import StringIO
18+
from build_swift import cache_utils
19+
from build_swift.versions import Version
1920

20-
from swift_build_support.swift_build_support import cache_util
21+
import six
22+
from six import StringIO
2123

2224

2325
__all__ = [
@@ -27,6 +29,7 @@
2729
'requires_attr',
2830
'requires_module',
2931
'requires_platform',
32+
'requires_python',
3033

3134
'BUILD_SCRIPT_IMPL_PATH',
3235
'BUILD_SWIFT_PATH',
@@ -38,6 +41,8 @@
3841
# -----------------------------------------------------------------------------
3942
# Constants
4043

44+
_PYTHON_VERSION = Version(platform.python_version())
45+
4146
TESTS_PATH = os.path.abspath(os.path.dirname(__file__))
4247
BUILD_SWIFT_PATH = os.path.abspath(os.path.join(TESTS_PATH, os.pardir))
4348
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):
124129
sys.stderr = self._old_stdout
125130

126131

127-
@cache_util.cached
132+
@cache_utils.cache
128133
def requires_attr(obj, attr):
129-
"""
134+
"""Decorator used to skip tests if an object does not have the required
135+
attribute.
130136
"""
131137

132138
try:
@@ -137,7 +143,7 @@ def requires_attr(obj, attr):
137143
attr, obj))
138144

139145

140-
@cache_util.cached
146+
@cache_utils.cache
141147
def requires_module(fullname):
142148
"""Decorator used to skip tests if a module is not imported.
143149
"""
@@ -148,7 +154,7 @@ def requires_module(fullname):
148154
return unittest.skip('Unable to import "{}"'.format(fullname))
149155

150156

151-
@cache_util.cached
157+
@cache_utils.cache
152158
def requires_platform(name):
153159
"""Decorator used to skip tests if not running on the given platform.
154160
"""
@@ -157,4 +163,20 @@ def requires_platform(name):
157163
return lambda func: func
158164

159165
return unittest.skip(
160-
'Required platform "{}"" does not match system'.format(name))
166+
'Required platform "{}" does not match system'.format(name))
167+
168+
169+
@cache_utils.cache
170+
def requires_python(version):
171+
"""Decorator used to skip tests if the running Python version is not
172+
greater or equal to the required version.
173+
"""
174+
175+
if isinstance(version, six.string_types):
176+
version = Version(version)
177+
178+
if _PYTHON_VERSION >= version:
179+
return lambda func: func
180+
181+
return unittest.skip(
182+
'Requires Python version {} or greater'.format(version))

utils/swift_build_support/swift_build_support/cache_util.py

-58
This file was deleted.

utils/swift_build_support/swift_build_support/products/ninja.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
import platform
1919
import sys
2020

21+
from build_swift.build_swift import cache_utils
2122
from build_swift.build_swift.wrappers import xcrun
2223

2324
from . import product
24-
from .. import cache_util
2525
from .. import shell
2626

2727

@@ -44,7 +44,7 @@ def __init__(self, product_class, args, toolchain, workspace):
4444
self.args = args
4545
self.toolchain = toolchain
4646

47-
@cache_util.reify
47+
@cache_utils.reify
4848
def ninja_bin_path(self):
4949
return os.path.join(self.build_dir, 'ninja')
5050

utils/swift_build_support/swift_build_support/toolchain.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import platform
2020

21+
from build_swift.build_swift import cache_utils
2122
from build_swift.build_swift.shell import which
2223
from build_swift.build_swift.wrappers import xcrun
2324

24-
from . import cache_util
2525
from . import shell
2626

2727

@@ -44,7 +44,7 @@ def _register(name, *tool):
4444
def _getter(self):
4545
return self.find_tool(*tool)
4646
_getter.__name__ = name
47-
setattr(Toolchain, name, cache_util.reify(_getter))
47+
setattr(Toolchain, name, cache_utils.reify(_getter))
4848

4949

5050
if platform.system() == 'Windows':
@@ -162,7 +162,7 @@ def __init__(self):
162162
suffixes = ['38', '37', '36', '35']
163163
super(FreeBSD, self).__init__(suffixes)
164164

165-
@cache_util.reify
165+
@cache_utils.reify
166166
def _release_date(self):
167167
"""Return the release date for FreeBSD operating system on this host.
168168
If the release date cannot be ascertained, return None.

0 commit comments

Comments
 (0)