Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add official support for current Python versions #353

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,27 @@ on:
branches: [ master ]
paths-ignore:
- '**.md'
- '**.rst'
pull_request:
branches: [ master ]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has restricted the CI run to PRs from the master branch itself, which is usually not what is wanted.

paths-ignore:
- '**.md'
- '**.rst'

jobs:
run:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
# operating-system: [ubuntu-latest, windows-latest, macos-latest]
operating-system: [ubuntu-latest, windows-latest]
python-version: [3.5, 3.6, 3.7, 3.8]
operating-system: [ubuntu-latest, windows-latest, macos-latest]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
include:
- python-version: "pypy-3.9"
operating-system: ubuntu-latest
- python-version: "pypy-3.10"
operating-system: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,16 @@ Before you submit a pull request, check that it meets these guidelines:
1. The pull request should include tests.
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.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.
feature to the list in README.md.
3. The pull request should work for all Python versions defined as classifiers in `setup.py`.
Make sure that the [GitHub Action tests](https://github.com/pydanny/cached-property/actions)
pass for all supported Python versions.

## Tips

To run a subset of tests:

```bash
$ python -m unittest tests.test_cached-property
$ pytest tests/test_cached-property.py
$ pytest tests/test_cached-property.py::TestCachedProperty
```
6 changes: 3 additions & 3 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# History

## 2.0 (2020-09-??)
## 2.0.0 (2024-??-??)

* Remove formal support for Python 2.7
* Convert RST to MD
* Remove support for Python versions < 3.8
* Add formal support for Python versions up to 3.13

## 1.5.2 (2020-09-21)

Expand Down
12 changes: 4 additions & 8 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
include AUTHORS.rst
include CONTRIBUTING.rst
include HISTORY.rst
include AUTHORS.md
include CONTRIBUTING.md
include HISTORY.md
include LICENSE
include README.rst
include README.md

recursive-include tests *
include conftest.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

recursive-include docs *.rst conftest.py Makefile make.bat
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Let's define a class with an expensive property. Every time you stay there the
price goes up by $50!

```python
class Monopoly(object):
class Monopoly:

def __init__(self):
self.boardwalk_price = 500
Expand Down Expand Up @@ -101,7 +101,7 @@ unfortunately causes problems with the standard `cached_property`. In this case,
```python
from cached_property import threaded_cached_property

class Monopoly(object):
class Monopoly:

def __init__(self):
self.boardwalk_price = 500
Expand Down Expand Up @@ -135,7 +135,7 @@ Now use it:
>>> self.assertEqual(m.boardwalk, 550)
```

## Working with async/await (Python 3.5+)
## Working with async/await

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
Expand All @@ -144,7 +144,7 @@ computed once and then cached:
```python
from cached_property import cached_property

class Monopoly(object):
class Monopoly:

def __init__(self):
self.boardwalk_price = 500
Expand Down Expand Up @@ -214,7 +214,7 @@ is why they are broken out into seperate tools. See https://github.com/pydanny/c

## Credits

* Pip, Django, Werkzueg, Bottle, Pyramid, and Zope for having their own implementations. This package originally used an implementation that matched the Bottle version.
* Pip, Django, Werkzeug, 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.
Expand Down
19 changes: 6 additions & 13 deletions cached_property.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
# -*- coding: utf-8 -*-

__author__ = "Daniel Greenfeld"
__email__ = "[email protected]"
__version__ = "1.5.2"
__version__ = "2.0.0"
__license__ = "BSD"

from functools import wraps
from time import time
import threading

try:
import asyncio
except (ImportError, SyntaxError):
asyncio = None
import asyncio


class cached_property(object):
class cached_property:
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
Expand All @@ -30,15 +24,14 @@ def __get__(self, obj, cls):
if obj is None:
return self

if asyncio and asyncio.iscoroutinefunction(self.func):
if 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):
@wraps(obj)
@asyncio.coroutine
def wrapper():
future = asyncio.ensure_future(self.func(obj))
obj.__dict__[self.func.__name__] = future
Expand All @@ -47,7 +40,7 @@ def wrapper():
return wrapper()


class threaded_cached_property(object):
class threaded_cached_property:
"""
A cached_property version for use in environments where multiple threads
might concurrently try to access the property.
Expand All @@ -74,7 +67,7 @@ def __get__(self, obj, cls):
return obj_dict.setdefault(name, self.func(obj))


class cached_property_with_ttl(object):
class cached_property_with_ttl:
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Setting the ttl to a number expresses how long
Expand Down
20 changes: 0 additions & 20 deletions conftest.py

This file was deleted.

8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Testing and deployment packages.
coverage==4.4.2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, all this should go into a pyproject.yaml, but I wanted to keep the changes small.

pytest==3.8.2
pytest-cov==2.6.0
freezegun==0.3.10
coverage==7.6.1
pytest==8.3.3
pytest-cov==5.0.0
freezegun==1.5.1
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[wheel]
universal = 1
universal = 0
15 changes: 8 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
Expand Down Expand Up @@ -53,12 +52,14 @@ def read(fname):
"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.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
)
1 change: 0 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
# -*- coding: utf-8 -*-
28 changes: 7 additions & 21 deletions tests/test_async_cached_property.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import asyncio
import time
import unittest
Expand All @@ -7,23 +6,13 @@
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):
class Check:
def __init__(self):
self.control_total = 0
self.cached_total = 0
Expand Down Expand Up @@ -63,7 +52,7 @@ def call_add_cached():
return Check


class TestCachedProperty(unittest.TestCase):
class TestCachedProperty(unittest.IsolatedAsyncioTestCase):
"""Tests for cached_property"""

cached_property_factory = cached_property.cached_property
Expand All @@ -72,18 +61,17 @@ 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)
self.assertEqual(expected, await check.add_control())
self.assertEqual(expected, check.control_total)

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)
self.assertEqual(expected, await check.add_cached)
self.assertEqual(expected, check.cached_total)

@unittest_run_loop
async def test_cached_property(self):
Check = CheckFactory(self.cached_property_factory)
check = Check()
Expand All @@ -104,7 +92,6 @@ async def test_cached_property(self):
# 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()
Expand All @@ -120,9 +107,8 @@ async def test_reset_cached_property(self):
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):
class Check:
def __init__(self):
self.cached_total = None

Expand Down
Loading
Loading