Repository: cevans87/atools

Issue:
Remove `memoize.set_default_db_path` function
The `memoize.set_default_db_path` function is difficult to use correctly as is. It requires that you call the default before anyone using the decorator actually applies it. AKA, you have to call the function before importing any code that applies the memoize decorator. This is an unnatural way of doing things in Python and should be improved.

Instead, users should take advantage of `memoize(db=my_db_path)`. The argument doesn't need to be applied everywhere. Users can create their own package-specific memoize decorator with alternate default arguments very easily. For example

```python3
# file: my_persistent_memoize.py
from atools import memoize
from pathlib import Path as _Path

memoize_db = memoize(db=_Path.home() / '.my_alternate_db_location')
```
And now they can use this new memoize_db in their package.

```python3
# file: foo.py
from my_persistent_memoize import memoize_db

@memoize_db
async def foobar(): ...
```

Discussion:

Patch:
diff --git a/README.md b/README.md
index 30b3966..04e017b 100644
--- a/README.md
+++ b/README.md
@@ -210,7 +210,7 @@ Python 3.6+ decorators including
 
         - Values can persist to disk and be reloaded when memoize is initialized again.
 
-            @memoize(db=True)
+            @memoize(db_path=Path.home() / '.memoize')
             def foo(a) -> Any: ...
 
             foo(1)  # Function actually called. Result cached.
@@ -219,34 +219,6 @@ Python 3.6+ decorators including
 
             foo(1)  # Function not called. Cached result returned.
 
-        - Be careful with 'db' and memoize values that don't hash consistently upon process restart.
-
-            class Foo:
-                @classmethod
-                @memoize(db=True)
-                def bar(cls, a) -> Any: ...
-
-            Foo.bar(1)  # Function actually called. Result cached.
-            Foo.bar(1)  # Function not called. Cached result returned.
-
-            # Process is restarted. Upon restart, the state of the memoize decorator is reloaded.
-
-            # Hash value of 'cls', is now different.
-            Foo.bar(1)  # Function actually called. Result cached.
-
-            # You can create a consistent hash key to avoid this.
-            class Foo:
-                @classmethod
-                @memoize(db=True, keygen=lambda cls, a: (f'{cls.__package__}:{cls.__name__}', a))
-                def bar(cls, a) -> Any: ...
-
-        - Alternative location of 'db' can also be given as pathlib.Path or str.
-            @memoize(db=Path.home() / 'foo_memoize')
-            def foo() -> Any: ...
-
-            @memoize(db='~/bar_memoize')
-            def bar() -> Any: ...
-
 ## rate
     Function decorator that rate limits the number of calls to function.
 
diff --git a/atools/_memoize_decorator.py b/atools/_memoize_decorator.py
index 4cc93e3..9412f92 100644
--- a/atools/_memoize_decorator.py
+++ b/atools/_memoize_decorator.py
@@ -533,7 +533,7 @@ class _Memoize:
 
         - Values can persist to disk and be reloaded when memoize is initialized again.
 
-            @memoize(db=True)
+            @memoize(db_path=Path.home() / '.memoize')
             def foo(a) -> Any: ...
 
             foo(1)  # Function actually called. Result cached.
@@ -541,53 +541,24 @@ class _Memoize:
             # Process is restarted. Upon restart, the state of the memoize decorator is reloaded.
 
             foo(1)  # Function not called. Cached result returned.
