Merging upstream version 1.4.0.

This commit is contained in:
Mathias Behrle 2018-03-30 17:40:26 +02:00
parent 4a2939a58d
commit 76f1e27276
11 changed files with 453 additions and 22 deletions

View File

@ -5,7 +5,7 @@ Credits
Development Lead
----------------
* Daniel Roy Greenfeld <pydanny@gmail.com>
* Daniel Roy Greenfeld (@pydanny)
* Audrey Roy Greenfeld (@audreyr)
Contributors
@ -17,3 +17,4 @@ Contributors
* Adam Williamson <awilliam AT redhat DOT com>
* Ionel Cristian Mărieș (@ionelmc)
* Malyshev Artem (@proofit404)
* Volker Braun (@vbraun)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
tests/test_async_cached_property.py
tests/test_cached_property.py
tests/test_coroutine_cached_property.py

View File

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

View File

@ -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',
],
)

View File

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

View File

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