From 8bea55afe090372deddecc7f996ecf6fa86393b7 Mon Sep 17 00:00:00 2001 From: GigantPro Date: Wed, 30 Aug 2023 14:37:16 +0300 Subject: [PATCH 1/2] Add async support --- frozenclass/cache.py | 33 ++++++++++++++++-- poetry.lock | 73 +++++++++++---------------------------- pyproject.toml | 3 +- tests/test_async_cache.py | 47 +++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 56 deletions(-) create mode 100644 tests/test_async_cache.py diff --git a/frozenclass/cache.py b/frozenclass/cache.py index fef1fff..1879ea6 100644 --- a/frozenclass/cache.py +++ b/frozenclass/cache.py @@ -4,7 +4,8 @@ class CacheController: """The main class of the cache logic. Includes all caches logic""" - def cache(*, ttl: Optional[time] = time(minute=10)) -> Callable: # ( TTL_end, result ) + def cache(*, ttl: Optional[time] = time(minute=10), + is_async: bool = False) -> Callable: # ( TTL_end, result ) """Function-decorate for runtime caching. The cache can either be overwritten or remain until the program terminates. @@ -16,7 +17,6 @@ def cache(*, ttl: Optional[time] = time(minute=10)) -> Callable: # ( TTL_end, r def wrapper_func(target_func: Callable) -> Callable: __cached_vals = {} - def cached_func_with_time(*args, **kwargs) -> Any: cached_ = __cached_vals.get((*args, *kwargs), None) if cached_ and cached_[0] > datetime.now(): @@ -37,7 +37,34 @@ def cached_func_without_time(*args, **kwargs) -> Any: result = target_func(*args, **kwargs) __cached_vals[(*args, *kwargs)] = result return result + + + async def async_cached_func_with_time(*args, **kwargs) -> Any: + cached_ = __cached_vals.get((*args, *kwargs), None) + if cached_ and cached_[0] > datetime.now(): + return cached_[1] + + result = await target_func(*args, **kwargs) + + __cached_vals[(*args, *kwargs)] = \ + (datetime.now() + timedelta(hours=ttl.hour, minutes=ttl.minute, seconds=ttl.second), result) + + return result + + + async def async_cached_func_without_time(*args, **kwargs) -> Any: + try: + return __cached_vals[(*args, *kwargs)] + except KeyError: + result = await target_func(*args, **kwargs) + __cached_vals[(*args, *kwargs)] = result + return result + + + if not is_async: + return cached_func_with_time if ttl else cached_func_without_time + else: + return async_cached_func_with_time if ttl else async_cached_func_without_time - return cached_func_with_time if ttl else cached_func_without_time return wrapper_func diff --git a/poetry.lock b/poetry.lock index 1b8fbc8..4d506d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "astroid" version = "2.15.1" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -25,7 +24,6 @@ wrapt = [ name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -44,7 +42,6 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "bleach" version = "6.0.0" description = "An easy safelist-based HTML-sanitizing tool." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -63,7 +60,6 @@ css = ["tinycss2 (>=1.1.0,<1.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -75,7 +71,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -152,7 +147,6 @@ pycparser = "*" name = "charset-normalizer" version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.5.0" files = [ @@ -167,7 +161,6 @@ unicode-backport = ["unicodedata2"] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -179,7 +172,6 @@ files = [ name = "cryptography" version = "40.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -221,7 +213,6 @@ tox = ["tox"] name = "dill" version = "0.3.6" description = "serialize all of python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -236,7 +227,6 @@ graph = ["objgraph (>=1.7.2)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -248,7 +238,6 @@ files = [ name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -263,7 +252,6 @@ test = ["pytest (>=6)"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -275,7 +263,6 @@ files = [ name = "importlib-metadata" version = "6.1.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -296,7 +283,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -315,7 +301,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -327,7 +312,6 @@ files = [ name = "isort" version = "5.11.5" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -345,7 +329,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jaraco-classes" version = "3.2.3" description = "Utility functions for Python class constructs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -364,7 +347,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "jeepney" version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -380,7 +362,6 @@ trio = ["async_generator", "trio"] name = "keyring" version = "23.13.1" description = "Store and access your passwords safely." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -405,7 +386,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -451,7 +431,6 @@ files = [ name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -477,7 +456,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -489,7 +467,6 @@ files = [ name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -501,7 +478,6 @@ files = [ name = "more-itertools" version = "9.1.0" description = "More routines for operating on iterables, beyond itertools" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -513,7 +489,6 @@ files = [ name = "packaging" version = "23.0" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -525,7 +500,6 @@ files = [ name = "pkginfo" version = "1.9.6" description = "Query metadata from sdists / bdists / installed packages." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -540,7 +514,6 @@ testing = ["pytest", "pytest-cov"] name = "platformdirs" version = "3.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -559,7 +532,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -578,7 +550,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -590,7 +561,6 @@ files = [ name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -605,7 +575,6 @@ plugins = ["importlib-metadata"] name = "pylint" version = "2.17.1" description = "python code static checker" -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -635,7 +604,6 @@ testutils = ["gitpython (>3)"] name = "pytest" version = "7.2.2" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -656,11 +624,29 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "pywin32-ctypes" version = "0.2.0" description = "" -category = "main" optional = false python-versions = "*" files = [ @@ -672,7 +658,6 @@ files = [ name = "readme-renderer" version = "37.3" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -692,7 +677,6 @@ md = ["cmarkgfm (>=0.8.0)"] name = "requests" version = "2.27.1" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -714,7 +698,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] name = "requests-toolbelt" version = "0.10.1" description = "A utility belt for advanced users of python-requests" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -729,7 +712,6 @@ requests = ">=2.0.1,<3.0.0" name = "rfc3986" version = "2.0.0" description = "Validating URI References per RFC 3986" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -744,7 +726,6 @@ idna2008 = ["idna"] name = "rich" version = "13.3.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -764,7 +745,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -780,7 +760,6 @@ jeepney = ">=0.6" name = "setuptools" version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -797,7 +776,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -809,7 +787,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -821,7 +798,6 @@ files = [ name = "tomlkit" version = "0.11.7" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -833,7 +809,6 @@ files = [ name = "twine" version = "4.0.2" description = "Collection of utilities for publishing packages on PyPI" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -856,7 +831,6 @@ urllib3 = ">=1.26.0" name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -890,7 +864,6 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -902,7 +875,6 @@ files = [ name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -919,7 +891,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -931,7 +902,6 @@ files = [ name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1016,7 +986,6 @@ files = [ name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1031,4 +1000,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7.2" -content-hash = "16aa7591d777cccb8c43022c7f7c868b7ce1b86a299461cd01aac656236c27f8" +content-hash = "e18d9f57ba5ca3b7722f13ac270dea7b6b3a83c665d944d1766aff5de7733281" diff --git a/pyproject.toml b/pyproject.toml index 4df13e0..73426e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,14 @@ license = "The GPLv3 License (GPLv3)" readme = "README.rst" [tool.poetry.dependencies] -python = ">=3.7" +python = ">=3.7.2" [tool.poetry.group.dev.dependencies] setuptools = "^67.6.1" pylint = "^2.17.1" pytest = "^7.2.2" twine = "^4.0.2" +pytest-asyncio = "^0.21.1" [build-system] requires = ["poetry-core"] diff --git a/tests/test_async_cache.py b/tests/test_async_cache.py new file mode 100644 index 0000000..105cc76 --- /dev/null +++ b/tests/test_async_cache.py @@ -0,0 +1,47 @@ +import sqlite3 +from datetime import time +from time import sleep, time as t_time +from timeit import timeit +import pytest + +from frozenclass import CacheController + + +@pytest.mark.asyncio +async def test_cache_with_time(): + b = 1 + + @CacheController.cache(ttl=time(second=1), is_async=True) + async def test_cache(): + return b + + res = await test_cache() + assert res == 1 + + b = 2 + res = await test_cache() + assert res == 1 + + sleep(1) + res = await test_cache() + assert res == 2 + + +@pytest.mark.asyncio +async def test_cache_without_time(): + b = 1 + + @CacheController.cache(ttl=None, is_async=True) + async def test_cache(): + return b + + res = await test_cache() + assert res == 1 + + b = 2 + res = await test_cache() + assert res == 1 + + sleep(1) + res = await test_cache() + assert res == 1 From 0e3cdfa5d2feee6ca4ac6aad08bb63e4fd96bdc5 Mon Sep 17 00:00:00 2001 From: GigantPro Date: Wed, 30 Aug 2023 14:45:20 +0300 Subject: [PATCH 2/2] Fix linting --- frozenclass/cache.py | 8 ++++---- tests/test_async_cache.py | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/frozenclass/cache.py b/frozenclass/cache.py index 1879ea6..1006651 100644 --- a/frozenclass/cache.py +++ b/frozenclass/cache.py @@ -11,6 +11,8 @@ def cache(*, ttl: Optional[time] = time(minute=10), :param ttl: Time-To-Live of cached valume, defaults to time(minute=10) :type ttl: time | None, optional + :param is_async: Set True if decorated func is async, defaults to False + :type is_async: bool, optional :return: Decorated func :rtype: Callable """ @@ -37,7 +39,7 @@ def cached_func_without_time(*args, **kwargs) -> Any: result = target_func(*args, **kwargs) __cached_vals[(*args, *kwargs)] = result return result - + async def async_cached_func_with_time(*args, **kwargs) -> Any: cached_ = __cached_vals.get((*args, *kwargs), None) @@ -63,8 +65,6 @@ async def async_cached_func_without_time(*args, **kwargs) -> Any: if not is_async: return cached_func_with_time if ttl else cached_func_without_time - - else: - return async_cached_func_with_time if ttl else async_cached_func_without_time + return async_cached_func_with_time if ttl else async_cached_func_without_time return wrapper_func diff --git a/tests/test_async_cache.py b/tests/test_async_cache.py index 105cc76..e5383b7 100644 --- a/tests/test_async_cache.py +++ b/tests/test_async_cache.py @@ -1,7 +1,5 @@ -import sqlite3 from datetime import time -from time import sleep, time as t_time -from timeit import timeit +from time import sleep import pytest from frozenclass import CacheController