Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record manifest providing package in history, use specific warning for _check_extensions #1758

Merged
merged 5 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

- Deprecate ``asdf.util.filepath_to_url`` use ``pathlib.Path.to_uri`` [#1735]

- Record package providing manifest for extensions used to write
a file and ``AsdfPackageVersionWarning`` when installed extension/manifest
package does not match that used to write the file [#1758]


3.2.0 (2024-04-05)
------------------
Expand Down
40 changes: 38 additions & 2 deletions asdf/_asdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .exceptions import (
AsdfConversionWarning,
AsdfDeprecationWarning,
AsdfPackageVersionWarning,
AsdfWarning,
DelimiterNotFoundError,
ValidationError,
Expand Down Expand Up @@ -356,7 +357,7 @@ def _check_extensions(self, tree, strict=False):
if strict:
raise RuntimeError(msg)

warnings.warn(msg, AsdfWarning)
warnings.warn(msg, AsdfPackageVersionWarning)

elif extension.software:
# Local extensions may not have a real version. If the package name changed,
Expand All @@ -372,7 +373,33 @@ def _check_extensions(self, tree, strict=False):
if strict:
raise RuntimeError(msg)

warnings.warn(msg, AsdfWarning)
warnings.warn(msg, AsdfPackageVersionWarning)

# check version of manifest providing package (if one was recorded)
if "manifest_software" in extension:
package_name = extension["manifest_software"]["name"]
package_version = Version(extension["manifest_software"]["version"])
package_description = f"{package_name}=={package_version}"
installed_version = None
for mapping in get_config().resource_manager._resource_mappings:
if mapping.package_name == package_name:
installed_version = Version(mapping.package_version)
break
msg = None
if installed_version is None:
msg = (
f"File {filename}was created with package {package_description}, "
"which is currently not installed"
)
elif installed_version < package_version:
msg = (
f"File {filename}was created with package {package_description}, "
f"but older package({package_name}=={installed_version}) is installed."
)
if msg:
if strict:
raise RuntimeError(msg)
warnings.warn(msg, AsdfPackageVersionWarning)

def _process_plugin_extensions(self):
"""
Expand Down Expand Up @@ -460,6 +487,15 @@ def _update_extension_history(self, tree, serialization_context):
ext_meta["extension_uri"] = extension.extension_uri
if extension.compressors:
ext_meta["supported_compression"] = [comp.label.decode("ascii") for comp in extension.compressors]
manifest = getattr(extension._delegate, "_manifest", None)
if manifest is not None:
# check if this extension was built from a manifest is a different package
resource_mapping = get_config().resource_manager._mappings_by_uri.get(manifest["id"])
if resource_mapping.package_name != extension.package_name:
ext_meta["manifest_software"] = Software(
name=resource_mapping.package_name,
version=resource_mapping.package_version,
)

for i, entry in enumerate(tree["history"]["extensions"]):
# Update metadata about this extension if it already exists
Expand Down
59 changes: 56 additions & 3 deletions asdf/_tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

import asdf
from asdf import config_context, get_config, treeutil, versioning
from asdf.exceptions import AsdfDeprecationWarning, AsdfWarning, ValidationError
from asdf.exceptions import AsdfDeprecationWarning, AsdfPackageVersionWarning, ValidationError
from asdf.extension import ExtensionProxy
from asdf.resource import ResourceMappingProxy
from asdf.testing.helpers import roundtrip_object, yaml_to_asdf

from ._helpers import assert_tree_match
Expand Down Expand Up @@ -284,6 +285,7 @@ def test_open_pathlib_path(tmp_path):
@pytest.mark.parametrize(
("installed", "extension", "warns"),
[
(None, "2.0.0", True),
("1.2.3", "2.0.0", True),
("1.2.3", "2.0.dev10842", True),
("2.0.0", "2.0.0", False),
Expand All @@ -298,7 +300,8 @@ class FooExtension:
proxy = ExtensionProxy(FooExtension(), package_name="foo", package_version=installed)

with config_context() as config:
config.add_extension(proxy)
if installed is not None:
config.add_extension(proxy)
af = asdf.AsdfFile()

af._fname = "test.asdf"
Expand All @@ -315,7 +318,7 @@ class FooExtension:
}

if warns:
with pytest.warns(AsdfWarning, match=r"File 'test.asdf' was created with"):
with pytest.warns(AsdfPackageVersionWarning, match=r"File 'test.asdf' was created with"):
af._check_extensions(tree)

with pytest.raises(RuntimeError, match=r"^File 'test.asdf' was created with"):
Expand All @@ -325,6 +328,56 @@ class FooExtension:
af._check_extensions(tree)


@pytest.mark.parametrize(
("installed", "extension", "warns"),
[
(None, "2.0.0", True),
("1.2.3", "2.0.0", True),
("1.2.3", "2.0.dev10842", True),
("2.0.0", "2.0.0", False),
("2.0.1", "2.0.0", False),
("2.0.1", "2.0.dev12345", False),
],
)
def test_check_extension_manifest_software(installed, extension, warns):
class FooExtension:
extension_uri = "asdf://somewhere.org/extensions/foo-1.0.0"

proxy = ExtensionProxy(FooExtension(), package_name="foo", package_version="1.0.0")

mapping = ResourceMappingProxy({}, package_name="bar", package_version=installed)

with config_context() as config:
config.add_extension(proxy)
if installed is not None:
config.add_resource_mapping(mapping)
af = asdf.AsdfFile()

af._fname = "test.asdf"

tree = {
"history": {
"extensions": [
asdf.tags.core.ExtensionMetadata(
extension_uri=FooExtension.extension_uri,
software=asdf.tags.core.Software(name="foo", version="1.0.0"),
manifest_software=asdf.tags.core.Software(name="bar", version=extension),
),
],
},
}

if warns:
with pytest.warns(AsdfPackageVersionWarning, match=r"File 'test.asdf' was created with"):
af._check_extensions(tree)

with pytest.raises(RuntimeError, match=r"^File 'test.asdf' was created with"):
af._check_extensions(tree, strict=True)

else:
af._check_extensions(tree)


def test_extension_check_no_warning_on_builtin():
"""
Prior to asdf 3.0 files were written using the asdf.extension.BuiltinExtension
Expand Down
6 changes: 6 additions & 0 deletions asdf/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ class AsdfProvisionalAPIWarning(AsdfWarning, FutureWarning):
are likely to be added in a future ASDF version. However, Use of
provisional features is highly discouraged for production code.
"""


class AsdfPackageVersionWarning(AsdfWarning):
"""
A warning indicating a package version mismatch
"""
Loading