diff --git a/CHANGES.rst b/CHANGES.rst index acf53aaf7..d1af1b8c3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,12 @@ - Fix issue where roundtripping a masked array with no masked values removes the mask [#1803] +- Use a custom exception ``AsdfSerializationError`` to indicate when an object in the + tree fails to be serialized by asdf (and by yaml). This exception currently inherits + from ``yaml.representer.RepresenterError`` to provide backwards compatibility. However + this inheritance may be dropped in a future asdf version. Please migrate to the new + ``AsdfSerializationError``. [#1809] + 3.3.0 (2024-07-12) ------------------ diff --git a/asdf/_tests/test_extension.py b/asdf/_tests/test_extension.py index fd7a0ec6d..f9b8baf50 100644 --- a/asdf/_tests/test_extension.py +++ b/asdf/_tests/test_extension.py @@ -3,11 +3,10 @@ import pytest from packaging.specifiers import SpecifierSet -from yaml.representer import RepresenterError import asdf from asdf import AsdfFile, config_context -from asdf.exceptions import AsdfManifestURIMismatchWarning, AsdfWarning, ValidationError +from asdf.exceptions import AsdfManifestURIMismatchWarning, AsdfSerializationError, AsdfWarning, ValidationError from asdf.extension import ( Compressor, Converter, @@ -578,7 +577,7 @@ class FooExtension(Extension): tree = {"obj": Foo()} with config_context() as cfg: cfg.add_extension(FooExtension()) - with pytest.raises(RepresenterError, match=r"cannot represent an object"): + with pytest.raises(AsdfSerializationError, match=r"is not serializable by asdf"): roundtrip_object(tree) diff --git a/asdf/_tests/test_yaml.py b/asdf/_tests/test_yaml.py index 34598420c..55648dd46 100644 --- a/asdf/_tests/test_yaml.py +++ b/asdf/_tests/test_yaml.py @@ -10,7 +10,7 @@ import asdf from asdf import tagged, treeutil, yamlutil -from asdf.exceptions import AsdfConversionWarning, AsdfDeprecationWarning, AsdfWarning +from asdf.exceptions import AsdfConversionWarning, AsdfDeprecationWarning, AsdfSerializationError, AsdfWarning from asdf.testing.helpers import yaml_to_asdf @@ -104,7 +104,7 @@ class Foo: buff = io.BytesIO() ff = asdf.AsdfFile(tree) - with pytest.raises(yaml.YAMLError, match=r"\('cannot represent an object', .*\)"): + with pytest.raises(AsdfSerializationError, match=r".*is not serializable by asdf.*"): ff.write_to(buff) @@ -350,7 +350,7 @@ class MyNDArray(np.ndarray): with asdf.config.config_context() as cfg: cfg.convert_unknown_ndarray_subclasses = False - with pytest.raises(yaml.representer.RepresenterError, match=r".*cannot represent.*"): + with pytest.raises(AsdfSerializationError, match=r".*is not serializable by asdf.*"): af.write_to(fn) diff --git a/asdf/exceptions.py b/asdf/exceptions.py index 879aa25c2..0851a8baa 100644 --- a/asdf/exceptions.py +++ b/asdf/exceptions.py @@ -1,3 +1,5 @@ +from yaml.representer import RepresenterError + from asdf._jsonschema import ValidationError __all__ = [ @@ -7,6 +9,7 @@ "AsdfManifestURIMismatchWarning", "AsdfPackageVersionWarning", "AsdfProvisionalAPIWarning", + "AsdfSerializationError", "AsdfWarning", "DelimiterNotFoundError", "ValidationError", @@ -73,3 +76,11 @@ class AsdfLazyReferenceError(ReferenceError): collected and you may need to update your code to keep the AsdfFile in memory (by keeping a reference). """ + + +class AsdfSerializationError(RepresenterError): + """ + An object failed serialization by asdf and by yaml. This likely indicates + that the object does not have a supporting asdf Converter and needs to + be manually converted to a supported type. + """ diff --git a/asdf/yamlutil.py b/asdf/yamlutil.py index b93ab3740..63c2d864a 100644 --- a/asdf/yamlutil.py +++ b/asdf/yamlutil.py @@ -7,7 +7,7 @@ from . import config, schema, tagged, treeutil, util from .constants import STSCI_SCHEMA_TAG_BASE, YAML_TAG_PREFIX -from .exceptions import AsdfConversionWarning +from .exceptions import AsdfConversionWarning, AsdfSerializationError from .extension._serialization_context import BlockAccess from .tags.core import AsdfObject from .versioning import _YAML_VERSION, _yaml_base_loader @@ -403,14 +403,26 @@ def dump_tree(tree, fd, ctx, tree_finalizer=None, _serialization_context=None): if key not in tags: tags[key] = val - yaml.dump_all( - [tree], - stream=fd, - Dumper=AsdfDumper, - explicit_start=True, - explicit_end=True, - version=_YAML_VERSION, - allow_unicode=True, - encoding="utf-8", - tags=tags, - ) + try: + yaml.dump_all( + [tree], + stream=fd, + Dumper=AsdfDumper, + explicit_start=True, + explicit_end=True, + version=_YAML_VERSION, + allow_unicode=True, + encoding="utf-8", + tags=tags, + ) + except yaml.representer.RepresenterError as err: + if len(err.args) < 2: + raise err + # inspect the exception arguments to determine what object failed + obj = err.args[1] + msg = ( + f"Object of type[{type(obj)}] is not serializable by asdf. " + "Please convert the object to a supported type or implement " + "a Converter for this type to allow the tree to be serialized." + ) + raise AsdfSerializationError(msg, obj) from err diff --git a/docs/conf.py b/docs/conf.py index a7d7699be..a570016bd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,12 @@ nitpicky = True +# ignore a few pyyaml docs links since they don't appear to support intersphinx +nitpick_ignore = [ + ("py:class", "yaml.representer.RepresenterError"), + ("py:class", "yaml.error.YAMLError"), +] + # Add intersphinx mappings intersphinx_mapping["semantic_version"] = ("https://python-semanticversion.readthedocs.io/en/latest/", None) intersphinx_mapping["jsonschema"] = ("https://python-jsonschema.readthedocs.io/en/stable/", None)