flake8: no wildcards in core; only import * from spack in packages

There are now separate flake8 configs for core vs. packages:
- core has a smaller set of flake8 exceptions
- packages allow `from spack import *` and module globals
- Allows core to take advantage of static checking for undefined names
- Allows packages to keep using Spack tricks like `from spack import *`
  and dependencies setting globals for dependents.
This commit is contained in:
Todd Gamblin 2017-10-23 14:57:46 +02:00
parent beab0cb92e
commit 7757ebc0bc
4 changed files with 78 additions and 29 deletions

13
.flake8
View file

@ -1,8 +1,8 @@
# -*- conf -*- # -*- conf -*-
# flake8 settings for Spack. # flake8 settings for Spack core files.
# #
# Below we describe which flake8 checks Spack ignores and what the # These exceptions ar for Spack core files. We're slightly more lenient
# rationale is. # with packages. See .flake8_packages for that.
# #
# Let people line things up nicely: # Let people line things up nicely:
# - E129: visually indented line with same indent as next logical line # - E129: visually indented line with same indent as next logical line
@ -13,14 +13,9 @@
# Let people use terse Python features: # Let people use terse Python features:
# - E731: lambda expressions # - E731: lambda expressions
# #
# Spack allows wildcard imports:
# - F403: disable wildcard import
#
# These are required to get the package.py files to test clean: # These are required to get the package.py files to test clean:
# - F405: `name` may be undefined, or undefined from star imports: `module`
# - F821: undefined name `name` (needed for cmake, configure, etc.)
# - F999: syntax error in doctest # - F999: syntax error in doctest
# #
[flake8] [flake8]
ignore = E129,E221,E241,E272,E731,F403,F405,F821,F999 ignore = E129,E221,E241,E272,E731,F999
max-line-length = 79 max-line-length = 79

22
.flake8_packages Normal file
View file

@ -0,0 +1,22 @@
# -*- conf -*-
# flake8 settings for Spack package files.
#
# This should include all the same exceptions that we use for core files.
#
# In Spack packages, we also allow the single `from spack import *`
# wildcard import and dependencies can set globals for their
# dependents. So we add exceptions for checks related to undefined names.
#
# Note that we also add *per-line* exemptions for certain patters in the
# `spack flake8` command. This is where F403 for `from spack import *`
# is added (beause we *only* allow that wildcard).
#
# See .flake8 for regular exceptions.
#
# Redefinition exceptions:
# - F405: `name` may be undefined, or undefined from star imports: `module`
# - F821: undefined name `name` (needed for cmake, configure, etc.)
#
[flake8]
ignore = E129,E221,E241,E272,E731,F999,F405,F821
max-line-length = 79

View file