-
-        - Be careful with 'db' and memoize values that don't hash consistently upon process restart.
-
-            class Foo:
-                @classmethod
-                @memoize(db=True)
-                def bar(cls, a) -> Any: ...
-
-            Foo.bar(1)  # Function actually called. Result cached.
-            Foo.bar(1)  # Function not called. Cached result returned.
-
-            # Process is restarted. Upon restart, the state of the memoize decorator is reloaded.
-
-            # Hash value of 'cls', is now different.
-            Foo.bar(1)  # Function actually called. Result cached.
-
-            # You can create a consistent hash key to avoid this.
-            class Foo:
-                @classmethod
-                @memoize(db=True, keygen=lambda cls, a: (f'{cls.__package__}:{cls.__name__}', a))
-                def bar(cls, a) -> Any: ...
-
-        - Alternative location of 'db' can also be given as pathlib.Path or str.
-            @memoize(db=Path.home() / 'foo_memoize')
-            def foo() -> Any: ...
-
-            @memoize(db='~/bar_memoize')
-            def bar() -> Any: ...
     """
 
-    _default_db_path = Path.home() / '.memoize'
     _all_decorators = WeakSet()
 
     @staticmethod
     def __call__(
             _decoratee: Optional[Decoratee] = None,
             *,
-            db: Union[bool, Path, str] = False,
+            db_path: Optional[Path] = None,
             duration: Optional[Union[int, float, timedelta]] = None,
             keygen: Optional[Keygen] = None,
             size: Optional[int] = None,
     ):
         if _decoratee is None:
-            return partial(memoize, db=db, duration=duration, keygen=keygen, size=size)
+            return partial(memoize, db_path=db_path, duration=duration, keygen=keygen, size=size)
 
         if inspect.isclass(_decoratee):
-            assert not db, 'Class memoization not allowed with db.'
+            assert db_path is None, 'Class memoization not allowed with db.'
 
             class WrappedMeta(type(_decoratee)):
                 # noinspection PyMethodParameters
@@ -600,14 +571,7 @@ class _Memoize:
 
             return type(_decoratee.__name__, (Wrapped,), {'__doc__': _decoratee.__doc__})
 
-        if isinstance(db, (str, Path)):
-            db = connect(f'{db}')
-        elif isinstance(db, bool):
-            if db:
-                db = connect(f'{_Memoize._default_db_path}')
-            else:
-                db = None
-
+        db = connect(f'{db_path}') if db_path is not None else None
         duration = timedelta(seconds=duration) if isinstance(duration, (int, float)) else duration
         assert (duration is None) or (duration.total_seconds() > 0)
         assert (size is None) or (size > 0)
@@ -640,9 +604,5 @@ class _Memoize:
         for decorator in _Memoize._all_decorators:
             decorator.memoize.reset()
 
-    @staticmethod
-    def set_default_db_path(path: Union[Path, str]) -> None:
-        _Memoize._default_db_path = Path(f'{path}')
-
 
 memoize = _Memoize()
diff --git a/setup.py b/setup.py
index f27ee37..dd5f7eb 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
 
 setup(
     name='atools',
-    version='0.11.0',
+    version='0.12.0',
     packages=find_packages(),
     python_requires='>=3.6',
     url='https://github.com/cevans87/atools',


Test patch:
diff --git a/test/test_memoize_decorator.py b/test/test_memoize_decorator.py
index 5172e1b..2ccc0ac 100644
--- a/test/test_memoize_decorator.py
+++ b/test/test_memoize_decorator.py
@@ -6,18 +6,15 @@ import atools._memoize_decorator as test_module
 from datetime import timedelta
 from pathlib import Path, PosixPath
 import pytest
-from sqlite3 import connect, Connection
+from sqlite3 import connect
 from tempfile import NamedTemporaryFile
-from typing import Callable, FrozenSet, Hashable, Iterable, Tuple, Union
-from unittest.mock import call, MagicMock, patch, PropertyMock
+from typing import Callable, FrozenSet, Hashable, Iterable, Tuple
+from unittest.mock import call, MagicMock, patch
 from weakref import ref
 
 
-def get_table_len(db) -> int:
-    if isinstance(db, bool):
-        db = connect(f'{memoize._default_db_path}')
-    elif isinstance(db, (Path, str)):
-        db = connect(f'{db}')
+def get_table_len(db_path: Path) -> int:
+    db = connect(f'{db_path}')
     # noinspection SqlResolve
     return len(db.execute("SELECT name FROM sqlite_master where type='table'").fetchall())
 
@@ -28,17 +25,10 @@ def async_lock() -> MagicMock:
         yield async_lock
 
 
-@pytest.fixture(params=[bool, connect, Path, str])
-def db(request) -> Union[bool, Connection, Path, str]:
+@pytest.fixture
+def db_path() -> Path:
     with NamedTemporaryFile() as f:
-        if request.param is bool:
-            with patch.object(
-                    type(memoize), '_default_db_path', new_callable=PropertyMock
-            ) as default_db_path:
-                default_db_path.return_value = Path(f.name)
-                yield True
-        else:
-            yield request.param(f.name)
+        yield Path(f.name)
 
 
 @pytest.fixture
@@ -629,29 +619,29 @@ async def test_keygen_awaits_awaitable_parts() -> None:
     )
 
 
-def test_db_creates_table_for_each_decorator(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_creates_table_for_each_decorator(db_path: Path) -> None:
     
-    assert get_table_len(db) == 0
+    assert get_table_len(db_path) == 0
 
-    @memoize(db=db)
+    @memoize(db_path=db_path)
     def foo() -> None:
         ...
 
-    assert get_table_len(db) == 1
+    assert get_table_len(db_path) == 1
 
-    @memoize(db=db)
+    @memoize(db_path=db_path)
     def bar() -> None:
         ...
 
-    assert get_table_len(db) == 2
+    assert get_table_len(db_path) == 2
 
 
-def test_db_reloads_values_from_disk(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_reloads_values_from_disk(db_path: Path) -> None:
     body = MagicMock()
     
     def foo() -> None:
 
-        @memoize(db=db)
+        @memoize(db_path=db_path)
         def foo_inner() -> None:
             body()
 
@@ -663,11 +653,11 @@ def test_db_reloads_values_from_disk(db: Union[bool, Connection, Path, str]) ->
     assert body.call_count == 1
 
 
-def test_reset_removes_values_on_disk(db: Union[bool, Connection, Path, str]) -> None:
+def test_reset_removes_values_on_disk(db_path: Path) -> None:
     body = MagicMock()
 
     def foo() -> None:
-        @memoize(db=db)
+        @memoize(db_path=db_path)
         def foo_inner() -> None:
             body()
 
@@ -680,11 +670,11 @@ def test_reset_removes_values_on_disk(db: Union[bool, Connection, Path, str]) ->
     assert body.call_count == 2
 
 
-def test_db_expires_memo(db: Union[bool, Connection, Path, str], time: MagicMock) -> None:
+def test_db_expires_memo(db_path: Path, time: MagicMock) -> None:
     body = MagicMock()
 
     def foo() -> None:
-        @memoize(db=db, duration=timedelta(days=1))
+        @memoize(db_path=db_path, duration=timedelta(days=1))
         def foo_inner() -> None:
             body()
 
@@ -698,11 +688,11 @@ def test_db_expires_memo(db: Union[bool, Connection, Path, str], time: MagicMock
     assert body.call_count == 2
 
 
-def test_db_memoizes_multiple_values(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_memoizes_multiple_values(db_path: Path) -> None:
     body = MagicMock()
 
     def get_foo() -> Callable[[int], None]:
-        @memoize(db=db)
+        @memoize(db_path=db_path)
         def _foo(_i: int) -> None:
             body(_i)
 
@@ -717,11 +707,11 @@ def test_db_memoizes_multiple_values(db: Union[bool, Connection, Path, str]) ->
     assert len(foo.memoize) == 10
 
 
-def test_db_with_size_expires_lru(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_with_size_expires_lru(db_path: Path) -> None:
     body = MagicMock()
 
     def foo(it: Iterable[int]) -> None:
-        @memoize(db=db, size=5)
+        @memoize(db_path=db_path, size=5)
         def foo_inner(_i: int) -> None:
             body(_i)
 
@@ -735,13 +725,13 @@ def test_db_with_size_expires_lru(db: Union[bool, Connection, Path, str]) -> Non
     
     
 def test_db_with_duration_expires_stale_values(
-        db: Union[bool, Connection, Path, str],
+        db_path: Path,
         time: MagicMock,
 ) -> None:
     body = MagicMock()
 
     def foo(it: Iterable[int]) -> None:
-        @memoize(db=db, duration=timedelta(hours=1))
+        @memoize(db_path=db_path, duration=timedelta(hours=1))
         def foo_inner(_i: int) -> None:
             body(_i)
 
@@ -761,11 +751,11 @@ def test_db_with_duration_expires_stale_values(
     assert body.call_count == 20
 
 
-def test_db_memoizes_frozenset(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_memoizes_frozenset(db_path: Path) -> None:
     body = MagicMock()
 
     def foo() -> FrozenSet[int]:
-        @memoize(db=db)
+        @memoize(db_path=db_path)
         def foo_inner() -> FrozenSet[int]:
             body()
             return frozenset({1, 2, 3})
@@ -832,11 +822,11 @@ async def test_async_reset_call_resets_call() -> None:
     assert body.call_count == 11
 
 
-def test_reset_call_with_db_resets_call(db: Union[bool, Connection, Path, str]) -> None:
+def test_reset_call_with_db_resets_call(db_path: Path) -> None:
     body = MagicMock()
 
     def get_foo() -> Callable[[int], None]:
-        @memoize(db=db)
+        @memoize(db_path=db_path)
         def foo(_i: int) -> None:
             body(_i)
 
@@ -854,19 +844,6 @@ def test_reset_call_with_db_resets_call(db: Union[bool, Connection, Path, str])
     assert body.call_count == 11
 
 
-def test_set_default_db_path_uses_given_path() -> None:
-    with NamedTemporaryFile() as f, patch.object(type(memoize), '_default_db_path'):
-        memoize.set_default_db_path(f.name)
-
-        assert get_table_len(f.name) == 0
-
-        @memoize(db=True)
-        def foo() -> None:
-            ...
-
-        assert get_table_len(f.name) == 1
-
-
 @pytest.mark.asyncio
 async def test_async_keygen_can_return_non_tuple() -> None:
     body = MagicMock()
@@ -883,9 +860,9 @@ async def test_async_keygen_can_return_non_tuple() -> None:
     assert body.call_count == 1
 
 
-def test_db_can_return_type_of_callers_globals(db: Union[bool, Connection, Path, str]) -> None:
+def test_db_can_return_type_of_callers_globals(db_path: Path) -> None:
 
-    @memoize(db=db)
+    @memoize(db_path=db_path)
     def foo():
         return PosixPath.cwd()