diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a9352d..787df18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,51 +30,36 @@ jobs: strategy: fail-fast: false matrix: - include: - - platform: ubuntu-latest - python-version: 3.8 - py: py38 - - platform: ubuntu-latest - python-version: 3.9 - py: py39 - - platform: ubuntu-latest - python-version: "3.10" - py: py310 - - platform: ubuntu-latest - python-version: "3.11" - py: py311 - - platform: ubuntu-latest - python-version: "3.12" - py: py312 - - platform: macos-latest - python-version: 3.8 - py: py38 - - platform: macos-latest - python-version: "3.11" - py: py311 - - platform: macos-latest - python-version: "3.12" - py: py312 - - platform: windows-latest - python-version: 3.8 - py: py38 - - platform: windows-latest - python-version: "3.11" - py: py311 - - platform: windows-latest - python-version: "3.12" - py: py312 + py: + - 3.8 + - 3.9 + - "3.10" + - "3.11" + - "3.12" + platform: + - ubuntu-latest + - macos-latest + - windows-latest steps: - uses: actions/checkout@v4 - - name: Set up Python v${{ matrix.python-version }} + - name: Set up Python v${{ matrix.py }} uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.py }} + - name: Pick environment to run + id: env + shell: python + run: | + import codecs, os, sys + env = f"py=py3{sys.version_info[1]}\n" + print(f"Picked {env.split('=')[1].strip()} for {sys.version}") + with codecs.open(os.environ["GITHUB_OUTPUT"], "a", "utf-8") as file_handler: + file_handler.write(env) - name: Install dependencies run: pip install -U hatch - name: Run tests run: | - hatch run +py=${{ matrix.py }} tests:all + hatch run +py=${{ steps.env.outputs.py }} tests:all - name: Convert coverage to XML run: | pip install coverage covdefaults @@ -97,7 +82,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: "3.10" + python-version: "3.11" - name: Install build dependencies run: pip install -U hatch - name: Build package diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74f9e65..9ab0710 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,14 +26,14 @@ repos: - id: check-useless-excludes name: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace name: trailing-whitespace - id: end-of-file-fixer name: end-of-file-fixer - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.2 hooks: - id: ruff args: @@ -44,7 +44,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/mondeja/mdpo - rev: v2.0.0 + rev: v2.0.1 hooks: - id: md2po2md files: ^README\.md @@ -67,7 +67,7 @@ repos: hooks: - id: yamllint - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.12.1 + rev: v0.13.0 hooks: - id: markdownlint-cli2 name: markdownlint-readme @@ -79,12 +79,23 @@ repos: name: editorconfig-checker alias: ec - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy + name: mypy-mkdocs-1.5.0 files: ^src additional_dependencies: - - mkdocs + - mkdocs>=1.5.0,<1.6.0 + - platformdirs + - wcmatch + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 + hooks: + - id: mypy + name: mypy-mkdocs-1.6.0 + files: ^src + additional_dependencies: + - mkdocs>=1.6.0,<1.7.0 - platformdirs - wcmatch - repo: https://github.com/tcort/markdown-link-check diff --git a/pyproject.toml b/pyproject.toml index 1466d96..60e2f16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mkdocs-include-markdown-plugin" -version = "6.0.5" +version = "6.0.6" description = "Mkdocs Markdown includer plugin." readme = "README.md" license = "Apache-2.0" @@ -74,7 +74,7 @@ dependencies = ["pytest~=7.0", "coverage~=6.4", "covdefaults"] [[tool.hatch.envs.tests.matrix]] python = ["py38", "py39", "py310", "py311", "py312"] -mkdocs = ["1.4.0", "1.4.3", "1.5.0", "1.5.3"] +mkdocs = ["1.4.0", "1.4.3", "1.5.0", "1.5.3", "1.6.0"] cache = ["yes", "no"] [tool.hatch.envs.tests.overrides] @@ -83,6 +83,7 @@ matrix.mkdocs.dependencies = [ { value = "mkdocs==1.4.3", if = ["1.4.3"] }, { value = "mkdocs==1.5.0", if = ["1.5.0"] }, { value = "mkdocs==1.5.3", if = ["1.5.3"] }, + { value = "mkdocs==1.6.0", if = ["1.6.0"] }, ] matrix.cache.dependencies = [{ value = "platformdirs", if = ["yes"] }] diff --git a/src/mkdocs_include_markdown_plugin/directive.py b/src/mkdocs_include_markdown_plugin/directive.py index 41145df..4c8a432 100644 --- a/src/mkdocs_include_markdown_plugin/directive.py +++ b/src/mkdocs_include_markdown_plugin/directive.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +from mkdocs.exceptions import PluginError from wcmatch import glob from mkdocs_include_markdown_plugin import process @@ -174,19 +175,19 @@ def parse_bool_options( def resolve_file_paths_to_include( - filename_or_url: str, - includer_page_src_path: str, + include_string: str, + includer_page_src_path: str | None, docs_dir: str, ignore_paths: list[str], ) -> tuple[list[str], bool]: """Resolve the file paths to include for a directive.""" - if process.is_url(filename_or_url): - return [filename_or_url], True + if process.is_url(include_string): + return [include_string], True - if process.is_absolute_path(filename_or_url): + if process.is_absolute_path(include_string): if os.name == 'nt': # pragma: nt cover # Windows - fpath = os.path.normpath(filename_or_url) + fpath = os.path.normpath(include_string) if not os.path.isfile(fpath): return [], False @@ -195,13 +196,19 @@ def resolve_file_paths_to_include( ), False return process.filter_paths( glob.iglob( - os.path.normpath(filename_or_url), + os.path.normpath(include_string), flags=GLOB_FLAGS, ), ignore_paths, ), False - if process.is_relative_path(filename_or_url): + if process.is_relative_path(include_string): + if includer_page_src_path is None: # pragma: no cover + raise PluginError( + 'Relative paths are not allowed when the includer page' + ' source path is not provided. The include string' + f" '{include_string}' is located inside a generated page.", + ) root_dir = os.path.abspath( os.path.dirname(includer_page_src_path), ) @@ -209,7 +216,7 @@ def resolve_file_paths_to_include( ( os.path.normpath(os.path.join(root_dir, fp)) for fp in glob.iglob( - filename_or_url, + include_string, flags=GLOB_FLAGS, root_dir=root_dir, ) @@ -221,7 +228,7 @@ def resolve_file_paths_to_include( ( os.path.normpath(os.path.join(docs_dir, fp)) for fp in glob.iglob( - filename_or_url, + include_string, flags=GLOB_FLAGS, root_dir=docs_dir, ) @@ -232,7 +239,7 @@ def resolve_file_paths_to_include( def resolve_file_paths_to_exclude( exclude_string: str, - includer_page_src_path: str, + includer_page_src_path: str | None, docs_dir: str, ) -> list[str]: """Resolve the file paths to exclude for a directive.""" @@ -241,6 +248,12 @@ def resolve_file_paths_to_exclude( return glob.glob(exclude_string, flags=GLOB_FLAGS) if process.is_relative_path(exclude_string): + if includer_page_src_path is None: # pragma: no cover + raise PluginError( + 'Relative paths are not allowed when the includer page' + ' source path is not provided. The exclude string' + f" '{exclude_string}' is located inside a generated page.", + ) root_dir = os.path.abspath( os.path.dirname(includer_page_src_path), ) diff --git a/src/mkdocs_include_markdown_plugin/event.py b/src/mkdocs_include_markdown_plugin/event.py index 9b705b3..237502d 100644 --- a/src/mkdocs_include_markdown_plugin/event.py +++ b/src/mkdocs_include_markdown_plugin/event.py @@ -58,9 +58,26 @@ def lineno_from_content_start(content: str, start: int) -> int: return content[:start].count('\n') + 1 +def file_lineno_message( + page_src_path: str | None, + docs_dir: str, + lineno: int, +) -> str: + """Return a message with the file path and line number.""" + if page_src_path is None: # pragma: no cover + return f'generated page content (line {lineno})' + return ( + f'{os.path.relpath(page_src_path, docs_dir)}' + f':{lineno}' + ) + + def get_file_content( # noqa: PLR0913, PLR0915 markdown: str, - page_src_path: str, + # Generated pages return `None` for `file.abs_src_path` because + # they are not read from a file. In this case, page_src_path is + # set to `None`. + page_src_path: str | None, docs_dir: str, tags: IncludeTags, defaults: DefaultValues, @@ -85,8 +102,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Found no path passed including with 'include'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + ' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) arguments_string = match['arguments'] @@ -114,9 +131,10 @@ def found_include_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'exclude' argument in 'include'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + ' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) + ignore_paths.extend( resolve_file_paths_to_exclude( exclude_string, @@ -139,9 +157,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 directive_match_start, ) raise PluginError( - f"No files found including '{raw_filename}'" - f' at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f"No files found including '{raw_filename}' at" + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) if files_watcher is not None and not is_url: @@ -160,8 +177,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 raise PluginError( f"Invalid value for '{invalid_bool_args[0]}' argument of" " 'include' directive at" - f' {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}. Possible values are true or false.', + f' {file_lineno_message(page_src_path, docs_dir, lineno)}.' + f' Possible values are true or false.', ) start_match = ARGUMENT_REGEXES['start'].search(arguments_string) @@ -173,8 +190,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 directive_match_start, ) raise PluginError( - "Invalid empty 'start' argument in 'include' directive at " - f'{os.path.relpath(page_src_path, docs_dir)}:{lineno}', + "Invalid empty 'start' argument in 'include' directive at" + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: start = defaults['start'] @@ -188,8 +205,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 directive_match_start, ) raise PluginError( - "Invalid empty 'end' argument in 'include' directive at " - f'{os.path.relpath(page_src_path, docs_dir)}:{lineno}', + "Invalid empty 'end' argument in 'include' directive at" + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: end = defaults['end'] @@ -204,8 +221,8 @@ def found_include_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'encoding' argument in 'include'" - ' directive at ' - f'{os.path.relpath(page_src_path, docs_dir)}:{lineno}', + ' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: encoding = defaults['encoding'] @@ -278,12 +295,14 @@ def found_include_tag( # noqa: PLR0912, PLR0915 markdown, directive_match_start, ) - relative_path = os.path.relpath(page_src_path, docs_dir) + file_lineno = file_lineno_message( + page_src_path, docs_dir, lineno, + ) logger.warning( ( f"Delimiter {delimiter_name} '{delimiter_value}'" " of 'include' directive at" - f' {relative_path}:{lineno}' + f' {file_lineno}' f' not detected in the file{plural_suffix}' f' {readable_files_to_include}' ), @@ -307,8 +326,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Found no path passed including with 'include-markdown'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) arguments_string = match['arguments'] @@ -336,8 +355,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'exclude' argument in 'include-markdown'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) ignore_paths.extend( resolve_file_paths_to_exclude( @@ -362,8 +381,7 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( f"No files found including '{raw_filename}' at" - f' {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) if files_watcher is not None and not is_url: @@ -386,8 +404,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 raise PluginError( f"Invalid value for '{invalid_bool_args[0]}' argument of" " 'include-markdown' directive at" - f' {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}. Possible values are true or false.', + f' {file_lineno_message(page_src_path, docs_dir, lineno)}.' + f' Possible values are true or false.', ) # start and end arguments @@ -401,8 +419,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'start' argument in 'include-markdown'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: start = defaults['start'] @@ -417,8 +435,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'end' argument in 'include-markdown'" - f' directive at {os.path.relpath(page_src_path, docs_dir)}' - f':{lineno}', + f' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: end = defaults['end'] @@ -433,8 +451,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( "Invalid empty 'encoding' argument in 'include-markdown'" - ' directive at ' - f'{os.path.relpath(page_src_path, docs_dir)}:{lineno}', + ' directive at' + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) else: encoding = defaults['encoding'] @@ -453,7 +471,7 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 raise PluginError( "Invalid empty 'heading-offset' argument in" " 'include-markdown' directive at" - f' {os.path.relpath(page_src_path, docs_dir)}:{lineno}', + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) try: offset = int(offset_raw_value) @@ -464,8 +482,8 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ) raise PluginError( f"Invalid 'heading-offset' argument \"{offset_raw_value}\"" - " in 'include-markdown' directive at " - f'{os.path.relpath(page_src_path, docs_dir)}:{lineno}', + " in 'include-markdown' directive at" + f' {file_lineno_message(page_src_path, docs_dir, lineno)}', ) from None else: offset = defaults['heading-offset'] @@ -523,11 +541,17 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 # relative URLs rewriting if bool_options['rewrite-relative-urls'].value: - new_text_to_include = process.rewrite_relative_urls( - new_text_to_include, - source_path=file_path, - destination_path=page_src_path, - ) + if page_src_path is None: # pragma: no cover + logger.warning( + 'Relative URLs rewriting is not supported in' + ' generated pages.', + ) + else: + new_text_to_include = process.rewrite_relative_urls( + new_text_to_include, + source_path=file_path, + destination_path=page_src_path, + ) # comments if bool_options['comments'].value: @@ -577,12 +601,14 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 markdown, directive_match_start, ) - relative_path = os.path.relpath(page_src_path, docs_dir) + file_lineno = file_lineno_message( + page_src_path, docs_dir, lineno, + ) logger.warning( ( f"Delimiter {delimiter_name} '{delimiter_value}' of" " 'include-markdown' directive at" - f' {relative_path}:{lineno}' + f' {file_lineno}' f' not detected in the file{plural_suffix}' f' {readable_files_to_include}' ),