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

Add some Google Styleguide formatting rules #4

Merged
merged 17 commits into from
Mar 6, 2017
Merged

Conversation

cfournie
Copy link
Contributor

@cfournie cfournie commented Mar 3, 2017

Let's try to adhere to the Google Python Style Guide using pylint. This PR adds rules for:

  • "Use imports for packages and modules only";
  • "Import each module using the full pathname location of the module";
  • "Avoid global variables.".

@@ -6,7 +6,6 @@ python:
- "3.3"
- "3.4"
- "3.5"
- "3.6"
Copy link
Contributor Author

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.

@@ -27,7 +27,7 @@ autolint: autopep8 lint
run_tests: clean
py.test --durations=10 .

test: autopep8 lint run_tests
test: autopep8 run_tests lint
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's lint ourselves.

self._import_modules_only(node)
self._import_full_path_only(node)

def _import_modules_only(self, node): # type: (astroid.ImportFrom) -> None
Copy link
Member

Choose a reason for hiding this comment

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

Double-underscore class-private methods as per the code style guidelines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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



class GoogleStyleGuideChecker(checkers.BaseChecker):
__implements__ = (interfaces.IAstroidChecker,)
Copy link
Member

Choose a reason for hiding this comment

The 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, _ = name
module = '.'.join((node.modname, name))
try:
importlib.import_module(module)
Copy link
Member

Choose a reason for hiding this comment

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

This has the potential for side-effects. It would be nice if we could check at least if the rule was disabled globally, and not execute this if required.

"""
def can_import(module):
try:
importlib.import_module(module)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has the potential for side-effects. It would be nice if we could check at least if the rule was disabled globally, and not execute this if required.

Choose a reason for hiding this comment

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

Can you use inspect for this? Squinting at the docs, it doesn't look like it requires an actual import.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I could get the path to the source file then I could use inspect. I can get the path by importing the module and then calling __file__on it but that defeats the purpose of this. I'm not sure if there's a way to resolve package.module into path/to/package/module without actually importing the module.

Copy link

@honkfestival honkfestival left a comment

Choose a reason for hiding this comment

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

Just a request to avoid importing if possible. And some test cases.

name = 'google-styleguide-checker'

msgs = {
'C9901': ('Imported an object that is not a package or module',

Choose a reason for hiding this comment

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

Is there any official advice on choosing the msg_id for new rules? Is e.g. the C99XX range reserved at all, or are we just trying to avoid collisions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The pylint docs on custom checkers state:

The message id should be a 5-digits number, prefixed with a message category. There are multiple message categories, these being C, W, E, F, R, standing for Convention, Warning, Error, Fatal and Refactoring. The rest of the 5 digits should not conflict with existing checkers and they should be consistent across the checker. For instance, the first two digits should not be different across the checker.

"""
def can_import(module):
try:
importlib.import_module(module)

Choose a reason for hiding this comment

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

Can you use inspect for this? Squinting at the docs, it doesn't look like it requires an actual import.


CHECKER_CLASS = google_styleguide.GoogleStyleGuideChecker

def test_importing_function_fails(self):

Choose a reason for hiding this comment

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

Please add a test for importing classes as well.

Choose a reason for hiding this comment

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

Oh, and attributes.

from pylint import lint
from shopify_python import google_styleguide


__version__ = '0.0.0'

Choose a reason for hiding this comment

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

Not really for this PR, but we should have a process for deciding how and when to bump versions.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bails before we call importlib.import_module if this message is disabled.

Copy link
Member

Choose a reason for hiding this comment

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

👍

@JasonMWhite
Copy link
Member

:shipit:

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

def can_import(module):

Choose a reason for hiding this comment

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

We talked IRL about maybe using importlib's find_spec to perform this check instead.

Choose a reason for hiding this comment

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

(there are older, deprecated functions for doing this for versions < 3.4)

Pylint already handles bare-except and broad-except (for Exception).
"""
if node.type.name == 'StandardError':
self.add_message('catch-standard-error', node=node)

Choose a reason for hiding this comment

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

I think it's fine to catch Exception as long as you re-raise later on in the handler. You might also want to support the exception chaining syntax.

Choose a reason for hiding this comment

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

(this is a weak opinion weakly held, btw)


Pylint already handles bare-except and broad-except (for Exception).
"""
if node.type.name == 'StandardError':

Choose a reason for hiding this comment

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

Don't you mean to check for Exception here as well? (also see below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nay, pylint already checks for Exception as broad-except.

Choose a reason for hiding this comment

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

sorry, I didn't read carefully enough

"Use either raise Exception('message') or raise Exception"),
'C2606': ('Caught StandardError',
'catch-standard-error',
"Don't catch broad exceptions"),

Choose a reason for hiding this comment

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

This line is inconsistent with the specificity of the other two lines above.

"The larger the except body, the more likely that an exception will be raised"),
}

max_try_exc_finally_body_size = 5

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@cfournie cfournie Mar 3, 2017

Choose a reason for hiding this comment

The 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).

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

Choose a reason for hiding this comment

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

If people intentionally rewrite their code to get around linter rules instead of adhering to the principle, that's what code review is for.

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 try block than too many lines of code split up into functions.

I'm in favour of the spirit of the rule, but not this particular implementation.

Another option is to specify an overridable default, similar to the "max lines per module" rule.

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.

Choose a reason for hiding this comment

The 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

👍

try:
return bool(importlib.util.find_spec(module_name))
except AttributeError:
return False

Choose a reason for hiding this comment

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

👍

self.add_message('import-full-path', node=node)

def __avoid_global_variables(self, node): # type: (astroid.Assign) -> None
"""Avoid global variables."""

Choose a reason for hiding this comment

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

Can you please expand the docstring to explain how this works?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.


Pylint already handles bare-except and broad-except (for Exception).
"""
if node.type.name == 'StandardError':
Copy link
Member

@JasonMWhite JasonMWhite Mar 6, 2017

Choose a reason for hiding this comment

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

This fails if no exception type is given, e.g.:

try:
    foo()
except:
    bar()

node.type is None in this case

def can_import(module_name):
if sys.version_info >= (3, 4):
try:
return bool(importlib.util.find_spec(module_name))
Copy link
Member

Choose a reason for hiding this comment

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

This throws an ImportError when trying to import pyspark, which of course needs some path mangling to import dynamically. I think we should just ignore on ImportError.

Choose a reason for hiding this comment

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

boo-urns

@cfournie cfournie force-pushed the google_styleguide branch from d6da79b to f84246f Compare March 6, 2017 16:23
@cfournie cfournie force-pushed the google_styleguide branch from f84246f to 541710e Compare March 6, 2017 16:25
@honkfestival
Copy link

LGTM after addressing @JasonMWhite's pyspark comment

@cfournie cfournie merged commit 858424f into master Mar 6, 2017
@cfournie cfournie deleted the google_styleguide branch March 6, 2017 17:32
zeedann added a commit that referenced this pull request Apr 12, 2017
# This is the 1st commit message:
added some tets

# This is the commit message #2:

show me numbers not types

# This is the commit message #3:

tests and giggles

# This is the commit message #4:

linted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants