Repository: fatiando/pooch

Issue:
Create a function to fetch a single file
**Description of the desired feature**

<!--
Please be as detailed as you can in your description. If possible, include an
example of how you would like to use this feature (even better if it's a code
example).
-->

Consider the following use case: 

> I'm a scientist/educator writing a notebook and I need to get data from this one file on the internet. I'm on Jupyter Lab, possibly on a remote server. I don't know the hash of the file and don't need to download many files, just this one. I want the file to be cached locally and not downloaded multiple times. After the first download, I want to set the hash so that I can make sure future runs of the notebook use the same version of the data. 

Right now, if you want to use Pooch for that you need to write this code:

```python
from unittest import mock

url = "https://www.ngdc.noaa.gov/mgg/ocean_age/data/2008/grids/age/age.3.6.nc.bz2"
remote_file = url.split("/")[-1]

fname = pooch.create(
    path=pooch.os_cache("meh"), 
    base_url="", 
    registry={remote_file: mock.ANY}, 
    urls={remote_file: url}
).fetch(remote_file)

print(pooch.file_hash(fname))
# Copy and paste the hash in the registry above for future runs.
```
There is a lot of repetition and feels like a hack. The same thing in fsspec would look like:

```python
# files is a not-yet-open OpenFile object. The "with" context actually opens it
with fsspec.open(url, mode="r") as f:
    # Read in the data
```

which is much more readable but it doesn't seem to cache automatically (requiring further setup) and doesn't have a hash checking mechanism (at least not that I could find in the docs).

**Proposal**

Add a function `pooch.retrieve` that would make the code above:

```python
fname = pooch.retrieve(url, hash=None)
# hash of downloaded file logged to screen and copy+pasted above for future runs
```

* Automatically cache to a `pooch.os_cache("pooch")` location without further setup.
* File names would be set to a short hash of the file content to make sure no overwriting happens. 
* The return is the file name only, keeping with the pooch way of not trying to do anything fancy. 
* Automatically printing out the hash makes it easier to download first then make sure future downloads are the same.
* Cache location and file name can be set by user if desired.
* Function takes `downloader` and `processor` arguments like `pooch.Pooch.fetch`.

This makes it easy to have all the features of Pooch for single file downloads in very little code.

The name `pooch.fetch` is nicer but it may cause confusion with `pooch.Pooch.fetch` so `retrieve` is a good alternative.

**Are you willing to help implement and maintain this feature?** Yes

<!--
Every feature we add is code that we will have to maintain and keep updated.
This takes a lot of effort. If you are willing to be involved in the project
and help maintain your feature, it will make it easier for us to accept it.
-->


Discussion:
leouieda: TODO:

- [ ] Name files with `hash-filename.ext` to make sure the extension is preserved and the cache is human readable.
leouieda: This got fairly big so I'll separate it into smaller PRs to make them easier to review.
leouieda: Alright, this is pretty much done and tests pass everywhere. I'd appreciate a review if anyone has the time.
leouieda: > Wouldn't be nice to have an optional argument to download the file even if a version of it already exists in the cache? This could be useful for files that are regularly updated. What do you think?

Do you mean like a `force=True` argument to download the file regardless? I can see the point but I wonder if this wouldn't make it too close to what `urllib` already does. The main feature here is the hash checking and caching so I'm hesitant to include something that shouldn't be used too often.

What do you think?
santisoler: > Do you mean like a `force=True` argument to download the file regardless? I can see the point but I wonder if this wouldn't make it too close to what `urllib` already does. The main feature here is the hash checking and caching so I'm hesitant to include something that shouldn't be used too often.
> 
> What do you think?

I was thinking that would be nice to have such option in cases where we still want to use decompressors, or when we want to redownload the file without manually removing the cached file.
Yet, I don't know how often this will be used.

If you want to take time to think about this, we can merge this and add this later.
leouieda: > If you want to take time to think about this, we can merge this and add the new argument later.

