Repository: justindujardin/pathy (language: python)

Issue:
Misleading error message when missing GCS application-default credentials
We have a few `assert self.client is not None, _MISSING_DEPS` in the `pathy.gcs` module. This displays an error message about installing the google cloud libraries.

However, the missing installation is not the only reason we can end up with `self.client is None`. We can also end up in that state when the libraries are installed, but the credentials can't be found. This is especially problematic since the GCS authentication is kind of confusing. I still don't really understand the application-default credentials thing.

Anyway, I got this error:

```
You are using the GCS functionality of Pathy without
having the required dependencies installed.

Please try installing them:

    pip install pathy[gcs]
```

But the actual solution was:

```
gcloud auth login
gcloud auth application-default login
```



Discussion / hints:
Nice catch! I looked at this a bit, and I think we have two options:

- Stop handling `DefaultCredentialsError` in the GCS client and let it bubble up. This way you'll see the underlying auth error which includes a link to a google site about installing the Cloud SDK. This is what smart_open does when you use streaming methods.
- Handle `DefaultCredentialsError` and provide a custom error like for missing deps. We'll probably also want to handle it for smart_open streams to ensure consistent behavior.

I don't have a strong preference, because the GCS error contains a helpful link, so we'll end up with a useful error either way.
For now let's at least "do no harm", and get out of the way of the underlying error? If we think we've got a better message later we can add it.

Fix patch:
diff --git a/pathy/__init__.py b/pathy/__init__.py
index 1e29bf3..f9193de 100644
--- a/pathy/__init__.py
+++ b/pathy/__init__.py
@@ -22,4 +22,4 @@
     use_fs_cache,
 )
 from .file import BlobFS, BucketClientFS, BucketEntryFS, BucketFS
-from .gcs import BlobGCS, BucketClientGCS, BucketEntryGCS, BucketGCS
+from .gcs import BlobGCS, BucketClientGCS, BucketEntryGCS, BucketGCS, has_gcs
diff --git a/pathy/gcs.py b/pathy/gcs.py
index 4617280..62d60dc 100644
--- a/pathy/gcs.py
+++ b/pathy/gcs.py
@@ -115,12 +115,14 @@ def __init__(self, **kwargs: Any) -> None:
         self.recreate(**kwargs)
 
     def recreate(self, **kwargs: Any) -> None:
+        creds = kwargs["credentials"] if "credentials" in kwargs else None
+        if creds is not None:
+            kwargs["project"] = creds.project_id
         try:
-            creds = kwargs["credentials"] if "credentials" in kwargs else None
-            if creds is not None:
-                kwargs["project"] = creds.project_id
-            self.client = GCSNativeClient(**kwargs) if GCSNativeClient else None
-        except (BaseException, DefaultCredentialsError):
+            self.client = GCSNativeClient(**kwargs)
+        except TypeError:
+            # TypeError is raised if the imports for GCSNativeClient fail and are
+            #  assigned to Any, which is not callable.
             self.client = None
 
     def make_uri(self, path: PurePathy) -> str:


Test patch:
diff --git a/pathy/tests/test_gcs.py b/pathy/tests/test_gcs.py
new file mode 100644
index 0000000..d984d4f
--- /dev/null
+++ b/pathy/tests/test_gcs.py
@@ -0,0 +1,24 @@
+from unittest.mock import patch
+
+import pytest
+
+from pathy import has_gcs, set_client_params
+
+
+def raise_default_creds_error() -> None:
+    from google.auth.exceptions import DefaultCredentialsError
+
+    raise DefaultCredentialsError()
+
+
+@pytest.mark.parametrize("adapter", ["gcs"])
+@patch("google.auth.default", raise_default_creds_error)
+@pytest.mark.skipif(not has_gcs, reason="requires gcs")
+def test_gcs_default_credentials_error_is_preserved(
+    with_adapter: str, bucket: str
+) -> None:
+    from google.auth.exceptions import DefaultCredentialsError
+
+    # Remove the default credentials (this will recreate the client and error)
+    with pytest.raises(DefaultCredentialsError):
+        set_client_params("gs")