diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2c1e9aa..179886d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,8 +1,15 @@ Contributing the Scame project ============================== +Check the Makefile for available helpers. + Check the Makefile for tips. + make deps + +Run the test:: + + make test Code style ---------- diff --git a/Makefile b/Makefile index 91b6bc3..6d3be2c 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ env: deps: env + @build/bin/pip install -Ue '.[dev]' + @build/bin/pip install bandit scame nose @build/bin/python -m pip install -Ue '.[dev]' @@ -23,7 +25,7 @@ check: @echo "========= bandit ==================" #@build/bin/bandit -n 0 -f txt -r scame/ @echo "========= pylint =============" - @build/bin/pylint scame/ + #@build/bin/pylint scame/ -test: run - @build/bin/nosetests scame/ +test: run check + @build/bin/nosetests --with-id scame/ diff --git a/README.rst b/README.rst index 0ee0f44..38bf59b 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,9 @@ It has the following goals: * Support checking different source parts using different configurations. +* Use soft dependencies on the checker. + Only import it when enabled. + * Use soft dependencies on the checker. Only import it when enabled. * You can ignore a single line for all reports using ` # noqa` marker. diff --git a/release-notes.rst b/release-notes.rst index 2140872..c4d96f9 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,3 +1,8 @@ +scame-0.4.2 - 2017/11/01 +======================== + +* Add a semantic newline checker. + scame-0.4.1 - 2017/10/29 ======================== @@ -68,8 +73,8 @@ Fixed rules to ensure Zope zcml and pt are recongnised as XML. pocket-lint-0.1: The first release ================================== -Pocket-lint a composite linter and style checker. It has several notable -features: +Pocket-lint a composite linter and style checker. +It has several notable features: * Provides a consistent report of issues raised by the subordinate checkers. diff --git a/scame/__main__.py b/scame/__main__.py index 80b5ecf..2f5ede9 100644 --- a/scame/__main__.py +++ b/scame/__main__.py @@ -24,7 +24,7 @@ def parse_command_line(args): version=VERSION, ) parser.add_option( - "-q", "--quiet", action="store_false", dest="verbose", + "-q", "--quiet", action="store_true", dest="quiet", help="Show errors only.") parser.add_option( "--progress", action="store_true", dest="progress", @@ -87,6 +87,9 @@ def parse_command_line(args): exclude.append(part) options.scope['include'] = sources + options.verbose = not command_options.quiet + + options.pycodestyle['enabled'] = True options.scope['exclude'] = exclude return options @@ -253,6 +256,8 @@ def main(args=None): args = sys.argv[1:] options = parse_command_line(args=args) + if len(options.scope['include']) == 0: + sys.stderr.write("Expected file paths.\n") if not options.scope['include'] and not options.diff_branch: sys.stderr.write("Expected file paths or branch diff reference.\n") diff --git a/scame/__version__.py b/scame/__version__.py index 10e5002..b0dff69 100644 --- a/scame/__version__.py +++ b/scame/__version__.py @@ -1,4 +1,5 @@ """ Keeps the version of the project. """ + VERSION = '0.4.1' diff --git a/scame/formatcheck.py b/scame/formatcheck.py index a3b186f..8b1b2cd 100755 --- a/scame/formatcheck.py +++ b/scame/formatcheck.py @@ -385,12 +385,14 @@ def _isExceptedLine(self, line, category, code): A category can be ignored using MARKER:CATEGORY A code from a category can be ignored using MARKER:CATEGORY=ID1,ID """ + if line.find(' # ' + self._IGNORE_MARKER) == -1: if self._IGNORE_MARKER not in line: # Not an excepted line. return False comment = line + if comment.find(':' + self._IGNORE_MARKER) == -1: if comment.find(self._IGNORE_MARKER + ':') == -1: # We have a generic exception. return True @@ -400,6 +402,7 @@ def _isExceptedLine(self, line, category, code): # a category. return False + if comment.find('%s:%s' % (category, self._IGNORE_MARKER)) == -1: if comment.find('%s:%s' % (self._IGNORE_MARKER, category)) == -1: # Not this category. return False @@ -1037,7 +1040,7 @@ def check_ascii(self, line_no, line): line.encode('ascii') except UnicodeEncodeError as error: self.message( - line_no, 'Non-ascii characer at position %s.' % error.end, + line_no, 'Non-ascii character at position %s.' % error.end, icon='error', ) @@ -1177,6 +1180,7 @@ def check_lines(self): self.check_tab(line_no, line) self.check_conflicts(line_no, line) self.check_regex_line(line_no, line) + self.check_semantic_newline(line_no, line) if self.isTransition(line_no - 1): self.check_transition(line_no - 1) @@ -1185,6 +1189,16 @@ def check_lines(self): else: pass + def check_semantic_newline(self, line_no, line): + """ + All lines should have semantic newlines. + """ + # Any ., ?, or ! with a space following after is a bad line, + # as it signals the end of a sentence and anything after that + # should start on a separate line. + if '. ' in line: + self.message(line_no, 'Sentence without a new line.', icon='info') + def isTransition(self, line_number): '''Return True if the current line is a line transition.''' line = self.lines[line_number] @@ -1245,14 +1259,14 @@ def isSectionDelimiter(self, line_number): def check_section_delimiter(self, line_number): """Checks for section delimiter. - These checkes are designed for sections delimited by top and bottom + These checks are designed for sections delimited by top and bottom markers. ======= <- top marker Section <- text_line ======= <- bottom marker - If the section is delimted only by bottom marker, the section text + If the section is delimited only by bottom marker, the section text is considered the top marker. Section <- top marker, text_line diff --git a/scame/tests/test_json.py b/scame/tests/test_json.py index 82e9788..c766d21 100644 --- a/scame/tests/test_json.py +++ b/scame/tests/test_json.py @@ -8,7 +8,7 @@ unicode_literals, ) -from scame.formatcheck import IS_PY3, JSONChecker +from scame.formatcheck import JSONChecker from scame.tests import CheckerTestCase @@ -93,17 +93,10 @@ def test_compile_error_with_line(self): content = '{\n1: "something"}\n' checker = JSONChecker('bogus', content, self.reporter) checker.check() - - if IS_PY3: - self.assertEqual( - [(2, 'Expecting property name enclosed in double quotes: ' - 'line 2 column 1 (char 2)')], - self.reporter.messages) - else: - self.assertEqual( - [(2, 'Expecting property name: line 2 column 1 (char 2)')], - self.reporter.messages) - + self.assertEqual( + [(2, 'Expecting property name enclosed in double quotes: ' + 'line 2 column 1 (char 2)')], + self.reporter.messages) self.assertEqual(1, self.reporter.call_count) def test_compile_error_on_multiple_line(self): diff --git a/scame/tests/test_python.py b/scame/tests/test_python.py index f832603..8bba60d 100644 --- a/scame/tests/test_python.py +++ b/scame/tests/test_python.py @@ -9,7 +9,7 @@ from tempfile import NamedTemporaryFile -from scame.formatcheck import ScameOptions, PythonChecker +from scame.formatcheck import PythonChecker from scame.tests import CheckerTestCase from scame.tests.test_text import AnyTextMixin @@ -63,7 +63,6 @@ def __init__(self): a = "okay" """ - ugly_style_lines_python = """\ a = 1 # Post comment. @@ -98,7 +97,9 @@ class TestPyflakes(CheckerTestCase): def test_code_without_issues(self): self.reporter.call_count = 0 checker = PythonChecker('bogus', good_python, self.reporter) - checker.check_flakes() + + checker.check() + self.assertEqual([], self.reporter.messages) self.assertEqual(0, self.reporter.call_count) @@ -106,7 +107,9 @@ def test_windows_code_without_issues(self): self.reporter.call_count = 0 checker = PythonChecker( 'bogus', good_python_on_windows, self.reporter) - checker.check_flakes() + + checker.check() + self.assertEqual([], self.reporter.messages) self.assertEqual(0, self.reporter.call_count) @@ -114,7 +117,9 @@ def test_code_with_SyntaxError(self): self.reporter.call_count = 0 checker = PythonChecker( 'bogus', bad_syntax_python, self.reporter) - checker.check_flakes() + + checker.check() + expected = [( 2, 'Could not compile; non-default argument follows ' 'default argument: ')] @@ -124,7 +129,9 @@ def test_code_with_SyntaxError(self): def test_code_with_very_bad_SyntaxError(self): checker = PythonChecker( 'bogus', bad_syntax2_python, self.reporter) - checker.check_flakes() + + checker.check() + expected = [( 2, 'Could not compile; invalid syntax: def __init__(self, val):')] self.assertEqual(expected, self.reporter.messages) @@ -132,7 +139,9 @@ def test_code_with_very_bad_SyntaxError(self): def test_code_with_IndentationError(self): checker = PythonChecker( 'bogus', bad_indentation_python, self.reporter) - checker.check_flakes() + + checker.check() + expected = [ (4, 'Could not compile; unindent does not match any ' 'outer indentation level: b = 1')] @@ -143,7 +152,9 @@ def test_code_with_warnings(self): self.file = NamedTemporaryFile(prefix='pocketlint_', suffix='.py') self.write_to_file(self.file, ugly_python) checker = PythonChecker(self.file.name, ugly_python, self.reporter) - checker.check_flakes() + + checker.check() + self.assertEqual( [(3, "undefined name 'b'"), (3, "local variable 'a' is assigned to but never used")], @@ -153,10 +164,12 @@ def test_code_with_warnings(self): def test_pyflakes_ignore(self): pyflakes_ignore = ( 'def something():\n' - ' unused_variable = 1 # pyflakes:ignore\n') + ' unused_variable = 1 # noqa:pyflakes\n') self.reporter.call_count = 0 checker = PythonChecker('bogus', pyflakes_ignore, self.reporter) - checker.check_flakes() + + checker.check() + self.assertEqual([], self.reporter.messages) self.assertEqual(0, self.reporter.call_count) @@ -167,8 +180,9 @@ def test_pyflakes_unicode(self): 'variable = u"r\xe9sum\xe9"' ) checker = PythonChecker('bogus', source, self.reporter) + # This should set the correct encoding. - checker.check_text() + checker.check() checker.check_flakes() self.assertEqual([], self.reporter.messages) @@ -179,24 +193,26 @@ class TestPyCodeStyle(CheckerTestCase): Verify pycodestyle integration. """ + def getChecker(self, content): + """ + Return a new PythonChecker which is connected to the reported. + """ + checker = PythonChecker('fake/path', content, self.reporter) + checker.options.pycodestyle['enabled'] = True + return checker + def test_code_without_issues(self): - checker = PythonChecker( - 'file/path', good_python, self.reporter) - checker.check_pycodestyle() - self.assertEqual([], self.reporter.messages) + checker = self.getChecker(good_python) - def test_bad_syntax(self): - checker = PythonChecker( - 'file/path', ugly_style_python, self.reporter) checker.check_pycodestyle() - self.assertEqual( - [(4, 'E222 multiple spaces after operator')], - self.reporter.messages) + + self.assertEqual([], self.reporter.messages) def test_code_with_IndentationError(self): - checker = PythonChecker( - 'file/path', bad_indentation_python, self.reporter) + checker = self.getChecker(bad_indentation_python) + checker.check_pycodestyle() + expected = [( 4, 'E901 IndentationError: ' @@ -205,53 +221,62 @@ def test_code_with_IndentationError(self): checker.check_pycodestyle() def test_code_closing_bracket(self): - checker = PythonChecker( - 'file/path', hanging_style_python, self.reporter) + checker = self.getChecker(hanging_style_python) checker.options.pycodestyle['hang_closing'] = True + checker.check_pycodestyle() + self.assertEqual([], self.reporter.messages) checker.options.pycodestyle['hang_closing'] = False + checker.check_pycodestyle() + self.assertEqual( [(4, "E123 closing bracket does not match indentation of " "opening bracket's line")], self.reporter.messages) def test_code_with_issues(self): - checker = PythonChecker( - 'file/path', ugly_style_python, self.reporter) + checker = self.getChecker(ugly_style_python) + checker.check_pycodestyle() + self.assertEqual( [(4, 'E222 multiple spaces after operator')], self.reporter.messages) def test_code_with_comments(self): - checker = PythonChecker( - 'file/path', ugly_style_lines_python, self.reporter) + checker = self.getChecker(ugly_style_lines_python) + checker.check_pycodestyle() + self.assertEqual([], self.reporter.messages) def test_long_length_good(self): long_line = '1234 56189' * 7 + '12345678' + '\n' - checker = PythonChecker('file/path', long_line, self.reporter) + checker = self.getChecker(long_line) + checker.check_pycodestyle() + self.assertEqual([], self.reporter.messages) def test_long_length_bad(self): long_line = '1234 56189' * 8 + '\n' - checker = PythonChecker('file/path', long_line, self.reporter) + checker = self.getChecker(long_line) + checker.check_pycodestyle() + self.assertEqual( - [(1, 'E501 line too long (80 > 79 characters)')], + [(1, 'E501 line too long (80 > 78 characters)')], self.reporter.messages) def test_long_length_options(self): long_line = '1234 56189' * 7 + '\n' - options = ScameOptions() - options.max_line_length = 60 - checker = PythonChecker( - 'file/path', long_line, self.reporter, options) + checker = self.getChecker(long_line) + checker.options.max_line_length = 60 + checker.check_pycodestyle() + self.assertEqual( [(1, 'E501 line too long (70 > 59 characters)')], self.reporter.messages) @@ -324,5 +349,5 @@ def test_code_ascii_is_not_utf8(self): checker = PythonChecker('bogus', utf8_python, self.reporter) checker.check_text() self.assertEqual( - [(1, 'Non-ascii characer at position 21.')], + [(1, 'Non-ascii character at position 21.')], self.reporter.messages) diff --git a/scame/tests/test_restructuredtext.py b/scame/tests/test_restructuredtext.py index 4d4a78c..807adb8 100644 --- a/scame/tests/test_restructuredtext.py +++ b/scame/tests/test_restructuredtext.py @@ -23,14 +23,14 @@ -------------------- -Second emtpy section +Second empty section -------------------- Third section ^^^^^^^^^^^^^ -Paragrhap for +Paragraph for third section `with link`_. :: @@ -43,6 +43,15 @@ | Line blocks are useful for addresses, | verse, and adornment-free lists. +Newline test has newline. +Indeed! +How come? + +We can have multiple lines, +and list, and other things, on multiple lines. + +Somethines ... I think ok ... + .. _section-permalink: @@ -141,7 +150,7 @@ def test_no_empty_last_line(self): self.reporter.call_count = 0 content = ( 'Some first line\n' - 'the second and last line witout newline') + 'the second and last line without newline') checker = ReStructuredTextChecker('bogus', content, self.reporter) checker.check_empty_last_line(2) expected = [( @@ -343,6 +352,70 @@ def test_check_section_delimiter_bad_marker_length(self): self.assertEqual(expect, self.reporter.messages) self.assertEqual(1, self.reporter.call_count) + def test_check_semantic_newline_alltests_true(self): + """ + When a ., ?, or ! is not the last character of the line, it is + considered a semantic newline violation and an error is reported. + """ + content = ( + 'Sentence. Other\n' + 'Sentence! More here\n' + 'Sentence? Something...\n' + ) + sut = ReStructuredTextChecker('bogus', content, self.reporter) + + sut.check() + + expect = [ + 'Check that a new sentence is created after a full stop, ! or ?.'] + self.assertEqual(expect, self.reporter.messages) + self.assertEqual(1, self.reporter.call_count) + + def test_check_semantic_newline_fullstop(self): + """ + When a full stop is not the last character of the line, it is + considered a semantic newline violation and an error is reported. + """ + content = ( + 'First line is ok.\n' + 'Sentence. New sentence\n' + ) + sut = ReStructuredTextChecker('bogus', content, self.reporter) + + sut.check() + + expect = [(2, u'Sentence without a new line.')] + self.assertEqual(expect, self.reporter.messages) + self.assertEqual(1, self.reporter.call_count) + + def test_check_semantic_newline_questionmark(self): + """When a question mark is not the last character of the line, it is + considered a semantic newline violation and an error is reported.""" + content = ( + 'Sentence? New sentence\n' + ) + sut = ReStructuredTextChecker('bogus', content, self.reporter) + + sut.check() + + expect = [('Newline not created after a ? sentence.')] + self.assertEqual(expect, self.reporter.messages) + self.assertEqual(1, self.reporter.call_count) + + def test_check_semantic_newline_exclamationmark(self): + """When an exclamation mark is not the last character of the line, it + is considered a semantic newline violation and an error is reported.""" + content = ( + 'Sentence! New sentence\n' + ) + sut = ReStructuredTextChecker('bogus', content, self.reporter) + + sut.check() + + expect = [('Newline not created after a ! sentence.')] + self.assertEqual(expect, self.reporter.messages) + self.assertEqual(1, self.reporter.call_count) + def test_check_section_delimiter_bad_length_both_markers(self): content = ( '---------\n' diff --git a/scame/tests/test_text.py b/scame/tests/test_text.py index 6d592eb..cc42d75 100644 --- a/scame/tests/test_text.py +++ b/scame/tests/test_text.py @@ -8,8 +8,8 @@ ) from scame.__main__ import parse_command_line -from scame.formatcheck import AnyTextChecker -from scame.tests import Bunch, CheckerTestCase +from scame.formatcheck import AnyTextChecker, ScameOptions +from scame.tests import CheckerTestCase class AnyTextMixin(object): @@ -77,14 +77,13 @@ def test_regex_line(self): A list of regex and corresponding error messages can be passed to check each line. """ - options = Bunch( - max_line_length=80, - hang_closing=True, - regex_line=[ - ('.*marker.*', 'Explanation.'), - ('.*sign.*', 'Message.'), - ], - ) + options = ScameOptions() + options.max_line_length = 80 + options.pycodestyle['hang_closing'] = True + options.regex_line = [ + ('.*marker.*', 'Explanation.'), + ('.*sign.*', 'Message.'), + ] self.create_and_check( 'bogus', @@ -171,7 +170,7 @@ def test_multiple_empty_last_lines(self): def test_single_last_line_no_newline(self): """An error is reported if file contains a single newline.""" content = ( - 'the second and last line without newline') + '\nthe second and last line without newline') checker = AnyTextChecker('bogus', content, self.reporter) checker.check_empty_last_line(2) expected = [(