-
Notifications
You must be signed in to change notification settings - Fork 13
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
Add some Google Styleguide formatting rules #4
Changes from 15 commits
4963d39
011c5ea
6ff8a46
40ce171
dcf85c7
e7a78b0
cef8dbf
0c2bff0
752a505
a021916
f2969cd
36e002f
beb2121
d410ee9
5c9b22b
541710e
a81732d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,6 @@ python: | |
- "3.3" | ||
- "3.4" | ||
- "3.5" | ||
- "3.6" | ||
before_install: | ||
- pip install -U pip setuptools | ||
install: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ autolint: autopep8 lint | |
run_tests: clean | ||
py.test --durations=10 . | ||
|
||
test: autopep8 lint run_tests | ||
test: autopep8 run_tests lint | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we lint the linter with its own rules, we should run tests first. |
||
|
||
setup_dev: | ||
pip install -e .[dev] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ persistent=yes | |
|
||
# List of plugins (as comma separated values of python modules names) to load, | ||
# usually to register additional checkers. | ||
load-plugins= | ||
load-plugins=shopify_python | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's lint ourselves. |
||
|
||
|
||
[MESSAGES CONTROL] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import sys | ||
|
||
import astroid # pylint: disable=unused-import | ||
import six | ||
|
||
from pylint import checkers | ||
from pylint import interfaces | ||
from pylint import lint # pylint: disable=unused-import | ||
|
||
|
||
if sys.version_info >= (3, 4): | ||
import importlib.util # pylint: disable=no-name-in-module,import-error | ||
else: | ||
import importlib | ||
|
||
|
||
def register_checkers(linter): # type: (lint.PyLinter) -> None | ||
"""Register checkers.""" | ||
linter.register_checker(GoogleStyleGuideChecker(linter)) | ||
|
||
|
||
class GoogleStyleGuideChecker(checkers.BaseChecker): | ||
""" | ||
Pylint checker for the Google Python Style Guide. | ||
|
||
See https://google.github.io/styleguide/pyguide.html | ||
|
||
Checks that can't be implemented include: | ||
- When capturing an exception, use as rather than a comma | ||
""" | ||
__implements__ = (interfaces.IAstroidChecker,) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should get in the habit of docstrings for at least these classes. |
||
|
||
name = 'google-styleguide-checker' | ||
|
||
msgs = { | ||
'C2601': ('Imported an object that is not a package or module', | ||
'import-modules-only', | ||
'Use imports for packages and modules only'), | ||
'C2602': ('Imported using a partial path', | ||
'import-full-path', | ||
'Import each module using the full pathname location of the module.'), | ||
'C2603': ('Variable declared at the module level (i.e. global)', | ||
'global-variable', | ||
'Avoid global variables in favor of class variables'), | ||
'C2604': ('Raised two-argument exception', | ||
'two-arg-exception', | ||
"Use either raise Exception('message') or raise Exception"), | ||
'C2605': ('Raised deprecated string-exception', | ||
'string-exception', | ||
"Use either raise Exception('message') or raise Exception"), | ||
'C2606': ('Caught StandardError', | ||
'catch-standard-error', | ||
"Don't catch StandardError"), | ||
'C2607': ('Try body too long', | ||
'try-too-long', | ||
"The larger the try body, the more likely that an unexpected exception will be raised"), | ||
'C2608': ('Except body too long', | ||
'except-too-long', | ||
"The larger the except body, the more likely that an exception will be raised"), | ||
'C2609': ('Finally body too long', | ||
'finally-too-long', | ||
"The larger the except body, the more likely that an exception will be raised"), | ||
} | ||
|
||
max_try_exc_finally_body_size = 5 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems rather low, especially if it's counting raw lines of code instead of logical statements/expressions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, does it make sense to lint on this at all? I can easily see people disabling this check (or calling out to error handling functions) to avoid it if their error handling is sound. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's technically counting AST objects, not raw lines of code. E.g. an embedded try/except is one object (even if it has a try body with 5 lines and an except body with another 5). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm in favour of this linter rule. Its purpose is to educate and inform people of the expectation, and to catch the accidental violations we all make. If people intentionally rewrite their code to get around linter rules instead of adhering to the principle, that's what code review is for. People can also just disable the check with a comment. I worry less about what the limit is than that there is a limit. 5 seems reasonable to me. I could be OK with <=10. Another option is to specify an overridable default, similar to the "max lines per module" rule. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now I'll remove these linter rules and address them in a future PR where I'll probably consider tree size as a measure of complexity over AST children (as it was implemented here). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Code review is also for pointing out complexity problems like the ones this linter rule is meant to address. It's easier to spot too many lines of code in a single I'm in favour of the spirit of the rule, but not this particular implementation.
I'd be in favour of something like this. Number of lines might even make the best metric since they're easy to understand (and therefore adjust) by other teams on other projects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 |
||
|
||
def visit_assign(self, node): # type: (astroid.Assign) -> None | ||
self.__avoid_global_variables(node) | ||
|
||
def visit_excepthandler(self, node): # type: (astroid.ExceptHandler) -> None | ||
self.__dont_catch_standard_error(node) | ||
|
||
def visit_tryexcept(self, node): # type: (astroid.TryExcept) -> None | ||
self.__minimize_code_in_try_except(node) | ||
|
||
def visit_tryfinally(self, node): # type: (astroid.TryFinally) -> None | ||
self.__minimize_code_in_finally(node) | ||
|
||
def visit_importfrom(self, node): # type: (astroid.ImportFrom) -> None | ||
self.__import_modules_only(node) | ||
self.__import_full_path_only(node) | ||
|
||
def visit_raise(self, node): # type: (astroid.Raise) -> None | ||
self.__dont_use_archaic_raise_syntax(node) | ||
|
||
def __import_modules_only(self, node): # type: (astroid.ImportFrom) -> None | ||
"""Use imports for packages and modules only.""" | ||
|
||
if hasattr(self.linter, 'config') and 'import-modules-only' in self.linter.config.disable: | ||
return # Skip if disable to avoid side-effects from importing modules | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bails before we call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
def can_import(module_name): | ||
if sys.version_info >= (3, 4): | ||
try: | ||
return bool(importlib.util.find_spec(module_name)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This throws an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. boo-urns |
||
except AttributeError: | ||
return False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
else: | ||
try: | ||
importlib.import_module(module_name) | ||
return True | ||
except ImportError: | ||
return False | ||
|
||
if not node.level and node.modname != '__future__': | ||
for name in node.names: | ||
name, _ = name | ||
parent_module = node.modname | ||
child_module = '.'.join((node.modname, name)) # Rearrange "from x import y" as "import x.y" | ||
if can_import(parent_module) and not can_import(child_module): | ||
self.add_message('import-modules-only', node=node) | ||
|
||
def __import_full_path_only(self, node): # type: (astroid.ImportFrom) -> None | ||
"""Import each module using the full pathname location of the module.""" | ||
if node.level: | ||
self.add_message('import-full-path', node=node) | ||
|
||
def __avoid_global_variables(self, node): # type: (astroid.Assign) -> None | ||
"""Avoid global variables.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please expand the docstring to explain how this works? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment below; I assume that implementation details like this should be a comment whereas the purpose of the method should be a docstring. |
||
# Is this an assignment happening within a module? If so report on each assignment name | ||
# whether its in a tuple or not | ||
if isinstance(node.parent, astroid.Module): | ||
for target in node.targets: | ||
if hasattr(target, 'elts'): | ||
for elt in target.elts: | ||
if elt.name != '__version__': | ||
self.add_message('global-variable', node=elt) | ||
elif hasattr(target, 'name'): | ||
if target.name != '__version__': | ||
self.add_message('global-variable', node=target) | ||
|
||
def __dont_use_archaic_raise_syntax(self, node): # type: (astroid.Raise) -> None | ||
"""Don't use the two-argument form of raise or the string raise""" | ||
children = list(node.get_children()) | ||
if len(children) > 1: | ||
self.add_message('two-arg-exception', node=node) | ||
elif len(children) == 1 and isinstance(children[0], six.string_types): | ||
self.add_message('string-exception', node=node) | ||
|
||
def __dont_catch_standard_error(self, node): # type: (astroid.ExceptHandler) -> None | ||
""" | ||
Never use catch-all 'except:' statements, or catch Exception or StandardError. | ||
|
||
Pylint already handles bare-except and broad-except (for Exception). | ||
""" | ||
if hasattr(node.type, 'name') and node.type.name == 'StandardError': | ||
self.add_message('catch-standard-error', node=node) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine to catch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (this is a weak opinion weakly held, btw) |
||
|
||
def __minimize_code_in_try_except(self, node): # type: (astroid.TryExcept) -> None | ||
"""Minimize the amount of code in a try/except block.""" | ||
if len(node.body) > self.max_try_exc_finally_body_size: | ||
self.add_message('try-too-long', node=node) | ||
for handler in node.handlers: | ||
if len(handler.body) > self.max_try_exc_finally_body_size: | ||
self.add_message('except-too-long', node=handler) | ||
|
||
def __minimize_code_in_finally(self, node): # type: (astroid.TryFinally) -> None | ||
"""Minimize the amount of code in a finally block.""" | ||
if len(node.finalbody) > self.max_try_exc_finally_body_size: | ||
self.add_message('finally-too-long', node=node) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import sys | ||
|
||
import astroid.test_utils | ||
import pylint.testutils | ||
import pytest | ||
|
||
from shopify_python import google_styleguide | ||
|
||
|
||
class TestGoogleStyleGuideChecker(pylint.testutils.CheckerTestCase): | ||
|
||
CHECKER_CLASS = google_styleguide.GoogleStyleGuideChecker | ||
|
||
def test_importing_function_fails(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a test for importing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and attributes. |
||
root = astroid.builder.parse(""" | ||
from os.path import join | ||
from io import FileIO | ||
from os import environ | ||
""") | ||
messages = [] | ||
for node in root.body: | ||
messages.append(pylint.testutils.Message('import-modules-only', node=node)) | ||
with self.assertAddsMessages(*messages): | ||
self.walk(root) | ||
|
||
def test_importing_modules_passes(self): | ||
root = astroid.builder.parse(""" | ||
from __future__ import unicode_literals | ||
from xml import dom | ||
from xml import sax | ||
from nonexistent_package import nonexistent_module | ||
""") | ||
with self.assertNoMessages(): | ||
self.walk(root) | ||
|
||
def test_importing_relatively_fails(self): | ||
root = astroid.builder.parse(""" | ||
from . import string | ||
from .. import string | ||
""") | ||
messages = [] | ||
for node in root.body: | ||
messages.append(pylint.testutils.Message('import-full-path', node=node)) | ||
with self.assertAddsMessages(*messages): | ||
self.walk(root) | ||
|
||
def test_global_variables_fail(self): | ||
root = astroid.builder.parse(""" | ||
module_var, other_module_var = 10 | ||
__version__ = '0.0.0' | ||
class MyClass(object): | ||
class_var = 10 | ||
""") | ||
with self.assertAddsMessages( | ||
pylint.testutils.Message('global-variable', node=root.body[0].targets[0].elts[0]), | ||
pylint.testutils.Message('global-variable', node=root.body[0].targets[0].elts[1]), | ||
): | ||
self.walk(root) | ||
|
||
@pytest.mark.skipif(sys.version_info >= (3, 0), reason="Tests code that is Python 3 incompatible") | ||
def test_using_archaic_raise_fails(self): | ||
root = astroid.builder.parse(""" | ||
raise MyException, 'Error message' | ||
raise 'Error message' | ||
""") | ||
node = root.body[0] | ||
message = pylint.testutils.Message('two-arg-exception', node=node) | ||
with self.assertAddsMessages(message): | ||
self.walk(root) | ||
|
||
def test_catch_standard_error_fails(self): | ||
root = astroid.builder.parse(""" | ||
try: | ||
pass | ||
except StandardError: | ||
pass | ||
""") | ||
node = root.body[0].handlers[0] | ||
message = pylint.testutils.Message('catch-standard-error', node=node) | ||
with self.assertAddsMessages(message): | ||
self.walk(root) | ||
|
||
def test_catch_blank_passes(self): | ||
root = astroid.builder.parse(""" | ||
try: | ||
pass | ||
except: | ||
pass | ||
""") | ||
with self.assertAddsMessages(): | ||
self.walk(root) | ||
|
||
def test_try_exc_finally_size(self): | ||
root = astroid.builder.parse(""" | ||
try: | ||
# Comments are OK. | ||
# They are needed to document | ||
# complicated exception | ||
# scenarious and should | ||
# not be penalized. | ||
l = 1 | ||
except AssertionError: | ||
# Comments are OK. | ||
# They are needed to document | ||
# complicated exception | ||
# scenarious and should | ||
# not be penalized. | ||
raise | ||
finally: | ||
# Comments are OK. | ||
# They are needed to document | ||
# complicated exception | ||
# scenarious and should | ||
# not be penalized. | ||
l = 1 | ||
|
||
try: | ||
l = 1 | ||
l = 2 | ||
l = 3 | ||
l = 4 | ||
l = 5 | ||
l = 6 | ||
except AssertionError: | ||
l = 1 | ||
l = 2 | ||
l = 3 | ||
l = 4 | ||
l = 5 | ||
l = 6 | ||
finally: | ||
l = 1 | ||
l = 2 | ||
l = 3 | ||
l = 4 | ||
l = 5 | ||
l = 6 | ||
""") | ||
with self.assertAddsMessages( | ||
pylint.testutils.Message('finally-too-long', node=root.body[1]), | ||
pylint.testutils.Message('try-too-long', node=root.body[1].body[0]), | ||
pylint.testutils.Message('except-too-long', node=root.body[1].body[0].handlers[0]), | ||
): | ||
self.walk(root) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly pylint doesn't support Python 3.6 yet and our Python 3.6 tests fail.