Repository: spacetelescope/asdf
Pull request #716 (2019-12-04 17:48:27)

Refresh NDArrayType when underlying mmap is closed
This is the start of a fix for https://github.com/spacetelescope/asdf/issues/711.

When an asdf file is updated, we call [calculate_updated_layout](https://github.com/spacetelescope/asdf/blob/cfa6a039d2e8c5dc93f2fd52e7d0faa9c7b16d51/asdf/block.py#L1253), which shuffles blocks around the file as needed to make room for the expansion of the tree and changes to the other blocks.  When a block gets moved, its underlying memory map is closed and then reopened in the new location.

This is a problem for NDArrayType, which keeps a [memoized copy](https://github.com/spacetelescope/asdf/blob/d8d8b3b62b7a3a6e0ce06dc83708b4c4fb807935/asdf/tags/core/ndarray.py#L251) of an ndarray that is built on top of that mmap.  The segfault occurs when the tree calls `__repr__` on the ndarray.

This PR gives NDArrayType the ability to detect that the block has been moved, and rebuild the ndarray in that case.  It does fix the issue as reported but it might be worth revisiting whether the optimizations that cause this problem are even necessary?  Maybe we don't need to memoize the ndarray at all, or maybe it's not really necessary to be able to update a small part of the asdf file without rewriting the entire thing.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 95a057d4..f229f5ab 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,12 +2,16 @@
 ------------------
 
 - Added asdf-standard 1.4.0 to the list of supported versions. [#704]
+- Fix load_schema LRU cache memory usage issue [#682]
+- Add convenience method for fetching the default resolver [#682]
 
 - ``SpecItem`` and ``Spec`` were depreicated  in ``semantic_version``
   and were replaced with ``SimpleSpec``. [#715]
 
 - Pinned the minimum required ``semantic_version`` to 2.8. [#715]
 
+- Fix bug causing segfault after update of a memory-mapped file. [#716]
+
 2.4.2 (2019-08-29)
 ------------------
 
diff --git a/asdf/asdf.py b/asdf/asdf.py
index 46bea72d..0879c74b 100644
--- a/asdf/asdf.py
+++ b/asdf/asdf.py
@@ -313,8 +313,9 @@ class AsdfFile(versioning.VersionedMixin):
     def url_mapping(self):
         return self._extensions.url_mapping
 
-    def resolver(self, uri):
-        return self.url_mapping(self.tag_mapping(uri))
+    @property
+    def resolver(self):
+        return self._extensions.resolver
 
     @property
     def type_index(self):
diff --git a/asdf/extension.py b/asdf/extension.py
index da920dcd..33f88b43 100644
--- a/asdf/extension.py
+++ b/asdf/extension.py
@@ -137,6 +137,7 @@ class AsdfExtensionList:
                     validators.update(sibling.validators)
         self._tag_mapping = resolver.Resolver(tag_mapping, 'tag')
         self._url_mapping = resolver.Resolver(url_mapping, 'url')
+        self._resolver = resolver.ResolverChain(self._tag_mapping, self._url_mapping)
         self._validators = validators
 
     @property
@@ -156,6 +157,10 @@ class AsdfExtensionList:
     def url_mapping(self):
         return self._url_mapping
 
+    @property
+    def resolver(self):
+        return self._resolver
+
     @property
     def type_index(self):
         return self._type_index
@@ -244,10 +249,16 @@ class _DefaultExtensions:
         self._extension_list = None
         self._package_metadata = {}
 
-    def resolver(self, uri):
-        tag_mapping = self.extension_list.tag_mapping
-        url_mapping = self.extension_list.url_mapping
-        return url_mapping(tag_mapping(uri))
+    @property
+    def resolver(self):
+        return self.extension_list.resolver
 
 
 default_extensions = _DefaultExtensions()
+
+
+def get_default_resolver():
+    """
+    Get the resolver that includes mappings from all installed extensions.
+    """
+    return default_extensions.resolver
diff --git a/asdf/resolver.py b/asdf/resolver.py
index df71d475..c80b1d06 100644
--- a/asdf/resolver.py
+++ b/asdf/resolver.py
@@ -3,9 +3,11 @@
 
 import sys
 import os.path
+import warnings
 
 from . import constants
 from . import util
+from .exceptions import AsdfDeprecationWarning
 
 
 def find_schema_path():
@@ -24,11 +26,11 @@ class Resolver:
     A class that can be used to map strings with a particular prefix
     to another.
     """
-    def __init__(self, mapping=[], prefix=''):
+    def __init__(self, mappings, prefix):
         """
         Parameters
         ----------
-        mapping : list of tuple or callable, optional
+        mappings : list of tuple or callable
             A list of mappings to try, in order.
             For each entry:
 
@@ -48,63 +50,104 @@ class Resolver:
               - ``{X_suffix}``: The part of the string following the
                 prefix.
 
-        prefix : str, optional
+        prefix : str
             The prefix to use for the Python formatting token names.
         """
-        self._mapping = tuple()
-        if mapping:
-            self.add_mapping(mapping, prefix)
+        self._mappings = self._validate_mappings(mappings)
+        self._prefix = prefix
 
-    def add_mapping(self, mapping, prefix=''):
-        self._mapping = self._mapping + self._validate_mapping(mapping, prefix)
 
-    def _make_map_func(self, mapping, prefix):
-        def _map_func(uri):
-            if uri.startswith(mapping[0]):
+    def add_mapping(self, mappings, prefix=''):
+        # Deprecating this because Resolver is used as part of a dictionary key
+        # and so shouldn't be mutable.
+        assert prefix == self._prefix
+
+        warnings.warn("The 'add_mapping' method is deprecated.", AsdfDeprecationWarning)
+        self._mappings = self._mappings + self._validate_mappings(mappings)
+
+
+    def _perform_mapping(self, mapping, input):
+        if callable(mapping):
+            output = mapping(input)
+            if output is not None:
+                return (sys.maxsize, mapping(input))
+            else:
+                return None
+        else:
+            if input.startswith(mapping[0]):
                 format_tokens = {
-                    prefix: uri,
-                    prefix + "_prefix": mapping[0],
-                    prefix + "_suffix": uri[len(mapping[0]):]
+                    self._prefix: input,
+                    self._prefix + "_prefix": mapping[0],
+                    self._prefix + "_suffix": input[len(mapping[0]):]
                 }
 
                 return len(mapping[0]), mapping[1].format(**format_tokens)
-            return None
-        return _map_func
+            else:
+                return None
 
-    def _validate_mapping(self, mappings, prefix):
+    def _validate_mappings(self, mappings):
         normalized = []
         for mapping in mappings:
             if callable(mapping):
-                func = mapping
+                normalized.append(mapping)
             elif (isinstance(mapping, (list, tuple)) and
                   len(mapping) == 2 and
                   isinstance(mapping[0], str) and
                   isinstance(mapping[1], str)):
-
-                func = self._make_map_func(mapping, prefix)
+                normalized.append(tuple(mapping))
             else:
                 raise ValueError("Invalid mapping '{0}'".format(mapping))
 
-            normalized.append(func)
-
         return tuple(normalized)
 
+
     def __call__(self, input):
-        candidates = []
-        for mapper in self._mapping:
-            output = mapper(input)
-            if isinstance(output, tuple):
+        candidates = [(0, input)]
+        for mapping in self._mappings:
+            output = self._perform_mapping(mapping, input)
+            if output is not None:
                 candidates.append(output)
-            elif output is not None:
-                candidates.append((sys.maxsize, output))
-        if len(candidates):
-            candidates.sort()
-            return candidates[-1][1]
-        else:
-            return input
+
+        candidates.sort()
+        return candidates[-1][1]
 
     def __hash__(self):
-        return hash(self._mapping)
+        return hash(self._mappings)
+
+    def __eq__(self, other):
+        if not isinstance(other, Resolver):
+            return NotImplemented
+
+        return self._mappings == other._mappings
+
+
+class ResolverChain:
+    """
+    A chain of Resolvers, each of which is called with the previous Resolver's
+    output to produce the final transformed string.
+    """
+    def __init__(self, *resolvers):
+        """
+        Parameters
+        ----------
+        *resolvers : list of Resolver
+            Resolvers to include in the chain.
+        """
+        self._resolvers = tuple(resolvers)
+
+    def __call__(self, input):
+        for resolver in self._resolvers:
+            input = resolver(input)
+        return input
+
+    def __hash__(self):
+        return hash(self._resolvers)
+
+    def __eq__(self, other):
+        if not isinstance(other, ResolverChain):
+            return NotImplemented
+
+        return self._resolvers == other._resolvers
 
 
 DEFAULT_URL_MAPPING = [
@@ -117,9 +160,20 @@ DEFAULT_TAG_TO_URL_MAPPING = [
      'http://stsci.edu/schemas/asdf{tag_suffix}')
 ]
 
+def default_url_mapping(uri):
+    warnings.warn("'default_url_mapping' is deprecated.", AsdfDeprecationWarning)
+    return default_url_mapping._resolver(uri)
+default_url_mapping._resolver = Resolver(DEFAULT_URL_MAPPING, 'url')
 
-default_url_mapping = Resolver(DEFAULT_URL_MAPPING, 'url')
-default_tag_to_url_mapping = Resolver(DEFAULT_TAG_TO_URL_MAPPING, 'tag')
+def default_tag_to_url_mapping(uri):
+    warnings.warn("'default_tag_to_url_mapping' is deprecated.", AsdfDeprecationWarning)
+    return default_tag_to_url_mapping._resolver(uri)
+default_tag_to_url_mapping._resolver = Resolver(DEFAULT_TAG_TO_URL_MAPPING, 'tag')
 
 def default_resolver(uri):
-    return default_url_mapping(default_tag_to_url_mapping(uri))
+    warnings.warn(
+        "The 'default_resolver(...)' function is deprecated. Use "
+        "'asdf.extension.get_default_resolver()(...)' instead.",
+        AsdfDeprecationWarning)
+    return default_resolver._resolver(uri)
+default_resolver._resolver = ResolverChain(default_tag_to_url_mapping._resolver, default_url_mapping._resolver)
diff --git a/asdf/schema.py b/asdf/schema.py
index 1f26c0f6..fb181102 100644
--- a/asdf/schema.py
+++ b/asdf/schema.py
@@ -5,6 +5,7 @@ import os
 import json
 import datetime
 import warnings
+import copy
 from numbers import Integral
 from functools import lru_cache
 from collections import OrderedDict
@@ -23,6 +24,8 @@ from . import treeutil
 from . import util
 from .extension import default_extensions
 from .compat.jsonschemacompat import JSONSCHEMA_LT_3
+from . import extension
+from .exceptions import AsdfDeprecationWarning
 
 
 YAML_SCHEMA_METASCHEMA_ID = 'http://stsci.edu/schemas/yaml-schema/draft-01'
@@ -41,9 +44,13 @@ def default_ext_resolver(uri):
     """
     Resolver that uses tag/url mappings from all installed extensions
     """
-    tag_mapping = default_extensions.extension_list.tag_mapping
-    url_mapping = default_extensions.extension_list.url_mapping
-    return url_mapping(tag_mapping(uri))
+    # Deprecating this because it doesn't play nicely with the caching on
+    # load_schema(...).
+    warnings.warn(
+            "The 'default_ext_resolver(...)' function is deprecated. Use "
+            "'asdf.extension.get_default_resolver()(...)' instead.",
+            AsdfDeprecationWarning)
+    return extension.get_default_resolver()(uri)
 
 
 PYTHON_TYPE_TO_YAML_TAG = {
@@ -199,10 +206,9 @@ for key in ('allOf', 'anyOf', 'oneOf', 'items'):
 REMOVE_DEFAULTS['properties'] = validate_remove_default
 
 
-
 @lru_cache()
 def _create_validator(validators=YAML_VALIDATORS):
-    meta_schema = load_schema(YAML_SCHEMA_METASCHEMA_ID, default_ext_resolver)
+    meta_schema = load_schema(YAML_SCHEMA_METASCHEMA_ID, extension.get_default_resolver())
 
     if JSONSCHEMA_LT_3:
         base_cls = mvalidators.create(meta_schema=meta_schema, validators=validators)
@@ -358,7 +364,7 @@ def load_custom_schema(url):
 
     return update(custom, core)
 
-@lru_cache()
+
 def load_schema(url, resolver=None, resolve_references=False,
                 resolve_local_refs=False):
     """
@@ -385,7 +391,20 @@ def load_schema(url, resolver=None, resolve_references=False,
         control local reference resolution separately.
     """
     if resolver is None:
-        resolver = default_ext_resolver
+        # We can't just set this as the default in load_schema's definition
+        # because invoking get_default_resolver at import time leads to a circular import.
+        resolver = extension.get_default_resolver()
+
+    # We want to cache the work that went into constructing the schema, but returning
+    # the same object is treacherous, because users who mutate the result will not
+    # expect that they're changing the schema everywhere.
+    return copy.deepcopy(
+        _load_schema_cached(url, resolver, resolve_references, resolve_local_refs)
+    )
+
+
+@lru_cache()
+def _load_schema_cached(url, resolver, resolve_references, resolve_local_refs):
     loader = _make_schema_loader(resolver)
     if url in HARDCODED_SCHEMA:
         schema = HARDCODED_SCHEMA[url]()
@@ -609,9 +628,9 @@ def check_schema(schema):
     })
 
     meta_schema_id = schema.get('$schema', YAML_SCHEMA_METASCHEMA_ID)
-    meta_schema = load_schema(meta_schema_id, default_ext_resolver)
+    meta_schema = load_schema(meta_schema_id, extension.get_default_resolver())
 
-    resolver = _make_resolver(default_ext_resolver)
+    resolver = _make_resolver(extension.get_default_resolver())
 
     cls = mvalidators.create(meta_schema=meta_schema,
                              validators=VALIDATORS)
diff --git a/asdf/tags/core/ndarray.py b/asdf/tags/core/ndarray.py
index 5cd8fde4..cf2000a7 100644
--- a/asdf/tags/core/ndarray.py
+++ b/asdf/tags/core/ndarray.py
@@ -2,6 +2,7 @@
 # -*- coding: utf-8 -*-
 
 import sys
+import weakref
 
 import numpy as np
 from numpy import ma
@@ -221,6 +222,7 @@ class NDArrayType(AsdfType):
         self._asdffile = asdffile
         self._source = source
         self._block = None
+        self._block_data_weakref = None
         self._array = None
         self._mask = mask
 
@@ -244,6 +246,17 @@ class NDArrayType(AsdfType):
             self._make_array()
 
     def _make_array(self):
+        # If the ASDF file has been updated in-place, then there's
+        # a chance that the block's original data object has been
+        # closed and replaced.  We need to check here and re-generate
+        # the array if necessary, otherwise we risk segfaults when
+        # memory mapping.
+        if self._block_data_weakref is not None:
+            block_data = self._block_data_weakref()
+            if block_data is not self.block.data:
+                self._array = None
+                self._block_data_weakref = None
+
         if self._array is None:
             block = self.block
             shape = self.get_actual_shape(
@@ -251,6 +264,7 @@ class NDArrayType(AsdfType):
             self._array = np.ndarray(
                 shape, self._dtype, block.data,
                 self._offset, self._strides, self._order)
+            self._block_data_weakref = weakref.ref(block.data)
             self._array = self._apply_mask(self._array, self._mask)
             if block.readonly:
                 self._array.setflags(write=False)
@@ -353,6 +367,7 @@ class NDArrayType(AsdfType):
             self._make_array().__setitem__(*args)
         except Exception as e:
             self._array = None
+            self._block_data_weakref = None
             raise e from None
 
     @classmethod


Test patch:
diff --git a/asdf/tags/core/tests/test_ndarray.py b/asdf/tags/core/tests/test_ndarray.py
index a660d373..edae4609 100644
--- a/asdf/tags/core/tests/test_ndarray.py
+++ b/asdf/tags/core/tests/test_ndarray.py
@@ -858,7 +858,6 @@ def test_readonly(tmpdir):
 
 
 def test_readonly_inline(tmpdir):
-
     tmpfile = str(tmpdir.join('data.asdf'))
     tree = dict(data=np.ndarray((100)))
 
@@ -869,3 +868,18 @@ def test_readonly_inline(tmpdir):
     with asdf.open(tmpfile, mode='r') as af:
         assert af['data'].flags.writeable == True
         af['data'][0] = 42
+
+
+# Confirm that NDArrayType's internal array is regenerated
+# following an update.
+def test_block_data_change(tmpdir):
+    tmpfile = str(tmpdir.join("data.asdf"))
+    tree = {"data": np.ndarray(10)}
+    with asdf.AsdfFile(tree) as af:
+        af.write_to(tmpfile)
+
+    with asdf.open(tmpfile, mode="rw") as af:
+        array_before = af.tree["data"].__array__()
+        af.update()
+        array_after = af.tree["data"].__array__()
+        assert array_before is not array_after
diff --git a/asdf/tests/test_api.py b/asdf/tests/test_api.py
index 657fcd52..51407c70 100644
--- a/asdf/tests/test_api.py
+++ b/asdf/tests/test_api.py
@@ -13,6 +13,8 @@ import pytest
 import asdf
 from asdf import treeutil
 from asdf import extension
+from asdf import resolver
+from asdf import schema
 from asdf import versioning
 from asdf.exceptions import AsdfDeprecationWarning
 from .helpers import assert_tree_match, assert_roundtrip_tree, display_warnings
@@ -408,3 +410,22 @@ def test_inline_threshold_override(tmpdir):
         af.write_to(tmpfile, all_array_storage='inline')
         assert len(list(af.blocks.inline_blocks)) == 2
         assert len(list(af.blocks.internal_blocks)) == 0
+
+
+def test_resolver_deprecations():
+    for resolver_method in [
+        resolver.default_resolver,
+        resolver.default_tag_to_url_mapping,
+        resolver.default_url_mapping,
+        schema.default_ext_resolver
+    ]:
+        with pytest.warns(AsdfDeprecationWarning):
+            resolver_method("foo")
+
+
+def test_get_default_resolver():
+    resolver = extension.get_default_resolver()
+
+    result = resolver('tag:stsci.edu:asdf/core/ndarray-1.0.0')
+
+    assert result.endswith("asdf-standard/schemas/stsci.edu/asdf/core/ndarray-1.0.0.yaml")
diff --git a/asdf/tests/test_resolver.py b/asdf/tests/test_resolver.py
new file mode 100644
index 00000000..50c0aeb0
--- /dev/null
+++ b/asdf/tests/test_resolver.py
@@ -0,0 +1,109 @@
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from asdf.resolver import Resolver, ResolverChain
+from asdf.exceptions import AsdfDeprecationWarning
+
+def test_resolver_no_mappings():
+    r = Resolver([], "test")
+    assert r("united_states:maryland:baltimore") == "united_states:maryland:baltimore"
+
+
+def test_resolver_tuple_mapping():
+    r = Resolver([("united_states:", "earth:{test}")], "test")
+    assert r("united_states:maryland:baltimore") == "earth:united_states:maryland:baltimore"
+
+    r = Resolver([("united_states:", "{test_prefix}texas:houston")], "test")
+    assert r("united_states:maryland:baltimore") == "united_states:texas:houston"
+
+    r = Resolver([("united_states:", "{test_suffix}:hampden")], "test")
+    assert r("united_states:maryland:baltimore") == "maryland:baltimore:hampden"
+
+
+def test_resolver_callable_mapping():
+    r = Resolver([lambda inp: "nowhere"], "test")
+    assert r("united_states:maryland:baltimore") == "nowhere"
+
+
+def test_resolver_multiple_mappings():
+    r = Resolver([
+        ("united_states:", "unknown_region:{test_suffix}"),
+        ("united_states:maryland:", "mid_atlantic:maryland:{test_suffix}")
+        ], "test")
+    # Should choose the mapping with the longest matched prefix:
+    assert r("united_states:maryland:baltimore") == "mid_atlantic:maryland:baltimore"
+
+    r = Resolver([
+        ("united_states:", "unknown_region:{test_suffix}"),
+        lambda inp: "nowhere",
+        ("united_states:maryland:", "mid_atlantic:maryland:{test_suffix}")
+        ], "test")
+    # Should prioritize the mapping offered by the callable:
+    assert r("united_states:maryland:baltimore") == "nowhere"
+
+    r = Resolver([
+        ("united_states:", "unknown_region:{test_suffix}"),
+        lambda inp: None,
+        ("united_states:maryland:", "mid_atlantic:maryland:{test_suffix}")
+        ], "test")
+    # None from the callable is a signal that it can't handle the input,
+    # so we should fall back to the longest matched prefix:
+    assert r("united_states:maryland:baltimore") == "mid_atlantic:maryland:baltimore"
+
+
+def test_resolver_non_prefix():
+    r = Resolver([("maryland:", "shouldn't happen")], "test")
+    assert r("united_states:maryland:baltimore") == "united_states:maryland:baltimore"
+
+
+def test_resolver_invalid_mapping():
+    with pytest.raises(ValueError):
+        Resolver([("foo",)], "test")
+
+    with pytest.raises(ValueError):
+        Resolver([12], "test")
+
+
+def test_resolver_hash_and_equals():
+    r1 = Resolver([("united_states:", "earth:{test}")], "test")
+    r2 = Resolver([("united_states:", "earth:{test}")], "test")
+    r3 = Resolver([("united_states:", "{test}:hampden")], "test")
+
+    assert hash(r1) == hash(r2)
+    assert r1 == r2
+
+    assert hash(r1) != hash(r3)
+    assert r1 != r3
+
+
+def test_resolver_add_mapping_deprecated():
+    r = Resolver([], "test")
+    with pytest.warns(AsdfDeprecationWarning):
+        r.add_mapping([("united_states:", "earth:{test}")], "test")
+
+
+def test_resolver_chain():
+    r1 = Resolver([("maryland:", "united_states:{test}")], "test")
+    r2 = Resolver([("united_states:", "earth:{test}")], "test")
+
+    chain = ResolverChain(r1, r2)
+
+    assert chain("maryland:baltimore") == "earth:united_states:maryland:baltimore"
+
+
+def test_resolver_chain_hash_and_equals():
+    r1 = Resolver([("united_states:", "earth:{test}")], "test")
+    r2 = Resolver([("united_states:", "earth:{test}")], "test")
+    r3 = Resolver([("united_states:", "{test}:hampden")], "test")
+
+    c1 = ResolverChain(r1, r3)
+    c2 = ResolverChain(r2, r3)
+    c3 = ResolverChain(r1, r2)
+
+    assert hash(c1) == hash(c2)
+    assert c1 == c2
+
+    assert hash(c1) != hash(c3)
+    assert c1 != c3
diff --git a/asdf/tests/test_schema.py b/asdf/tests/test_schema.py
index 9ae4e574..bbb713b4 100644
--- a/asdf/tests/test_schema.py
+++ b/asdf/tests/test_schema.py
@@ -162,7 +162,7 @@ properties:
 
 required: [foobar]
 ...
-    """.format(resolver.default_resolver('tag:stsci.edu:asdf/core/ndarray-1.0.0'))
+    """.format(extension.get_default_resolver()('tag:stsci.edu:asdf/core/ndarray-1.0.0'))
     schema_path = tmpdir.join('nugatory.yaml')
     schema_path.write(schema_def.encode())
 
@@ -171,15 +171,27 @@ required: [foobar]
 
 
 def test_schema_caching():
-    # Make sure that if we request the same URL, we get the *exact
-    # same* object, to ensure the cache is working.
+    # Make sure that if we request the same URL, we get a different object
+    # (despite the caching internal to load_schema).  Changes to a schema
+    # dict should not impact other uses of that schema.
+
     s1 = schema.load_schema(
         'http://stsci.edu/schemas/asdf/core/asdf-1.0.0')
     s2 = schema.load_schema(
         'http://stsci.edu/schemas/asdf/core/asdf-1.0.0')
-    assert s1 is s2
+    assert s1 is not s2
+
+
+def test_asdf_file_resolver_hashing():
+    # Confirm that resolvers from distinct AsdfFile instances
+    # hash to the same value (this allows schema caching to function).
+    a1 = asdf.AsdfFile()
+    a2 = asdf.AsdfFile()
 
+    assert hash(a1.resolver) == hash(a2.resolver)
+    assert a1.resolver == a2.resolver
 
+    
 def test_flow_style():
     class CustomFlowStyleType(dict, types.CustomType):
         name = 'custom_flow'
@@ -452,9 +464,9 @@ def test_self_reference_resolution():
 
 def test_schema_resolved_via_entry_points():
     """Test that entry points mappings to core schema works"""
-    r = asdf.AsdfFile().resolver
+    r = extension.get_default_resolver()
     tag = types.format_tag('stsci.edu', 'asdf', '1.0.0', 'fits/fits')
-    url = resolver.default_tag_to_url_mapping(tag)
+    url = extension.default_extensions.extension_list.tag_mapping(tag)
 
     s = schema.load_schema(url, resolver=r, resolve_references=True)
     assert tag in repr(s)