Repository: PyCQA/docformatter (python)

Issue:
URL matching only matches certain schemes, not others
Specifically it split up an `s3` scheme URL :sob: 

Likely due to this: https://github.com/PyCQA/docformatter/blob/6706e9591c9f605956372eb1ee6228c465e0ac93/src/docformatter/syntax.py#L114

Pull request:
fix: add additional URL patterns
Adds a long list of URL patterns to match.

Closes #146

Patch:
diff --git a/README.rst b/README.rst
index cd3fae7..1d208a0 100644
--- a/README.rst
+++ b/README.rst
@@ -15,8 +15,12 @@ docformatter
     :target: https://pycqa.github.io/isort/
 .. |SELF| image:: https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg
     :target: https://github.com/PyCQA/docformatter
-.. |DOCSTYLE| image:: https://img.shields.io/badge/%20style-numpy-459db9.svg
+.. |SPHINXSTYLE| image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg
+    :target: https://www.sphinx-doc.org/en/master/usage/index.html
+.. |NUMPSTYLE| image:: https://img.shields.io/badge/%20style-numpy-459db9.svg
     :target: https://numpydoc.readthedocs.io/en/latest/format.html
+.. |GOOGSTYLE| image:: https://img.shields.io/badge/%20style-google-3666d6.svg
+    :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings
 
 .. |VERSION| image:: https://img.shields.io/pypi/v/docformatter
 .. |LICENSE| image:: https://img.shields.io/pypi/l/docformatter
@@ -27,7 +31,7 @@ docformatter
 +----------------+----------------------------------------------------------+
 | **Code**       + |BLACK| |ISORT|                                          +
 +----------------+----------------------------------------------------------+
-| **Docstrings** + |SELF| |DOCSTYLE|                                        +
+| **Docstrings** + |SELF| |NUMPSTYLE|                                       +
 +----------------+----------------------------------------------------------+
 | **GitHub**     + |CI| |CONTRIBUTORS| |COMMIT|                             +
 +----------------+----------------------------------------------------------+
@@ -171,28 +175,26 @@ Do you use *docformatter*?  What style docstrings do you use?  Add some badges t
     .. image:: https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg
         :target: https://github.com/PyCQA/docformatter
 
-    .. image:: https://img.shields.io/badge/%20style-google-3666d6.svg
-        :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings
+|SPHINXSTYLE|
 
 .. code-block::
 
-    .. image:: https://img.shields.io/badge/%20style-google-3666d6.svg
-        :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings
+    .. image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg
+        :target: https://www.sphinx-doc.org/en/master/usage/index.html
 
-|DOCSTYLE|
+|NUMPSTYLE|
 
 .. code-block::
 
     .. image:: https://img.shields.io/badge/%20style-numpy-459db9.svg
         :target: https://numpydoc.readthedocs.io/en/latest/format.html
 
-    .. image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg
-        :target: https://www.sphinx-doc.org/en/master/usage/index.html
+|GOOGSTYLE|
 
 .. code-block::
 
-    .. image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg
-        :target: https://www.sphinx-doc.org/en/master/usage/index.html
+    .. image:: https://img.shields.io/badge/%20style-google-3666d6.svg
+        :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings
 
 Issues
 ======
