diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index 958db4b..c8e53d4 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -3,6 +3,8 @@ from functools import cached_property from typing import Any, Self +from asyncstdlib.functools import CachedProperty + from adcm_aio_client.core.requesters import Requester from adcm_aio_client.core.types import AwareOfOwnPath, Endpoint, WithRequester @@ -18,7 +20,7 @@ def __init_subclass__(cls: type[Self]) -> None: for name in dir(cls): # None is for declared, but unset values attr = getattr(cls, name, None) - if isinstance(attr, cached_property): # TODO: asyncstdlib.functools.CachedProperty + if isinstance(attr, (cached_property, CachedProperty)): cls._delete_on_refresh.append(name) def __init__(self: Self, requester: Requester, data: dict[str, Any]) -> None: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 456a7ea..6a88139 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -2,6 +2,8 @@ from typing import Iterable, Self import asyncio +from asyncstdlib.functools import cached_property as async_cached_property + from adcm_aio_client.core.errors import NotFoundError, OperationError, ResponseError from adcm_aio_client.core.objects._accessors import ( PaginatedAccessor, @@ -29,7 +31,7 @@ class ADCM(InteractiveObject, WithActions, WithConfig): def id(self: Self) -> int: return 1 - @cached_property + @async_cached_property async def version(self: Self) -> str: # TODO: override root_path for being without /api/v2 response = await self._requester.get("versions") @@ -68,7 +70,7 @@ def description(self: Self) -> str: # todo think how such properties will be invalidated when data is updated # during `refresh()` / `reread()` calls. # See cache invalidation or alternatives in documentation for `cached_property` - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def bundle(self: Self) -> Bundle: prototype_id = self._data["prototype"]["id"] response = await self._requester.get("prototypes", prototype_id) @@ -162,7 +164,7 @@ def name(self: Self) -> str: def display_name(self: Self) -> str: return self._data["displayName"] - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def constraint(self: Self) -> list[int | str]: response = (await self._requester.get(*self.cluster.get_own_path(), "mapping", "components")).as_list() for component in response: @@ -240,13 +242,13 @@ async def get_status(self: Self) -> ADCMEntityStatus: response = await self._requester.get(*self.get_own_path()) return ADCMEntityStatus(response.as_dict()["status"]) - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def cluster(self: Self) -> Cluster | None: if not self._data["cluster"]: return None return await Cluster.with_id(requester=self._requester, object_id=self._data["cluster"]["id"]) - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def hostprovider(self: Self) -> HostProvider: return await HostProvider.with_id(requester=self._requester, object_id=self._data["hostprovider"]["id"]) diff --git a/poetry.lock b/poetry.lock index 48ef0aa..a4a080f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,22 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "asyncstdlib" +version = "3.13.0" +description = "The missing async toolbox" +optional = false +python-versions = "~=3.8" +files = [ + {file = "asyncstdlib-3.13.0-py3-none-any.whl", hash = "sha256:60e097c19e815f3c419a77426cf6c3653aebcb766544d631d5ce6128d0851ae8"}, + {file = "asyncstdlib-3.13.0.tar.gz", hash = "sha256:f2a6ffb44f118233bb99bef50861d6f64c432decbdcc4c2cb93b3fff40d1b533"}, +] + +[package.extras] +doc = ["sphinx", "sphinxcontrib-trio"] +test = ["black", "coverage", "flake8", "flake8-2020", "flake8-bugbear", "mypy", "pytest", "pytest-cov"] +typetest = ["mypy", "pyright", "typing-extensions"] + [[package]] name = "certifi" version = "2024.8.30" @@ -271,4 +287,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "20136fad059dd6f087334eea57612f368edbe485fcd2874666338000b4859d1b" +content-hash = "c14d12ccbcd8910aed151decce58a67d6f4798d9a9c6b7c086be12ca2c935587" diff --git a/pyproject.toml b/pyproject.toml index ce8b2c4..6cfadd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" httpx = "^0.27.2" +asyncstdlib = "^3.13.0" [tool.poetry.group.dev] optional = true diff --git a/tests/integration/test_misc.py b/tests/integration/test_misc.py new file mode 100644 index 0000000..13772ae --- /dev/null +++ b/tests/integration/test_misc.py @@ -0,0 +1,34 @@ +from typing import Self + +from asyncstdlib.functools import cached_property as async_cached_property +import pytest + +pytestmark = [pytest.mark.asyncio] + + +class Dummy: + def __init__(self: Self) -> None: + self.counter = 0 + + @async_cached_property + async def func(self: Self) -> int: + self.counter += 1 + + return self.counter + + +async def test_async_cached_property() -> None: + obj = Dummy() + assert "func" not in obj.__dict__, "`func` key should not be cached yet" + + res = await obj.func + assert res == 1 + assert "func" in obj.__dict__, "`func` key should be cached" + + res = await obj.func + assert res == 1, "Cached value must be used" + + delattr(obj, "func") + res = await obj.func + assert res == 2, "Expected to execute func() again, increasing the counter" + assert "func" in obj.__dict__