Repository: mkdocs/mkdocs
Pull request #3265 (2023-06-16 13:11:04)

Pass config file path through SubConfig to Config
This ensures FilesystemObject fields in Config are correctly evaluated relative to mkdocs.yaml.

See #3264

oprypin: Thanks for the PR.

Well, the more important thing here would be adding the test. Also I'll need to think for a bit whether there are any implications for passing `config_file_path` to all subclasses of `Config` or whether it can be considered a breaking change by now
couling: @oprypin Thanks!  Please confirm that my legacy unit test is actually a legacy unit test, I've not used that style before. Hopefully this will be non-breaking. 
oprypin: Hi sorry, I'm overthinking how to deal with a pull request. Would it be OK for me to just push my edits to this pull request?
couling: Yeah should be fine. I don't recall if I have a local copy so please don't force push.

Patch:
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index f8ea7f29..23a113c0 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -59,7 +59,8 @@ class SubConfig(Generic[SomeConfig], BaseConfigOption[SomeConfig]):
     enable validation with `validate=True`.
     """
 
-    config_class: Callable[[], SomeConfig]
+    _config_file_path: str | None = None
+    config_class: type[SomeConfig]
 
     @overload
     def __init__(
@@ -95,8 +96,11 @@ class SubConfig(Generic[SomeConfig], BaseConfigOption[SomeConfig]):
         name = f'{cls.__name__}[{config_class.__name__}]'
         return type(name, (cls,), dict(config_class=config_class))
 
+    def pre_validation(self, config: Config, key_name: str):
+        self._config_file_path = config.config_file_path
+
     def run_validation(self, value: object) -> SomeConfig:
-        config = self.config_class()
+        config = self.config_class(config_file_path=self._config_file_path)
         try:
             config.load_dict(value)  # type: ignore
             failed, warnings = config.validate()
@@ -907,8 +911,8 @@ class ExtraScriptValue(Config):
     async_ = Type(bool, default=False)
     """Whether to add the `async` tag to the script."""
 
-    def __init__(self, path: str = ''):
-        super().__init__()
+    def __init__(self, path: str = '', config_file_path=None):
+        super().__init__(config_file_path=config_file_path)
         self.path = path
 
     def __str__(self):


Test patch:
diff --git a/mkdocs/tests/config/config_options_legacy_tests.py b/mkdocs/tests/config/config_options_legacy_tests.py
index dc765dba..22f8dace 100644
--- a/mkdocs/tests/config/config_options_legacy_tests.py
+++ b/mkdocs/tests/config/config_options_legacy_tests.py
@@ -1262,6 +1262,23 @@ class SubConfigTest(TestCase):
         conf = self.get_config(Schema, {'option': {'cc': 'foo'}})
         self.assertEqual(conf['option'], {'cc': 'foo'})
 
+    def test_config_file_path_pass_through(self):
+        """Necessary to ensure FilesystemObject validates the correct path"""
+
+        passed_config_path = None
+
+        class SubType(c.BaseConfigOption):
+            def pre_validation(self, config, key_name):
+                nonlocal passed_config_path
+                passed_config_path = config.config_file_path
+
+        class Schema:
+            sub = c.SubConfig(('opt', SubType()))
+
+        config_path = "foo/mkdocs.yaml"
+        self.get_config(Schema, {"sub": {"opt": "bar"}}, config_file_path=config_path)
+        self.assertEqual(passed_config_path, config_path)
+
 
 class ConfigItemsTest(TestCase):
     def test_subconfig_with_multiple_items(self):
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index 4575079e..e13a20ca 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -1552,6 +1552,26 @@ class SubConfigTest(TestCase):
         with self.expect_error(sub="Required configuration not provided."):
             conf = self.get_config(Schema, {'sub': None})
 
+    def test_config_file_path_pass_through(self) -> None:
+        """Necessary to ensure FilesystemObject validates the correct path"""
+
+        passed_config_path = None
+
+        class SubType(c.BaseConfigOption):
+            def pre_validation(self, config: Config, key_name: str) -> None:
+                nonlocal passed_config_path
+                passed_config_path = config.config_file_path
+
+        class Sub(Config):
+            opt = SubType()
+
+        class Schema(Config):
+            sub = c.ListOfItems(c.SubConfig(Sub), default=[])
+
+        config_path = "foo/mkdocs.yaml"
+        self.get_config(Schema, {"sub": [{"opt": "bar"}]}, config_file_path=config_path)
+        self.assertEqual(passed_config_path, config_path)
+
 
 class MarkdownExtensionsTest(TestCase):
     @patch('markdown.Markdown')