Repository: mkdocs/mkdocs (python)

Issue:
Improve edit_uri flexability
Hi guys, 
According to the documentation, I should be able to only add the link to my bitbucket repository to get edit links. 
I added it like this: repo_url: https://bitbucket.org/josephkobti/testdocs/
but the links I'm getting are not working
i.e. https://bitbucket.org/josephkobti/testdocs/src/default/docs/index.md

I think maybe I should add the repo_url in another way, any suggestions? 

Pull request description:
Add `edit_uri_template` config
Resolves #1378

Patch:
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index 953ce14f..8d4c2660 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -93,6 +93,24 @@ directory.
 edit_uri: root/path/docs/
 ```
 
+For example, having this config:
+
+```yaml
+repo_url: https://example.com/project/repo
+edit_uri: blob/main/docs/
+```
+
+means that a page named 'foo/bar.md' will have its edit link lead to:  
+<https://example.com/project/repo/blob/main/docs/foo/bar.md>
+
+`edit_uri` can actually be just an absolute URL, not necessarily relative to `repo_url`, so this can achieve the same result:
+
+```yaml
+repo_url: https://example.com/project/repo/blob/main/docs/
+```
+
+For more flexibility, see [edit_uri_template](#edit_uri_template) below.
+
 > NOTE:
 > On a few known hosts (specifically GitHub, Bitbucket and GitLab), the
 > `edit_uri` is derived from the 'repo_url' and does not need to be set
@@ -124,6 +142,63 @@ access.
 `src/default/docs/` for a Bitbucket repo, if `repo_url` matches those domains,
 otherwise `null`
 
+### edit_uri_template
+
+The more flexible variant of [edit_uri](#edit_uri). These two are equivalent:
+
+```yaml
+edit_uri: 'blob/main/docs/'
+edit_uri_template: 'blob/main/docs/{path}'
+```
+
+(they are also mutually exclusive -- don't specify both).
+
+Starting from here, you can change the positioning or formatting of the path, in case the default behavior of appending the path isn't enough.
+
+The contents of `edit_uri_template` are normal [Python format strings](https://docs.python.org/3/library/string.html#formatstrings), with only these fields available:
+
+* `{path}`, e.g. `foo/bar.md`
+* `{path_noext}`, e.g. `foo/bar`
+
+And the conversion flag `!q` is available, to percent-encode the field:
+
+* `{path!q}`, e.g. `foo%2Fbar.md`
+
+Here are some suggested configurations that can be useful:
+
+GitHub Wiki:  
+(e.g. `https://github.com/project/repo/wiki/foo/bar/_edit`)
+
+```yaml
+repo_url: 'https://github.com/project/repo/wiki'
+edit_uri_template: '{path_noext}/_edit'
+```
+
+BitBucket editor:  
+(e.g. `https://bitbucket.org/project/repo/src/master/docs/foo/bar.md?mode=edit`)
+
+```yaml
+repo_url: 'https://bitbucket.org/project/repo/'
+edit_uri_template: 'src/master/docs/{path}?mode=edit'
+```
+
+GitLab Static Site Editor:  
+(e.g. `https://gitlab.com/project/repo/-/sse/master/docs%2Ffoo%2bar.md`)
+
+```yaml
+repo_url: 'https://gitlab.com/project/repo'
+edit_uri_template: '-/sse/master/docs%2F{path!q}'
+```
+
+GitLab Web IDE:  
+(e.g. `https://gitlab.com/-/ide/project/repo/edit/master/-/docs/foo/bar.md`)
+
+```yaml
+edit_uri_template: 'https://gitlab.com/-/ide/project/repo/edit/master/-/docs/{path}'
+```
+
+**default**: `null`
+
 ### site_description
 
 Set the site description. This will add a meta tag to the generated HTML header.
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 0c421866..9839114e 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -2,10 +2,14 @@ from __future__ import annotations
 
 import ipaddress
 import os
+import string
 import sys
 import traceback
 import typing as t