@ -42,18 +42,34 @@
level = "long" level = "long"
"""List of directories to exclude from checks.""" def is_package(f):
"""Whether flake8 should consider a file as a core file or a package.
We run flake8 with different exceptions for the core and for
packages, since we allow `from spack import *` and poking globals
into packages.
"""
return f.startswith('var/spack/repos/') or 'docs/tutorial/examples' in f
#: List of directories to exclude from checks.
exclude_directories = [spack.external_path] exclude_directories = [spack.external_path]
"""
This is a dict that maps: #: This is a dict that maps:
filename pattern -> #: filename pattern ->
a flake8 exemption code -> #: flake8 exemption code ->
list of patterns, for which matching lines should have codes applied. #: list of patterns, for which matching lines should have codes applied.
""" #:
exemptions = { #: For each file, if the filename pattern matches, we'll add per-line
#: exemptions if any patterns in the sub-dict match.
pattern_exemptions = {
# exemptions applied only to package.py files. # exemptions applied only to package.py files.
r'package.py$': { r'package.py$': {
# Allow 'from spack import *' in packages, but no other wildcards
'F403': [
r'^from spack import \*$'
],
# Exempt lines with urls and descriptions from overlong line errors. # Exempt lines with urls and descriptions from overlong line errors.
'E501': [ 'E501': [
r'^\s*homepage\s*=', r'^\s*homepage\s*=',
@ -87,10 +103,11 @@
} }
# compile all regular expressions. # compile all regular expressions.
exemptions = dict((re.compile(file_pattern), pattern_exemptions = dict(
dict((code, [re.compile(p) for p in patterns]) (re.compile(file_pattern),
for code, patterns in error_dict.items())) dict((code, [re.compile(p) for p in patterns])
for file_pattern, error_dict in exemptions.items()) for code, patterns in error_dict.items()))
for file_pattern, error_dict in pattern_exemptions.items())
def changed_files(args): def changed_files(args):
@ -135,7 +152,7 @@ def changed_files(args):
return sorted(changed) return sorted(changed)
def add_exemptions(line, codes): def add_pattern_exemptions(line, codes):
"""Add a flake8 exemption to a line.""" """Add a flake8 exemption to a line."""
if line.startswith('#'): if line.startswith('#'):
return line return line
@ -163,7 +180,7 @@ def add_exemptions(line, codes):
def filter_file(source, dest, output=False): def filter_file(source, dest, output=False):
"""Filter a single file through all the patterns in exemptions.""" """Filter a single file through all the patterns in pattern_exemptions."""
with open(source) as infile: with open(source) as infile:
parent = os.path.dirname(dest) parent = os.path.dirname(dest)
mkdirp(parent) mkdirp(parent)
@ -171,7 +188,9 @@ def filter_file(source, dest, output=False):
with open(dest, 'w') as outfile: with open(dest, 'w') as outfile:
for line in infile: for line in infile:
line_errors = [] line_errors = []
for file_pattern, errors in exemptions.items():
# pattern exemptions
for file_pattern, errors in pattern_exemptions.items():
if not file_pattern.search(source): if not file_pattern.search(source):
continue continue
@ -182,7 +201,7 @@ def filter_file(source, dest, output=False):
break break
if line_errors: if line_errors:
line = add_exemptions(line, line_errors) line = add_pattern_exemptions(line, line_errors)
outfile.write(line) outfile.write(line)
if output: if output:
@ -226,7 +245,6 @@ def prefix_relative(path):
with working_dir(spack.prefix): with working_dir(spack.prefix):
if not file_list: if not file_list:
file_list = changed_files(args) file_list = changed_files(args)
shutil.copy('.flake8', os.path.join(temp, '.flake8'))
print('=======================================================') print('=======================================================')
print('flake8: running flake8 code checks on spack.') print('flake8: running flake8 code checks on spack.')
@ -242,10 +260,23 @@ def prefix_relative(path):
dest_path = os.path.join(temp, filename) dest_path = os.path.join(temp, filename)
filter_file(src_path, dest_path, args.output) filter_file(src_path, dest_path, args.output)
# run flake8 on the temporary tree. # run flake8 on the temporary tree, once for core, once for pkgs
package_file_list = [f for f in file_list if is_package(f)]
file_list = [f for f in file_list if not is_package(f)]
with working_dir(temp): with working_dir(temp):
output = flake8('--format', 'pylint', *file_list, output = ''
fail_on_error=False, output=str) if file_list:
output += flake8(
'--format', 'pylint',
'--config=%s' % os.path.join(spack.prefix, '.flake8'),
*file_list, fail_on_error=False, output=str)
if package_file_list:
output += flake8(
'--format', 'pylint',
'--config=%s' % os.path.join(spack.prefix,
'.flake8_packages'),
*package_file_list, fail_on_error=False, output=str)
if args.root_relative: if args.root_relative:
# print results relative to repo root. # print results relative to repo root.

View file

@ -34,6 +34,7 @@
from spack.repository import Repo from spack.repository import Repo
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def parser(): def parser():
"""Returns the parser for the ``flake8`` command""" """Returns the parser for the ``flake8`` command"""