From 76f1e272768ad1d83bcecc169f8b01699d7042c8 Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Fri, 30 Mar 2018 17:40:26 +0200 Subject: [PATCH] Merging upstream version 1.4.0. --- AUTHORS.rst | 3 +- CONTRIBUTING.rst | 2 +- HISTORY.rst | 7 ++ PKG-INFO | 63 +++++++++-- README.rst | 51 ++++++++- cached_property.egg-info/PKG-INFO | 63 +++++++++-- cached_property.egg-info/SOURCES.txt | 4 +- cached_property.py | 16 ++- setup.py | 4 +- tests/test_async_cached_property.py | 135 ++++++++++++++++++++++++ tests/test_coroutine_cached_property.py | 127 ++++++++++++++++++++++ 11 files changed, 453 insertions(+), 22 deletions(-) create mode 100644 tests/test_async_cached_property.py create mode 100644 tests/test_coroutine_cached_property.py diff --git a/AUTHORS.rst b/AUTHORS.rst index 51d714e..4e5f411 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,7 +5,7 @@ Credits Development Lead ---------------- -* Daniel Roy Greenfeld +* Daniel Roy Greenfeld (@pydanny) * Audrey Roy Greenfeld (@audreyr) Contributors @@ -17,3 +17,4 @@ Contributors * Adam Williamson * Ionel Cristian Mărieș (@ionelmc) * Malyshev Artem (@proofit404) +* Volker Braun (@vbraun) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 971e19a..ba7f9b6 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -99,7 +99,7 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check +3. The pull request should work for Python 2.7, and 3.3, 3.4, 3.5, 3.6 and for PyPy. Check https://travis-ci.org/pydanny/cached-property/pull_requests and make sure that the tests pass for all supported Python versions. diff --git a/HISTORY.rst b/HISTORY.rst index f27cab8..51a65b2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ History ------- +1.4.0 (2018-02-25) +++++++++++++++++++ + +* Added asyncio support, thanks to @vbraun +* Remove Python 2.6 support, whose end of life was 5 years ago, thanks to @pydanny + + 1.3.1 (2017-09-21) ++++++++++++++++++ diff --git a/PKG-INFO b/PKG-INFO index f9e7abe..d03e115 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,11 +1,12 @@ Metadata-Version: 1.1 Name: cached-property -Version: 1.3.1 +Version: 1.4.0 Summary: A decorator for caching properties in classes. Home-page: https://github.com/pydanny/cached-property Author: Daniel Greenfeld Author-email: pydanny@gmail.com License: BSD +Description-Content-Type: UNKNOWN Description: =============================== cached-property =============================== @@ -153,6 +154,49 @@ Description: =============================== >>> self.assertEqual(m.boardwalk, 550) + Working with async/await (Python 3.5+) + -------------------------------------- + + The cached property can be async, in which case you have to use await + as usual to get the value. Because of the caching, the value is only + computed once and then cached: + + .. code-block:: python + + from cached_property import cached_property + + class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @cached_property + async def boardwalk(self): + self.boardwalk_price += 50 + return self.boardwalk_price + + Now use it: + + .. code-block:: python + + >>> async def print_boardwalk(): + ... monopoly = Monopoly() + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + >>> import asyncio + >>> asyncio.get_event_loop().run_until_complete(print_boardwalk()) + 550 + 550 + 550 + + Note that this does not work with threading either, most asyncio + objects are not thread-safe. And if you run separate event loops in + each thread, the cached version will most likely have the wrong event + loop. To summarize, either use cooperative multitasking (event loop) + or threading, but not both at the same time. + + Timing out the cache -------------------- @@ -207,11 +251,11 @@ Description: =============================== This project is maintained by volunteers. Support their efforts by spreading the word about: - .. image:: https://s3.amazonaws.com/tsacademy/images/tsa-logo-250x60-transparent-01.png - :name: Two Scoops Academy + .. image:: https://cdn.shopify.com/s/files/1/0304/6901/t/2/assets/logo.png?8399580890922549623 + :name: Two Scoops Press :align: center - :alt: Two Scoops Academy - :target: http://www.twoscoops.academy/ + :alt: Two Scoops Press + :target: https://www.twoscoopspress.com @@ -219,6 +263,13 @@ Description: =============================== History ------- + 1.4.0 (2018-02-25) + ++++++++++++++++++ + + * Added asyncio support, thanks to @vbraun + * Remove Python 2.6 support, whose end of life was 5 years ago, thanks to @pydanny + + 1.3.1 (2017-09-21) ++++++++++++++++++ @@ -294,9 +345,9 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 diff --git a/README.rst b/README.rst index 0d428f5..27bb40a 100644 --- a/README.rst +++ b/README.rst @@ -145,6 +145,49 @@ Now use it: >>> self.assertEqual(m.boardwalk, 550) +Working with async/await (Python 3.5+) +-------------------------------------- + +The cached property can be async, in which case you have to use await +as usual to get the value. Because of the caching, the value is only +computed once and then cached: + +.. code-block:: python + + from cached_property import cached_property + + class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @cached_property + async def boardwalk(self): + self.boardwalk_price += 50 + return self.boardwalk_price + +Now use it: + +.. code-block:: python + + >>> async def print_boardwalk(): + ... monopoly = Monopoly() + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + >>> import asyncio + >>> asyncio.get_event_loop().run_until_complete(print_boardwalk()) + 550 + 550 + 550 + +Note that this does not work with threading either, most asyncio +objects are not thread-safe. And if you run separate event loops in +each thread, the cached version will most likely have the wrong event +loop. To summarize, either use cooperative multitasking (event loop) +or threading, but not both at the same time. + + Timing out the cache -------------------- @@ -199,8 +242,8 @@ Support This Project This project is maintained by volunteers. Support their efforts by spreading the word about: -.. image:: https://s3.amazonaws.com/tsacademy/images/tsa-logo-250x60-transparent-01.png - :name: Two Scoops Academy +.. image:: https://cdn.shopify.com/s/files/1/0304/6901/t/2/assets/logo.png?8399580890922549623 + :name: Two Scoops Press :align: center - :alt: Two Scoops Academy - :target: http://www.twoscoops.academy/ + :alt: Two Scoops Press + :target: https://www.twoscoopspress.com diff --git a/cached_property.egg-info/PKG-INFO b/cached_property.egg-info/PKG-INFO index f9e7abe..d03e115 100644 --- a/cached_property.egg-info/PKG-INFO +++ b/cached_property.egg-info/PKG-INFO @@ -1,11 +1,12 @@ Metadata-Version: 1.1 Name: cached-property -Version: 1.3.1 +Version: 1.4.0 Summary: A decorator for caching properties in classes. Home-page: https://github.com/pydanny/cached-property Author: Daniel Greenfeld Author-email: pydanny@gmail.com License: BSD +Description-Content-Type: UNKNOWN Description: =============================== cached-property =============================== @@ -153,6 +154,49 @@ Description: =============================== >>> self.assertEqual(m.boardwalk, 550) + Working with async/await (Python 3.5+) + -------------------------------------- + + The cached property can be async, in which case you have to use await + as usual to get the value. Because of the caching, the value is only + computed once and then cached: + + .. code-block:: python + + from cached_property import cached_property + + class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @cached_property + async def boardwalk(self): + self.boardwalk_price += 50 + return self.boardwalk_price + + Now use it: + + .. code-block:: python + + >>> async def print_boardwalk(): + ... monopoly = Monopoly() + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + ... print(await monopoly.boardwalk) + >>> import asyncio + >>> asyncio.get_event_loop().run_until_complete(print_boardwalk()) + 550 + 550 + 550 + + Note that this does not work with threading either, most asyncio + objects are not thread-safe. And if you run separate event loops in + each thread, the cached version will most likely have the wrong event + loop. To summarize, either use cooperative multitasking (event loop) + or threading, but not both at the same time. + + Timing out the cache -------------------- @@ -207,11 +251,11 @@ Description: =============================== This project is maintained by volunteers. Support their efforts by spreading the word about: - .. image:: https://s3.amazonaws.com/tsacademy/images/tsa-logo-250x60-transparent-01.png - :name: Two Scoops Academy + .. image:: https://cdn.shopify.com/s/files/1/0304/6901/t/2/assets/logo.png?8399580890922549623 + :name: Two Scoops Press :align: center - :alt: Two Scoops Academy - :target: http://www.twoscoops.academy/ + :alt: Two Scoops Press + :target: https://www.twoscoopspress.com @@ -219,6 +263,13 @@ Description: =============================== History ------- + 1.4.0 (2018-02-25) + ++++++++++++++++++ + + * Added asyncio support, thanks to @vbraun + * Remove Python 2.6 support, whose end of life was 5 years ago, thanks to @pydanny + + 1.3.1 (2017-09-21) ++++++++++++++++++ @@ -294,9 +345,9 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 diff --git a/cached_property.egg-info/SOURCES.txt b/cached_property.egg-info/SOURCES.txt index d808466..b71dd2d 100644 --- a/cached_property.egg-info/SOURCES.txt +++ b/cached_property.egg-info/SOURCES.txt @@ -13,4 +13,6 @@ cached_property.egg-info/dependency_links.txt cached_property.egg-info/not-zip-safe cached_property.egg-info/top_level.txt tests/__init__.py -tests/test_cached_property.py \ No newline at end of file +tests/test_async_cached_property.py +tests/test_cached_property.py +tests/test_coroutine_cached_property.py \ No newline at end of file diff --git a/cached_property.py b/cached_property.py index 7e8bebe..53edd60 100644 --- a/cached_property.py +++ b/cached_property.py @@ -2,11 +2,15 @@ __author__ = 'Daniel Greenfeld' __email__ = 'pydanny@gmail.com' -__version__ = '1.3.1' +__version__ = '1.4.0' __license__ = 'BSD' from time import time import threading +try: + import asyncio +except ImportError: + asyncio = None class cached_property(object): @@ -23,9 +27,19 @@ class cached_property(object): def __get__(self, obj, cls): if obj is None: return self + if asyncio and asyncio.iscoroutinefunction(self.func): + return self._wrap_in_coroutine(obj) value = obj.__dict__[self.func.__name__] = self.func(obj) return value + def _wrap_in_coroutine(self, obj): + @asyncio.coroutine + def wrapper(): + future = asyncio.ensure_future(self.func(obj)) + obj.__dict__[self.func.__name__] = future + return future + return wrapper() + class threaded_cached_property(object): """ diff --git a/setup.py b/setup.py index 2b670b8..a94df4f 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ try: except ImportError: from distutils.core import setup -__version__ = '1.3.1' +__version__ = '1.4.0' def read(fname): @@ -45,11 +45,11 @@ setup( 'License :: OSI Approved :: BSD License', 'Natural Language :: English', "Programming Language :: Python :: 2", - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], ) diff --git a/tests/test_async_cached_property.py b/tests/test_async_cached_property.py new file mode 100644 index 0000000..36892be --- /dev/null +++ b/tests/test_async_cached_property.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +import time +import unittest +import asyncio +from threading import Lock, Thread +from freezegun import freeze_time + +import cached_property + + +def unittest_run_loop(f): + def wrapper(*args, **kwargs): + coro = asyncio.coroutine(f) + future = coro(*args, **kwargs) + loop = asyncio.get_event_loop() + loop.run_until_complete(future) + return wrapper + + +def CheckFactory(cached_property_decorator, threadsafe=False): + """ + Create dynamically a Check class whose add_cached method is decorated by + the cached_property_decorator. + """ + + class Check(object): + + def __init__(self): + self.control_total = 0 + self.cached_total = 0 + self.lock = Lock() + + async def add_control(self): + self.control_total += 1 + return self.control_total + + @cached_property_decorator + async def add_cached(self): + if threadsafe: + time.sleep(1) + # Need to guard this since += isn't atomic. + with self.lock: + self.cached_total += 1 + else: + self.cached_total += 1 + + return self.cached_total + + def run_threads(self, num_threads): + threads = [] + for _ in range(num_threads): + def call_add_cached(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(self.add_cached) + thread = Thread(target=call_add_cached) + thread.start() + threads.append(thread) + for thread in threads: + thread.join() + + return Check + + +class TestCachedProperty(unittest.TestCase): + """Tests for cached_property""" + + cached_property_factory = cached_property.cached_property + + async def assert_control(self, check, expected): + """ + Assert that both `add_control` and 'control_total` equal `expected` + """ + self.assertEqual(await check.add_control(), expected) + self.assertEqual(check.control_total, expected) + + async def assert_cached(self, check, expected): + """ + Assert that both `add_cached` and 'cached_total` equal `expected` + """ + print('assert_cached', check.add_cached) + self.assertEqual(await check.add_cached, expected) + self.assertEqual(check.cached_total, expected) + + @unittest_run_loop + async def test_cached_property(self): + Check = CheckFactory(self.cached_property_factory) + check = Check() + + # The control shows that we can continue to add 1 + await self.assert_control(check, 1) + await self.assert_control(check, 2) + + # The cached version demonstrates how nothing is added after the first + await self.assert_cached(check, 1) + await self.assert_cached(check, 1) + + # The cache does not expire + with freeze_time("9999-01-01"): + await self.assert_cached(check, 1) + + # Typically descriptors return themselves if accessed though the class + # rather than through an instance. + self.assertTrue(isinstance(Check.add_cached, + self.cached_property_factory)) + + @unittest_run_loop + async def test_reset_cached_property(self): + Check = CheckFactory(self.cached_property_factory) + check = Check() + + # Run standard cache assertion + await self.assert_cached(check, 1) + await self.assert_cached(check, 1) + + # Clear the cache + del check.add_cached + + # Value is cached again after the next access + await self.assert_cached(check, 2) + await self.assert_cached(check, 2) + + @unittest_run_loop + async def test_none_cached_property(self): + class Check(object): + + def __init__(self): + self.cached_total = None + + @self.cached_property_factory + async def add_cached(self): + return self.cached_total + + await self.assert_cached(Check(), None) diff --git a/tests/test_coroutine_cached_property.py b/tests/test_coroutine_cached_property.py new file mode 100644 index 0000000..ede9baf --- /dev/null +++ b/tests/test_coroutine_cached_property.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +""" +The same tests as in :mod:`.test_async_cached_property`, but with the old +yield from instead of the new async/await syntax. Used to test Python 3.4 +compatibility which has asyncio but doesn't have async/await yet. +""" + +import unittest +import asyncio +from freezegun import freeze_time + +import cached_property + + +def unittest_run_loop(f): + def wrapper(*args, **kwargs): + coro = asyncio.coroutine(f) + future = coro(*args, **kwargs) + loop = asyncio.get_event_loop() + loop.run_until_complete(future) + return wrapper + + +def CheckFactory(cached_property_decorator): + """ + Create dynamically a Check class whose add_cached method is decorated by + the cached_property_decorator. + """ + + class Check(object): + + def __init__(self): + self.control_total = 0 + self.cached_total = 0 + + @asyncio.coroutine + def add_control(self): + self.control_total += 1 + return self.control_total + + @cached_property_decorator + @asyncio.coroutine + def add_cached(self): + self.cached_total += 1 + return self.cached_total + + return Check + + +class TestCachedProperty(unittest.TestCase): + """Tests for cached_property""" + + cached_property_factory = cached_property.cached_property + + @asyncio.coroutine + def assert_control(self, check, expected): + """ + Assert that both `add_control` and 'control_total` equal `expected` + """ + value = yield from check.add_control() + self.assertEqual(value, expected) + self.assertEqual(check.control_total, expected) + + @asyncio.coroutine + def assert_cached(self, check, expected): + """ + Assert that both `add_cached` and 'cached_total` equal `expected` + """ + print('assert_cached', check.add_cached) + value = yield from check.add_cached + self.assertEqual(value, expected) + self.assertEqual(check.cached_total, expected) + + @unittest_run_loop + @asyncio.coroutine + def test_cached_property(self): + Check = CheckFactory(self.cached_property_factory) + check = Check() + + # The control shows that we can continue to add 1 + yield from self.assert_control(check, 1) + yield from self.assert_control(check, 2) + + # The cached version demonstrates how nothing is added after the first + yield from self.assert_cached(check, 1) + yield from self.assert_cached(check, 1) + + # The cache does not expire + with freeze_time("9999-01-01"): + yield from self.assert_cached(check, 1) + + # Typically descriptors return themselves if accessed though the class + # rather than through an instance. + self.assertTrue(isinstance(Check.add_cached, + self.cached_property_factory)) + + @unittest_run_loop + @asyncio.coroutine + def test_reset_cached_property(self): + Check = CheckFactory(self.cached_property_factory) + check = Check() + + # Run standard cache assertion + yield from self.assert_cached(check, 1) + yield from self.assert_cached(check, 1) + + # Clear the cache + del check.add_cached + + # Value is cached again after the next access + yield from self.assert_cached(check, 2) + yield from self.assert_cached(check, 2) + + @unittest_run_loop + @asyncio.coroutine + def test_none_cached_property(self): + class Check(object): + + def __init__(self): + self.cached_total = None + + @self.cached_property_factory + @asyncio.coroutine + def add_cached(self): + return self.cached_total + + yield from self.assert_cached(Check(), None)