+import warnings
+from collections import UserString
 from typing import NamedTuple
+from urllib.parse import quote as urlquote
 from urllib.parse import urlsplit, urlunsplit
 
 import markdown
@@ -347,12 +351,11 @@ class URL(OptionallyRequired):
 
 
 class RepoURL(URL):
-    """
-    Repo URL Config Option
-
-    A small extension to the URL config that sets the repo_name and edit_uri,
-    based on the url if they haven't already been provided.
-    """
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            "RepoURL is no longer used in MkDocs and will be removed.", DeprecationWarning
+        )
+        super().__init__(*args, **kwargs)
 
     def post_validation(self, config, key_name):
         repo_host = urlsplit(config['repo_url']).netloc.lower()
@@ -385,6 +388,88 @@ class RepoURL(URL):
         config['edit_uri'] = edit_uri
 
 
+class EditURI(Type):
+    def __init__(self, repo_url_key):
+        super().__init__(str)
+        self.repo_url_key = repo_url_key
+
+    def post_validation(self, config, key_name):
+        edit_uri = config.get(key_name)
+        repo_url = config.get(self.repo_url_key)
+
+        if edit_uri is None and repo_url is not None:
+            repo_host = urlsplit(repo_url).netloc.lower()
+            if repo_host == 'github.com' or repo_host == 'gitlab.com':
+                edit_uri = 'edit/master/docs/'
+            elif repo_host == 'bitbucket.org':
+                edit_uri = 'src/default/docs/'
+
+        # ensure a well-formed edit_uri
+        if edit_uri and not edit_uri.endswith('/'):
+            edit_uri += '/'
+
+        config[key_name] = edit_uri
+
+
+class EditURITemplate(OptionallyRequired):
+    class Formatter(string.Formatter):
+        def convert_field(self, value, conversion):
+            if conversion == 'q':
+                return urlquote(value, safe='')
+            return super().convert_field(value, conversion)
+
+    class Template(UserString):
+        def __init__(self, formatter, data):
+            super().__init__(data)
+            self.formatter = formatter
+            try:
+                self.format('', '')
+            except KeyError as e:
+                raise ValueError(f"Unknown template substitute: {e}")
+
+        def format(self, path, path_noext):
+            return self.formatter.format(self.data, path=path, path_noext=path_noext)
+
+    def __init__(self, edit_uri_key=None):
+        super().__init__()
+        self.edit_uri_key = edit_uri_key
+
+    def run_validation(self, value):
+        try:
+            return self.Template(self.Formatter(), value)
+        except Exception as e:
+            raise ValidationError(e)
+
+    def post_validation(self, config, key_name):
+        if self.edit_uri_key and config.get(key_name) and config.get(self.edit_uri_key):
+            self.warnings.append(
+                f"The option '{self.edit_uri_key}' has no effect when '{key_name}' is set."
+            )
+
+
+class RepoName(Type):
+    def __init__(self, repo_url_key):
+        super().__init__(str)
+        self.repo_url_key = repo_url_key
+
+    def post_validation(self, config, key_name):
+        repo_name = config.get(key_name)
+        repo_url = config.get(self.repo_url_key)
+
+        # derive repo_name from repo_url if unset
+        if repo_url is not None and repo_name is None:
+            repo_host = urlsplit(config['repo_url']).netloc.lower()
+            if repo_host == 'github.com':
+                repo_name = 'GitHub'
+            elif repo_host == 'bitbucket.org':
+                repo_name = 'Bitbucket'
+            elif repo_host == 'gitlab.com':
+                repo_name = 'GitLab'
+            else:
+                repo_name = repo_host.split('.')[0].title()
+            config[key_name] = repo_name
+
+
 class FilesystemObject(Type):
     """
     Base class for options that point to filesystem objects.
diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py
index d2f99845..cf4b4073 100644
--- a/mkdocs/config/defaults.py
+++ b/mkdocs/config/defaults.py
@@ -63,17 +63,18 @@ class _MkDocsConfig:
     True generates nicer URLs, but False is useful if browsing the output on
     a filesystem."""
 
