Skip to content

Commit

Permalink
Merge pull request #248 from linkml/issue-1012
Browse files Browse the repository at this point in the history
Imports to metamodel resolved locally rather than over network, #1102.
  • Loading branch information
cmungall authored Feb 23, 2023
2 parents 163d459 + e6e1aa8 commit eccc0a3
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 10 deletions.
8 changes: 8 additions & 0 deletions linkml_runtime/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from linkml_runtime.utils.curienamespace import CurieNamespace
from linkml_runtime.utils.schemaview import SchemaView
from rdflib import RDF, RDFS, SKOS, XSD, OWL
Expand All @@ -21,6 +22,13 @@
__version__ = importlib_metadata.version(__name__)


THIS_PATH = Path(__file__).parent

SCHEMA_DIRECTORY = THIS_PATH / "linkml_model" / "model" / "schema"

MAIN_SCHEMA_PATH = SCHEMA_DIRECTORY / "meta.yaml"


class MappingError(ValueError):
"""
An error when mapping elements of a LinkML model to runtime objects
Expand Down
15 changes: 14 additions & 1 deletion linkml_runtime/utils/context_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,24 @@ def to_file_uri(fname: str) -> str:


def map_import(importmap: Dict[str, str], namespaces: Callable[[None], "Namespaces"], imp: Any) -> str:
"""
lookup an import in an importmap.
:param importmap:
:param namespaces:
:param imp:
:return:
"""
sname = str(imp)
if ':' in sname:
# the importmap may contain mappings for prefixes
prefix, lname = sname.split(':', 1)
prefix += ':'
sname = importmap.get(prefix, prefix) + lname
expanded_prefix = importmap.get(prefix, prefix)
if expanded_prefix.startswith("http"):
sname = expanded_prefix + lname
else:
sname = os.path.join(expanded_prefix, lname)
sname = importmap.get(sname, sname) # Import map may use CURIE
sname = str(namespaces().uri_for(sname)) if ':' in sname else sname
return importmap.get(sname, sname) # It may also use URI or other forms
Expand Down
2 changes: 2 additions & 0 deletions linkml_runtime/utils/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ def uri_for(self, uri_or_curie: Any) -> URIRef:
uri_or_curie_str = str(uri_or_curie)
if '://' in uri_or_curie_str:
return URIRef(uri_or_curie_str)
if ':\\' in uri_or_curie_str: # Windows drive letters
return URIRef(uri_or_curie_str)
if ':' in uri_or_curie_str:
prefix, local = str(uri_or_curie_str).split(':', 1)
if not prefix:
Expand Down
40 changes: 36 additions & 4 deletions linkml_runtime/utils/schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ def load_schema_wrap(path: str, **kwargs):
yaml_loader = YAMLLoader()
schema: SchemaDefinition
schema = yaml_loader.load(path, target_class=SchemaDefinition, **kwargs)
schema.source_file = path
if "\n" not in path:
# only set path if the input is not a yaml string.
# Setting the source path is necessary for relative imports;
# while initializing a schema with a yaml string is possible, there
# should be no expectation of relative imports working.
schema.source_file = path
return schema


Expand Down Expand Up @@ -144,13 +149,40 @@ def namespaces(self) -> Namespaces:
return namespaces

def load_import(self, imp: str, from_schema: SchemaDefinition = None):
"""
Handles import directives.
The value of the import can be:
- a URL (specified as either a full URL or a CURIE)
- a local file path
The import should leave off the .yaml suffix.
If the import is a URL then the import is fetched over the network UNLESS this is a metamodel
import, in which case it is fetched from within the linkml_runtime package, where the yaml
is distributed. This ensures that the version of the metamodel imported matches the version
of the linkml_runtime package.
In future, this mechanism may be extended to arbitrary modules, such that we avoid
network dependence at runtime in general.
:param imp:
:param from_schema:
:return:
"""
if from_schema is None:
from_schema = self.schema
sname = map_import(self.importmap, self.namespaces, imp)
logging.info(f'Loading schema {sname} from {from_schema.source_file}')
from linkml_runtime import SCHEMA_DIRECTORY
default_import_map = {
"linkml:": str(SCHEMA_DIRECTORY)
}
importmap = {**default_import_map, **self.importmap}
sname = map_import(importmap, self.namespaces, imp)
logging.info(f'Importing {imp} as {sname} from source {from_schema.source_file}')
schema = load_schema_wrap(sname + '.yaml',
base_dir=os.path.dirname(
from_schema.source_file) if from_schema.source_file else None)
from_schema.source_file) if from_schema.source_file else None)
return schema

@lru_cache()
Expand Down
37 changes: 32 additions & 5 deletions tests/test_utils/test_schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \
ClassDefinitionName
from linkml_runtime.loaders.yaml_loader import YAMLLoader
from linkml_runtime.utils.introspection import package_schemaview, object_class_definition
from linkml_runtime.utils.introspection import package_schemaview
from linkml_runtime.utils.schemaview import SchemaView, SchemaUsage, OrderedBy
from linkml_runtime.utils.schemaops import roll_up, roll_down
from tests.test_utils import INPUT_DIR
Expand Down Expand Up @@ -471,6 +471,34 @@ def test_merge_imports(self):
all_c2_noi = copy(view.all_classes(imports=False))
self.assertEqual(len(all_c2_noi), len(all_c2))

def test_metamodel_imports(self):
"""
Tests imports of the metamodel.
Note: this test and others should be able to run without network connectivity.
SchemaView should make use of the version of the metamodel distributed with the package
over the network available version.
TODO: use mock testing framework to emulate no access to network.
- `<https://github.com/linkml/linkml/issues/502>`_
:return:
"""
schema = SchemaDefinition(id='test', name='metamodel-imports-test',
imports=["linkml:meta"])
sv = SchemaView(schema)
all_classes = sv.all_classes()
self.assertGreater(len(all_classes), 20)
schema_str = yaml_dumper.dumps(schema)
sv = SchemaView(schema_str)
self.assertGreater(len(sv.all_classes()), 20)
self.assertCountEqual(all_classes, sv.all_classes())






def test_traversal(self):
schema = SchemaDefinition(id='test', name='traversal-test')
view = SchemaView(schema)
Expand Down Expand Up @@ -689,7 +717,7 @@ def test_mergeimports(self):
self.assertIn("was generated by", slots_list)

prefixes_list = list(sv.schema.prefixes.keys())
self.assertListEqual(
self.assertCountEqual(
["pav",
"dce",
"lego",
Expand All @@ -701,9 +729,8 @@ def test_mergeimports(self):
"tax",
"core",
"prov",
"xsd",
"shex",
"schema"],
"xsd",
"shex"],
prefixes_list
)

Expand Down

0 comments on commit eccc0a3

Please sign in to comment.