Skip to content

Commit 5242d1c

Browse files
committed
reactor(binding/python): Add multiple custom exception for each of error code in Rust Core
Signed-off-by: Manjusaka <[email protected]>
1 parent d130425 commit 5242d1c

13 files changed

+243
-54
lines changed

bindings/python/python/opendal/__init__.pyi

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ from typing import AsyncIterable, Iterable, Optional
1919

2020
from opendal.layers import Layer
2121

22-
class Error(Exception): ...
23-
2422
class Operator:
2523
def __init__(self, scheme: str, **kwargs): ...
2624
def layer(self, layer: Layer): ...
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
class Error(Exception):
19+
"""OpenDAL unrelated errors"""
20+
21+
pass
22+
23+
class UnexpectedError(Exception):
24+
"""Unexpected errors"""
25+
26+
pass
27+
28+
class UnsupportedError(Exception):
29+
"""Unsupported operation"""
30+
31+
pass
32+
33+
class ConfigInvalidError(Exception):
34+
"""Config is invalid"""
35+
36+
pass
37+
38+
class NotFoundError(Exception):
39+
"""Not found"""
40+
41+
pass
42+
43+
class PermissionDeniedError(Exception):
44+
"""Permission denied"""
45+
46+
pass
47+
48+
class IsADirectoryError(Exception):
49+
"""Is a directory"""
50+
51+
pass
52+
53+
class NotADirectoryError(Exception):
54+
"""Not a directory"""
55+
56+
pass
57+
58+
class AlreadyExistsError(Exception):
59+
"""Already exists"""
60+
61+
pass
62+
63+
class IsSameFileError(Exception):
64+
"""Is same file"""
65+
66+
pass
67+
68+
class ConditionNotMatchError(Exception):
69+
"""Condition not match"""
70+
71+
pass
72+
73+
class ContentTruncatedError(Exception):
74+
"""Content truncated"""
75+
76+
pass
77+
78+
class ContentIncompleteError(Exception):
79+
"""Content incomplete"""
80+
81+
pass
82+
83+
class InvalidInputError(Exception):
84+
"""Invalid input"""
85+
86+
pass

bindings/python/src/errors.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use pyo3::create_exception;
19+
use pyo3::exceptions::PyException;
20+
21+
use crate::*;
22+
23+
create_exception!(opendal, UnexpectedError, PyException, "Unexpected errors");
24+
create_exception!(
25+
opendal,
26+
UnsupportedError,
27+
PyException,
28+
"Unsupported operation"
29+
);
30+
create_exception!(
31+
opendal,
32+
ConfigInvalidError,
33+
PyException,
34+
"Config is invalid"
35+
);
36+
create_exception!(opendal, NotFoundError, PyException, "Not found");
37+
create_exception!(
38+
opendal,
39+
PermissionDeniedError,
40+
PyException,
41+
"Permission denied"
42+
);
43+
create_exception!(opendal, IsADirectoryError, PyException, "Is a directory");
44+
create_exception!(opendal, NotADirectoryError, PyException, "Not a directory");
45+
create_exception!(opendal, AlreadyExistsError, PyException, "Already exists");
46+
create_exception!(opendal, IsSameFileError, PyException, "Is same file");
47+
create_exception!(
48+
opendal,
49+
ConditionNotMatchError,
50+
PyException,
51+
"Condition not match"
52+
);
53+
create_exception!(
54+
opendal,
55+
ContentTruncatedError,
56+
PyException,
57+
"Content truncated"
58+
);
59+
create_exception!(
60+
opendal,
61+
ContentIncompleteError,
62+
PyException,
63+
"Content incomplete"
64+
);
65+
create_exception!(opendal, InvalidInputError, PyException, "Invalid input");
66+
create_exception!(opendal, Error, PyException, "OpenDAL unrelated errors");
67+
68+
pub fn format_pyerr(err: ocore::Error) -> PyErr {
69+
use ocore::ErrorKind::*;
70+
match err.kind() {
71+
Unexpected => UnexpectedError::new_err(err.to_string()),
72+
Unsupported => UnsupportedError::new_err(err.to_string()),
73+
ConfigInvalid => ConfigInvalidError::new_err(err.to_string()),
74+
NotFound => NotFoundError::new_err(err.to_string()),
75+
PermissionDenied => PermissionDeniedError::new_err(err.to_string()),
76+
IsADirectory => IsADirectoryError::new_err(err.to_string()),
77+
NotADirectory => NotADirectoryError::new_err(err.to_string()),
78+
AlreadyExists => AlreadyExistsError::new_err(err.to_string()),
79+
IsSameFile => IsSameFileError::new_err(err.to_string()),
80+
ConditionNotMatch => ConditionNotMatchError::new_err(err.to_string()),
81+
ContentTruncated => ContentTruncatedError::new_err(err.to_string()),
82+
ContentIncomplete => ContentIncompleteError::new_err(err.to_string()),
83+
InvalidInput => InvalidInputError::new_err(err.to_string()),
84+
_ => Error::new_err(err.to_string()),
85+
}
86+
}