diff --git a/src/docformatter/syntax.py b/src/docformatter/syntax.py
index 119b156..c50c67e 100644
--- a/src/docformatter/syntax.py
+++ b/src/docformatter/syntax.py
@@ -107,14 +107,7 @@ def do_preserve_links(
     if len(lines) < 2:
         return lines
 
-    url = next(
-        (
-            line
-            for line in lines
-            if re.search(r"<?(http://|https://|ftp://|sftp://)", line)
-        ),
-        "",
-    )
+    url = is_some_sort_of_link(lines)
 
     if url != "":
         url_idx = lines.index(url)
@@ -165,6 +158,77 @@ def do_preserve_links(
     return lines
 
 
+def is_some_sort_of_link(lines: List[str]) -> str:
+    """Determine if docstring line contains a link.
+
+    URL patterns based on table at
+    <https://en.wikipedia.org/wiki/List_of_URI_schemes>
+
+    Parameters
+    ----------
+    lines: str
+        the list of docstring lines to check for a link pattern.
+
+    Returns
+    -------
+    url: str
+        the line with the url pattern.
+    """
+    url_patterns = (
+        "("
+        "afp://|"
+        "apt:|"
+        "bitcoin:|"
+        "chrome://|"
+        "cvs://|"
+        "dav://|"
+        "dns:|"
+        "file://|"
+        "finger://|"
+        "fish://|"
+        "ftp://|"
+        "ftps://|"
+        "git://|"
+        "http://|"
+        "https://|"
+        "imap://|"
+        "ipp://|"
+        "ipps://|"
+        "irc://|"
+        "irc6://|"
+        "ircs://|"
+        "jar:|"
+        "ldap://|"
+        "ldaps://|"
+        "mailto:|"
+        "news:|"
+        "nfs://|"
+        "nntp://|"
+        "pop://|"
+        "rsync://|"
+        "s3://|"
+        "sftp://|"
+        "shttp://|"
+        "sip:|"
+        "sips:|"
+        "smb://|"
+        "sms:|"
+        "snmp://|"
+        "ssh://|"
+        "svn://|"
+        "telnet://|"
+        "vnc://|"
+        "xmpp:|"
+        "xri://"
+        ")"
+    )
+
+    return next(
+        (line for line in lines if re.search(rf"<?{url_patterns}", line)),
+        "",
+    )
+
+
 # pylint: disable=line-too-long
 def is_some_sort_of_list(text, strict) -> bool:
     """Determine if docstring is a reST list.


Test patch:
diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py
index 270a064..86009a1 100644
--- a/tests/test_utility_functions.py
+++ b/tests/test_utility_functions.py
@@ -50,7 +50,6 @@ class TestFindPyFiles:
     @pytest.mark.unit
     def test_is_hidden(self):
         """Skip files that are .hidden."""
-
         assert docformatter.find_py_files("not_hidden", ".hidden_file.py")
 
     @pytest.mark.xfail(
@@ -58,7 +57,6 @@ class TestFindPyFiles:
     )
     def test_non_recursive_ignore_non_py_files(self):
         """Only process python (*.py) files."""
-
         sources = ["one.py", "two.py", "three.toml"]
 
         test_only_py = list(docformatter.find_py_files(sources, False))
@@ -67,7 +65,6 @@ class TestFindPyFiles:
     @pytest.mark.unit
     def test_recursive_ignore_non_py_files(self):
         """Only process python (*.py) files when recursing directories."""
-
         sources = {"/root"}
         patch_data = [
             ("/root", [], ["one.py", "two.py", "three.toml"]),
@@ -226,7 +223,429 @@ class TestIsProbablySentence:
         )
 
 
-class TestIsSomeSortdOfList:
+class TestIsSomeSortOfLink:
+    """Class for testing the is_some_sort_of_link() function."""
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_file_system_link(self):
+        """Identify afp://, nfs://, smb:// as a link."""
+        lines = [
+            "This is an Apple Filing Protocol URL pattern:",
+            "afp://[<user@]<host>[:<port>][/[<path>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "afp://[<user@]<host>[:<port>][/[<path>]]"
+        )
+        lines = [
+            "This is an Network File System URL pattern:",
+            "nfs://server<:port>/<path>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "nfs://server<:port>/<path>"
+        )
+        lines = [
+            "This is an Samba URL pattern:",
+            "smb://[<user>@]<host>[:<port>][/[<path>]][?<param1>=<value1>[;<param2>=<value2>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "smb://[<user>@]<host>[:<port>][/[<path>]][?<param1>=<value1>[;<param2>=<value2>]]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_miscellaneous_link(self):
+        """Identify apt:, bitcoin:, chrome://, and jar: as a link."""
+        lines = ["This is an apt URL pattern:", "apt:docformatter"]
+        assert docformatter.is_some_sort_of_link(lines) == "apt:docformatter"
+        lines = [
+            "This is a bitcoin URL pattern:",
+            "bitcoin:<address>[?[amount=<size>][&][label=<label>][&]["
+            "message=<message>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "bitcoin:<address>[?[amount=<size>][&][label=<label>][&]["
+            "message=<message>]]"
+        )
+        lines = [
+            "This is a chrome URL pattern:",
+            "chrome://<package>/<section>/<path>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "chrome://<package>/<section>/<path>"
+        )
+        lines = [
+            "This is a Java compressed archive URL pattern:",
+            "jar:<url>!/[<entry>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines) == "jar:<url>!/[<entry>]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_version_control_link(self):
+        """Identify cvs://, git://, or svn:// as a link."""
+        lines = [
+            "This is a Concurrent Versions System URL pattern:",
+            "cvs://<method:logindetails>@<repository>/<modulepath>;[date=date "
+            "to retrieve | tag=tag to retrieve]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "cvs://<method:logindetails>@<repository>/<modulepath>;["
+            "date=date to retrieve | tag=tag to retrieve]"
+        )
+        lines = [
+            "This is a Git URL pattern:",
+            "git://github.com/PyCQA/docformatter.git",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "git://github.com/PyCQA/docformatter.git"
+        )
+        lines = [
+            "This is a Subversion URL pattern:",
+            "svn://<logindetails>@<repository><:port>/<modulepath>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "svn://<logindetails>@<repository><:port>/<modulepath>"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_domain_name_system_link(self):
+        """Identify dns: as a link."""
+        lines = [
+            "This is a Domain Name System URL pattern:",
+            "dns:example?TYPE=A;CLASS=IN",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "dns:example?TYPE=A;CLASS=IN"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_file_transfer_protocol_link(self):
+        """Identify file://, ftp://, ftps://, and sftp:// as a link."""
+        lines = [
+            "This is a URL pattern for addressing a file:",
+            "file://[host]/path",
+        ]
+        assert docformatter.is_some_sort_of_link(lines) == "file://[host]/path"
+        lines = [
+            "This is a File Transfer Protocol URL pattern:",
+            "ftp://[user[:password]@]host[:port]/url-path",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ftp://[user[:password]@]host[:port]/url-path"
+        )
+        lines = [
+            "This is a Secure File Transfer Protocol URL pattern:",
+            "ftps://[user[:password]@]host[:port]/url-path",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ftps://[user[:password]@]host[:port]/url-path"
+        )
+        lines = [
+            "This is a SSH File Transfer Protocol URL pattern:",
+            "sftp://[<user>[;fingerprint=<host-key fingerprint>]@]<host>["
+            ":<port>]/<path>/<file>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "sftp://[<user>[;fingerprint=<host-key fingerprint>]@]<host>["
+            ":<port>]/<path>/<file>"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_network_command_link(self):
+        """Identify finger://, rsync://, telnet://, and vnc:// as a link."""
+        lines = [
+            "This is a finger protocol URL pattern:",
+            "finger://host[:port][/<request>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "finger://host[:port][/<request>]"
+        )
+        lines = [
+            "This is a remote synchronization URL pattern:",
+            "rsync://<host>[:<port>]/<path>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "rsync://<host>[:<port>]/<path>"
+        )
+        lines = [
+            "This is a telnet URL pattern:",
+            "telnet://<user>:<password>@<host>[:<port>/]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "telnet://<user>:<password>@<host>[:<port>/]"
+        )
+        lines = [
+            "This is a Virtual Network Computing URL pattern:",
+            "vnc://[<host>[:<port>]][?<params>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "vnc://[<host>[:<port>]][?<params>]"
+        )
+        lines = [
+            "This is an eXtensible Resource Identifier URL pattern:",
+            "xri://<authority>[/[<path>]][?<query>][#fragment]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "xri://<authority>[/[<path>]][?<query>][#fragment]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_remote_shell_link(self):
+        """Identify fish:// and ssh:// as a link."""
+        lines = [
+            "This is a fish URL pattern:",
+            "fish://[<username>[:<password>]@]<hostname>[:<port>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "fish://[<username>[:<password>]@]<hostname>[:<port>]"
+        )
+        lines = [
+            "This is a Secure Shell URL pattern:",
+            "ssh://[<user>[;fingerprint=<host-key fingerprint>]@]<host>["
+            ":<port>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ssh://[<user>[;fingerprint=<host-key fingerprint>]@]<host>["
+            ":<port>]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_internet_transfer_protocol_link(self):
+        """Identify dav:, http://, https://, and shttp:// as a link."""
+        lines = [
+            "This is a WebDAV Transfer Protocol URL pattern:",
+            "dav://example.com/directory/subdirectory",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "dav://example.com/directory/subdirectory"
+        )
+        lines = [
+            "This is a Hypertext Transfer Protocol URL pattern:",
+            "http://github.com/PyCQA/docformatter",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "http://github.com/PyCQA/docformatter"
+        )
+        lines = [
+            "This is a Secure Hypertext Transfer Protocol URL pattern:",
+            "https://github.com/PyCQA/docformatter",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "https://github.com/PyCQA/docformatter"
+        )
+        lines = [
+            "This is an obsolete Secure Hypertext Transfer Protocol URL "
+            "pattern:",
+            "shttp://github.com/PyCQA/docformatter",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "shttp://github.com/PyCQA/docformatter"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_mail_link(self):
+        """Identify imap://, mailto:, and pop:// as a link."""
+        lines = [
+            "This is a Internet Message Access Protocol URL pattern:",
+            "imap://[<user>[;AUTH=<type>]@]<host>[:<port>]/<command>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "imap://[<user>[;AUTH=<type>]@]<host>[:<port>]/<command>"
+        )
+        lines = [
+            "This is a Simple Mail Transfer Protocol URL pattern:",
+            "mailto:<address>[?<header1>=<value1>[&<header2>=<value2>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "mailto:<address>[?<header1>=<value1>[&<header2>=<value2>]]"
+        )
+        lines = [
+            "This is a Post Office Protocol URL pattern:",
+            "pop://[<user>[;AUTH=<auth>]@]<host>[:<port>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "pop://[<user>[;AUTH=<auth>]@]<host>[:<port>]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_printer_link(self):
+        """Identify ipp:// and ipps:// as a link."""
+        lines = [
+            "This is a Internet Printing Protocol URL pattern:",
+            "ipp://printer.example.com/ipp/print",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ipp://printer.example.com/ipp/print"
+        )
+        lines = [
+            "This is a Secure Internet Printing Protocol URL pattern:",
+            "ipps://printer2.example.com:443/ipp/print",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ipps://printer2.example.com:443/ipp/print"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_messaging_link(self):
+        """Identify irc://, irc6://, ircs://, sms://, and xmpp:// as a link."""
+        lines = [
+            "This is a Internet Relay Chat URL pattern:",
+            "irc://<host>[:<port>]/[<channel>[?<password>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "irc://<host>[:<port>]/[<channel>[?<password>]]"
+        )
+        lines = [
+            "This is a Internet Relay Chat v6 URL pattern:",
+            "irc6://<host>[:<port>]/[<channel>[?<password>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "irc6://<host>[:<port>]/[<channel>[?<password>]]"
+        )
+        lines = [
+            "This is a Secure Internet Relay Chat URL pattern:",
+            "ircs://<host>[:<port>]/[<channel>[?<password>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ircs://<host>[:<port>]/[<channel>[?<password>]]"
+        )
+        lines = [
+            "This is a Short Message Service URL pattern:",
+            "sms:+15558675309?body=hello%20there",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "sms:+15558675309?body=hello%20there"
+        )
+        lines = [
+            "This is a Extensible Messaging and Presence Protocol URL "
+            "pattern:",
+            "xmpp:[<user>]@<host>[:<port>]/[<resource>][?<query>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "xmpp:[<user>]@<host>[:<port>]/[<resource>][?<query>]"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_directory_access_link(self):
+        """Identify ldap://, ldaps://, and s3:// as a link."""
+        lines = [
+            "This is a Lightweight Directory Access Protocol URL pattern:",
+            "ldap://[<host>[:<port>]][/<dn> [?[<attributes>][?[<scope>][?["
+            "<filter>][?<extensions>]]]]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ldap://[<host>[:<port>]][/<dn> [?[<attributes>][?[<scope>][?["
+            "<filter>][?<extensions>]]]]]"
+        )
+        lines = [
+            "This is a Secure Lightweight Directory Access Protocol URL "
+            "pattern:",
+            "ldaps://[<host>[:<port>]][/<dn> [?[<attributes>][?[<scope>][?["
+            "<filter>][?<extensions>]]]]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "ldaps://[<host>[:<port>]][/<dn> [?[<attributes>][?["
+            "<scope>][?[<filter>][?<extensions>]]]]]"
+        )
+        lines = [
+            "This is an Amazon S3 bucket URL pattern:",
+            "s3://mybucket/puppy.jpg",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "s3://mybucket/puppy.jpg"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_news_link(self):
+        """Identify news: and nntp:// as a link."""
+        lines = [
+            "This is a Usenet newsgroup URL pattern:",
+            "news:<newsgroupname>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines) == "news:<newsgroupname>"
+        )
+        lines = [
+            "This is a Network News Transfer Protocol URL pattern:",
+            "nntp://<host>:<port>/<newsgroup-name>/<article-number>",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "nntp://<host>:<port>/<newsgroup-name>/<article-number>"
+        )
+
+    @pytest.mark.unit
+    def test_is_some_sort_of_initiation_link(self):
+        """Identify sip:, sips:, and snmp:// as a link."""
+        lines = [
+            "This is a Session Initiation Protocol URL pattern:",
+            "sip:<user>[:<password>]@<host>[:<port>][;<uri-parameters>]["
+            "?<headers>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "sip:<user>[:<password>]@<host>[:<port>][;<uri-parameters>]["
+            "?<headers>]"
+        )
+        lines = [
+            "This is a Secure Session Initiation Protocol URL pattern:",
+            "sips:<user>[:<password>]@<host>[:<port>][;<uri-parameters>]["
+            "?<headers>]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "sips:<user>[:<password>]@<host>[:<port>][;<uri-parameters>]["
+            "?<headers>]"
+        )
+        lines = [
+            "This is a Simple Network Management Protocol URL pattern:",
+            "snmp://[user@]host[:port][/[<context>[;<contextEngineID>]]["
+            "/<oid>]]",
+        ]
+        assert (
+            docformatter.is_some_sort_of_link(lines)
+            == "snmp://[user@]host[:port][/[<context>[;<contextEngineID>]]["
+            "/<oid>]]"
+        )
+
+
+class TestIsSomeSortOfList:
     """Class for testing the is_some_sort_of_list() function."""
 
     @pytest.mark.unit