Repository: drivendataorg/cloudpathlib
Pull request #279 (2022-10-20 15:55:25)

Prevent file loss/exceptions when renaming: (#278)
Local branch of #278 for running live tests

----------

Do nothing if renaming to the same path
Explicit exception when attempting to rename a dir

github-actions[bot]: <!-- NETLIFY DEPLOY COMMENT GENERATED BY ACTIONS_NETLIFY -->
🚀 Deployed on https://deploy-preview-279--gallant-agnesi-5f7bb3.netlify.app
codecov-commenter: # [Codecov](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg) Report
> Merging [#279](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg) (586f891) into [master](https://codecov.io/gh/drivendataorg/cloudpathlib/commit/b7f601023e478adbe400a4bbc57efae5bfc95a81?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg) (b7f6010) will **decrease** coverage by `0.4%`.
> The diff coverage is `100.0%`.

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


```diff
@@           Coverage Diff            @@
##           master    #279     +/-   ##
========================================
- Coverage    94.8%   94.3%   -0.5%     
========================================
  Files          20      20             
  Lines        1314    1293     -21     
========================================
- Hits         1246    1220     -26     
- Misses         68      73      +5     
```


| [Impacted Files](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg) | Coverage Δ | |
|---|---|---|
| [cloudpathlib/cloudpath.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2Nsb3VkcGF0aC5weQ==) | `93.4% <100.0%> (-0.2%)` | :arrow_down: |
| [cloudpathlib/s3/s3client.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL3MzL3MzY2xpZW50LnB5) | `92.9% <0.0%> (-2.7%)` | :arrow_down: |
| [cloudpathlib/gs/gsclient.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2dzL2dzY2xpZW50LnB5) | `92.8% <0.0%> (-1.8%)` | :arrow_down: |
| [cloudpathlib/azure/azblobpath.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2F6dXJlL2F6YmxvYnBhdGgucHk=) | `92.5% <0.0%> (-0.2%)` | :arrow_down: |
| [cloudpathlib/local/implementations/azure.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2xvY2FsL2ltcGxlbWVudGF0aW9ucy9henVyZS5weQ==) | `97.4% <0.0%> (-0.2%)` | :arrow_down: |
| [cloudpathlib/gs/gspath.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2dzL2dzcGF0aC5weQ==) | `94.0% <0.0%> (-0.2%)` | :arrow_down: |
| [cloudpathlib/s3/s3path.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL3MzL3MzcGF0aC5weQ==) | `97.8% <0.0%> (-0.1%)` | :arrow_down: |
| [cloudpathlib/local/localclient.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2xvY2FsL2xvY2FsY2xpZW50LnB5) | `96.5% <0.0%> (-0.1%)` | :arrow_down: |
| [cloudpathlib/anypath.py](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg#diff-Y2xvdWRwYXRobGliL2FueXBhdGgucHk=) | `100.0% <0.0%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/drivendataorg/cloudpathlib/pull/279/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=drivendataorg) | |

</details>

Patch:
diff --git a/HISTORY.md b/HISTORY.md
index bd13e90..6558b61 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,7 +2,8 @@
 
 ## v0.11.0 (UNRELEASED)
 
- - API change: Add `ignore` parameter to `CloudPath.copytree` in order to match `shutil` API. ([Issue #145](https://github.com/drivendataorg/cloudpathlib/issues/234), [PR #272](https://github.com/drivendataorg/cloudpathlib/pull/250))
+ - API change: Add `ignore` parameter to `CloudPath.copytree` in order to match `shutil` API. ([Issue #145](https://github.com/drivendataorg/cloudpathlib/issues/145), [PR #272](https://github.com/drivendataorg/cloudpathlib/pull/272))
+ - Prevent data loss when renaming by skipping files that would be renamed to the same thing. ([Issue #277](https://github.com/drivendataorg/cloudpathlib/issues/277), [PR #278](https://github.com/drivendataorg/cloudpathlib/pull/278))
 
 
 ## v0.10.0 (2022-08-18)
diff --git a/cloudpathlib/cloudpath.py b/cloudpathlib/cloudpath.py
index 555fb49..3838ddc 100644
--- a/cloudpathlib/cloudpath.py
+++ b/cloudpathlib/cloudpath.py
@@ -453,6 +453,15 @@ class CloudPath(metaclass=CloudPathMeta):
                 f"The target based to rename must be an instantiated class of type: {type(self)}"
             )
 
+        if self.is_dir():
+            raise CloudPathIsADirectoryError(
+                f"Path {self} is a directory; rename/replace the files recursively."
+            )
+
+        if target == self:
+            # Request is to replace/rename this with the same path - nothing to do
+            return self
+
         if target.exists():
             target.unlink()
 


Test patch:
diff --git a/tests/test_cloudpath_file_io.py b/tests/test_cloudpath_file_io.py
index 55e5804..b96d9cb 100644
--- a/tests/test_cloudpath_file_io.py
+++ b/tests/test_cloudpath_file_io.py
@@ -38,6 +38,9 @@ def test_file_discovery(rig):
     with pytest.raises(CloudPathIsADirectoryError):
         p3.unlink()
 
+    with pytest.raises(CloudPathIsADirectoryError):
+        p3.rename(rig.create_cloud_path("dir_2/"))
+
     with pytest.raises(DirectoryNotEmptyError):
         p3.rmdir()
     p3.rmtree()
@@ -275,8 +278,13 @@ def test_file_read_writes(rig, tmp_path):
     assert not dest.exists()
     p.rename(dest)
     assert dest.exists()
-
     assert not p.exists()
+
+    dest_duplicate = rig.create_cloud_path("dir2/new_file0_0.txt")
+    assert dest == dest_duplicate
+    dest.rename(dest_duplicate)
+    assert dest.exists()
+
     p.touch()
     dest.replace(p)
     assert p.exists()