Skip to content

Commit bd0b702

Browse files
authored
fix: wrapping issues with reST directives, quoted URLs, and Sphinx field lists (#219)
* fix: rest regex to find single backtick directives * chore: update prioritize issues workflow * fix: issues 215, 217, and 218 * test: for issue 215,217,218 fixes
1 parent 4e625cd commit bd0b702

File tree

4 files changed

+227
-42
lines changed

4 files changed

+227
-42
lines changed

.github/workflows/do-prioritize-issues.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,22 @@ jobs:
2424
uses: weibullguy/get-labels-action@main
2525

2626
- name: Add High Urgency Labels
27-
if: (endsWith(steps.getlabels.outputs.labels, 'convention') && endsWith(steps.getlabels.outputs.labels, 'bug'))
27+
if: |
28+
${{ (contains(steps.getlabels.outputs.labels, "C: convention") && contains (steps.getlabels.outputs.labels, "P: bug")) }}
2829
uses: andymckay/labeler@master
2930
with:
3031
add-labels: "U: high"
3132

3233
- name: Add Medium Urgency Labels
33-
if: (endsWith(steps.getlabels.outputs.labels, 'style') && endsWith(steps.getlabels.outputs.labels, 'bug')) || (endsWith(steps.getlabels.outputs.labels, 'stakeholder') && endsWith(steps.getlabels.outputs.labels, 'bug')) || (endsWith(steps.getlabels.outputs.labels, 'convention') && endsWith(steps.getlabels.outputs.labels, 'enhancement'))
34+
if: |
35+
${{ (contains(steps.getlabels.outputs.labels, "C: style") && contains(steps.getlabels.outputs.labels, "P: bug")) || (contains(steps.getlabels.outputs.labels, "C: stakeholder") && contains(steps.getlabels.outputs.labels, "P: bug")) || (contains(steps.getlabels.outputs.labels, "C: convention") && contains(steps.getlabels.outputs.labels, "P: enhancement")) }}
3436
uses: andymckay/labeler@master
3537
with:
3638
add-labels: "U: medium"
3739

3840
- name: Add Low Urgency Labels
39-
if: (endsWith(steps.getlabels.outputs.labels, 'style') && endsWith(steps.getlabels.outputs.labels, 'enhancement')) || (endsWith(steps.getlabels.outputs.labels, 'stakeholder') && endsWith(steps.getlabels.outputs.labels, 'enhancement')) || contains(steps.getlabels.outputs.labels, 'doc') || contains(steps.getlabels.outputs.labels, 'chore')
41+
if: |
42+
${{ (contains(steps.getlabels.outputs.labels, "C: style") && contains(steps.getlabels.outputs.labels, "P: enhancement")) || (contains(steps.getlabels.outputs.labels, "C: stakeholder") && contains(steps.getlabels.outputs.labels, "P: enhancement")) || contains(steps.getlabels.outputs.labels, "doc") || contains(steps.getlabels.outputs.labels, "chore") }}
4043
uses: andymckay/labeler@master
4144
with:
4245
add-labels: "U: low"

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ repos:
2121
- id: isort
2222
args: [--settings-file, ./pyproject.toml]
2323
- repo: https://github.com/PyCQA/docformatter
24-
rev: v1.7.0
24+
rev: v1.7.1
2525
hooks:
2626
- id: docformatter
2727
additional_dependencies: [tomli]
2828
args: [--in-place, --config, ./pyproject.toml]
2929
- repo: https://github.com/charliermarsh/ruff-pre-commit
30-
rev: 'v0.0.267'
30+
rev: 'v0.0.269'
3131
hooks:
3232
- id: ruff
3333
args: [ --select, "PL", --select, "F" ]

src/docformatter/syntax.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import textwrap
3131
from typing import Iterable, List, Tuple, Union
3232

33+
DEFAULT_INDENT = 4
34+
3335
BULLET_REGEX = r"\s*[*\-+] [\S ]+"
3436
"""Regular expression to use for finding bullet lists."""
3537

@@ -51,7 +53,7 @@
5153
OPTION_REGEX = r"^-{1,2}[\S ]+ {2}\S+"
5254
"""Regular expression to use for finding option lists."""
5355

54-
REST_REGEX = r"(\.{2}|``) ?[\w-]+(:{1,2}|``)?"
56+
REST_REGEX = r"((\.{2}|`{2}) ?[\w.~-]+(:{2}|`{2})?[\w ]*?|`[\w.~]+`)"
5557
"""Regular expression to use for finding reST directives."""
5658

5759
SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:"
@@ -466,23 +468,30 @@ def do_wrap_parameter_lists( # noqa: PLR0913
466468
_parameter[1] : parameter_idx[_idx + 1][0]
467469
].strip()
468470
except IndexError:
469-
_parameter_description = text[_parameter[1] :].strip()
471+
_parameter_description = (
472+
text[_parameter[1] :].strip().replace(" ", "").replace("\t", "")
473+
)
470474

471475
if len(_parameter_description) <= (wrap_length - len(indentation)):
472476
lines.append(
473477
f"{indentation}{text[_parameter[0]: _parameter[1]]} "
474478
f"{_parameter_description}"
475479
)
476480
else:
481+
if len(indentation) > DEFAULT_INDENT:
482+
_subsequent = indentation + int(0.5 * len(indentation)) * " "
483+
else:
484+
_subsequent = 2 * indentation
485+
477486
lines.extend(
478487
textwrap.wrap(
479488
textwrap.dedent(
480489
f"{text[_parameter[0]:_parameter[1]]} "
481-
f"{_parameter_description.replace(2*indentation, '')}"
490+
f"{_parameter_description.strip()}"
482491
),
483492
width=wrap_length,
484493
initial_indent=indentation,
485-
subsequent_indent=2 * indentation,
494+
subsequent_indent=_subsequent,
486495
)
487496
)
488497

@@ -537,12 +546,19 @@ def do_wrap_urls(
537546
wrap_length,
538547
)
539548
)
549+
540550
with contextlib.suppress(IndexError):
541-
if not text[_url[0] - len(indentation) - 2] == "\n" and not _lines[-1]:
551+
if text[_url[0] - len(indentation) - 2] != "\n" and not _lines[-1]:
542552
_lines.pop(-1)
543553

