Skip to content

Commit

Permalink
fix: wrapping issues with reST directives, quoted URLs, and Sphinx fi…
Browse files Browse the repository at this point in the history
…eld 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
  • Loading branch information
weibullguy committed May 23, 2023
1 parent 4e625cd commit bd0b702
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 42 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/do-prioritize-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ jobs:
uses: weibullguy/get-labels-action@main

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

- name: Add Medium Urgency Labels
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'))
if: |
${{ (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")) }}
uses: andymckay/labeler@master
with:
add-labels: "U: medium"

- name: Add Low Urgency Labels
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')
if: |
${{ (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") }}
uses: andymckay/labeler@master
with:
add-labels: "U: low"
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ repos:
- id: isort
args: [--settings-file, ./pyproject.toml]
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.0
rev: v1.7.1
hooks:
- id: docformatter
additional_dependencies: [tomli]
args: [--in-place, --config, ./pyproject.toml]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.267'
rev: 'v0.0.269'
hooks:
- id: ruff
args: [ --select, "PL", --select, "F" ]
Expand Down
30 changes: 23 additions & 7 deletions src/docformatter/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import textwrap
from typing import Iterable, List, Tuple, Union

DEFAULT_INDENT = 4

BULLET_REGEX = r"\s*[*\-+] [\S ]+"
"""Regular expression to use for finding bullet lists."""

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

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

SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:"
Expand Down Expand Up @@ -466,23 +468,30 @@ def do_wrap_parameter_lists( # noqa: PLR0913
_parameter[1] : parameter_idx[_idx + 1][0]
].strip()
except IndexError:
_parameter_description = text[_parameter[1] :].strip()
_parameter_description = (
text[_parameter[1] :].strip().replace(" ", "").replace("\t", "")
)

if len(_parameter_description) <= (wrap_length - len(indentation)):
lines.append(
f"{indentation}{text[_parameter[0]: _parameter[1]]} "
f"{_parameter_description}"
)
else:
if len(indentation) > DEFAULT_INDENT:
_subsequent = indentation + int(0.5 * len(indentation)) * " "
else:
_subsequent = 2 * indentation

lines.extend(
textwrap.wrap(
textwrap.dedent(
f"{text[_parameter[0]:_parameter[1]]} "
f"{_parameter_description.replace(2*indentation, '')}"
f"{_parameter_description.strip()}"
),
width=wrap_length,
initial_indent=indentation,
subsequent_indent=2 * indentation,
subsequent_indent=_subsequent,
)
)

Expand Down Expand Up @@ -537,12 +546,19 @@ def do_wrap_urls(
wrap_length,
)
)

with contextlib.suppress(IndexError):
if not text[_url[0] - len(indentation) - 2] == "\n" and not _lines[-1]:
if text[_url[0] - len(indentation) - 2] != "\n" and not _lines[-1]:
_lines.pop(-1)

# Add the URL.
_lines.append(f"{do_clean_url(text[_url[0] : _url[1]], indentation)}")
# Add the URL making sure that the leading quote is kept with a quoted URL.
_text = f"{text[_url[0]: _url[1]]}"
with contextlib.suppress(IndexError):
if _lines[0][-1] == '"':
_lines[0] = _lines[0][:-2]
_text = f'"{text[_url[0] : _url[1]]}'

_lines.append(f"{do_clean_url(_text, indentation)}")

text_idx = _url[1]

Expand Down
226 changes: 196 additions & 30 deletions tests/test_format_docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,60 @@ def test_format_docstring_for_one_line_summary_alone_but_too_long(
)
)

@pytest.mark.unit
@pytest.mark.parametrize("args", [[""]])
def test_format_docstring_with_class_attributes(self, test_args, args):
"""Wrap long class attribute docstrings."""
uut = Formatter(
test_args,
sys.stderr,
sys.stdin,
sys.stdout,
)

docstring = '''\
class TestClass:
"""This is a class docstring."""
test_int = 1
"""This is a very, very, very long docstring that should really be
reformatted nicely by docformatter."""
'''
assert docstring == uut._do_format_code(
'''\
class TestClass:
"""This is a class docstring."""
test_int = 1
"""This is a very, very, very long docstring that should really be reformatted nicely by docformatter."""
'''
)

@pytest.mark.unit
@pytest.mark.parametrize("args", [[""]])
def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args):
"""Wrap summary with symbol should not add newline.
See issue #79.
"""
uut = Formatter(
test_args,
sys.stderr,
sys.stdin,
sys.stdout,
)

docstring = '''\
def function2():
"""Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet
-v."""
'''
assert docstring == uut._do_format_code(docstring)


class TestFormatWrapURL:
"""Class for testing _do_format_docstring() with line wrapping and URLs."""

@pytest.mark.unit
@pytest.mark.parametrize(
"args",
Expand Down Expand Up @@ -1472,8 +1526,11 @@ def test_format_docstring_with_short_anonymous_link(self, test_args, args):

@pytest.mark.unit
@pytest.mark.parametrize("args", [[""]])
def test_format_docstring_with_class_attributes(self, test_args, args):
"""Wrap long class attribute docstrings."""
def test_format_docstring_with_quoted_link(self, test_args, args):
"""Anonymous link references should not be wrapped into the link.
See issue #218.
"""
uut = Formatter(
test_args,
sys.stderr,
Expand All @@ -1482,43 +1539,32 @@ def test_format_docstring_with_class_attributes(self, test_args, args):
)

docstring = '''\
class TestClass:
"""This is a class docstring."""
"""Construct a candidate project URL from the bundle and app name.
test_int = 1
"""This is a very, very, very long docstring that should really be
reformatted nicely by docformatter."""
It's not a perfect guess, but it's better than having
"https://example.com".
:param bundle: The bundle identifier.
:param app_name: The app name.
:returns: The candidate project URL
"""
'''
assert docstring == uut._do_format_code(
'''\
class TestClass:
"""This is a class docstring."""
"""Construct a candidate project URL from the bundle and app name.
test_int = 1
"""This is a very, very, very long docstring that should really be reformatted nicely by docformatter."""
It's not a perfect guess, but it's better than having "https://example.com".
:param bundle: The bundle identifier.
:param app_name: The app name.
:returns: The candidate project URL
"""
'''
)

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

See issue #79.
"""
uut = Formatter(
test_args,
sys.stderr,
sys.stdin,
sys.stdout,
)

docstring = '''\
def function2():
"""Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet
-v."""
'''
assert docstring == uut._do_format_code(docstring)
class TestFormatWrapBlack:
"""Class for testing _do_format_docstring() with line wrapping and black option."""

@pytest.mark.unit
@pytest.mark.parametrize(
Expand Down Expand Up @@ -1585,6 +1631,10 @@ def test_format_docstring_black(
)
)


class TestFormatWrapEpytext:
"""Class for testing _do_format_docstring() with line wrapping and Epytext lists."""

@pytest.mark.unit
@pytest.mark.parametrize(
"args",
Expand Down Expand Up @@ -1720,6 +1770,10 @@ def test_format_docstring_non_epytext_style(
)
)


class TestFormatWrapSphinx:
"""Class for testing _do_format_docstring() with line wrapping and Sphinx lists."""

@pytest.mark.unit
@pytest.mark.parametrize(
"args",
Expand Down Expand Up @@ -1857,6 +1911,118 @@ def test_format_docstring_non_sphinx_style(
)
)

@pytest.mark.unit
@pytest.mark.parametrize(
"args",
[
[
"--wrap-descriptions",
"88",
"--wrap-summaries",
"88",
"",
]
],
)
def test_format_docstring_sphinx_style_remove_excess_whitespace(
self,
test_args,
args,
):
"""Should remove unneeded whitespace.
See issue #217
"""
uut = Formatter(
test_args,
sys.stderr,
sys.stdin,
sys.stdout,
)

assert (
(
'''\
"""Base for all Commands.
:param logger: Logger for console and logfile.
:param console: Facilitates console interaction and input solicitation.
:param tools: Cache of tools populated by Commands as they are required.
:param apps: Dictionary of project's Apps keyed by app name.
:param base_path: Base directory for Briefcase project.
:param data_path: Base directory for Briefcase tools, support packages, etc.
:param is_clone: Flag that Command was triggered by the user's requested Command;
for instance, RunCommand can invoke UpdateCommand and/or BuildCommand.
"""\
'''
)
== uut._do_format_docstring(
INDENTATION,
'''\
"""Base for all Commands.
:param logger: Logger for console and logfile.
:param console: Facilitates console interaction and input solicitation.
:param tools: Cache of tools populated by Commands as they are required.
:param apps: Dictionary of project's Apps keyed by app name.
:param base_path: Base directory for Briefcase project.
:param data_path: Base directory for Briefcase tools, support packages, etc.
:param is_clone: Flag that Command was triggered by the user's requested Command;
for instance, RunCommand can invoke UpdateCommand and/or BuildCommand.
"""\
''',
)
)

@pytest.mark.unit
@pytest.mark.parametrize(
"args",
[
[
"--wrap-descriptions",
"88",
"--wrap-summaries",
"88",
"",
]
],
)
def test_format_docstring_sphinx_style_two_directives_in_row(
self,
test_args,
args,
):
"""Should remove unneeded whitespace.
See issue #215.
"""
uut = Formatter(
test_args,
sys.stderr,
sys.stdin,
sys.stdout,
)

assert (
(
'''\
"""Create or return existing HTTP session.
:return: Requests :class:`~requests.Session` object
"""\
'''
)
== uut._do_format_docstring(
INDENTATION,
'''\
"""Create or return existing HTTP session.
:return: Requests :class:`~requests.Session` object
"""\
''',
)
)


class TestFormatStyleOptions:
"""Class for testing format_docstring() when requesting style options."""
Expand Down

0 comments on commit bd0b702

Please sign in to comment.