Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CHANGES_AST switch to plugin API so that AST breaking plugins can disable the equality check #49

Merged
merged 2 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion mdformat/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions mdformat/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
74 changes: 59 additions & 15 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"