Skip to content

Commit

Permalink
Merge pull request #195 from adamtheturtle/separate-rst-and-markdown-…
Browse files Browse the repository at this point in the history
…parsers

Separate rst and markdown parsers - error if unsupported file suffix
  • Loading branch information
adamtheturtle authored Nov 5, 2024
2 parents 8a17779 + a914252 commit cfffa3c
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 29 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ repos:
stages: [pre-push]
entry: uv run --extra=dev doccmd --language=python --command="mypy"
language: python
types_or: [markdown, rst, python, toml]
types_or: [markdown, rst]
additional_dependencies: [uv==0.4.25]

- id: check-manifest
Expand All @@ -154,7 +154,7 @@ repos:
stages: [pre-push]
entry: uv run --extra=dev doccmd --language=python --command="pyright"
language: python
types_or: [markdown, rst, python, toml]
types_or: [markdown, rst]
additional_dependencies: [uv==0.4.25]

- id: vulture
Expand Down Expand Up @@ -200,7 +200,7 @@ repos:
entry: uv run --extra=dev doccmd --language=python --command="pylint"
language: python
stages: [manual]
types_or: [markdown, rst, python, toml]
types_or: [markdown, rst]
additional_dependencies: [uv==0.4.25]

- id: ruff-check-fix
Expand Down
102 changes: 76 additions & 26 deletions src/doccmd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from pygments.lexers import get_all_lexers
from sybil import Sybil
from sybil.evaluators.skip import Skipper
from sybil.parsers.abstract.skip import AbstractSkipParser
from sybil.parsers.myst import CodeBlockParser as MystCodeBlockParser
from sybil.parsers.rest import CodeBlockParser as RestCodeBlockParser
from sybil_extras.evaluators.shell_evaluator import ShellCommandEvaluator
Expand Down Expand Up @@ -122,7 +121,7 @@ def _map_languages_to_suffix() -> dict[str, str]:
@beartype
def _get_skip_directives(skip_markers: Iterable[str]) -> Sequence[str]:
"""
Skip directives for reST and MyST based on the provided skip markers.
Skip directives based on the provided skip markers.
"""
skip_directives: Sequence[str] = []

Expand All @@ -133,23 +132,67 @@ def _get_skip_directives(skip_markers: Iterable[str]) -> Sequence[str]:


@beartype
def _get_skip_parsers(
skip_directives: Sequence[str],
) -> Sequence[AbstractSkipParser]:
class _UnknownMarkupLanguageError(Exception):
"""
Skip parsers for reST and MyST based on the provided skip markers.
Raised when the markup language is not recognized.
"""
skip_parsers: Sequence[AbstractSkipParser] = []

for skip_directive in skip_directives:
rest_skip_parser = RestCustomDirectiveSkipParser(
directive=skip_directive,
)
myst_skip_parser = MystCustomDirectiveSkipParser(
directive=skip_directive
)
skip_parsers = [*skip_parsers, rest_skip_parser, myst_skip_parser]
return skip_parsers
def __init__(self, file_path: Path) -> None:
"""
Args:
file_path: The file path for which the markup language is unknown.
"""
super().__init__(f"Markup language not known for {file_path}.")


@beartype
@unique
class _MarkupLanguage(Enum):
"""
Supported markup languages.
"""

MYST = auto()
RESTRUCTURED_TEXT = auto()

@classmethod
def from_file_path(cls, file_path: Path) -> "_MarkupLanguage":
"""
Determine the markup language from the file path.
"""
if file_path.suffix == ".md":
return cls.MYST
if file_path.suffix == ".rst":
return cls.RESTRUCTURED_TEXT
raise _UnknownMarkupLanguageError(file_path=file_path)

@property
def skip_parser_cls(
self,
) -> type[MystCustomDirectiveSkipParser | RestCustomDirectiveSkipParser]:
"""
Skip parser class.
"""
match self:
case _MarkupLanguage.MYST:
return MystCustomDirectiveSkipParser
# Ignore coverage because this never not reached.
case _MarkupLanguage.RESTRUCTURED_TEXT: # pragma: no cover
return RestCustomDirectiveSkipParser

