Skip to content

Commit

Permalink
Add support for disabling line-too-long for multilines strings
Browse files Browse the repository at this point in the history
This commit adds support for disabling `line-too-long` messages
for multilines strings such as docstrings.

When a pylint disable pragma is present at the end of the docstring, it is taken
in account for the entire docstring.

Close #2957
  • Loading branch information
hippo91 authored and PCManticore committed Nov 19, 2019
1 parent 0ed3782 commit 9bdae8b
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 103 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ What's New in Pylint 2.5.0?

Release date: TBA

* Don't emit ``line-too-long`` for multilines when a
`pylint:disable=line-too-long` comment stands at their end

Close #2957

* Do not exempt bare except from ``undefined-variable`` and similar checks

If a node was wrapped in a ``TryExcept``, ``pylint`` was taking a hint
Expand Down
2 changes: 1 addition & 1 deletion doc/whatsnew/2.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ New checkers
Other Changes
=============

* Don't emit ``protected-access`` when a single underscore prefixed attribute is used
* Don't emit ``protected-access`` when a single underscore prefixed attribute is used
inside a special method

Close #1802
Expand Down
13 changes: 13 additions & 0 deletions doc/whatsnew/2.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ New checkers
Other Changes
=============

* Don't emit ``line-too-long`` for multilines when a
`pylint:disable=line-too-long` comment stands at their end.

For example the following code will not trigger any ``line-too-long`` message::

def example():
"""
This is a very very very long line within a docstring that should trigger a pylint C0301 error line-too-long

Even spread on multiple lines, the disable command is still effective on very very very, maybe too much long docstring
"""#pylint: disable=line-too-long
pass

* Configuration can be read from a setup.cfg or pyproject.toml file
in the current directory.
A setup.cfg must prepend pylintrc section names with ``pylint.``,
Expand Down
134 changes: 87 additions & 47 deletions pylint/checkers/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import keyword
import tokenize
from functools import reduce # pylint: disable=redefined-builtin
from typing import List

from astroid import nodes

Expand All @@ -56,8 +57,9 @@
is_protocol_class,
node_frame_class,
)
from pylint.constants import OPTION_RGX, WarningScope
from pylint.constants import WarningScope
from pylint.interfaces import IAstroidChecker, IRawChecker, ITokenChecker
from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragma

