Skip to content

Commit

Permalink
Support for ASDF Standard requirements on extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Ed Slavich committed Aug 10, 2020
1 parent 15336c1 commit 3f54695
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 4 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- Drop support for Python 3.5. [#856]

- Add new extension API to support versioned extensions.
[#850]
[#850, #851]

2.7.0 (2020-07-23)
------------------
Expand Down
20 changes: 17 additions & 3 deletions asdf/asdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ def __init__(self, tree=None, uri=None, extensions=None, version=None,
files follow custom conventions beyond those enforced by the
standard.
"""
# Don't use the version setter here; it tries to access
# the extensions, which haven't been assigned yet.
if version is None:
self.version = get_config().default_version
self._version = versioning.AsdfVersion(get_config().default_version)
else:
self.version = version
self._version = versioning.AsdfVersion(validate_version(version))

self.extensions = extensions

Expand Down Expand Up @@ -190,6 +192,9 @@ def version(self, value):
value : str or asdf.versioning.AsdfVersion
"""
self._version = versioning.AsdfVersion(validate_version(value))
# Reassigning extensions cues the processing that checks each
# extension's ASDF Standard requirement.
self.extensions = self.extensions

@property
def version_string(self):
Expand Down Expand Up @@ -354,11 +359,20 @@ def _get_extension(e):

requested_extensions = [_get_extension(e) for e in requested_extensions]

for extension in requested_extensions:
if self.version_string not in extension.asdf_standard_requirement:
warnings.warn(
"Extension {} does not support ASDF Standard {}. It has been disabled.".format(
extension, self.version_string
),
AsdfWarning
)

extensions = []
# Add requested extensions to the list first, so that they
# take precedence.
for extension in requested_extensions + get_config().extensions:
if extension not in extensions:
if extension not in extensions and self.version_string in extension.asdf_standard_requirement:
extensions.append(extension)

return extensions
Expand Down
21 changes: 21 additions & 0 deletions asdf/extension/_extension.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from packaging.specifiers import SpecifierSet

from ..util import get_class_name
from ._legacy import AsdfExtension

Expand Down Expand Up @@ -34,6 +36,14 @@ def __init__(self, delegate, package_name=None, package_version=None):
else:
raise TypeError("Extension property 'legacy_class_names' must contain str values")

value = getattr(self._delegate, "asdf_standard_requirement", None)
if isinstance(value, str):
self._asdf_standard_requirement = SpecifierSet(value)
elif value is None:
self._asdf_standard_requirement = SpecifierSet()
else:
raise TypeError("Extension property 'asdf_standard_requirement' must be str or None")

if self._legacy:
self._legacy_class_names.add(self._class_name)

Expand Down Expand Up @@ -62,6 +72,17 @@ def legacy_class_names(self):
"""
return self._legacy_class_names

@property
def asdf_standard_requirement(self):
"""
Get the extension's ASDF Standard requirement.
Returns
-------
packaging.specifiers.SpecifierSet
"""
return self._asdf_standard_requirement

@property
def types(self):
"""
Expand Down
43 changes: 43 additions & 0 deletions asdf/tests/test_asdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ def __init__(
self,
extension_uri=None,
legacy_class_names=None,
asdf_standard_requirement=None,
types=None,
tag_mapping=None,
url_mapping=None,
):
self._extension_uri = extension_uri
self._legacy_class_names = set() if legacy_class_names is None else legacy_class_names
self._asdf_standard_requirement = asdf_standard_requirement
self._types = [] if types is None else types
self._tag_mapping = [] if tag_mapping is None else tag_mapping
self._url_mapping = [] if url_mapping is None else url_mapping
Expand All @@ -45,6 +47,10 @@ def extension_uri(self):
def legacy_class_names(self):
return self._legacy_class_names

@property
def asdf_standard_requirement(self):
return self._asdf_standard_requirement


def test_asdf_file_version():
with config_context() as config:
Expand Down Expand Up @@ -130,6 +136,43 @@ def test_asdf_file_extensions():
AsdfFile(extensions="not-a-URI")


def test_asdf_file_version_requirement():
extension_with_requirement = TestExtension(
extension_uri="asdf://somewhere.org/extensions/foo-1.0",
asdf_standard_requirement="==1.5.0",
)

# No warnings if the requirement is fulfilled:
with assert_no_warnings():
AsdfFile(version="1.5.0", extensions=[extension_with_requirement])

# Version doesn't match the requirement, so we should see a warning
# and the extension should not be enabled:
with pytest.warns(AsdfWarning, match="does not support ASDF Standard 1.4.0"):
af = AsdfFile(version="1.4.0", extensions=[extension_with_requirement])
assert ExtensionProxy(extension_with_requirement) not in af.extensions

# Version initially matches the requirement, but changing
# the version on the AsdfFile invalidates it:
af = AsdfFile(version="1.5.0", extensions=[extension_with_requirement])
assert ExtensionProxy(extension_with_requirement) in af.extensions
with pytest.warns(AsdfWarning, match="does not support ASDF Standard 1.4.0"):
af.version = "1.4.0"
assert ExtensionProxy(extension_with_requirement) not in af.extensions

# Extension registered with the config should not provoke
# a warning:
with config_context() as config:
config.add_extension(extension_with_requirement)
with assert_no_warnings():
af = AsdfFile(version="1.4.0")
assert ExtensionProxy(extension_with_requirement) not in af.extensions

# ... unless the user explicitly requested the invalid exception:
with pytest.warns(AsdfWarning, match="does not support ASDF Standard 1.4.0"):
af = AsdfFile(version="1.4.0", extensions=[extension_with_requirement])


def test_open_asdf_extensions(tmpdir):
extension = TestExtension(extension_uri="asdf://somewhere.org/extensions/foo-1.0")

Expand Down
3 changes: 3 additions & 0 deletions asdf/tests/test_extension.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from packaging.specifiers import SpecifierSet

from asdf.extension import (
BuiltinExtension,
Expand All @@ -9,6 +10,7 @@

from asdf.tests.helpers import assert_extension_correctness


def test_builtin_extension():
extension = BuiltinExtension()
assert_extension_correctness(extension)
Expand Down Expand Up @@ -42,6 +44,7 @@ def test_proxy_legacy():

assert proxy.extension_uri is None
assert proxy.legacy_class_names == {"asdf.tests.test_extension.LegacyExtension"}
assert proxy.asdf_standard_requirement == SpecifierSet()
assert proxy.types == [LegacyType]
assert proxy.tag_mapping == LegacyExtension.tag_mapping
assert proxy.url_mapping == LegacyExtension.url_mapping
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ install_requires =
jsonschema>=3.0.2,<4
numpy>=1.10
importlib_resources>=3;python_version<"3.9"
packaging>=16.0

[options.extras_require]
all =
Expand Down

0 comments on commit 3f54695

Please sign in to comment.