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 support for reStructuredText literal blocks #196

Merged
merged 9 commits into from Jan 16, 2023
Merged
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