Repository: drivendataorg/cloudpathlib (python)

Issue:
rmtree not working when there is directory inside (google cloud storage)
This package is really helpful to work with different cloud providers but I'm facing an issue when trying to remove directory in gcs.

I tried it even in some test script using this line (after handle the authentication, because I do get True for methods like .exists() or is_dir().

```CloudPath("gs://<bucket>/<path_to_folder(prefix)>").rmtree()```

and getting exception from the  _remove function. ('NoneType' object has no attribute 'delete').

Am I missing something in my usage or maybe it is knows bug?
Thanks 

Pull request:
Fix nested remove for azure and gcs
Closes #184 

Problem is that `_list_dir` implementations return both directories and files, but our `_remove` function for both of these clients assumes only files. 

Added regression test which fails on Azure + GCS without this fixes, but passes with the fix.

Updated history and version to cut new release with this fix

Patch:
diff --git a/HISTORY.md b/HISTORY.md
index ab3f13c..12dd0f8 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,11 @@
 # cloudpathlib Changelog
 
+## v0.6.3 (2021-12-29)
+
+- Fixed error when using `rmtree` on nested directories for Google Cloud Storage and Azure Blob Storage. ([Issue #184](https://github.com/drivendataorg/cloudpathlib/issues/184), [PR #185](https://github.com/drivendataorg/cloudpathlib/pull/185))
+- Fixed broken builds due mypy errors in azure dependency ([PR #177](https://github.com/drivendataorg/cloudpathlib/pull/177))
+- Fixed dev tools for building and serving documentation locally ([PR #178](https://github.com/drivendataorg/cloudpathlib/pull/178))
+
 ## v0.6.2 (2021-09-20)
 
 - Fixed error when importing `cloudpathlib` for missing `botocore` dependency when not installed with S3 dependencies. ([PR #168](https://github.com/drivendataorg/cloudpathlib/pull/168))
diff --git a/cloudpathlib/azure/azblobclient.py b/cloudpathlib/azure/azblobclient.py
index ee4f351..84ccb25 100644
--- a/cloudpathlib/azure/azblobclient.py
+++ b/cloudpathlib/azure/azblobclient.py
@@ -198,7 +198,7 @@ class AzureBlobClient(Client):
 
     def _remove(self, cloud_path: AzureBlobPath) -> None:
         if self._is_file_or_dir(cloud_path) == "dir":
-            blobs = [b.blob for b in self._list_dir(cloud_path, recursive=True)]
+            blobs = [b.blob for b in self._list_dir(cloud_path, recursive=True) if b.is_file()]
             container_client = self.service_client.get_container_client(cloud_path.container)
             container_client.delete_blobs(*blobs)
         elif self._is_file_or_dir(cloud_path) == "file":
diff --git a/cloudpathlib/gs/gsclient.py b/cloudpathlib/gs/gsclient.py
index b226e11..e3be404 100644
--- a/cloudpathlib/gs/gsclient.py
+++ b/cloudpathlib/gs/gsclient.py
@@ -185,7 +185,7 @@ class GSClient(Client):
 
     def _remove(self, cloud_path: GSPath) -> None:
         if self._is_file_or_dir(cloud_path) == "dir":
-            blobs = [b.blob for b in self._list_dir(cloud_path, recursive=True)]
+            blobs = [b.blob for b in self._list_dir(cloud_path, recursive=True) if b.is_file()]
             bucket = self.client.bucket(cloud_path.bucket)
             for blob in blobs:
                 bucket.get_blob(blob).delete()
diff --git a/setup.py b/setup.py
index f217bf5..66d82b5 100644
--- a/setup.py
+++ b/setup.py
@@ -59,5 +59,5 @@ setup(
         "Source Code": "https://github.com/drivendataorg/cloudpathlib",
     },
     url="https://github.com/drivendataorg/cloudpathlib",
-    version="0.6.2",
+    version="0.6.3",
 )


Test patch:
diff --git a/tests/test_cloudpath_file_io.py b/tests/test_cloudpath_file_io.py
index 1644c2a..917b19e 100644
--- a/tests/test_cloudpath_file_io.py
+++ b/tests/test_cloudpath_file_io.py
@@ -39,6 +39,19 @@ def test_file_discovery(rig):
 
     assert list(p4.glob("**/*")) == list(p4.rglob("*"))
 
+    # try rmtree on dir_1, which has a nested directory
+    p5 = rig.create_cloud_path("dir_1/")
+    assert p5.exists()
+    assert len(list(p5.glob("**/*"))) == 3
+    with pytest.raises(CloudPathIsADirectoryError):
+        p5.unlink()
+
+    with pytest.raises(DirectoryNotEmptyError):
+        p5.rmdir()
+
+    p5.rmtree()
+    assert not p5.exists()
+
 
 def test_file_read_writes(rig, tmp_path):
     p = rig.create_cloud_path("dir_0/file0_0.txt")