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 #7868: Added lint check for a newline above args in python doc string #7929

Merged
merged 5 commits into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Added lint check for newline above args
  • Loading branch information
Hudda committed Nov 6, 2019
commit 51469947cde995eea4c0eb5723fa57f5762fd217
66 changes: 64 additions & 2 deletions scripts/pylint_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,6 @@ def check_single_constructor_params(self, class_doc, init_doc, class_node):
"""Checks whether a class and corresponding init() method are
documented. If both of them are documented, it adds an error message.


Args:
class_doc: Docstring. Pylint docstring class instance representing
a class's docstring.
Expand All @@ -658,6 +657,7 @@ def check_single_constructor_params(self, class_doc, init_doc, class_node):
def _handle_no_raise_doc(self, excs, node):
"""Checks whether the raised exception in a function has been
documented, add a message otherwise.

Args:
excs: list(str). A list of exception types.
node: astroid.scoped_nodes.Function. Node to access module content.
Expand Down Expand Up @@ -1010,7 +1010,6 @@ def process_module(self, node):

if file_content[line_num] == b'\n':
blank_line_counter += 1

else:
blank_line_counter = 0

Expand All @@ -1020,6 +1019,68 @@ def process_module(self, node):
self.add_message('excessive-new-lines', line=line_num + 1)


class SingleNewlineAboveArgsChecker(checkers.BaseChecker):
"""Checker for single space above args in python doc string."""

__implements__ = interfaces.IRawChecker
name = 'single-space-above-args'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single-space-above-args-raises-returns

priority = -1
msgs = {
'C0012': (
'Files must have a single newline above args in doc string.',
'single-space-above-args',
'Please enter a single newline above doc string.'
)
}

def process_module(self, node):
"""Process a module to ensure that there is a single newline above args
in python doc string.

Args:
node: astroid.scoped_nodes.Function. Node to access module content.
"""

in_multi_line_comment = False
multi_line_indicator = b'"""'
file_content = read_from_node(node)
file_length = len(file_content)
blank_line_counter = 0

for line_num in python_utils.RANGE(file_length):
line = file_content[line_num].strip()

# Single multi-line comment, ignore it.
if line.count(multi_line_indicator) == 2:
continue

# Flip multi-line boolean depending on whether or not we see
# the multi-line indicator. Possible for multiline comment to
# be somewhere other than the start of a line (e.g. func arg),
# so we can't look at start of or end of a line, which is why
# the case where two indicators in a single line is handled
# separately (i.e. one line comment with multi-line strings).
if multi_line_indicator in line:
in_multi_line_comment = not in_multi_line_comment

# Ignore anything inside a multi-line comment.
if in_multi_line_comment:
continue

if file_content[line_num] == b'\n':
blank_line_counter += 1
else:
blank_line_counter = 0

if (line_num + 1 < file_length and (
blank_line_counter == 0 or blank_line_counter > 1)):
line = file_content[line_num + 1].strip()
if (len(line.split()) == 1 and
re.match(br'^[A-Z][a-z]+[:]', line)):
self.add_message(
'single-space-above-args', line=line_num + 1)


def register(linter):
"""Registers the checker with pylint.

Expand All @@ -1036,3 +1097,4 @@ def register(linter):
linter.register_checker(SingleCharAndNewlineAtEOFChecker(linter))
linter.register_checker(SingleSpaceAfterYieldChecker(linter))
linter.register_checker(ExcessiveEmptyLinesChecker(linter))
linter.register_checker(SingleNewlineAboveArgsChecker(linter))
140 changes: 140 additions & 0 deletions scripts/pylint_extensions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,3 +1143,143 @@ def func3():

with checker_test_object.assertNoMessages():
temp_file.close()


class SingleNewlineAboveArgsCheckerTests(unittest.TestCase):
kevinlee12 marked this conversation as resolved.
Show resolved Hide resolved

def test_checks_newline_above_docstring(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split up this test and more tests with varying combinations of args/returns/raises with and without spaces. (There should be at least 8 cases to cover all of them).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still haven't split up the tests yet. Specifically, what I mean is to break this method up so that you have multiple def test_....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

checker_test_object = testutils.CheckerTestCase()
checker_test_object.CHECKER_CLASS = (
pylint_extensions.SingleNewlineAboveArgsChecker)
checker_test_object.setup_method()
node_single_newline_above_args = astroid.scoped_nodes.Module(
name='test',
doc='Custom test')
temp_file = tempfile.NamedTemporaryFile()
filename = temp_file.name

with python_utils.open_file(filename, 'w') as tmp:
tmp.write(
u"""def func(arg):
'''Returns something.
Args:
arg: argument
Returns:
returns_something
'''

return returns_something
""")
node_single_newline_above_args.file = filename
node_single_newline_above_args.path = filename

checker_test_object.checker.process_module(
node_single_newline_above_args)

with checker_test_object.assertAddsMessages(
testutils.Message(
msg_id='single-space-above-args',
line=2
),
testutils.Message(
msg_id='single-space-above-args',
line=4
),
):
temp_file.close()

node_with_two_newline = astroid.scoped_nodes.Module(
name='test',
doc='Custom test')

temp_file = tempfile.NamedTemporaryFile()
filename = temp_file.name
with python_utils.open_file(filename, 'w') as tmp:
tmp.write(
u"""def func(arg):
'''Returns something.


Args:
arg: argument


Returns:
returns_something
'''
returns something
""")
node_with_two_newline.file = filename
node_with_two_newline.path = filename

checker_test_object.checker.process_module(
node_with_two_newline)

with checker_test_object.assertAddsMessages(
testutils.Message(
msg_id='single-space-above-args',
line=4
),
testutils.Message(
msg_id='single-space-above-args',
line=8
),
):
temp_file.close()

node_with_return_in_comment = astroid.scoped_nodes.Module(
name='test',
doc='Custom test')
temp_file = tempfile.NamedTemporaryFile()
filename = temp_file.name

with python_utils.open_file(filename, 'w') as tmp:
tmp.write(
u"""def func(arg):
'''Returns something.

Args:
arg: argument

Returns:
returns_something
'''
"Returns: something"
returns_something
""")
node_with_return_in_comment.file = filename
node_with_return_in_comment.path = filename

checker_test_object.checker.process_module(
node_with_return_in_comment)

with checker_test_object.assertNoMessages():
temp_file.close()

node_with_no_error_message = astroid.scoped_nodes.Module(
name='test',
doc='Custom test')

temp_file = tempfile.NamedTemporaryFile()
filename = temp_file.name
with python_utils.open_file(filename, 'w') as tmp:
tmp.write(
u"""def func(arg):
'''Returns something.

Args:
arg: argument

Returns:
returns_something
'''

returns_something
""")
node_with_no_error_message.file = filename
node_with_no_error_message.path = filename

checker_test_object.checker.process_module(node_with_no_error_message)

with checker_test_object.assertNoMessages():
temp_file.close()