bindings/python/src/lib.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ mod file;
3737
pub use file::*;
3838
mod utils;
3939
pub use utils::*;
40+
mod errors;
41+
pub use errors::*;
4042

4143
/// OpenDAL Python binding
4244
///
@@ -82,7 +84,6 @@ fn _opendal(py: Python, m: &PyModule) -> PyResult<()> {
8284
m.add_class::<Metadata>()?;
8385
m.add_class::<PresignedRequest>()?;
8486
m.add_class::<Capability>()?;
85-
m.add("Error", py.get_type::<Error>())?;
8687

8788
// Layer module
8889
let layers_module = PyModule::new(py, "layers")?;
@@ -93,5 +94,36 @@ fn _opendal(py: Python, m: &PyModule) -> PyResult<()> {
9394
.getattr("modules")?
9495
.set_item("opendal.layers", layers_module)?;
9596

97+
let exception_module = PyModule::new(py, "exceptions")?;
98+
exception_module.add("Error", py.get_type::<Error>())?;
99+
exception_module.add("UnexpectedError", py.get_type::<UnexpectedError>())?;
100+
exception_module.add("UnsupportedError", py.get_type::<UnsupportedError>())?;
101+
exception_module.add("ConfigInvalidError", py.get_type::<ConfigInvalidError>())?;
102+
exception_module.add("NotFoundError", py.get_type::<NotFoundError>())?;
103+
exception_module.add(
104+
"PermissionDeniedError",
105+
py.get_type::<PermissionDeniedError>(),
106+
)?;
107+
exception_module.add("IsADirectoryError", py.get_type::<IsADirectoryError>())?;
108+
exception_module.add("NotADirectoryError", py.get_type::<NotADirectoryError>())?;
109+
exception_module.add("AlreadyExistsError", py.get_type::<AlreadyExistsError>())?;
110+
exception_module.add("IsSameFileError", py.get_type::<IsSameFileError>())?;
111+
exception_module.add(
112+
"ConditionNotMatchError",
113+
py.get_type::<ConditionNotMatchError>(),
114+
)?;
115+
exception_module.add(
116+
"ContentTruncatedError",
117+
py.get_type::<ContentTruncatedError>(),
118+
)?;
119+
exception_module.add(
120+
"ContentIncompleteError",
121+
py.get_type::<ContentIncompleteError>(),
122+
)?;
123+
exception_module.add("InvalidInputError", py.get_type::<InvalidInputError>())?;
124+
m.add_submodule(exception_module)?;
125+
py.import("sys")?
126+
.getattr("modules")?
127+
.set_item("opendal.exceptions", exception_module)?;
96128
Ok(())
97129
}

bindings/python/src/utils.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,10 @@
1717

1818
use std::os::raw::c_int;
1919

20-
use pyo3::create_exception;
21-
use pyo3::exceptions::PyException;
22-
use pyo3::exceptions::PyFileExistsError;
23-
use pyo3::exceptions::PyFileNotFoundError;
24-
use pyo3::exceptions::PyNotImplementedError;
25-
use pyo3::exceptions::PyPermissionError;
2620
use pyo3::ffi;
2721
use pyo3::prelude::*;
2822
use pyo3::AsPyPointer;
2923

30-
use crate::*;
31-
32-
create_exception!(opendal, Error, PyException, "OpenDAL related errors");
33-
3424
/// A bytes-like object that implements buffer protocol.
3525
#[pyclass(module = "opendal")]
3626
pub struct Buffer {
@@ -83,14 +73,3 @@ impl Buffer {
8373
Ok(())
8474
}
8575
}
86-
87-
pub fn format_pyerr(err: ocore::Error) -> PyErr {
88-
use ocore::ErrorKind::*;
89-
match err.kind() {
90-
NotFound => PyFileNotFoundError::new_err(err.to_string()),
91-
AlreadyExists => PyFileExistsError::new_err(err.to_string()),
92-
PermissionDenied => PyPermissionError::new_err(err.to_string()),
93-
Unsupported => PyNotImplementedError::new_err(err.to_string()),
94-
_ => Error::new_err(err.to_string()),
95-
}
96-
}

bindings/python/tests/test_async_copy.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from uuid import uuid4
2121

2222
import pytest
23+
from opendal.exceptions import IsADirectoryError, IsSameFileError, NotFoundError
2324

2425

2526
@pytest.mark.asyncio
@@ -42,7 +43,7 @@ async def test_async_copy(service_name, operator, async_operator):
4243
async def test_async_copy_non_exist(service_name, operator, async_operator):
4344
source_path = f"random_file_{str(uuid4())}"
4445
target_path = f"random_file_{str(uuid4())}"
45-
with pytest.raises(Exception) as e_info:
46+
with pytest.raises(NotFoundError) as e_info:
4647
await async_operator.copy(source_path, target_path)
4748

4849

@@ -52,7 +53,7 @@ async def test_async_copy_source_directory(service_name, operator, async_operato
5253
source_path = f"random_file_{str(uuid4())}/"
5354
await async_operator.create_dir(source_path)
5455
target_path = f"random_file_{str(uuid4())}"
55-
with pytest.raises(Exception) as e_info:
56+
with pytest.raises(IsADirectoryError) as e_info:
5657
await async_operator.copy(source_path, target_path)
5758

5859

@@ -64,7 +65,7 @@ async def test_async_copy_target_directory(service_name, operator, async_operato
6465
await async_operator.write(source_path, content)
6566
target_path = f"random_file_{str(uuid4())}/"
6667
await async_operator.create_dir(target_path)
67-
with pytest.raises(Exception) as e_info:
68+
with pytest.raises(IsADirectoryError) as e_info:
6869
await async_operator.copy(source_path, target_path)
6970
await async_operator.delete(source_path)
7071
await async_operator.delete(target_path)
@@ -76,7 +77,7 @@ async def test_async_copy_self(service_name, operator, async_operator):
7677
source_path = f"random_file_{str(uuid4())}"
7778
content = os.urandom(1024)
7879
await async_operator.write(source_path, content)
79-
with pytest.raises(Exception) as e_info:
80+
with pytest.raises(IsSameFileError) as e_info:
8081
await async_operator.copy(source_path, source_path)
8182
await async_operator.delete(source_path)
8283

bindings/python/tests/test_async_delete.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from uuid import uuid4
2121

2222
import pytest
23+
from opendal.exceptions import NotFoundError
2324

2425

2526
@pytest.mark.asyncio
@@ -43,6 +44,6 @@ async def test_async_remove_all(service_name, operator, async_operator):
4344
await async_operator.remove_all(f"{parent}/x/")
4445
for path in excepted:
4546
if not path.endswith("/"):
46-
with pytest.raises(FileNotFoundError) as e_info:
47+
with pytest.raises(NotFoundError) as e_info:
4748
await async_operator.read(f"{parent}/{path}")
4849
await async_operator.remove_all(f"{parent}/")

0 commit comments

Comments
 (0)