Repository: PyCQA/docformatter (python)

Issue:
Breaking long line does not preserve links
Hi, first, for this nice tool!
We crossed some difficulty with long links which are split into multiple lines (when a line is over 120 chars) but then it is not valid link anymore, for example this one:
```
`Area Under the Receiver Operating Characteristic Curve (ROC AUC)
    <https://en.wikipedia.org/wiki/Receiver_operating_characteristic#Further_interpretations>`_.
```
is there some solution for such a case? for example, do when the link is too long start automatically on a new line and even the line is too long, do not break it...

Pull request description:
fix: don't wrap URLs
Adds function and associated tests to reconnect URLs that are broken into parts when docstrings are wrapped.

Closes #75

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4f91773..b725ea4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,35 +11,63 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"]
+        python-version:
+          - "pypy-3.6-v7.3.3"
+          - "3.10"
+          - "3.9"
+          - "3.8"
+          - "3.7"
+          - "3.6"
         os: [ubuntu-latest]
     runs-on: ${{ matrix.os }}
     name: "${{ matrix.os }} Python: ${{ matrix.python-version }}"
     steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install -U pip
-        python -m pip install .[tomli]
-        python -m pip install --quiet coverage coveralls pytest pytest-cov mock tox
-    - name: Run tests with tox
-      run: |
-        tox -e py
-    - name: Create Coveralls report
-      uses: miurahr/coveralls-python-action@patch-pyprject-toml
-      with:
-        parallel: true
+      - name: Setup Python for tox
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
+      - name: Install tox
+        run: python -m pip install tox
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Set up Python ${{ matrix.python-version }} for test
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Setup test suite
+        run: tox -vv --notest
+      - name: Run tests with tox
+        run: tox -e py --skip-pkg-install
+      - name: Upload coverage data
+        uses: actions/upload-artifact@v3
+        with:
+          name: coverage-data
+          path: ".tox/.coverage.*"
 
   upload_coveralls:
     name: Upload Results to Coveralls
     needs: test
     runs-on: ubuntu-latest
     steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
+      - name: Install tox
+        run: python -m pip install tox
+      - name: Setup coverage
+        run: tox -e coverage --notest
+      - name: Download coverage data
+        uses: actions/download-artifact@v3
+        with:
+          name: coverage-data
+          path: .tox
+      - name: Combine coverage reports
+        run: tox -e coverage
       - name: Upload coverage report to Coveralls
         uses: miurahr/coveralls-python-action@patch-pyprject-toml
         with:
-          parallel-finished: true
\ No newline at end of file
+          base-path: .tox
diff --git a/.gitignore b/.gitignore
index 51246d6..edd9153 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ htmlcov/
 poetry.lock
 .idea/
 .vscode/
+.tox/
diff --git a/docformatter.py b/docformatter.py
index 13c3355..2c07300 100755
--- a/docformatter.py
+++ b/docformatter.py
@@ -50,7 +50,7 @@ from typing import Dict, List, TextIO, Tuple, Union
 
 # Third Party Imports
 import untokenize  # type: ignore
-from charset_normalizer import from_path
+from charset_normalizer import from_path  # pylint: disable=import-error
 
 try:
     # Third Party Imports
@@ -171,8 +171,7 @@ class Configurator:
             type=int,
             metavar="length",
             help="wrap descriptions at this length; "
-            "set to 0 to disable wrapping "
-            "(default: 72)",
+            "set to 0 to disable wrapping (default: 72)",
         )
         self.parser.add_argument(
             "--force-wrap",
@@ -258,13 +257,19 @@ class Configurator:
             "issue #67) (default: False)",
         )
         self.parser.add_argument(
-            "--config", help="path to file containing docformatter options"
+            "--config",
+            default=self.config_file,
+            help="path to file containing docformatter options",
         )
         self.parser.add_argument(
-            "--version", action="version", version=f"%(prog)s {__version__}"
+            "--version",
+            action="version",
+            version=f"%(prog)s {__version__}",
         )
         self.parser.add_argument(
-            "files", nargs="+", help="files to format or '-' for standard in"
+            "files",
+            nargs="+",
+            help="files to format or '-' for standard in",
         )
 
         self.args = self.parser.parse_args(self.args_lst[1:])
@@ -324,7 +329,11 @@ class Configurator:
         config = ConfigParser()
         config.read(self.config_file)
 
-        for _section in ["tool:docformatter", "docformatter"]:
+        for _section in [
+            "tool.docformatter",
+            "tool:docformatter",
+            "docformatter",
+        ]:
             if _section in config.sections():
                 self.flargs_dct = {
                     k: v if isinstance(v, list) else str(v)
@@ -834,81 +843,6 @@ class Encodor:
         )  # Preserve line endings
 
 
-class Encodor:
-    """Encoding and decoding of files."""
-
-    CR = "\r"
-    LF = "\n"
-    CRLF = "\r\n"
-
-    def __init__(self):
-        """Initialize an Encodor instance."""
-        self.encoding = "latin-1"
-        self.system_encoding = (
-            locale.getpreferredencoding() or sys.getdefaultencoding()
-        )
-
-    def do_detect_encoding(self, filename: str) -> None:
-        """Return the detected file encoding.
-
-        Parameters
-        ----------
-        filename : str
-            The full path name of the file whose encoding is to be detected.
-        """
-        try:
-            self.encoding = from_path(filename).best().encoding
-
-            # Check for correctness of encoding.
-            with self.do_open_with_encoding(filename) as check_file:
-                check_file.read()
-        except (SyntaxError, LookupError, UnicodeDecodeError):
-            self.encoding = "latin-1"
-
-    def do_find_newline(self, source: str) -> Dict[int, int]:
-        """Return type of newline used in source.
-
-        Paramaters
-        ----------
-        source : list
-            A list of lines.
-
-        Returns
-        -------
-        counter : dict
-            A dict with the count of new line types found.
-        """
-        assert not isinstance(source, unicode)
-
-        counter = collections.defaultdict(int)
-        for line in source:
-            if line.endswith(self.CRLF):
-                counter[self.CRLF] += 1
-            elif line.endswith(self.CR):
-                counter[self.CR] += 1
-            elif line.endswith(self.LF):
-                counter[self.LF] += 1
-
-        return (sorted(counter, key=counter.get, reverse=True) or [self.LF])[0]
-
-    def do_open_with_encoding(self, filename: str, mode: str = "r"):
-        """Return opened file with a specific encoding.
-
-        Parameters
-        ----------
-        filename : str
-            The full path name of the file to open.
-        mode : str
-            The mode to open the file in.  Defaults to read-only.
-
-        Returns
-        -------
-        """
-        return io.open(
-            filename, mode=mode, encoding=self.encoding, newline=""
-        )  # Preserve line endings
-
-
 def has_correct_length(length_range, start, end):
     """Return True if docstring's length is in range."""
     if length_range is None:
@@ -943,12 +877,97 @@ def is_probably_beginning_of_sentence(line):
     return is_beginning_of_sentence and not is_pydoc_ref
 
 
-def is_some_sort_of_code(text):
+def is_some_sort_of_code(text: str) -> bool:
     """Return True if text looks like code."""
-    return any(len(word) > 50 for word in text.split())
+    return any(
+        len(word) > 50
+        and not re.match(r"<{0,1}(http:|https:|ftp:|sftp:)", word)
+        for word in text.split()
+    )
+
+
+def do_preserve_links(
+    text: str,
+    indentation: str,
+    wrap_length: int,
+) -> List[str]:
+    """Rebuild links in docstring.
+
+    Parameters
+    ----------
+    text : str
+        The docstring description.
+    indentation : str
+        The indentation (number of spaces or tabs) to place in front of each
+        line.
+    wrap_length : int
+        The column to wrap each line at.
+
+    Returns
+    -------
+    lines : list
+        A list containing each line of the description with any links put
+        back together.
+    """
+    lines = textwrap.wrap(
+        textwrap.dedent(text),
+        width=wrap_length,
+        initial_indent=indentation,
+        subsequent_indent=indentation,
+    )
+
+    url = next(
+        (
+            line
+            for line in lines
+            if re.search(r"<?(http://|https://|ftp://|sftp://)", line)
+        ),
+        "",
+    )
+
+    if url != "":
+        url_idx = lines.index(url)
+
+        # Is this an in-line link (i.e., enclosed in <>)?  We want to keep
+        # the '<' and '>' part of the link.
+        if re.search(r"<", url):
+            lines[url_idx] = f"{indentation}" + url.split(sep="<")[0].strip()
+            url = f"{indentation}<" + url.split(sep="<")[1]
+            url = url + lines[url_idx + 1].strip()
+            lines[url_idx + 1] = url
+        # Is this a link target definition (i.e., .. a link: https://)?  We
+        # want to keep the .. a link: on the same line as the url.
+        elif re.search(r"(\.\. )", url):
+            url = url + lines[url_idx + 1].strip()
+            lines[url_idx] = url
+            lines.pop(url_idx + 1)
+        # Is this a simple link (i.e., just a link in the text) that should
+        # be unwrapped?  We want to break the url out from the rest of the
+        # text.
+        elif len(lines[url_idx]) >= wrap_length:
+            lines[url_idx] = (
+                f"{indentation}" + url.strip().split(sep=" ")[0].strip()
+            )
+            url = f"{indentation}" + url.strip().split(sep=" ")[1].strip()
+            url = url + lines[url_idx + 1].strip().split(sep=" ")[0].strip()
+            lines.append(
+                indentation
+                + " ".join(lines[url_idx + 1].strip().split(sep=" ")[1:])
+            )
+            lines[url_idx + 1] = url
+
+        with contextlib.suppress(IndexError):
+            if lines[url_idx + 2].strip() in [".", "?", "!", ";"] or re.search(
+                r">", lines[url_idx + 2]
+            ):
+                url = url + lines[url_idx + 2].strip()
+                lines[url_idx + 1] = url
+                lines.pop(url_idx + 2)
+
+    return lines
 
 
-def is_some_sort_of_list(text, strict):
+def is_some_sort_of_list(text, strict) -> bool:
     """Return True if text looks like a list."""
     split_lines = text.rstrip().splitlines()
 
@@ -1161,21 +1180,13 @@ def wrap_description(text, indentation, wrap_length, force_wrap, strict):
     # Ignore possibly complicated cases.
     if wrap_length <= 0 or (
         not force_wrap
-        and (is_some_sort_of_list(text, strict) or is_some_sort_of_code(text))
+        and (is_some_sort_of_code(text) or is_some_sort_of_list(text, strict))
     ):
         return text
 
-    return (
-        indentation
-        + "\n".join(
-            textwrap.wrap(
-                textwrap.dedent(text),
-                width=wrap_length,
-                initial_indent=indentation,
-                subsequent_indent=indentation,
-            )
-        ).strip()
-    )
+    text = do_preserve_links(text, indentation, wrap_length)
+
+    return indentation + "\n".join(text).strip()
 
 
 def remove_section_header(text):
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index f98139a..e50922f 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -15,7 +15,7 @@ If no configuration file is explicitly passed, ``docformatter`` will search
 the current directory for the supported files and use the first one found.
 The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``.
 
-In ``pyproject.toml`` or ``tox.ini``, add a section ``[tool.docformatter]`` with
+In ``pyproject.toml``, add a section ``[tool.docformatter]`` with
 options listed using the same name as command line argument.  For example:
 
 .. code-block:: yaml
@@ -25,7 +25,7 @@ options listed using the same name as command line argument.  For example:
       wrap-summaries = 82
       blank = true
 
-In ``setup.cfg``, add a ``[docformatter]`` section.
+In ``setup.cfg`` or ``tox.ini``, add a ``[docformatter]`` section.
 
 .. code-block:: yaml
 
diff --git a/docs/source/requirements.rst b/docs/source/requirements.rst
index 157bae4..351cfd0 100644
--- a/docs/source/requirements.rst
+++ b/docs/source/requirements.rst
@@ -194,6 +194,7 @@ the requirement falls in, the type of requirement, and whether
     ' docformatter_10.1.1', ' Shall not wrap lists or syntax directive statements', ' Derived', ' Shall', ' Yes'
     ' docformatter_10.1.1.1', ' Should allow wrapping of lists and syntax directive statements.', ' Stakeholder', ' Should', ' Yes [*PR #5*, *PR #93*]'
     ' docformatter_10.1.2', ' Should allow/disallow wrapping of one-line docstrings.', ' Derived', ' Should', ' No'
+    ' docformatter_10.1.3', ' Shall not wrap links that exceed the wrap length.', Derived', ' Shall', ' Yes [*PR #114*]'
     ' docformatter_10.2', ' Should format docstrings using NumPy style.', ' Style', ' Should', ' No'
     ' docformatter_10.3', ' Should format docstrings using Google style.', ' Style', ' Should', ' No'
     ' docformatter_10.4', ' Should format docstrings using Sphinx style.',' Style', ' Should', ' No'
diff --git a/pyproject.toml b/pyproject.toml
index 799f8b9..072372f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,7 @@ pylint = "^2.12.0"
 pytest = "<7.0.0"
 pytest-cov = "^3.0.0"
 rstcheck = "<6.0.0"
+tox = "^3.25.0"
 Sphinx = "^5.0.0"
 twine = [
     {version="<4.0.0", python = "<3.7"},
@@ -170,30 +171,57 @@ line_length = 79
 [tool.tox]
 legacy_tox_ini = """
 [tox]
-skipsdist = True
-isolated_build = True
 envlist =
-    py{36,37,38,39,310,py36},
+    py36
+    py37
+    py38
+    py39
+    py310
+    pypy3
+    coverage
     style
+isolated_build = true
+skip_missing_interpreters = true
+skipsdist = true
 
-[testenv:py{36,37,38,39,310,py36}]
+[testenv]
+description = run the test suite using pytest under {basepython}
 deps =
-    coverage
+    charset_normalizer
+    coverage[toml]
     mock
     pytest
     pytest-cov
+    tomli
+    untokenize
 setenv =
-    COVERAGE_FILE={toxinidir}/.coverage
+    COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
+commands =
+    pytest -s -x -c ./pyproject.toml \
+        --cache-clear \
+        --cov=docformatter \
+        --cov-config={toxinidir}/pyproject.toml \
+        --cov-branch \
+        tests/
+
+[testenv:coverage]
+description = combine coverage data and create report
+setenv =
+    COVERAGE_FILE = {toxworkdir}/.coverage
+skip_install = true
+deps =
+    coverage[toml]
+parallel_show_output = true
 commands =
-    pip install -U pip
-    pip install .[tomli]
-    pytest -s -x -c ./pyproject.toml --cache-clear \
-        --cov-config=pyproject.toml --cov=docformatter \
-        --cov-branch --cov-report=term tests/
-    coverage xml --rcfile=pyproject.toml
+    coverage combine
+    coverage report -m
+    coverage xml -o {toxworkdir}/coverage.xml
+depends = py36, py37, py38, py39, py310, pypy3
 
 [testenv:style]
+description = run autoformatters and style checkers
 deps =
+    charset_normalizer
     docformatter
     pycodestyle
     pydocstyle


Test patch:
diff --git a/tests/test_configuration_functions.py b/tests/test_configuration_functions.py
index 410a1f8..8c4cd8e 100644
--- a/tests/test_configuration_functions.py
+++ b/tests/test_configuration_functions.py
@@ -51,45 +51,61 @@ class TestConfigurator:
     @pytest.mark.unit
     def test_initialize_configurator_with_default(self):
         """Return a Configurator() instance using default pyproject.toml."""
-        unit_under_test = Configurator([])
+        argb = [
+            "/path/to/docformatter",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.config_file == "./pyproject.toml"
+        assert uut.args_lst == argb
+        assert uut.config_file == "./pyproject.toml"
 
     @pytest.mark.unit
     def test_initialize_configurator_with_pyproject_toml(self):
         """Return a Configurator() instance loaded from a pyproject.toml."""
         argb = [
+            "/path/to/docformatter",
+            "-c",
             "--config",
             "./tests/_data/pyproject.toml",
+            "",
         ]
 
-        unit_under_test = Configurator(argb)
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.args is None
-        assert unit_under_test.flargs_dct == {
-            "recursive": "True",
-            "wrap-summaries": "82",
-        }
-        assert unit_under_test.configuration_file_lst == [
+        assert uut.args.check
+        assert not uut.args.in_place
+        assert uut.args_lst == argb
+        assert uut.config_file == "./tests/_data/pyproject.toml"
+        assert uut.configuration_file_lst == [
             "pyproject.toml",
             "setup.cfg",
             "tox.ini",
         ]
-        assert unit_under_test.args_lst == argb
-        assert unit_under_test.config_file == "./tests/_data/pyproject.toml"
+        assert uut.flargs_dct == {
+            "recursive": "True",
+            "wrap-summaries": "82",
+        }
 
     @pytest.mark.unit
     def test_initialize_configurator_with_setup_cfg(self):
         """Return docformatter configuration loaded from a setup.cfg file."""
         argb = [
+            "/path/to/docformatter",
+            "-c",
             "--config",
             "./tests/_data/setup.cfg",
+            "",
         ]
 
-        unit_under_test = Configurator(argb)
-        unit_under_test.do_parse_arguments()
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.flargs_dct == {
+        assert uut.config_file == "./tests/_data/setup.cfg"
+        assert uut.flargs_dct == {
             "blank": "False",
             "wrap-summaries": "79",
             "wrap-descriptions": "72",
@@ -99,14 +115,18 @@ class TestConfigurator:
     def test_initialize_configurator_with_tox_ini(self):
         """Return docformatter configuration loaded from a tox.ini file."""
         argb = [
+            "/path/to/docformatter",
+            "-c",
             "--config",
             "./tests/_data/tox.ini",
+            "",
         ]
 
-        unit_under_test = Configurator(argb)
-        unit_under_test.do_parse_arguments()
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.flargs_dct == {
+        assert uut.config_file == "./tests/_data/tox.ini"
+        assert uut.flargs_dct == {
             "blank": "False",
             "wrap-summaries": "79",
             "wrap-descriptions": "72",
@@ -116,46 +136,177 @@ class TestConfigurator:
     def test_unsupported_config_file(self):
         """Return empty configuration dict when file is unsupported."""
         argb = [
+            "/path/to/docformatter",
+            "-c",
             "--config",
             "./tests/conf.py",
+            "",
         ]
 
-        unit_under_test = Configurator(argb)
-        unit_under_test.do_parse_arguments()
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.flargs_dct == {}
+        assert uut.config_file == "./tests/conf.py"
+        assert uut.flargs_dct == {}
 
     @pytest.mark.unit
     def test_cli_override_config_file(self):
         """Command line arguments override configuration file options."""
         argb = [
+            "/path/to/docformatter",
+            "-c",
             "--config",
             "./tests/_data/tox.ini",
             "--make-summary-multi-line",
             "--blank",
             "--wrap-summaries",
             "88",
+            "",
         ]
 
-        unit_under_test = Configurator(argb)
-        unit_under_test.do_parse_arguments()
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
 
-        assert unit_under_test.flargs_dct == {
+        assert uut.config_file == "./tests/_data/tox.ini"
+        assert uut.flargs_dct == {
             "blank": "False",
             "wrap-descriptions": "72",
             "wrap-summaries": "79",
         }
-        assert not unit_under_test.args.in_place
-        assert not unit_under_test.args.check
-        assert not unit_under_test.args.recursive
-        assert unit_under_test.args.exclude is None
-        assert unit_under_test.args.wrap_summaries == 88
-        assert unit_under_test.args.wrap_descriptions == 72
-        assert unit_under_test.args.post_description_blank
-        assert not unit_under_test.args.pre_summary_newline
-        assert not unit_under_test.args.pre_summary_space
-        assert unit_under_test.args.make_summary_multi_line
-        assert not unit_under_test.args.force_wrap
-        assert unit_under_test.args.line_range is None
-        assert unit_under_test.args.length_range is None
-        assert not unit_under_test.args.non_strict
+        assert not uut.args.in_place
+        assert uut.args.check
+        assert not uut.args.recursive
+        assert uut.args.exclude is None
+        assert uut.args.wrap_summaries == 88
+        assert uut.args.wrap_descriptions == 72
+        assert uut.args.post_description_blank
+        assert not uut.args.pre_summary_newline
+        assert not uut.args.pre_summary_space
+        assert uut.args.make_summary_multi_line
+        assert not uut.args.force_wrap
+        assert uut.args.line_range is None
+        assert uut.args.length_range is None
+        assert not uut.args.non_strict
+
+    @pytest.mark.unit
+    def test_only_format_in_line_range(self, capsys):
+        """Only format docstrings in line range."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--range",
+            "1",
+            "3",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
+
+        assert uut.args.line_range == [1, 3]
+
+    @pytest.mark.unit
+    def test_low_line_range_is_zero(self, capsys):
+        """Raise parser error if the first value for the range is zero."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--range",
+            "0",
+            "10",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        with pytest.raises(SystemExit):
+            uut.do_parse_arguments()
+
+        out, err = capsys.readouterr()
+        assert out == ""
+        assert "--range must be positive numbers" in err
+
+    @pytest.mark.unit
+    def test_low_line_range_greater_than_high_line_range(self, capsys):
+        """Raise parser error if the first value for the range is greater than
+        the second."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--range",
+            "10",
+            "1",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        with pytest.raises(SystemExit):
+            uut.do_parse_arguments()
+
+        out, err = capsys.readouterr()
+        assert out == ""
+        assert (
+            "First value of --range should be less than or equal to the second"
+            in err
+        )
+
+    @pytest.mark.unit
+    def test_only_format_in_length_range(self, capsys):
+        """Only format docstrings in length range."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--docstring-length",
+            "25",
+            "55",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        uut.do_parse_arguments()
+
+        assert uut.args.length_range == [25, 55]
+
+    @pytest.mark.unit
+    def test_low_length_range_is_zero(self, capsys):
+        """Raise parser error if the first value for the length range is
+        zero."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--docstring-length",
+            "0",
+            "10",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        with pytest.raises(SystemExit):
+            uut.do_parse_arguments()
+
+        out, err = capsys.readouterr()
+        assert out == ""
+        assert "--docstring-length must be positive numbers" in err
+
+    @pytest.mark.unit
+    def test_low_length_range_greater_than_high_length_range(self, capsys):
+        """Raise parser error if the first value for the range is greater than
+        the second."""
+        argb = [
+            "/path/to/docformatter",
+            "-c",
+            "--docstring-length",
+            "55",
+            "25",
+            "",
+        ]
+
+        uut = Configurator(argb)
+        with pytest.raises(SystemExit):
+            uut.do_parse_arguments()
+
+        out, err = capsys.readouterr()
+        assert out == ""
+        assert (
+            "First value of --docstring-length should be less than or equal "
+            "to the second" in err
+        )
diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py
index 476e2e8..f9c449d 100644
--- a/tests/test_format_docstring.py
+++ b/tests/test_format_docstring.py
@@ -662,15 +662,7 @@ num_iterations is the number of updates - instead of a better definition of conv
     @pytest.mark.unit
     @pytest.mark.parametrize(
         "args",
-        [
-            [
-                "--wrap-summaries",
-                "30",
-                "--tab-width",
-                "4",
-                "",
-            ]
-        ],
+        [["--wrap-summaries", "30", "--tab-width", "4", ""]],
     )
     def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation(
         self,
@@ -701,14 +693,7 @@ num_iterations is the number of updates - instead of a better definition of conv
     @pytest.mark.unit
     @pytest.mark.parametrize(
         "args",
-        [
-            [
-                "--wrap-summaries",
-                "69",
-                "--close-quotes-on-newline",
-                "",
-            ]
-        ],
+        [["--wrap-summaries", "69", "--close-quotes-on-newline", ""]],
     )
     def test_format_docstring_for_multi_line_summary_alone(
         self,
@@ -742,14 +727,7 @@ num_iterations is the number of updates - instead of a better definition of conv
     @pytest.mark.unit
     @pytest.mark.parametrize(
         "args",
-        [
-            [
-                "--wrap-summaries",
-                "88",
-                "--close-quotes-on-newline",
-                "",
-            ]
-        ],
+        [["--wrap-summaries", "88", "--close-quotes-on-newline", ""]],
     )
     def test_format_docstring_for_one_line_summary_alone_but_too_long(
         self,
@@ -773,12 +751,165 @@ num_iterations is the number of updates - instead of a better definition of conv
             == uut._do_format_docstring(
                 INDENTATION,
                 '''\
-"""This one-line docstring will not be wrapped and quotes will be 
-in-line."""\
+"""This one-line docstring will not be wrapped and quotes will be in-line."""\
 ''',
             )
         )
 
+    @pytest.mark.unit
+    @pytest.mark.parametrize(
+        "args",
+        [["--wrap-descriptions", "72", ""]],
+    )
+    def test_format_docstring_with_inline_links(
+        self,
+        test_args,
+        args,
+    ):
+        """Preserve links instead of wrapping them.
+
+        See issue #75. See requirement docformatter_10.1.3.
+        """
+        uut = Formator(
+            test_args,
+            sys.stderr,
+            sys.stdin,
+            sys.stdout,
+        )
+
+        docstring = '''\
+"""This is a docstring with a link.
+
+    Here is an elaborate description containing a link.
+    `Area Under the Receiver Operating Characteristic Curve (ROC AUC)
+        <https://en.wikipedia.org/wiki/Receiver_operating_characteristic#Further_interpretations>`_.
+    """\
+'''
+
+        assert '''\
+"""This is a docstring with a link.
+
+    Here is an elaborate description containing a link. `Area Under the
+    Receiver Operating Characteristic Curve (ROC AUC)
+    <https://en.wikipedia.org/wiki/Receiver_operating_characteristic#Further_interpretations>`_.
+    """\
+''' == uut._do_format_docstring(
+            INDENTATION, docstring.strip()
+        )
+
+    @pytest.mark.unit
+    @pytest.mark.parametrize(
+        "args",
+        [["--wrap-descriptions", "72", ""]],
+    )
+    def test_format_docstring_with_target_links(
+        self,
+        test_args,
+        args,
+    ):
+        """Preserve links instead of wrapping them.
+
+        See issue #75. See requirement docformatter_10.1.3.
+        """
+        uut = Formator(
+            test_args,
+            sys.stderr,
+            sys.stdin,
+            sys.stdout,
+        )
+
+        docstring = '''\
+"""This is another docstring with `a link`_.
+
+    .. a link: http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html.
+    """\
+'''
+
+        assert '''\
+"""This is another docstring with `a link`_.
+
+    .. a link: http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html.
+    """\
+''' == uut._do_format_docstring(
+            INDENTATION, docstring.strip()
+        )
+
+    @pytest.mark.unit
+    @pytest.mark.parametrize(
+        "args",
+        [["--wrap-descriptions", "72", ""]],
+    )
+    def test_format_docstring_with_sinmple_link(
+        self,
+        test_args,
+        args,
+    ):
+        """Preserve links instead of wrapping them.
+
+        See issue #75. See requirement docformatter_10.1.3.
+        """
+        uut = Formator(
+            test_args,
+            sys.stderr,
+            sys.stdin,
+            sys.stdout,
+        )
+
+        docstring = '''\
+"""This is another docstring with a link.
+
+    See http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for additional information.
+    """\
+'''
+
+        assert '''\
+"""This is another docstring with a link.
+
+    See
+    http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html
+    for additional information.
+    """\
+''' == uut._do_format_docstring(
+            INDENTATION, docstring.strip()
+        )
+
+    @pytest.mark.unit
+    @pytest.mark.parametrize(
+        "args",
+        [["--wrap-descriptions", "72", ""]],
+    )
+    def test_format_docstring_with_short_link(
+        self,
+        test_args,
+        args,
+    ):
+        """Short links will remain untouched.
+
+        See issue #75. See requirement docformatter_10.1.3.
+        """
+        uut = Formator(
+            test_args,
+            sys.stderr,
+            sys.stdin,
+            sys.stdout,
+        )
+
+        docstring = '''\
+"""This is yanf with a short link.
+
+    See http://www.reliaqual.com for examples.
+    """\
+'''
+
+        assert '''\
+"""This is yanf with a short link.
+
+    See http://www.reliaqual.com for examples.
+    """\
+''' == uut._do_format_docstring(
+            INDENTATION, docstring.strip()
+        )
+
 
 class TestFormatStyleOptions:
     """Class for testing format_docstring() when requesting style options."""
@@ -1051,8 +1182,8 @@ class TestStripDocstring:
     ):
         """Raise ValueError when strings begin with single single quotes.
 
-        See requirement PEP_257_1.  See issue #66 for example of docformatter
-        breaking code when encountering single quote.
+        See requirement PEP_257_1.  See issue #66 for example of
+        docformatter breaking code when encountering single quote.
         """
         uut = Formator(
             test_args,
@@ -1073,8 +1204,8 @@ class TestStripDocstring:
     ):
         """Raise ValueError when strings begin with single double quotes.
 
-        See requirement PEP_257_1.  See issue #66 for example of docformatter
-        breaking code when encountering single quote.
+        See requirement PEP_257_1.  See issue #66 for example of
+        docformatter breaking code when encountering single quote.
         """
         uut = Formator(
             test_args,