Skip to content

Commit 1cef4e9

Browse files
authored
feat: support epytext style (#211)
* feat: add wrapping support for Epytex * docs: update documentation for Epytext style * test: add tests for epytext style
1 parent f45a8e3 commit 1cef4e9

8 files changed

Lines changed: 313 additions & 128 deletions

File tree

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ Features
6363
whitespace. Such trailing whitespace is visually indistinguishable
6464
and some editors (or more recently, reindent.py) will trim them.
6565

66+
``docformatter`` formats docstrings compatible with ``black`` when passed the
67+
``--black`` option.
68+
69+
``docformatter`` formats field lists that use Epytext or Sphinx styles.
70+
6671
See the the full documentation at `read-the-docs`_, especially the
6772
`requirements`_ section for a more detailed discussion of PEP 257 and other
6873
requirements.
@@ -82,6 +87,8 @@ Python < 3.11::
8287

8388
$ pip install --upgrade docformatter[tomli]
8489

90+
With Python >=3.11, ``tomllib`` from the standard library is used.
91+
8592
Or, if you want to use a release candidate (or any other tag)::
8693

8794
$ pip install git+https://github.com/PyCQA/docformatter.git@<RC_TAG>

docs/source/configuration.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,9 @@ and
7979
``""""This" summary does get a space."""`` becomes ``""" "This" summary does get a space."""``
8080

8181
The ``--style`` argument takes a string which is the name of the parameter
82-
list style you are using. Currently, only ``sphinx`` is recognized, but
83-
``epydoc``, ``numpy``, and ``google`` are future styles. For the selected
84-
style, each line in the parameter lists will be wrapped at the
85-
``--wrap-descriptions`` length as well as any portion of the elaborate
86-
description preceding the parameter list. Parameter lists that don't follow the
87-
passed style will cause the entire elaborate description to be ignored and
88-
remain unwrapped.
82+
list style you are using. Currently, only ``sphinx`` and ``epytext`` are recognized,
83+
but ``numpy`` and ``google`` are future styles. For the selected style, each line in
84+
the parameter lists will be wrapped at the ``--wrap-descriptions`` length as well as
85+
any portion of the elaborate description preceding the parameter list. Parameter lists
86+
that don't follow the passed style will cause the entire elaborate description to be
87+
ignored and remain unwrapped.

docs/source/requirements.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ Docstring Style
5555
---------------
5656

5757
There are at least four "flavors" of docstrings in common use today;
58-
Epydoc, Sphinx, NumPy, and Google. Each of these docstring flavors follow the
58+
Epytext, Sphinx, NumPy, and Google. Each of these docstring flavors follow the
5959
PEP 257 *convention* requirements. What differs between the three docstring
6060
flavors is the reST syntax used in the parameter description of the multi-line
6161
docstring.
6262

6363
For example, here is how each syntax documents function arguments.
6464

65-
Epydoc syntax:
65+
Epytext syntax:
6666

6767
.. code-block::
6868
@@ -221,6 +221,9 @@ the requirement falls in, the type of requirement, and whether
221221
' docformatter_10.5.2', ' Should wrap descriptions at 88 characters by default in black mode.', ' Style', ' Should', ' Yes'
222222
' docformatter_10.5.3', ' Should insert a space before the first word in the summary if that word is quoted when in black mode.', ' Style', ' Should', ' Yes'
223223
' docformatter_10.5.4', ' Default black mode options should be over-rideable by passing arguments or using configuration files.', ' Style', ' Should', ' Yes'
224+
' docformatter_10.6', ' Should format docstrings using Epytext style.', ' Style', ' Should', ' Yes'
225+
' docformatter_10.6.1', ' Shall ignore docstrings in other styles when using Epytext style.', ' Shall', ' Yes'
226+
' docformatter_10.6.2', ' Shall wrap Epytext-style parameter descriptions that exceed wrap length when using Epytext style.', ' Shall', ' Yes'
224227
' docformatter_11', '**Program Control**'
225228
' docformatter_11.1', ' Should check formatting and report incorrectly documented docstrings.', ' Stakeholder', ' Should', ' Yes [*PR #32*]'
226229
' docformatter_11.2', ' Should fix formatting and save changes to file.', ' Stakeholder', ' Should', ' Yes'
@@ -344,4 +347,4 @@ version bump (i.e., 1.5.0 -> 1.6.0). One or more release candidates will be
344347
provided for each minor or major version bump. These will be indicated by
345348
appending `-rcX` to the version number, where the X is the release candidate
346349
number beginning with 1. Release candidates will not be uploaded to PyPi,
347-
but will be made available via GitHub Releases.
350+
but will be made available via GitHub Releases.

docs/source/usage.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ help output provides a summary of these options:
4545
4646
-s style, --style style
4747
the docstring style to use when formatting parameter
48-
lists (default: sphinx)
48+
lists. One of epytext, sphinx. (default: sphinx)
4949
--black
5050
make formatting compatible with standard black options
5151
(default: False)

pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ use_parentheses = true
167167
ensure_newline_before_comments = true
168168
line_length = 88
169169

170+
[tool.rstcheck]
171+
report = "warning"
172+
ignore_directives = [
173+
"automodule",
174+
"tabularcolumns",
175+
"toctree",
176+
]
177+
ignore_roles = [
178+
"numref",
179+
]
180+
170181
[tool.tox]
171182
legacy_tox_ini = """
172183
[tox]

src/docformatter/__main__.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@
2424
"""Formats docstrings to follow PEP 257."""
2525

2626

27-
from __future__ import (
28-
absolute_import,
29-
division,
30-
print_function,
31-
unicode_literals,
32-
)
27+
from __future__ import absolute_import, division, print_function, unicode_literals
3328

3429
# Standard Library Imports
3530
import contextlib
@@ -73,7 +68,7 @@ def _help():
7368
7469
-s style, --style style
7570
the docstring style to use when formatting parameter
76-
lists (default: sphinx)
71+
lists. One of epytext, sphinx. (default: sphinx)
7772
--black make formatting compatible with standard black options
7873
(default: False)
7974
--wrap-summaries length

src/docformatter/syntax.py

Lines changed: 133 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,33 @@
3030
import textwrap
3131
from typing import Iterable, List, Tuple, Union
3232

33-
SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:"
34-
"""Regular expression to use for finding Sphinx-style field lists."""
33+
BULLET_REGEX = r"\s*[*\-+] [\S ]+"
34+
"""Regular expression to use for finding bullet lists."""
35+
36+
ENUM_REGEX = r"\s*\d\."
37+
"""Regular expression to use for finding enumerated lists."""
38+
39+
EPYTEXT_REGEX = r"@[a-zA-Z0-9_\-\s]+:"
40+
"""Regular expression to use for finding Epytext-style field lists."""
41+
42+
GOOGLE_REGEX = r"^ *[a-zA-Z0-9_\- ]*:$"
43+
"""Regular expression to use for finding Google-style field lists."""
44+
45+
LITERAL_REGEX = r"[\S ]*::"
46+
"""Regular expression to use for finding literal blocks."""
47+
48+
NUMPY_REGEX = r"^\s[a-zA-Z0-9_\- ]+ ?: [\S ]+"
49+
"""Regular expression to use for finding Numpy-style field lists."""
50+
51+
OPTION_REGEX = r"^-{1,2}[\S ]+ {2}\S+"
52+
"""Regular expression to use for finding option lists."""
3553

3654
REST_REGEX = r"(\.{2}|``) ?[\w-]+(:{1,2}|``)?"
3755
"""Regular expression to use for finding reST directives."""
3856

57+
SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:"
58+
"""Regular expression to use for finding Sphinx-style field lists."""
59+
3960
URL_PATTERNS = (
4061
"afp|"
4162
"apt|"
@@ -238,22 +259,36 @@ def do_find_directives(text: str) -> bool:
238259
return bool([(_rest.start(0), _rest.end(0)) for _rest in _rest_iter])
239260

240261

241-
def do_find_sphinx_field_lists(text: str) -> List[Tuple[int, int]]:
262+
def do_find_field_lists(text: str, style: str):
242263
r"""Determine if docstring contains any field lists.
243264
244265
Parameters
245266
----------
246267
text : str
247268
The docstring description to check for field list patterns.
269+
style : str
270+
The field list style used.
248271
249272
Returns
250273
-------
251-
field_index : list
274+
_field_idx, _wrap_parameters : tuple
252275
A list of tuples with each tuple containing the starting and ending
253276
position of each field list found in the passed description.
277+
A boolean indicating whether long field list lines should be wrapped.
254278
"""
255-
_field_iter = re.finditer(SPHINX_REGEX, text)
256-
return [(_field.start(0), _field.end(0)) for _field in _field_iter]
279+
_field_idx = []
280+
_wrap_parameters = False
281+
282+
if style == "epytext":
283+
_field_iter = re.finditer(EPYTEXT_REGEX, text)
284+
_field_idx = [(_field.start(0), _field.end(0)) for _field in _field_iter]
285+
_wrap_parameters = True
286+
elif style == "sphinx":
287+
_field_iter = re.finditer(SPHINX_REGEX, text)
288+
_field_idx = [(_field.start(0), _field.end(0)) for _field in _field_iter]
289+
_wrap_parameters = True
290+
291+
return _field_idx, _wrap_parameters
257292

258293

259294
def do_find_links(text: str) -> List[Tuple[int, int]]:
@@ -333,12 +368,9 @@ def do_split_description(
333368

334369
# Check if the description contains any URLs.
335370
_url_idx = do_find_links(text)
336-
if style == "sphinx":
337-
_parameter_idx = do_find_sphinx_field_lists(text)
338-
_wrap_parameters = True
339-
else:
340-
_parameter_idx = []
341-
_wrap_parameters = False
371+
372+
# Check if the description contains any field lists.
373+
_parameter_idx, _wrap_parameters = do_find_field_lists(text, style)
342374

343375
if not _url_idx and not (_parameter_idx and _wrap_parameters):
344376
return description_to_list(
@@ -511,6 +543,47 @@ def do_wrap_urls(
511543
return _lines, text_idx
512544

513545

546+
def is_some_sort_of_field_list(
547+
text: str,
548+
style: str,
549+
) -> bool:
550+
"""Determine if docstring contains field lists.
551+
552+
Parameters
553+
----------
554+
text : str
555+
The docstring text.
556+
style : str
557+
The field list style to use.
558+
559+
Returns
560+
-------
561+
is_field_list : bool
562+
Whether the field list pattern for style was found in the docstring.
563+
"""
564+
split_lines = text.rstrip().splitlines()
565+
566+
if style == "epytext":
567+
return any(
568+
(
569+
# "@param x:" <-- Epytext style
570+
# "@type x:" <-- Epytext style
571+
re.match(EPYTEXT_REGEX, line)
572+
)
573+
for line in split_lines
574+
)
575+
elif style == "sphinx":
576+
return any(
577+
(
578+
# ":parameter: description" <-- Sphinx style
579+
re.match(SPHINX_REGEX, line)
580+
)
581+
for line in split_lines
582+
)
583+
584+
return False
585+
586+
514587
# pylint: disable=line-too-long
515588
def is_some_sort_of_list(
516589
text: str,
@@ -545,81 +618,55 @@ def is_some_sort_of_list(
545618
) and not strict:
546619
return True
547620

548-
if style == "sphinx":
549-
return any(
550-
(
551-
# "* parameter" <-- Bullet list
552-
# "- parameter" <-- Bullet list
553-
# "+ parameter" <-- Bullet list
554-
re.match(r"\s*[*\-+] [\S ]+", line)
555-
or
556-
# "1. item" <-- Enumerated list
557-
re.match(r"\s*\d\.", line)
558-
or
559-
# "-a description" <-- Option list
560-
# "--long description" <-- Option list
561-
re.match(r"^-{1,2}[\S ]+ {2}\S+", line)
562-
or
563-
# "@parameter" <-- Epydoc style
564-
re.match(r"\s*@\S*", line)
565-
or
566-
# "parameter : description" <-- Numpy style
567-
# "parameter: description" <-- Numpy style
568-
re.match(r"^\s*(?!:)\S+ ?: \S+", line)
569-
or
570-
# "word\n----" <-- Numpy headings
571-
re.match(r"^\s*-+", line)
572-
or
573-
# "parameter - description"
574-
re.match(r"[\S ]+ - \S+", line)
575-
or
576-
# "parameter -- description"
577-
re.match(r"\s*\S+\s+--\s+", line)
578-
or
579-
# Literal block
580-
re.match(r"[\S ]*::", line)
581-
)
582-
for line in split_lines
583-
)
584-
else:
585-
return any(
586-
(
587-
# "* parameter" <-- Bullet list
588-
# "- parameter" <-- Bullet list
589-
# "+ parameter" <-- Bullet list
590-
re.match(r"\s*[*\-+] [\S ]+", line)
591-
or
592-
# "1. item" <-- Enumerated list
593-
re.match(r"\s*\d\.", line)
594-
or
595-
# "-a description" <-- Option list
596-
# "--long description" <-- Option list
597-
re.match(r"^-{1,2}[\S ]+ {2}\S+", line)
598-
or
599-
# "@parameter" <-- Epydoc style
600-
re.match(r"\s*@\S*", line)
601-
or
602-
# ":parameter: description" <-- Sphinx style
603-
re.match(SPHINX_REGEX, line)
604-
or
605-
# "parameter : description" <-- Numpy style
606-
# "parameter: description" <-- Numpy style
607-
re.match(r"^\s[\S ]+ ?: [\S ]+", line)
608-
or
609-
# "word\n----" <-- Numpy headings
610-
re.match(r"^\s*-+", line)
611-
or
612-
# "parameter - description"
613-
re.match(r"[\S ]+ - \S+", line)
614-
or
615-
# "parameter -- description"
616-
re.match(r"\s*\S+\s+--\s+", line)
617-
or
618-
# Literal block
619-
re.match(r"[\S ]*::", line)
620-
)
621-
for line in split_lines
621+
if is_some_sort_of_field_list(text, style):
622+
return False
623+
624+
return any(
625+
(
626+
# "* parameter" <-- Bullet list
627+
# "- parameter" <-- Bullet list
628+
# "+ parameter" <-- Bullet list
629+
re.match(BULLET_REGEX, line)
630+
or
631+
# "1. item" <-- Enumerated list
632+
re.match(ENUM_REGEX, line)
633+
or
634+
# "-a description" <-- Option list
635+
# "--long description" <-- Option list
636+
re.match(OPTION_REGEX, line)
637+
or
638+
# "@param x:" <-- Epytext style
639+
# "@type x:" <-- Epytext style
640+
re.match(EPYTEXT_REGEX, line)
641+
or
642+
# ":parameter: description" <-- Sphinx style
643+
re.match(SPHINX_REGEX, line)
644+
or
645+
# "parameter : description" <-- Numpy style
646+
# "parameter: description" <-- Numpy style
647+
re.match(NUMPY_REGEX, line)
648+
or
649+
# "word\n----" <-- Numpy headings
650+
re.match(r"^\s*-+", line)
651+
or
652+
# "Args:" <-- Google style
653+
# "parameter:" <-- Google style
654+
re.match(GOOGLE_REGEX, line)
655+
or
656+
# "parameter - description"
657+
re.match(r"[\S ]+ - \S+", line)
658+
or
659+
# "parameter -- description"
660+
re.match(r"\s*\S+\s+--\s+", line)
661+
or
662+
# Literal block
663+
re.match(LITERAL_REGEX, line)
664+
or
665+
# "@parameter"
666+
re.match(r"^ *@[a-zA-Z0-9_\- ]*(?:(?!:).)*$", line)
622667
)
668+
for line in split_lines
669+
)
623670

624671

625672
def is_some_sort_of_code(text: str) -> bool:

0 commit comments

Comments
 (0)