summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdi Roiban <adi.roiban@chevah.com>2018-04-08 16:19:11 +0100
committerIan Stapleton Cordasco <graffatcolmingov@gmail.com>2018-04-08 10:19:11 -0500
commit5d31e7ee2e5996be10a0452d4b01b799a6698ad5 (patch)
tree434e7be1531636dca58e4ffde4b6b340febfa3aa
parent9f225ac876b6340825770c9c665060dfc28300c6 (diff)
downloadpep8-5d31e7ee2e5996be10a0452d4b01b799a6698ad5.tar.gz
Add variables so blank lines may be configures
This adds some module level configuration points for users to define how many blank lines they want in their code. It paves the way for someone to develop a flake8 plugin to configure this in pycodestyle. Fixes #732
-rw-r--r--CONTRIBUTING.rst38
-rwxr-xr-xpycodestyle.py48
-rw-r--r--testsuite/support.py20
-rw-r--r--testsuite/test_all.py10
-rw-r--r--testsuite/test_blank_lines.py552
5 files changed, 653 insertions, 15 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index aad6ad6..9f55859 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -66,6 +66,44 @@ At this point you can create a pull request back to the official pycodestyles
repository for review! For more information on how to make a pull request,
GitHub has an excellent `guide`_.
+The current tests are written in 2 styles:
+
+* standard xUnit based only on stdlib unittest
+ (can be executed with nose)
+* functional test using a custom framework and executed by the
+ pycodestyle itself when installed in dev mode.
+
+
+Running unittest
+~~~~~~~~~~~~~~~~
+
+While the tests are writted using stdlib `unittest` module, the existing
+test include unit, integration and functional tests.
+
+There are a couple of ways to run the tests::
+
+ $ python setup.py test
+ $ # Use nose to run specific test
+ $ nosetests \
+ > testsuite.test_blank_lines:TestBlankLinesDefault.test_initial_no_blank
+ $ # Use nose to run a subset and check coverage, and check the resulting
+ $ $ cover/pycodestyle_py.html in your browser
+ $ nosetests --with-coverage --cover-html -s testsuite.test_blank_lines
+
+
+Running functional
+~~~~~~~~~~~~~~~~~~
+
+When installed in dev mode, pycodestyle will have the `--testsuite`
+option which can be used to run the tests::
+
+ $ pip install -e .
+ $ # Run all tests.
+ $ pycodestyle --testsuite testsuite
+ $ # Run a subset of the tests.
+ $ pycodestyle --testsuite testsuite/E30.py
+
+
.. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/
.. _guide: https://guides.github.com/activities/forking/
.. _tox: https://tox.readthedocs.io/en/latest/
diff --git a/pycodestyle.py b/pycodestyle.py
index 5371a67..96779ab 100755
--- a/pycodestyle.py
+++ b/pycodestyle.py
@@ -95,6 +95,13 @@ except ImportError:
PROJECT_CONFIG = ('setup.cfg', 'tox.ini')
TESTSUITE_PATH = os.path.join(os.path.dirname(__file__), 'testsuite')
MAX_LINE_LENGTH = 79
+# Number of blank lines between various code parts.
+BLANK_LINES_CONFIG = {
+ # Top level class and function.
+ 'top_level': 2,
+ # Methods and nested class and function.
+ 'method': 1,
+}
REPORT_FORMAT = {
'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s',
'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s',
@@ -332,37 +339,50 @@ def blank_lines(logical_line, blank_lines, indent_level, line_number,
E305: def a():\n pass\na()
E306: def a():\n def b():\n pass\n def c():\n pass
"""
- if line_number < 3 and not previous_logical:
+ top_level_lines = BLANK_LINES_CONFIG['top_level']
+ method_lines = BLANK_LINES_CONFIG['method']
+
+ if line_number < top_level_lines + 1 and not previous_logical:
return # Don't expect blank lines before the first line
if previous_logical.startswith('@'):
if blank_lines:
yield 0, "E304 blank lines found after function decorator"
- elif blank_lines > 2 or (indent_level and blank_lines == 2):
+ elif (blank_lines > top_level_lines or
+ (indent_level and blank_lines == method_lines + 1)
+ ):
yield 0, "E303 too many blank lines (%d)" % blank_lines
elif STARTSWITH_TOP_LEVEL_REGEX.match(logical_line):
if indent_level:
- if not (blank_before or previous_indent_level < indent_level or
- DOCSTRING_REGEX.match(previous_logical)):
+ if not (blank_before == method_lines or
+ previous_indent_level < indent_level or
+ DOCSTRING_REGEX.match(previous_logical)
+ ):
ancestor_level = indent_level
nested = False
# Search backwards for a def ancestor or tree root (top level).
- for line in lines[line_number - 2::-1]:
+ for line in lines[line_number - top_level_lines::-1]:
if line.strip() and expand_indent(line) < ancestor_level:
ancestor_level = expand_indent(line)
nested = line.lstrip().startswith('def ')
if nested or ancestor_level == 0:
break
if nested:
- yield 0, "E306 expected 1 blank line before a " \
- "nested definition, found 0"
+ yield 0, "E306 expected %s blank line before a " \
+ "nested definition, found 0" % (method_lines,)
else:
- yield 0, "E301 expected 1 blank line, found 0"
- elif blank_before != 2:
- yield 0, "E302 expected 2 blank lines, found %d" % blank_before
- elif (logical_line and not indent_level and blank_before != 2 and
- previous_unindented_logical_line.startswith(('def ', 'class '))):
- yield 0, "E305 expected 2 blank lines after " \
- "class or function definition, found %d" % blank_before
+ yield 0, "E301 expected %s blank line, found 0" % (
+ method_lines,)
+ elif blank_before != top_level_lines:
+ yield 0, "E302 expected %s blank lines, found %d" % (
+ top_level_lines, blank_before)
+ elif (logical_line and
+ not indent_level and
+ blank_before != top_level_lines and
+ previous_unindented_logical_line.startswith(('def ', 'class '))
+ ):
+ yield 0, "E305 expected %s blank lines after " \
+ "class or function definition, found %d" % (
+ top_level_lines, blank_before)
@register_check
diff --git a/testsuite/support.py b/testsuite/support.py
index cf9abc5..825def1 100644
--- a/testsuite/support.py
+++ b/testsuite/support.py
@@ -83,6 +83,26 @@ class TestReport(StandardReport):
print("Test failed." if self.total_errors else "Test passed.")
+class InMemoryReport(BaseReport):
+ """
+ Collect the results in memory, without printing anything.
+ """
+
+ def __init__(self, options):
+ super(InMemoryReport, self).__init__(options)
+ self.in_memory_errors = []
+
+ def error(self, line_number, offset, text, check):
+ """
+ Report an error, according to options.
+ """
+ code = text[:4]
+ self.in_memory_errors.append('%s:%s:%s' % (
+ code, line_number, offset + 1))
+ return super(InMemoryReport, self).error(
+ line_number, offset, text, check)
+
+
def selftest(options):
"""
Test all check functions with test cases in docstrings.
diff --git a/testsuite/test_all.py b/testsuite/test_all.py
index f571a7b..0e4bc7d 100644
--- a/testsuite/test_all.py
+++ b/testsuite/test_all.py
@@ -48,11 +48,19 @@ class PycodestyleTestCase(unittest.TestCase):
def suite():
- from testsuite import test_api, test_parser, test_shell, test_util
+ from testsuite import (
+ test_api,
+ test_blank_lines,
+ test_parser,
+ test_shell,
+ test_util,
+ )
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(PycodestyleTestCase))
suite.addTest(unittest.makeSuite(test_api.APITestCase))
+ suite.addTest(unittest.makeSuite(test_blank_lines.TestBlankLinesDefault))
+ suite.addTest(unittest.makeSuite(test_blank_lines.TestBlankLinesTwisted))
suite.addTest(unittest.makeSuite(test_parser.ParserTestCase))
suite.addTest(unittest.makeSuite(test_shell.ShellTestCase))
suite.addTest(unittest.makeSuite(test_util.UtilTestCase))
diff --git a/testsuite/test_blank_lines.py b/testsuite/test_blank_lines.py
new file mode 100644
index 0000000..870403a
--- /dev/null
+++ b/testsuite/test_blank_lines.py
@@ -0,0 +1,552 @@
+"""
+Tests for the blank_lines checker.
+
+It uses dedicated assertions which work with TestReport.
+"""
+import unittest
+
+import pycodestyle
+from testsuite.support import InMemoryReport
+
+
+class BlankLinesTestCase(unittest.TestCase):
+ """
+ Common code for running blank_lines tests.
+ """
+
+ def check(self, content):
+ """
+ Run checks on `content` and return the the list of errors.
+ """
+ sut = pycodestyle.StyleGuide()
+ reporter = sut.init_report(InMemoryReport)
+ sut.input_file(
+ filename='in-memory-test-file.py',
+ lines=content.splitlines(True),
+ )
+ return reporter.in_memory_errors
+
+ def assertNoErrors(self, actual):
+ """
+ Check that the actual result from the checker has no errors.
+ """
+ self.assertEqual([], actual)
+
+
+class TestBlankLinesDefault(BlankLinesTestCase):
+ """
+ Tests for default blank with 2 blank lines for top level and 1 blank line
+ for methods.
+ """
+
+ def test_initial_no_blank(self):
+ """
+ It will accept no blank lines at the start of the file.
+ """
+ result = self.check("""def some_function():
+ pass
+""")
+
+ self.assertNoErrors(result)
+
+ def test_initial_lines_one_blank(self):
+ """
+ It will accept 1 blank lines before the first line of actual code,
+ even if in other places it asks for 2
+ """
+ result = self.check("""
+def some_function():
+ pass
+""")
+
+ self.assertNoErrors(result)
+
+ def test_initial_lines_two_blanks(self):
+ """
+ It will accept 2 blank lines before the first line of actual code,
+ as normal.
+ """
+ result = self.check("""
+
+def some_function():
+ pass
+""")
+
+ self.assertNoErrors(result)
+
+ def test_method_less_blank_lines(self):
+ """
+ It will trigger an error when less than 1 blank lin is found before
+ method definitions.
+ """
+ result = self.check("""# First comment line.
+class X:
+
+ def a():
+ pass
+ def b():
+ pass
+""")
+ self.assertEqual([
+ 'E301:6:5', # b() call
+ ], result)
+
+ def test_method_less_blank_lines_comment(self):
+ """
+ It will trigger an error when less than 1 blank lin is found before
+ method definition, ignoring comments.
+ """
+ result = self.check("""# First comment line.
+class X:
+
+ def a():
+ pass
+ # A comment will not make it better.
+ def b():
+ pass
+""")
+ self.assertEqual([
+ 'E301:7:5', # b() call
+ ], result)
+
+ def test_top_level_fewer_blank_lines(self):
+ """
+ It will trigger an error when less 2 blank lines are found before top
+ level definitions.
+ """
+ result = self.check("""# First comment line.
+# Second line of comment.
+
+def some_function():
+ pass
+
+async def another_function():
+ pass
+
+
+def this_one_is_good():
+ pass
+
+class SomeCloseClass(object):
+ pass
+
+
+async def this_async_is_good():
+ pass
+
+
+class AFarEnoughClass(object):
+ pass
+""")
+ self.assertEqual([
+ 'E302:4:1', # some_function
+ 'E302:7:1', # another_function
+ 'E302:14:1', # SomeCloseClass
+ ], result)
+
+ def test_top_level_more_blank_lines(self):
+ """
+ It will trigger an error when more 2 blank lines are found before top
+ level definitions.
+ """
+ result = self.check("""# First comment line.
+# Second line of comment.
+
+
+
+def some_function():
+ pass
+
+
+def this_one_is_good():
+ pass
+
+
+
+class SomeFarClass(object):
+ pass
+
+
+class AFarEnoughClass(object):
+ pass
+""")
+ self.assertEqual([
+ 'E303:6:1', # some_function
+ 'E303:15:1', # SomeFarClass
+ ], result)
+
+ def test_method_more_blank_lines(self):
+ """
+ It will trigger an error when more than 1 blank line is found before
+ method definition
+ """
+ result = self.check("""# First comment line.
+
+
+class SomeCloseClass(object):
+
+
+ def oneMethod(self):
+ pass
+
+
+ def anotherMethod(self):
+ pass
+
+ def methodOK(self):
+ pass
+
+
+
+ def veryFar(self):
+ pass
+""")
+ self.assertEqual([
+ 'E303:7:5', # oneMethod
+ 'E303:11:5', # anotherMethod
+ 'E303:19:5', # veryFar
+ ], result)
+
+ def test_initial_lines_more_blank(self):
+ """
+ It will trigger an error for more than 2 blank lines before the first
+ line of actual code.
+ """
+ result = self.check("""
+
+
+def some_function():
+ pass
+""")
+ self.assertEqual(['E303:4:1'], result)
+
+ def test_blank_line_between_decorator(self):
+ """
+ It will trigger an error when the decorator is followed by a blank
+ line.
+ """
+ result = self.check("""# First line.
+
+
+@some_decorator
+
+def some_function():
+ pass
+
+
+class SomeClass(object):
+
+ @method_decorator
+
+ def some_method(self):
+ pass
+""")
+ self.assertEqual(['E304:6:1', 'E304:14:5'], result)
+
+ def test_blank_line_decorator(self):
+ """
+ It will accept the decorators which are adjacent to the function and
+ method definition.
+ """
+ result = self.check("""# First line.
+
+
+@another_decorator
+@some_decorator
+def some_function():
+ pass
+
+
+class SomeClass(object):
+
+ @method_decorator
+ def some_method(self):
+ pass
+""")
+ self.assertNoErrors(result)
+
+ def test_top_level_fewer_follow_lines(self):
+ """
+ It will trigger an error when less than 2 blank lines are
+ found between a top level definitions and other top level code.
+ """
+ result = self.check("""
+def a():
+ print('Something')
+
+a()
+""")
+ self.assertEqual([
+ 'E305:5:1', # a call
+ ], result)
+
+ def test_top_level_fewer_follow_lines_comments(self):
+ """
+ It will trigger an error when less than 2 blank lines are
+ found between a top level definitions and other top level code,
+ even if we have comments before
+ """
+ result = self.check("""
+def a():
+ print('Something')
+
+ # comment
+
+ # another comment
+
+# With comment still needs 2 spaces before,
+# as comments are ignored.
+a()
+""")
+ self.assertEqual([
+ 'E305:11:1', # a call
+ ], result)
+
+ def test_top_level_good_follow_lines(self):
+ """
+ It not trigger an error when 2 blank lines are
+ found between a top level definitions and other top level code.
+ """
+ result = self.check("""
+def a():
+ print('Something')
+
+ # Some comments in other parts.
+
+ # More comments.
+
+
+# With the right spaces,
+# It will work, even when we have comments.
+a()
+""")
+ self.assertNoErrors(result)
+
+ def test_method_fewer_follow_lines(self):
+ """
+ It will trigger an error when less than 1 blank line is
+ found between a method and previous definitions.
+ """
+ result = self.check("""
+def a():
+ x = 1
+ def b():
+ pass
+""")
+ self.assertEqual([
+ 'E306:4:5', # b() call
+ ], result)
+
+ def test_method_nested_fewer_follow_lines(self):
+ """
+ It will trigger an error when less than 1 blank line is
+ found between a method and previous definitions, even when nested.
+ """
+ result = self.check("""
+def a():
+ x = 2
+
+ def b():
+ x = 1
+ def c():
+ pass
+""")
+ self.assertEqual([
+ 'E306:7:9', # c() call
+ ], result)
+
+ def test_method_nested_less_class(self):
+ """
+ It will trigger an error when less than 1 blank line is found
+ between a method and previous definitions, even when used to
+ define a class.
+ """
+ result = self.check("""
+def a():
+ x = 1
+ class C:
+ pass
+""")
+ self.assertEqual([
+ 'E306:4:5', # class C definition.
+ ], result)
+
+ def test_method_nested_ok(self):
+ """
+ Will not trigger an error when 1 blank line is found
+ found between a method and previous definitions, even when nested.
+ """
+ result = self.check("""
+def a():
+ x = 2
+
+ def b():
+ x = 1
+
+ def c():
+ pass
+
+ class C:
+ pass
+""")
+ self.assertNoErrors(result)
+
+
+class TestBlankLinesTwisted(BlankLinesTestCase):
+ """
+ Tests for blank_lines with 3 blank lines for top level and 2 blank line
+ for methods as used by the Twisted coding style.
+ """
+
+ def setUp(self):
+ self._original_lines_config = pycodestyle.BLANK_LINES_CONFIG.copy()
+ pycodestyle.BLANK_LINES_CONFIG['top_level'] = 3
+ pycodestyle.BLANK_LINES_CONFIG['method'] = 2
+
+ def tearDown(self):
+ pycodestyle.BLANK_LINES_CONFIG = self._original_lines_config
+
+ def test_initial_lines_one_blanks(self):
+ """
+ It will accept less than 3 blank lines before the first line of actual
+ code.
+ """
+ result = self.check("""
+
+
+def some_function():
+ pass
+""")
+
+ self.assertNoErrors(result)
+
+ def test_initial_lines_tree_blanks(self):
+ """
+ It will accept 3 blank lines before the first line of actual code,
+ as normal.
+ """
+ result = self.check("""
+
+
+def some_function():
+ pass
+""")
+
+ self.assertNoErrors(result)
+
+ def test_top_level_fewer_blank_lines(self):
+ """
+ It will trigger an error when less 2 blank lines are found before top
+ level definitions.
+ """
+ result = self.check("""# First comment line.
+# Second line of comment.
+
+
+def some_function():
+ pass
+
+
+async def another_function():
+ pass
+
+
+
+def this_one_is_good():
+ pass
+
+class SomeCloseClass(object):
+ pass
+
+
+
+async def this_async_is_good():
+ pass
+
+
+
+class AFarEnoughClass(object):
+ pass
+""")
+ self.assertEqual([
+ 'E302:5:1', # some_function
+ 'E302:9:1', # another_function
+ 'E302:17:1', # SomeCloseClass
+ ], result)
+
+ def test_top_level_more_blank_lines(self):
+ """
+ It will trigger an error when more 2 blank lines are found before top
+ level definitions.
+ """
+ result = self.check("""# First comment line.
+# Second line of comment.
+
+
+
+
+def some_function():
+ pass
+
+
+
+def this_one_is_good():
+ pass
+
+
+
+
+class SomeVeryFarClass(object):
+ pass
+
+
+
+class AFarEnoughClass(object):
+ pass
+""")
+ self.assertEqual([
+ 'E303:7:1', # some_function
+ 'E303:18:1', # SomeVeryFarClass
+ ], result)
+
+ def test_the_right_blanks(self):
+ """
+ It will accept 3 blank for top level and 2 for nested.
+ """
+ result = self.check("""
+
+
+def some_function():
+ pass
+
+
+
+# With comments.
+some_other = code_here
+
+
+
+class SomeClass:
+ '''
+ Docstring here.
+ '''
+
+ def some_method():
+ pass
+
+
+ def another_method():
+ pass
+
+
+ # More methods.
+ def another_method_with_comment():
+ pass
+
+
+ @decorator
+ def another_method_with_comment():
+ pass
+""")
+
+ self.assertNoErrors(result)