From 9a7b6d2a1a4968b6847336aec820cc307ab93dc3 Mon Sep 17 00:00:00 2001 From: Laurens Duijvesteijn Date: Sat, 19 Jun 2021 07:50:50 +0200 Subject: [PATCH] serialize: Wrap `UnicodeDecodeError`s (#6193) Also expose `encoding` and `path` as attributes, so programmatic consumers can access it easily. --- dvc/utils/serialize/_common.py | 16 ++++++++++++++-- tests/unit/utils/serialize/test_yaml.py | 13 ++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/dvc/utils/serialize/_common.py b/dvc/utils/serialize/_common.py index 8e1a7b77e..71436523c 100644 --- a/dvc/utils/serialize/_common.py +++ b/dvc/utils/serialize/_common.py @@ -3,6 +3,7 @@ import os from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Callable, ContextManager, Dict, Union +from funcy import reraise from typing_extensions import Protocol from dvc.exceptions import DvcException @@ -42,13 +43,24 @@ class ParseError(DvcException): from dvc.utils import relpath path = relpath(path) + self.path = path super().__init__(f"unable to read: '{path}', {message}") +class EncodingError(ParseError): + """We could not read a file with the given encoding""" + + def __init__(self, path: "AnyPath", encoding: str): + self.encoding = encoding + super().__init__(path, f"is not valid {encoding}") + + def _load_data(path: "AnyPath", parser: ParserFn, fs: "BaseFileSystem" = None): open_fn = fs.open if fs else open - with open_fn(path, encoding="utf-8") as fd: # type: ignore - return parser(fd.read(), path) + encoding = "utf-8" + with open_fn(path, encoding=encoding) as fd: # type: ignore + with reraise(UnicodeDecodeError, EncodingError(path, encoding)): + return parser(fd.read(), path) def _dump_data(path, data: Any, dumper: DumperFn, fs: "BaseFileSystem" = None): diff --git a/tests/unit/utils/serialize/test_yaml.py b/tests/unit/utils/serialize/test_yaml.py index 0240bdc55..cca5956c9 100644 --- a/tests/unit/utils/serialize/test_yaml.py +++ b/tests/unit/utils/serialize/test_yaml.py @@ -1,6 +1,6 @@ import pytest -from dvc.utils.serialize._yaml import YAMLError, parse_yaml +from dvc.utils.serialize import EncodingError, YAMLError, load_yaml, parse_yaml def test_parse_yaml_duplicate_key_error(): @@ -12,3 +12,14 @@ def test_parse_yaml_duplicate_key_error(): """ with pytest.raises(YAMLError, match='found duplicate key "mykey"'): parse_yaml(text, "mypath") + + +def test_parse_yaml_invalid_unicode(tmp_dir): + filename = "invalid_utf8.yaml" + tmp_dir.gen(filename, b"\x80some: stuff") + + with pytest.raises(EncodingError) as excinfo: + load_yaml(tmp_dir / filename) + + assert filename in excinfo.value.path + assert excinfo.value.encoding == "utf-8"