@property
def code_block_parser_cls(
self,
) -> type[MystCodeBlockParser | RestCodeBlockParser]:
"""
Skip parser class.
"""
match self:
case _MarkupLanguage.MYST:
return MystCodeBlockParser
# Ignore coverage because this never not reached.
case _MarkupLanguage.RESTRUCTURED_TEXT: # pragma: no cover
return RestCodeBlockParser


@beartype
Expand Down Expand Up @@ -186,6 +229,7 @@ def _run_args_against_docs(
"""
Run commands on the given file.
"""
markup_language = _MarkupLanguage.from_file_path(file_path=document_path)
temporary_file_extension = _get_temporary_file_extension(
language=code_block_language,
given_file_extension=temporary_file_extension,
Expand All @@ -204,17 +248,17 @@ def _run_args_against_docs(

skip_markers = {*skip_markers, "all"}
skip_directives = _get_skip_directives(skip_markers=skip_markers)
skip_parsers = _get_skip_parsers(skip_directives=skip_directives)
skip_parsers = [
markup_language.skip_parser_cls(directive=skip_directive)
for skip_directive in skip_directives
]
code_block_parsers = [
markup_language.code_block_parser_cls(
language=code_block_language,
evaluator=evaluator,
)
]

rest_parser = RestCodeBlockParser(
language=code_block_language,
evaluator=evaluator,
)
myst_parser = MystCodeBlockParser(
language=code_block_language,
evaluator=evaluator,
)
code_block_parsers = [rest_parser, myst_parser]
parsers: Sequence[Parser] = [*code_block_parsers, *skip_parsers]
sybil = Sybil(parsers=parsers)
try:
Expand Down Expand Up @@ -415,6 +459,12 @@ def main(
else "Not using PTY for running commands."
)

try:
for document_path in document_paths:
_MarkupLanguage.from_file_path(file_path=document_path)
except _UnknownMarkupLanguageError as exc:
raise click.UsageError(message=str(exc)) from exc

for document_path in document_paths:
for language in languages:
_run_args_against_docs(
Expand Down
70 changes: 70 additions & 0 deletions tests/test_doccmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,76 @@ def test_detect_line_endings(
assert bool(b"\n" in result.stdout_bytes) == expect_lf


def test_one_supported_markup_in_another_extension(tmp_path: Path) -> None:
"""
Code blocks in a supported markup language in a file with an extension
which matches another extension are not run.
"""
runner = CliRunner(mix_stderr=False)
rst_file = tmp_path / "example.rst"
content = """\
```python
print("In simple markdown code block")
```
```{code-block} python
print("In MyST code-block")
```
"""
rst_file.write_text(data=content, encoding="utf-8")
arguments = ["--language", "python", "--command", "cat", str(rst_file)]
result = runner.invoke(
cli=main,
args=arguments,
catch_exceptions=False,
)
assert result.exit_code == 0, (result.stdout, result.stderr)
# Empty because the Markdown-style code block is not run in.
expected_output = ""
assert result.stdout == expected_output
assert result.stderr == ""


@pytest.mark.parametrize(argnames="extension", argvalues=[".unknown", ""])
def test_unknown_file_suffix(extension: str, tmp_path: Path) -> None:
"""
An error is shown when the file suffix is not known.
"""
runner = CliRunner(mix_stderr=False)
document_file = tmp_path / ("example" + extension)
content = """\
.. code-block:: python
x = 2 + 2
assert x == 4
"""
document_file.write_text(data=content, encoding="utf-8")
arguments = [
"--language",
"python",
"--command",
"cat",
str(document_file),
]
result = runner.invoke(
cli=main,
args=arguments,
catch_exceptions=False,
)
assert result.exit_code != 0, (result.stdout, result.stderr)
expected_stderr = textwrap.dedent(
text=f"""\
Usage: doccmd [OPTIONS] [DOCUMENT_PATHS]...
Try 'doccmd --help' for help.
Error: Markup language not known for {document_file}.
""",
)

assert result.stdout == ""
assert result.stderr == expected_stderr


@pytest.mark.parametrize(
argnames=["options", "expected_output"],
argvalues=[
Expand Down

0 comments on commit cfffa3c

Please sign in to comment.