Repository: mkdocs/mkdocs

Issue:
Re-building w/ symbolic links stopped working, regression after #2385
Since a444c43 in master using the local development server via `mkdocs serve` updating files that are symbolically linked is not triggering to rebuild (and therefore not reloading browser tabs).

On first glance this is due to the switch to watchdog for detecting file-system changes which needs more guidance to handle this file-type.

Preparing a PR with a patch.

Ref: a444c43474f91dea089922dd8fb188d1db3a4535


Discussion:
@waylan this issue might need reopening, I don't know if its me and late, but the new changes doe not work the slightest for me :/ . Test setup is unchanged and I re-test according to my earlier notes, just symlinks don't work.

I have to re-check tomorrow with a pair of fresh eyes.

/cc @oprypin 
> 
> 
> @waylan this issue might need reopening, I don't know if its me and late, but the new changes doe not work the slightest for me :/ . Test setup is unchanged and I re-test according to my earlier notes, just symlinks don't work.
> 
> I have to re-check tomorrow with a pair of fresh eyes.


Well, that is concerning. Currently this would be the only thing holding up the 2.1 release. I just resolved the last couple items on the milestone.

Could you provide a reproducible test which fails (but previously passed)?



Resolving patch:
diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
--- a/mkdocs/livereload/__init__.py
+++ b/mkdocs/livereload/__init__.py
@@ -101,29 +101,26 @@ def callback(event, allowed_path=None):
 
         def schedule(path):
             seen.add(path)
-            if os.path.isfile(path):
+            if path.is_file():
                 # Watchdog doesn't support watching files, so watch its directory and filter by path
                 handler = watchdog.events.FileSystemEventHandler()
-                handler.on_any_event = lambda event: callback(event, allowed_path=path)
+                handler.on_any_event = lambda event: callback(event, allowed_path=os.fspath(path))
 
-                parent = os.path.dirname(path)
+                parent = path.parent
                 log.debug(f"Watching file '{path}' through directory '{parent}'")
                 self.observer.schedule(handler, parent)
             else:
                 log.debug(f"Watching directory '{path}'")
                 self.observer.schedule(dir_handler, path, recursive=recursive)
 
-        schedule(os.path.realpath(path))
+        schedule(pathlib.Path(path).resolve())
 
         def watch_symlink_targets(path_obj):  # path is os.DirEntry or pathlib.Path
             if path_obj.is_symlink():
-                # The extra `readlink` is needed due to https://bugs.python.org/issue9949
-                target = os.path.realpath(os.readlink(os.fspath(path_obj)))
-                if target in seen or not os.path.exists(target):
+                path_obj = pathlib.Path(path_obj).resolve()
+                if path_obj in seen or not path_obj.exists():
                     return
-                schedule(target)
-
-                path_obj = pathlib.Path(target)
+                schedule(path_obj)
 
             if path_obj.is_dir() and recursive:
                 with os.scandir(os.fspath(path_obj)) as scan:


Test patch:
diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py
--- a/mkdocs/tests/livereload_tests.py
+++ b/mkdocs/tests/livereload_tests.py
@@ -3,6 +3,7 @@
 import contextlib
 import email
 import io
+import os
 import sys
 import threading
 import time
@@ -496,6 +497,28 @@ def wait_for_build():
             Path(tmp_dir, "file_dest_unused.md").write_text("edited")
             self.assertFalse(started_building.wait(timeout=0.2))
 
+    @tempdir(prefix="site_dir")
+    @tempdir(["docs/unused.md", "README.md"], prefix="origin_dir")
+    def test_watches_through_relative_symlinks(self, origin_dir, site_dir):
+        docs_dir = Path(origin_dir, "docs")
+        old_cwd = os.getcwd()
+        os.chdir(docs_dir)
+        try:
+            Path(docs_dir, "README.md").symlink_to(Path("..", "README.md"))
+        except NotImplementedError:  # PyPy on Windows
+            self.skipTest("Creating symlinks not supported")
+        finally:
+            os.chdir(old_cwd)
+
+        started_building = threading.Event()
+
+        with testing_server(docs_dir, started_building.set) as server:
+            server.watch(docs_dir)
+            time.sleep(0.01)
+
+            Path(origin_dir, "README.md").write_text("edited")
+            self.assertTrue(started_building.wait(timeout=10))
+
     @tempdir()
     def test_watch_with_broken_symlinks(self, docs_dir):
         Path(docs_dir, "subdir").mkdir()
@@ -503,7 +526,6 @@ def test_watch_with_broken_symlinks(self, docs_dir):
         try:
             if sys.platform != "win32":
                 Path(docs_dir, "subdir", "circular").symlink_to(Path(docs_dir))
-            Path(docs_dir, "self_link").symlink_to(Path(docs_dir, "self_link"))
 
             Path(docs_dir, "broken_1").symlink_to(Path(docs_dir, "oh no"))
             Path(docs_dir, "broken_2").symlink_to(Path(docs_dir, "oh no"), target_is_directory=True)