Skip to content

Commit c19c57f

Browse files
Add support for reStructuredText literal blocks (#196)
Co-authored-by: Adam Johnson <me@adamj.eu> fix #195
1 parent 6af8099 commit c19c57f

4 files changed

Lines changed: 124 additions & 4 deletions

File tree

HISTORY.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ History
44

55
* Require Black 22.1.0+.
66

7+
* Add ``--rst-literal-blocks`` option, to also format text in reStructuredText literal blocks, starting with ``::``.
8+
Sphinx highlights these with the project’s default language, which defaults to Python.
9+
710
1.12.1 (2022-01-30)
811
-------------------
912

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ options:
2929
Following additional parameters can be used:
3030

3131
- `-E` / `--skip-errors`
32+
- `--rst-literal-blocks`
3233

3334
`blacken-docs` will format code in the following block types:
3435

@@ -59,6 +60,8 @@ Following additional parameters can be used:
5960
print("hello world")
6061
```
6162

63+
This style is enabled with the `--use-sphinx-default` option.
64+
6265
(rst `pycon`)
6366
```rst
6467
.. code-block:: pycon
@@ -68,6 +71,19 @@ Following additional parameters can be used:
6871
...
6972
```
7073

74+
(rst literal blocks - activated with ``--rst-literal-blocks``)
75+
76+
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.
77+
However Sphinx interprets them as Python code [by default](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-literal-blocks).
78+
If your project uses Sphinx and such a configuration, add `--rst-literal-blocks` to also format such blocks.
79+
80+
``rst
81+
An example::
82+
83+
def hello():
84+
print("hello world")
85+
```
86+
7187
(latex)
7288
```latex
7389
\begin{minted}{python}

src/blacken_docs/__init__.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
rf'(?P<code>(^((?P=indent) +.*)?\n)+)',
4343
re.MULTILINE,
4444
)
45+
RST_LITERAL_BLOCKS_RE = re.compile(
46+
r'(?P<before>'
47+
r'^(?! *\.\. )(?P<indent> *).*::\n'
48+
r'((?P=indent) +:.*\n)*'
49+
r'\n*'
50+
r')'
51+
r'(?P<code>(^((?P=indent) +.*)?\n)+)',
52+
re.MULTILINE,
53+
)
4554
RST_PYCON_RE = re.compile(
4655
r'(?P<before>'
4756
r'(?P<indent> *)\.\. ((code|code-block):: pycon|doctest::.*)\n'
@@ -85,7 +94,10 @@ class CodeBlockError(NamedTuple):
8594

8695

8796
def format_str(
88-
src: str, black_mode: black.FileMode,
97+
src: str,
98+
black_mode: black.FileMode,
99+
*,
100+
rst_literal_blocks: bool = False,
89101
) -> tuple[str, Sequence[CodeBlockError]]:
90102
errors: list[CodeBlockError] = []
91103

@@ -117,6 +129,19 @@ def _rst_match(match: Match[str]) -> str:
117129
code = textwrap.indent(code, min_indent)
118130
return f'{match["before"]}{code.rstrip()}{trailing_ws}'
119131

132+
def _rst_literal_blocks_match(match: Match[str]) -> str:
133+
if not match['code'].strip():
134+
return match[0]
135+
min_indent = min(INDENT_RE.findall(match['code']))
136+
trailing_ws_match = TRAILING_NL_RE.search(match['code'])
137+
assert trailing_ws_match
138+
trailing_ws = trailing_ws_match.group()
139+
code = textwrap.dedent(match['code'])
140+
with _collect_error(match):
141+
code = black.format_str(code, mode=black_mode)
142+
code = textwrap.indent(code, min_indent)
143+
return f'{match["before"]}{code.rstrip()}{trailing_ws}'
144+
120145
def _pycon_match(match: Match[str]) -> str:
121146
code = ''
122147
fragment: str | None = None
@@ -189,18 +214,30 @@ def _latex_pycon_match(match: Match[str]) -> str:
189214
src = MD_PYCON_RE.sub(_md_pycon_match, src)
190215
src = RST_RE.sub(_rst_match, src)
191216
src = RST_PYCON_RE.sub(_rst_pycon_match, src)
217+
if rst_literal_blocks:
218+
src = RST_LITERAL_BLOCKS_RE.sub(
219+
_rst_literal_blocks_match,
220+
src,
221+
)
192222
src = LATEX_RE.sub(_latex_match, src)
193223
src = LATEX_PYCON_RE.sub(_latex_pycon_match, src)
194224
src = PYTHONTEX_RE.sub(_latex_match, src)
195225
return src, errors
196226

197227

198228
def format_file(
199-
filename: str, black_mode: black.FileMode, skip_errors: bool,
229+
filename: str,
230+
black_mode: black.FileMode,
231+
skip_errors: bool,
232+
rst_literal_blocks: bool,
200233
) -> int:
201234
with open(filename, encoding='UTF-8') as f:
202235
contents = f.read()
203-
new_contents, errors = format_str(contents, black_mode)
236+
new_contents, errors = format_str(
237+
contents,
238+
black_mode,
239+
rst_literal_blocks=rst_literal_blocks,
240+
)
204241
for error in errors:
205242
lineno = contents[:error.offset].count('\n') + 1
206243
print(f'{filename}:{lineno}: code block parse error {error.exc}')
@@ -233,6 +270,9 @@ def main(argv: Sequence[str] | None = None) -> int:
233270
'-S', '--skip-string-normalization', action='store_true',
234271
)
235272
parser.add_argument('-E', '--skip-errors', action='store_true')
273+
parser.add_argument(
274+
'--rst-literal-blocks', action='store_true',
275+
)
236276
parser.add_argument('filenames', nargs='*')
237277
args = parser.parse_args(argv)
238278

@@ -244,5 +284,10 @@ def main(argv: Sequence[str] | None = None) -> int:
244284

245285
retv = 0
246286
for filename in args.filenames:
247-
retv |= format_file(filename, black_mode, skip_errors=args.skip_errors)
287+
retv |= format_file(
288+
filename,
289+
black_mode,
290+
skip_errors=args.skip_errors,
291+
rst_literal_blocks=args.rst_literal_blocks,
292+
)
248293
return retv

tests/blacken_docs_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from textwrap import dedent
4+
35
import black
46
from black.const import DEFAULT_LINE_LENGTH
57

@@ -197,6 +199,60 @@ def test_format_src_rst():
197199
)
198200

199201

202+
def test_format_src_rst_literal_blocks():
203+
before = (
204+
'hello::\n'
205+
'\n'
206+
' f(1,2,3)\n'
207+
'\n'
208+
'world\n'
209+
)
210+
after, _ = blacken_docs.format_str(
211+
before, BLACK_MODE, rst_literal_blocks=True,
212+
)
213+
assert after == (
214+
'hello::\n'
215+
'\n'
216+
' f(1, 2, 3)\n'
217+
'\n'
218+
'world\n'
219+
)
220+
221+
222+
def test_format_src_rst_literal_blocks_nested():
223+
before = dedent(
224+
'''
225+
* hello
226+
227+
.. warning::
228+
229+
don't hello too much
230+
''',
231+
)
232+
after, errors = blacken_docs.format_str(
233+
before, BLACK_MODE, rst_literal_blocks=True,
234+
)
235+
assert after == before
236+
assert errors == []
237+
238+
239+
def test_format_src_rst_literal_blocks_empty():
240+
before = dedent(
241+
'''
242+
Example::
243+
244+
.. warning::
245+
246+
There was no example.
247+
''',
248+
)
249+
after, errors = blacken_docs.format_str(
250+
before, BLACK_MODE, rst_literal_blocks=True,
251+
)
252+
assert after == before
253+
assert errors == []
254+
255+
200256
def test_format_src_rst_sphinx_doctest():
201257
before = (
202258
'.. testsetup:: group1\n'

0 commit comments

Comments
 (0)