From 4056dca17cfc1bbe18f43819ece998cedb994a28 Mon Sep 17 00:00:00 2001 From: Daniel Feldroy Date: Mon, 21 Sep 2020 14:28:04 -0700 Subject: [PATCH] Merge in work by @secrus --- HISTORY.rst => HISTORY.md | 70 ++++------- README.md | 231 ++++++++++++++++++++++++++++++++++ README.rst | 259 -------------------------------------- 3 files changed, 254 insertions(+), 306 deletions(-) rename HISTORY.rst => HISTORY.md (66%) create mode 100644 README.md delete mode 100644 README.rst diff --git a/HISTORY.rst b/HISTORY.md similarity index 66% rename from HISTORY.rst rename to HISTORY.md index c481044..2d6a4f4 100644 --- a/HISTORY.rst +++ b/HISTORY.md @@ -1,61 +1,46 @@ -.. :changelog: +# History -History -------- - -2.0 (2020-09-21) -++++++++++++++++++ +## 2.0 (2020-09-??) * Remove formal support for Python 2.7 * Convert RST to MD -1.5.2 (2020-09-21) -++++++++++++++++++ +## 1.5.2 (2020-09-21) * Add formal support for Python 3.8 * Remove formal support for Python 3.4 * Switch from Travis to GitHub actions * Made tests pass flake8 for Python 2.7 -1.5.1 (2018-08-05) -++++++++++++++++++ +## 1.5.1 (2018-08-05) * Added formal support for Python 3.7 * Removed formal support for Python 3.3 -1.4.3 (2018-06-14) -+++++++++++++++++++ +## 1.4.3 (2018-06-14) * Catch SyntaxError from asyncio import on older versions of Python, thanks to @asottile -1.4.2 (2018-04-08) -++++++++++++++++++ +## 1.4.2 (2018-04-08) * Really fixed tests, thanks to @pydanny -1.4.1 (2018-04-08) -++++++++++++++++++ +## 1.4.1 (2018-04-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) -++++++++++++++++++ +## 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) -++++++++++++++++++ +## 1.3.1 (2017-09-21) * Validate for Python 3.6 - -1.3.0 (2015-11-24) -++++++++++++++++++ +## 1.3.0 (2015-11-24) * Drop some non-ASCII characters from HISTORY.rst, thanks to @AdamWill * Added official support for Python 3.5, thanks to @pydanny and @audreyr @@ -63,55 +48,46 @@ History * Corrected invalidation cache documentation, thanks to @proofit404 * Updated to latest Travis-CI environment, thanks to @audreyr -1.2.0 (2015-04-28) -++++++++++++++++++ +## 1.2.0 (2015-04-28) * Overall code and test refactoring, thanks to @gsakkis * Allow the del statement for resetting cached properties with ttl instead of del obj._cache[attr], thanks to @gsakkis. * Uncovered a bug in PyPy, https://bitbucket.org/pypy/pypy/issue/2033/attributeerror-object-attribute-is-read, thanks to @gsakkis * Fixed threaded_cached_property_with_ttl to actually be thread-safe, thanks to @gsakkis -1.1.0 (2015-04-04) -++++++++++++++++++ +## 1.1.0 (2015-04-04) * Regression: As the cache was not always clearing, we've broken out the time to expire feature to its own set of specific tools, thanks to @pydanny * Fixed typo in README, thanks to @zoidbergwill -1.0.0 (2015-02-13) -++++++++++++++++++ +## 1.0.0 (2015-02-13) -* Added timed to expire feature to ``cached_property`` decorator. -* **Backwards incompatiblity**: Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk']`` in order to support the new TTL feature. +* Added timed to expire feature to `cached_property` decorator. +* **Backwards incompatiblity**: Changed `del monopoly.boardwalk` to `del monopoly['boardwalk']` in order to support the new TTL feature. -0.1.5 (2014-05-20) -++++++++++++++++++ +## 0.1.5 (2014-05-20) -* Added threading support with new ``threaded_cached_property`` decorator +* Added threading support with new `threaded_cached_property` decorator * Documented cache invalidation * Updated credits * Sourced the bottle implementation -0.1.4 (2014-05-17) -++++++++++++++++++ +## 0.1.4 (2014-05-17) * Fix the dang-blarged py_modules argument. -0.1.3 (2014-05-17) -++++++++++++++++++ +## 0.1.3 (2014-05-17) -* Removed import of package into ``setup.py`` +* Removed import of package into `setup.py` -0.1.2 (2014-05-17) -++++++++++++++++++ +## 0.1.2 (2014-05-17) * Documentation fixes. Not opening up a RTFD instance for this because it's so simple to use. -0.1.1 (2014-05-17) -++++++++++++++++++ +## 0.1.1 (2014-05-17) * setup.py fix. Whoops! -0.1.0 (2014-05-17) -++++++++++++++++++ +## 0.1.0 (2014-05-17) * First release on PyPI. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d701c89 --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +# cached-property + +[![Github Actions status](https://github.com/pydanny/cached-property/workflows/Python%20package/badge.svg)](https://github.com/pydanny/cached-property/actions) +[![PyPI](https://img.shields.io/pypi/v/cached-property.svg)](https://pypi.python.org/pypi/cached-property) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +A decorator for caching properties in classes. + +## Why? + +* Makes caching of time or computational expensive properties quick and easy. +* Because I got tired of copy/pasting this code from non-web project to non-web project. +* I needed something really simple that worked in Python 2 and 3. + +## How to use it + +Let's define a class with an expensive property. Every time you stay there the +price goes up by $50! + +```python + class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @property + def boardwalk(self): + # In reality, this might represent a database call or time + # intensive task like calling a third-party API. + self.boardwalk_price += 50 + return self.boardwalk_price +``` + +Now run it: + +```python +>>> monopoly = Monopoly() +>>> monopoly.boardwalk +550 +>>> monopoly.boardwalk +600 +``` + +Let's convert the boardwalk property into a `cached_property`. + +```python + from cached_property import cached_property + + class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @cached_property + def boardwalk(self): + # Again, this is a silly example. Don't worry about it, this is + # just an example for clarity. + self.boardwalk_price += 50 + return self.boardwalk_price +``` + +Now when we run it the price stays at $550. + +```python +>>> monopoly = Monopoly() +>>> monopoly.boardwalk +550 +>>> monopoly.boardwalk +550 +>>> monopoly.boardwalk +550 +``` + +Why doesn't the value of `monopoly.boardwalk` change? Because it's a **cached property**! + +## Invalidating the Cache + +Results of cached functions can be invalidated by outside forces. Let's demonstrate how to force the cache to invalidate: + +```python +>>> monopoly = Monopoly() +>>> monopoly.boardwalk +550 +>>> monopoly.boardwalk +550 +>>> # invalidate the cache +>>> del monopoly.__dict__['boardwalk'] +>>> # request the boardwalk property again +>>> monopoly.boardwalk +600 +>>> monopoly.boardwalk +600 +``` + +## Working with Threads + +What if a whole bunch of people want to stay at Boardwalk all at once? This means using threads, which +unfortunately causes problems with the standard `cached_property`. In this case, switch to using the +`threaded_cached_property`: + +```python +from cached_property import threaded_cached_property + +class Monopoly(object): + + def __init__(self): + self.boardwalk_price = 500 + + @threaded_cached_property + def boardwalk(self): + """threaded_cached_property is really nice for when no one waits + for other people to finish their turn and rudely start rolling + dice and moving their pieces.""" + + sleep(1) + self.boardwalk_price += 50 + return self.boardwalk_price +``` + +Now use it: + +```python +>>> from threading import Thread +>>> from monopoly import Monopoly +>>> monopoly = Monopoly() +>>> threads = [] +>>> for x in range(10): +>>> thread = Thread(target=lambda: monopoly.boardwalk) +>>> thread.start() +>>> threads.append(thread) + +>>> for thread in threads: +>>> thread.join() + +>>> 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: + +```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: + +```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 + +Sometimes you want the price of things to reset after a time. Use the `ttl` +versions of `cached_property` and `threaded_cached_property`. + +```python +import random +from cached_property import cached_property_with_ttl + +class Monopoly(object): + + @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds + def dice(self): + # I dare the reader to implement a game using this method of 'rolling dice'. + return random.randint(2,12) +``` + +Now use it: + +```python +>>> monopoly = Monopoly() +>>> monopoly.dice +10 +>>> monopoly.dice +10 +>>> from time import sleep +>>> sleep(6) # Sleeps long enough to expire the cache +>>> monopoly.dice +3 +>>> monopoly.dice +3 +``` + +**Note:** The `ttl` tools do not reliably allow the clearing of the cache. This +is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. + +## Credits + +* Pip, Django, Werkzueg, Bottle, Pyramid, and Zope for having their own implementations. This package originally used an implementation that matched the Bottle version. +* Reinout Van Rees for pointing out the `cached_property` decorator to me. +* My awesome wife [@audreyfeldroy](https://github.com/audreyfeldroy) who created [`cookiecutter`](https://github.com/cookiecutter/cookiecutter), which meant rolling this out took me just 15 minutes. +* @tinche for pointing out the threading issue and providing a solution. +* @bcho for providing the time-to-expire feature + +## Support this project + +This project is maintained by volunteers. Support their efforts by spreading the word about: + +### Django Crash Course + +[![A Wedge of Django](https://cdn.shopify.com/s/files/1/0304/6901/products/AWoD-Front-5.5x8.5in_540x.jpg?v=1600471198)](https://www.feldroy.com/products/django-crash-course) + +Django Crash Course for Django 3.0 and Python 3.8 is the best cheese-themed Django reference in the universe! diff --git a/README.rst b/README.rst deleted file mode 100644 index c5c2411..0000000 --- a/README.rst +++ /dev/null @@ -1,259 +0,0 @@ -=============================== -cached-property -=============================== - -.. image:: https://img.shields.io/pypi/v/cached-property.svg - :target: https://pypi.python.org/pypi/cached-property - -.. image:: https://github.com/pydanny/cached-property/workflows/Python%20package/badge.svg - :target: https://github.com/pydanny/cached-property/actions - - -.. 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. - -Why? ------ - -* Makes caching of time or computational expensive properties quick and easy. -* Because I got tired of copy/pasting this code from non-web project to non-web project. -* I needed something really simple that worked in Python 2 and 3. - -How to use it --------------- - -Let's define a class with an expensive property. Every time you stay there the -price goes up by $50! - -.. code-block:: python - - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 - - @property - def boardwalk(self): - # In reality, this might represent a database call or time - # intensive task like calling a third-party API. - self.boardwalk_price += 50 - return self.boardwalk_price - -Now run it: - -.. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 600 - -Let's convert the boardwalk property into a ``cached_property``. - -.. code-block:: python - - from cached_property import cached_property - - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 - - @cached_property - def boardwalk(self): - # Again, this is a silly example. Don't worry about it, this is - # just an example for clarity. - self.boardwalk_price += 50 - return self.boardwalk_price - -Now when we run it the price stays at $550. - -.. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 - -Why doesn't the value of ``monopoly.boardwalk`` change? Because it's a **cached property**! - -Invalidating the Cache ----------------------- - -Results of cached functions can be invalidated by outside forces. Let's demonstrate how to force the cache to invalidate: - -.. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 - >>> # invalidate the cache - >>> del monopoly.__dict__['boardwalk'] - >>> # request the boardwalk property again - >>> monopoly.boardwalk - 600 - >>> monopoly.boardwalk - 600 - -Working with Threads ---------------------- - -What if a whole bunch of people want to stay at Boardwalk all at once? This means using threads, which -unfortunately causes problems with the standard ``cached_property``. In this case, switch to using the -``threaded_cached_property``: - -.. code-block:: python - - from cached_property import threaded_cached_property - - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 - - @threaded_cached_property - def boardwalk(self): - """threaded_cached_property is really nice for when no one waits - for other people to finish their turn and rudely start rolling - dice and moving their pieces.""" - - sleep(1) - self.boardwalk_price += 50 - return self.boardwalk_price - -Now use it: - -.. code-block:: python - - >>> from threading import Thread - >>> from monopoly import Monopoly - >>> monopoly = Monopoly() - >>> threads = [] - >>> for x in range(10): - >>> thread = Thread(target=lambda: monopoly.boardwalk) - >>> thread.start() - >>> threads.append(thread) - - >>> for thread in threads: - >>> thread.join() - - >>> 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 --------------------- - -Sometimes you want the price of things to reset after a time. Use the ``ttl`` -versions of ``cached_property`` and ``threaded_cached_property``. - -.. code-block:: python - - import random - from cached_property import cached_property_with_ttl - - class Monopoly(object): - - @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds - def dice(self): - # I dare the reader to implement a game using this method of 'rolling dice'. - return random.randint(2,12) - -Now use it: - -.. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.dice - 10 - >>> monopoly.dice - 10 - >>> from time import sleep - >>> sleep(6) # Sleeps long enough to expire the cache - >>> monopoly.dice - 3 - >>> monopoly.dice - 3 - -**Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This -is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. - -Credits --------- - -* Pip, Django, Werkzueg, Bottle, Pyramid, and Zope for having their own implementations. This package originally used an implementation that matched the Bottle version. -* Reinout Van Rees for pointing out the `cached_property` decorator to me. -* My awesome wife `@audreyr`_ who created `cookiecutter`_, which meant rolling this out took me just 15 minutes. -* @tinche for pointing out the threading issue and providing a solution. -* @bcho for providing the time-to-expire feature - -.. _`@audreyr`: https://github.com/audreyr -.. _`cookiecutter`: https://github.com/audreyr/cookiecutter - -Support This Project ---------------------------- - -This project is maintained by volunteers. Support their efforts by spreading the word about: - -Django Crash Course -++++++++++++++++++++++++++++++++++++ - -.. image:: https://cdn.shopify.com/s/files/1/0304/6901/files/Django-Crash-Course-300x436.jpg - :name: Django Crash Course: Covers Django 3.0 and Python 3.8 - :align: center - :alt: Django Crash Course - :target: https://www.roygreenfeld.com/products/django-crash-course - -Django Crash Course for Django 3.0 and Python 3.8 is the best cheese-themed Django reference in the universe! -- 2.30.2