Skip to content

Commit

Permalink
Add support for reStructuredText literal blocks (#196)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Johnson <me@adamj.eu>
fix #195
  • Loading branch information
carltongibson committed Jan 16, 2023
1 parent 6af8099 commit c19c57f
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 4 deletions.
3 changes: 3 additions & 0 deletions HISTORY.rst
Expand Up @@ -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)
-------------------

Expand Down
16 changes: 16 additions & 0 deletions README.md
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
53 changes: 49 additions & 4 deletions src/blacken_docs/__init__.py
Expand Up @@ -42,6 +42,15 @@
rf'(?P<code>(^((?P=indent) +.*)?\n)+)',
re.MULTILINE,
)
RST_LITERAL_BLOCKS_RE = re.compile(
r'(?P<before>'
r'^(?! *\.\. )(?P<indent> *).*::\n'
r'((?P=indent) +:.*\n)*'
r'\n*'
r')'
r'(?P<code>(^((?P=indent) +.*)?\n)+)',
re.MULTILINE,
)
RST_PYCON_RE = re.compile(
r'(?P<before>'
r'(?P<indent> *)\.\. ((code|code-block):: pycon|doctest::.*)\n'
Expand Down Expand Up @@ -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] = []

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -189,18 +214,30 @@ 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)
return src, errors


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}')
Expand Down Expand Up @@ -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)

Expand All @@ -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
56 changes: 56 additions & 0 deletions 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

Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit c19c57f

Please sign in to comment.