This is an automated email from the ASF dual-hosted git repository. manjusaka pushed a commit to branch manjusaka/polish-exception in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
commit 5242d1c1ff2ed1a9a77f81fa2f553b0309e129fd Author: Manjusaka <[email protected]> AuthorDate: Mon Nov 6 21:00:57 2023 +0800 reactor(binding/python): Add multiple custom exception for each of error code in Rust Core 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 | 86 +++++++++++++++++++++++++++ bindings/python/src/lib.rs | 34 ++++++++++- bindings/python/src/utils.rs | 21 ------- bindings/python/tests/test_async_copy.py | 9 +-- bindings/python/tests/test_async_delete.py | 3 +- bindings/python/tests/test_async_rename.py | 17 +++--- bindings/python/tests/test_read.py | 5 +- bindings/python/tests/test_sync_copy.py | 9 +-- bindings/python/tests/test_sync_delete.py | 3 +- bindings/python/tests/test_sync_rename.py | 17 +++--- bindings/python/tests/test_write.py | 5 +- 13 files changed, 243 insertions(+), 54 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..9ca922335 --- /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): + """OpenDAL unrelated errors""" + + pass + +class UnexpectedError(Exception): + """Unexpected errors""" + + pass + +class UnsupportedError(Exception): + """Unsupported operation""" + + pass + +class ConfigInvalidError(Exception): + """Config is invalid""" + + pass + +class NotFoundError(Exception): + """Not found""" + + pass + +class PermissionDeniedError(Exception): + """Permission denied""" + + pass + +class IsADirectoryError(Exception): + """Is a directory""" + + pass + +class NotADirectoryError(Exception): + """Not a directory""" + + pass + +class AlreadyExistsError(Exception): + """Already exists""" + + pass + +class IsSameFileError(Exception): + """Is same file""" + + pass + +class ConditionNotMatchError(Exception): + """Condition not match""" + + pass + +class ContentTruncatedError(Exception): + """Content truncated""" + + pass + +class ContentIncompleteError(Exception): + """Content incomplete""" + + pass + +class InvalidInputError(Exception): + """Invalid input""" + + pass diff --git a/bindings/python/src/errors.rs b/bindings/python/src/errors.rs new file mode 100644 index 000000000..5d7f98e49 --- /dev/null +++ b/bindings/python/src/errors.rs @@ -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. + +use pyo3::create_exception; +use pyo3::exceptions::PyException; + +use crate::*; + +create_exception!(opendal, UnexpectedError, PyException, "Unexpected errors"); +create_exception!( + opendal, + UnsupportedError, + PyException, + "Unsupported operation" +); +create_exception!( + opendal, + ConfigInvalidError, + PyException, + "Config is invalid" +); +create_exception!(opendal, NotFoundError, PyException, "Not found"); +create_exception!( + opendal, + PermissionDeniedError, + PyException, + "Permission denied" +); +create_exception!(opendal, IsADirectoryError, PyException, "Is a directory"); +create_exception!(opendal, NotADirectoryError, PyException, "Not a directory"); +create_exception!(opendal, AlreadyExistsError, PyException, "Already exists"); +create_exception!(opendal, IsSameFileError, PyException, "Is same file"); +create_exception!( + opendal, + ConditionNotMatchError, + PyException, + "Condition not match" +); +create_exception!( + opendal, + ContentTruncatedError, + PyException, + "Content truncated" +); +create_exception!( + opendal, + ContentIncompleteError, + PyException, + "Content incomplete" +); +create_exception!(opendal, InvalidInputError, PyException, "Invalid input"); +create_exception!(opendal, Error, PyException, "OpenDAL unrelated errors"); + +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()), + _ => Error::new_err(err.to_string()), + } +} diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index a9c41fc00..bada6d204 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,36 @@ 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("UnexpectedError", py.get_type::<UnexpectedError>())?; + exception_module.add("UnsupportedError", py.get_type::<UnsupportedError>())?; + exception_module.add("ConfigInvalidError", py.get_type::<ConfigInvalidError>())?; + exception_module.add("NotFoundError", py.get_type::<NotFoundError>())?; + exception_module.add( + "PermissionDeniedError", + py.get_type::<PermissionDeniedError>(), + )?; + exception_module.add("IsADirectoryError", py.get_type::<IsADirectoryError>())?; + exception_module.add("NotADirectoryError", py.get_type::<NotADirectoryError>())?; + exception_module.add("AlreadyExistsError", py.get_type::<AlreadyExistsError>())?; + exception_module.add("IsSameFileError", py.get_type::<IsSameFileError>())?; + exception_module.add( + "ConditionNotMatchError", + py.get_type::<ConditionNotMatchError>(), + )?; + exception_module.add( + "ContentTruncatedError", + py.get_type::<ContentTruncatedError>(), + )?; + exception_module.add( + "ContentIncompleteError", + py.get_type::<ContentIncompleteError>(), + )?; + exception_module.add("InvalidInputError", 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/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..551709e1b 100644 --- a/bindings/python/tests/test_async_copy.py +++ b/bindings/python/tests/test_async_copy.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import IsADirectoryError, IsSameFileError, NotFoundError @pytest.mark.asyncio @@ -42,7 +43,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(NotFoundError) as e_info: await async_operator.copy(source_path, target_path) @@ -52,7 +53,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(IsADirectoryError) as e_info: await async_operator.copy(source_path, target_path) @@ -64,7 +65,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(IsADirectoryError) as e_info: await async_operator.copy(source_path, target_path) await async_operator.delete(source_path) await async_operator.delete(target_path) @@ -76,7 +77,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(IsSameFileError) as e_info: 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..ab1051b58 100644 --- a/bindings/python/tests/test_async_delete.py +++ b/bindings/python/tests/test_async_delete.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import NotFoundError @pytest.mark.asyncio @@ -43,6 +44,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(NotFoundError) as e_info: 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..33737600d 100644 --- a/bindings/python/tests/test_async_rename.py +++ b/bindings/python/tests/test_async_rename.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import IsADirectoryError, IsSameFileError, NotFoundError @pytest.mark.asyncio @@ -30,7 +31,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(NotFoundError) as e_info: await async_operator.read(source_path) assert await async_operator.read(target_path) == content await async_operator.delete(target_path) @@ -42,7 +43,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(NotFoundError) as e_info: await async_operator.rename(source_path, target_path) @@ -52,7 +53,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(IsADirectoryError) as e_info: await async_operator.rename(source_path, target_path) @@ -63,7 +64,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(IsADirectoryError) as e_info: await async_operator.rename(source_path, target_path) await async_operator.delete(source_path) @@ -74,7 +75,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(IsSameFileError) as e_info: await async_operator.rename(source_path, source_path) await async_operator.delete(source_path) @@ -87,7 +88,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(NotFoundError) as e_info: await async_operator.read(source_path) assert await async_operator.read(target_path) == content await async_operator.delete(target_path) @@ -105,8 +106,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(NotFoundError) as e_info: + 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_read.py b/bindings/python/tests/test_read.py index 62a424104..9e6b40460 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 IsADirectoryError, IsSameFileError, NotFoundError @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(NotFoundError): 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(NotFoundError): 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..de2ad35c9 100644 --- a/bindings/python/tests/test_sync_copy.py +++ b/bindings/python/tests/test_sync_copy.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import IsADirectoryError, IsSameFileError, NotFoundError @pytest.mark.need_capability("read", "write", "copy") @@ -40,7 +41,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(NotFoundError) as e_info: operator.copy(source_path, target_path) @@ -49,7 +50,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(IsADirectoryError) as e_info: operator.copy(source_path, target_path) @@ -60,7 +61,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(IsADirectoryError) as e_info: operator.copy(source_path, target_path) operator.delete(source_path) operator.delete(target_path) @@ -71,7 +72,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(IsSameFileError) as e_info: 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..d969e5587 100644 --- a/bindings/python/tests/test_sync_delete.py +++ b/bindings/python/tests/test_sync_delete.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import NotFoundError @pytest.mark.need_capability("read", "write", "delete", "list", "blocking") @@ -42,6 +43,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(NotFoundError) as e_info: 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..12e0c73e4 100644 --- a/bindings/python/tests/test_sync_rename.py +++ b/bindings/python/tests/test_sync_rename.py @@ -20,6 +20,7 @@ from random import randint from uuid import uuid4 import pytest +from opendal.exceptions import IsADirectoryError, IsSameFileError, NotFoundError @pytest.mark.need_capability("read", "write", "rename") @@ -29,7 +30,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(NotFoundError) as e_info: operator.read(source_path) assert operator.read(target_path) == content operator.delete(target_path) @@ -40,7 +41,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(NotFoundError) as e_info: operator.rename(source_path, target_path) @@ -49,7 +50,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(IsADirectoryError) as e_info: operator.rename(source_path, target_path) @@ -59,7 +60,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(IsADirectoryError) as e_info: operator.rename(source_path, target_path) operator.delete(source_path) @@ -69,7 +70,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(IsSameFileError) as e_info: operator.rename(source_path, source_path) operator.delete(source_path) @@ -81,7 +82,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(NotFoundError) as e_info: operator.read(source_path) assert operator.read(target_path) == content operator.delete(target_path) @@ -98,8 +99,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(NotFoundError) as e_info: + 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..8f7d1a0ed 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 NotFoundError @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(NotFoundError): 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(NotFoundError): await operator.stat(filename)
