This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 87a7ac153 refactor(binding/python): Add multiple custom exception for 
each of error code in Rust Core (#3492)
87a7ac153 is described below

commit 87a7ac15386b75d7135bdce425144ed32abce2e7
Author: Nadeshiko Manju <[email protected]>
AuthorDate: Tue Nov 7 14:37:32 2023 +0800

    refactor(binding/python): Add multiple custom exception for each of error 
code in Rust Core (#3492)
    
    * refactor(binding/python): Add multiple custom exception for each of error 
code in Rust Core
    
    Signed-off-by: Manjusaka <[email protected]>
    
    * update code
    
    Signed-off-by: Manjusaka <[email protected]>
    
    * Update code
    
    Signed-off-by: Manjusaka <[email protected]>
    
    * Update code
    
    Signed-off-by: Manjusaka <[email protected]>
    
    * Update code
    
    Signed-off-by: Manjusaka <[email protected]>
    
    * Revert "Update code"
    
    This reverts commit 1d7d5a797c9c4236df78ba780b117451001dd02d.
    
    * Update code
    
    Signed-off-by: Manjusaka <[email protected]>
    
    ---------
    
    Signed-off-by: Manjusaka <[email protected]>
---
 bindings/python/python/opendal/__init__.pyi        |  2 -
 bindings/python/python/opendal/exceptions.pyi      | 86 ++++++++++++++++++++++
 bindings/python/src/errors.rs                      | 61 +++++++++++++++
 bindings/python/src/lib.rs                         | 22 +++++-
 bindings/python/src/operator.rs                    | 10 ++-
 bindings/python/src/utils.rs                       | 21 ------
 bindings/python/tests/test_async_copy.py           | 10 +--
 bindings/python/tests/test_async_delete.py         |  4 +-
 bindings/python/tests/test_async_rename.py         | 18 ++---
 bindings/python/tests/test_capability.py           |  2 +-
 .../{test_capability.py => test_exceptions.py}     | 18 ++---
 bindings/python/tests/test_read.py                 |  5 +-
 bindings/python/tests/test_sync_copy.py            | 10 +--
 bindings/python/tests/test_sync_delete.py          |  4 +-
 bindings/python/tests/test_sync_rename.py          | 18 ++---
 bindings/python/tests/test_write.py                |  5 +-
 bindings/python/upgrade.md                         | 19 +++++
 17 files changed, 239 insertions(+), 76 deletions(-)

diff --git a/bindings/python/python/opendal/__init__.pyi 
b/bindings/python/python/opendal/__init__.pyi
index 7713e2a3c..2a28f523f 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -19,8 +19,6 @@ from typing import AsyncIterable, Iterable, Optional
 
 from opendal.layers import Layer
 
-class Error(Exception): ...
-
 class Operator:
     def __init__(self, scheme: str, **kwargs): ...
     def layer(self, layer: Layer): ...
diff --git a/bindings/python/python/opendal/exceptions.pyi 
b/bindings/python/python/opendal/exceptions.pyi
new file mode 100644
index 000000000..a14f25c26
--- /dev/null
+++ b/bindings/python/python/opendal/exceptions.pyi
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+class Error(Exception):
+    """Base class for exceptions in this module."""
+
+    pass
+
+class Unexpected(Error):
+    """Unexpected errors"""
+
+    pass
+
+class Unsupported(Error):
+    """Unsupported operation"""
+
+    pass
+
+class ConfigInvalid(Error):
+    """Config is invalid"""
+
+    pass
+
+class NotFound(Error):
+    """Not found"""
+
+    pass
+
+class PermissionDenied(Error):
+    """Permission denied"""
+
+    pass
+
+class IsADirectory(Error):
+    """Is a directory"""
+
+    pass
+
+class NotADirectory(Error):
+    """Not a directory"""
+
+    pass
+
+class AlreadyExists(Error):
+    """Already exists"""
+
+    pass
+
+class IsSameFile(Error):
+    """Is same file"""
+
+    pass
+
+class ConditionNotMatch(Error):
+    """Condition not match"""
+
+    pass
+
+class ContentTruncated(Error):
+    """Content truncated"""
+
+    pass
+
+class ContentIncomplete(Error):
+    """Content incomplete"""
+
+    pass
+
+class InvalidInput(Error):
+    """Invalid input"""
+
+    pass
diff --git a/bindings/python/src/errors.rs b/bindings/python/src/errors.rs
new file mode 100644
index 000000000..45bff4060
--- /dev/null
+++ b/bindings/python/src/errors.rs
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use pyo3::create_exception;
+use pyo3::exceptions::PyException;
+
+use crate::*;
+
+create_exception!(opendal, Error, PyException, "OpenDAL Base Exception");
+create_exception!(opendal, UnexpectedError, Error, "Unexpected errors");
+create_exception!(opendal, UnsupportedError, Error, "Unsupported operation");
+create_exception!(opendal, ConfigInvalidError, Error, "Config is invalid");
+create_exception!(opendal, NotFoundError, Error, "Not found");
+create_exception!(opendal, PermissionDeniedError, Error, "Permission denied");
+create_exception!(opendal, IsADirectoryError, Error, "Is a directory");
+create_exception!(opendal, NotADirectoryError, Error, "Not a directory");
+create_exception!(opendal, AlreadyExistsError, Error, "Already exists");
+create_exception!(opendal, IsSameFileError, Error, "Is same file");
+create_exception!(
+    opendal,
+    ConditionNotMatchError,
+    Error,
+    "Condition not match"
+);
+create_exception!(opendal, ContentTruncatedError, Error, "Content truncated");
+create_exception!(opendal, ContentIncompleteError, Error, "Content 
incomplete");
+create_exception!(opendal, InvalidInputError, Error, "Invalid input");
+
+pub fn format_pyerr(err: ocore::Error) -> PyErr {
+    use ocore::ErrorKind::*;
+    match err.kind() {
+        Unexpected => UnexpectedError::new_err(err.to_string()),
+        Unsupported => UnsupportedError::new_err(err.to_string()),
+        ConfigInvalid => ConfigInvalidError::new_err(err.to_string()),
+        NotFound => NotFoundError::new_err(err.to_string()),
+        PermissionDenied => PermissionDeniedError::new_err(err.to_string()),
+        IsADirectory => IsADirectoryError::new_err(err.to_string()),
+        NotADirectory => NotADirectoryError::new_err(err.to_string()),
+        AlreadyExists => AlreadyExistsError::new_err(err.to_string()),
+        IsSameFile => IsSameFileError::new_err(err.to_string()),
+        ConditionNotMatch => ConditionNotMatchError::new_err(err.to_string()),
+        ContentTruncated => ContentTruncatedError::new_err(err.to_string()),
+        ContentIncomplete => ContentIncompleteError::new_err(err.to_string()),
+        InvalidInput => InvalidInputError::new_err(err.to_string()),
+        _ => UnexpectedError::new_err(err.to_string()),
+    }
+}
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index a9c41fc00..076c75696 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -37,6 +37,8 @@ mod file;
 pub use file::*;
 mod utils;
 pub use utils::*;
+mod errors;
+pub use errors::*;
 
 /// OpenDAL Python binding
 ///
@@ -82,7 +84,6 @@ fn _opendal(py: Python, m: &PyModule) -> PyResult<()> {
     m.add_class::<Metadata>()?;
     m.add_class::<PresignedRequest>()?;
     m.add_class::<Capability>()?;
-    m.add("Error", py.get_type::<Error>())?;
 
     // Layer module
     let layers_module = PyModule::new(py, "layers")?;
@@ -93,5 +94,24 @@ fn _opendal(py: Python, m: &PyModule) -> PyResult<()> {
         .getattr("modules")?
         .set_item("opendal.layers", layers_module)?;
 
+    let exception_module = PyModule::new(py, "exceptions")?;
+    exception_module.add("Error", py.get_type::<Error>())?;
+    exception_module.add("Unexpected", py.get_type::<UnexpectedError>())?;
+    exception_module.add("Unsupported", py.get_type::<UnsupportedError>())?;
+    exception_module.add("ConfigInvalid", 
py.get_type::<ConfigInvalidError>())?;
+    exception_module.add("NotFound", py.get_type::<NotFoundError>())?;
+    exception_module.add("PermissionDenied", 
py.get_type::<PermissionDeniedError>())?;
+    exception_module.add("IsADirectory", py.get_type::<IsADirectoryError>())?;
+    exception_module.add("NotADirectory", 
py.get_type::<NotADirectoryError>())?;
+    exception_module.add("AlreadyExists", 
py.get_type::<AlreadyExistsError>())?;
+    exception_module.add("IsSameFile", py.get_type::<IsSameFileError>())?;
+    exception_module.add("ConditionNotMatch", 
py.get_type::<ConditionNotMatchError>())?;
+    exception_module.add("ContentTruncated", 
py.get_type::<ContentTruncatedError>())?;
+    exception_module.add("ContentIncomplete", 
py.get_type::<ContentIncompleteError>())?;
+    exception_module.add("InvalidInput", py.get_type::<InvalidInputError>())?;
+    m.add_submodule(exception_module)?;
+    py.import("sys")?
+        .getattr("modules")?
+        .set_item("opendal.exceptions", exception_module)?;
     Ok(())
 }
diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs
index 5bc95a2cc..f6e8c3f86 100644
--- a/bindings/python/src/operator.rs
+++ b/bindings/python/src/operator.rs
@@ -86,7 +86,7 @@ impl Operator {
             let w = this.writer(&path).map_err(format_pyerr)?;
             Ok(File::new_writer(w))
         } else {
-            Err(Error::new_err(format!(
+            Err(UnsupportedError::new_err(format!(
                 "OpenDAL doesn't support mode: {mode}"
             )))
         }
@@ -245,7 +245,7 @@ impl AsyncOperator {
                 let w = this.writer(&path).await.map_err(format_pyerr)?;
                 Ok(AsyncFile::new_writer(w))
             } else {
-                Err(Error::new_err(format!(
+                Err(UnsupportedError::new_err(format!(
                     "OpenDAL doesn't support mode: {mode}"
                 )))
             }
@@ -547,9 +547,11 @@ impl PresignedRequest {
         let mut headers = HashMap::new();
         for (k, v) in self.0.header().iter() {
             let k = k.as_str();
-            let v = v.to_str().map_err(|err| Error::new_err(err.to_string()))?;
+            let v = v
+                .to_str()
+                .map_err(|err| UnexpectedError::new_err(err.to_string()))?;
             if headers.insert(k, v).is_some() {
-                return Err(Error::new_err("duplicate header"));
+                return Err(UnexpectedError::new_err("duplicate header"));
             }
         }
         Ok(headers)
diff --git a/bindings/python/src/utils.rs b/bindings/python/src/utils.rs
index 24cbca9da..df3a41aaa 100644
--- a/bindings/python/src/utils.rs
+++ b/bindings/python/src/utils.rs
@@ -17,20 +17,10 @@
 
 use std::os::raw::c_int;
 
-use pyo3::create_exception;
-use pyo3::exceptions::PyException;
-use pyo3::exceptions::PyFileExistsError;
-use pyo3::exceptions::PyFileNotFoundError;
-use pyo3::exceptions::PyNotImplementedError;
-use pyo3::exceptions::PyPermissionError;
 use pyo3::ffi;
 use pyo3::prelude::*;
 use pyo3::AsPyPointer;
 
-use crate::*;
-
-create_exception!(opendal, Error, PyException, "OpenDAL related errors");
-
 /// A bytes-like object that implements buffer protocol.
 #[pyclass(module = "opendal")]
 pub struct Buffer {
@@ -83,14 +73,3 @@ impl Buffer {
         Ok(())
     }
 }
-
-pub fn format_pyerr(err: ocore::Error) -> PyErr {
-    use ocore::ErrorKind::*;
-    match err.kind() {
-        NotFound => PyFileNotFoundError::new_err(err.to_string()),
-        AlreadyExists => PyFileExistsError::new_err(err.to_string()),
-        PermissionDenied => PyPermissionError::new_err(err.to_string()),
-        Unsupported => PyNotImplementedError::new_err(err.to_string()),
-        _ => Error::new_err(err.to_string()),
-    }
-}
diff --git a/bindings/python/tests/test_async_copy.py 
b/bindings/python/tests/test_async_copy.py
index b5fea80d1..e2965a65c 100644
--- a/bindings/python/tests/test_async_copy.py
+++ b/bindings/python/tests/test_async_copy.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import IsADirectory, IsSameFile, NotFound
 
 
 @pytest.mark.asyncio
@@ -42,7 +42,7 @@ async def test_async_copy(service_name, operator, 
async_operator):
 async def test_async_copy_non_exist(service_name, operator, async_operator):
     source_path = f"random_file_{str(uuid4())}"
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(NotFound) :
         await async_operator.copy(source_path, target_path)
 
 
@@ -52,7 +52,7 @@ async def test_async_copy_source_directory(service_name, 
operator, async_operato
     source_path = f"random_file_{str(uuid4())}/"
     await async_operator.create_dir(source_path)
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         await async_operator.copy(source_path, target_path)
 
 
@@ -64,7 +64,7 @@ async def test_async_copy_target_directory(service_name, 
operator, async_operato
     await async_operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/"
     await async_operator.create_dir(target_path)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         await async_operator.copy(source_path, target_path)
     await async_operator.delete(source_path)
     await async_operator.delete(target_path)
@@ -76,7 +76,7 @@ async def test_async_copy_self(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}"
     content = os.urandom(1024)
     await async_operator.write(source_path, content)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsSameFile) :
         await async_operator.copy(source_path, source_path)
     await async_operator.delete(source_path)
 
diff --git a/bindings/python/tests/test_async_delete.py 
b/bindings/python/tests/test_async_delete.py
index 07e94f85e..78f1c962f 100644
--- a/bindings/python/tests/test_async_delete.py
+++ b/bindings/python/tests/test_async_delete.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import NotFound
 
 
 @pytest.mark.asyncio
@@ -43,6 +43,6 @@ async def test_async_remove_all(service_name, operator, 
async_operator):
     await async_operator.remove_all(f"{parent}/x/")
     for path in excepted:
         if not path.endswith("/"):
-            with pytest.raises(FileNotFoundError) as e_info:
+            with pytest.raises(NotFound) :
                 await async_operator.read(f"{parent}/{path}")
     await async_operator.remove_all(f"{parent}/")
diff --git a/bindings/python/tests/test_async_rename.py 
b/bindings/python/tests/test_async_rename.py
index 77ed1fc32..181e9d184 100644
--- a/bindings/python/tests/test_async_rename.py
+++ b/bindings/python/tests/test_async_rename.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import IsADirectory, IsSameFile, NotFound
 
 
 @pytest.mark.asyncio
@@ -30,7 +30,7 @@ async def test_async_rename_file(service_name, operator, 
async_operator):
     await async_operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}"
     await async_operator.rename(source_path, target_path)
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         await async_operator.read(source_path)
     assert await async_operator.read(target_path) == content
     await async_operator.delete(target_path)
@@ -42,7 +42,7 @@ async def test_async_rename_file(service_name, operator, 
async_operator):
 async def test_async_rename_non_exists_file(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}"
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         await async_operator.rename(source_path, target_path)
 
 
@@ -52,7 +52,7 @@ async def test_async_rename_directory(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}/"
     await async_operator.create_dir(source_path)
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         await async_operator.rename(source_path, target_path)
 
 
@@ -63,7 +63,7 @@ async def test_async_rename_file_to_directory(service_name, 
operator, async_oper
     content = os.urandom(1024)
     await async_operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         await async_operator.rename(source_path, target_path)
     await async_operator.delete(source_path)
 
@@ -74,7 +74,7 @@ async def test_async_rename_self(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}"
     content = os.urandom(1024)
     await async_operator.write(source_path, content)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsSameFile) :
         await async_operator.rename(source_path, source_path)
     await async_operator.delete(source_path)
 
@@ -87,7 +87,7 @@ async def test_async_rename_nested(service_name, operator, 
async_operator):
     await async_operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}"
     await async_operator.rename(source_path, target_path)
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         await async_operator.read(source_path)
     assert await async_operator.read(target_path) == content
     await async_operator.delete(target_path)
@@ -105,8 +105,8 @@ async def test_async_rename_overwrite(service_name, 
operator, async_operator):
     await async_operator.write(source_path, source_content)
     await async_operator.write(target_path, target_content)
     await async_operator.rename(source_path, target_path)
-    with pytest.raises(Exception) as e_info:
-        await async_operator.read(source_content)
+    with pytest.raises(NotFound) :
+        await async_operator.read(source_path)
     assert await async_operator.read(target_path) == source_content
     await async_operator.delete(target_path)
     await async_operator.delete(source_path)
diff --git a/bindings/python/tests/test_capability.py 
b/bindings/python/tests/test_capability.py
index da2e9c14f..ed0ea6493 100644
--- a/bindings/python/tests/test_capability.py
+++ b/bindings/python/tests/test_capability.py
@@ -27,5 +27,5 @@ def test_capability(service_name, operator):
 def test_capability_exception(service_name, operator):
     cap = operator.capability()
     assert cap is not None
-    with pytest.raises(AttributeError) as e_info:
+    with pytest.raises(AttributeError) :
         cap.read_demo
diff --git a/bindings/python/tests/test_capability.py 
b/bindings/python/tests/test_exceptions.py
similarity index 70%
copy from bindings/python/tests/test_capability.py
copy to bindings/python/tests/test_exceptions.py
index da2e9c14f..0d872e5ef 100644
--- a/bindings/python/tests/test_capability.py
+++ b/bindings/python/tests/test_exceptions.py
@@ -15,17 +15,13 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import pytest
+import inspect
 
+from opendal import exceptions
+from opendal.exceptions import Error
 
-def test_capability(service_name, operator):
-    cap = operator.capability()
-    assert cap is not None
-    assert cap.read is not None
 
-
-def test_capability_exception(service_name, operator):
-    cap = operator.capability()
-    assert cap is not None
-    with pytest.raises(AttributeError) as e_info:
-        cap.read_demo
+def test_exceptions():
+    for name, obj in inspect.getmembers(exceptions):
+        if inspect.isclass(obj):
+            assert issubclass(obj, Error)
diff --git a/bindings/python/tests/test_read.py 
b/bindings/python/tests/test_read.py
index 62a424104..4e88f2a9a 100644
--- a/bindings/python/tests/test_read.py
+++ b/bindings/python/tests/test_read.py
@@ -20,6 +20,7 @@ from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import NotFound
 
 
 @pytest.mark.need_capability("read", "write", "delete")
@@ -134,12 +135,12 @@ async def test_async_read_stat(service_name, operator, 
async_operator):
 
 @pytest.mark.need_capability("read")
 def test_sync_read_not_exists(service_name, operator, async_operator):
-    with pytest.raises(FileNotFoundError):
+    with pytest.raises(NotFound):
         operator.read(str(uuid4()))
 
 
 @pytest.mark.asyncio
 @pytest.mark.need_capability("read")
 async def test_async_read_not_exists(service_name, operator, async_operator):
-    with pytest.raises(FileNotFoundError):
+    with pytest.raises(NotFound):
         await async_operator.read(str(uuid4()))
diff --git a/bindings/python/tests/test_sync_copy.py 
b/bindings/python/tests/test_sync_copy.py
index 3db7bb6f1..f55ec92fc 100644
--- a/bindings/python/tests/test_sync_copy.py
+++ b/bindings/python/tests/test_sync_copy.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import IsADirectory, IsSameFile, NotFound
 
 
 @pytest.mark.need_capability("read", "write", "copy")
@@ -40,7 +40,7 @@ def test_sync_copy(service_name, operator, async_operator):
 def test_sync_copy_non_exist(service_name, operator, async_operator):
     source_path = f"random_file_{str(uuid4())}"
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(NotFound) :
         operator.copy(source_path, target_path)
 
 
@@ -49,7 +49,7 @@ def test_sync_copy_source_directory(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}/"
     operator.create_dir(source_path)
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         operator.copy(source_path, target_path)
 
 
@@ -60,7 +60,7 @@ def test_sync_copy_target_directory(service_name, operator, 
async_operator):
     operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/"
     operator.create_dir(target_path)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         operator.copy(source_path, target_path)
     operator.delete(source_path)
     operator.delete(target_path)
@@ -71,7 +71,7 @@ def test_sync_copy_self(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}"
     content = os.urandom(1024)
     operator.write(source_path, content)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsSameFile) :
         operator.copy(source_path, source_path)
     operator.delete(source_path)
 
diff --git a/bindings/python/tests/test_sync_delete.py 
b/bindings/python/tests/test_sync_delete.py
index 9d06d34cd..0ce3d32b8 100644
--- a/bindings/python/tests/test_sync_delete.py
+++ b/bindings/python/tests/test_sync_delete.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import NotFound
 
 
 @pytest.mark.need_capability("read", "write", "delete", "list", "blocking")
@@ -42,6 +42,6 @@ def test_sync_remove_all(service_name, operator, 
async_operator):
     operator.remove_all(f"{parent}/x/")
     for path in excepted:
         if not path.endswith("/"):
-            with pytest.raises(FileNotFoundError) as e_info:
+            with pytest.raises(NotFound) :
                 operator.read(f"{parent}/{path}")
     operator.remove_all(f"{parent}/")
diff --git a/bindings/python/tests/test_sync_rename.py 
b/bindings/python/tests/test_sync_rename.py
index 02def2805..ca6adca01 100644
--- a/bindings/python/tests/test_sync_rename.py
+++ b/bindings/python/tests/test_sync_rename.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import os
-from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import IsADirectory, IsSameFile, NotFound
 
 
 @pytest.mark.need_capability("read", "write", "rename")
@@ -29,7 +29,7 @@ def test_sync_rename_file(service_name, operator, 
async_operator):
     operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}"
     operator.rename(source_path, target_path)
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         operator.read(source_path)
     assert operator.read(target_path) == content
     operator.delete(target_path)
@@ -40,7 +40,7 @@ def test_sync_rename_file(service_name, operator, 
async_operator):
 def test_sync_rename_non_exists_file(service_name, operator, async_operator):
     source_path = f"random_file_{str(uuid4())}"
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         operator.rename(source_path, target_path)
 
 
@@ -49,7 +49,7 @@ def test_sync_rename_directory(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}/"
     operator.create_dir(source_path)
     target_path = f"random_file_{str(uuid4())}"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         operator.rename(source_path, target_path)
 
 
@@ -59,7 +59,7 @@ def test_sync_rename_file_to_directory(service_name, 
operator, async_operator):
     content = os.urandom(1024)
     operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/"
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsADirectory) :
         operator.rename(source_path, target_path)
     operator.delete(source_path)
 
@@ -69,7 +69,7 @@ def test_sync_rename_self(service_name, operator, 
async_operator):
     source_path = f"random_file_{str(uuid4())}"
     content = os.urandom(1024)
     operator.write(source_path, content)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(IsSameFile) :
         operator.rename(source_path, source_path)
     operator.delete(source_path)
 
@@ -81,7 +81,7 @@ def test_sync_rename_nested(service_name, operator, 
async_operator):
     operator.write(source_path, content)
     target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}"
     operator.rename(source_path, target_path)
-    with pytest.raises(FileNotFoundError) as e_info:
+    with pytest.raises(NotFound) :
         operator.read(source_path)
     assert operator.read(target_path) == content
     operator.delete(target_path)
@@ -98,8 +98,8 @@ def test_sync_rename_overwrite(service_name, operator, 
async_operator):
     operator.write(source_path, source_content)
     operator.write(target_path, target_content)
     operator.rename(source_path, target_path)
-    with pytest.raises(Exception) as e_info:
-        operator.read(source_content)
+    with pytest.raises(NotFound) :
+        operator.read(source_path)
     assert operator.read(target_path) == source_content
     operator.delete(target_path)
     operator.delete(source_path)
diff --git a/bindings/python/tests/test_write.py 
b/bindings/python/tests/test_write.py
index 987f0ce3c..e98c6e959 100644
--- a/bindings/python/tests/test_write.py
+++ b/bindings/python/tests/test_write.py
@@ -20,6 +20,7 @@ from random import randint
 from uuid import uuid4
 
 import pytest
+from opendal.exceptions import NotFound
 
 
 @pytest.mark.need_capability("write", "delete", "stat")
@@ -84,7 +85,7 @@ def test_sync_delete(service_name, operator, async_operator):
     size = len(content)
     operator.write(filename, content)
     operator.delete(filename)
-    with pytest.raises(FileNotFoundError):
+    with pytest.raises(NotFound):
         operator.stat(filename)
 
 
@@ -97,5 +98,5 @@ async def test_async_delete(service_name, operator, 
async_operator):
     size = len(content)
     await async_operator.write(filename, content)
     await async_operator.delete(filename)
-    with pytest.raises(FileNotFoundError):
+    with pytest.raises(NotFound):
         await operator.stat(filename)
diff --git a/bindings/python/upgrade.md b/bindings/python/upgrade.md
index de9041e61..cbb43b85a 100644
--- a/bindings/python/upgrade.md
+++ b/bindings/python/upgrade.md
@@ -27,3 +27,22 @@ Open a file for reading in async way:
 async with await op.open(filename, "rb") as r:
     content = await r.read()
 ```
+
+## Breaking change for Errors
+
+We remove the old error classes and provide a couple of Exception based class 
for the error handling.
+
+1. `opendal.Error` is based class for all the exceptions now.
+2. `opendal.exceptions.Unexpected` is added.
+3. `opendal.exceptions.Unsupported` is added.
+4. `opendal.exceptions.ConfigInvalid` is added.
+5. `opendal.exceptions.NotFound` is added.
+6. `opendal.exceptions.PermissionDenied` is added.
+7. `opendal.exceptions.IsADirectory` is added.
+8. `opendal.exceptions.NotADirectory` is added.
+9. `opendal.exceptions.AlreadyExists` is added.
+10. `opendal.exceptions.IsSameFile` is added.
+11. `opendal.exceptions.ConditionNotMatch` is added.
+12. `opendal.exceptions.ContentTruncated` is added.
+13. `opendal.exceptions.ContentIncomplete` is added.
+14. `opendal.exceptions.InvalidInput` is added.
\ No newline at end of file

Reply via email to