Repository: mkdocs/mkdocs

Issue:
Unhashable type: 'ExtraScriptValue' Error
I have been encountering the following error when I add an extra_javascript to my yml file:
```
INFO    -  DeprecationWarning: warning_filter doesn't do anything since MkDocs 1.2 and will be removed soon. All messages on the
           `mkdocs` logger get counted automatically.
             File
           "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs_print_site_plugin/plugin.py",
           line 10, in <module>
               from mkdocs.utils import write_file, copy_file, get_relative_url, warning_filter
             File
           "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/utils/__init__.py",
           line 453, in __getattr__
               warnings.warn(
INFO    -  Building documentation...
INFO    -  Cleaning site directory
INFO    -  The following pages exist in the docs directory, but are not included in the "nav" configuration:
             - pages/integration/export.md
Traceback (most recent call last):
  File "/home/mouhand/work_projects/service-observatory-backend/env/bin/mkdocs", line 8, in <module>
    sys.exit(cli())
             ^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/__main__.py", line 270, in serve_command
    serve.serve(**kwargs)
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/commands/serve.py", line 98, in serve
    builder(config)
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/commands/serve.py", line 79, in builder
    build(config, live_server=None if is_clean else server, dirty=is_dirty)
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/commands/build.py", line 340, in build
    _build_theme_template(template, env, files, config, nav)
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/commands/build.py", line 110, in _build_theme_template
    output = _build_template(template_name, template, files, config, nav)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/commands/build.py", line 87, in _build_template
    context = config.plugins.on_template_context(context, template_name=name, config=config)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/plugins.py", line 555, in on_template_context
    return self.run_event(
           ^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/plugins.py", line 507, in run_event
    result = method(item, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs_print_site_plugin/plugin.py", line 288, in on_template_context
    self.context['extra_javascript'] = [get_relative_url(f, self.print_page.file.url) for f in config.get('extra_javascript')]
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs_print_site_plugin/plugin.py", line 288, in <listcomp>
    self.context['extra_javascript'] = [get_relative_url(f, self.print_page.file.url) for f in config.get('extra_javascript')]
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mouhand/work_projects/service-observatory-backend/env/lib/python3.11/site-packages/mkdocs/utils/__init__.py", line 243, in get_relative_url
    dest_parts = _norm_parts(url)
                 ^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'ExtraScriptValue'
```
in my mkdocs.yml:
```
extra_javascript:
  - javascripts/extra.js
```

Discussion:

Patch:
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index cb95e741..c0c827ff 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -945,11 +945,18 @@ class ExtraScriptValue(Config):
         return self.path
 
 
-class ExtraScript(SubConfig[ExtraScriptValue]):
-    def run_validation(self, value: object) -> ExtraScriptValue:
+class ExtraScript(BaseConfigOption[Union[ExtraScriptValue, str]]):
+    def __init__(self):
+        super().__init__()
+        self.option_type = SubConfig[ExtraScriptValue]()
+
+    def run_validation(self, value: object) -> ExtraScriptValue | str:
+        self.option_type.warnings = self.warnings
         if isinstance(value, str):
-            value = {'path': value, 'type': 'module' if value.endswith('.mjs') else ''}
-        return super().run_validation(value)
+            if value.endswith('.mjs'):
+                return self.option_type.run_validation({'path': value, 'type': 'module'})
+            return value
+        return self.option_type.run_validation(value)
 
 
 class MarkdownExtensions(OptionallyRequired[List[str]]):
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 34c0bc76..e42faffc 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -437,7 +437,7 @@ class _ExtractTitleTreeprocessor(markdown.treeprocessors.Treeprocessor):
         for el in root:
             if el.tag == 'h1':
                 # Drop anchorlink from the element, if present.
-                if len(el) > 0 and el[-1].tag == 'a' and not (el.tail or '').strip():
+                if len(el) > 0 and el[-1].tag == 'a' and not (el[-1].tail or '').strip():
                     el = copy.copy(el)
                     del el[-1]
                 # Extract the text only, recursively.
diff --git a/readthedocs.yml b/readthedocs.yml
deleted file mode 100644
index ae64ca7f..00000000
--- a/readthedocs.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-build:
-  image: latest
-
-python:
-  setup_py_install: true
-
-requirements_file: requirements/project.txt


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index abd7cb20..6101c2e5 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -9,7 +9,7 @@ import re
 import sys
 import textwrap
 import unittest
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeVar
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeVar, Union
 from unittest.mock import patch
 
 if TYPE_CHECKING:
@@ -700,14 +700,14 @@ class ExtraScriptsTest(TestCase):
             option = c.ListOfItems(c.ExtraScript(), default=[])
 
         conf = self.get_config(Schema, {'option': ['foo.js', {'path': 'bar.js', 'async': True}]})
-        assert_type(conf.option, List[c.ExtraScriptValue])
+        assert_type(conf.option, List[Union[c.ExtraScriptValue, str]])
         self.assertEqual(len(conf.option), 2)
-        self.assertIsInstance(conf.option[0], c.ExtraScriptValue)
+        self.assertIsInstance(conf.option[1], c.ExtraScriptValue)
         self.assertEqual(
-            [(x.path, x.type, x.defer, x.async_) for x in conf.option],
+            conf.option,
             [
-                ('foo.js', '', False, False),
-                ('bar.js', '', False, True),
+                'foo.js',
+                {'path': 'bar.js', 'type': '', 'defer': False, 'async': True},
             ],
         )
 
@@ -718,14 +718,14 @@ class ExtraScriptsTest(TestCase):
         conf = self.get_config(
             Schema, {'option': ['foo.mjs', {'path': 'bar.js', 'type': 'module'}]}
         )
-        assert_type(conf.option, List[c.ExtraScriptValue])
+        assert_type(conf.option, List[Union[c.ExtraScriptValue, str]])
         self.assertEqual(len(conf.option), 2)
         self.assertIsInstance(conf.option[0], c.ExtraScriptValue)
         self.assertEqual(
-            [(x.path, x.type, x.defer, x.async_) for x in conf.option],
+            conf.option,
             [
-                ('foo.mjs', 'module', False, False),
-                ('bar.js', 'module', False, False),
+                {'path': 'foo.mjs', 'type': 'module', 'defer': False, 'async': False},
+                {'path': 'bar.js', 'type': 'module', 'defer': False, 'async': False},
             ],
         )
 
@@ -748,8 +748,8 @@ class ExtraScriptsTest(TestCase):
             warnings=dict(option="Sub-option 'foo': Unrecognised configuration name: foo"),
         )
         self.assertEqual(
-            [(x.path, x.type, x.defer, x.async_) for x in conf.option],
-            [('foo.js', '', False, False)],
+            conf.option,
+            [{'path': 'foo.js', 'type': '', 'defer': False, 'async': False, 'foo': 'bar'}],
         )