338 lines
13 KiB
Python
338 lines
13 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import imp
|
|
import inspect
|
|
import os
|
|
import re
|
|
import sys
|
|
import traceback
|
|
|
|
from hacking import core
|
|
|
|
RE_RELATIVE_IMPORT = re.compile('^from\s*[.]')
|
|
|
|
modules_cache = dict((mod, True) for mod in tuple(sys.modules.keys())
|
|
+ sys.builtin_module_names)
|
|
|
|
|
|
@core.flake8ext
|
|
def hacking_import_rules(logical_line, physical_line, filename, noqa):
|
|
r"""Check for imports.
|
|
|
|
OpenStack HACKING guide recommends one import per line:
|
|
Do not import more than one module per line
|
|
|
|
Examples:
|
|
Okay: from nova.compute import api
|
|
H301: from nova.compute import api, utils
|
|
|
|
|
|
Imports should usually be on separate lines.
|
|
|
|
OpenStack HACKING guide recommends importing only modules:
|
|
Do not import objects, only modules
|
|
|
|
Examples:
|
|
Okay: from os import path
|
|
Okay: from os import path as p
|
|
Okay: from os import (path as p)
|
|
Okay: import os.path
|
|
Okay: from nova.compute import rpcapi
|
|
Okay: from os.path import dirname as dirname2 # noqa
|
|
Okay: from six.moves.urllib import parse
|
|
H302: from os.path import dirname as dirname2
|
|
H302: from os.path import (dirname as dirname2)
|
|
H303: from os.path import *
|
|
H304: from .compute import rpcapi
|
|
"""
|
|
# TODO(jogo): make the following doctests pass:
|
|
# H301: import os, sys
|
|
# NOTE(afazekas): An old style relative import example will not be able to
|
|
# pass the doctest, since the relativity depends on the file's locality
|
|
# TODO(mordred: We need to split this into 4 different checks so that they
|
|
# can be disabled by command line switches properly
|
|
|
|
if noqa:
|
|
return
|
|
|
|
def is_module_for_sure(mod, search_path=sys.path):
|
|
mod = mod.replace('(', '') # Ignore parentheses
|
|
for finder in sys.meta_path:
|
|
if finder.find_module(mod) is not None:
|
|
return True
|
|
try:
|
|
mod_name = mod
|
|
while '.' in mod_name:
|
|
pack_name, _sep, mod_name = mod_name.partition('.')
|
|
f, p, d = imp.find_module(pack_name, search_path)
|
|
search_path = [p]
|
|
imp.find_module(mod_name, search_path)
|
|
except ImportError:
|
|
try:
|
|
# NOTE(vish): handle namespace modules
|
|
if '.' in mod:
|
|
pack_name, mod_name = mod.rsplit('.', 1)
|
|
__import__(pack_name, fromlist=[mod_name])
|
|
else:
|
|
__import__(mod)
|
|
except ImportError:
|
|
# NOTE(imelnikov): import error here means the thing is
|
|
# not importable in current environment, either because
|
|
# of missing dependency, typo in code being checked, or
|
|
# any other reason. Anyway, we have no means to know if
|
|
# it is module or not, so we return True to avoid
|
|
# false positives.
|
|
return True
|
|
except Exception:
|
|
# NOTE(jogo) don't stack trace if unexpected import error,
|
|
# log and continue.
|
|
traceback.print_exc()
|
|
return False
|
|
else:
|
|
# NOTE(imelnikov): we imported the thing; if it was module,
|
|
# it must be there:
|
|
if mod in sys.modules:
|
|
return True
|
|
else:
|
|
# NOTE(dhellmann): If the thing isn't there under
|
|
# its own name, look to see if it is a module
|
|
# redirection import in one of the oslo libraries
|
|
# where we are moving things out of the namespace
|
|
# package.
|
|
pack_name, _sep, mod_name = mod.rpartition('.')
|
|
if pack_name in sys.modules:
|
|
the_mod = getattr(sys.modules[pack_name],
|
|
mod_name, None)
|
|
return inspect.ismodule(the_mod)
|
|
return False
|
|
return True
|
|
|
|
def is_module(mod):
|
|
"""Checks for non module imports."""
|
|
if mod in modules_cache:
|
|
return modules_cache[mod]
|
|
res = is_module_for_sure(mod)
|
|
modules_cache[mod] = res
|
|
return res
|
|
|
|
current_path = os.path.dirname(filename)
|
|
current_mod = os.path.basename(filename)
|
|
if current_mod[-3:] == ".py":
|
|
current_mod = current_mod[:-3]
|
|
|
|
split_line = logical_line.split()
|
|
split_line_len = len(split_line)
|
|
if (split_line_len > 1 and split_line[0] in ('import', 'from') and
|
|
not core.is_import_exception(split_line[1])):
|
|
pos = logical_line.find(',')
|
|
if pos != -1:
|
|
if split_line[0] == 'from':
|
|
yield pos, "H301: one import per line"
|
|
return # ',' is not supported by the H302 checker yet
|
|
pos = logical_line.find('*')
|
|
if pos != -1:
|
|
yield pos, "H303: No wildcard (*) import."
|
|
return
|
|
|
|
if split_line_len in (2, 4, 6) and split_line[1] != "__future__":
|
|
if 'from' == split_line[0] and split_line_len > 3:
|
|
mod = '.'.join((split_line[1], split_line[3]))
|
|
if core.is_import_exception(mod):
|
|
return
|
|
if RE_RELATIVE_IMPORT.search(logical_line):
|
|
yield logical_line.find('.'), (
|
|
"H304: No relative imports. '%s' is a relative import"
|
|
% logical_line)
|
|
return
|
|
|
|
if not is_module(mod):
|
|
yield 0, ("H302: import only modules."
|
|
"'%s' does not import a module" % logical_line)
|
|
return
|
|
|
|
# NOTE(afazekas): import searches first in the package
|
|
# The import keyword just imports modules
|
|
# The guestfs module now imports guestfs
|
|
mod = split_line[1]
|
|
if (current_mod != mod and not is_module(mod) and
|
|
is_module_for_sure(mod, [current_path])):
|
|
yield 0, ("H304: No relative imports."
|
|
" '%s' is a relative import" % logical_line)
|
|
|
|
|
|
# Get the location of a known stdlib module
|
|
_, p, _ = imp.find_module('imp')
|
|
stdlib_path_prefix = os.path.dirname(p)
|
|
module_cache = dict()
|
|
|
|
|
|
def _get_import_type(module):
|
|
mod_base, _, _ = module.partition('.')
|
|
if mod_base in module_cache:
|
|
return module_cache[mod_base]
|
|
|
|
def cache_type(module_type):
|
|
module_cache[mod_base] = module_type
|
|
return module_type
|
|
|
|
# First check if the module is local
|
|
try:
|
|
imp.find_module(mod_base, ['.'])
|
|
# If the previous line succeeded then it must be a project module
|
|
return cache_type('project')
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
_, path, _ = imp.find_module(mod_base)
|
|
except ImportError:
|
|
return cache_type('third-party')
|
|
|
|
if path is None:
|
|
# NOTE(imelnikov): python 3 returns None for path of builtin
|
|
# modules, like sys or builtin; they are definitely stdlib
|
|
return cache_type('stdlib')
|
|
if 'site-packages' in path or 'dist-packages' in path:
|
|
return cache_type('third-party')
|
|
std_paths = [stdlib_path_prefix, sys.prefix]
|
|
|
|
# NOTE(imelnikov): if we are in virtualenv, we should consider
|
|
# real prefix too, as python 3 copies some stdlib modules to
|
|
# virtualenv, but not all of them.
|
|
real_prefix = getattr(sys, 'real_prefix', None)
|
|
if real_prefix is not None:
|
|
std_paths.append(real_prefix)
|
|
|
|
if path == module or any(path.startswith(p) for p in std_paths):
|
|
return cache_type('stdlib')
|
|
return cache_type('third-party')
|
|
|
|
|
|
@core.flake8ext
|
|
def hacking_import_groups(logical_line, blank_before, previous_logical,
|
|
indent_level, previous_indent_level, physical_line,
|
|
noqa):
|
|
r"""Check that imports are grouped correctly.
|
|
|
|
OpenStack HACKING guide recommendation for imports:
|
|
imports grouped such that Python standard library imports are together,
|
|
third party library imports are together, and project imports are
|
|
together
|
|
|
|
Okay: import os\nimport sys\n\nimport six\n\nimport hacking
|
|
Okay: import six\nimport znon_existent_package
|
|
Okay: import os\nimport threading
|
|
H305: import hacking\nimport os
|
|
H305: import os\nimport six
|
|
H305: import os\nimport znon_existent_package
|
|
"""
|
|
if (noqa or blank_before > 0 or
|
|
indent_level != previous_indent_level):
|
|
return
|
|
|
|
normalized_line = core.import_normalize(logical_line.strip()).split()
|
|
normalized_previous = core.import_normalize(previous_logical.
|
|
strip()).split()
|
|
if normalized_line and normalized_line[0] == 'import':
|
|
current_type = _get_import_type(normalized_line[1])
|
|
if normalized_previous and normalized_previous[0] == 'import':
|
|
previous_type = _get_import_type(normalized_previous[1])
|
|
if current_type != previous_type:
|
|
yield(0, 'H305: imports not grouped correctly '
|
|
'(%s: %s, %s: %s)' %
|
|
(normalized_previous[1], previous_type,
|
|
normalized_line[1], current_type))
|
|
|
|
|
|
@core.flake8ext
|
|
def hacking_import_alphabetical(logical_line, blank_before, previous_logical,
|
|
indent_level, previous_indent_level):
|
|
r"""Check for imports in alphabetical order.
|
|
|
|
OpenStack HACKING guide recommendation for imports:
|
|
imports in human alphabetical order
|
|
|
|
Okay: import os\nimport sys\n\nimport nova\nfrom nova import test
|
|
Okay: import os\nimport sys
|
|
H306: import sys\nimport os
|
|
Okay: import sys\n\n# foo\nimport six
|
|
"""
|
|
# handle import x
|
|
# use .lower since capitalization shouldn't dictate order
|
|
if blank_before < 1 and indent_level == previous_indent_level:
|
|
split_line = core.import_normalize(logical_line.
|
|
strip()).lower().split()
|
|
split_previous = core.import_normalize(previous_logical.
|
|
strip()).lower().split()
|
|
length = [2, 4]
|
|
if (len(split_line) in length and len(split_previous) in length and
|
|
split_line[0] == "import" and split_previous[0] == "import"):
|
|
if split_line[1] < split_previous[1]:
|
|
yield (0, "H306: imports not in alphabetical order (%s, %s)"
|
|
% (split_previous[1], split_line[1]))
|
|
|
|
|
|
class ImportGroupData:
|
|
"""A class to hold persistent state data for import group checks.
|
|
|
|
To verify import grouping, it is necessary to know the current group
|
|
for the current file. This can not always be known solely from the
|
|
current and previous line, so this class can be used to keep track.
|
|
"""
|
|
|
|
# NOTE(bnemec): *args is needed because the test code tries to run this
|
|
# as a flake8 check and passes an argument to it.
|
|
def __init__(self, *args):
|
|
self.current_group = None
|
|
self.current_filename = None
|
|
self.current_import = None
|
|
|
|
|
|
together_data = ImportGroupData()
|
|
|
|
|
|
@core.flake8ext
|
|
def hacking_import_groups_together(logical_line, blank_lines, indent_level,
|
|
previous_indent_level, line_number,
|
|
physical_line, filename, noqa):
|
|
r"""Check that like imports are grouped together.
|
|
|
|
OpenStack HACKING guide recommendation for imports:
|
|
Imports should be grouped together by type.
|
|
|
|
Okay: import os\nimport sys
|
|
Okay: try:\n import foo\nexcept ImportError:\n pass\n\nimport six
|
|
H307: import os\n\nimport sys
|
|
"""
|
|
if line_number == 1 or filename != together_data.current_filename:
|
|
together_data.current_group = None
|
|
together_data.current_filename = filename
|
|
|
|
if noqa:
|
|
return
|
|
|
|
normalized_line = core.import_normalize(logical_line.strip()).split()
|
|
if normalized_line and normalized_line[0] == 'import':
|
|
current_type = _get_import_type(normalized_line[1])
|
|
previous_import = together_data.current_import
|
|
together_data.current_import = normalized_line[1]
|
|
matched = current_type == together_data.current_group
|
|
together_data.current_group = current_type
|
|
if (matched and indent_level == previous_indent_level and
|
|
blank_lines >= 1):
|
|
yield(0, 'H307: like imports should be grouped together (%s and '
|
|
'%s from %s are separated by whitespace)' %
|
|
(previous_import,
|
|
together_data.current_import,
|
|
current_type))
|