Repository: mondeja/mkdocs-include-markdown-plugin
Pull request #262 (2025-03-07 22:35:02)

Don't compile argument regexes if not required


Patch:
diff --git a/examples/basic/docs/index.md b/examples/basic/docs/index.md
index a55ae8b..e620fcd 100644
--- a/examples/basic/docs/index.md
+++ b/examples/basic/docs/index.md
@@ -1,5 +1,1 @@
-```yaml
-{ % include '../mkdocs.yml'% }
-```
-
 {% include-markdown './included.md' start='<--start-->' %}
diff --git a/src/mkdocs_include_markdown_plugin/directive.py b/src/mkdocs_include_markdown_plugin/directive.py
index 517cb74..61bf42b 100644
--- a/src/mkdocs_include_markdown_plugin/directive.py
+++ b/src/mkdocs_include_markdown_plugin/directive.py
@@ -20,7 +20,7 @@ from mkdocs_include_markdown_plugin.logger import logger
 @dataclass
 class DirectiveBoolArgument:  # noqa: D101
     value: bool
-    regex: re.Pattern[str]
+    regex: Callable[[], re.Pattern[str]]
 
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -168,8 +168,9 @@ def warn_invalid_directive_arguments(
     directive: Literal['include', 'include-markdown'],
     page_src_path: str | None,
     docs_dir: str,
-) -> None:
+) -> list[str]:
     """Warns about the invalid arguments passed to a directive."""
