Repository: anntzer/defopt

Issue:
Support URLs in docstring
Defopt seems to raise an error if you have a URL (both plain text URLs and [RST-formatted URLs](https://sublime-and-sphinx-guide.readthedocs.io/en/latest/references.html#links-to-external-web-pages)) in your docstring. I can submit a PR to fix this.

Discussion:
anntzer: ```rst
Go to Google_.

.. _Google: https://www.google.com
```
currently breaks too, can you fix that as well? I think you just need to Skip the target node when current_paragraph is None?  (Then the URI doesn't show up in the rendered text, which seems correct.)

Patch:
diff --git a/lib/defopt.py b/lib/defopt.py
index bb2422f..72114fc 100644
--- a/lib/defopt.py
+++ b/lib/defopt.py
@@ -754,6 +754,18 @@ def _parse_docstring(doc):
 
         depart_Text = _do_nothing
 
+        visit_reference = depart_reference = _do_nothing
+
+        def visit_target(self, node):
+            if self._current_paragraph is None:
+                raise SkipNode
+
+            if node.get('refuri'):
+                self._current_paragraph.append(" ({})".format(node['refuri']))
+            else:
+                self._current_paragraph.append(node.astext())
+            raise SkipNode
+
         def visit_emphasis(self, node):
             self._current_paragraph.append('\033[3m')  # *foo*: italic
 
diff --git a/noxfile.py b/noxfile.py
index 5c553c7..ce92e88 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -60,7 +60,6 @@ def docs(session: Session) -> None:
     """Produce the coverage report."""
     args = session.posargs or ['-b', 'html', 'doc/source', 'doc/build']
 
-    session.install('-r', 'doc/requirements.txt')
-    session.install('-e', '.')
+    session.install('-e', '.[docs]')
 
     session.run('sphinx-build', *args)


Test patch:
diff --git a/test_defopt.py b/test_defopt.py
index 28ebf08..98976b0 100644
--- a/test_defopt.py
+++ b/test_defopt.py
@@ -1163,6 +1163,33 @@ class TestHelp(unittest.TestCase):
         self.assert_in_help('%(prog)s', foo, '')
         self.assert_not_in_help('%%', foo, '')
 
+    def test_hyperlink_plaintext(self):
+        def foo():
+            """This site https://www.python.org/ is cool"""
+        self.assert_in_help(
+            "This site https://www.python.org/ is cool",
+            foo,
+            ""
+        )
+
+    def test_hyperlink_target_embedded(self):
+        def foo():
+            """`This site <https://www.python.org/>`_ is cool"""
+        self.assert_in_help(
+            "This site (https://www.python.org/) is cool",
+            foo,
+            ""
+        )
+
+    def test_hyperlink_target_separated(self):
+        def foo():
+            """This site_ is cool
+
+            .. _site: https://www.python.org/
+            """
+        self.assert_in_help("This site is cool", foo, "")
+        self.assert_not_in_help("https://www.python.org/", foo, "")
+
     def test_rst_ansi(self):
         def foo():
             """**bold** *italic* `underlined`"""
@@ -1428,9 +1455,18 @@ class TestDefaultsPreserved(unittest.TestCase):
 
 
 class TestStyle(unittest.TestCase):
-    def test_line_length(self):
+    def _iter_stripped_lines(self):
         for path in [defopt.__file__, __file__]:
             with tokenize.open(path) as src:
                 for i, line in enumerate(src, 1):
-                    if len(line) > 80:  # 79 chars + newline.
-                        self.fail('{}:{} is too long'.format(path, i))
+                    yield '{}:{}'.format(path, i), line.rstrip('\n')
+
+    def test_line_length(self):
+        for name, line in self._iter_stripped_lines():
+            if len(line) > 79:
+                self.fail('{} is too long'.format(name))
+
+    def test_trailing_whitespace(self):
+        for name, line in self._iter_stripped_lines():
+            if line and line[-1].isspace():
+                self.fail('{} has trailing whitespace'.format(name))