Repository: PyCQA/docformatter (python)

Issue:
Short anonymous hyperlink references get messed up
docformatter doesn't correctly recognize the short form of anonymous hyperlink references (`__ URI`) as a special block, and merges them into the preceding paragraph:

```diff
     """[...]
 
     This graphics format generates terminal escape codes that transfer
-    PNG data to a TTY using the `kitty graphics protocol`__.
-
-    __ https://sw.kovidgoyal.net/kitty/graphics-protocol/
+    PNG data to a TTY using the `kitty graphics protocol
+    `__. __ https://sw.kovidgoyal.net/kitty/graphics-protocol/
     """
```

This breaks the reST, and makes Sphinx unable to correctly process the reference.

Note that this only affects this shorter form; the long form (`.. __: URI`) is unaffected. The following variation (correctly) remains unchanged by docformatter:

```
    """[...]

    This graphics format generates terminal escape codes that transfer
    PNG data to a TTY using the `kitty graphics protocol`__.

    .. __: https://sw.kovidgoyal.net/kitty/graphics-protocol/
    """
```

Bisecting brought up 7898b69f49d43a0a0ac63a8e92afac974a478383 as the first "bad" commit, which is exactly v1.6.0. The earlier 917fed92248d1fb91a1f4da61b5aaaec7367fe23 also changed the output already, but in a slightly different way and it was probably rewritten anyways.

Pull request description:
fix: improper wrapping of short anonymous hyperlnks
Fixes problem with the end of the previous line being wrapped with the short anonymous hyper link.

Closes #210 

Patch:
diff --git a/.github/workflows/do-prioritize-issues.yml b/.github/workflows/do-prioritize-issues.yml
index 3150d34..7c88394 100644
--- a/.github/workflows/do-prioritize-issues.yml
+++ b/.github/workflows/do-prioritize-issues.yml
@@ -1,4 +1,4 @@
-# This workflow runs daily at 1pm.
+# This workflow runs when labels are applied to issues.
 #
 # - Get list of labels.
 # - Determine issue priority based on labels:
