Repository: aio-libs/aiocache

Issue:
Ensure that underlying value of enum key is used by BaseCache._build_key() across Python versions
Key builders and tests currently cast key values (notably `Enum` values) to `str` to maintain compatibility with Python 3.11 and older versions.  ~~This requirement can be eliminated by using f-strings rather than `str.__format__`.~~

EDIT: Whoops, that erroneous statement was based on testing in the wrong python environment.  Instead of using `str()`, perhaps we should use a simple guard that check for an enum key.  Something like:

```
def ensure_key(key):
    if isinstance(key, Enum):
        return key.value
    else:
        return key
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
> Merging [#635](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) (f257305) into [master](https://codecov.io/gh/aio-libs/aiocache/commit/076ac895df6b5758bd97b5d46b73b3fa7ea622a6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) (076ac89) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head f257305 differs from pull request most recent head c1ae586. Consider uploading reports for the commit c1ae586 to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/aio-libs/aiocache/pull/635/graphs/tree.svg?width=650&height=150&src=pr&token=IjVDGI14Tx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)

```diff
@@            Coverage Diff             @@
##           master     #635      +/-   ##
==========================================
+ Coverage   99.66%   99.67%   +0.01%     
==========================================
  Files          36       36              
  Lines        3557     3713     +156     
==========================================
+ Hits         3545     3701     +156     
  Misses         12       12              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit | `99.67% <100.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Coverage Δ | |
|---|---|---|
| [aiocache/backends/redis.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-YWlvY2FjaGUvYmFja2VuZHMvcmVkaXMucHk=) | `99.16% <100.00%> (ø)` | |
| [aiocache/base.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-YWlvY2FjaGUvYmFzZS5weQ==) | `99.27% <100.00%> (+0.03%)` | :arrow_up: |
| [aiocache/decorators.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-YWlvY2FjaGUvZGVjb3JhdG9ycy5weQ==) | `100.00% <100.00%> (ø)` | |
| [aiocache/lock.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-YWlvY2FjaGUvbG9jay5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/acceptance/test\_decorators.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvYWNjZXB0YW5jZS90ZXN0X2RlY29yYXRvcnMucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/acceptance/test\_lock.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvYWNjZXB0YW5jZS90ZXN0X2xvY2sucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/performance/test\_footprint.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvcGVyZm9ybWFuY2UvdGVzdF9mb290cHJpbnQucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/ut/backends/test\_memcached.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvdXQvYmFja2VuZHMvdGVzdF9tZW1jYWNoZWQucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/ut/backends/test\_redis.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvdXQvYmFja2VuZHMvdGVzdF9yZWRpcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/ut/test\_base.py](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvdXQvdGVzdF9iYXNlLnB5) | `100.00% <100.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs). Last update [5426793...c1ae586](https://codecov.io/gh/aio-libs/aiocache/pull/635?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

pshafer-als: > Ah, I think you branched off your other PR. Unfortunately, we always squash commit, so that's messed up the diff/conflicts.
> 
> If you can rename it to `_ensure_key()`, as I'd like to replace it in the next release, and clean up the merge, that'd be great. We can push a new release once this is merged.

No problem.  Will do that now.
pshafer-als: > If you can rename it to `_ensure_key()`, as I'd like to replace it in the next release, and clean up the merge, that'd be great. We can push a new release once this is merged.

Done.  

Thank you for nudging these tests along and for all your help.  It's been a pleasure working with you on these PRs.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c584b9c..0e66ae1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,8 @@
 * Add ``async with`` support to ``BaseCache``.
 * Remove deprecated ``loop`` parameters.
 * Remove deprecated ``cache`` parameter from ``create()``.
-* Use ``str()`` in ``_build_key()`` to ensure consistency of enum keys between different Python versions (if using enum keys, upgrading to 0.12 may invalidate existing caches due to key values changing).
+* Use ``base._ensure_key()`` in ``_build_key()`` to ensure consistency of enum
+keys between different Python versions. [#633](https://github.com/aio-libs/aiocache/issues/633) -- Padraic Shafer
 * Improved support for ``build_key(key, namespace)`` [#569](https://github.com/aio-libs/aiocache/issues/569) - Padraic Shafer
     * `BaseCache.build_key` uses `namespace` argument if provided,
     otherwise it uses `self.namespace`.
diff --git a/aiocache/backends/redis.py b/aiocache/backends/redis.py
index d569ec2..407efec 100644
--- a/aiocache/backends/redis.py
+++ b/aiocache/backends/redis.py
@@ -4,7 +4,7 @@ import warnings
 import redis.asyncio as redis
 from redis.exceptions import ResponseError as IncrbyException
 
-from aiocache.base import BaseCache
+from aiocache.base import BaseCache, _ensure_key
 from aiocache.serializers import JsonSerializer
 
 
@@ -219,11 +219,12 @@ class RedisCache(RedisBackend):
         return options
 
     def _build_key(self, key, namespace=None):
-        # TODO(PY311): Remove str()
         if namespace is not None:
-            return "{}{}{}".format(namespace, ":" if namespace else "", str(key))
+            return "{}{}{}".format(
+                namespace, ":" if namespace else "", _ensure_key(key))
         if self.namespace is not None:
-            return "{}{}{}".format(self.namespace, ":" if self.namespace else "", str(key))
+            return "{}{}{}".format(
+                self.namespace, ":" if self.namespace else "", _ensure_key(key))
         return key
 
     def __repr__(self):  # pragma: no cover
diff --git a/aiocache/base.py b/aiocache/base.py
index bc4df81..54e9d18 100644
--- a/aiocache/base.py
+++ b/aiocache/base.py
@@ -3,6 +3,7 @@ import functools
 import logging
 import os
 import time
+from enum import Enum
 from types import TracebackType
 from typing import Callable, Optional, Set, Type
 
@@ -498,14 +499,10 @@ class BaseCache:
         pass
 
     def _build_key(self, key, namespace=None):
-        # TODO(PY311): Remove str() calls.
-        # str() is needed to ensure consistent results when using enums between
-        # Python 3.11+ and older releases due to changed __format__() method:
-        # https://docs.python.org/3/whatsnew/3.11.html#enum
         if namespace is not None:
-            return "{}{}".format(namespace, str(key))
+            return "{}{}".format(namespace, _ensure_key(key))
         if self.namespace is not None:
-            return "{}{}".format(self.namespace, str(key))
+            return "{}{}".format(self.namespace, _ensure_key(key))
         return key
 
     def _get_ttl(self, ttl):
@@ -553,5 +550,12 @@ class _Conn:
         return _do_inject_conn
 
 
+def _ensure_key(key):
+    if isinstance(key, Enum):
+        return key.value
+    else:
+        return key
+
+
 for cmd in API.CMDS:
     setattr(_Conn, cmd.__name__, _Conn._inject_conn(cmd.__name__))


Test patch:
diff --git a/tests/acceptance/test_decorators.py b/tests/acceptance/test_decorators.py
index 13783db..2f755a1 100644
--- a/tests/acceptance/test_decorators.py
+++ b/tests/acceptance/test_decorators.py
@@ -5,6 +5,7 @@ from unittest import mock
 import pytest
 
 from aiocache import cached, cached_stampede, multi_cached
+from aiocache.base import _ensure_key
 from ..utils import Keys
 
 
@@ -137,17 +138,16 @@ class TestMultiCachedDecorator:
         assert await cache.exists(Keys.KEY) is True
 
     async def test_multi_cached_key_builder(self, cache):
-        # TODO(PY311): Remove str() calls
         def build_key(key, f, self, keys, market="ES"):
-            return "{}_{}_{}".format(f.__name__, str(key), market)
+            return "{}_{}_{}".format(f.__name__, _ensure_key(key), market)
 
         @multi_cached(keys_from_attr="keys", key_builder=build_key)
         async def fn(self, keys, market="ES"):
             return {Keys.KEY: 1, Keys.KEY_1: 2}
 
         await fn("self", keys=[Keys.KEY, Keys.KEY_1])
-        assert await cache.exists("fn_" + str(Keys.KEY) + "_ES") is True
-        assert await cache.exists("fn_" + str(Keys.KEY_1) + "_ES") is True
+        assert await cache.exists("fn_" + _ensure_key(Keys.KEY) + "_ES") is True
+        assert await cache.exists("fn_" + _ensure_key(Keys.KEY_1) + "_ES") is True
 
     async def test_fn_with_args(self, cache):
         @multi_cached("keys")
diff --git a/tests/ut/backends/test_memcached.py b/tests/ut/backends/test_memcached.py
index 5f856b4..7e01161 100644
--- a/tests/ut/backends/test_memcached.py
+++ b/tests/ut/backends/test_memcached.py
@@ -4,7 +4,7 @@ import aiomcache
 import pytest
 
 from aiocache.backends.memcached import MemcachedBackend, MemcachedCache
-from aiocache.base import BaseCache
+from aiocache.base import BaseCache, _ensure_key
 from aiocache.serializers import JsonSerializer
 from ...utils import Keys
 
@@ -249,8 +249,7 @@ class TestMemcachedCache:
 
     @pytest.mark.parametrize(
         "namespace, expected",
-        # TODO(PY311): Remove str()
-        ([None, "test" + str(Keys.KEY)], ["", str(Keys.KEY)], ["my_ns", "my_ns" + str(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
+        ([None, "test" + _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["my_ns", "my_ns" + _ensure_key(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
     )
     def test_build_key_bytes(self, set_test_namespace, memcached_cache, namespace, expected):
         assert memcached_cache.build_key(Keys.KEY, namespace=namespace) == expected.encode()
diff --git a/tests/ut/backends/test_redis.py b/tests/ut/backends/test_redis.py
index dd41522..2470584 100644
--- a/tests/ut/backends/test_redis.py
+++ b/tests/ut/backends/test_redis.py
@@ -5,7 +5,7 @@ from redis.asyncio.client import Pipeline
 from redis.exceptions import ResponseError
 
 from aiocache.backends.redis import RedisBackend, RedisCache
-from aiocache.base import BaseCache
+from aiocache.base import BaseCache, _ensure_key
 from aiocache.serializers import JsonSerializer
 from ...utils import Keys
 
@@ -253,8 +253,7 @@ class TestRedisCache:
 
     @pytest.mark.parametrize(
         "namespace, expected",
-        # TODO(PY311): Remove str()
-        ([None, "test:" + str(Keys.KEY)], ["", str(Keys.KEY)], ["my_ns", "my_ns:" + str(Keys.KEY)]),  # noqa: B950
+        ([None, "test:" + _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["my_ns", "my_ns:" + _ensure_key(Keys.KEY)]),  # noqa: B950
     )
     def test_build_key_double_dot(self, set_test_namespace, redis_cache, namespace, expected):
         assert redis_cache.build_key(Keys.KEY, namespace=namespace) == expected
diff --git a/tests/ut/test_base.py b/tests/ut/test_base.py
index 09b36dd..569b524 100644
--- a/tests/ut/test_base.py
+++ b/tests/ut/test_base.py
@@ -4,7 +4,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch
 
 import pytest
 
-from aiocache.base import API, BaseCache, _Conn
+from aiocache.base import API, BaseCache, _Conn, _ensure_key
 from ..utils import Keys
 
 
@@ -205,8 +205,7 @@ class TestBaseCache:
 
     @pytest.mark.parametrize(
         "namespace, expected",
-        # TODO(PY311): Remove str()
-        ([None, "test" + str(Keys.KEY)], ["", str(Keys.KEY)], ["my_ns", "my_ns" + str(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
+        ([None, "test" + _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["my_ns", "my_ns" + _ensure_key(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
     )
     def test_build_key(self, set_test_namespace, base_cache, namespace, expected):
         assert base_cache.build_key(Keys.KEY, namespace=namespace) == expected
@@ -219,18 +218,16 @@ class TestBaseCache:
     def alt_base_cache(self, init_namespace="test"):
         """Custom key_builder for cache"""
         def build_key(key, namespace=None):
-            # TODO(PY311): Remove str()
             ns = namespace if namespace is not None else ""
             sep = ":" if namespace else ""
-            return f"{ns}{sep}{str(key)}"
+            return f"{ns}{sep}{_ensure_key(key)}"
 
         cache = BaseCache(key_builder=build_key, namespace=init_namespace)
         return cache
 
     @pytest.mark.parametrize(
         "namespace, expected",
-        # TODO(PY311): Remove str()
-        ([None, str(Keys.KEY)], ["", str(Keys.KEY)], ["my_ns", "my_ns:" + str(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
+        ([None, _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["my_ns", "my_ns:" + _ensure_key(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
     )
     def test_alt_build_key_override_namespace(self, alt_base_cache, namespace, expected):
         """Custom key_builder overrides namespace of cache"""
@@ -239,8 +236,7 @@ class TestBaseCache:
 
     @pytest.mark.parametrize(
         "init_namespace, expected",
-        # TODO(PY311): Remove str()
-        ([None, str(Keys.KEY)], ["", str(Keys.KEY)], ["test", "test:" + str(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
+        ([None, _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["test", "test:" + _ensure_key(Keys.KEY)]),  # type: ignore[attr-defined]  # noqa: B950
     )
     async def test_alt_build_key_default_namespace(
             self, init_namespace, alt_base_cache, expected):