Repository: mkdocs/mkdocs (python)

Issue:
` SubConfig` ignore configuration failures
While developing [mkdocs-gallery]() I found out that the `SubConfig` class does not retrieve the failures and warnings from the nested options.

This is because `run_validation` executes `Config.validate` but does not look at its results (`failures, warnings`):
https://github.com/mkdocs/mkdocs/blob/97eafb9094fdd56d19022ae8fda332e728df674b/mkdocs/config/config_options.py#L61


Pull request description:
SubConfig now catches failures and warnings
Fixes #2709


Patch:
diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml
index 32cdf450..e003ff90 100644
--- a/.github/workflows/tox.yml
+++ b/.github/workflows/tox.yml
@@ -9,7 +9,7 @@ jobs:
       max-parallel: 4
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: [3.6, 3.7, 3.8, 3.9, pypy3]
+        python-version: [3.6, 3.7, 3.8, 3.9, '3.10', pypy-3.7-v7.x]
         include:
           - python-version: 3.6
             tox-env: py36
@@ -19,7 +19,9 @@ jobs:
             tox-env: py38
           - python-version: 3.9
             tox-env: py39
-          - python-version: pypy3
+          - python-version: '3.10'
+            tox-env: py310
+          - python-version: pypy-3.7-v7.x
             tox-env: pypy3
     env:
       TOXENV: ${{ matrix.tox-env }}-{unittests,min-req,integration,integration-no-babel}
diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md
index 990694c3..432e9fce 100644
--- a/docs/about/release-notes.md
+++ b/docs/about/release-notes.md
@@ -339,7 +339,7 @@ NodeJS environment if so desired. For more information please read the
 [`prebuild_index` documentation][prebuildindex-docs].
 
 [lunrpy-docs]: http://lunr.readthedocs.io/
-[prebuildindex-docs]: ../../user-guide/configuration/#prebuild_index
+[prebuildindex-docs]: ../user-guide/configuration.md#prebuild_index
 
 #### `readthedocs` theme updated with upstream (#588 and #1374)
 
diff --git a/docs/dev-guide/themes.md b/docs/dev-guide/themes.md
index 2ab7c710..5f21b430 100644
--- a/docs/dev-guide/themes.md
+++ b/docs/dev-guide/themes.md
@@ -349,6 +349,10 @@ page.
 
 [base_url]: #base_url
 
+##### page.file
+
+The documentation `File` that the page is being rendered from.
+
 ##### page.abs_url
 
 The absolute URL of the page from the server root as determined by the value
@@ -673,7 +677,7 @@ objects.
 ```json
 {
     config: {...},
-    data: [...],
+    docs: [...],
     index: {...}
 }
 ```
@@ -682,7 +686,7 @@ If present, the `config` object contains the key/value pairs of config options
 defined for the plugin in the user's `mkdocs.yml` config file under
 `plugings.search`. The `config` object was new in MkDocs version *1.0*.
 
-The `data` object contains a list of document objects. Each document object is
+The `docs` object contains a list of document objects. Each document object is
 made up of a `location` (URL), a `title`, and `text` which can be used to create
 a search index and/or display search results.
 
diff --git a/docs/user-guide/choosing-your-theme.md b/docs/user-guide/choosing-your-theme.md
index dd554d50..4dc4fd2e 100644
--- a/docs/user-guide/choosing-your-theme.md
+++ b/docs/user-guide/choosing-your-theme.md
@@ -106,6 +106,7 @@ supports the following options:
     * `pt_BR`: Portuguese (Brazil)
     * `zh_CN`: Simplified Chinese
     * `de`: German
+    * `fa`: Persian (Farsi)
 
     See the guide on [localizing your theme] for more information.
 
@@ -184,6 +185,7 @@ theme supports the following options:
     * `pt_BR`: Portuguese (Brazil)
     * `zh_CN`: Simplified Chinese
     * `de`: German
+    * `fa`: Persian (Farsi)
 
     See the guide on [localizing your theme] for more information.
 
diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py
index 33758c66..bcbff4f2 100644
--- a/mkdocs/__main__.py
+++ b/mkdocs/__main__.py
@@ -10,7 +10,6 @@ import shutil
 from mkdocs import __version__
 from mkdocs import utils
 from mkdocs import config
-from mkdocs.commands import build, gh_deploy, new, serve
 
 
 if sys.platform.startswith("win"):
