diff --git a/asdf/_tests/test_extension.py b/asdf/_tests/test_extension.py index 1c351d326..22bcf6d99 100644 --- a/asdf/_tests/test_extension.py +++ b/asdf/_tests/test_extension.py @@ -1,4 +1,5 @@ import fractions +import sys import pytest from packaging.specifiers import SpecifierSet @@ -18,6 +19,7 @@ Validator, get_cached_extension_manager, ) +from asdf.extension._manager import _resolve_type from asdf.testing.helpers import roundtrip_object @@ -941,3 +943,66 @@ class FractionWithInverseExtension: read_f1 = roundtrip_object(f1) assert read_f1.inverse.inverse is read_f1 + + +def test_resolve_type_not_imported(): + path = "mailbox.Mailbox" + + if "mailbox" in sys.modules: + del sys.modules["mailbox"] + + assert _resolve_type(path) is None + + import mailbox + + assert _resolve_type(path) is mailbox.Mailbox + + +@pytest.mark.parametrize( + "path, obj", (("sys", sys), ("asdf.AsdfFile", AsdfFile), ("asdf.Missing", None), ("not_a_module", None)) +) +def test_resolve_type(path, obj): + assert _resolve_type(path) is obj + + +def test_extension_converter_by_class_path(): + class MailboxConverter: + tags = ["asdf://example.com/tags/mailbox-1.0.0"] + types = ["mailbox.Mailbox"] + + def to_yaml_tree(self, obj, tag, ctx): + return {} + + def from_yaml_tree(self, node, tag, ctx): + return None + + class MailboxExtension: + tags = MailboxConverter.tags + converters = [MailboxConverter()] + extension_uri = "asdf://example.com/extensions/mailbox-1.0.0" + + # grab the type so we can use it for extension_manager.get_converter_for_type + import mailbox + + typ = mailbox.Mailbox + del sys.modules["mailbox"], mailbox + + with config_context() as cfg: + cfg.add_extension(MailboxExtension()) + extension_manager = AsdfFile().extension_manager + + # make sure that registering the extension did not load the module + assert "mailbox" not in sys.modules + + # as the module hasn't been loaded, the converter shouldn't be found + with pytest.raises(KeyError, match="No support available for Python type 'mailbox.Mailbox'"): + extension_manager.get_converter_for_type(typ) + + # make sure inspecting the type didn't import the module + assert "mailbox" not in sys.modules + + # finally, import the module and check that the converter can now be found + import mailbox + + converter = extension_manager.get_converter_for_type(mailbox.Mailbox) + assert isinstance(converter.delegate, MailboxConverter) diff --git a/asdf/extension/_manager.py b/asdf/extension/_manager.py index 634231b1e..cb6a10d2c 100644 --- a/asdf/extension/_manager.py +++ b/asdf/extension/_manager.py @@ -8,11 +8,26 @@ def _resolve_type(path): + """ + Convert a class path (like the string "asdf.AsdfFile") to a + class (``asdf.AsdfFile``) only if the module implementing the + class has already been imported. + + Parameters + ---------- + + path : str + Path/name of class (for example, "asdf.AsdfFile") + + Returns + ------- + + typ : class or None + The class (if it's already been imported) or None + """ if "." not in path: - # this path does not appear to include a module - if path in globals(): - return globals()[path] - elif path in sys.modules: + # check if this path is a module + if path in sys.modules: return sys.modules[path] return None # this type is part of a module