Repository: linkml/linkml

Issue:
LinkML 1.6.2 breaks local imports
**Describe the bug**
Validation using local imports doesn't work is not run from directory of importing schema

**To reproduce**
Steps to reproduce the behavior:
1. Create a schema importing another local schema
2. Run `linkml-validate` from another directory than that of the importing schema
4. See error reporting `FileNotFoundError: [Errno 2] No such file or directory: <imported-schema>`

**Expected behavior**
Full-path resolution is relative to the path of the importing schema and not relative to the current working directory.

**About your computer (if applicable, please complete the following information):**
 - Version 1.6.2

Discussion:

Patch:
diff --git a/linkml/validator/validator.py b/linkml/validator/validator.py
index 860a0b0b..1ddc9f4f 100644
--- a/linkml/validator/validator.py
+++ b/linkml/validator/validator.py
@@ -36,6 +36,8 @@ class Validator:
             self._schema = schema
         else:
             self._schema: SchemaDefinition = yaml_loader.load(schema, SchemaDefinition)
+        if isinstance(schema, str) and "\n" not in schema:
+            self._schema.source_file = schema
         self._validation_plugins = validation_plugins
         self.strict = strict
 


Test patch:
diff --git a/tests/test_validator/test_validator.py b/tests/test_validator/test_validator.py
index 46c65af5..ef32055a 100644
--- a/tests/test_validator/test_validator.py
+++ b/tests/test_validator/test_validator.py
@@ -1,6 +1,6 @@
-import unittest
 from typing import Iterable
 
+import pytest
 from linkml_runtime.linkml_model import ClassDefinition, SchemaDefinition
 
 from linkml.validator import Validator
@@ -50,78 +50,125 @@ class TestDataLoader(Loader):
             yield {"id": i}
 
 