_ASYNC_TOKEN = "async"
_CONTINUATION_BLOCK_OPENERS = [
Expand Down Expand Up @@ -1243,41 +1245,64 @@ def _check_multi_statement_line(self, node, line):
self.add_message("multiple-statements", node=node)
self._visited_lines[line] = 2

def check_lines(self, lines, i):
"""check lines have less than a maximum number of characters
def check_line_ending(self, line: str, i: int) -> None:
"""
Check that the final newline is not missing and that there is no trailing whitespace.
"""
if not line.endswith("\n"):
self.add_message("missing-final-newline", line=i)
else:
# exclude \f (formfeed) from the rstrip
stripped_line = line.rstrip("\t\n\r\v ")
if not stripped_line and _EMPTY_LINE in self.config.no_space_check:
# allow empty lines
pass
elif line[len(stripped_line) :] not in ("\n", "\r\n"):
self.add_message(
"trailing-whitespace", line=i, col_offset=len(stripped_line)
)

def check_line_length(self, line: str, i: int) -> None:
"""
Check that the line length is less than the authorized value
"""
max_chars = self.config.max_line_length
ignore_long_line = self.config.ignore_long_lines
line = line.rstrip()
if len(line) > max_chars and not ignore_long_line.search(line):
self.add_message("line-too-long", line=i, args=(len(line), max_chars))

def check_line(line, i):
if not line.endswith("\n"):
self.add_message("missing-final-newline", line=i)
else:
# exclude \f (formfeed) from the rstrip
stripped_line = line.rstrip("\t\n\r\v ")
if not stripped_line and _EMPTY_LINE in self.config.no_space_check:
# allow empty lines
pass
elif line[len(stripped_line) :] not in ("\n", "\r\n"):
self.add_message(
"trailing-whitespace", line=i, col_offset=len(stripped_line)
)
# Don't count excess whitespace in the line length.
line = stripped_line
mobj = OPTION_RGX.search(line)
if mobj and "=" in line:
front_of_equal, _, back_of_equal = mobj.group(1).partition("=")
if front_of_equal.strip() == "disable":
if "line-too-long" in {
_msg_id.strip() for _msg_id in back_of_equal.split(",")
}:
return None
line = line.rsplit("#", 1)[0].rstrip()

if len(line) > max_chars and not ignore_long_line.search(line):
self.add_message("line-too-long", line=i, args=(len(line), max_chars))
return i + 1
@staticmethod
def remove_pylint_option_from_lines(options_pattern_obj) -> str:
"""
Remove the `# pylint ...` pattern from lines
"""
lines = options_pattern_obj.string
purged_lines = (
lines[: options_pattern_obj.start(1)].rstrip()
+ lines[options_pattern_obj.end(1) :]
)
return purged_lines

@staticmethod
def is_line_length_check_activated(pylint_pattern_match_object) -> bool:
"""
Return true if the line length check is activated
"""
try:
for pragma in parse_pragma(pylint_pattern_match_object.group(2)):
if pragma.action == "disable" and "line-too-long" in pragma.messages:
return False
except PragmaParserError:
# Printing usefull informations dealing with this error is done in lint.py
pass
return True

@staticmethod
def specific_splitlines(lines: str) -> List[str]:
"""
Split lines according to universal newlines except those in a specific sets
"""
unsplit_ends = {
"\v",
"\x0b",
Expand All @@ -1290,23 +1315,38 @@ def check_line(line, i):
"\u2028",
"\u2029",
}
unsplit = []
for line in lines.splitlines(True):
if line[-1] in unsplit_ends:
unsplit.append(line)
continue

if unsplit:
unsplit.append(line)
line = "".join(unsplit)
unsplit = []

i = check_line(line, i)
if i is None:
break
res = []
buffer = ""
for atomic_line in lines.splitlines(True):
if atomic_line[-1] not in unsplit_ends:
res.append(buffer + atomic_line)
buffer = ""
else:
buffer += atomic_line
return res

if unsplit:
check_line("".join(unsplit), i)
def check_lines(self, lines: str, lineno: int) -> None:
"""
Check lines have :
- a final newline
- no trailing whitespace
- less than a maximum number of characters
"""
#  By default, check the line length
check_l_length = True

# Line length check may be deactivated through `pylint: disable` comment
mobj = OPTION_PO.search(lines)
if mobj:
check_l_length = self.is_line_length_check_activated(mobj)
# The 'pylint: disable whatever' should not be taken into account for line length count
lines = self.remove_pylint_option_from_lines(mobj)

for line in self.specific_splitlines(lines):
if check_l_length:
self.check_line_length(line, lineno)
self.check_line_ending(line, lineno)
lineno += 1

def check_indent_level(self, string, expected, line_num):
"""return the indent level of the string
Expand Down
18 changes: 14 additions & 4 deletions pylint/checkers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
import tokenize

from pylint.checkers import BaseChecker
from pylint.constants import OPTION_RGX
from pylint.interfaces import IRawChecker, ITokenChecker
from pylint.message import MessagesHandlerMixIn
from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragma


class ByIdManagedMessagesChecker(BaseChecker):
Expand Down Expand Up @@ -138,11 +138,21 @@ def process_tokens(self, tokens):
comment_text = comment.string[1:].lstrip() # trim '#' and whitespaces

# handle pylint disable clauses
disable_option_match = OPTION_RGX.search(comment_text)
disable_option_match = OPTION_PO.search(comment_text)
if disable_option_match:
try:
_, value = disable_option_match.group(1).split("=", 1)
values = [_val.strip().upper() for _val in value.split(",")]
values = []
try:
for pragma_repr in (
p_rep
for p_rep in parse_pragma(disable_option_match.group(2))
if p_rep.action == "disable"
):
values.extend(pragma_repr.messages)
except PragmaParserError:
# Printing usefull informations dealing with this error is done in lint.py
pass
values = [_val.upper() for _val in values]
if set(values) & set(self.config.notes):
continue
except ValueError:
Expand Down
103 changes: 55 additions & 48 deletions pylint/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,16 @@

from pylint import __pkginfo__, checkers, config, exceptions, interfaces, reporters
from pylint.__pkginfo__ import version
from pylint.constants import MAIN_CHECKER_NAME, MSG_TYPES, OPTION_RGX
from pylint.constants import MAIN_CHECKER_NAME, MSG_TYPES
from pylint.message import Message, MessageDefinitionStore, MessagesHandlerMixIn
from pylint.reporters.ureports import nodes as report_nodes
from pylint.utils import ASTWalker, FileState, utils
from pylint.utils.pragma_parser import (
OPTION_PO,
InvalidPragmaError,
UnRecognizedOptionError,
parse_pragma,
)

try:
import multiprocessing
Expand Down Expand Up @@ -803,50 +809,41 @@ def process_tokens(self, tokens):

if tok_type != tokenize.COMMENT:
continue
match = OPTION_RGX.search(content)
match = OPTION_PO.search(content)
if match is None:
continue

first_group = match.group(1)
if (
first_group.strip() == "disable-all"
or first_group.strip() == "skip-file"
):
if first_group.strip() == "disable-all":
self.add_message(
"deprecated-pragma",
line=start[0],
args=("disable-all", "skip-file"),
)
self.add_message("file-ignored", line=start[0])
self._ignore_file = True
return
try:
opt, value = first_group.split("=", 1)
except ValueError:
self.add_message(
"bad-inline-option", args=first_group.strip(), line=start[0]
)
continue
opt = opt.strip()
if opt in self._options_methods or opt in self._bw_options_methods:
try:
meth = self._options_methods[opt]
except KeyError:
meth = self._bw_options_methods[opt]
# found a "(dis|en)able-msg" pragma deprecated suppression
self.add_message(
"deprecated-pragma",
line=start[0],
args=(opt, opt.replace("-msg", "")),
)
for msgid in utils._splitstrip(value):
# Add the line where a control pragma was encountered.
if opt in control_pragmas:
self._pragma_lineno[msgid] = start[0]

for pragma_repr in parse_pragma(match.group(2)):
if pragma_repr.action in ("disable-all", "skip-file"):
if pragma_repr.action == "disable-all":
self.add_message(
"deprecated-pragma",
line=start[0],
args=("disable-all", "skip-file"),
)
self.add_message("file-ignored", line=start[0])
self._ignore_file = True
return
try:
if (opt, msgid) == ("disable", "all"):
meth = self._options_methods[pragma_repr.action]
except KeyError:
meth = self._bw_options_methods[pragma_repr.action]
# found a "(dis|en)able-msg" pragma deprecated suppression
self.add_message(
"deprecated-pragma",
line=start[0],
args=(
pragma_repr.action,
pragma_repr.action.replace("-msg", ""),
),
)
for msgid in pragma_repr.messages:
# Add the line where a control pragma was encountered.
if pragma_repr.action in control_pragmas:
self._pragma_lineno[msgid] = start[0]

if (pragma_repr.action, msgid) == ("disable", "all"):
self.add_message(
"deprecated-pragma",
line=start[0],
Expand All @@ -855,15 +852,25 @@ def process_tokens(self, tokens):
self.add_message("file-ignored", line=start[0])
self._ignore_file = True
return
# If we did not see a newline between the previous line and now,
# we saw a backslash so treat the two lines as one.
# If we did not see a newline between the previous line and now,
# we saw a backslash so treat the two lines as one.
l_start = start[0]
if not saw_newline:
meth(msgid, "module", start[0] - 1)
meth(msgid, "module", start[0])
except exceptions.UnknownMessageError:
self.add_message("bad-option-value", args=msgid, line=start[0])
else:
self.add_message("unrecognized-inline-option", args=opt, line=start[0])
l_start -= 1
try:
meth(msgid, "module", l_start)
except exceptions.UnknownMessageError:
self.add_message(
"bad-option-value", args=msgid, line=start[0]
)
except UnRecognizedOptionError as err:
self.add_message(
"unrecognized-inline-option", args=err.token, line=start[0]
)
continue
except InvalidPragmaError as err:
self.add_message("bad-inline-option", args=err.token, line=start[0])
continue

# code checking methods ###################################################

Expand Down

0 comments on commit 9bdae8b

Please sign in to comment.