From 58731b4072eb0128e834e54b26a283cbf7bfd79c Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen Date: Sat, 3 Oct 2020 04:23:04 +0200 Subject: [PATCH] Add `CHANGES_AST` switch to plugin API so that AST breaking plugins can disable the equality check (#49) * Add `CHANGES_AST` switch to plugin API so that AST breaking plugins can disable the equality check * Remove bad test docstring --- mdformat/_cli.py | 5 ++- mdformat/plugins.py | 12 +++++-- tests/test_plugins.py | 74 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/mdformat/_cli.py b/mdformat/_cli.py index 2ffb64eb..39d52b62 100644 --- a/mdformat/_cli.py +++ b/mdformat/_cli.py @@ -22,9 +22,12 @@ def run(cli_args: Sequence[str]) -> int: # noqa: C901 action="store_true", help="Apply consecutive numbering to ordered lists", ) + changes_ast = False for plugin in mdformat.plugins.PARSER_EXTENSIONS.values(): if hasattr(plugin, "add_cli_options"): plugin.add_cli_options(parser) + if getattr(plugin, "CHANGES_AST", False): + changes_ast = True args = parser.parse_args(cli_args) @@ -64,7 +67,7 @@ def run(cli_args: Sequence[str]) -> int: # noqa: C901 format_errors_found = True sys.stderr.write(f'Error: File "{path_str}" is not formatted.\n') else: - if not is_md_equal( + if not changes_ast and not is_md_equal( original_str, formatted_str, options, diff --git a/mdformat/plugins.py b/mdformat/plugins.py index 69193b7f..fb2614e0 100644 --- a/mdformat/plugins.py +++ b/mdformat/plugins.py @@ -28,15 +28,21 @@ def _load_codeformatters() -> Dict[str, Callable[[str, str], str]]: class ParserExtensionInterface(Protocol): """A interface for parser extension plugins.""" - def add_cli_options(self, parser: argparse.ArgumentParser) -> None: + # Does the plugin's formatting change Markdown AST or not? + # (optional, default: False) + CHANGES_AST: bool = False + + @staticmethod + def add_cli_options(parser: argparse.ArgumentParser) -> None: """Add options to the mdformat CLI, to be stored in mdit.options["mdformat"] (optional)""" - def update_mdit(self, mdit: MarkdownIt) -> None: + @staticmethod + def update_mdit(mdit: MarkdownIt) -> None: """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" + @staticmethod def render_token( - self, renderer: MDRenderer, tokens: Sequence[Token], index: int, diff --git a/tests/test_plugins.py b/tests/test_plugins.py index f1e2efbc..6941e75b 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,6 +1,6 @@ import argparse from textwrap import dedent -from typing import List, Optional, Tuple +from typing import Any, Mapping, Optional, Sequence, Tuple from unittest.mock import call, patch from markdown_it import MarkdownIt @@ -16,19 +16,20 @@ class ExampleFrontMatterPlugin: - """A class for extending the base parser.""" + """A plugin that adds front_matter extension to the parser.""" @staticmethod def update_mdit(mdit: MarkdownIt): - """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" mdit.use(front_matter.front_matter_plugin) @staticmethod def render_token( - renderer: MDRenderer, tokens: List[Token], index: int, options: dict, env: dict + renderer: MDRenderer, + tokens: Sequence[Token], + index: int, + options: Mapping[str, Any], + env: dict, ) -> Optional[Tuple[str, int]]: - """Convert a token to a string, or return None if no render method - available.""" token = tokens[index] if token.type == "front_matter": text = yaml.dump(yaml.safe_load(token.content)) @@ -62,19 +63,20 @@ def test_front_matter(monkeypatch): class ExampleTablePlugin: - """A class for extending the base parser.""" + """A plugin that adds table extension to the parser.""" @staticmethod def update_mdit(mdit: MarkdownIt): - """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" mdit.enable("table") @staticmethod def render_token( - renderer: MDRenderer, tokens: List[Token], index: int, options: dict, env: dict + renderer: MDRenderer, + tokens: Sequence[Token], + index: int, + options: Mapping[str, Any], + env: dict, ) -> Optional[Tuple[str, int]]: - """Convert a token to a string, or return None if no render method - available.""" token = tokens[index] if token.type == "table_open": # search for the table close, and return a dummy output @@ -111,17 +113,14 @@ def test_table(monkeypatch): class ExamplePluginWithCli: - """A class for extending the base parser.""" + """A plugin that adds CLI options.""" @staticmethod def update_mdit(mdit: MarkdownIt): - """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" mdit.enable("table") @staticmethod def add_cli_options(parser: argparse.ArgumentParser) -> None: - """Add options to the mdformat CLI, to be stored in - mdit.options["mdformat"]""" parser.add_argument("--o1", type=str) parser.add_argument("--o2", type=str, default="a") parser.add_argument("--o3", dest="arg_name", type=int) @@ -161,3 +160,48 @@ def test_cli_options(monkeypatch, tmp_path): }, } assert calls[0] == call([], expected, {}), calls[0] + + +class ExampleASTChangingPlugin: + """A plugin that makes AST breaking formatting changes.""" + + CHANGES_AST = True + + TEXT_REPLACEMENT = "Content replaced completely. AST is now broken!" + + @staticmethod + def update_mdit(mdit: MarkdownIt): + pass + + @staticmethod + def render_token( + renderer: MDRenderer, + tokens: Sequence[Token], + index: int, + options: Mapping[str, Any], + env: dict, + ) -> Optional[Tuple[str, int]]: + token = tokens[index] + if token.type == "text": + return ExampleASTChangingPlugin.TEXT_REPLACEMENT, index + return None + + +def test_ast_changing_plugin(monkeypatch, tmp_path): + plugin = ExampleASTChangingPlugin() + monkeypatch.setitem(PARSER_EXTENSIONS, "ast_changer", plugin) + file_path = tmp_path / "test_markdown.md" + + # Test that the AST changing formatting is applied successfully + # under normal operation. + file_path.write_text("Some markdown here\n") + assert run((str(file_path),)) == 0 + assert file_path.read_text() == plugin.TEXT_REPLACEMENT + "\n" + + # Set the plugin's `CHANGES_AST` flag to False and test that the + # equality check triggers, notices the AST breaking changes and a + # non-zero error code is returned. + plugin.CHANGES_AST = False + file_path.write_text("Some markdown here\n") + assert run((str(file_path),)) == 1 + assert file_path.read_text() == "Some markdown here\n"