544-
# Add the URL.
545-
_lines.append(f"{do_clean_url(text[_url[0] : _url[1]], indentation)}")
554+
# Add the URL making sure that the leading quote is kept with a quoted URL.
555+
_text = f"{text[_url[0]: _url[1]]}"
556+
with contextlib.suppress(IndexError):
557+
if _lines[0][-1] == '"':
558+
_lines[0] = _lines[0][:-2]
559+
_text = f'"{text[_url[0] : _url[1]]}'
560+
561+
_lines.append(f"{do_clean_url(_text, indentation)}")
546562

547563
text_idx = _url[1]
548564

tests/test_format_docstring.py

Lines changed: 196 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,60 @@ def test_format_docstring_for_one_line_summary_alone_but_too_long(
934934
)
935935
)
936936

937+
@pytest.mark.unit
938+
@pytest.mark.parametrize("args", [[""]])
939+
def test_format_docstring_with_class_attributes(self, test_args, args):
940+
"""Wrap long class attribute docstrings."""
941+
uut = Formatter(
942+
test_args,
943+
sys.stderr,
944+
sys.stdin,
945+
sys.stdout,
946+
)
947+
948+
docstring = '''\
949+
class TestClass:
950+
"""This is a class docstring."""
951+
952+
test_int = 1
953+
"""This is a very, very, very long docstring that should really be
954+
reformatted nicely by docformatter."""
955+
'''
956+
assert docstring == uut._do_format_code(
957+
'''\
958+
class TestClass:
959+
"""This is a class docstring."""
960+
961+
test_int = 1
962+
"""This is a very, very, very long docstring that should really be reformatted nicely by docformatter."""
963+
'''
964+
)
965+
966+
@pytest.mark.unit
967+
@pytest.mark.parametrize("args", [[""]])
968+
def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args):
969+
"""Wrap summary with symbol should not add newline.
970+
971+
See issue #79.
972+
"""
973+
uut = Formatter(
974+
test_args,
975+
sys.stderr,
976+
sys.stdin,
977+
sys.stdout,
978+
)
979+
980+
docstring = '''\
981+
def function2():
982+
"""Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet
983+
-v."""
984+
'''
985+
assert docstring == uut._do_format_code(docstring)
986+
987+
988+
class TestFormatWrapURL:
989+
"""Class for testing _do_format_docstring() with line wrapping and URLs."""
990+
937991
@pytest.mark.unit
938992
@pytest.mark.parametrize(
939993
"args",
@@ -1472,8 +1526,11 @@ def test_format_docstring_with_short_anonymous_link(self, test_args, args):
14721526

14731527
@pytest.mark.unit
14741528
@pytest.mark.parametrize("args", [[""]])
1475-
def test_format_docstring_with_class_attributes(self, test_args, args):
1476-
"""Wrap long class attribute docstrings."""
1529+
def test_format_docstring_with_quoted_link(self, test_args, args):
1530+
"""Anonymous link references should not be wrapped into the link.
1531+
1532+
See issue #218.
1533+
"""
14771534
uut = Formatter(
14781535
test_args,
14791536
sys.stderr,
@@ -1482,43 +1539,32 @@ def test_format_docstring_with_class_attributes(self, test_args, args):
14821539
)
14831540

14841541
docstring = '''\
1485-
class TestClass:
1486-
"""This is a class docstring."""
1542+
"""Construct a candidate project URL from the bundle and app name.
14871543
1488-
test_int = 1
1489-
"""This is a very, very, very long docstring that should really be
1490-
reformatted nicely by docformatter."""
1544+
It's not a perfect guess, but it's better than having
1545+
"https://example.com".
1546+
1547+
:param bundle: The bundle identifier.
1548+
:param app_name: The app name.
1549+
:returns: The candidate project URL
1550+
"""
14911551
'''
14921552
assert docstring == uut._do_format_code(
14931553
'''\
1494-
class TestClass:
1495-
"""This is a class docstring."""
1554+
"""Construct a candidate project URL from the bundle and app name.
14961555
1497-
test_int = 1
1498-
"""This is a very, very, very long docstring that should really be reformatted nicely by docformatter."""
1556+
It's not a perfect guess, but it's better than having "https://example.com".
1557+
1558+
:param bundle: The bundle identifier.
1559+
:param app_name: The app name.
1560+
:returns: The candidate project URL
1561+
"""
14991562
'''
15001563
)
15011564

1502-
@pytest.mark.unit
1503-
@pytest.mark.parametrize("args", [[""]])
1504-
def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args):
1505-
"""Wrap summary with symbol should not add newline.
15061565

1507-
See issue #79.
1508-
"""
1509-
uut = Formatter(
1510-
test_args,
1511-
sys.stderr,
1512-
sys.stdin,
1513-
sys.stdout,
1514-
)
1515-
1516-
docstring = '''\
1517-
def function2():
1518-
"""Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet
1519-
-v."""
1520-
'''
1521-
assert docstring == uut._do_format_code(docstring)
1566+
class TestFormatWrapBlack:
1567+
"""Class for testing _do_format_docstring() with line wrapping and black option."""
15221568