@@ -24,19 +24,19 @@ 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: (endsWith(steps.getlabels.outputs.labels, 'convention') && endsWith(steps.getlabels.outputs.labels, '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, ' convenction ') && endsWith(steps.getlabels.outputs.labels, ' enhancement '))
+        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'))
         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 ')) || endsWith(steps.getlabels.outputs.labels, ' doc ') || contains(steps.getlabels.outputs.labels, ' chore ')
+        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')
         uses: andymckay/labeler@master
         with:
           add-labels: "U: low"
diff --git a/src/docformatter/syntax.py b/src/docformatter/syntax.py
index 2a650ad..7aad330 100644
--- a/src/docformatter/syntax.py
+++ b/src/docformatter/syntax.py
@@ -110,13 +110,16 @@ Based on the table at <https://en.wikipedia.org/wiki/List_of_URI_schemes>
 
 # This is the regex used to find URL links:
 #
-# (`{{2}}|`\w[\w. :\n]*|\.\. _?[\w. :]+|')? is used to find in-line links that
+# (__ |`{{2}}|`\w[\w. :\n]*|\.\. _?[\w. :]+|')? is used to find in-line links that
 # should remain on a single line even if it exceeds the wrap length.
+#   __ is used to find to underscores followed by a single space.
+#   This finds patterns like: __ https://sw.kovidgoyal.net/kitty/graphics-protocol/
+#
 #   `{{2}} is used to find two back-tick characters.
 #   This finds patterns like: ``http://www.example.com``
 #
-#   `\w[\w. :#\n]* matches the back-tick character immediately followed by one
-#   letter, then followed by any number of letters, periods, spaces, colons,
+#   `\w[a-zA-Z0-9. :#\n]* matches the back-tick character immediately followed by one
+#   letter, then followed by any number of letters, numbers, periods, spaces, colons,
 #   hash marks or newlines.
 #   This finds patterns like: `Link text <https://domain.invalid/>`_
 #
@@ -137,8 +140,10 @@ Based on the table at <https://en.wikipedia.org/wiki/List_of_URI_schemes>
 #   (//)? matches two forward slashes zero or one time.
 #   (\S*) matches any non-whitespace character between zero and infinity times.
 #   >? matches the character > between zero and one times.
-URL_REGEX = rf"(`{{2}}|`\w[\w. :#\n]*|\.\. _?[\w. :]+|')?<?({URL_PATTERNS}):(\
-//)?(\S*)>?"
+URL_REGEX = (
+    rf"(__ |`{{2}}|`\w[\w :#\n]*[.|\.\. _?[\w. :]+|')?<?"
+    rf"({URL_PATTERNS}):(\//)?(\S*)>?"
+)
 
 URL_SKIP_REGEX = rf"({URL_PATTERNS}):(/){{0,2}}(``|')"
 """The regex used to ignore found hyperlinks.
@@ -195,9 +200,11 @@ def description_to_list(
             initial_indent=indentation,
             subsequent_indent=indentation,
         )
+
         if _text:
             _lines.extend(_text)
         _lines.append("")
+
         with contextlib.suppress(IndexError):
             if not _lines[-1] and not _lines[-2]:
                 _lines.pop(-1)
@@ -530,9 +537,8 @@ def do_wrap_urls(
                     wrap_length,
                 )
             )
-
             with contextlib.suppress(IndexError):
-                if not _lines[-1]:
+                if not text[_url[0] - len(indentation) - 2] == "\n" and not _lines[-1]:
                     _lines.pop(-1)
 
             # Add the URL.


Test patch:
diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py
index 20b059e..54f9a41 100644
--- a/tests/test_format_docstring.py
+++ b/tests/test_format_docstring.py
@@ -1391,12 +1391,12 @@ num_iterations is the number of updates - instead of a better definition of conv
             ]
         ],
     )
-    def test_format_docstring_link_only_one_newline_after_link(
+    def test_format_docstring_link_no_newline_after_link(
         self,
         test_args,
         args,
     ):
-        """Links should have no newline before them and only one after.
+        """Links should have no newline before or after them.
 
         See issue #180.
         """
@@ -1413,11 +1413,9 @@ num_iterations is the number of updates - instead of a better definition of conv
     Generated by 'django-admin startproject' using Django 4.1.1.
 
     For more information on this file, see
-
     https://docs.djangoproject.com/en/4.1/topics/settings/
 
     For the full list of settings and their values, see
-
     https://docs.djangoproject.com/en/4.1/ref/settings/
     """\
 '''
@@ -1437,6 +1435,41 @@ num_iterations is the number of updates - instead of a better definition of conv
             INDENTATION, docstring.strip()
         )
 
+    @pytest.mark.unit
+    @pytest.mark.parametrize("args", [[""]])
+    def test_format_docstring_with_short_anonymous_link(self, test_args, args):
+        """Anonymous link references should not be wrapped into the link.
+
+        See issue #210.
+        """
+        uut = Formatter(
+            test_args,
+            sys.stderr,
+            sys.stdin,
+            sys.stdout,
+        )
+
+        docstring = '''\
+"""Short description.
+
+This graphics format generates terminal escape codes that transfer PNG
+data to a TTY using the `kitty graphics protocol`__.
+
+__ https://sw.kovidgoyal.net/kitty/graphics-protocol/
+"""
+'''
+        assert docstring == uut._do_format_code(
+            '''\
+"""Short description.
+
+This graphics format generates terminal escape codes that transfer
+PNG data to a TTY using the `kitty graphics protocol`__.
+
+__ https://sw.kovidgoyal.net/kitty/graphics-protocol/
+"""
+'''
+        )
+
     @pytest.mark.unit
     @pytest.mark.parametrize("args", [[""]])
     def test_format_docstring_with_class_attributes(self, test_args, args):