Repository: joblib/joblib

Issue:
Auto-added `.gitignore` in cache directories
I am quite sure that nobody wants to add cache to their version control. Therefore, many relevant tools like Mypy, Ruff, or, Pytest automatically adds (within the cache directory) a `.gitignore` file, as simple as:

```gitignore
# .cache/.gitignore
*
```

I guess this could also a feature proposal for the current project.

Anyway, thank you all for creating and maintaining it 👏🏻 ❤️

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/joblib/joblib/pull/1674?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib) Report
Attention: Patch coverage is `94.11765%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 70.16%. Comparing base [(`6066109`)](https://app.codecov.io/gh/joblib/joblib/commit/6066109bf0c2ea018aaf528aa1cff2cff6dca2cd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib) to head [(`eac852d`)](https://app.codecov.io/gh/joblib/joblib/commit/eac852dc57d0bbfc8b6d866ee7cc4856b6e9a403?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/joblib/joblib/pull/1674?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib) | Patch % | Lines |
|---|---|---|
| [joblib/disk.py](https://app.codecov.io/gh/joblib/joblib/pull/1674?src=pr&el=tree&filepath=joblib%2Fdisk.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib#diff-am9ibGliL2Rpc2sucHk=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/joblib/joblib/pull/1674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib) |

> :exclamation:  There is a different number of reports uploaded between BASE (6066109) and HEAD (eac852d). Click for more details.
> 
> <details><summary>HEAD has 26 uploads less than BASE</summary>
>
>| Flag | BASE (6066109) | HEAD (eac852d) |
>|------|------|------|
>||27|1|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #1674       +/-   ##
===========================================
- Coverage   95.72%   70.16%   -25.56%     
===========================================
  Files          46       46               
  Lines        7829     7844       +15     
===========================================
- Hits         7494     5504     -1990     
- Misses        335     2340     +2005     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/joblib/joblib/pull/1674?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=joblib).

StefanieSenger: Thank you for reviewing and taking care of absolute paths, @tomMoral. I had not been aware that you could pass them.

I have committed your suggestions.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index efe8504..77f45bf 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -40,6 +40,9 @@ In development
   the warnings that are emitted in this case.
   https://github.com/joblib/joblib/pull/1681
 
+- The Memory object now automatically creates a .gitignore file in its cache directory,
+  instructing git to ignore the entire folder.
+  https://github.com/joblib/joblib/pull/1674
 
 Release 1.4.2 -- 2024/05/02
 ---------------------------
diff --git a/doc/memory.rst b/doc/memory.rst
index a692f2f..9a4760c 100644
--- a/doc/memory.rst
+++ b/doc/memory.rst
@@ -15,7 +15,7 @@ On demand recomputing: the `Memory` class
 Use case
 --------
 
-The :class:`~joblib.Memory` class defines a context for lazy evaluation of
+The :class:`~joblib.Memory` class defines a context for lazy evaluation of a
 function, by putting the results in a store, by default using a disk, and not
 re-running the function twice for the same arguments.
 
@@ -28,12 +28,12 @@ A simple example:
 
   First, define the cache directory::
 
-    >>> cachedir = 'your_cache_location_directory'
+    >>> location = 'your_cache_location_directory'
 
   Then, instantiate a memory context that uses this cache directory::
 
     >>> from joblib import Memory
-    >>> memory = Memory(cachedir, verbose=0)
+    >>> memory = Memory(location, verbose=0)
 
   After these initial steps, just decorate a function to cache its output in
   this context::
@@ -131,8 +131,8 @@ Using memmapping
 Memmapping (memory mapping) speeds up cache looking when reloading large numpy
 arrays::
 
-    >>> cachedir2 = 'your_cachedir2_location'
-    >>> memory2 = Memory(cachedir2, mmap_mode='r')
+    >>> location2 = 'your_2nd_cache_location_directory'
+    >>> memory2 = Memory(location2, mmap_mode='r')
     >>> square = memory2.cache(np.square)
     >>> a = np.vander(np.arange(3)).astype(float)
     >>> square(a)
@@ -521,8 +521,8 @@ without actually needing to call the function itself::
 
     >>> import shutil
     >>> try:
-    ...     shutil.rmtree(cachedir)
-    ...     shutil.rmtree(cachedir2)
+    ...     shutil.rmtree(location)
+    ...     shutil.rmtree(location2)
     ... except OSError:
     ...     pass  # this can sometimes fail under Windows
 
diff --git a/joblib/__init__.py b/joblib/__init__.py
index 3ffe33a..93d2800 100644
--- a/joblib/__init__.py
+++ b/joblib/__init__.py
@@ -56,8 +56,8 @@ Main features
    computation to disk and rerun it only if necessary::
 
       >>> from joblib import Memory
-      >>> cachedir = 'your_cache_dir_goes_here'
-      >>> mem = Memory(cachedir)
+      >>> location = 'your_cache_dir_goes_here'
+      >>> mem = Memory(location, verbose=1)
       >>> import numpy as np
       >>> a = np.vander(np.arange(3)).astype(float)
       >>> square = mem.cache(np.square)
@@ -86,7 +86,7 @@ Main features
    *joblib.dump* & *joblib.load* ).
 
 ..
-    >>> import shutil ; shutil.rmtree(cachedir)
+    >>> import shutil ; shutil.rmtree(location)
 
 """
 
diff --git a/joblib/_store_backends.py b/joblib/_store_backends.py
index c94e67c..c822f0f 100644
--- a/joblib/_store_backends.py
+++ b/joblib/_store_backends.py
@@ -456,6 +456,21 @@ class FileSystemStoreBackend(StoreBackendBase, StoreBackendMixin):
         if not os.path.exists(self.location):
             mkdirp(self.location)
 
+        # Automatically add `.gitignore` file to the cache folder.
+        # XXX: the condition is necessary because in `Memory.__init__`, the user
+        # passed `location` param is modified to be either `{location}` or
+        # `{location}/joblib` depending on input type (`pathlib.Path` vs `str`).
+        # The proper resolution of this inconsistency is tracked in:
+        # https://github.com/joblib/joblib/issues/1684
+        cache_directory = (
+            os.path.dirname(location)
+            if os.path.dirname(location) and os.path.basename(location) == "joblib"
+            else location
+        )
+        with open(os.path.join(cache_directory, ".gitignore"), "w") as file:
+            file.write("# Created by joblib automatically.\n")
+            file.write("*\n")
+
         # item can be stored compressed for faster I/O
         self.compress = backend_options.get("compress", False)
 
diff --git a/joblib/disk.py b/joblib/disk.py
index f48713a..61222e2 100644
--- a/joblib/disk.py
+++ b/joblib/disk.py
@@ -78,7 +78,7 @@ def rm_subdirs(path, onerror=None):
     If onerror is set, it is called to handle the error with arguments (func,
     path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
     path is the argument to that function that caused it to fail; and
-    exc_info is a tuple returned by sys.exc_info().  If onerror is None,
+    exc_info is a tuple returned by sys.exc_info(). If onerror is None,
     an exception is raised.
     """
 
diff --git a/joblib/memory.py b/joblib/memory.py
index 97656f5..aff91c9 100644
--- a/joblib/memory.py
+++ b/joblib/memory.py
@@ -977,9 +977,8 @@ class Memory(Logger):
         the Memory object is completely transparent. This option
         replaces cachedir since version 0.12.
 
-    backend: str, optional
+    backend: str, optional, default='local'
         Type of store backend for reading/writing cache files.
-        Default: 'local'.
         The 'local' backend is using regular filesystem operations to
         manipulate data (open, mv, etc) in the backend.
 


Test patch:
diff --git a/joblib/test/test_memory.py b/joblib/test/test_memory.py
index b8ceb94..985593b 100644
--- a/joblib/test/test_memory.py
+++ b/joblib/test/test_memory.py
@@ -1262,7 +1262,10 @@ def test_instanciate_store_backend_with_pathlib_path():
     # Instantiate a FileSystemStoreBackend using a pathlib.Path object
     path = pathlib.Path("some_folder")
     backend_obj = _store_backend_factory("local", path)
-    assert backend_obj.location == "some_folder"
+    try:
+        assert backend_obj.location == "some_folder"
+    finally:  # remove cache folder after test
+        shutil.rmtree("some_folder", ignore_errors=True)
 
 
 def test_filesystem_store_backend_repr(tmpdir):
@@ -1538,3 +1541,33 @@ class TestMemorizedFunc:
         assert isinstance(meta, dict), (
             "Metadata are not returned by MemorizedFunc.call."
         )
+
+
+@with_numpy
+@parametrize(
+    "location",
+    [
+        "test_cache_dir",
+        pathlib.Path("test_cache_dir"),
+        pathlib.Path("test_cache_dir").resolve(),
+    ],
+)
+def test_memory_creates_gitignore(location):
+    """Test that using the memory object automatically creates a `.gitignore` file
+    within the new cache directory."""
+
+    mem = Memory(location)
+    arr = np.asarray([[1, 2, 3], [4, 5, 6]])
+    costly_operation = mem.cache(np.square)
+    costly_operation(arr)
+
+    location = pathlib.Path(location)
+
+    try:
+        path_to_gitignore_file = os.path.join(location, ".gitignore")
+        gitignore_file_content = "# Created by joblib automatically.\n*\n"
+        with open(path_to_gitignore_file) as f:
+            assert gitignore_file_content == f.read()
+
+    finally:  # remove cache folder after test
+        shutil.rmtree(location, ignore_errors=True)