From c19c57fdccdc80f8405426de96d8e9d97899d867 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 16 Jan 2023 10:07:01 +0100 Subject: [PATCH] Add support for reStructuredText literal blocks (#196) Co-authored-by: Adam Johnson fix https://github.com/adamchainz/blacken-docs/issues/195 --- HISTORY.rst | 3 ++ README.md | 16 +++++++++++ src/blacken_docs/__init__.py | 53 +++++++++++++++++++++++++++++++--- tests/blacken_docs_test.py | 56 ++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 06901c0..8e1073e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,9 @@ History * Require Black 22.1.0+. +* Add ``--rst-literal-blocks`` option, to also format text in reStructuredText literal blocks, starting with ``::``. + Sphinx highlights these with the project’s default language, which defaults to Python. + 1.12.1 (2022-01-30) ------------------- diff --git a/README.md b/README.md index 2499cf3..11acba5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ options: Following additional parameters can be used: - `-E` / `--skip-errors` + - `--rst-literal-blocks` `blacken-docs` will format code in the following block types: @@ -59,6 +60,8 @@ Following additional parameters can be used: print("hello world") ``` +This style is enabled with the `--use-sphinx-default` option. + (rst `pycon`) ```rst .. code-block:: pycon @@ -68,6 +71,19 @@ Following additional parameters can be used: ... ``` +(rst literal blocks - activated with ``--rst-literal-blocks``) + +reStructuredText [literal blocks](https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks) are marked with `::` and can be any monospaced text by default. +However Sphinx interprets them as Python code [by default](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-literal-blocks). +If your project uses Sphinx and such a configuration, add `--rst-literal-blocks` to also format such blocks. + +``rst + An example:: + + def hello(): + print("hello world") +``` + (latex) ```latex \begin{minted}{python} diff --git a/src/blacken_docs/__init__.py b/src/blacken_docs/__init__.py index b281ddd..4487a3f 100644 --- a/src/blacken_docs/__init__.py +++ b/src/blacken_docs/__init__.py @@ -42,6 +42,15 @@ rf'(?P(^((?P=indent) +.*)?\n)+)', re.MULTILINE, ) +RST_LITERAL_BLOCKS_RE = re.compile( + r'(?P' + r'^(?! *\.\. )(?P *).*::\n' + r'((?P=indent) +:.*\n)*' + r'\n*' + r')' + r'(?P(^((?P=indent) +.*)?\n)+)', + re.MULTILINE, +) RST_PYCON_RE = re.compile( r'(?P' r'(?P *)\.\. ((code|code-block):: pycon|doctest::.*)\n' @@ -85,7 +94,10 @@ class CodeBlockError(NamedTuple): def format_str( - src: str, black_mode: black.FileMode, + src: str, + black_mode: black.FileMode, + *, + rst_literal_blocks: bool = False, ) -> tuple[str, Sequence[CodeBlockError]]: errors: list[CodeBlockError] = [] @@ -117,6 +129,19 @@ def _rst_match(match: Match[str]) -> str: code = textwrap.indent(code, min_indent) return f'{match["before"]}{code.rstrip()}{trailing_ws}' + def _rst_literal_blocks_match(match: Match[str]) -> str: + if not match['code'].strip(): + return match[0] + min_indent = min(INDENT_RE.findall(match['code'])) + trailing_ws_match = TRAILING_NL_RE.search(match['code']) + assert trailing_ws_match + trailing_ws = trailing_ws_match.group() + code = textwrap.dedent(match['code']) + with _collect_error(match): + code = black.format_str(code, mode=black_mode) + code = textwrap.indent(code, min_indent) + return f'{match["before"]}{code.rstrip()}{trailing_ws}' + def _pycon_match(match: Match[str]) -> str: code = '' fragment: str | None = None @@ -189,6 +214,11 @@ def _latex_pycon_match(match: Match[str]) -> str: src = MD_PYCON_RE.sub(_md_pycon_match, src) src = RST_RE.sub(_rst_match, src) src = RST_PYCON_RE.sub(_rst_pycon_match, src) + if rst_literal_blocks: + src = RST_LITERAL_BLOCKS_RE.sub( + _rst_literal_blocks_match, + src, + ) src = LATEX_RE.sub(_latex_match, src) src = LATEX_PYCON_RE.sub(_latex_pycon_match, src) src = PYTHONTEX_RE.sub(_latex_match, src) @@ -196,11 +226,18 @@ def _latex_pycon_match(match: Match[str]) -> str: def format_file( - filename: str, black_mode: black.FileMode, skip_errors: bool, + filename: str, + black_mode: black.FileMode, + skip_errors: bool, + rst_literal_blocks: bool, ) -> int: with open(filename, encoding='UTF-8') as f: contents = f.read() - new_contents, errors = format_str(contents, black_mode) + new_contents, errors = format_str( + contents, + black_mode, + rst_literal_blocks=rst_literal_blocks, + ) for error in errors: lineno = contents[:error.offset].count('\n') + 1 print(f'{filename}:{lineno}: code block parse error {error.exc}') @@ -233,6 +270,9 @@ def main(argv: Sequence[str] | None = None) -> int: '-S', '--skip-string-normalization', action='store_true', ) parser.add_argument('-E', '--skip-errors', action='store_true') + parser.add_argument( + '--rst-literal-blocks', action='store_true', + ) parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) @@ -244,5 +284,10 @@ def main(argv: Sequence[str] | None = None) -> int: retv = 0 for filename in args.filenames: - retv |= format_file(filename, black_mode, skip_errors=args.skip_errors) + retv |= format_file( + filename, + black_mode, + skip_errors=args.skip_errors, + rst_literal_blocks=args.rst_literal_blocks, + ) return retv diff --git a/tests/blacken_docs_test.py b/tests/blacken_docs_test.py index cead166..cb03702 100644 --- a/tests/blacken_docs_test.py +++ b/tests/blacken_docs_test.py @@ -1,5 +1,7 @@ from __future__ import annotations +from textwrap import dedent + import black from black.const import DEFAULT_LINE_LENGTH @@ -197,6 +199,60 @@ def test_format_src_rst(): ) +def test_format_src_rst_literal_blocks(): + before = ( + 'hello::\n' + '\n' + ' f(1,2,3)\n' + '\n' + 'world\n' + ) + after, _ = blacken_docs.format_str( + before, BLACK_MODE, rst_literal_blocks=True, + ) + assert after == ( + 'hello::\n' + '\n' + ' f(1, 2, 3)\n' + '\n' + 'world\n' + ) + + +def test_format_src_rst_literal_blocks_nested(): + before = dedent( + ''' + * hello + + .. warning:: + + don't hello too much + ''', + ) + after, errors = blacken_docs.format_str( + before, BLACK_MODE, rst_literal_blocks=True, + ) + assert after == before + assert errors == [] + + +def test_format_src_rst_literal_blocks_empty(): + before = dedent( + ''' + Example:: + + .. warning:: + + There was no example. + ''', + ) + after, errors = blacken_docs.format_str( + before, BLACK_MODE, rst_literal_blocks=True, + ) + assert after == before + assert errors == [] + + def test_format_src_rst_sphinx_doctest(): before = ( '.. testsetup:: group1\n'