-    repo_url = config_options.RepoURL()
+    repo_url = config_options.URL()
     """Specify a link to the project source repo to be included
     in the documentation pages."""
 
-    repo_name = config_options.Type(str)
+    repo_name = config_options.RepoName('repo_url')
     """A name to use for the link to the project source repo.
     Default, If repo_url is unset then None, otherwise
     "GitHub", "Bitbucket" or "GitLab" for known url or Hostname
     for unknown urls."""
 
-    edit_uri = config_options.Type(str)
+    edit_uri_template = config_options.EditURITemplate('edit_uri')
+    edit_uri = config_options.EditURI('repo_url')
     """Specify a URI to the docs dir in the project source repo, relative to the
     repo_url. When set, a link directly to the page in the source repo will
     be added to the generated HTML. If repo_url is not set also, this option
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 651f8bca..7f9d2ef5 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -41,7 +41,9 @@ class Page:
         self.update_date = get_build_date()
 
         self._set_canonical_url(config.get('site_url', None))
-        self._set_edit_url(config.get('repo_url', None), config.get('edit_uri', None))
+        self._set_edit_url(
+            config.get('repo_url', None), config.get('edit_uri'), config.get('edit_uri_template')
+        )
 
         # Placeholders to be filled in later in the build process.
         self.markdown = None
@@ -172,10 +174,20 @@ class Page:
             self.canonical_url = None
             self.abs_url = None
 
-    def _set_edit_url(self, repo_url: Optional[str], edit_uri: Optional[str]) -> None:
-        if edit_uri:
+    def _set_edit_url(
+        self,
+        repo_url: Optional[str],
+        edit_uri: Optional[str] = None,
+        edit_uri_template: Optional[str] = None,
+    ) -> None:
+        if edit_uri or edit_uri_template:
             src_uri = self.file.src_uri
-            edit_uri += src_uri
+            if edit_uri_template:
+                noext = posixpath.splitext(src_uri)[0]
+                edit_uri = edit_uri_template.format(path=src_uri, path_noext=noext)
+            else:
+                assert edit_uri is not None and edit_uri.endswith('/')
+                edit_uri += src_uri
             if repo_url:
                 # Ensure urljoin behavior is correct
                 if not edit_uri.startswith(('?', '#')) and not repo_url.endswith('/'):


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index 3ad71f2f..a1b0b3c1 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -417,11 +417,12 @@ class URLTest(TestCase):
             self.get_config(Schema, {'option': 1})
 
 
-class RepoURLTest(TestCase):
+class EditURITest(TestCase):
     class Schema:
-        repo_url = config_options.RepoURL()
-        repo_name = config_options.Type(str)
-        edit_uri = config_options.Type(str)
+        repo_url = config_options.URL()
+        repo_name = config_options.RepoName('repo_url')
+        edit_uri_template = config_options.EditURITemplate('edit_uri')
+        edit_uri = config_options.EditURI('repo_url')
 
     def test_repo_name_github(self):
         config = self.get_config(
@@ -479,7 +480,7 @@ class RepoURLTest(TestCase):
             self.Schema,
             {'repo_url': "https://launchpad.net/python-tuskarclient"},
         )
-        self.assertEqual(config.get('edit_uri'), '')
+        self.assertEqual(config.get('edit_uri'), None)
         self.assertEqual(config['repo_url'], "https://launchpad.net/python-tuskarclient")
 
     def test_repo_name_custom_and_empty_edit_uri(self):
@@ -489,6 +490,60 @@ class RepoURLTest(TestCase):
         )
         self.assertEqual(config.get('edit_uri'), 'edit/master/docs/')
 
+    def test_edit_uri_template_ok(self):
+        config = self.get_config(
+            self.Schema,
+            {
+                'repo_url': "https://github.com/mkdocs/mkdocs",
+                'edit_uri_template': 'edit/foo/docs/{path}',
+            },
+        )
+        self.assertEqual(config['edit_uri_template'], 'edit/foo/docs/{path}')
+
+    def test_edit_uri_template_errors(self):
+        with self.expect_error(
+            edit_uri_template=re.compile(r'.*[{}].*')  # Complains about unclosed '{' or missing '}'
+        ):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{path',
+                },
+            )
+
+        with self.expect_error(edit_uri_template=re.compile(r'.*\bz\b.*')):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{path!z}',
+                },
+            )
+
+        with self.expect_error(edit_uri_template="Unknown template substitute: 'foo'"):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{foo}',
+                },
+            )
+
+    def test_edit_uri_template_warning(self):
+        config = self.get_config(
+            self.Schema,
+            {
+                'repo_url': "https://github.com/mkdocs/mkdocs",
+                'edit_uri': 'edit',
+                'edit_uri_template': 'edit/master/{path}',
+            },
+            warnings=dict(
+                edit_uri_template="The option 'edit_uri' has no effect when 'edit_uri_template' is set."
+            ),
+        )
+        self.assertEqual(config['edit_uri_template'], 'edit/master/{path}')
+
 
 class ListOfItemsTest(TestCase):
     def test_int_type(self):
diff --git a/mkdocs/tests/structure/page_tests.py b/mkdocs/tests/structure/page_tests.py
index a0d1e316..24b17a01 100644
--- a/mkdocs/tests/structure/page_tests.py
+++ b/mkdocs/tests/structure/page_tests.py
@@ -429,156 +429,139 @@ class PageTests(unittest.TestCase):
         self.assertEqual(pg.markdown, md_src)
         self.assertEqual(pg.meta, {})
 
-    def test_page_edit_url(self):
+    def test_page_edit_url(
+        self, paths={'testing.md': 'testing/', 'sub1/non-index.md': 'sub1/non-index/'}
+    ):
         for case in [
             dict(
                 config={'repo_url': 'http://github.com/mkdocs/mkdocs'},
                 edit_url='http://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
+                edit_url2='http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'https://github.com/mkdocs/mkdocs/'},
                 edit_url='https://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
+                edit_url2='https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com'},
                 edit_url=None,
+                edit_url2=None,
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': 'edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master/'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo', 'edit_uri': '/edit/master/'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': 'edit/master/'},
                 edit_url='http://example.com/foo/edit/master/testing.md',
+                edit_url2='http://example.com/foo/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo', 'edit_uri': 'edit/master/'},
                 edit_url='http://example.com/foo/edit/master/testing.md',
+                edit_url2='http://example.com/foo/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '?query=edit/master'},
                 edit_url='http://example.com?query=edit/master/testing.md',
+                edit_url2='http://example.com?query=edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/', 'edit_uri': '?query=edit/master/'},
                 edit_url='http://example.com/?query=edit/master/testing.md',
+                edit_url2='http://example.com/?query=edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '#edit/master'},
                 edit_url='http://example.com#edit/master/testing.md',
+                edit_url2='http://example.com#edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/', 'edit_uri': '#edit/master/'},
                 edit_url='http://example.com/#edit/master/testing.md',
+                edit_url2='http://example.com/#edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'edit_uri': 'http://example.com/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': ''},  # Set to blank value
-                edit_url=None,
-            ),
-            dict(config={}, edit_url=None),  # Nothing defined
-        ]:
-            with self.subTest(case['config']):
-                cfg = load_config(**case['config'])
-                fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
-                pg = Page('Foo', fl, cfg)
-                self.assertEqual(pg.url, 'testing/')
-                self.assertEqual(pg.edit_url, case['edit_url'])
-
-    def test_nested_page_edit_url(self, file_src_path='sub1/non-index.md'):
-        for case in [
-            dict(
-                config={'repo_url': 'http://github.com/mkdocs/mkdocs'},
-                edit_url='http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'https://github.com/mkdocs/mkdocs/'},
-                edit_url='https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com'},
-                edit_url=None,
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': 'edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master/'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo', 'edit_uri': '/edit/master/'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': 'edit/master/'},
-                edit_url='http://example.com/foo/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo', 'edit_uri': 'edit/master/'},
-                edit_url='http://example.com/foo/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '?query=edit/master'},
-                edit_url='http://example.com?query=edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/', 'edit_uri': '?query=edit/master/'},
-                edit_url='http://example.com/?query=edit/master/sub1/non-index.md',
+                config={'edit_uri_template': 'https://github.com/project/repo/wiki/{path_noext}'},
+                edit_url='https://github.com/project/repo/wiki/testing',
+                edit_url2='https://github.com/project/repo/wiki/sub1/non-index',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '#edit/master'},
-                edit_url='http://example.com#edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://github.com/project/repo/wiki',
+                    'edit_uri_template': '{path_noext}/_edit',
+                },
+                edit_url='https://github.com/project/repo/wiki/testing/_edit',
+                edit_url2='https://github.com/project/repo/wiki/sub1/non-index/_edit',
             ),
             dict(
-                config={'repo_url': 'http://example.com/', 'edit_uri': '#edit/master/'},
-                edit_url='http://example.com/#edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://gitlab.com/project/repo',
+                    'edit_uri_template': '-/sse/master/docs%2F{path!q}',
+                },
+                edit_url='https://gitlab.com/project/repo/-/sse/master/docs%2Ftesting.md',
+                edit_url2='https://gitlab.com/project/repo/-/sse/master/docs%2Fsub1%2Fnon-index.md',
             ),
             dict(
-                config={'edit_uri': 'http://example.com/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://bitbucket.org/project/repo/',
+                    'edit_uri_template': 'src/master/docs/{path}?mode=edit',
+                },
+                edit_url='https://bitbucket.org/project/repo/src/master/docs/testing.md?mode=edit',
+                edit_url2='https://bitbucket.org/project/repo/src/master/docs/sub1/non-index.md?mode=edit',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': ''},  # Set to blank value
+                config={
+                    'repo_url': 'http://example.com',
+                    'edit_uri': '',
+                    'edit_uri_template': '',
+                },  # Set to blank value
                 edit_url=None,
+                edit_url2=None,
             ),
+            dict(config={}, edit_url=None, edit_url2=None),  # Nothing defined
         ]:
-            with self.subTest(case['config']):
-                cfg = load_config(**case['config'], docs_dir=self.DOCS_DIR)
-                fl = File(
-                    file_src_path, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']
-                )
-                pg = Page('Foo', fl, cfg)
-                self.assertEqual(pg.url, 'sub1/non-index/')
-                self.assertEqual(pg.edit_url, case['edit_url'])
+            for i, path in enumerate(paths, 1):
+                edit_url_key = f'edit_url{i}' if i > 1 else 'edit_url'
+                with self.subTest(case['config'], path=path):
+                    cfg = load_config(**case['config'])
+                    fl = File(path, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
+                    pg = Page('Foo', fl, cfg)
+                    self.assertEqual(pg.url, paths[path])
+                    self.assertEqual(pg.edit_url, case[edit_url_key])
+
+    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
+    def test_page_edit_url_windows(self):
+        self.test_page_edit_url(
+            paths={'testing.md': 'testing/', 'sub1\\non-index.md': 'sub1/non-index/'}
+        )
 
     def test_page_edit_url_warning(self):
         for case in [
@@ -600,10 +583,6 @@ class PageTests(unittest.TestCase):
                 self.assertEqual(pg.edit_url, case['edit_url'])
                 self.assertEqual(cm.output, [case['warning']])
 
-    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
-    def test_nested_page_edit_url_windows(self):
-        self.test_nested_page_edit_url(file_src_path='sub1\\non-index.md')
-
     def test_page_render(self):
         cfg = load_config()
         fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])