diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index 4a21a3da..2c9e9793 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -22,8 +22,6 @@ from mkdocs.utils import templates if TYPE_CHECKING: from mkdocs.config.defaults import MkDocsConfig -if TYPE_CHECKING: - from mkdocs.livereload import LiveReloadServer log = logging.getLogger(__name__) @@ -247,9 +245,7 @@ def _build_page( config._current_page = None -def build( - config: MkDocsConfig, live_server: LiveReloadServer | None = None, dirty: bool = False -) -> None: +def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = False) -> None: """Perform a full site build.""" logger = logging.getLogger('mkdocs') @@ -259,7 +255,7 @@ def build( if config.strict: logging.getLogger('mkdocs').addHandler(warning_counter) - inclusion = InclusionLevel.all if live_server else InclusionLevel.is_included + inclusion = InclusionLevel.all if serve_url else InclusionLevel.is_included try: start = time.monotonic() @@ -280,7 +276,7 @@ def build( " links within your site. This option is designed for site development purposes only." ) - if not live_server: # pragma: no cover + if not serve_url: # pragma: no cover log.info(f"Building documentation to directory: {config.site_dir}") if dirty and site_directory_contains_stale_files(config.site_dir): log.info("The directory contains stale files. Use --clean to remove them.") @@ -306,8 +302,8 @@ def build( for file in files.documentation_pages(inclusion=inclusion): log.debug(f"Reading: {file.src_uri}") if file.page is None and file.inclusion.is_not_in_nav(): - if live_server and file.inclusion.is_excluded(): - excluded.append(urljoin(live_server.url, file.url)) + if serve_url and file.inclusion.is_excluded(): + excluded.append(urljoin(serve_url, file.url)) Page(None, file, config) assert file.page is not None _populate_page(file.page, config, files, dirty) diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py index dc472a66..d6e73b16 100644 --- a/mkdocs/commands/serve.py +++ b/mkdocs/commands/serve.py @@ -9,7 +9,7 @@ from urllib.parse import urlsplit from mkdocs.commands.build import build from mkdocs.config import load_config -from mkdocs.livereload import LiveReloadServer +from mkdocs.livereload import LiveReloadServer, _serve_url if TYPE_CHECKING: from mkdocs.config.defaults import MkDocsConfig @@ -37,9 +37,6 @@ def serve( # string is returned. And it makes MkDocs temp dirs easier to identify. site_dir = tempfile.mkdtemp(prefix='mkdocs_') - def mount_path(config: MkDocsConfig): - return urlsplit(config.site_url or '/').path - def get_config(): config = load_config( config_file=config_file, @@ -47,7 +44,6 @@ def serve( **kwargs, ) config.watch.extend(watch) - config.site_url = f'http://{config.dev_addr}{mount_path(config)}' return config is_clean = build_type == 'clean' @@ -56,16 +52,20 @@ def serve( config = get_config() config.plugins.on_startup(command=('build' if is_clean else 'serve'), dirty=is_dirty) + host, port = config.dev_addr + mount_path = urlsplit(config.site_url or '/').path + config.site_url = serve_url = _serve_url(host, port, mount_path) + def builder(config: MkDocsConfig | None = None): log.info("Building documentation...") if config is None: config = get_config() + config.site_url = serve_url - build(config, live_server=None if is_clean else server, dirty=is_dirty) + build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty) - host, port = config.dev_addr server = LiveReloadServer( - builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config) + builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path ) def error_handler(code) -> bytes | None: diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py index f98da658..e1d7db31 100644 --- a/mkdocs/livereload/__init__.py +++ b/mkdocs/livereload/__init__.py @@ -81,6 +81,15 @@ class _LoggerAdapter(logging.LoggerAdapter): log = _LoggerAdapter(logging.getLogger(__name__), {}) +def _normalize_mount_path(mount_path: str) -> str: + """Ensure the mount path starts and ends with a slash.""" + return ("/" + mount_path.lstrip("/")).rstrip("/") + "/" + + +def _serve_url(host: str, port: int, path: str) -> str: + return f"http://{host}:{port}{_normalize_mount_path(path)}" + + class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGIServer): daemon_threads = True poll_response_timeout = 60 @@ -96,16 +105,14 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe shutdown_delay: float = 0.25, ) -> None: self.builder = builder - self.server_name = host - self.server_port = port try: if isinstance(ipaddress.ip_address(host), ipaddress.IPv6Address): self.address_family = socket.AF_INET6 except Exception: pass self.root = os.path.abspath(root) - self.mount_path = ("/" + mount_path.lstrip("/")).rstrip("/") + "/" - self.url = f"http://{self.server_name}:{self.server_port}{self.mount_path}" + self.mount_path = _normalize_mount_path(mount_path) + self.url = _serve_url(host, port, mount_path) self.build_delay = 0.1 self.shutdown_delay = shutdown_delay # To allow custom error pages. diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 4b7596bf..95145298 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -16,7 +16,6 @@ import markdown.preprocessors from mkdocs.commands import build from mkdocs.config import base from mkdocs.exceptions import PluginError -from mkdocs.livereload import LiveReloadServer from mkdocs.structure.files import File, Files from mkdocs.structure.nav import get_navigation from mkdocs.structure.pages import Page @@ -36,13 +35,6 @@ def build_page(title, path, config, md_src=''): return page, files -def testing_server(root, builder=lambda: None, mount_path="/"): - with mock.patch("socket.socket"): - return LiveReloadServer( - builder, host="localhost", port=123, root=root, mount_path=mount_path - ) - - class BuildTests(PathAssertionMixin, unittest.TestCase): def _get_env_with_null_translations(self, config): env = config.theme.get_env() @@ -596,7 +588,7 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): exclude_docs='ba*.md', ) - with self.subTest(live_server=None): + with self.subTest(serve_url=None): expected_logs = ''' INFO:Doc file 'test/foo.md' contains a link to 'test/bar.md' which is excluded from the built site. ''' @@ -606,8 +598,8 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): self.assertPathNotExists(site_dir, 'test', 'baz.html') self.assertPathNotExists(site_dir, '.zoo.html') - server = testing_server(site_dir, mount_path='/documentation/') - with self.subTest(live_server=server): + serve_url = 'http://localhost:123/documentation/' + with self.subTest(serve_url=serve_url): expected_logs = ''' INFO:Doc file 'test/bar.md' contains a relative link 'nonexistent.md', but the target 'test/nonexistent.md' is not found among documentation files. INFO:Doc file 'test/foo.md' contains a link to 'test/bar.md' which is excluded from the built site. @@ -617,7 +609,7 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): - http://localhost:123/documentation/test/baz.html ''' with self._assert_build_logs(expected_logs): - build.build(cfg, live_server=server) + build.build(cfg, serve_url=serve_url) foo_path = Path(site_dir, 'test', 'foo.html') self.assertTrue(foo_path.is_file()) @@ -639,13 +631,13 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): def test_conflicting_readme_and_index(self, site_dir, docs_dir): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, use_directory_urls=False) - for server in None, testing_server(site_dir): - with self.subTest(live_server=server): + for serve_url in None, 'http://localhost:123/': + with self.subTest(serve_url=serve_url): expected_logs = ''' WARNING:Excluding 'foo/README.md' from the site because it conflicts with 'foo/index.md'. ''' with self._assert_build_logs(expected_logs): - build.build(cfg, live_server=server) + build.build(cfg, serve_url=serve_url) index_path = Path(site_dir, 'foo', 'index.html') self.assertPathIsFile(index_path) @@ -663,10 +655,10 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): docs_dir=docs_dir, site_dir=site_dir, use_directory_urls=False, exclude_docs='index.md' ) - for server in None, testing_server(site_dir): - with self.subTest(live_server=server): + for serve_url in None, 'http://localhost:123/': + with self.subTest(serve_url=serve_url): with self._assert_build_logs(''): - build.build(cfg, live_server=server) + build.build(cfg, serve_url=serve_url) index_path = Path(site_dir, 'foo', 'index.html') self.assertPathIsFile(index_path) @@ -693,9 +685,9 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): assert f is not None config.nav = Path(f.abs_src_path).read_text().splitlines() - for server in None, testing_server(site_dir): + for serve_url in None, 'http://localhost:123/': for exclude in 'full', 'nav', None: - with self.subTest(live_server=server, exclude=exclude): + with self.subTest(serve_url=serve_url, exclude=exclude): cfg = load_config( docs_dir=docs_dir, site_dir=site_dir, @@ -711,13 +703,13 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): INFO:The following pages exist in the docs directory, but are not included in the "nav" configuration: - SUMMARY.md ''' - if exclude == 'full' and server: + if exclude == 'full' and serve_url: expected_logs = ''' INFO:The following pages are being built only for the preview but will be excluded from `mkdocs build` per `exclude_docs`: - http://localhost:123/SUMMARY.html ''' with self._assert_build_logs(expected_logs): - build.build(cfg, live_server=server) + build.build(cfg, serve_url=serve_url) foo_path = Path(site_dir, 'foo.html') self.assertPathIsFile(foo_path) @@ -727,7 +719,7 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): ) summary_path = Path(site_dir, 'SUMMARY.html') - if exclude == 'full' and not server: + if exclude == 'full' and not serve_url: self.assertPathNotExists(summary_path) else: self.assertPathExists(summary_path) diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py index 6a7a0c9f..b19009a3 100644 --- a/mkdocs/tests/livereload_tests.py +++ b/mkdocs/tests/livereload_tests.py @@ -39,6 +39,8 @@ def testing_server(root, builder=lambda: None, mount_path="/"): mount_path=mount_path, polling_interval=0.2, ) + server.server_name = "localhost" + server.server_port = 0 server.setup_environ() server.observer.start() thread = threading.Thread(target=server._build_loop, daemon=True)