install: pip install -r requirements.txt
# command to run tests, e.g. python setup.py test
-script: py.test
+script: pytest tests/
Now you can make your changes locally.
-5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox::
+
+5. Clean up the formatting (must be running at least Python 3.6)::
+
+ $ pip install -U black
+ $ black .
+
+6. When you're done making changes, check that your changes pass the tests, including testing other Python versions with tox::
- $ flake8 cached-property tests
- $ python setup.py test
+ $ pytest tests/
$ tox
- To get flake8 and tox, just pip install them into your virtualenv.
+ To get tox, just pip install it into your virtualenv.
-6. Commit your changes and push your branch to GitHub::
+7. Commit your changes and push your branch to GitHub::
$ git add .
$ git commit -m "Your detailed description of your changes."
$ git push origin name-of-your-bugfix-or-feature
-7. Submit a pull request through the GitHub website.
+8. Submit a pull request through the GitHub website.
Pull Request Guidelines
-----------------------
History
-------
-1.4.1 (2018-02-26)
+1.4.2 (2018-03-08)
+++++++++++++++++++
+
+* Really fixed tests, thanks to @pydanny
+
+1.4.1 (2018-03-08)
++++++++++++++++++
* Added conftest.py to manifest so tests work properly off the tarball, thanks to @dotlambda
+* Ensured new asyncio tests didn't break Python 2.7 builds on Debian, thanks to @pydanny
+* Code formatting via black, thanks to @pydanny and @ambv
+
1.4.0 (2018-02-25)
++++++++++++++++++
.. image:: https://img.shields.io/travis/pydanny/cached-property/master.svg
:target: https://travis-ci.org/pydanny/cached-property
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+ :alt: Code style: black
A decorator for caching properties in classes.
# -*- coding: utf-8 -*-
-__author__ = 'Daniel Greenfeld'
-__email__ = 'pydanny@gmail.com'
-__version__ = '1.4.0'
-__license__ = 'BSD'
+__author__ = "Daniel Greenfeld"
+__email__ = "pydanny@gmail.com"
+__version__ = "1.4.2"
+__license__ = "BSD"
from time import time
import threading
+
try:
import asyncio
except ImportError:
""" # noqa
def __init__(self, func):
- self.__doc__ = getattr(func, '__doc__')
+ self.__doc__ = getattr(func, "__doc__")
self.func = func
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()
"""
def __init__(self, func):
- self.__doc__ = getattr(func, '__doc__')
+ self.__doc__ = getattr(func, "__doc__")
self.func = func
self.lock = threading.RLock()
try:
# check if the value was computed before the lock was acquired
return obj_dict[name]
+
except KeyError:
# if not, do the calculation and release the lock
return obj_dict.setdefault(name, self.func(obj))
self.__name__ = func.__name__
self.__module__ = func.__module__
+
# Aliases to make cached_property_with_ttl easier to use
cached_property_ttl = cached_property_with_ttl
timed_cached_property = cached_property_with_ttl
def __get__(self, obj, cls):
with self.lock:
- return super(threaded_cached_property_with_ttl, self).__get__(obj,
- cls)
+ return super(threaded_cached_property_with_ttl, self).__get__(obj, cls)
+
# Alias to make threaded_cached_property_with_ttl easier to use
threaded_cached_property_ttl = threaded_cached_property_with_ttl
import sys
# Whether "import asyncio" works
-has_asyncio = (
- sys.version_info[0] == 3 and sys.version_info[1] >= 4
-)
+has_asyncio = (sys.version_info[0] == 3 and sys.version_info[1] >= 4)
# Whether the async and await keywords work
-has_async_await = (
- sys.version_info[0] == 3 and sys.version_info[1] >= 5
-)
+has_async_await = (sys.version_info[0] == 3 and sys.version_info[1] >= 5)
-print('conftest.py', has_asyncio, has_async_await)
+print("conftest.py", has_asyncio, has_async_await)
collect_ignore = []
if not has_asyncio:
- collect_ignore.append('tests/test_coroutine_cached_property.py')
+ collect_ignore.append("tests/test_coroutine_cached_property.py")
if not has_async_await:
- collect_ignore.append('tests/test_async_cached_property.py')
+ collect_ignore.append("tests/test_async_cached_property.py")
pytest==3.5.0
pytest-cov==2.5.1
freezegun==0.3.10
+twine==1.11.0
wheel==0.31.0
except ImportError:
from distutils.core import setup
-__version__ = '1.4.0'
+__version__ = "1.4.2"
def read(fname):
return codecs.open(
- os.path.join(os.path.dirname(__file__), fname), 'r', 'utf-8').read()
+ os.path.join(os.path.dirname(__file__), fname), "r", "utf-8"
+ ).read()
-readme = read('README.rst')
-history = read('HISTORY.rst').replace('.. :changelog:', '')
-if sys.argv[-1] == 'publish':
- os.system('python setup.py sdist bdist_wheel upload')
+readme = read("README.rst")
+history = read("HISTORY.rst").replace(".. :changelog:", "")
+
+if sys.argv[-1] == "publish":
+ os.system("python setup.py sdist bdist_wheel")
+ os.system("twine upload dist/*")
os.system("git tag -a %s -m 'version %s'" % (__version__, __version__))
os.system("git push --tags")
sys.exit()
setup(
- name='cached-property',
+ name="cached-property",
version=__version__,
- description='A decorator for caching properties in classes.',
- long_description=readme + '\n\n' + history,
- author='Daniel Greenfeld',
- author_email='pydanny@gmail.com',
- url='https://github.com/pydanny/cached-property',
- py_modules=['cached_property'],
+ description="A decorator for caching properties in classes.",
+ long_description=readme + "\n\n" + history,
+ author="Daniel Greenfeld",
+ author_email="pydanny@gmail.com",
+ url="https://github.com/pydanny/cached-property",
+ py_modules=["cached_property"],
include_package_data=True,
license="BSD",
zip_safe=False,
- keywords='cached-property',
+ keywords="cached-property",
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: BSD License',
- 'Natural Language :: English',
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Natural Language :: English",
"Programming Language :: Python :: 2",
- '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',
+ "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",
],
)
# -*- coding: utf-8 -*-
+import asyncio
+import time
+import unittest
+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)
-try:
- import asyncio
- import time
- import unittest
- from threading import Lock, Thread
- from freezegun import freeze_time
- import cached_property
+ thread = Thread(target=call_add_cached)
+ thread.start()
+ threads.append(thread)
+ for thread in threads:
+ thread.join()
+ return Check
- 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
+class TestCachedProperty(unittest.TestCase):
+ """Tests for cached_property"""
+ cached_property_factory = cached_property.cached_property
- def CheckFactory(cached_property_decorator, threadsafe=False):
+ async def assert_control(self, check, expected):
"""
- Create dynamically a Check class whose add_cached method is decorated by
- the cached_property_decorator.
+ Assert that both `add_control` and 'control_total` equal `expected`
"""
+ self.assertEqual(await check.add_control(), expected)
+ self.assertEqual(check.control_total, expected)
- class Check(object):
+ 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)
- def __init__(self):
- self.control_total = 0
- self.cached_total = 0
- self.lock = Lock()
+ @unittest_run_loop
+ async def test_cached_property(self):
+ Check = CheckFactory(self.cached_property_factory)
+ check = Check()
- async def add_control(self):
- self.control_total += 1
- return self.control_total
+ # The control shows that we can continue to add 1
+ await self.assert_control(check, 1)
+ await self.assert_control(check, 2)
- @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
+ # The cached version demonstrates how nothing is added after the first
+ await self.assert_cached(check, 1)
+ await self.assert_cached(check, 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)
+ # The cache does not expire
+ with freeze_time("9999-01-01"):
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))
+ # 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()
+ @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)
+ # Run standard cache assertion
+ await self.assert_cached(check, 1)
+ await self.assert_cached(check, 1)
- # Clear the cache
- del check.add_cached
+ # 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)
+ # 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):
+ @unittest_run_loop
+ async def test_none_cached_property(self):
- def __init__(self):
- self.cached_total = None
+ class Check(object):
- @self.cached_property_factory
- async def add_cached(self):
- return self.cached_total
+ def __init__(self):
+ self.cached_total = None
- await self.assert_cached(Check(), None)
+ @self.cached_property_factory
+ async def add_cached(self):
+ return self.cached_total
-
-except ImportError:
- pass # Running older version of Python that doesn't support asyncio
-
+ await self.assert_cached(Check(), None)
\ No newline at end of file
# Typically descriptors return themselves if accessed though the class
# rather than through an instance.
- self.assertTrue(isinstance(Check.add_cached,
- self.cached_property_factory))
+ self.assertTrue(isinstance(Check.add_cached, self.cached_property_factory))
def test_reset_cached_property(self):
Check = CheckFactory(self.cached_property_factory)
self.assert_cached(check, 2)
def test_none_cached_property(self):
+
class Check(object):
def __init__(self):
def test_set_cached_property(self):
Check = CheckFactory(self.cached_property_factory)
check = Check()
- check.add_cached = 'foo'
- self.assertEqual(check.add_cached, 'foo')
+ check.add_cached = "foo"
+ self.assertEqual(check.add_cached, "foo")
self.assertEqual(check.cached_total, 0)
def test_threads(self):
self.assert_cached(check, 2)
def test_threads_ttl_expiry(self):
- Check = CheckFactory(self.cached_property_factory(ttl=100000),
- threadsafe=True)
+ Check = CheckFactory(self.cached_property_factory(ttl=100000), threadsafe=True)
check = Check()
num_threads = 5
self.assert_cached(check, 2 * num_threads)
-class TestThreadedCachedPropertyWithTTL(TestThreadedCachedProperty,
- TestCachedPropertyWithTTL):
+class TestThreadedCachedPropertyWithTTL(
+ TestThreadedCachedProperty, TestCachedPropertyWithTTL
+):
"""Tests for threaded_cached_property_with_ttl"""
cached_property_factory = cached_property.threaded_cached_property_with_ttl
def test_threads_ttl_expiry(self):
- Check = CheckFactory(self.cached_property_factory(ttl=100000),
- threadsafe=True)
+ Check = CheckFactory(self.cached_property_factory(ttl=100000), threadsafe=True)
check = Check()
num_threads = 5
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
"""
Assert that both `add_cached` and 'cached_total` equal `expected`
"""
- print('assert_cached', check.add_cached)
+ print("assert_cached", check.add_cached)
value = yield from check.add_cached
self.assertEqual(value, expected)
self.assertEqual(check.cached_total, expected)
# Typically descriptors return themselves if accessed though the class
# rather than through an instance.
- self.assertTrue(isinstance(Check.add_cached,
- self.cached_property_factory))
+ self.assertTrue(isinstance(Check.add_cached, self.cached_property_factory))
@unittest_run_loop
@asyncio.coroutine
@unittest_run_loop
@asyncio.coroutine
def test_none_cached_property(self):
+
class Check(object):
def __init__(self):
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/cached-property
-commands = py.test
+commands = pytest tests/
deps =
pytest
freezegun