15231569
@pytest.mark.unit
15241570
@pytest.mark.parametrize(
@@ -1585,6 +1631,10 @@ def test_format_docstring_black(
15851631
)
15861632
)
15871633

1634+
1635+
class TestFormatWrapEpytext:
1636+
"""Class for testing _do_format_docstring() with line wrapping and Epytext lists."""
1637+
15881638
@pytest.mark.unit
15891639
@pytest.mark.parametrize(
15901640
"args",
@@ -1720,6 +1770,10 @@ def test_format_docstring_non_epytext_style(
17201770
)
17211771
)
17221772

1773+
1774+
class TestFormatWrapSphinx:
1775+
"""Class for testing _do_format_docstring() with line wrapping and Sphinx lists."""
1776+
17231777
@pytest.mark.unit
17241778
@pytest.mark.parametrize(
17251779
"args",
@@ -1857,6 +1911,118 @@ def test_format_docstring_non_sphinx_style(
18571911
)
18581912
)
18591913

1914+
@pytest.mark.unit
1915+
@pytest.mark.parametrize(
1916+
"args",
1917+
[
1918+
[
1919+
"--wrap-descriptions",
1920+
"88",
1921+
"--wrap-summaries",
1922+
"88",
1923+
"",
1924+
]
1925+
],
1926+
)
1927+
def test_format_docstring_sphinx_style_remove_excess_whitespace(
1928+
self,
1929+
test_args,
1930+
args,
1931+
):
1932+
"""Should remove unneeded whitespace.
1933+
1934+
See issue #217
1935+
"""
1936+
uut = Formatter(
1937+
test_args,
1938+
sys.stderr,
1939+
sys.stdin,
1940+
sys.stdout,
1941+
)
1942+
1943+
assert (
1944+
(
1945+
'''\
1946+
"""Base for all Commands.
1947+
1948+
:param logger: Logger for console and logfile.
1949+
:param console: Facilitates console interaction and input solicitation.
1950+
:param tools: Cache of tools populated by Commands as they are required.
1951+
:param apps: Dictionary of project's Apps keyed by app name.
1952+
:param base_path: Base directory for Briefcase project.
1953+
:param data_path: Base directory for Briefcase tools, support packages, etc.
1954+
:param is_clone: Flag that Command was triggered by the user's requested Command;
1955+
for instance, RunCommand can invoke UpdateCommand and/or BuildCommand.
1956+
"""\
1957+
'''
1958+
)
1959+
== uut._do_format_docstring(
1960+
INDENTATION,
1961+
'''\
1962+
"""Base for all Commands.
1963+
1964+
:param logger: Logger for console and logfile.
1965+
:param console: Facilitates console interaction and input solicitation.
1966+
:param tools: Cache of tools populated by Commands as they are required.
1967+
:param apps: Dictionary of project's Apps keyed by app name.
1968+
:param base_path: Base directory for Briefcase project.
1969+
:param data_path: Base directory for Briefcase tools, support packages, etc.
1970+
:param is_clone: Flag that Command was triggered by the user's requested Command;
1971+
for instance, RunCommand can invoke UpdateCommand and/or BuildCommand.
1972+
"""\
1973+
''',
1974+
)
1975+
)
1976+
1977+
@pytest.mark.unit
1978+
@pytest.mark.parametrize(
1979+
"args",
1980+
[
1981+
[
1982+
"--wrap-descriptions",
1983+
"88",
1984+
"--wrap-summaries",
1985+
"88",
1986+
"",
1987+
]
1988+
],
1989+
)
1990+
def test_format_docstring_sphinx_style_two_directives_in_row(
1991+
self,
1992+
test_args,
1993+
args,
1994+
):
1995+
"""Should remove unneeded whitespace.
1996+
1997+
See issue #215.
1998+
"""
1999+
uut = Formatter(
2000+
test_args,
2001+
sys.stderr,
2002+
sys.stdin,
2003+
sys.stdout,
2004+
)
2005+
2006+
assert (
2007+
(
2008+
'''\
2009+
"""Create or return existing HTTP session.
2010+
2011+
:return: Requests :class:`~requests.Session` object
2012+
"""\
2013+
'''
2014+
)
2015+
== uut._do_format_docstring(
2016+
INDENTATION,
2017+
'''\
2018+
"""Create or return existing HTTP session.
2019+
2020+
:return: Requests :class:`~requests.Session` object
2021+
"""\
2022+
''',
2023+
)
2024+
)
2025+
18602026

18612027
class TestFormatStyleOptions:
18622028
"""Class for testing format_docstring() when requesting style options."""

0 commit comments

Comments
 (0)