Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: don't wrap URLs #115

Merged
merged 13 commits into from Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 47 additions & 19 deletions .github/workflows/ci.yml
Expand Up @@ -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
base-path: .tox
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -16,3 +16,4 @@ htmlcov/
poetry.lock
.idea/
.vscode/
.tox/
205 changes: 108 additions & 97 deletions docformatter.py
Expand Up @@ -50,7 +50,7 @@

# 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
Expand Down Expand Up @@ -171,8 +171,7 @@ def do_parse_arguments(self) -> None:
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",
Expand Down Expand Up @@ -258,13 +257,19 @@ def do_parse_arguments(self) -> None:
"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:])
Expand Down Expand Up @@ -324,7 +329,11 @@ def _do_read_parser_configuration(self) -> None:
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)
Expand Down Expand Up @@ -834,81 +843,6 @@ def do_open_with_encoding(self, filename: str, mode: str = "r"):
) # 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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions docs/source/configuration.rst
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/source/requirements.rst
Expand Up @@ -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'
Expand Down