Repository: smarie/mkdocs-gallery

Issue:
TypeError: startswith first arg must be str or a tuple of str, not WindowsPath
This happens when we use the `mkdocs serve` mode:

```
  File "C:\(...)\mkdocs_gallery\plugin.py", line 352, in _callback
    if event.src_path.startswith(g):
TypeError: startswith first arg must be str or a tuple of str, not WindowsPath
```

Probably related to the last changes with the pathlib paths everywhere.


Discussion:

Patch:
diff --git a/src/mkdocs_gallery/gen_data_model.py b/src/mkdocs_gallery/gen_data_model.py
index b12b51f..b3ead8f 100644
--- a/src/mkdocs_gallery/gen_data_model.py
+++ b/src/mkdocs_gallery/gen_data_model.py
@@ -22,7 +22,13 @@ from typing import List, Dict, Any, Tuple, Union, Iterable
 from pathlib import Path
 
 from .errors import ExtensionError
-from .utils import _smart_copy_md5, get_md5sum, _replace_by_new_if_needed, _new_file, matches_filepath_pattern
+from .utils import (_smart_copy_md5,
+                    get_md5sum,
+                    _replace_by_new_if_needed,
+                    _new_file,
+                    matches_filepath_pattern,
+                    is_relative_to,
+                    )
 
 
 def _has_readme(folder: Path) -> bool:
@@ -660,7 +666,7 @@ class Gallery(GalleryBase):
         self._attach(all_info=all_info)
 
         # Check that generated dir is inside docs dir
-        if not self.generated_dir.as_posix().startswith(self.all_info.mkdocs_docs_dir.as_posix()):
+        if not is_relative_to(self.all_info.mkdocs_docs_dir, self.generated_dir):
             raise ValueError("Generated gallery dirs can only be located as subfolders of the mkdocs 'docs_dir'.")
 
     def has_subsections(self) -> bool:
diff --git a/src/mkdocs_gallery/plugin.py b/src/mkdocs_gallery/plugin.py
index dcc674f..61d5f24 100644
--- a/src/mkdocs_gallery/plugin.py
+++ b/src/mkdocs_gallery/plugin.py
@@ -25,6 +25,7 @@ from . import glr_path_static
 from .binder import copy_binder_files
 # from .docs_resolv import embed_code_links
 from .gen_gallery import parse_config, generate_gallery_md, summarize_failing_examples, fill_mkdocs_nav
+from .utils import is_relative_to
 
 
 class ConfigList(co.OptionallyRequired):
@@ -348,8 +349,7 @@ markdown_extensions:
         def wrap_callback(original_callback):
             def _callback(event):
                 for g in excluded_dirs:
-                    # TODO maybe use fnmatch rather ?
-                    if event.src_path.startswith(g):
+                    if is_relative_to(g, Path(event.src_path)):
                         # ignore this event: the file is in the gallery target dir.
                         # log.info(f"Ignoring event: {event}")
                         return
diff --git a/src/mkdocs_gallery/utils.py b/src/mkdocs_gallery/utils.py
index b5ddcf2..e388817 100644
--- a/src/mkdocs_gallery/utils.py
+++ b/src/mkdocs_gallery/utils.py
@@ -347,3 +347,32 @@ def matches_filepath_pattern(filepath: Path, pattern: str) -> bool:
     result = re.search(pattern, str(filepath))
 
     return True if result is not None else False
+
+
+def is_relative_to(parentpath: Path, subpath: Path) -> bool:
+    """
+    Check if subpath is relative to parentpath
+
+    Parameters
+    ----------
+    parentpath
+        The (potential) parent path
+
+    subpath
+        The (potential) subpath
+
+    Returns
+    -------
+    rc
+        A boolean indicating whether subpath is relative to parentpath
+    """
+
+    if not (isinstance(parentpath, Path) and isinstance(subpath, Path)):
+        raise TypeError("Arguments must both be pathlib objects")
+
+    try:
+        subpath.relative_to(parentpath)
+        return True
+
+    except ValueError:
+        return False


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 58d4c6a..879fba9 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,7 +2,7 @@ from pathlib import Path
 import re
 import os
 import pytest
-from mkdocs_gallery.utils import matches_filepath_pattern
+from mkdocs_gallery.utils import matches_filepath_pattern, is_relative_to
 
 
 class TestFilepathPatternMatch:
@@ -42,3 +42,28 @@ class TestFilepathPatternMatch:
 
         with pytest.raises(AssertionError):
             matches_filepath_pattern(filepath, pattern)
+
+
+class TestRelativePaths:
+
+    @pytest.mark.parametrize(
+        "path1, path2, expected", [
+            ("parent", "parent/sub", True),
+            ("notparent", "parent/sub", False),
+        ])
+    def test_behavior(self, path1, path2, expected):
+        """Test that the function behaves as expected"""
+
+        assert is_relative_to(Path(path1), Path(path2)) == expected
+
+    @pytest.mark.parametrize(
+        "path1, path2", [
+            ("parent", "parent/sub"),
+            (Path("parent"), "parent/sub"),
+            ("parent", Path("parent/sub")),
+        ])
+    def test_not_paths_raises(self, path1, path2):
+        """Test that the function raises an exception when both arguments are not Path objects"""
+
+        with pytest.raises(TypeError):
+            is_relative_to(path1, path2)