@@ -95,6 +94,7 @@ remote_branch_help = ("The remote branch to commit to for Github Pages. This "
 remote_name_help = ("The remote name to commit to for Github Pages. This "
                     "overrides the value specified in config")
 force_help = "Force the push to the repository."
+no_history_help = "Replace the whole Git history with one new commit."
 ignore_version_help = "Ignore check that build is not being deployed with an older version of MkDocs."
 watch_theme_help = ("Include the theme in list of files to watch for live reloading. "
                     "Ignored when live reload is not used.")
@@ -177,6 +177,7 @@ def cli():
 @common_options
 def serve_command(dev_addr, livereload, watch, **kwargs):
     """Run the builtin development server"""
+    from mkdocs.commands import serve
     serve.serve(dev_addr=dev_addr, livereload=livereload, watch=watch, **kwargs)
 
 
@@ -187,6 +188,7 @@ def serve_command(dev_addr, livereload, watch, **kwargs):
 @common_options
 def build_command(clean, **kwargs):
     """Build the MkDocs documentation"""
+    from mkdocs.commands import build
     build.build(config.load_config(**kwargs), dirty=not clean)
 
 
@@ -196,20 +198,29 @@ def build_command(clean, **kwargs):
 @click.option('-b', '--remote-branch', help=remote_branch_help)
 @click.option('-r', '--remote-name', help=remote_name_help)
 @click.option('--force', is_flag=True, help=force_help)
+@click.option('--no-history', is_flag=True, help=no_history_help)
 @click.option('--ignore-version', is_flag=True, help=ignore_version_help)
 @click.option('--shell', is_flag=True, help=shell_help)
 @common_config_options
 @click.option('-d', '--site-dir', type=click.Path(), help=site_dir_help)
 @common_options
-def gh_deploy_command(clean, message, remote_branch, remote_name, force, ignore_version, shell, **kwargs):
+def gh_deploy_command(clean, message, remote_branch, remote_name, force, no_history, ignore_version, shell, **kwargs):
     """Deploy your documentation to GitHub Pages"""
     cfg = config.load_config(
         remote_branch=remote_branch,
         remote_name=remote_name,
         **kwargs
     )
+    from mkdocs.commands import build, gh_deploy
     build.build(cfg, dirty=not clean)
-    gh_deploy.gh_deploy(cfg, message=message, force=force, ignore_version=ignore_version, shell=shell)
+    gh_deploy.gh_deploy(
+        cfg,
+        message=message,
+        force=force,
+        no_history=no_history,
+        ignore_version=ignore_version,
+        shell=shell
+    )
 
 
 @cli.command(name="new")
@@ -217,6 +228,7 @@ def gh_deploy_command(clean, message, remote_branch, remote_name, force, ignore_
 @common_options
 def new_command(project_directory):
     """Create a new MkDocs project"""
+    from mkdocs.commands import new
     new.new(project_directory)
 
 
diff --git a/mkdocs/commands/gh_deploy.py b/mkdocs/commands/gh_deploy.py
index b6b80fb4..2748447e 100644
--- a/mkdocs/commands/gh_deploy.py
+++ b/mkdocs/commands/gh_deploy.py
@@ -84,7 +84,7 @@ def _check_version(branch):
         raise Abort('Deployment Aborted!')
 
 
-def gh_deploy(config, message=None, force=False, ignore_version=False, shell=False):
+def gh_deploy(config, message=None, force=False, no_history=False, ignore_version=False, shell=False):
 
     if not _is_cwd_git_repo():
         log.error('Cannot deploy - this directory does not appear to be a git '
@@ -113,6 +113,7 @@ def gh_deploy(config, message=None, force=False, ignore_version=False, shell=Fal
             push=True,
             force=force,
             use_shell=shell,
+            no_history=no_history,
             nojekyll=True
         )
     except ghp_import.GhpError as e:
diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py
index 28be7fd4..fec06fa2 100644
--- a/mkdocs/commands/serve.py
+++ b/mkdocs/commands/serve.py
@@ -4,6 +4,8 @@ import tempfile
 from urllib.parse import urlsplit
 from os.path import isdir, isfile, join
 
+import jinja2.exceptions
+
 from mkdocs.commands.build import build
 from mkdocs.config import load_config
 from mkdocs.exceptions import Abort
@@ -93,9 +95,12 @@ def serve(config_file=None, dev_addr=None, strict=None, theme=None,
             log.info("Shutting down...")
         finally:
             server.shutdown()
+    except jinja2.exceptions.TemplateError:
+        # This is a subclass of OSError, but shouldn't be suppressed.
+        raise
     except OSError as e:  # pragma: no cover
         # Avoid ugly, unhelpful traceback
-        raise Abort(str(e))
+        raise Abort(f'{type(e).__name__}: {e}')
     finally:
         if isdir(site_dir):
             shutil.rmtree(site_dir)
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 8caf16ad..59e111b2 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -59,7 +59,12 @@ class SubConfig(BaseConfigOption, Config):
         return self.run_validation(value)
 
     def run_validation(self, value):
-        Config.validate(self)
+        failed, self.warnings = Config.validate(self)
+        if len(failed) > 0:
+            # get the first failing one
+            key, err = failed[0]
+            raise ValidationError(f"Sub-option {key!r} configuration error: {err}")
+
         return self
 
 
@@ -383,9 +388,7 @@ class FilesystemObject(Type):
             value = os.path.join(self.config_dir, value)
         if self.exists and not self.existence_test(value):
             raise ValidationError(f"The path {value} isn't an existing {self.name}.")
-        value = os.path.abspath(value)
-        assert isinstance(value, str)
-        return value
+        return os.path.abspath(value)
 
 
 class Dir(FilesystemObject):
diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
index deef55ba..55554a40 100644
--- a/mkdocs/livereload/__init__.py
+++ b/mkdocs/livereload/__init__.py
@@ -8,6 +8,7 @@ import pathlib
 import posixpath
 import re
 import socketserver
+import string
 import threading
 import time
 import warnings
@@ -16,6 +17,30 @@ import wsgiref.simple_server
 import watchdog.events
 import watchdog.observers.polling
 
+_SCRIPT_TEMPLATE = """
+var livereload = function(epoch, requestId) {
+    var req = new XMLHttpRequest();
+    req.onloadend = function() {
+        if (parseFloat(this.responseText) > epoch) {
+            location.reload();
+            return;
+        }
+        var launchNext = livereload.bind(this, epoch, requestId);
+        if (this.status === 200) {
+            launchNext();
+        } else {
+            setTimeout(launchNext, 3000);
+        }
+    };
+    req.open("GET", "${mount_path}livereload/" + epoch + "/" + requestId);
+    req.send();
+
+    console.log('Enabled live reload');
+}
+livereload(${epoch}, ${request_id});
+"""
+_SCRIPT_TEMPLATE = string.Template(_SCRIPT_TEMPLATE)
+
 
 class _LoggerAdapter(logging.LoggerAdapter):
     def process(self, msg, kwargs):
@@ -176,26 +201,25 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
         # https://github.com/bottlepy/bottle/blob/f9b1849db4/bottle.py#L984
         path = environ["PATH_INFO"].encode("latin-1").decode("utf-8", "ignore")
 
-        m = re.fullmatch(r"/livereload/([0-9]+)/[0-9]+", path)
-        if m:
-            epoch = int(m[1])
-            start_response("200 OK", [("Content-Type", "text/plain")])
+        if path.startswith(self.mount_path):
+            rel_file_path = path[len(self.mount_path):]
 
-            def condition():
-                return self._visible_epoch > epoch
+            m = re.fullmatch(r"livereload/([0-9]+)/[0-9]+", rel_file_path)
+            if m:
+                epoch = int(m[1])
+                start_response("200 OK", [("Content-Type", "text/plain")])
+
+                def condition():
+                    return self._visible_epoch > epoch
+
+                with self._epoch_cond:
+                    if not condition():
+                        # Stall the browser, respond as soon as there's something new.
+                        # If there's not, respond anyway after a minute.
+                        self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path)
+                        self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)
+                    return [b"%d" % self._visible_epoch]
 
-            with self._epoch_cond:
-                if not condition():
-                    # Stall the browser, respond as soon as there's something new.
-                    # If there's not, respond anyway after a minute.
-                    self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path)
-                    self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)
-                return [b"%d" % self._visible_epoch]
-
-        if path == "/js/livereload.js":
-            file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "livereload.js")
-        elif path.startswith(self.mount_path):
-            rel_file_path = path[len(self.mount_path):]
             if path.endswith("/"):
                 rel_file_path += "index.html"
             # Prevent directory traversal - normalize the path.