Right, you have a good point. I think I prefer to release this first and wait to see if the need arises. It's easier to add an option than to take it away later :)

Patch:
diff --git a/README.rst b/README.rst
index 390a95a..ab58cca 100644
--- a/README.rst
+++ b/README.rst
@@ -68,6 +68,25 @@ Here are Pooch's main features:
 Example
 -------
 
+For a **scientist downloading a data file** for analysis:
+
+.. code:: python
+
+    from pooch import retrieve
+
+
+    # Download the file and save it locally. Running this again will not cause
+    # a download. Pooch will check the hash (checksum) of the downloaded file
+    # against the given value to make sure it's the right file (not corrupted
+    # or outdated).
+    fname = retrieve(
+        url="https://some-data-server.org/a-data-file.nc",
+        known_hash="md5:70e2afd3fd7e336ae478b1e740a5f08e",
+    )
+
+
+For **package developers** including sample data in their projects:
+
 .. code:: python
 
     """
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 4b61738..35b8297 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -15,6 +15,7 @@ Core
 
     create
     Pooch
+    retrieve
 
 Utilities
 ---------
diff --git a/doc/index.rst b/doc/index.rst
index 86175b3..71e563b 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -17,5 +17,6 @@
     install.rst
     citing.rst
     usage.rst
+    retrieve.rst
     api/index.rst
     changes.rst
diff --git a/doc/retrieve.rst b/doc/retrieve.rst
new file mode 100644
index 0000000..af059a4
--- /dev/null
+++ b/doc/retrieve.rst
@@ -0,0 +1,51 @@
+.. _retrieve:
+
+Downloading a single file
+=========================
+
+Sometimes, you just want to download a single file (caching it locally and
+checking the hash to make sure you have the right one). In these cases,
+:class:`pooch.Pooch` is overkill and requires too much setup.
+
+For that, :func:`pooch.retrieve` is what you want:
+
+.. code-block:: python
+
+    from pooch import retrieve
+
+
+    # Download the file and save it locally. Will check the MD5 checksum of
+    # the downloaded file against the given value to make sure it's the right
+    # file. You can use other hashes by specifying different algorithm
+    # names (sha256, sha1, etc).
+    fname = retrieve(
+        # URL to one of Pooch's test files
+        url="https://github.com/fatiando/pooch/raw/v1.0.0/data/tiny-data.txt",
+        known_hash="md5:70e2afd3fd7e336ae478b1e740a5f08e",
+    )
+
+The file is stored locally, by default in a ``pooch`` folder in the default
+cache location of your operating system (see :func:`pooch.os_cache`).
+Running this code a second time will not trigger a download, same as with
+:meth:`pooch.Pooch.fetch`.
+
+If you don't know the hash of the file, you can set ``known_hash=None`` to
+bypass the check. If this is the case, :func:`~pooch.retrieve` will show a log
+message with the SHA256 hash of the downloaded file. **It's highly recommended
+that you copy and paste this hash into your code** and use it as the
+``known_hash``. That way, the next time your code is run (by you or someone
+else) you can guarantee that the exact same file is downloaded. This is a way
+to help make sure the results of your code are reproducible.
+
+Function :func:`~pooch.retrieve` has support for all of Pooch's
+:ref:`custom downloaders <downloaders>` and
+:ref:`post-processing hooks <processors>`. So you can use HTTP and FTP (with or
+without authentication), decompress files, unpack archives, and print progress
+bars with a bit of configuration.
+
+.. note::
+
+    This function is meant for downloading single files. If you need to
+    manage the download and caching of several files, with versioning, use
+    :func:`pooch.create` and :class:`pooch.Pooch` instead. See :ref:`usage`.
+
diff --git a/doc/usage.rst b/doc/usage.rst
index a5eae6b..daacad8 100644
--- a/doc/usage.rst
+++ b/doc/usage.rst
@@ -144,26 +144,26 @@ Bypassing the hash check
 Sometimes we might not know the hash of the file or it could change on the server
 periodically. In these cases, we need a way of bypassing the hash check.
 One way of doing that is with Python's ``unittest.mock`` module. It defines the object
-``unittest.mock.ANY`` which passes all equality tests made against it. To bypass the 
-check, we can set the hash value to ``unittest.mock.ANY`` when specifying the 
+``unittest.mock.ANY`` which passes all equality tests made against it. To bypass the
+check, we can set the hash value to ``unittest.mock.ANY`` when specifying the
 ``registry`` argument for :func:`pooch.create`.
 
-In this example, we want to use Pooch to download a list of weather stations around 
-Australia. The file with the stations is in an FTP server and we want to store it locally 
-in separate folders for each day that the code is run. The problem is that the 
-``stations.zip`` file is updated on the server instead of creating a new one, so the 
+In this example, we want to use Pooch to download a list of weather stations around
+Australia. The file with the stations is in an FTP server and we want to store it locally
+in separate folders for each day that the code is run. The problem is that the
+``stations.zip`` file is updated on the server instead of creating a new one, so the
 hash check would fail. This is how you can solve this problem:
 
 
 .. code:: python
 
     import datetime
-    import unittest.mock  
+    import unittest.mock
     import pooch
 
     # Get the current data to store the files in separate folders
     CURRENT_DATE = datetime.datetime.now().date()
-    
+
     GOODBOY = pooch.create(
         path=pooch.os_cache("bom_daily_stations") / CURRENT_DATE,
         base_url="ftp://ftp.bom.gov.au/anon2/home/ncc/metadata/sitelists/",
@@ -173,14 +173,14 @@ hash check would fail. This is how you can solve this problem:
         },
     )
 
-Because hash check is always ``True``, Pooch will only download the file once. When running 
-again at a different date, the file will be downloaded again because the local cache folder 
-changed and the file is no longer present in it. If you omit ``CURRENT_DATE`` from the cache 
+Because hash check is always ``True``, Pooch will only download the file once. When running
+again at a different date, the file will be downloaded again because the local cache folder
+changed and the file is no longer present in it. If you omit ``CURRENT_DATE`` from the cache
 path, then Pooch will only fetch the files once, unless they are deleted from the cache.
 
-.. note:: 
+.. note::
 
-    If run over a period of time, your cache directory will increase in size, as the 
+    If run over a period of time, your cache directory will increase in size, as the
     files are stored in daily subdirectories.
 
 
@@ -240,6 +240,8 @@ names of these files in the registry **must use Unix-style separators** (``'/'``
 on Windows. We will handle the appropriate conversions.
 
 
+.. _processors:
+
 Post-processing hooks
 ---------------------
 
@@ -374,6 +376,8 @@ keeping a decompressed copy of the file:
         return data
 
 
+.. _downloaders:
+
 Downloaders and authentication
 ------------------------------
 
diff --git a/pooch/__init__.py b/pooch/__init__.py
index 258fc2b..f11e7a0 100644
--- a/pooch/__init__.py
+++ b/pooch/__init__.py
@@ -1,7 +1,7 @@
 # pylint: disable=missing-docstring,import-outside-toplevel
 # Import functions/classes to make the API
 from . import version
-from .core import Pooch, create
+from .core import Pooch, create, retrieve
 from .utils import os_cache, file_hash, make_registry, check_version, get_logger
 from .downloaders import HTTPDownloader, FTPDownloader
 from .processors import Unzip, Untar, Decompress
diff --git a/pooch/core.py b/pooch/core.py
index 265cffe..8a09770 100644
--- a/pooch/core.py
+++ b/pooch/core.py
@@ -15,10 +15,221 @@ from .utils import (
     make_local_storage,
     hash_matches,
     temporary_file,
+    os_cache,
+    unique_file_name,
+    file_hash,
 )
 from .downloaders import choose_downloader
 
 
+def retrieve(url, known_hash, fname=None, path=None, processor=None, downloader=None):
+    """
+    Download and cache a single file locally.
+
+    Uses HTTP or FTP by default, depending on the protocol in the given *url*.
+    Other download methods can be controlled through the *downloader* argument
+    (see below).
+
+    The file will be downloaded to a temporary location first and its hash will
+    be compared to the given *known_hash*. This is done to ensure that the
+    download happened correctly and securely. If the hash doesn't match, the
+    file will be deleted and an exception will be raised.
+
+    If the file already exists locally, its hash will be compared to
+    *known_hash*. If they are not the same, this is interpreted as the file
+    needing to be updated and it will be downloaded again.
+
+    You can bypass these checks by passing ``known_hash=None``. If this is
+    done, the SHA256 hash of the downloaded file will be logged to the screen.
+    It is highly recommended that you copy and paste this hash as *known_hash*
+    so that future downloads are guaranteed to be the exact same file. This is
+    crucial for reproducible computations.
+
+    If the file exists in the given *path* with the given *fname* and the hash
+    matches, it will not be downloaded and the absolute path to the file will
+    be returned.
+
+    .. note::
+
+        This function is meant for downloading single files. If you need to
+        manage the download and caching of several files, with versioning, use
+        :func:`pooch.create` and :class:`pooch.Pooch` instead.
+
+    Parameters
+    ----------
+    url : str
+        The URL to the file that is to be downloaded. Ideally, the URL should
+        end in a file name.
+    known_hash : str
+        A known hash (checksum) of the file. Will be used to verify the
+        download or check if an existing file needs to be updated. By default,
+        will assume it's a SHA256 hash. To specify a different hashing method,
+        prepend the hash with ``algorithm:``, for example
+        ``md5:pw9co2iun29juoh`` or ``sha1:092odwhi2ujdp2du2od2odh2wod2``. If
+        None, will NOT check the hash of the downloaded file or check if an
+        existing file needs to be updated.
+    fname : str or None
+        The name that will be used to save the file. Should NOT include the
+        full the path, just the file name (it will be appended to *path*). If
+        None, will create a unique file name using a combination of the last
+        part of the URL (assuming it's the file name) and the MD5 hash of the
+        URL. For example, ``81whdo2d2e928yd1wi22-data-file.csv``. This ensures
+        that files from different URLs never overwrite each other, even if they
+        have the same name.
+    path : str or PathLike or None
+        The location of the cache folder on disk. This is where the file will
+        be saved. If None, will save to a ``pooch`` folder in the default cache
+        location for your operating system (see :func:`pooch.os_cache`).
+    processor : None or callable
+        If not None, then a function (or callable object) that will be called
+        before returning the full path and after the file has been downloaded
+        (if required). See :meth:`pooch.Pooch.fetch` for details.
+    downloader : None or callable
+        If not None, then a function (or callable object) that will be called
+        to download a given URL to a provided local file name. By default,
+        downloads are done through HTTP without authentication using
+        :class:`pooch.HTTPDownloader`. See :meth:`pooch.Pooch.fetch` for
+        details.
+
+    Returns
+    -------
+    full_path : str
+        The absolute path (including the file name) of the file in the local
+        storage.
+
+    Examples
+    --------
+
+    Download one of the data files from the Pooch repository on GitHub:
+
+    >>> import os
+    >>> from pooch import version, check_version, retrieve
+    >>> # Make a URL for the version of pooch we have installed
+    >>> url = "https://github.com/fatiando/pooch/raw/{}/data/tiny-data.txt"
+    >>> url = url.format(check_version(version.full_version))
+    >>> # Download the file and save it locally. Will check the MD5 checksum of
+    >>> # the downloaded file against the given value to make sure it's the
+    >>> # right file. You can use other hashes by specifying different
+    >>> # algorithm names (sha256, sha1, etc).
+    >>> fname = retrieve(
+    ...     url, known_hash="md5:70e2afd3fd7e336ae478b1e740a5f08e",
+    ... )
+    >>> with open(fname) as f:
+    ...     print(f.read().strip())
+    # A tiny data file for test purposes only
+    1  2  3  4  5  6
+    >>> # Running again won't trigger a download and only return the path to
+    >>> # the existing file.
+    >>> fname2 = retrieve(
+    ...     url, known_hash="md5:70e2afd3fd7e336ae478b1e740a5f08e",
+    ... )
+    >>> print(fname2 == fname)
+    True
+    >>> os.remove(fname)
+
+    Files that are compressed with gzip, xz/lzma, or bzip2 can be automatically
+    decompressed by passing using the :class:`pooch.Decompress` processor:
+
+    >>> from pooch import Decompress
+    >>> # URLs to a gzip compressed version of the data file.
+    >>> url = ("https://github.com/fatiando/pooch/raw/{}/"
+    ...        + "pooch/tests/data/tiny-data.txt.gz")
+    >>> url = url.format(check_version(version.full_version))
+    >>> # By default, you would have to decompress the file yourself
+    >>> fname = retrieve(
+    ...     url,
+    ...     known_hash="md5:8812ba10b6c7778014fdae81b03f9def",
+    ... )
+    >>> print(os.path.splitext(fname)[1])
+    .gz
+    >>> # Use the processor to decompress after download automatically and
+    >>> # return the path to the decompressed file instead.
+    >>> fname2 = retrieve(
+    ...     url,
+    ...     known_hash="md5:8812ba10b6c7778014fdae81b03f9def",
+    ...     processor=Decompress(),
+    ... )
+    >>> print(fname2 == fname)
+    False
+    >>> with open(fname2) as f:
+    ...     print(f.read().strip())
+    # A tiny data file for test purposes only
+    1  2  3  4  5  6
+    >>> os.remove(fname)
+    >>> os.remove(fname2)
+
+    When downloading archives (zip or tar), it can be useful to unpack them
+    after download to avoid having to do that yourself. Use the processors
+    :class:`pooch.Unzip` or :class:`pooch.Untar` to do this automatically:
+
+    >>> from pooch import Unzip
+    >>> # URLs to a zip archive with a single data file.
+    >>> url = ("https://github.com/fatiando/pooch/raw/{}/"
+    ...        + "pooch/tests/data/tiny-data.zip")
+    >>> url = url.format(check_version(version.full_version))
+    >>> # By default, you would get the path to the archive
+    >>> fname = retrieve(
+    ...     url,
+    ...     known_hash="md5:e9592cb46cf3514a1079051f8a148148",
+    ... )
+    >>> print(os.path.splitext(fname)[1])
+    .zip
+    >>> os.remove(fname)
+    >>> # Using the processor, the archive will be unzipped and a list with the
+    >>> # path to every file will be returned instead of a single path.
+    >>> fnames = retrieve(
+    ...     url,
+    ...     known_hash="md5:e9592cb46cf3514a1079051f8a148148",
+    ...     processor=Unzip(),
+    ... )
+    >>> # There was only a single file in our archive.
+    >>> print(len(fnames))
+    1
+    >>> with open(fnames[0]) as f:
+    ...     print(f.read().strip())
+    # A tiny data file for test purposes only
+    1  2  3  4  5  6
+    >>> for f in fnames:
+    ...     os.remove(f)
+
+
+    """
+    if path is None:
+        path = os_cache("pooch")
+    if fname is None:
+        fname = unique_file_name(url)
+    # Create the local data directory if it doesn't already exist and make the
+    # path absolute.
+    path = make_local_storage(path, env=None, version=None).resolve()
+
+    full_path = path / fname
+    action, verb = download_action(full_path, known_hash)
+
+    if action in ("download", "update"):
+        get_logger().info(
+            "%s data from '%s' to file '%s'.", verb, url, str(full_path),
+        )
+
+        if downloader is None:
+            downloader = choose_downloader(url)
+
+        stream_download(url, full_path, known_hash, downloader, pooch=None)
+
+        if known_hash is None:
+            get_logger().info(
+                "SHA256 hash of downloaded file: %s\n"
+                "Use this value as the 'known_hash' argument of 'pooch.retrieve'"
+                " to ensure that the file hasn't changed if it is downloaded again"
+                " in the future.",
+                file_hash(str(full_path)),
+            )
+
+    if processor is not None:
+        return processor(str(full_path), action, None)
+
+    return str(full_path)
+
+
 def create(
     path,
     base_url,
diff --git a/pooch/downloaders.py b/pooch/downloaders.py
index dbe2906..c06c7ee 100644
--- a/pooch/downloaders.py
+++ b/pooch/downloaders.py
@@ -3,7 +3,9 @@ Download hooks for Pooch.fetch
 """
 import sys
 import ftplib
+
 import requests
+
 from .utils import parse_url
 
 
diff --git a/pooch/utils.py b/pooch/utils.py
index 8cd84c2..841ad0c 100644
--- a/pooch/utils.py
+++ b/pooch/utils.py
@@ -279,7 +279,8 @@ def hash_algorithm(hash_string):
     The hash string should have the following form ``algorithm:hash``, where
     algorithm can be the name of any algorithm known to :mod:`hashlib`.
 
-    If the algorithm is omitted, will default to ``"sha256"``.
+    If the algorithm is omitted or the hash string is None, will default to
+    ``"sha256"``.
 
     Parameters
     ----------
@@ -300,13 +301,17 @@ def hash_algorithm(hash_string):
     md5
     >>> print(hash_algorithm("sha256:qouuwhwd2j192y1lb1iwgowdj2898wd2d9"))
     sha256
+    >>> print(hash_algorithm(None))
+    sha256
 
     """
-    parts = hash_string.split(":")
-    if len(parts) == 1:
-        algorithm = "sha256"
+    default = "sha256"
+    if hash_string is None:
+        algorithm = default
+    elif ":" not in hash_string:
+        algorithm = default
     else:
-        algorithm = parts[0]
+        algorithm = hash_string.split(":")[0]
     return algorithm
 
 
@@ -314,6 +319,8 @@ def hash_matches(fname, known_hash, strict=False, source=None):
     """
     Check if the hash of a file matches a known hash.
 
+    If the *known_hash* is None, will always return True.
+
     Parameters
     ----------
     fname : str or PathLike
@@ -336,6 +343,8 @@ def hash_matches(fname, known_hash, strict=False, source=None):
         True if the hash matches, False otherwise.
 
     """
+    if known_hash is None:
+        return True
     algorithm = hash_algorithm(known_hash)
     new_hash = file_hash(fname, alg=algorithm)
     matches = new_hash == known_hash.split(":")[-1]
@@ -382,3 +391,46 @@ def temporary_file(path=None):
     finally:
         if os.path.exists(tmp.name):
             os.remove(tmp.name)
+
+
+def unique_file_name(url):
+    """
+    Create a unique file name based on the given URL.
+
+    The file name will be unique to the URL by prepending the name with the MD5
+    hash (hex digest) of the URL. The name will also include the last portion
+    of the URL.
+
+    The format will be: ``{md5}-{filename}.{ext}``
+
+    The file name will be cropped so that the entire name (including the hash)
+    is less than 255 characters long (the limit on most file systems).
+
+    Parameters
+    ----------
+    url : str
+        The URL with a file name at the end.
+
+    Returns
+    -------
+    fname : str
+        The file name, unique to this URL.
+
+    Examples
+    --------
+
+    >>> print(unique_file_name("https://www.some-server.org/2020/data.txt"))
+    02ddee027ce5ebb3d7059fb23d210604-data.txt
+    >>> print(unique_file_name("https://www.some-server.org/2019/data.txt"))
+    9780092867b497fca6fc87d8308f1025-data.txt
+    >>> print(unique_file_name("https://www.some-server.org/2020/data.txt.gz"))
+    181a9d52e908219c2076f55145d6a344-data.txt.gz
+
+    """
+    md5 = hashlib.md5(url.encode()).hexdigest()
+    fname = parse_url(url)["path"].split("/")[-1]
+    # Crop the start of the file name to fit 255 characters including the hash
+    # and the :
+    fname = fname[-(255 - len(md5) - 1) :]
+    unique_name = "{}-{}".format(md5, fname)
+    return unique_name


Test patch:
diff --git a/pooch/tests/test_core.py b/pooch/tests/test_core.py
index b143ab0..5c0ecbd 100644
--- a/pooch/tests/test_core.py
+++ b/pooch/tests/test_core.py
@@ -8,8 +8,8 @@ from tempfile import TemporaryDirectory
 
 import pytest
 
-from ..core import Pooch, download_action, stream_download
-from ..utils import file_hash, get_logger, temporary_file
+from ..core import Pooch, retrieve, download_action, stream_download
+from ..utils import file_hash, get_logger, temporary_file, os_cache
 from ..downloaders import HTTPDownloader
 
 from .utils import (
@@ -31,6 +31,72 @@ REGISTRY_CORRUPTED = {
 }
 
 
+def test_retrieve():
+    "Try downloading some data with retrieve"
+    with TemporaryDirectory() as local_store:
+        data_file = "tiny-data.txt"
+        url = BASEURL + data_file
+        # Check that the logs say that the file is being downloaded
+        with capture_log() as log_file:
+            fname = retrieve(url, known_hash=None, path=local_store)
+            logs = log_file.getvalue()
+            assert logs.split()[0] == "Downloading"
+            assert "SHA256 hash of downloaded file:" in logs
+            assert REGISTRY[data_file] in logs
+        # Check that the downloaded file has the right content
+        assert data_file == fname[-len(data_file) :]
+        check_tiny_data(fname)
+        assert file_hash(fname) == REGISTRY[data_file]
+        # Check that no logging happens when not downloading
+        with capture_log() as log_file:
+            fname = retrieve(url, known_hash=None, path=local_store)
+            assert log_file.getvalue() == ""
+        with capture_log() as log_file:
+            fname = retrieve(url, known_hash=REGISTRY[data_file], path=local_store)
+            assert log_file.getvalue() == ""
+
+
+def test_retrieve_fname():
+    "Try downloading some data with retrieve and setting the file name"
+    with TemporaryDirectory() as local_store:
+        data_file = "tiny-data.txt"
+        url = BASEURL + data_file
+        # Check that the logs say that the file is being downloaded
+        with capture_log() as log_file:
+            fname = retrieve(url, known_hash=None, path=local_store, fname=data_file)
+            logs = log_file.getvalue()
+            assert logs.split()[0] == "Downloading"
+            assert "SHA256 hash of downloaded file:" in logs
+            assert REGISTRY[data_file] in logs
+        # Check that the downloaded file has the right name and content
+        assert data_file == os.path.split(fname)[1]
+        check_tiny_data(fname)
+        assert file_hash(fname) == REGISTRY[data_file]
+
+
+def test_retrieve_default_path():
+    "Try downloading some data with retrieve to the default cache location"
+    data_file = "tiny-data.txt"
+    url = BASEURL + data_file
+    expected_location = os_cache("pooch") / data_file
+    try:
+        # Check that the logs say that the file is being downloaded
+        with capture_log() as log_file:
+            fname = retrieve(url, known_hash=None, fname=data_file)
+            logs = log_file.getvalue()
+            assert logs.split()[0] == "Downloading"
+            assert str(os_cache("pooch").resolve()) in logs
+            assert "SHA256 hash of downloaded file" in logs
+            assert REGISTRY[data_file] in logs
+        # Check that the downloaded file has the right content
+        assert fname == str(expected_location.resolve())
+        check_tiny_data(fname)
+        assert file_hash(fname) == REGISTRY[data_file]
+    finally:
+        if os.path.exists(str(expected_location)):
+            os.remove(str(expected_location))
+
+
 def test_pooch_local():
     "Setup a pooch that already has the local data and test the fetch."
     pup = Pooch(path=DATA_DIR, base_url="some bogus URL", registry=REGISTRY)
diff --git a/pooch/tests/test_processors.py b/pooch/tests/test_processors.py
index b47f9d4..06277f7 100644
--- a/pooch/tests/test_processors.py
+++ b/pooch/tests/test_processors.py
@@ -37,8 +37,8 @@ def test_decompress(method, ext):
             lines = logs.splitlines()
             assert len(lines) == 2
             assert lines[0].split()[0] == "Downloading"
-            assert lines[1].startswith("Decompressing")
-            assert method in lines[1]
+            assert lines[-1].startswith("Decompressing")
+            assert method in lines[-1]
         assert fname == true_path
         check_tiny_data(fname)
         # Check that processor doesn't execute when not downloading
@@ -102,7 +102,7 @@ def test_processors(proc_cls, ext):
             lines = logs.splitlines()
             assert len(lines) == 2
             assert lines[0].split()[0] == "Downloading"
-            assert lines[1].startswith("Extracting 'tiny-data.txt'")
+            assert lines[-1].startswith("Extracting 'tiny-data.txt'")
 
         assert fname == true_path
         check_tiny_data(fname)
@@ -141,7 +141,7 @@ def test_processor_multiplefiles(proc_cls, ext, msg):
             lines = logs.splitlines()
             assert len(lines) == 2
             assert lines[0].split()[0] == "Downloading"
-            assert lines[1].startswith("{} contents".format(msg))
+            assert lines[-1].startswith("{} contents".format(msg))
             assert len(fnames) == 2
             assert true_paths == set(fnames)
             for fname in fnames:
diff --git a/pooch/tests/test_utils.py b/pooch/tests/test_utils.py
index f60ce95..4e6501f 100644
--- a/pooch/tests/test_utils.py
+++ b/pooch/tests/test_utils.py
@@ -17,6 +17,7 @@ from ..utils import (
     file_hash,
     hash_matches,
     temporary_file,
+    unique_file_name,
 )
 from .utils import check_tiny_data, capture_log
 
@@ -30,6 +31,16 @@ REGISTRY_RECURSIVE = (
 )
 
 
+def test_unique_name_long():
+    "The file name should never be longer than 255 characters"
+    url = "https://www.something.com/data{}.txt".format("a" * 500)
+    assert len(url) > 255
+    fname = unique_file_name(url)
+    assert len(fname) == 255
+    assert fname[-10:] == "aaaaaa.txt"
+    assert fname.split("-")[1][:10] == "aaaaaaaaaa"
+
+
 def test_local_storage_makedirs_permissionerror(monkeypatch):
     "Should warn the user when can't create the local data dir"
 
@@ -194,6 +205,16 @@ def test_hash_matches_strict():
         assert fname in str(error.value)
 
 
+def test_hash_matches_none():
+    "The hash checking function should always returns True if known_hash=None"
+    fname = os.path.join(DATA_DIR, "tiny-data.txt")
+    assert hash_matches(fname, known_hash=None)
+    # Should work even if the file is invalid
+    assert hash_matches(fname="", known_hash=None)
+    # strict should cause an error if this wasn't working
+    assert hash_matches(fname, known_hash=None, strict=True)
+
+
 def test_temporary_file():
     "Make sure the file is writable and cleaned up in the end"
     with temporary_file() as tmp: