From c85e91f3518a4a1594e8eb496e0b67f4358f024b Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Thu, 5 Jun 2025 19:40:43 +0100 Subject: [PATCH] 1.1.1: Remove overly restrictive directory structure (#140) * feat: add support for monorepos with symlink folders * edit_uri: fix edit URL for nested site_name fixes #127 and adds regression test for nested site_name in included project * Avoid replacing dots for underscores Resolves #123 * feat: removed restrictions on using directories outside root * chore: fmt * chore: bump dependencies * docs: mention fixtures in readme * fix: bump to 1.1.1 * docs: mention #129 in changelog * docs: mention #108 * docs: add #122 * docs: add #128 --------- Co-authored-by: Bart van den Ende Co-authored-by: Duncan Macleod Co-authored-by: Fokko Driesprong --- README.md | 2 + .../api/docs/index.md | 3 + .../api/docs/other/other.md | 1 + .../api/mkdocs.yml | 11 ++ .../docs/index.md | 3 + .../docs/other.md | 1 + .../mkdocs.yml | 12 ++ .../docs/index.md | 0 .../mkdocs.yml | 2 +- __tests__/integration/test.bats | 22 ++- docs/CHANGELOG.md | 9 ++ mkdocs_monorepo_plugin/edit_uri.py | 146 +++++++++-------- mkdocs_monorepo_plugin/merger.py | 36 +++-- mkdocs_monorepo_plugin/parser.py | 149 +++++++++++------- mkdocs_monorepo_plugin/plugin.py | 19 +-- mkdocs_monorepo_plugin/tests/test_plugin.py | 12 +- requirements.txt | 12 +- setup.py | 50 +++--- 18 files changed, 289 insertions(+), 201 deletions(-) create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/mkdocs.yml create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/other.md create mode 100644 __tests__/integration/fixtures/ok-include-path-edit-uri-nested/mkdocs.yml rename __tests__/integration/fixtures/{error-include-path-is-parent => ok-include-path-outside-root}/docs/index.md (100%) rename __tests__/integration/fixtures/{error-include-path-is-parent => ok-include-path-outside-root}/mkdocs.yml (70%) mode change 100644 => 100755 __tests__/integration/test.bats diff --git a/README.md b/README.md index 58bc9d8..e88dc33 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ $ pip install mkdocs-monorepo-plugin Take a look in [our sample project](./sample-docs) for an example implementation, or see [what it looks like after running `mkdocs build`](https://backstage.github.io/mkdocs-monorepo-plugin/monorepo-example/). +For further examples, take a look at our [test suite](__tests__/integration/fixtures) which covers dozens of good and bad examples of using Mkdocs with the `monorepo` plugin. + In general, this plugin introduces the `!include` syntax in your Mkdocs navigation structure and then merges them together. ```yaml diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md new file mode 100644 index 0000000..711b5da --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md @@ -0,0 +1,3 @@ +# API Documentation + +Some sub documentation \ No newline at end of file diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md new file mode 100644 index 0000000..8e2a183 --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md @@ -0,0 +1 @@ +# Other Documentation diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/mkdocs.yml b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/mkdocs.yml new file mode 100644 index 0000000..9e711e0 --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: "test/api" +site_description: "This is an api." +repo_url: https://github.com/backstage/mkdocs-monorepo-plugin +edit_uri: edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/ + +plugins: + - monorepo + +nav: + - Home: index.md + - Other: other/other.md diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md new file mode 100644 index 0000000..8296e87 --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md @@ -0,0 +1,3 @@ +# Top Level Project Documentation + +Some higher level info about the project \ No newline at end of file diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/other.md b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/other.md new file mode 100644 index 0000000..8e2a183 --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/other.md @@ -0,0 +1 @@ +# Other Documentation diff --git a/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/mkdocs.yml b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/mkdocs.yml new file mode 100644 index 0000000..a644ab7 --- /dev/null +++ b/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/mkdocs.yml @@ -0,0 +1,12 @@ +site_name: "Example" +site_description: "Description Here" +repo_url: https://github.com/backstage/mkdocs-monorepo-plugin +edit_uri: edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/ + +plugins: + - monorepo + +nav: + - Home: "index.md" + - Other: "other.md" + - API: "!include api/mkdocs.yml" diff --git a/__tests__/integration/fixtures/error-include-path-is-parent/docs/index.md b/__tests__/integration/fixtures/ok-include-path-outside-root/docs/index.md similarity index 100% rename from __tests__/integration/fixtures/error-include-path-is-parent/docs/index.md rename to __tests__/integration/fixtures/ok-include-path-outside-root/docs/index.md diff --git a/__tests__/integration/fixtures/error-include-path-is-parent/mkdocs.yml b/__tests__/integration/fixtures/ok-include-path-outside-root/mkdocs.yml similarity index 70% rename from __tests__/integration/fixtures/error-include-path-is-parent/mkdocs.yml rename to __tests__/integration/fixtures/ok-include-path-outside-root/mkdocs.yml index 395651d..9ba784a 100644 --- a/__tests__/integration/fixtures/error-include-path-is-parent/mkdocs.yml +++ b/__tests__/integration/fixtures/ok-include-path-outside-root/mkdocs.yml @@ -8,4 +8,4 @@ plugins: nav: - Home: index.md - - Section: '!include ../mkdocs.yml' + - Section: '!include ../ok/project-a/mkdocs.yml' diff --git a/__tests__/integration/test.bats b/__tests__/integration/test.bats old mode 100644 new mode 100755 index c7f133c..2926590 --- a/__tests__/integration/test.bats +++ b/__tests__/integration/test.bats @@ -198,10 +198,15 @@ teardown() { [[ "$output" == *"This contains a sentence which only exists in the ok-include-wildcard/project-b fixture."* ]] } -@test "fails if !include path is above current folder" { - cd ${fixturesDir}/error-include-path-is-parent - assertFailedMkdocs build - [[ "$output" == *"[mkdocs-monorepo] The mkdocs file "*"/__tests__/integration/fixtures/mkdocs.yml is outside of the current directory. Please move the file and try again."* ]] +@test "builds a mkdocs site if !include path is outside root" { + cd ${fixturesDir}/ok-include-path-outside-root + assertSuccessMkdocs build + + assertFileExists site/index.html + [[ "$output" == *"Lorem markdownum aequora Famemque, a ramos regna Ulixem verba, posito qui +nubilus membra."* ]] + assertFileExists site/test/index.html + [[ "$output" == *"This contains a sentence which only exists in the ok/project-a fixture."* ]] } @test "fails if !include path contains !include" { @@ -268,6 +273,15 @@ teardown() { assertFileContains './site/test/other/other/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri/api/docs/other/other.md"' } +@test "sets edit url for nested included path pages" { + cd ${fixturesDir}/ok-include-path-edit-uri-nested + assertSuccessMkdocs build + + assertFileContains './site/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md"' + assertFileContains './site/test/api/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md"' + assertFileContains './site/test/api/other/other/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md"' +} + @test "sets edit url for included wildcard pages" { cd ${fixturesDir}/ok-include-wildcard-edit-uri assertSuccessMkdocs build diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dd2656a..cf382be 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.1.1 + +- Removed overly restrictive directory structure. It is now possible to call directories outside of your current directory. [#139] +- Bumped core dependencies +- Avoid replacing dots for underscores [#129] +- Remove use of warning_filter (deprecated) [#108] +- add support for monorepos with symlink folders [#122] +- Fix edit URL for included projects with nested site_name [#128] + ## 1.1.0 - Dropped official support for Python 3.7 diff --git a/mkdocs_monorepo_plugin/edit_uri.py b/mkdocs_monorepo_plugin/edit_uri.py index 4b0bbe5..8845ec3 100644 --- a/mkdocs_monorepo_plugin/edit_uri.py +++ b/mkdocs_monorepo_plugin/edit_uri.py @@ -12,94 +12,102 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mkdocs.utils import yaml_load from os import path +from mkdocs.utils import yaml_load + + class EditUrl: - def __init__(self, config, page, plugin): - self.config = config - self.page = page - self.plugin = plugin + def __init__(self, config, page, plugin): + self.config = config + self.page = page + self.plugin = plugin - def __get_root_config_file_path(self): - return path.dirname(self.config['config_file_path']) + def __get_root_config_file_path(self): + return path.dirname(self.config["config_file_path"]) - def __get_root_docs_dir(self): - abs_root_config_file_dir = self.__get_root_config_file_path() - return path.relpath(self.plugin.originalDocsDir, abs_root_config_file_dir) + def __get_root_docs_dir(self): + abs_root_config_file_dir = self.__get_root_config_file_path() + return path.relpath(self.plugin.originalDocsDir, abs_root_config_file_dir) - def __get_page_dir_alias(self): - parts = self.page.url.split('/') - while True: - parts.pop() - alias = path.join(*parts) - if alias in self.plugin.aliases: - return alias + def __get_page_dir_alias(self): + parts = self.page.url.split("/") + while True: + parts.pop() + alias = path.join(*parts) + if alias in self.plugin.aliases: + return alias - def __get_page_docs_dir(self): - alias = self.__get_page_dir_alias() - abs_root_config_file_dir = self.__get_root_config_file_path() - abs_page_config_file_dir = self.plugin.aliases[alias]['docs_dir'] - return path.relpath(abs_page_config_file_dir, abs_root_config_file_dir) + def __get_page_docs_dir(self): + alias = self.__get_page_dir_alias() + abs_root_config_file_dir = self.__get_root_config_file_path() + abs_page_config_file_dir = self.plugin.aliases[alias]["docs_dir"] + return path.relpath(abs_page_config_file_dir, abs_root_config_file_dir) - def __get_page_src_path(self): - alias = self.page.url.split('/')[0] - path = self.page.file.src_path - return path.replace('{}/'.format(alias), '') + def __get_page_src_path(self): + alias = self.__get_page_dir_alias() + path = self.page.file.src_path + return path.replace("{}/".format(alias), "") - def __get_page_config_file_path(self): - alias = self.__get_page_dir_alias() - return self.plugin.aliases[alias]['yaml_file'] + def __get_page_config_file_path(self): + alias = self.__get_page_dir_alias() + return self.plugin.aliases[alias]["yaml_file"] - def __load_page_config_file(self, file): - config = yaml_load(file) + def __load_page_config_file(self, file): + config = yaml_load(file) - root_docs_dir = self.__get_root_docs_dir() - root_repo_url = self.config.get('repo_url') - root_edit_uri = self.config.get('edit_uri', '') or '' + root_docs_dir = self.__get_root_docs_dir() + root_repo_url = self.config.get("repo_url") + root_edit_uri = self.config.get("edit_uri", "") or "" - page_docs_dir = self.__get_page_docs_dir() - page_repo_url = config.get('repo_url', root_repo_url) - page_edit_uri = config.get('edit_uri', root_edit_uri.replace(root_docs_dir, page_docs_dir)) + page_docs_dir = self.__get_page_docs_dir() + page_repo_url = config.get("repo_url", root_repo_url) + page_edit_uri = config.get( + "edit_uri", root_edit_uri.replace(root_docs_dir, page_docs_dir) + ) - # ensure a well-formed edit_uri - if page_edit_uri: - if not page_edit_uri.startswith(('?', '#')) \ - and not page_repo_url.endswith('/'): - page_repo_url += '/' - if not page_edit_uri.endswith('/'): - page_edit_uri += '/' + # ensure a well-formed edit_uri + if page_edit_uri: + if not page_edit_uri.startswith(("?", "#")) and not page_repo_url.endswith( + "/" + ): + page_repo_url += "/" + if not page_edit_uri.endswith("/"): + page_edit_uri += "/" - config['docs_dir'] = page_docs_dir - config['edit_uri'] = page_edit_uri - config['repo_url'] = page_repo_url + config["docs_dir"] = page_docs_dir + config["edit_uri"] = page_edit_uri + config["repo_url"] = page_repo_url - return config + return config - def __get_page_config_file_yaml(self): - abs_page_config_file_path = self.__get_page_config_file_path() - with open(abs_page_config_file_path, 'rb') as f: - return self.__load_page_config_file(f) + def __get_page_config_file_yaml(self): + abs_page_config_file_path = self.__get_page_config_file_path() + with open(abs_page_config_file_path, "rb") as f: + return self.__load_page_config_file(f) - def __has_repo(self): - page_config_file_yaml = self.__get_page_config_file_yaml() - return bool(page_config_file_yaml.get('repo_url')) + def __has_repo(self): + page_config_file_yaml = self.__get_page_config_file_yaml() + return bool(page_config_file_yaml.get("repo_url")) - def __is_root(self): - root_config_docs_dir = self.__get_root_docs_dir() - abs_root_config_file_dir = self.__get_root_config_file_path() - abs_root_config_docs_dir = path.join(abs_root_config_file_dir, root_config_docs_dir) + def __is_root(self): + root_config_docs_dir = self.__get_root_docs_dir() + abs_root_config_file_dir = self.__get_root_config_file_path() + abs_root_config_docs_dir = path.join( + abs_root_config_file_dir, root_config_docs_dir + ) - return path.realpath(abs_root_config_docs_dir) in self.page.file.abs_src_path + return path.realpath(abs_root_config_docs_dir) in self.page.file.abs_src_path + + def build(self): + if self.__is_root(): + return self.page.edit_url + if self.__has_repo(): + config = self.__get_page_config_file_yaml() + return config["repo_url"] + config["edit_uri"] + self.__get_page_src_path() + return None - def build(self): - if self.__is_root(): - return self.page.edit_url - if self.__has_repo(): - config = self.__get_page_config_file_yaml() - return config['repo_url'] + config['edit_uri'] + self.__get_page_src_path() - return None def set_edit_url(config, page, plugin): - edit_url = EditUrl(config, page, plugin) - page.edit_url = edit_url.build() + edit_url = EditUrl(config, page, plugin) + page.edit_url = edit_url.build() diff --git a/mkdocs_monorepo_plugin/merger.py b/mkdocs_monorepo_plugin/merger.py index 3b3f9da..b4fe0e5 100644 --- a/mkdocs_monorepo_plugin/merger.py +++ b/mkdocs_monorepo_plugin/merger.py @@ -12,18 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from tempfile import TemporaryDirectory -from shutil import copytree - import logging import os from os.path import join from pathlib import Path - -from mkdocs.utils import warning_filter +from shutil import copytree +from tempfile import TemporaryDirectory log = logging.getLogger(__name__) -log.addFilter(warning_filter) # This collects the multiple docs/ folders and merges them together. @@ -31,23 +27,30 @@ log.addFilter(warning_filter) class Merger: def __init__(self, config): self.config = config - self.root_docs_dir = config['docs_dir'] + self.root_docs_dir = config["docs_dir"] self.docs_dirs = list() - self.append('', self.root_docs_dir) + self.append("", self.root_docs_dir) self.files_source_dir = dict() def append(self, alias, docs_dir): self.docs_dirs.append([alias, docs_dir]) def merge(self): - self.temp_docs_dir = TemporaryDirectory('', 'docs_') + self.temp_docs_dir = TemporaryDirectory("", "docs_") - aliases = list(filter(lambda docs_dir: len(docs_dir) > 0, map( - lambda docs_dir: docs_dir[0], self.docs_dirs))) + aliases = list( + filter( + lambda docs_dir: len(docs_dir) > 0, + map(lambda docs_dir: docs_dir[0], self.docs_dirs), + ) + ) if len(aliases) != len(set(aliases)): log.critical( - "[mkdocs-monorepo] You cannot have duplicated site names. " + - "Current registered site names in the monorepository: {}".format(', '.join(aliases))) + "[mkdocs-monorepo] You cannot have duplicated site names. " + + "Current registered site names in the monorepository: {}".format( + ", ".join(aliases) + ) + ) raise SystemExit(1) for alias, docs_dir in self.docs_dirs: @@ -60,7 +63,7 @@ class Merger: if os.path.exists(source_dir): copytree(source_dir, dest_dir, symlinks=True, dirs_exist_ok=True) - for file_abs_path in Path(source_dir).rglob('*.md'): + for file_abs_path in Path(source_dir).rglob("*.md"): file_abs_path = str(file_abs_path) # python 3.5 compatibility if os.path.isfile(file_abs_path): file_rel_path = os.path.relpath(file_abs_path, source_dir) @@ -69,8 +72,9 @@ class Merger: else: log.critical( - "[mkdocs-monorepo] The {} path is not valid. ".format(source_dir) + - "Please update your 'nav' with a valid path.") + "[mkdocs-monorepo] The {} path is not valid. ".format(source_dir) + + "Please update your 'nav' with a valid path." + ) raise SystemExit(1) return str(self.temp_docs_dir.name) diff --git a/mkdocs_monorepo_plugin/parser.py b/mkdocs_monorepo_plugin/parser.py index 4a80d5d..1dbed44 100644 --- a/mkdocs_monorepo_plugin/parser.py +++ b/mkdocs_monorepo_plugin/parser.py @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import logging import os -import copy import re from pathlib import Path - -from slugify import slugify -from mkdocs.utils import yaml_load, warning_filter, dirname_to_title, get_markdown_title from urllib.parse import urlsplit + +from mkdocs.utils import dirname_to_title, get_markdown_title, yaml_load +from slugify import slugify + log = logging.getLogger(__name__) -log.addFilter(warning_filter) INCLUDE_STATEMENT = "!include " WILDCARD_INCLUDE_STATEMENT = "*include " @@ -30,7 +30,7 @@ WILDCARD_INCLUDE_STATEMENT = "*include " class Parser: def __init__(self, config): - self.initialNav = config['nav'] + self.initialNav = config["nav"] self.config = config def __loadAliasesAndResolvedPaths(self, nav=None): @@ -45,21 +45,23 @@ class Parser: elif type(item) is dict: value = list(item.values())[0] if type(value) is str and value.startswith(WILDCARD_INCLUDE_STATEMENT): - root_dir = Path(self.config['config_file_path']).parent - mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT):] + root_dir = Path(self.config["config_file_path"]).parent + mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT) :] dirs = sorted(root_dir.glob(mkdocs_path)) if dirs: value = [] for mkdocs_config in dirs: site = {} if os.path.exists(mkdocs_config): - site[str(mkdocs_config)] = f"{INCLUDE_STATEMENT}{mkdocs_config.resolve()}" + site[str(mkdocs_config)] = ( + f"{INCLUDE_STATEMENT}{mkdocs_config.absolute()}" + ) value.append(site) else: value = None if type(value) is str and value.startswith(INCLUDE_STATEMENT): - paths.append(value[len(INCLUDE_STATEMENT):]) + paths.append(value[len(INCLUDE_STATEMENT) :]) elif type(value) is list: paths.extend(self.__loadAliasesAndResolvedPaths(value)) @@ -67,20 +69,25 @@ class Parser: def getResolvedPaths(self): """Return list of [alias, docs_dir, mkdocs.yml].""" + def extractAliasAndPath(absPath): loader = IncludeNavLoader(self.config, absPath).read() alias = loader.getAlias() - docsDir = os.path.join(loader.rootDir, os.path.dirname(absPath), loader.getDocsDir()) + docsDir = os.path.join( + loader.rootDir, os.path.dirname(absPath), loader.getDocsDir() + ) return [alias, docsDir, os.path.join(loader.rootDir, absPath)] resolvedPaths = list( - map(extractAliasAndPath, self.__loadAliasesAndResolvedPaths())) + map(extractAliasAndPath, self.__loadAliasesAndResolvedPaths()) + ) for alias, docsDir, ymlPath in resolvedPaths: if not os.path.exists(docsDir): log.critical( - "[mkdocs-monorepo] The {} path is not valid. ".format(docsDir) + - "Please update your 'nav' with a valid path.") + "[mkdocs-monorepo] The {} path is not valid. ".format(docsDir) + + "Please update your 'nav' with a valid path." + ) raise SystemExit(1) return resolvedPaths @@ -97,12 +104,13 @@ class Parser: key = list(item.keys())[0] value = list(item.values())[0] if type(value) is str and value.startswith(WILDCARD_INCLUDE_STATEMENT): - root_dir = Path(self.config['config_file_path']).parent - mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT):] + root_dir = Path(self.config["config_file_path"]).parent + mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT) :] if not mkdocs_path.endswith(tuple([".yml", ".yaml"])): log.critical( "[mkdocs-monorepo] The wildcard include path {} does not end with .yml (or .yaml)".format( - mkdocs_path) + mkdocs_path + ) ) raise SystemExit(1) dirs = sorted(root_dir.glob(mkdocs_path)) @@ -111,18 +119,24 @@ class Parser: for mkdocs_config in dirs: site = {} try: - with open(mkdocs_config, 'rb') as f: + with open(mkdocs_config, "rb") as f: site_yaml = yaml_load(f) site_name = site_yaml["site_name"] - site[site_name] = f"{INCLUDE_STATEMENT}{mkdocs_config.resolve()}" + site[site_name] = ( + f"{INCLUDE_STATEMENT}{mkdocs_config.absolute()}" + ) value.append(site) except OSError: - log.error(f"[mkdocs-monorepo] The {mkdocs_config} path is not valid.") + log.error( + f"[mkdocs-monorepo] The {mkdocs_config} path is not valid." + ) except KeyError: log.critical( - "[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(mkdocs_config) + - "in the YAML file. Please include it to indicate where your documentation " + - "should be moved to." + "[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format( + mkdocs_config + ) + + "in the YAML file. Please include it to indicate where your documentation " + + "should be moved to." ) raise SystemExit(1) if not value: @@ -135,10 +149,11 @@ class Parser: if type(value) is str and value.startswith(INCLUDE_STATEMENT): nav[index] = {} - nav[index][key] = IncludeNavLoader( - self.config, - value[len(INCLUDE_STATEMENT):] - ).read().getNav() + nav[index][key] = ( + IncludeNavLoader(self.config, value[len(INCLUDE_STATEMENT) :]) + .read() + .getNav() + ) if nav[index][key] is None: return None @@ -153,13 +168,18 @@ class Parser: class IncludeNavLoader: - def __init__(self, config, navPath): - self.rootDir = os.path.normpath(os.path.join( - os.getcwd(), config['config_file_path'], '../')) + def __init__(self, config, navPath, ancestors=None): + self.rootDir = os.path.normpath( + os.path.join(os.getcwd(), config["config_file_path"], "../") + ) self.navPath = navPath - self.absNavPath = os.path.normpath( - os.path.join(self.rootDir, self.navPath)) + self.absNavPath = os.path.normpath(os.path.join(self.rootDir, self.navPath)) self.navYaml = None + # Track ancestor paths to detect cycles + if ancestors is None: + self.ancestors = set() + else: + self.ancestors = set(ancestors) def getAbsNavPath(self): return self.absNavPath @@ -168,51 +188,53 @@ class IncludeNavLoader: if not self.absNavPath.endswith(tuple([".yml", ".yaml"])): log.critical( "[mkdocs-monorepo] The included file path {} does not point to a .yml (or .yaml) file".format( - self.absNavPath) + self.absNavPath + ) ) raise SystemExit(1) - if not self.absNavPath.startswith(self.rootDir): + # Cycle detection: block self-references + if self.absNavPath in self.ancestors: log.critical( - "[mkdocs-monorepo] The mkdocs file {} is outside of the current directory. ".format(self.absNavPath) + - "Please move the file and try again." + f"[mkdocs-monorepo] Detected a self-reference or cycle when including {self.absNavPath}. " + f"Inclusion chain: {' -> '.join(self.ancestors)} -> {self.absNavPath}" ) raise SystemExit(1) + # Add current file to ancestors for downstream includes + self.ancestors.add(self.absNavPath) + try: - with open(self.absNavPath, 'rb') as f: + with open(self.absNavPath, "rb") as f: self.navYaml = yaml_load(f) # This will check if there is a `docs_dir` property on the `mkdocs.yml` file of # the sub folder and scaffold the `nav` property from it - if self.navYaml and 'nav' not in self.navYaml: + if self.navYaml and "nav" not in self.navYaml: docsDir = self.navYaml.get("docs_dir", "docs") docsDirPath = os.path.join(os.path.dirname(self.absNavPath), docsDir) def navFromDir(path): directory = {} - for dirname, dirnames, filenames in os.walk(path): - dirnames.sort() filenames.sort() - if dirname == docsDirPath: dn = os.path.basename(dirname) else: dn = dirname_to_title(os.path.basename(dirname)) directory[dn] = [] - for dirItem in dirnames: subNav = navFromDir(path=os.path.join(path, dirItem)) if subNav: directory[dn].append(subNav) - for fileItem in filenames: fileName, fileExt = os.path.splitext(fileItem) - if fileExt == '.md': + if fileExt == ".md": fileTitle = get_markdown_title(fileName) - filePath = os.path.join(os.path.relpath(path, docsDirPath), fileItem) + filePath = os.path.join( + os.path.relpath(path, docsDirPath), fileItem + ) directory[dn].append({fileTitle: filePath}) if len(directory[dn]) == 0 or directory[dn] == [{}]: @@ -226,30 +248,36 @@ class IncludeNavLoader: except OSError: log.critical( - "[mkdocs-monorepo] The file path {} does not exist, ".format(self.absNavPath) + - "is not valid YAML, " + - "or does not contain a valid 'site_name' and 'nav' keys." + "[mkdocs-monorepo] The file path {} does not exist, ".format( + self.absNavPath + ) + + "is not valid YAML, " + + "or does not contain a valid 'site_name' and 'nav' keys." ) raise SystemExit(1) - if self.navYaml and 'site_name' not in self.navYaml: + if self.navYaml and "site_name" not in self.navYaml: log.critical( - "[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(self.absNavPath) + - "in the YAML file. Please include it to indicate where your documentation " + - "should be moved to." + "[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format( + self.absNavPath + ) + + "in the YAML file. Please include it to indicate where your documentation " + + "should be moved to." ) raise SystemExit(1) - if self.navYaml and 'nav' not in self.navYaml: + if self.navYaml and "nav" not in self.navYaml: log.critical( - "[mkdocs-monorepo] The file path {} ".format(self.absNavPath) + - "does not contain a valid 'nav' key in the YAML file " + - "and the docs folder is not the default one, i.e. `docs`. " + - "Please include the `nav` key to indicate how your documentation should be presented in the navigation, " + - "or include a 'docs_dir' to indicate that automatic nav generation should be used." + "[mkdocs-monorepo] The file path {} ".format(self.absNavPath) + + "does not contain a valid 'nav' key in the YAML file " + + "and the docs folder is not the default one, i.e. `docs`. " + + "Please include the `nav` key to indicate how your documentation should be presented in the navigation, " + + "or include a 'docs_dir' to indicate that automatic nav generation should be used." ) raise SystemExit(1) + # Remove current file from ancestors after processing (for sibling includes) + self.ancestors.remove(self.absNavPath) return self def getDocsDir(self): @@ -257,7 +285,7 @@ class IncludeNavLoader: def getAlias(self): alias = self.navYaml["site_name"] - regex = '^[a-zA-Z0-9_\-/]+$' # noqa: W605 + regex = "^[a-zA-Z0-9_\.\-/]+$" # noqa: W605 if re.match(regex, alias) is None: alias = slugify(self.navYaml["site_name"]) @@ -284,7 +312,8 @@ class IncludeNavLoader: if value.startswith(INCLUDE_STATEMENT): log.critical( - "[mkdocs-monorepo] We currently do not support nested !include statements inside of Mkdocs.") + "[mkdocs-monorepo] We currently do not support nested !include statements inside of Mkdocs." + ) raise SystemExit(1) def formatNavLink(alias, value): diff --git a/mkdocs_monorepo_plugin/plugin.py b/mkdocs_monorepo_plugin/plugin.py index 5774805..738d266 100644 --- a/mkdocs_monorepo_plugin/plugin.py +++ b/mkdocs_monorepo_plugin/plugin.py @@ -13,9 +13,10 @@ # limitations under the License. from mkdocs.plugins import BasePlugin -from .parser import Parser -from .merger import Merger + from .edit_uri import set_edit_url +from .merger import Merger +from .parser import Parser class MonorepoPlugin(BasePlugin): @@ -30,29 +31,29 @@ class MonorepoPlugin(BasePlugin): """Initialize MonorepoPlugin and return config of aggregated docs folder.""" # If no 'nav' defined, we don't need to run. - if not config.get('nav'): + if not config.get("nav"): return config # setting originalDocsDir means that on_config has been run - self.originalDocsDir = config['docs_dir'] + self.originalDocsDir = config["docs_dir"] # Handle !import statements self.parser = Parser(config) resolvedNav = self.parser.resolve() resolvedPaths = self.parser.getResolvedPaths() - config['nav'] = resolvedNav + config["nav"] = resolvedNav # Generate a new "docs" directory self.merger = Merger(config) self.aliases = {} for alias, docs_dir, yaml_file in resolvedPaths: - self.aliases[alias] = { 'docs_dir': docs_dir, 'yaml_file': yaml_file } + self.aliases[alias] = {"docs_dir": docs_dir, "yaml_file": yaml_file} self.merger.append(alias, docs_dir) new_docs_dir = self.merger.merge() # Update the docs_dir with our temporary one! - config['docs_dir'] = new_docs_dir + config["docs_dir"] = new_docs_dir # Store resolved paths for later. self.resolvedPaths = resolvedPaths @@ -77,8 +78,8 @@ class MonorepoPlugin(BasePlugin): if self.originalDocsDir is None: return # Support mkdocs < 1.2 - if hasattr(server, 'watcher'): - buildfunc = list(server.watcher._tasks.values())[0]['func'] + if hasattr(server, "watcher"): + buildfunc = list(server.watcher._tasks.values())[0]["func"] # still watch the original docs/ directory server.watch(self.originalDocsDir, buildfunc) diff --git a/mkdocs_monorepo_plugin/tests/test_plugin.py b/mkdocs_monorepo_plugin/tests/test_plugin.py index b049e1d..53afab7 100644 --- a/mkdocs_monorepo_plugin/tests/test_plugin.py +++ b/mkdocs_monorepo_plugin/tests/test_plugin.py @@ -1,10 +1,11 @@ #!/usr/bin/env python import unittest + from mkdocs_monorepo_plugin import plugin as p -class MockServer(): +class MockServer: """MockServer tracks what the livereload.Server instance is watching.""" def __init__(self): @@ -22,10 +23,7 @@ class TestMonorepoPlugin(unittest.TestCase): def test_plugin_on_config_with_nav(self): plugin = p.MonorepoPlugin() - plugin.on_config({ - "nav": {"page1": "page1.md"}, - "docs_dir": "docs" - }) + plugin.on_config({"nav": {"page1": "page1.md"}, "docs_dir": "docs"}) self.assertEqual(plugin.originalDocsDir, "docs") def test_plugin_on_serve_no_run(self): @@ -41,6 +39,4 @@ class TestMonorepoPlugin(unittest.TestCase): plugin.resolvedPaths = [] server = MockServer() plugin.on_serve(server, {}) - self.assertSetEqual(set(server.watched), { - "docs" - }) + self.assertSetEqual(set(server.watched), {"docs"}) diff --git a/requirements.txt b/requirements.txt index 7d1cbef..96c9a34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -flake8>=3.7.8 -mkdocs>=1.2.3 -mkdocs-git-authors-plugin==0.3.2 -mkdocs-git-revision-date-localized-plugin==0.5.0 -python-slugify==4.0.1 -importlib-metadata==4.13.0 +flake8>=7.2.0 +mkdocs>=1.6.1 +mkdocs-git-authors-plugin==0.9.5 +mkdocs-git-revision-date-localized-plugin==1.4.7 +python-slugify==8.0.4 +importlib-metadata==8.7.0 diff --git a/setup.py b/setup.py index f7827ff..d6e7581 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,35 @@ import setuptools - setuptools.setup( - name='mkdocs-monorepo-plugin', - version='1.1.0', - description='Plugin for adding monorepository support in Mkdocs.', + name="mkdocs-monorepo-plugin", + version="1.1.0", + description="Plugin for adding monorepository support in Mkdocs.", long_description=""" This introduces support for the !include syntax in mkdocs.yml, allowing you to import additional Mkdocs navigation. It enables large or complex repositories to have their own sets of docs/ folders, whilst generating only a single Mkdocs site. This is built and maintained by the Backstage open source team. """, # noqa: E501 - keywords='mkdocs monorepo', - url='https://github.com/backstage/mkdocs-monorepo-plugin', - author='Bilawal Hameed', - author_email='bil@spotify.com', - license='Apache-2.0', - python_requires='>=3', - install_requires=[ - 'mkdocs>=1.0.4', - 'python-slugify>=4.0.1' - ], + keywords="mkdocs monorepo", + url="https://github.com/backstage/mkdocs-monorepo-plugin", + author="Bilawal Hameed", + author_email="bil@spotify.com", + license="Apache-2.0", + python_requires=">=3", + install_requires=["mkdocs>=1.0.4", "python-slugify>=4.0.1"], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12' + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], packages=setuptools.find_packages(), entry_points={ - 'mkdocs.plugins': [ - "monorepo = mkdocs_monorepo_plugin.plugin:MonorepoPlugin" - ] - } + "mkdocs.plugins": ["monorepo = mkdocs_monorepo_plugin.plugin:MonorepoPlugin"] + }, )