@@ -220,7 +244,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
                 return []
             return None  # Not found
 
-        if file_path.endswith(".html"):
+        if self._watched_paths and file_path.endswith(".html"):
             with file:
                 content = file.read()
             content = self._inject_js_into_html(content, epoch)
@@ -235,17 +259,20 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
         )
         return wsgiref.util.FileWrapper(file)
 
-    @classmethod
-    def _inject_js_into_html(cls, content, epoch):
+    def _inject_js_into_html(self, content, epoch):
         try:
             body_end = content.rindex(b"</body>")
         except ValueError:
             body_end = len(content)
         # The page will reload if the livereload poller returns a newer epoch than what it knows.
         # The other timestamp becomes just a unique identifier for the initiating page.
-        return (
-            b'%b<script src="/js/livereload.js"></script><script>livereload(%d, %d);</script>%b'
-            % (content[:body_end], epoch, _timestamp(), content[body_end:])
+        script = _SCRIPT_TEMPLATE.substitute(
+            mount_path=self.mount_path, epoch=epoch, request_id=_timestamp()
+        )
+        return b"%b<script>%b</script>%b" % (
+            content[:body_end],
+            script.encode(),
+            content[body_end:],
         )
 
     @classmethod
diff --git a/mkdocs/livereload/livereload.js b/mkdocs/livereload/livereload.js
deleted file mode 100644
index a0f74365..00000000
--- a/mkdocs/livereload/livereload.js
+++ /dev/null
@@ -1,19 +0,0 @@
-function livereload(epoch, requestId) {
-    var req = new XMLHttpRequest();
-    req.onloadend = function() {
-        if (parseFloat(this.responseText) > epoch) {
-            location.reload();
-            return;
-        }
-        var launchNext = livereload.bind(this, epoch, requestId);
-        if (this.status === 200) {
-            launchNext();
-        } else {
-            setTimeout(launchNext, 3000);
-        }
-    };
-    req.open("GET", "/livereload/" + epoch + "/" + requestId);
-    req.send();
-
-    console.log('Enabled live reload');
-}
diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py
index e42c05fc..80aa8c3d 100644
--- a/mkdocs/structure/files.py
+++ b/mkdocs/structure/files.py
@@ -138,9 +138,6 @@ class File:
             self.url == other.url
         )
 
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
     def __repr__(self):
         return (
             f"File(src_path='{self.src_path}', dest_path='{self.dest_path}',"
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index b993ae24..951d3a88 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -49,9 +49,6 @@ class Page:
             self.file == other.file
         )
 
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
     def __repr__(self):
         title = f"'{self.title}'" if (self.title is not None) else '[blank]'
         url = self.abs_url or self.file.url
diff --git a/mkdocs/theme.py b/mkdocs/theme.py
index d0287496..04ae3c4d 100644
--- a/mkdocs/theme.py
+++ b/mkdocs/theme.py
@@ -112,7 +112,6 @@ class Theme:
         loader = jinja2.FileSystemLoader(self.dirs)
         # No autoreload because editing a template in the middle of a build is not useful.
         env = jinja2.Environment(loader=loader, auto_reload=False)
-        env.filters['tojson'] = filters.tojson
         env.filters['url'] = filters.url_filter
         localization.install_translations(env, self._vars['locale'], self.dirs)
         return env
diff --git a/mkdocs/themes/mkdocs/locales/fa/LC_MESSAGES/messages.po b/mkdocs/themes/mkdocs/locales/fa/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..31782326
--- /dev/null
+++ b/mkdocs/themes/mkdocs/locales/fa/LC_MESSAGES/messages.po
@@ -0,0 +1,97 @@
+# Persian translations for MkDocs.
+# Copyright (C) 2021 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.2\n"
+"Report-Msgid-Bugs-To: https://github.com/mkdocs/mkdocs/issues\n"
+"POT-Creation-Date: 2021-04-08 22:44+0200\n"
+"PO-Revision-Date: 2022-03-03 15:49+0330\n"
+"Last-Translator: Peyman Mohammadi <@peymanr34>\n"
+"Language: fa\n"
+"Language-Team: fa <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.9.1\n"
+
+#: mkdocs/themes/mkdocs/404.html:8
+msgid "Page not found"
+msgstr "صفحه یافت نشد"
+
+#: mkdocs/themes/mkdocs/base.html:107
+#: mkdocs/themes/mkdocs/keyboard-modal.html:31
+#: mkdocs/themes/mkdocs/search-modal.html:5
+msgid "Search"
+msgstr "جستجو"
+
+#: mkdocs/themes/mkdocs/base.html:117
+msgid "Previous"
+msgstr "قبلی"
+
+#: mkdocs/themes/mkdocs/base.html:122
+msgid "Next"
+msgstr "بعدی"
+
+#: mkdocs/themes/mkdocs/base.html:133 mkdocs/themes/mkdocs/base.html:135
+#: mkdocs/themes/mkdocs/base.html:137 mkdocs/themes/mkdocs/base.html:139
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "ویرایش در %(repo_name)s"
+
+#: mkdocs/themes/mkdocs/base.html:179
+#, python-format
+msgid "Documentation built with %(mkdocs_link)s."
+msgstr "مستندات ساخته شده توسط %(mkdocs_link)s."
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:5
+msgid "Keyboard Shortcuts"
+msgstr "میانبر های صفحه کلید"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:6
+#: mkdocs/themes/mkdocs/search-modal.html:6
+msgid "Close"
+msgstr "بستن"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:12
+msgid "Keys"
+msgstr "کلیدها"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:13
+msgid "Action"
+msgstr "عمل"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:19
+msgid "Open this help"
+msgstr "بازکردن این راهنما"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:23
+msgid "Next page"
+msgstr "صفحه بعدی"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:27
+msgid "Previous page"
+msgstr "صفحه قبلی"
+
+#: mkdocs/themes/mkdocs/search-modal.html:9
+msgid "From here you can search these documents. Enter your search terms below."
+msgstr "از این بخش می‌توانید اسناد را جستجو کنید. عبارت جستجوی خود را در بخش زیر وارد کنید."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Search..."
+msgstr "جستجو..."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Type search term here"
+msgstr "عبارت جستجو را اینجا وارد کنید"
+
+#: mkdocs/themes/mkdocs/search-modal.html:15
+msgid "No results found"
+msgstr "موردی یافت نشد"
+
+#: mkdocs/themes/mkdocs/toc.html:3
+msgid "Table of Contents"
+msgstr "فهرست مطالب"
+
diff --git a/mkdocs/themes/readthedocs/base.html b/mkdocs/themes/readthedocs/base.html
index dfdace69..e7dd8762 100644
--- a/mkdocs/themes/readthedocs/base.html
+++ b/mkdocs/themes/readthedocs/base.html
@@ -34,9 +34,9 @@
     {% if page %}
       <script>
         // Current page data
-        var mkdocs_page_name = {{ page.title|tojson|safe }};
-        var mkdocs_page_input_path = {{ page.file.src_path|string|tojson|safe }};
-        var mkdocs_page_url = {{ page.abs_url|tojson|safe }};
+        var mkdocs_page_name = {{ page.title|tojson }};
+        var mkdocs_page_input_path = {{ page.file.src_path|string|tojson }};
+        var mkdocs_page_url = {{ page.abs_url|tojson }};
       </script>
     {% endif %}
     <script src="{{ 'js/jquery-3.6.0.min.js'|url }}" defer></script>
diff --git a/mkdocs/themes/readthedocs/locales/fa/LC_MESSAGES/messages.po b/mkdocs/themes/readthedocs/locales/fa/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..92513103
--- /dev/null
+++ b/mkdocs/themes/readthedocs/locales/fa/LC_MESSAGES/messages.po
@@ -0,0 +1,102 @@
+# Persian translations for MkDocs.
+# Copyright (C) 2021 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.2.2\n"
+"Report-Msgid-Bugs-To: \"https://github.com/mkdocs/mkdocs/issues\"\n"
+"POT-Creation-Date: 2021-09-27 10:43-0400\n"
+"PO-Revision-Date: 2022-03-03 15:56+0330\n"
+"Last-Translator: Peyman Mohammadi <@peymanr34>\n"
+"Language: fa\n"
+"Language-Team: fa <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.9.1\n"
+
+#: mkdocs/themes/readthedocs/404.html:7
+msgid "Page not found"
+msgstr "صفحه یافت نشد"
+
+#: mkdocs/themes/readthedocs/base.html:97
+msgid "Logo"
+msgstr "لوگو"
+
+#: mkdocs/themes/readthedocs/base.html:111
+msgid "Navigation menu"
+msgstr "منوی ناوبری"
+
+#: mkdocs/themes/readthedocs/base.html:140
+msgid "Mobile navigation menu"
+msgstr "منوی ناوبری موبایل"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:3
+msgid "Docs"
+msgstr "مستندات"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:24
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "ویرایش در %(repo_name)s"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:31
+msgid "Breadcrumb Navigation"
+msgstr "فهرست مسیر"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:33
+#: mkdocs/themes/readthedocs/footer.html:7
+#: mkdocs/themes/readthedocs/versions.html:17
+msgid "Previous"
+msgstr "قبلی"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:36
+#: mkdocs/themes/readthedocs/footer.html:10
+#: mkdocs/themes/readthedocs/versions.html:20
+msgid "Next"
+msgstr "بعدی"
+
+#: mkdocs/themes/readthedocs/footer.html:5
+msgid "Footer Navigation"
+msgstr "ناوبری پاورقی"
+
+#: mkdocs/themes/readthedocs/footer.html:25
+#, python-format
+msgid ""
+"Built with %(mkdocs_link)s using a %(sphinx_link)s provided by "
+"%(rtd_link)s."
+msgstr ""
+"ساخته شده توسط %(mkdocs_link)s با استفاده از %(sphinx_link)s "
+"ارائه شده توسط %(rtd_link)s."
+
+#: mkdocs/themes/readthedocs/search.html:5
+msgid "Search Results"
+msgstr "نتایج جستجو"
+
+#: mkdocs/themes/readthedocs/search.html:9
+msgid "Search the Docs"
+msgstr "جستجوی مستندات"
+
+#: mkdocs/themes/readthedocs/search.html:9
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Type search term here"
+msgstr "عبارت جستجو را اینجا وارد کنید"
+
+#: mkdocs/themes/readthedocs/search.html:12
+msgid "No results found"
+msgstr "موردی یافت نشد"
+
+#: mkdocs/themes/readthedocs/search.html:13
+msgid "Searching..."
+msgstr "درحال جستجو..."
+
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Search docs"
+msgstr "جستجوی مستندات"
+
+#: mkdocs/themes/readthedocs/versions.html:1
+msgid "Versions"
+msgstr "نسخه‌ها"
+
diff --git a/mkdocs/utils/filters.py b/mkdocs/utils/filters.py
index e7fb163e..c3049db9 100644
--- a/mkdocs/utils/filters.py
+++ b/mkdocs/utils/filters.py
@@ -1,16 +1,12 @@
-import json
-
-import jinja2
-import markupsafe
+try:
+    from jinja2 import pass_context as contextfilter
+except ImportError:
+    from jinja2 import contextfilter
 
 from mkdocs.utils import normalize_url
 
 
