├── .coveragerc ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── NEWS.rst ├── README.rst ├── SECURITY.md ├── backports ├── __init__.py └── functools_lru_cache.py ├── docs ├── conf.py ├── history.rst └── index.rst ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── tests └── test_lru_cache.py ├── towncrier.toml └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # leading `*/` for pytest-dev/pytest-cov#456 4 | */.tox/* 5 | disable_warnings = 6 | couldnt-parse 7 | 8 | [report] 9 | show_missing = True 10 | exclude_also = 11 | # Exclude common false positives per 12 | # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion 13 | # Ref jaraco/skeleton#97 and jaraco/skeleton#135 14 | class .*\bProtocol\): 15 | if TYPE_CHECKING: 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.py] 11 | indent_style = space 12 | max_line_length = 88 13 | 14 | [*.{yml,yaml}] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.rst] 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: pypi/backports.functools_lru_cache 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | merge_group: 5 | push: 6 | branches-ignore: 7 | # temporary GH branches relating to merge queues (jaraco/skeleton#93) 8 | - gh-readonly-queue/** 9 | tags: 10 | # required if branches-ignore is supplied (jaraco/skeleton#103) 11 | - '**' 12 | pull_request: 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: read 17 | 18 | env: 19 | # Environment variable to support color support (jaraco/skeleton#66) 20 | FORCE_COLOR: 1 21 | 22 | # Suppress noisy pip warnings 23 | PIP_DISABLE_PIP_VERSION_CHECK: 'true' 24 | PIP_NO_WARN_SCRIPT_LOCATION: 'true' 25 | 26 | # Ensure tests can sense settings about the environment 27 | TOX_OVERRIDE: >- 28 | testenv.pass_env+=GITHUB_*,FORCE_COLOR 29 | 30 | 31 | jobs: 32 | test: 33 | strategy: 34 | # https://blog.jaraco.com/efficient-use-of-ci-resources/ 35 | matrix: 36 | python: 37 | - "3.9" 38 | - "3.13" 39 | platform: 40 | - ubuntu-latest 41 | - macos-latest 42 | - windows-latest 43 | include: 44 | - python: "3.10" 45 | platform: ubuntu-latest 46 | - python: "3.11" 47 | platform: ubuntu-latest 48 | - python: "3.12" 49 | platform: ubuntu-latest 50 | - python: "3.14" 51 | platform: ubuntu-latest 52 | - python: pypy3.10 53 | platform: ubuntu-latest 54 | runs-on: ${{ matrix.platform }} 55 | continue-on-error: ${{ matrix.python == '3.14' }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Install build dependencies 59 | # Install dependencies for building packages on pre-release Pythons 60 | # jaraco/skeleton#161 61 | if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' 62 | run: | 63 | sudo apt update 64 | sudo apt install -y libxml2-dev libxslt-dev 65 | - name: Setup Python 66 | uses: actions/setup-python@v5 67 | with: 68 | python-version: ${{ matrix.python }} 69 | allow-prereleases: true 70 | - name: Install tox 71 | run: python -m pip install tox 72 | - name: Run 73 | run: tox 74 | 75 | collateral: 76 | strategy: 77 | fail-fast: false 78 | matrix: 79 | job: 80 | - diffcov 81 | - docs 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v4 85 | with: 86 | fetch-depth: 0 87 | - name: Setup Python 88 | uses: actions/setup-python@v5 89 | with: 90 | python-version: 3.x 91 | - name: Install tox 92 | run: python -m pip install tox 93 | - name: Eval ${{ matrix.job }} 94 | run: tox -e ${{ matrix.job }} 95 | 96 | check: # This job does nothing and is only used for the branch protection 97 | if: always() 98 | 99 | needs: 100 | - test 101 | - collateral 102 | 103 | runs-on: ubuntu-latest 104 | 105 | steps: 106 | - name: Decide whether the needed jobs succeeded or failed 107 | uses: re-actors/alls-green@release/v1 108 | with: 109 | jobs: ${{ toJSON(needs) }} 110 | 111 | release: 112 | permissions: 113 | contents: write 114 | needs: 115 | - check 116 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 117 | runs-on: ubuntu-latest 118 | 119 | steps: 120 | - uses: actions/checkout@v4 121 | - name: Setup Python 122 | uses: actions/setup-python@v5 123 | with: 124 | python-version: 3.x 125 | - name: Install tox 126 | run: python -m pip install tox 127 | - name: Run 128 | run: tox -e release 129 | env: 130 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.9 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --unsafe-fixes] 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | install: 4 | - path: . 5 | extra_requirements: 6 | - doc 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # required boilerplate readthedocs/readthedocs.org#10401 12 | build: 13 | os: ubuntu-lts-latest 14 | tools: 15 | python: latest 16 | # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 17 | jobs: 18 | post_checkout: 19 | - git fetch --unshallow || true 20 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | v2.0.0 2 | ====== 3 | 4 | Features 5 | -------- 6 | 7 | - Refreshed implementation from CPython. 8 | 9 | 10 | Deprecations and Removals 11 | ------------------------- 12 | 13 | - Drop support for Python 2 and require Python 3.8 or later. In most cases, users should rely on the 1.x releases for an implementation that runs on older Pythons. This 2.0 series provides minimal value for users of Python 3.3 and later except to provide packaging updates and possibly preview access to functionality on later Python versions. 14 | 15 | 16 | v1.6.6 17 | ====== 18 | 19 | Bugfixes 20 | -------- 21 | 22 | - Restored install compatibilty on Python 2. (#20) 23 | 24 | 25 | v1.6.5 26 | ====== 27 | 28 | No significant changes. 29 | 30 | 31 | v1.6.4 32 | ====== 33 | 34 | #16: For test dependencies, when indicating Python 3, use ``>=3`` 35 | instead of ``>3`` to satisfy 36 | `python-poetry/poetry#3862 `_. 37 | 38 | v1.6.3 39 | ====== 40 | 41 | #15: Restore universal wheel. 42 | 43 | v1.6.2 44 | ====== 45 | 46 | Packaging refresh. 47 | 48 | v1.6.1 49 | ====== 50 | 51 | Publish release notes on readthedocs. 52 | 53 | v1.6.0 54 | ====== 55 | 56 | Refresh package metadata. 57 | Use black for code style. 58 | Enroll with Tidelift. 59 | 60 | 1.5 61 | === 62 | 63 | Refresh package metadata including publishing license with the 64 | wheel (#11). 65 | 66 | 1.4 67 | === 68 | 69 | #9: Updated namespace package to use pkgutil for declaring the 70 | namespace. 71 | 72 | 1.3 73 | === 74 | 75 | Tagged commits are automatically released following passing 76 | tests. 77 | 78 | 1.2 79 | === 80 | 81 | Issue #5: Added a minimal test suite. 82 | 83 | 1.1 84 | === 85 | 86 | Moved hosting to Github. 87 | Library uses setuptools_scm for version tagging. 88 | Added license declaration. 89 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/backports.functools_lru_cache.svg 2 | :target: https://pypi.org/project/backports.functools_lru_cache 3 | 4 | .. image:: https://img.shields.io/pypi/pyversions/backports.functools_lru_cache.svg 5 | 6 | .. image:: https://github.com/jaraco/backports.functools_lru_cache/actions/workflows/main.yml/badge.svg 7 | :target: https://github.com/jaraco/backports.functools_lru_cache/actions?query=workflow%3A%22tests%22 8 | :alt: tests 9 | 10 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 11 | :target: https://github.com/astral-sh/ruff 12 | :alt: Ruff 13 | 14 | .. image:: https://readthedocs.org/projects/backportsfunctools_lru_cache/badge/?version=latest 15 | :target: https://backportsfunctools_lru_cache.readthedocs.io/en/latest/?badge=latest 16 | 17 | .. image:: https://img.shields.io/badge/skeleton-2025-informational 18 | :target: https://blog.jaraco.com/skeleton 19 | 20 | .. image:: https://tidelift.com/badges/package/pypi/backports.functools_lru_cache 21 | :target: https://tidelift.com/subscription/pkg/pypi-backports.functools_lru_cache?utm_source=pypi-backports.functools_lru_cache&utm_medium=readme 22 | 23 | Backport of functools.lru_cache from Python 3.3 as published at `ActiveState 24 | `_. 25 | 26 | Usage 27 | ===== 28 | 29 | Consider using this technique for importing the 'lru_cache' function:: 30 | 31 | try: 32 | from functools import lru_cache 33 | except ImportError: 34 | from backports.functools_lru_cache import lru_cache 35 | 36 | 37 | For Enterprise 38 | ============== 39 | 40 | Available as part of the Tidelift Subscription. 41 | 42 | This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. 43 | 44 | `Learn more `_. 45 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Contact 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /backports/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /backports/functools_lru_cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import functools 4 | from collections import namedtuple 5 | from threading import RLock 6 | 7 | _CacheInfo = namedtuple("_CacheInfo", ["hits", "misses", "maxsize", "currsize"]) 8 | 9 | 10 | @functools.wraps(functools.update_wrapper) 11 | def update_wrapper( 12 | wrapper, 13 | wrapped, 14 | assigned=functools.WRAPPER_ASSIGNMENTS, 15 | updated=functools.WRAPPER_UPDATES, 16 | ): 17 | """ 18 | Patch two bugs in functools.update_wrapper. 19 | """ 20 | # workaround for http://bugs.python.org/issue3445 21 | assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr)) 22 | wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated) 23 | # workaround for https://bugs.python.org/issue17482 24 | wrapper.__wrapped__ = wrapped 25 | return wrapper 26 | 27 | 28 | class _HashedSeq(list): 29 | """This class guarantees that hash() will be called no more than once 30 | per element. This is important because the lru_cache() will hash 31 | the key multiple times on a cache miss. 32 | 33 | """ 34 | 35 | __slots__ = 'hashvalue' 36 | 37 | def __init__(self, tup, hash=hash): 38 | self[:] = tup 39 | self.hashvalue = hash(tup) 40 | 41 | def __hash__(self): 42 | return self.hashvalue 43 | 44 | 45 | def _make_key( 46 | args, 47 | kwds, 48 | typed, 49 | kwd_mark=(object(),), 50 | fasttypes={int, str}, 51 | tuple=tuple, 52 | type=type, 53 | len=len, 54 | ): 55 | """Make a cache key from optionally typed positional and keyword arguments 56 | 57 | The key is constructed in a way that is flat as possible rather than 58 | as a nested structure that would take more memory. 59 | 60 | If there is only a single argument and its data type is known to cache 61 | its hash value, then that argument is returned without a wrapper. This 62 | saves space and improves lookup speed. 63 | 64 | """ 65 | # All of code below relies on kwds preserving the order input by the user. 66 | # Formerly, we sorted() the kwds before looping. The new way is *much* 67 | # faster; however, it means that f(x=1, y=2) will now be treated as a 68 | # distinct call from f(y=2, x=1) which will be cached separately. 69 | key = args 70 | if kwds: 71 | key += kwd_mark 72 | for item in kwds.items(): 73 | key += item 74 | if typed: 75 | key += tuple(type(v) for v in args) 76 | if kwds: 77 | key += tuple(type(v) for v in kwds.values()) 78 | elif len(key) == 1 and type(key[0]) in fasttypes: 79 | return key[0] 80 | return _HashedSeq(key) 81 | 82 | 83 | def lru_cache(maxsize=128, typed=False): 84 | """Least-recently-used cache decorator. 85 | 86 | If *maxsize* is set to None, the LRU features are disabled and the cache 87 | can grow without bound. 88 | 89 | If *typed* is True, arguments of different types will be cached separately. 90 | For example, f(decimal.Decimal("3.0")) and f(3.0) will be treated as 91 | distinct calls with distinct results. Some types such as str and int may 92 | be cached separately even when typed is false. 93 | 94 | Arguments to the cached function must be hashable. 95 | 96 | View the cache statistics named tuple (hits, misses, maxsize, currsize) 97 | with f.cache_info(). Clear the cache and statistics with f.cache_clear(). 98 | Access the underlying function with f.__wrapped__. 99 | 100 | See: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) 101 | 102 | """ 103 | 104 | # Users should only access the lru_cache through its public API: 105 | # cache_info, cache_clear, and f.__wrapped__ 106 | # The internals of the lru_cache are encapsulated for thread safety and 107 | # to allow the implementation to change (including a possible C version). 108 | 109 | if isinstance(maxsize, int): 110 | # Negative maxsize is treated as 0 111 | if maxsize < 0: 112 | maxsize = 0 113 | elif callable(maxsize) and isinstance(typed, bool): 114 | # The user_function was passed in directly via the maxsize argument 115 | user_function, maxsize = maxsize, 128 116 | wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) 117 | wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed} 118 | return update_wrapper(wrapper, user_function) 119 | elif maxsize is not None: 120 | raise TypeError('Expected first argument to be an integer, a callable, or None') 121 | 122 | def decorating_function(user_function): 123 | wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) 124 | wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed} 125 | return update_wrapper(wrapper, user_function) 126 | 127 | return decorating_function 128 | 129 | 130 | def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo): # noqa: C901 131 | # Constants shared by all lru cache instances: 132 | sentinel = object() # unique object used to signal cache misses 133 | make_key = _make_key # build a key from the function arguments 134 | PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields 135 | 136 | cache = {} 137 | hits = misses = 0 138 | full = False 139 | cache_get = cache.get # bound method to lookup a key or return None 140 | cache_len = cache.__len__ # get cache size without calling len() 141 | lock = RLock() # because linkedlist updates aren't threadsafe 142 | root = [] # root of the circular doubly linked list 143 | root[:] = [root, root, None, None] # initialize by pointing to self 144 | 145 | if maxsize == 0: 146 | 147 | def wrapper(*args, **kwds): 148 | # No caching -- just a statistics update 149 | nonlocal misses 150 | misses += 1 151 | result = user_function(*args, **kwds) 152 | return result 153 | 154 | elif maxsize is None: 155 | 156 | def wrapper(*args, **kwds): 157 | # Simple caching without ordering or size limit 158 | nonlocal hits, misses 159 | key = make_key(args, kwds, typed) 160 | result = cache_get(key, sentinel) 161 | if result is not sentinel: 162 | hits += 1 163 | return result 164 | misses += 1 165 | result = user_function(*args, **kwds) 166 | cache[key] = result 167 | return result 168 | 169 | else: 170 | 171 | def wrapper(*args, **kwds): 172 | # Size limited caching that tracks accesses by recency 173 | nonlocal root, hits, misses, full 174 | key = make_key(args, kwds, typed) 175 | with lock: 176 | link = cache_get(key) 177 | if link is not None: 178 | # Move the link to the front of the circular queue 179 | link_prev, link_next, _key, result = link 180 | link_prev[NEXT] = link_next 181 | link_next[PREV] = link_prev 182 | last = root[PREV] 183 | last[NEXT] = root[PREV] = link 184 | link[PREV] = last 185 | link[NEXT] = root 186 | hits += 1 187 | return result 188 | misses += 1 189 | result = user_function(*args, **kwds) 190 | with lock: 191 | if key in cache: 192 | # Getting here means that this same key was added to the 193 | # cache while the lock was released. Since the link 194 | # update is already done, we need only return the 195 | # computed result and update the count of misses. 196 | pass 197 | elif full: 198 | # Use the old root to store the new key and result. 199 | oldroot = root 200 | oldroot[KEY] = key 201 | oldroot[RESULT] = result 202 | # Empty the oldest link and make it the new root. 203 | # Keep a reference to the old key and old result to 204 | # prevent their ref counts from going to zero during the 205 | # update. That will prevent potentially arbitrary object 206 | # clean-up code (i.e. __del__) from running while we're 207 | # still adjusting the links. 208 | root = oldroot[NEXT] 209 | oldkey = root[KEY] 210 | root[KEY] = root[RESULT] = None 211 | # Now update the cache dictionary. 212 | del cache[oldkey] 213 | # Save the potentially reentrant cache[key] assignment 214 | # for last, after the root and links have been put in 215 | # a consistent state. 216 | cache[key] = oldroot 217 | else: 218 | # Put result in a new link at the front of the queue. 219 | last = root[PREV] 220 | link = [last, root, key, result] 221 | last[NEXT] = root[PREV] = cache[key] = link 222 | # Use the cache_len bound method instead of the len() function 223 | # which could potentially be wrapped in an lru_cache itself. 224 | full = cache_len() >= maxsize 225 | return result 226 | 227 | def cache_info(): 228 | """Report cache statistics""" 229 | with lock: 230 | return _CacheInfo(hits, misses, maxsize, cache_len()) 231 | 232 | def cache_clear(): 233 | """Clear the cache and cache statistics""" 234 | nonlocal hits, misses, full 235 | with lock: 236 | cache.clear() 237 | root[:] = [root, root, None, None] 238 | hits = misses = 0 239 | full = False 240 | 241 | wrapper.cache_info = cache_info 242 | wrapper.cache_clear = cache_clear 243 | return wrapper 244 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | extensions = [ 4 | 'sphinx.ext.autodoc', 5 | 'jaraco.packaging.sphinx', 6 | ] 7 | 8 | master_doc = "index" 9 | html_theme = "furo" 10 | 11 | # Link dates and other references in the changelog 12 | extensions += ['rst.linker'] 13 | link_files = { 14 | '../NEWS.rst': dict( 15 | using=dict(GH='https://github.com'), 16 | replace=[ 17 | dict( 18 | pattern=r'(Issue #|\B#)(?P\d+)', 19 | url='{package_url}/issues/{issue}', 20 | ), 21 | dict( 22 | pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', 23 | with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', 24 | ), 25 | dict( 26 | pattern=r'PEP[- ](?P\d+)', 27 | url='https://peps.python.org/pep-{pep_number:0>4}/', 28 | ), 29 | ], 30 | ) 31 | } 32 | 33 | # Be strict about any broken references 34 | nitpicky = True 35 | nitpick_ignore: list[tuple[str, str]] = [] 36 | 37 | # Include Python intersphinx mapping to prevent failures 38 | # jaraco/skeleton#51 39 | extensions += ['sphinx.ext.intersphinx'] 40 | intersphinx_mapping = { 41 | 'python': ('https://docs.python.org/3', None), 42 | } 43 | 44 | # Preserve authored syntax for defaults 45 | autodoc_preserve_defaults = True 46 | 47 | # Add support for linking usernames, PyPI projects, Wikipedia pages 48 | github_url = 'https://github.com/' 49 | extlinks = { 50 | 'user': (f'{github_url}%s', '@%s'), 51 | 'pypi': ('https://pypi.org/project/%s', '%s'), 52 | 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), 53 | } 54 | extensions += ['sphinx.ext.extlinks'] 55 | 56 | # local 57 | 58 | extensions += ['jaraco.tidelift'] 59 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | History 6 | ******* 7 | 8 | .. include:: ../NEWS (links).rst 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to |project| documentation! 2 | =================================== 3 | 4 | .. sidebar-links:: 5 | :home: 6 | :pypi: 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | history 12 | 13 | .. tidelift-referral-banner:: 14 | 15 | .. automodule:: backports.functools_lru_cache 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Is the project well-typed? 3 | strict = False 4 | 5 | # Early opt-in even when strict = False 6 | warn_unused_ignores = True 7 | warn_redundant_casts = True 8 | enable_error_code = ignore-without-code 9 | 10 | # Support namespace packages per https://github.com/python/mypy/issues/14057 11 | explicit_package_bases = True 12 | 13 | disable_error_code = 14 | # Disable due to many false positives 15 | overload-overlap, 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=77", 4 | "setuptools_scm[toml]>=3.4.1", 5 | # jaraco/skeleton#174 6 | "coherent.licensed", 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [project] 11 | name = "backports.functools_lru_cache" 12 | authors = [ 13 | { name = "Raymond Hettinger", email = "raymond.hettinger@gmail.com" }, 14 | ] 15 | maintainers = [ 16 | { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, 17 | ] 18 | description = "Backport of functools.lru_cache" 19 | readme = "README.rst" 20 | classifiers = [ 21 | "Development Status :: 5 - Production/Stable", 22 | "Intended Audience :: Developers", 23 | "Programming Language :: Python :: 3", 24 | ] 25 | requires-python = ">=3.9" 26 | license = "MIT" 27 | dependencies = [ 28 | ] 29 | dynamic = ["version"] 30 | 31 | [project.urls] 32 | Source = "https://github.com/jaraco/backports.functools_lru_cache" 33 | 34 | [project.optional-dependencies] 35 | test = [ 36 | # upstream 37 | "pytest >= 6, != 8.1.*", 38 | 39 | # local 40 | ] 41 | 42 | doc = [ 43 | # upstream 44 | "sphinx >= 3.5", 45 | "jaraco.packaging >= 9.3", 46 | "rst.linker >= 1.9", 47 | "furo", 48 | "sphinx-lint", 49 | 50 | # tidelift 51 | "jaraco.tidelift >= 1.4", 52 | 53 | # local 54 | ] 55 | 56 | check = [ 57 | "pytest-checkdocs >= 2.4", 58 | "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", 59 | ] 60 | 61 | cover = [ 62 | "pytest-cov", 63 | ] 64 | 65 | enabler = [ 66 | "pytest-enabler >= 2.2", 67 | ] 68 | 69 | type = [ 70 | # upstream 71 | "pytest-mypy", 72 | 73 | # local 74 | ] 75 | 76 | 77 | [tool.setuptools_scm] 78 | 79 | 80 | [tool.pytest-enabler.mypy] 81 | # Disabled due to jaraco/skeleton#143 82 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=dist build .tox .eggs 3 | addopts= 4 | --doctest-modules 5 | --import-mode importlib 6 | consider_namespace_packages=true 7 | filterwarnings= 8 | ## upstream 9 | 10 | # Ensure ResourceWarnings are emitted 11 | default::ResourceWarning 12 | 13 | # realpython/pytest-mypy#152 14 | ignore:'encoding' argument not specified::pytest_mypy 15 | 16 | # python/cpython#100750 17 | ignore:'encoding' argument not specified::platform 18 | 19 | # pypa/build#615 20 | ignore:'encoding' argument not specified::build.env 21 | 22 | # dateutil/dateutil#1284 23 | ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz 24 | 25 | ## end upstream 26 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | extend-select = [ 3 | # upstream 4 | 5 | "C901", # complex-structure 6 | "I", # isort 7 | "PERF401", # manual-list-comprehension 8 | 9 | # Ensure modern type annotation syntax and best practices 10 | # Not including those covered by type-checkers or exclusive to Python 3.11+ 11 | "FA", # flake8-future-annotations 12 | "F404", # late-future-import 13 | "PYI", # flake8-pyi 14 | "UP006", # non-pep585-annotation 15 | "UP007", # non-pep604-annotation 16 | "UP010", # unnecessary-future-import 17 | "UP035", # deprecated-import 18 | "UP037", # quoted-annotation 19 | "UP043", # unnecessary-default-type-args 20 | 21 | # local 22 | ] 23 | ignore = [ 24 | # upstream 25 | 26 | # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, 27 | # irrelevant to this project. 28 | "PYI011", # typed-argument-default-in-stub 29 | # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 30 | "W191", 31 | "E111", 32 | "E114", 33 | "E117", 34 | "D206", 35 | "D300", 36 | "Q000", 37 | "Q001", 38 | "Q002", 39 | "Q003", 40 | "COM812", 41 | "COM819", 42 | 43 | # local 44 | ] 45 | 46 | [format] 47 | # Enable preview to get hugged parenthesis unwrapping and other nice surprises 48 | # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 49 | preview = true 50 | # https://docs.astral.sh/ruff/settings/#format_quote-style 51 | quote-style = "preserve" 52 | -------------------------------------------------------------------------------- /tests/test_lru_cache.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from backports.functools_lru_cache import lru_cache 4 | 5 | 6 | def test_invocation(): 7 | @lru_cache() 8 | def func(): 9 | return random.random() 10 | 11 | assert func() == func() 12 | -------------------------------------------------------------------------------- /towncrier.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | title_format = "{version}" 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | description = perform primary checks (tests, style, types, coverage) 3 | deps = 4 | setenv = 5 | PYTHONWARNDEFAULTENCODING = 1 6 | commands = 7 | pytest {posargs} 8 | usedevelop = True 9 | extras = 10 | test 11 | check 12 | cover 13 | enabler 14 | type 15 | 16 | [testenv:diffcov] 17 | description = run tests and check that diff from main is covered 18 | deps = 19 | {[testenv]deps} 20 | diff-cover 21 | commands = 22 | pytest {posargs} --cov-report xml 23 | diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html 24 | diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 25 | 26 | [testenv:docs] 27 | description = build the documentation 28 | extras = 29 | doc 30 | test 31 | changedir = docs 32 | commands = 33 | python -m sphinx -W --keep-going . {toxinidir}/build/html 34 | python -m sphinxlint 35 | 36 | [testenv:finalize] 37 | description = assemble changelog and tag a release 38 | skip_install = True 39 | deps = 40 | towncrier 41 | jaraco.develop >= 7.23 42 | pass_env = * 43 | commands = 44 | python -m jaraco.develop.finalize 45 | 46 | 47 | [testenv:release] 48 | description = publish the package to PyPI and GitHub 49 | skip_install = True 50 | deps = 51 | build 52 | twine>=3 53 | jaraco.develop>=7.1 54 | pass_env = 55 | TWINE_PASSWORD 56 | GITHUB_TOKEN 57 | setenv = 58 | TWINE_USERNAME = {env:TWINE_USERNAME:__token__} 59 | commands = 60 | python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" 61 | python -m build 62 | python -m twine upload dist/* 63 | python -m jaraco.develop.create-github-release 64 | --------------------------------------------------------------------------------