-class TestValidator(unittest.TestCase):
-    def test_validate_valid_instance(self):
-        plugins = [AcceptAnythingValidationPlugin()]
-        validator = Validator(SCHEMA, plugins)
-        report = validator.validate({"foo": "bar"})
-        self.assertEqual(len(report.results), 0)
-
-    def test_validate_invalid_instance(self):
-        plugins = [AcceptNothingValidationPlugin(10)]
-        validator = Validator(SCHEMA, plugins)
-        report = validator.validate({"foo": "bar"})
-        self.assertEqual(len(report.results), 10)
-
-    def test_validate_multiple_plugins(self):
-        plugins = [
-            AcceptAnythingValidationPlugin(),
-            AcceptNothingValidationPlugin(5),
-            AcceptNothingValidationPlugin(10),
-        ]
-        validator = Validator(SCHEMA, plugins)
-        report = validator.validate({"foo": "bar"})
-        self.assertEqual(len(report.results), 15)
-
-    def test_iter_results_valid_instance(self):
-        plugins = [AcceptAnythingValidationPlugin()]
-        validator = Validator(SCHEMA, plugins)
-        results = validator.iter_results({"foo": "bar"})
-        self.assertRaises(StopIteration, lambda: next(results))
-
-    def test_iter_results_invalid_instance(self):
-        plugins = [AcceptNothingValidationPlugin(2)]
-        validator = Validator(SCHEMA, plugins)
-        results = validator.iter_results({"foo": "bar"})
-        self.assertIn("0", next(results).message)
-        self.assertIn("1", next(results).message)
-        self.assertRaises(StopIteration, lambda: next(results))
-
-    def test_provides_default_target_class_in_context(self):
-        plugins = [AcceptNothingValidationPlugin(1)]
-        validator = Validator(SCHEMA, plugins)
-        results = validator.iter_results({"foo": "bar"})
-        result = next(results)
-        self.assertEqual(result.instantiates, "TreeRoot")
-
-    def test_provides_custom_target_class_in_context(self):
-        plugins = [AcceptNothingValidationPlugin(1)]
-        validator = Validator(SCHEMA, plugins)
-        target_class = "OtherClass"
-        results = validator.iter_results({"foo": "bar"}, target_class)
-        result = next(results)
-        self.assertEqual(result.instantiates, target_class)
-
-    def test_error_on_missing_target_class(self):
-        plugins = [AcceptNothingValidationPlugin(1)]
-        validator = Validator(SCHEMA, plugins)
-        self.assertRaises(ValueError, lambda: validator.validate({"foo": "bar"}, "NonExistentClass"))
-
-    def test_validate_source(self):
-        plugins = [AcceptNothingValidationPlugin(3)]
-        validator = Validator(SCHEMA, plugins)
-        loader = TestDataLoader(None, 4)
-        report = validator.validate_source(loader)
-        self.assertEqual(len(report.results), 12)
-
-    def test_iter_results_from_source(self):
-        plugins = [AcceptNothingValidationPlugin(2)]
-        validator = Validator(SCHEMA, plugins)
-        loader = TestDataLoader(None, 5)
-        results = list(validator.iter_results_from_source(loader))
-        self.assertEqual(len(results), 10)
-
-    def test_no_plugins(self):
-        validator = Validator(SCHEMA)
-        report = validator.validate({"foo": "bar"})
-        self.assertEqual(report.results, [])
+def test_validate_valid_instance():
+    plugins = [AcceptAnythingValidationPlugin()]
+    validator = Validator(SCHEMA, plugins)
+    report = validator.validate({"foo": "bar"})
+    assert len(report.results) == 0
+
+
+def test_validate_invalid_instance():
+    plugins = [AcceptNothingValidationPlugin(10)]
+    validator = Validator(SCHEMA, plugins)
+    report = validator.validate({"foo": "bar"})
+    assert len(report.results) == 10
+
+
+def test_validate_multiple_plugins():
+    plugins = [
+        AcceptAnythingValidationPlugin(),
+        AcceptNothingValidationPlugin(5),
+        AcceptNothingValidationPlugin(10),
+    ]
+    validator = Validator(SCHEMA, plugins)
+    report = validator.validate({"foo": "bar"})
+    assert len(report.results) == 15
+
+
+def test_iter_results_valid_instance():
+    plugins = [AcceptAnythingValidationPlugin()]
+    validator = Validator(SCHEMA, plugins)
+    results = validator.iter_results({"foo": "bar"})
+    with pytest.raises(StopIteration):
+        next(results)
+
+
+def test_iter_results_invalid_instance():
+    plugins = [AcceptNothingValidationPlugin(2)]
+    validator = Validator(SCHEMA, plugins)
+    results = validator.iter_results({"foo": "bar"})
+    assert "0" in next(results).message
+    assert "1" in next(results).message
+    with pytest.raises(StopIteration):
+        next(results)
+
+
+def test_provides_default_target_class_in_context():
+    plugins = [AcceptNothingValidationPlugin(1)]
+    validator = Validator(SCHEMA, plugins)
+    results = validator.iter_results({"foo": "bar"})
+    result = next(results)
+    assert result.instantiates == "TreeRoot"
+
+
+def test_provides_custom_target_class_in_context():
+    plugins = [AcceptNothingValidationPlugin(1)]
+    validator = Validator(SCHEMA, plugins)
+    target_class = "OtherClass"
+    results = validator.iter_results({"foo": "bar"}, target_class)
+    result = next(results)
+    assert result.instantiates == target_class
+
+
+def test_error_on_missing_target_class():
+    plugins = [AcceptNothingValidationPlugin(1)]
+    validator = Validator(SCHEMA, plugins)
+    with pytest.raises(ValueError):
+        validator.validate({"foo": "bar"}, "NonExistentClass")
+
+
+def test_validate_source():
+    plugins = [AcceptNothingValidationPlugin(3)]
+    validator = Validator(SCHEMA, plugins)
+    loader = TestDataLoader(None, 4)
+    report = validator.validate_source(loader)
+    assert len(report.results) == 12
+
+
+def test_iter_results_from_source():
+    plugins = [AcceptNothingValidationPlugin(2)]
+    validator = Validator(SCHEMA, plugins)
+    loader = TestDataLoader(None, 5)
+    results = list(validator.iter_results_from_source(loader))
+    assert len(results) == 10
+
+
+def test_no_plugins():
+    validator = Validator(SCHEMA)
+    report = validator.validate({"foo": "bar"})
+    assert report.results == []
+
+
+def test_load_schema_from_path(tmp_file_factory):
+    # https://github.com/linkml/linkml/issues/1694
+    main_path = tmp_file_factory(
+        "main.yaml",
+        """id: http://example.org/test_load_schema_from_path/main
+prefixes:
+  linkml: https://w3id.org/linkml/
+default_range: string
+imports:
+  - base
+""",
+    )
+    tmp_file_factory(
+        "base.yaml",
+        """id: http://example.org/base
+prefixes:
+  linkml: https://w3id.org/linkml/
+imports:
+  - linkml:types
+default_range: string
+
+classes:
+  AClass:
+    attributes:
+      an_attribute:
+""",
+    )
+
+    validator = Validator(main_path, [AcceptAnythingValidationPlugin()])
+    assert validator._schema.source_file == main_path
+
+    report = validator.validate({"an_attribute": "something"})
+    assert report.results == []