-def tojson(obj, **kwargs):
-    return markupsafe.Markup(json.dumps(obj, **kwargs))
-
-
-@jinja2.contextfilter
+@contextfilter
 def url_filter(context, value):
     """ A Template filter to normalize URLs. """
     return normalize_url(value, page=context['page'], base=context['base_url'])
diff --git a/requirements/project-min.txt b/requirements/project-min.txt
index 20a9327d..817420fe 100644
--- a/requirements/project-min.txt
+++ b/requirements/project-min.txt
@@ -1,7 +1,7 @@
 babel==2.9.0
 click==3.3
-Jinja2==2.10.1
-MarkupSafe==0.23
+Jinja2==2.10.2
+markupsafe<=2.0.1
 Markdown==3.2.1
 PyYAML==5.1
 watchdog==2.0.0
@@ -9,7 +9,7 @@ mdx_gh_links==0.2
 ghp-import==1.0
 pyyaml_env_tag==0.1
 mkdocs-redirects==1.0.1
-importlib_metadata==3.10.0
+importlib_metadata==4.3
 packaging==20.5
 mergedeep==1.3.4
 colorama==0.4; platform_system == 'Windows'
diff --git a/requirements/project.txt b/requirements/project.txt
index c8fd1133..14020237 100644
--- a/requirements/project.txt
+++ b/requirements/project.txt
@@ -1,7 +1,6 @@
 babel>=2.9.0
 click>=7.0
-Jinja2>=2.10.3
-MarkupSafe>=0.23
+Jinja2>=2.10.2
 Markdown>=3.2.1
 PyYAML>=5.2
 watchdog>=2.0.0
@@ -9,7 +8,7 @@ mdx_gh_links>=0.2
 ghp-import>=1.0
 pyyaml_env_tag>=0.1
 mkdocs-redirects>=1.0.1
-importlib_metadata>=3.10
+importlib_metadata>=4.3
 packaging>=20.5
 mergedeep>=1.3.4
 colorama>=0.4; platform_system == 'Windows'
