Repository: drivendataorg/cloudpathlib (python)

Issue:
Calling `.suffix` on a dir raises IndexError
`Path("/dir").suffix` returns the empty string, and we should do the same.


`S3Path("s3://bucket/dir").suffix` results in:

```python
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-21-161bf2e9d1ca> in <module>
----> 1 S3Path("s3://bucket/dir").suffix

~/miniconda3/envs/zamba-algorithms/lib/python3.6/site-packages/cloudpathlib/cloudpath.py in suffix(self)
    522     @property
    523     def suffix(self):
--> 524         return self._dispatch_to_path("suffix")
    525 
    526     @property

~/miniconda3/envs/zamba-algorithms/lib/python3.6/site-packages/cloudpathlib/cloudpath.py in _dispatch_to_path(self, func, *args, **kwargs)
    470 
    471         if isinstance(path_version, collections.abc.Sequence) and isinstance(
--> 472             path_version[0], PurePosixPath
    473         ):
    474             return [

IndexError: string index out of range



```

Pull request:
Fix suffix and suffixes for no suffix
Fixes #120 

Patch:
diff --git a/HISTORY.md b/HISTORY.md
index 7bd43a6..0cb7466 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,18 +2,22 @@
 
 ## v0.7.0 (Unreleased)
 
- - Re-implemented `glob` and `rglob` functions to use pathlib's globbing logic rather than fnmatch; update `iterdir` to not include self. ([Issue #154](https://github.com/drivendataorg/cloudpathlib/issues/154), [Issue #15](https://github.com/drivendataorg/cloudpathlib/issues/15))
- - Drop support for Python 3.6 [Issue #186](https://github.com/drivendataorg/cloudpathlib/issues/186)
+- Fixed `glob` and `rglob` functions by using pathlib's globbing logic rather than fnmatch. ([Issue #154](https://github.com/drivendataorg/cloudpathlib/issues/154))
+- Fixed `iterdir` to not include self. ([Issue #15](https://github.com/drivendataorg/cloudpathlib/issues/15))
+- Fixed error when calling `suffix` and `suffixes` on a cloud path with no suffix. ([Issue #120](https://github.com/drivendataorg/cloudpathlib/issues/120))
+- Changed `parents` return type from list to tuple, to better match pathlib's tuple-like `_PathParents` return type.
+- Remove support for Python 3.6. [Issue #186](https://github.com/drivendataorg/cloudpathlib/issues/186)
+
 
 ## v0.6.5 (2022-01-25)
 
- - Fixed error when "directories" created on AWS S3 were reported as files. ([Issue #148](https://github.com/drivendataorg/cloudpathlib/issues/148), [PR #190](https://github.com/drivendataorg/cloudpathlib/pull/190))
- - Fixed bug where GCE machines can instantiate default client, but we don't attempt it. ([Issue #191](https://github.com/drivendataorg/cloudpathlib/issues/191)
- - Support `AWS_ENDPOINT_URL` environment variable to set the `endpoint_url` for `S3Client`. ([PR #193](https://github.com/drivendataorg/cloudpathlib/pull/193))
+- Fixed error when "directories" created on AWS S3 were reported as files. ([Issue #148](https://github.com/drivendataorg/cloudpathlib/issues/148), [PR #190](https://github.com/drivendataorg/cloudpathlib/pull/190))
+- Fixed bug where GCE machines can instantiate default client, but we don't attempt it. ([Issue #191](https://github.com/drivendataorg/cloudpathlib/issues/191)
+- Support `AWS_ENDPOINT_URL` environment variable to set the `endpoint_url` for `S3Client`. ([PR #193](https://github.com/drivendataorg/cloudpathlib/pull/193))
 
 ## v0.6.4 (2021-12-29)
 
- - Fixed error where `BlobProperties` type hint causes import error if Azure dependencies not installed.
+- Fixed error where `BlobProperties` type hint causes import error if Azure dependencies not installed.
 
 ## v0.6.3 (2021-12-29)
 
diff --git a/cloudpathlib/cloudpath.py b/cloudpathlib/cloudpath.py
index 49c248b..dcb06e3 100644
--- a/cloudpathlib/cloudpath.py
+++ b/cloudpathlib/cloudpath.py
@@ -3,7 +3,15 @@ from collections import defaultdict
 import collections.abc
 from contextlib import contextmanager
 import os
-from pathlib import Path, PosixPath, PurePosixPath, WindowsPath, _make_selector, _posix_flavour  # type: ignore
+from pathlib import (  # type: ignore
+    Path,
+    PosixPath,
+    PurePosixPath,
+    WindowsPath,
+    _make_selector,
+    _posix_flavour,
+    _PathParents,
+)
 from typing import Any, IO, Iterable, Dict, Optional, TYPE_CHECKING, Union
 from urllib.parse import urlparse
 from warnings import warn
@@ -501,14 +509,21 @@ class CloudPath(metaclass=CloudPathMeta):
             path_version = _resolve(path_version)
             return self._new_cloudpath(path_version)
 
-        if isinstance(path_version, collections.abc.Sequence) and isinstance(
-            path_version[0], PurePosixPath
+        # When sequence of PurePosixPath, we want to convert to sequence of CloudPaths
+        if (
+            isinstance(path_version, collections.abc.Sequence)
+            and len(path_version) > 0
+            and isinstance(path_version[0], PurePosixPath)
         ):
-            return [
+            sequence_class = (
+                type(path_version) if not isinstance(path_version, _PathParents) else tuple
+            )
+            return sequence_class(
                 self._new_cloudpath(_resolve(p)) for p in path_version if _resolve(p) != p.root
-            ]
+            )
 
-        # when pathlib returns a string, etc. we probably just want that thing
+        # when pathlib something else, we probably just want that thing
+        # cases this should include: str, empty sequence, sequence of str, ...
         else:
             return path_version
 


Test patch:
diff --git a/tests/test_cloudpath_manipulation.py b/tests/test_cloudpath_manipulation.py
index ee6d9c6..146b542 100644
--- a/tests/test_cloudpath_manipulation.py
+++ b/tests/test_cloudpath_manipulation.py
@@ -1,17 +1,29 @@
 import pytest
 
 
-def test_joins(rig):
+def test_properties(rig):
     assert rig.create_cloud_path("a/b/c/d").name == "d"
     assert rig.create_cloud_path("a/b/c/d.file").name == "d.file"
+
+    assert rig.create_cloud_path("a/b/c/d").stem == "d"
     assert rig.create_cloud_path("a/b/c/d.file").stem == "d"
+
+    assert rig.create_cloud_path("a/b/c/d").suffix == ""
     assert rig.create_cloud_path("a/b/c/d.file").suffix == ".file"
+
+    assert rig.create_cloud_path("a/b/c/d").suffixes == []
+    assert rig.create_cloud_path("a/b/c/d.tar").suffixes == [".tar"]
     assert rig.create_cloud_path("a/b/c/d.tar.gz").suffixes == [".tar", ".gz"]
+
+
+def test_with_suffix(rig):
     assert (
         str(rig.create_cloud_path("a/b/c/d.file").with_suffix(".png"))
         == f"{rig.cloud_prefix}{rig.drive}/{rig.test_dir}/a/b/c/d.png"
     )
 
+
+def test_joins(rig):
     assert rig.create_cloud_path("a") / "b" == rig.create_cloud_path("a/b")
     assert rig.create_cloud_path("a/b/c/d") / "../../b" == rig.create_cloud_path("a/b/b")
 
@@ -22,13 +34,13 @@ def test_joins(rig):
     assert rig.create_cloud_path("a/b/c/d").anchor == rig.cloud_prefix
     assert rig.create_cloud_path("a/b/c/d").parent == rig.create_cloud_path("a/b/c")
 
-    assert rig.create_cloud_path("a/b/c/d").parents == [
+    assert rig.create_cloud_path("a/b/c/d").parents == (
         rig.create_cloud_path("a/b/c"),
         rig.create_cloud_path("a/b"),
         rig.create_cloud_path("a"),
         rig.path_class(f"{rig.cloud_prefix}{rig.drive}/{rig.test_dir}"),
         rig.path_class(f"{rig.cloud_prefix}{rig.drive}"),
-    ]
+    )
 
     assert rig.create_cloud_path("a").joinpath("b", "c") == rig.create_cloud_path("a/b/c")