diff --git a/CHANGES.rst b/CHANGES.rst index a20b4e3e8..5945a2666 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) ------------------ diff --git a/asdf/asdf.py b/asdf/asdf.py index 3547cc2f4..f2275122a 100644 --- a/asdf/asdf.py +++ b/asdf/asdf.py @@ -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 @@ -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): @@ -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 diff --git a/asdf/extension/_extension.py b/asdf/extension/_extension.py index 683a1aa48..8ce9e2564 100644 --- a/asdf/extension/_extension.py +++ b/asdf/extension/_extension.py @@ -1,3 +1,5 @@ +from packaging.specifiers import SpecifierSet + from ..util import get_class_name from ._legacy import AsdfExtension @@ -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) @@ -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): """ diff --git a/asdf/tests/test_asdf.py b/asdf/tests/test_asdf.py index f366a7b4e..54e96d3a1 100644 --- a/asdf/tests/test_asdf.py +++ b/asdf/tests/test_asdf.py @@ -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 @@ -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: @@ -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") diff --git a/asdf/tests/test_extension.py b/asdf/tests/test_extension.py index c8ad8e9c0..2c0c0db7c 100644 --- a/asdf/tests/test_extension.py +++ b/asdf/tests/test_extension.py @@ -1,4 +1,5 @@ import pytest +from packaging.specifiers import SpecifierSet from asdf.extension import ( BuiltinExtension, @@ -9,6 +10,7 @@ from asdf.tests.helpers import assert_extension_correctness + def test_builtin_extension(): extension = BuiltinExtension() assert_extension_correctness(extension) @@ -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 diff --git a/setup.cfg b/setup.cfg index 938cc20a1..acd3d104d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 =