diff --git a/setup.py b/setup.py
index e913eb1e..77cabc87 100755
--- a/setup.py
+++ b/setup.py
@@ -50,6 +50,9 @@ setup(
     name="mkdocs",
     version=get_version("mkdocs"),
     url='https://www.mkdocs.org',
+    project_urls={
+        'Source': 'https://github.com/mkdocs/mkdocs',
+    },
     license='BSD',
     description='Project documentation with Markdown.',
     long_description=long_description,
@@ -60,13 +63,13 @@ setup(
     include_package_data=True,
     install_requires=[
         'click>=3.3',
-        'Jinja2>=2.10.1',
+        'Jinja2>=2.10.2',
         'Markdown>=3.2.1',
         'PyYAML>=3.10',
         'watchdog>=2.0',
         'ghp-import>=1.0',
         'pyyaml_env_tag>=0.1',
-        'importlib_metadata>=3.10',
+        'importlib_metadata>=4.3',
         'packaging>=20.5',
         'mergedeep>=1.3.4'
     ],
@@ -97,6 +100,7 @@ setup(
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3 :: Only',
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
diff --git a/tox.ini b/tox.ini
index ad890b63..22e3fbc4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,20 +1,20 @@
 [tox]
 envlist =
-    py{36,37,38,39,py3}-{unittests,min-req,integration,integration-no-babel},
+    py{36,37,38,39,310,py3}-{unittests,min-req,integration,integration-no-babel},
     flake8, markdown-lint, linkchecker, jshint, csslint, nobabel, codespell
 
 [testenv]
 passenv = LANG
 deps=
-    py{36,37,38,39,py3}-{unittests,integration}: -rrequirements/project.txt
-    py{36,37,38,39,py3}-min-req: -rrequirements/project-min.txt
-    py{36,37,38,39,py3}-{unittests,min-req}: -rrequirements/test.txt
+    py{36,37,38,39,310,py3}-{unittests,integration}: -rrequirements/project.txt
+    py{36,37,38,39,310,py3}-min-req: -rrequirements/project-min.txt
+    py{36,37,38,39,310,py3}-{unittests,min-req}: -rrequirements/test.txt
 commands=
     {envpython} --version
-    py{36,37,38,39,py3}-{unittests,min-req}:  {envbindir}/coverage run --source=mkdocs --omit 'mkdocs/tests/*' -m unittest discover -p '*tests.py' mkdocs
-    py{36,37,38,39,py3}-unittests: {envbindir}/coverage xml
-    py{36,37,38,39,py3}-unittests: {envbindir}/coverage report --show-missing
-    py{36,37,38,39,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds
+    py{36,37,38,39,310,py3}-{unittests,min-req}:  {envbindir}/coverage run --source=mkdocs --omit 'mkdocs/tests/*' -m unittest discover -p '*tests.py' mkdocs --top-level-directory .
+    py{36,37,38,39,310,py3}-unittests: {envbindir}/coverage xml
+    py{36,37,38,39,310,py3}-unittests: {envbindir}/coverage report --show-missing
+    py{36,37,38,39,310,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds
 
 [testenv:flake8]
 deps=-rrequirements/test.txt
@@ -56,7 +56,7 @@ whitelist_externals = csslint
 passenv=*
 commands=csslint mkdocs/
 
-[testenv:py{36,37,38,39,py3}-integration-no-babel]
+[testenv:py{36,37,38,39,310,py3}-integration-no-babel]
 passenv = LANG
 deps=-rrequirements/project.txt
 commands=


Test patch:
diff --git a/mkdocs/tests/base.py b/mkdocs/tests/base.py
index cf425850..934f9570 100644
--- a/mkdocs/tests/base.py
+++ b/mkdocs/tests/base.py
@@ -6,6 +6,7 @@ from tempfile import TemporaryDirectory
 
 from mkdocs import config
 from mkdocs import utils
+from mkdocs.config import defaults as config_defaults
 
 
 def dedent(text):
@@ -32,7 +33,7 @@ def load_config(**cfg):
     if 'docs_dir' not in cfg:
         # Point to an actual dir to avoid a 'does not exist' error on validation.
         cfg['docs_dir'] = os.path.join(path_base, 'docs')
-    conf = config.Config(schema=config.defaults.get_schema(), config_file_path=cfg['config_file_path'])
+    conf = config.Config(schema=config_defaults.get_schema(), config_file_path=cfg['config_file_path'])
     conf.load_dict(cfg)
 
     errors_warnings = conf.validate()
diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py
index 6ec81fda..62483b5b 100644
--- a/mkdocs/tests/livereload_tests.py
+++ b/mkdocs/tests/livereload_tests.py
@@ -65,7 +65,7 @@ def do_request(server, content):
 
 
 SCRIPT_REGEX = (
-    r'<script src="/js/livereload.js"></script><script>livereload\([0-9]+, [0-9]+\);</script>'
+    r'<script>[\S\s]+?livereload\([0-9]+, [0-9]+\);\s*</script>'
 )
 
 
@@ -277,6 +277,8 @@ class BuildTests(unittest.TestCase):
     )
     def test_serves_modified_html(self, site_dir):
         with testing_server(site_dir) as server:
+            server.watch(site_dir)
+
             headers, output = do_request(server, "GET /normal.html")
             self.assertRegex(output, fr"^<html><body>hello{SCRIPT_REGEX}</body></html>$")
             self.assertEqual(headers.get("content-type"), "text/html")
@@ -296,14 +298,14 @@ class BuildTests(unittest.TestCase):
     def test_serves_directory_index(self, site_dir):
         with testing_server(site_dir) as server:
             headers, output = do_request(server, "GET /")
-            self.assertRegex(output, fr"^<body>aaa{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>aaa</body>$")
             self.assertEqual(headers["_status"], "200 OK")
             self.assertEqual(headers.get("content-type"), "text/html")
             self.assertEqual(headers.get("content-length"), str(len(output)))
 
             for path in "/foo/", "/foo/index.html":
                 _, output = do_request(server, "GET /foo/")
-                self.assertRegex(output, fr"^<body>bbb{SCRIPT_REGEX}</body>$")
+                self.assertRegex(output, r"^<body>bbb</body>$")
 
             with self.assertLogs("mkdocs.livereload"):
                 headers, _ = do_request(server, "GET /foo/index.html/")
@@ -321,29 +323,18 @@ class BuildTests(unittest.TestCase):
     def test_serves_with_unicode_characters(self, site_dir):
         with testing_server(site_dir) as server:
             _, output = do_request(server, "GET /я.html")
-            self.assertRegex(output, fr"^<body>aaa{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>aaa</body>$")
             _, output = do_request(server, "GET /%D1%8F.html")
-            self.assertRegex(output, fr"^<body>aaa{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>aaa</body>$")
 
             with self.assertLogs("mkdocs.livereload"):
                 headers, _ = do_request(server, "GET /%D1.html")
             self.assertEqual(headers["_status"], "404 Not Found")
 
             _, output = do_request(server, "GET /测试2/")
-            self.assertRegex(output, fr"^<body>bbb{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>bbb</body>$")
             _, output = do_request(server, "GET /%E6%B5%8B%E8%AF%952/index.html")
-            self.assertRegex(output, fr"^<body>bbb{SCRIPT_REGEX}</body>$")
-
-    @tempdir()
-    def test_serves_js(self, site_dir):
-        with testing_server(site_dir) as server:
-            for mount_path in "/", "/sub/":
-                server.mount_path = mount_path
-
-                headers, output = do_request(server, "GET /js/livereload.js")
-                self.assertIn("function livereload", output)
-                self.assertEqual(headers["_status"], "200 OK")
-                self.assertEqual(headers.get("content-type"), "application/javascript")
+            self.assertRegex(output, r"^<body>bbb</body>$")
 
     @tempdir()
     def test_serves_polling_instantly(self, site_dir):
@@ -351,6 +342,12 @@ class BuildTests(unittest.TestCase):
             _, output = do_request(server, "GET /livereload/0/0")
             self.assertTrue(output.isdigit())
 
+    @tempdir()
+    def test_serves_polling_from_mount_path(self, site_dir):
+        with testing_server(site_dir, mount_path="/test/f*o") as server:
+            _, output = do_request(server, "GET /test/f*o/livereload/0/0")
+            self.assertTrue(output.isdigit())
+
     @tempdir()
     @tempdir()
     def test_serves_polling_after_event(self, site_dir, docs_dir):
@@ -436,11 +433,11 @@ class BuildTests(unittest.TestCase):
     def test_serves_from_mount_path(self, site_dir):
         with testing_server(site_dir, mount_path="/sub") as server:
             headers, output = do_request(server, "GET /sub/")
-            self.assertRegex(output, fr"^<body>aaa{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>aaa</body>$")
             self.assertEqual(headers.get("content-type"), "text/html")
 
             _, output = do_request(server, "GET /sub/sub/sub.html")
-            self.assertRegex(output, fr"^<body>bbb{SCRIPT_REGEX}</body>$")
+            self.assertRegex(output, r"^<body>bbb</body>$")
 
             with self.assertLogs("mkdocs.livereload"):
                 headers, _ = do_request(server, "GET /sub/sub.html")
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index 50955965..fa0e3d08 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -8,15 +8,16 @@ import os
 from mkdocs import plugins
 from mkdocs import config
 from mkdocs.commands import build
+from mkdocs.config import config_options
 from mkdocs.exceptions import BuildError, PluginError, Abort
 from mkdocs.tests.base import load_config
 
 
 class DummyPlugin(plugins.BasePlugin):
     config_scheme = (
-        ('foo', config.config_options.Type(str, default='default foo')),
-        ('bar', config.config_options.Type(int, default=0)),
-        ('dir', config.config_options.Dir(exists=False)),
+        ('foo', config_options.Type(str, default='default foo')),
+        ('bar', config_options.Type(int, default=0)),
+        ('dir', config_options.Dir(exists=False)),
     )
 
     def on_pre_page(self, content, **kwargs):
diff --git a/mkdocs/tests/theme_tests.py b/mkdocs/tests/theme_tests.py
index 3ccfb07b..296540af 100644
--- a/mkdocs/tests/theme_tests.py
+++ b/mkdocs/tests/theme_tests.py
@@ -5,7 +5,7 @@ from unittest import mock
 
 import mkdocs
 from mkdocs.theme import Theme
-from localization import parse_locale
+from mkdocs.localization import parse_locale
 
 abs_path = os.path.abspath(os.path.dirname(__file__))
 mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))