Repository: joblib/joblib

Issue:
FileSystemStoreBackend.__repr__ should not return a string
Something that tripped me up when working on https://github.com/joblib/joblib/pull/746, is that `FileSystemStoreBackend.__repr__` makes it look like a string. I could not find a good reason for this. Do you remember something about this @aabadie?

Here is a snippet to show what I mean:
```
In [1]: from joblib import Memory

In [2]: mem = Memory('/tmp/test')

In [3]: mem.store_backend
Out[3]: /tmp/test/joblib
```

When I was debugging I lost at least one hour until I thought of doing `type(mem.store_backend)` ...

Also slightly less important but:
```py
from joblib._store_backends import FileSystemStoreBackend
backend = FileSystemStoreBackend()
backend  # Exception in __repr__ AttributeError: 'FileSystemStoreBackend' object has no attribute 'location'
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=h1) Report
> Merging [#765](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=desc) into [master](https://codecov.io/gh/joblib/joblib/commit/f64e1b8cf286c02d2782c6dd5bf7a42e0518c51d?src=pr&el=desc) will **decrease** coverage by `0.1%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/joblib/joblib/pull/765/graphs/tree.svg?width=650&token=gA6LF5DGTW&height=150&src=pr)](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #765      +/-   ##
==========================================
- Coverage   95.42%   95.31%   -0.11%     
==========================================
  Files          44       44              
  Lines        6224     6235      +11     
==========================================
+ Hits         5939     5943       +4     
- Misses        285      292       +7
```


| [Impacted Files](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [joblib/\_store\_backends.py](https://codecov.io/gh/joblib/joblib/pull/765/diff?src=pr&el=tree#diff-am9ibGliL19zdG9yZV9iYWNrZW5kcy5weQ==) | `90.57% <100%> (+0.62%)` | :arrow_up: |
| [joblib/test/test\_memory.py](https://codecov.io/gh/joblib/joblib/pull/765/diff?src=pr&el=tree#diff-am9ibGliL3Rlc3QvdGVzdF9tZW1vcnkucHk=) | `98.11% <100%> (+0.02%)` | :arrow_up: |
| [joblib/disk.py](https://codecov.io/gh/joblib/joblib/pull/765/diff?src=pr&el=tree#diff-am9ibGliL2Rpc2sucHk=) | `81.66% <0%> (-6.67%)` | :arrow_down: |
| [joblib/test/test\_dask.py](https://codecov.io/gh/joblib/joblib/pull/765/diff?src=pr&el=tree#diff-am9ibGliL3Rlc3QvdGVzdF9kYXNrLnB5) | `96.22% <0%> (-1.89%)` | :arrow_down: |
| [joblib/\_parallel\_backends.py](https://codecov.io/gh/joblib/joblib/pull/765/diff?src=pr&el=tree#diff-am9ibGliL19wYXJhbGxlbF9iYWNrZW5kcy5weQ==) | `95.6% <0%> (-0.41%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=footer). Last update [f64e1b8...bf5f24e](https://codecov.io/gh/joblib/joblib/pull/765?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

lesteve: Can you check that self.store_backend is not used in other classes `__repr__`. I seem to remember it is.
aabadie: > Can you check that self.store_backend is not used in other classes __repr__. I seem to remember it is.

I just did and you are right it was used in `MemorizedResult` and `MemorizedFunc` classes. It's fixed now.
aabadie: @ogrisel, is it ok to merge ?

Patch:
diff --git a/README.rst b/README.rst
index 472a5fd..1ed63ef 100644
--- a/README.rst
+++ b/README.rst
@@ -58,6 +58,8 @@ Dependencies
 - Joblib has an optional dependency on
   `python-lz4 <https://pypi.python.org/pypi/lz4>`_ as a faster alternative to
   zlib and gzip for compressed serialization.
+- Joblib has an optional dependency on psutil to mitigate memory leaks in
+  parallel worker processes.
 - Some examples require external dependencies such as pandas. See the
   instructions in the `Building the docs`_ section for details.
 
diff --git a/joblib/_store_backends.py b/joblib/_store_backends.py
index d48edcd..9196f0a 100644
--- a/joblib/_store_backends.py
+++ b/joblib/_store_backends.py
@@ -35,6 +35,8 @@ class StoreBackendBase(with_metaclass(ABCMeta)):
     """Helper Abstract Base Class which defines all methods that
        a StorageBackend must implement."""
 
+    location = None
+
     @abstractmethod
     def _open_item(self, f, mode):
         """Opens an item on the store and return a file-like object.
@@ -327,7 +329,8 @@ class StoreBackendMixin(object):
 
     def __repr__(self):
         """Printable representation of the store location."""
-        return self.location
+        return '{class_name}(location="{location}")'.format(
+            class_name=self.__class__.__name__, location=self.location)
 
 
 class FileSystemStoreBackend(StoreBackendBase, StoreBackendMixin):
diff --git a/joblib/memory.py b/joblib/memory.py
index 491238c..5ae6940 100644
--- a/joblib/memory.py
+++ b/joblib/memory.py
@@ -258,7 +258,7 @@ class MemorizedResult(Logger):
         return ('{class_name}(location="{location}", func="{func}", '
                 'args_id="{args_id}")'
                 .format(class_name=self.__class__.__name__,
-                        location=self.store_backend,
+                        location=self.store_backend.location,
                         func=self.func,
                         args_id=self.args_id
                         ))
@@ -769,9 +769,10 @@ class MemorizedFunc(Logger):
     # ------------------------------------------------------------------------
 
     def __repr__(self):
-        return ("{0}(func={1}, location={2})".format(self.__class__.__name__,
-                                                     self.func,
-                                                     self.store_backend,))
+        return '{class_name}(func={func}, location={location})'.format(
+            class_name=self.__class__.__name__,
+            func=self.func,
+            location=self.store_backend.location,)
 
 
 ###############################################################################
@@ -963,9 +964,10 @@ class Memory(Logger):
     # ------------------------------------------------------------------------
 
     def __repr__(self):
-        return '{0}(location={1})'.format(
-            self.__class__.__name__, (repr(None) if self.store_backend is None
-                                      else repr(self.store_backend)))
+        return '{class_name}(location={location})'.format(
+            class_name=self.__class__.__name__,
+            location=(None if self.store_backend is None
+                      else self.store_backend.location))
 
     def __getstate__(self):
         """ We don't store the timestamp when pickling, to avoid the hash


Test patch:
diff --git a/joblib/test/test_memory.py b/joblib/test/test_memory.py
index b3ac081..2f0c1b7 100644
--- a/joblib/test/test_memory.py
+++ b/joblib/test/test_memory.py
@@ -24,7 +24,7 @@ from joblib.memory import register_store_backend, _STORE_BACKENDS
 from joblib.memory import _build_func_identifier, _store_backend_factory
 from joblib.memory import JobLibCollisionWarning
 from joblib.parallel import Parallel, delayed
-from joblib._store_backends import StoreBackendBase
+from joblib._store_backends import StoreBackendBase, FileSystemStoreBackend
 from joblib.test.common import with_numpy, np
 from joblib.test.common import with_multiprocessing
 from joblib.testing import parametrize, raises, warns
@@ -989,6 +989,55 @@ def test_dummy_store_backend():
     assert isinstance(backend_obj, DummyStoreBackend)
 
 
+def test_filesystem_store_backend_repr(tmpdir):
+    # Verify string representation of a filesystem store backend.
+
+    repr_pattern = 'FileSystemStoreBackend(location="{location}")'
+    backend = FileSystemStoreBackend()
+    assert backend.location is None
+
+    repr(backend)  # Should not raise an exception
+
+    assert str(backend) == repr_pattern.format(location=None)
+
+    # backend location is passed explicitely via the configure method (called
+    # by the internal _store_backend_factory function)
+    backend.configure(tmpdir.strpath)
+
+    assert str(backend) == repr_pattern.format(location=tmpdir.strpath)
+
+    repr(backend)  # Should not raise an exception
+
+
+def test_memory_objects_repr(tmpdir):
+    # Verify printable reprs of MemorizedResult, MemorizedFunc and Memory.
+
+    def my_func(a, b):
+        return a + b
+
+    memory = Memory(location=tmpdir.strpath, verbose=0)
+    memorized_func = memory.cache(my_func)
+
+    memorized_func_repr = 'MemorizedFunc(func={func}, location={location})'
+
+    assert str(memorized_func) == memorized_func_repr.format(
+        func=my_func,
+        location=memory.store_backend.location)
+
+    memorized_result = memorized_func.call_and_shelve(42, 42)
+
+    memorized_result_repr = ('MemorizedResult(location="{location}", '
+                             'func="{func}", args_id="{args_id}")')
+
+    assert str(memorized_result) == memorized_result_repr.format(
+        location=memory.store_backend.location,
+        func=memorized_result.func_id,
+        args_id=memorized_result.args_id)
+
+    assert str(memory) == 'Memory(location={location})'.format(
+        location=memory.store_backend.location)
+
+
 def test_memorized_result_pickle(tmpdir):
     # Verify a MemoryResult object can be pickled/depickled. Non regression
     # test introduced following issue