diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index 3d291a57..9c1cc174 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -9,6 +9,7 @@ from pathlib import Path, PurePath from typing import Mapping, Optional, Tuple, TypeVar import warnings +from urllib.parse import urlparse from linkml_runtime.utils.namespaces import Namespaces from deprecated.classic import deprecated @@ -106,6 +107,25 @@ def is_absolute_path(path: str) -> bool: drive, tail = os.path.splitdrive(norm_path) return bool(drive and tail) +def _resolve_import(source_sch: str, imported_sch: str) -> str: + if os.path.isabs(imported_sch): + # Absolute import paths are not modified + return imported_sch + if urlparse(imported_sch).scheme: + # File with URL schemes are not modified + return imported_sch + + if WINDOWS: + path = PurePath(os.path.normpath(PurePath(source_sch).parent / imported_sch)).as_posix() + else: + path = os.path.normpath(str(Path(source_sch).parent / imported_sch)) + + if imported_sch.startswith(".") and not path.startswith("."): + # Above condition handles cases where both source schema and imported schema are relative paths: these should remain relative + return f"./{path}" + + return path + @dataclass class SchemaUsage(): @@ -305,12 +325,7 @@ def imports_closure(self, imports: bool = True, traverse: Optional[bool] = None, # - subdir/types.yaml # we should treat the two `types.yaml` as separate schemas from the POV of the # origin schema. - if sn.startswith('.') and ':' not in i: - if WINDOWS: - # This cannot be simplified. os.path.normpath() must be called before .as_posix() - i = PurePath(os.path.normpath(PurePath(sn).parent / i)).as_posix() - else: - i = os.path.normpath(str(Path(sn).parent / i)) + i = _resolve_import(sn, i) todo.append(i) # add item to closure diff --git a/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/L4_2_0_0_0/greatgrandchild.yaml b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/L4_2_0_0_0/greatgrandchild.yaml new file mode 100644 index 00000000..091a18df --- /dev/null +++ b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/L4_2_0_0_0/greatgrandchild.yaml @@ -0,0 +1,5 @@ +id: greatgrandchild +name: greatgrandchild +title: greatgrandchild +imports: + - linkml:types \ No newline at end of file diff --git a/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/grandchild.yaml b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/grandchild.yaml new file mode 100644 index 00000000..689723ef --- /dev/null +++ b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/L3_2_0_0/grandchild.yaml @@ -0,0 +1,6 @@ +id: grandchild +name: grandchild +title: grandchild +imports: + - linkml:types + - ./L4_2_0_0_0/greatgrandchild \ No newline at end of file diff --git a/tests/test_utils/input/imports_relative/L0_2/L2_2_0/child.yaml b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/child.yaml new file mode 100644 index 00000000..953042b2 --- /dev/null +++ b/tests/test_utils/input/imports_relative/L0_2/L2_2_0/child.yaml @@ -0,0 +1,6 @@ +id: child +name: child +title: child +imports: + - linkml:types + - ./L3_2_0_0/grandchild \ No newline at end of file diff --git a/tests/test_utils/input/imports_relative/L0_2/main.yaml b/tests/test_utils/input/imports_relative/L0_2/main.yaml new file mode 100644 index 00000000..e99261d8 --- /dev/null +++ b/tests/test_utils/input/imports_relative/L0_2/main.yaml @@ -0,0 +1,6 @@ +id: main +name: main +title: main +imports: + - linkml:types + - ./L2_2_0/child \ No newline at end of file diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 868b8d22..33ebfb02 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -22,6 +22,8 @@ SCHEMA_WITH_STRUCTURED_PATTERNS = Path(INPUT_DIR) / "pattern-example.yaml" SCHEMA_IMPORT_TREE = Path(INPUT_DIR) / 'imports' / 'main.yaml' SCHEMA_RELATIVE_IMPORT_TREE = Path(INPUT_DIR) / 'imports_relative' / 'L0_0' / 'L1_0_0' / 'main.yaml' +SCHEMA_RELATIVE_IMPORT_TREE2 = Path(INPUT_DIR) / 'imports_relative' / 'L0_2' / 'main.yaml' + yaml_loader = YAMLLoader() IS_CURRENT = 'is current' @@ -549,6 +551,11 @@ def test_imports_relative(): assert 'L2100Index' in classes assert 'L2101Index' in classes +def test_imports_relative_load(): + """Relative imports from relative imports should load without FileNotFoundError.""" + sv = SchemaView(SCHEMA_RELATIVE_IMPORT_TREE2) + sv.imports_closure(imports=True) + def test_direct_remote_imports(): """Tests that building a SchemaView directly from a remote URL works."""