+    used_arguments = []
     valid_args = (
         INCLUDE_DIRECTIVE_ARGS
         if directive == 'include'
@@ -184,6 +185,9 @@ def warn_invalid_directive_arguments(
                 f"Invalid argument '{maybe_arg}' in"
                 f" '{directive}' directive at {location}. Ignoring...",
             )
+        else:
+            used_arguments.append(maybe_arg)
+    return used_arguments
 
 
 def parse_filename_argument(
@@ -202,8 +206,10 @@ def parse_filename_argument(
     return filename, raw_filename
 
 
-def parse_string_argument(match: re.Match[str]) -> str | None:
+def parse_string_argument(match: re.Match[str] | None) -> str | None:
     """Return the string argument matched by ``match``."""
+    if match is None:
+        return None
     value = match[1]
     if value is None:
         value = match[3]
@@ -246,6 +252,7 @@ def parse_bool_options(
         option_names: list[str],
         defaults: DefaultValues,
         arguments_string: str,
+        used_arguments: list[str],
 ) -> tuple[DirectiveBoolArgumentsDict, list[str]]:
     """Parse boolean options from arguments string."""
     invalid_args: list[str] = []
@@ -254,16 +261,17 @@ def parse_bool_options(
     for option_name in option_names:
         bool_options[option_name] = DirectiveBoolArgument(
             value=defaults[option_name],  # type: ignore
-            regex=ARGUMENT_REGEXES[option_name](),
+            regex=ARGUMENT_REGEXES[option_name],
         )
 
     for arg_name, arg in bool_options.items():
-        bool_arg_match = arg.regex.search(arguments_string)
-        if bool_arg_match is None:
+        if arg_name not in used_arguments:
             continue
+        bool_arg_match = arg.regex().search(arguments_string)
         try:
             bool_options[arg_name].value = TRUE_FALSE_STR_BOOL[
-                bool_arg_match[1] or TRUE_FALSE_BOOL_STR[arg.value]
+                (bool_arg_match and bool_arg_match[1])
+                or TRUE_FALSE_BOOL_STR[arg.value]
             ]
         except KeyError:
             invalid_args.append(arg_name)
diff --git a/src/mkdocs_include_markdown_plugin/event.py b/src/mkdocs_include_markdown_plugin/event.py
index 28e2f25..300686b 100644
--- a/src/mkdocs_include_markdown_plugin/event.py
+++ b/src/mkdocs_include_markdown_plugin/event.py
@@ -125,7 +125,7 @@ def get_file_content(  # noqa: PLR0913, PLR0915
 
         arguments_string = match['arguments']
 
-        warn_invalid_directive_arguments(
+        used_arguments = warn_invalid_directive_arguments(
             arguments_string,
             directive_lineno,
             'include',
@@ -133,9 +133,11 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             docs_dir,
         )
 
-        exclude_match = ARGUMENT_REGEXES['exclude']().search(arguments_string)
         ignore_paths = [*settings_ignore_paths]
-        if exclude_match is not None:
+        if 'exclude' in used_arguments:
+            exclude_match = ARGUMENT_REGEXES['exclude']().search(
+                arguments_string,
+            )
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
                 location = process.file_lineno_message(
@@ -169,38 +171,22 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if files_watcher is not None and not is_url:
             files_watcher.included_files.extend(file_paths_to_include)
 
-        bool_options, invalid_bool_args = parse_bool_options(
-            ['preserve-includer-indent', 'dedent',
-                'trailing-newlines', 'recursive'],
-            defaults,
-            arguments_string,
-        )
-        if invalid_bool_args:
-            location = process.file_lineno_message(
-                page_src_path, docs_dir, directive_lineno(),
-            )
-            raise PluginError(
-                f"Invalid value for '{invalid_bool_args[0]}' argument of"
-                f" 'include' directive at {location}."
-                f' Possible values are true or false.',
-            )
-
-        start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
-        if start_match:
+        start = defaults['start']
+        if 'start' in used_arguments:
+            start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
             start = parse_string_argument(start_match)
             if start is None:
                 location = process.file_lineno_message(
                     page_src_path, docs_dir, directive_lineno(),
                 )
                 raise PluginError(
-                    "Invalid empty 'start' argument in 'include' directive at"
-                    f' {location}',
+                    "Invalid empty 'start' argument in 'include' directive"
+                    f' at {location}',
                 )
-        else:
-            start = defaults['start']
 
-        end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
-        if end_match:
+        end = defaults['end']
+        if 'end' in used_arguments:
+            end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
             end = parse_string_argument(end_match)
             if end is None:
                 location = process.file_lineno_message(
@@ -210,14 +196,14 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                     "Invalid empty 'end' argument in 'include' directive at"
                     f' {location}',
                 )
-        else:
-            end = defaults['end']
 
-        encoding_match = ARGUMENT_REGEXES['encoding']().search(
-            arguments_string)
-        if encoding_match:
-            encoding = parse_string_argument(encoding_match)
-            if encoding is None:
+        encoding = defaults['encoding']
+        if 'encoding' in used_arguments:
+            encoding_match = ARGUMENT_REGEXES['encoding']().search(
+                arguments_string,
+            )
+            encoding_ = parse_string_argument(encoding_match)
+            if encoding_ is None:
                 location = process.file_lineno_message(
                     page_src_path, docs_dir, directive_lineno(),
                 )
@@ -225,8 +211,28 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                     "Invalid empty 'encoding' argument in 'include'"
                     f' directive at {location}',
                 )
-        else:
-            encoding = defaults['encoding']
+            encoding = encoding_
+
+        bool_options, invalid_bool_args = parse_bool_options(
+            [
+                'preserve-includer-indent',
+                'dedent',
+                'trailing-newlines',
+                'recursive',
+            ],
+            defaults,
+            arguments_string,
+            used_arguments,
+        )
+        if invalid_bool_args:
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno(),
+            )
+            raise PluginError(
+                f"Invalid value for '{invalid_bool_args[0]}' argument of"
+                f" 'include' directive at {location}."
+                f' Possible values are true or false.',
+            )
 
         text_to_include = ''
         expected_but_any_found = [start is not None, end is not None]
@@ -322,7 +328,7 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         )
 
         includer_indent = match['_includer_indent']
-        empty_includer_indent = ' ' * len(includer_indent)
+        filled_includer_indent = ' ' * len(includer_indent)
 
         filename, raw_filename = parse_filename_argument(match)
         if filename is None:
@@ -336,7 +342,7 @@ def get_file_content(  # noqa: PLR0913, PLR0915
 
         arguments_string = match['arguments']
 
-        warn_invalid_directive_arguments(
+        used_arguments = warn_invalid_directive_arguments(
             arguments_string,
             directive_lineno,
             'include-markdown',
@@ -344,9 +350,11 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             docs_dir,
         )
 
-        exclude_match = ARGUMENT_REGEXES['exclude']().search(arguments_string)
         ignore_paths = [*settings_ignore_paths]
-        if exclude_match is not None:
+        if 'exclude' in used_arguments:
+            exclude_match = ARGUMENT_REGEXES['exclude']().search(
+                arguments_string,
+            )
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
                 location = process.file_lineno_message(
@@ -379,42 +387,23 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if files_watcher is not None and not is_url:
             files_watcher.included_files.extend(file_paths_to_include)
 
-        bool_options, invalid_bool_args = parse_bool_options(
-            [
-                'rewrite-relative-urls', 'comments',
-                'preserve-includer-indent', 'dedent',
-                'trailing-newlines', 'recursive',
-            ],
-            defaults,
-            arguments_string,
-        )
-        if invalid_bool_args:
-            location = process.file_lineno_message(
-                page_src_path, docs_dir, directive_lineno(),
-            )
-            raise PluginError(
-                f"Invalid value for '{invalid_bool_args[0]}' argument of"
-                " 'include-markdown' directive at"
-                f' {location}. Possible values are true or false.',
-            )
-
         # start and end arguments
-        start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
-        if start_match:
+        start = defaults['start']
+        if 'start' in used_arguments:
+            start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
             start = parse_string_argument(start_match)
             if start is None:
                 location = process.file_lineno_message(
                     page_src_path, docs_dir, directive_lineno(),
                 )
                 raise PluginError(
-                    "Invalid empty 'start' argument in 'include-markdown'"
-                    f' directive at {location}',
+                    "Invalid empty 'start' argument in"
+                    f" 'include-markdown' directive at {location}",
                 )
-        else:
-            start = defaults['start']
 
-        end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
-        if end_match:
+        end = defaults['end']
+        if 'end' in used_arguments:
+            end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
             end = parse_string_argument(end_match)
             if end is None:
                 location = process.file_lineno_message(
@@ -424,50 +413,73 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                     "Invalid empty 'end' argument in 'include-markdown'"
                     f' directive at {location}',
                 )
-        else:
-            end = defaults['end']
 
-        encoding_match = ARGUMENT_REGEXES['encoding']().search(
-            arguments_string)
-        if encoding_match:
-            encoding = parse_string_argument(encoding_match)
-            if encoding is None:
+        encoding = defaults['encoding']
+        if 'encoding' in used_arguments:
+            encoding_match = ARGUMENT_REGEXES['encoding']().search(
+                arguments_string,
+            )
+            encoding_ = parse_string_argument(encoding_match)
+            if encoding_ is None:
                 location = process.file_lineno_message(
                     page_src_path, docs_dir, directive_lineno(),
                 )
                 raise PluginError(
-                    "Invalid empty 'encoding' argument in 'include-markdown'"
-                    f' directive at {location}',
+                    "Invalid empty 'encoding' argument in"
+                    f" 'include-markdown' directive at {location}",
                 )
-        else:
-            encoding = defaults['encoding']
+            encoding = encoding_
 
         # heading offset
-        offset_match = ARGUMENT_REGEXES['heading-offset']().search(
+        offset = defaults['heading-offset']
+        if 'heading-offset' in used_arguments:
+            offset_match = ARGUMENT_REGEXES['heading-offset']().search(
+                arguments_string,
+            )
+            if offset_match:
+                offset_raw_value = offset_match[1]
+                if offset_raw_value == '':
+                    location = process.file_lineno_message(
+                        page_src_path, docs_dir, directive_lineno(),
+                    )
+                    raise PluginError(
+                        "Invalid empty 'heading-offset' argument in"
+                        f" 'include-markdown' directive at {location}",
+                    )
+                try:
+                    offset = int(offset_raw_value)
+                except ValueError:
+                    location = process.file_lineno_message(
+                        page_src_path, docs_dir, directive_lineno(),
+                    )
+                    raise PluginError(
+                        f"Invalid 'heading-offset' argument"
+                        f" '{offset_raw_value}' in 'include-markdown'"
+                        f" directive at {location}",
+                    ) from None
+
+        bool_options, invalid_bool_args = parse_bool_options(
+            [
+                'rewrite-relative-urls',
+                'comments',
+                'preserve-includer-indent',
+                'dedent',
+                'trailing-newlines',
+                'recursive',
+            ],
+            defaults,
             arguments_string,
+            used_arguments,
         )
-        if offset_match:
-            offset_raw_value = offset_match[1]
-            if offset_raw_value == '':
-                location = process.file_lineno_message(
-                    page_src_path, docs_dir, directive_lineno(),
-                )
-                raise PluginError(
-                    "Invalid empty 'heading-offset' argument in"
-                    f" 'include-markdown' directive at {location}",
-                )
-            try:
-                offset = int(offset_raw_value)
-            except ValueError:
-                location = process.file_lineno_message(
-                    page_src_path, docs_dir, directive_lineno(),
-                )
-                raise PluginError(
-                    f"Invalid 'heading-offset' argument \"{offset_raw_value}\""
-                    f" in 'include-markdown' directive at {location}",
-                ) from None
-        else:
-            offset = defaults['heading-offset']
+        if invalid_bool_args:
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno(),
+            )
+            raise PluginError(
+                f"Invalid value for '{invalid_bool_args[0]}' argument of"
+                " 'include-markdown' directive at"
+                f' {location}. Possible values are true or false.',
+            )
 
         separator = '\n' if bool_options['trailing-newlines'].value else ''
         if not start and not end:
@@ -555,14 +567,15 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                 new_text_to_include = textwrap.dedent(new_text_to_include)
 
             # includer indentation preservation
-            if bool_options['preserve-includer-indent'].value:
-                new_text_to_include = ''.join(
-                    (empty_includer_indent if i > 0 else '') + line
-                    for i, line in enumerate(
-                        new_text_to_include.splitlines(keepends=True)
-                        or [''],
+            if bool_options['preserve-includer-indent'].value and (
+                new_text_to_include
+            ):
+                lines = new_text_to_include.splitlines(keepends=True)
+                new_text_to_include = lines[0]
+                for i in range(1, len(lines)):
+                    new_text_to_include += (
+                        filled_includer_indent + lines[i]
                     )
-                )
 
             if offset:
                 new_text_to_include = process.increase_headings_offset(


Test patch:
diff --git a/tests/test_unit/test_arguments.py b/tests/test_unit/test_arguments.py
index c4c771a..821f1d6 100644
--- a/tests/test_unit/test_arguments.py
+++ b/tests/test_unit/test_arguments.py
@@ -226,7 +226,7 @@ def test_empty_encoding_argument(directive, page, tmp_path, plugin, caplog):
     (
         pytest.param(
             'invalidoption', (
-                "Invalid 'heading-offset' argument \"invalidoption\" in"
+                "Invalid 'heading-offset' argument 'invalidoption' in"
                 " 'include-markdown' directive at includer.md:1"
             ),
             id='invalidoption',