summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTodd Gamblin <gamblin2@llnl.gov>2021-07-27 14:09:17 -0700
committerGitHub <noreply@github.com>2021-07-27 15:09:17 -0600
commitc8efec02957b6381979a952156d8ef64fd792e3a (patch)
treec392f53334531a30180d52d1aba611b5d0e4c813 /lib
parente5bbb6e5b4a1af25dd46101bfc044d04deb10cb5 (diff)
downloadspack-c8efec02957b6381979a952156d8ef64fd792e3a.tar.gz
spack-c8efec02957b6381979a952156d8ef64fd792e3a.tar.bz2
spack-c8efec02957b6381979a952156d8ef64fd792e3a.tar.xz
spack-c8efec02957b6381979a952156d8ef64fd792e3a.zip
`spack style`: add `--root` option (#25085)
This adds a `--root` option so that `spack style` can check style for a spack instance other than its own. We also change the inner workings of `spack style` so that `--config FILE` (and similar options for the various tools) options are used. This ensures that when `spack style` runs, it always uses the config from the running spack, and does *not* pick up configuration from the external root. - [x] add `--root` option to `spack style` - [x] add `--config` (or similar) option when invoking style tools - [x] add a test that verifies we can check an external instance
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/cmd/style.py137
-rw-r--r--lib/spack/spack/test/cmd/style.py172
-rw-r--r--lib/spack/spack/test/data/style/broken.dummy12
-rw-r--r--lib/spack/spack/test/data/style/fixed.py14
4 files changed, 285 insertions, 50 deletions
diff --git a/lib/spack/spack/cmd/style.py b/lib/spack/spack/cmd/style.py
index 2841a4af0f..473827366b 100644
--- a/lib/spack/spack/cmd/style.py
+++ b/lib/spack/spack/cmd/style.py
@@ -39,11 +39,10 @@ def grouper(iterable, n, fillvalue=None):
yield filter(None, group)
-#: directory where spack style started, for relativizing paths
-initial_working_dir = None
-
-#: List of directories to exclude from checks.
-exclude_directories = [spack.paths.external_path]
+#: List of directories to exclude from checks -- relative to spack root
+exclude_directories = [
+ os.path.relpath(spack.paths.external_path, spack.paths.prefix),
+]
#: Order in which tools should be run. flake8 is last so that it can
#: double-check the results of other tools (if, e.g., --fix was provided)
@@ -66,7 +65,7 @@ def is_package(f):
packages, since we allow `from spack import *` and poking globals
into packages.
"""
- return f.startswith("var/spack/repos/") or "docs/tutorial/examples" in f
+ return f.startswith("var/spack/repos/")
#: decorator for adding tools to the list
@@ -80,8 +79,10 @@ class tool(object):
return fun
-def changed_files(base=None, untracked=True, all_files=False):
+def changed_files(base=None, untracked=True, all_files=False, root=None):
"""Get list of changed files in the Spack repository."""
+ if root is None:
+ root = spack.paths.prefix
git = which("git", required=True)
@@ -89,6 +90,16 @@ def changed_files(base=None, untracked=True, all_files=False):
if base is None:
base = os.environ.get("GITHUB_BASE_REF", "develop")
+ # ensure base is in the repo
+ git("show-ref", "--verify", "--quiet", "refs/heads/%s" % base,
+ fail_on_error=False)
+ if git.returncode != 0:
+ tty.die(
+ "This repository does not have a '%s' branch." % base,
+ "spack style needs this branch to determine which files changed.",
+ "Ensure that '%s' exists, or specify files to check explicitly." % base
+ )
+
range = "{0}...".format(base)
git_args = [
@@ -108,7 +119,10 @@ def changed_files(base=None, untracked=True, all_files=False):
if all_files:
git_args.append(["ls-files", "--exclude-standard"])
- excludes = [os.path.realpath(f) for f in exclude_directories]
+ excludes = [
+ os.path.realpath(os.path.join(root, f))
+ for f in exclude_directories
+ ]
changed = set()
for arg_list in git_args:
@@ -189,13 +203,19 @@ def setup_parser(subparser):
help="run black if available (default: skip black)",
)
subparser.add_argument(
+ "--root",
+ action="store",
+ default=None,
+ help="style check a different spack instance",
+ )
+ subparser.add_argument(
"files", nargs=argparse.REMAINDER, help="specific files to check"
)
-def cwd_relative(path):
+def cwd_relative(path, args):
"""Translate prefix-relative path to current working directory-relative."""
- return os.path.relpath(os.path.join(spack.paths.prefix, path), initial_working_dir)
+ return os.path.relpath(os.path.join(args.root, path), args.initial_working_dir)
def rewrite_and_print_output(
@@ -205,7 +225,7 @@ def rewrite_and_print_output(
# print results relative to current working directory
def translate(match):
return replacement.format(
- cwd_relative(match.group(1)), *list(match.groups()[1:])
+ cwd_relative(match.group(1), args), *list(match.groups()[1:])
)
for line in output.split("\n"):
@@ -218,14 +238,14 @@ def rewrite_and_print_output(
def print_style_header(file_list, args):
tools = [tool for tool, _ in tool_order if getattr(args, tool)]
- tty.msg("Running style checks on spack:", "selected: " + ", ".join(tools))
+ tty.msg("Running style checks on spack", "selected: " + ", ".join(tools))
# translate modified paths to cwd_relative if needed
paths = [filename.strip() for filename in file_list]
if not args.root_relative:
- paths = [cwd_relative(filename) for filename in paths]
+ paths = [cwd_relative(filename, args) for filename in paths]
- tty.msg("Modified files:", *paths)
+ tty.msg("Modified files", *paths)
sys.stdout.flush()
@@ -249,12 +269,9 @@ def run_flake8(flake8_cmd, file_list, args):
# run in chunks of 100 at a time to avoid line length limit
# filename parameter in config *does not work* for this reliably
for chunk in grouper(file_list, 100):
-
output = flake8_cmd(
- # use .flake8 implicitly to work around bug in flake8 upstream
- # append-config is ignored if `--config` is explicitly listed
- # see: https://gitlab.com/pycqa/flake8/-/issues/455
- # "--config=.flake8",
+ # always run with config from running spack prefix
+ "--config=%s" % os.path.join(spack.paths.prefix, ".flake8"),
*chunk,
fail_on_error=False,
output=str
@@ -269,12 +286,18 @@ def run_flake8(flake8_cmd, file_list, args):
@tool("mypy")
def run_mypy(mypy_cmd, file_list, args):
- mpy_args = ["--package", "spack", "--package", "llnl", "--show-error-codes"]
+ # always run with config from running spack prefix
+ mypy_args = [
+ "--config-file", os.path.join(spack.paths.prefix, "pyproject.toml"),
+ "--package", "spack",
+ "--package", "llnl",
+ "--show-error-codes",
+ ]
# not yet, need other updates to enable this
# if any([is_package(f) for f in file_list]):
- # mpy_args.extend(["--package", "packages"])
+ # mypy_args.extend(["--package", "packages"])
- output = mypy_cmd(*mpy_args, fail_on_error=False, output=str)
+ output = mypy_cmd(*mypy_args, fail_on_error=False, output=str)
returncode = mypy_cmd.returncode
rewrite_and_print_output(output, args)
@@ -285,13 +308,16 @@ def run_mypy(mypy_cmd, file_list, args):
@tool("isort")
def run_isort(isort_cmd, file_list, args):
- check_fix_args = () if args.fix else ("--check", "--diff")
+ # always run with config from running spack prefix
+ isort_args = ("--settings-file", os.path.join(spack.paths.prefix, "pyproject.toml"))
+ if not args.fix:
+ isort_args += ("--check", "--diff")
pat = re.compile("ERROR: (.*) Imports are incorrectly sorted")
replacement = "ERROR: {0} Imports are incorrectly sorted"
returncode = 0
for chunk in grouper(file_list, 100):
- packed_args = check_fix_args + tuple(chunk)
+ packed_args = isort_args + tuple(chunk)
output = isort_cmd(*packed_args, fail_on_error=False, output=str, error=str)
returncode |= isort_cmd.returncode
@@ -303,7 +329,12 @@ def run_isort(isort_cmd, file_list, args):
@tool("black")
def run_black(black_cmd, file_list, args):
- check_fix_args = () if args.fix else ("--check", "--diff", "--color")
+ # always run with config from running spack prefix
+ black_args = ("--config", os.path.join(spack.paths.prefix, "pyproject.toml"))
+ if not args.fix:
+ black_args += ("--check", "--diff")
+ if color.get_color_when(): # only show color when spack would
+ black_args += ("--color",)
pat = re.compile("would reformat +(.*)")
replacement = "would reformat {0}"
@@ -312,33 +343,49 @@ def run_black(black_cmd, file_list, args):
# run in chunks of 100 at a time to avoid line length limit
# filename parameter in config *does not work* for this reliably
for chunk in grouper(file_list, 100):
- packed_args = check_fix_args + tuple(chunk)
+ packed_args = black_args + tuple(chunk)
output = black_cmd(*packed_args, fail_on_error=False, output=str, error=str)
returncode |= black_cmd.returncode
rewrite_and_print_output(output, args, pat, replacement)
print_tool_result("black", returncode)
+
return returncode
def style(parser, args):
+ # ensure python version is new enough
+ if sys.version_info < (3, 6):
+ tty.die("spack style requires Python 3.6 or later.")
+
# save initial working directory for relativizing paths later
- global initial_working_dir
- initial_working_dir = os.getcwd()
+ args.initial_working_dir = os.getcwd()
+
+ # ensure that the config files we need actually exist in the spack prefix.
+ # assertions b/c users should not ever see these errors -- they're checked in CI.
+ assert os.path.isfile(os.path.join(spack.paths.prefix, "pyproject.toml"))
+ assert os.path.isfile(os.path.join(spack.paths.prefix, ".flake8"))
+
+ # validate spack root if the user provided one
+ args.root = os.path.realpath(args.root) if args.root else spack.paths.prefix
+ spack_script = os.path.join(args.root, "bin", "spack")
+ if not os.path.exists(spack_script):
+ tty.die(
+ "This does not look like a valid spack root.",
+ "No such file: '%s'" % spack_script
+ )
file_list = args.files
if file_list:
def prefix_relative(path):
- return os.path.relpath(
- os.path.abspath(os.path.realpath(path)), spack.paths.prefix
- )
+ return os.path.relpath(os.path.abspath(os.path.realpath(path)), args.root)
file_list = [prefix_relative(p) for p in file_list]
returncode = 0
- with working_dir(spack.paths.prefix):
+ with working_dir(args.root):
if not file_list:
file_list = changed_files(args.base, args.untracked, args.all)
print_style_header(file_list, args)
@@ -350,17 +397,23 @@ def style(parser, args):
run_function, required = tools[tool_name]
print_tool_header(tool_name)
- # Bootstrap tools so we don't need to require install
- with spack.bootstrap.ensure_bootstrap_configuration():
- spec = spack.spec.Spec(tool_spec)
- cmd = spack.bootstrap.get_executable(
- tool_name, spec=spec, install=True
+ try:
+ # Bootstrap tools so we don't need to require install
+ with spack.bootstrap.ensure_bootstrap_configuration():
+ spec = spack.spec.Spec(tool_spec)
+ cmd = None
+ cmd = spack.bootstrap.get_executable(
+ tool_name, spec=spec, install=True
+ )
+ if not cmd:
+ color.cprint(" @y{%s not in PATH, skipped}" % tool_name)
+ continue
+ returncode |= run_function(cmd, file_list, args)
+
+ except Exception as e:
+ raise spack.error.SpackError(
+ "Couldn't bootstrap %s:" % tool_name, str(e)
)
- if not cmd:
- color.cprint(" @y{%s not in PATH, skipped}" % tool_name)
- continue
-
- returncode |= run_function(cmd, file_list, args)
if returncode == 0:
tty.msg(color.colorize("@*{spack style checks were clean}"))
diff --git a/lib/spack/spack/test/cmd/style.py b/lib/spack/spack/test/cmd/style.py
index ceaae5f694..65a301bc27 100644
--- a/lib/spack/spack/test/cmd/style.py
+++ b/lib/spack/spack/test/cmd/style.py
@@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import filecmp
import os
import shutil
import sys
@@ -17,8 +18,20 @@ import spack.repo
from spack.cmd.style import changed_files
from spack.util.executable import which
+#: directory with sample style files
+style_data = os.path.join(spack.paths.test_path, 'data', 'style')
+
+
style = spack.main.SpackCommand("style")
+# spack style requires git to run -- skip the tests if it's not there
+pytestmark = pytest.mark.skipif(not which('git'), reason='requires git')
+
+# The style tools have requirements to use newer Python versions. We simplify by
+# requiring Python 3.6 or higher to run spack style.
+skip_old_python = pytest.mark.skipif(
+ sys.version_info < (3, 6), reason='requires Python 3.6 or higher')
+
@pytest.fixture(scope="function")
def flake8_package():
@@ -50,6 +63,9 @@ def flake8_package_with_errors(scope="function"):
shutil.copy(filename, tmp)
package = FileFilter(filename)
package.filter("state = 'unmodified'", "state = 'modified'", string=True)
+ package.filter(
+ "from spack import *", "from spack import *\nimport os", string=True
+ )
yield filename
finally:
shutil.move(tmp, filename)
@@ -65,6 +81,24 @@ def test_changed_files(flake8_package):
assert flake8_package in files
+def test_changed_no_base(tmpdir, capfd):
+ """Ensure that we fail gracefully with no base branch."""
+ tmpdir.join("bin").ensure("spack")
+ git = which("git", required=True)
+ with tmpdir.as_cwd():
+ git("init")
+ git("config", "user.name", "test user")
+ git("config", "user.email", "test@user.com")
+ git("add", ".")
+ git("commit", "-m", "initial commit")
+
+ with pytest.raises(SystemExit):
+ changed_files(base="foobar")
+
+ out, err = capfd.readouterr()
+ assert "This repository does not have a 'foobar' branch." in err
+
+
def test_changed_files_all_files(flake8_package):
# it's hard to guarantee "all files", so do some sanity checks.
files = set([
@@ -92,12 +126,134 @@ def test_changed_files_all_files(flake8_package):
assert not any(f.startswith(spack.paths.external_path) for f in files)
-# As of flake8 3.0.0, Python 2.6 and 3.3 are no longer supported
-# http://flake8.pycqa.org/en/latest/release-notes/3.0.0.html
-skip_old_python = pytest.mark.skipif(
- sys.version_info[:2] <= (2, 6) or (3, 0) <= sys.version_info[:2] <= (3, 3),
- reason="flake8 no longer supports Python 2.6 or 3.3 and older",
-)
+@pytest.mark.skipif(sys.version_info >= (3, 6), reason="doesn't apply to newer python")
+def test_fail_on_old_python():
+ """Ensure that `spack style` runs but fails with older python."""
+ style(fail_on_error=False)
+ assert style.returncode != 0
+
+
+@skip_old_python
+def test_bad_root(tmpdir):
+ """Ensure that `spack style` doesn't run on non-spack directories."""
+ style("--root", str(tmpdir), fail_on_error=False)
+ assert style.returncode != 0
+
+
+def test_style_is_package(tmpdir):
+ """Ensure the is_package() function works."""
+ assert spack.cmd.style.is_package(
+ "var/spack/repos/builtin/packages/hdf5/package.py"
+ )
+ assert spack.cmd.style.is_package(
+ "var/spack/repos/builtin/packages/zlib/package.py"
+ )
+ assert not spack.cmd.style.is_package("lib/spack/spack/spec.py")
+ assert not spack.cmd.style.is_package("lib/spack/external/pytest.py")
+
+
+@skip_old_python
+def test_bad_bootstrap(monkeypatch):
+ """Ensure we fail gracefully when we can't bootstrap spack style."""
+ monkeypatch.setattr(spack.cmd.style, "tool_order", [
+ ("foobartool", "foobartool"), # bad package to force concretization failure
+ ])
+ style(fail_on_error=False)
+ assert style.returncode != 0
+
+
+@pytest.fixture
+def external_style_root(flake8_package_with_errors, tmpdir):
+ """Create a mock git repository for running spack style."""
+ git = which("git", required=True)
+
+ # create a sort-of spack-looking directory
+ script = tmpdir / "bin" / "spack"
+ script.ensure()
+ spack_dir = tmpdir / "lib" / "spack" / "spack"
+ spack_dir.ensure("__init__.py")
+ llnl_dir = tmpdir / "lib" / "spack" / "llnl"
+ llnl_dir.ensure("__init__.py")
+
+ # create a base develop branch
+ with tmpdir.as_cwd():
+ git("init")
+ git("config", "user.name", "test user")
+ git("config", "user.email", "test@user.com")
+ git("add", ".")
+ git("commit", "-m", "initial commit")
+ git("branch", "-m", "develop")
+ git("checkout", "-b", "feature")
+
+ # copy the buggy package in
+ py_file = spack_dir / "dummy.py"
+ py_file.ensure()
+ shutil.copy(flake8_package_with_errors, str(py_file))
+
+ # add the buggy file on the feature branch
+ with tmpdir.as_cwd():
+ git("add", str(py_file))
+ git("commit", "-m", "add new file")
+
+ yield tmpdir, py_file
+
+
+@skip_old_python
+@pytest.mark.skipif(not which("black"), reason="black is not installed.")
+@pytest.mark.skipif(not which("flake8"), reason="flake8 is not installed.")
+def test_fix_style(external_style_root):
+ """Make sure spack style --fix works."""
+ tmpdir, py_file = external_style_root
+
+ broken_dummy = os.path.join(style_data, "broken.dummy")
+ broken_py = str(tmpdir / "lib" / "spack" / "spack" / "broken.py")
+ fixed_py = os.path.join(style_data, "fixed.py")
+
+ shutil.copy(broken_dummy, broken_py)
+ assert not filecmp.cmp(broken_py, fixed_py)
+
+ output = style(
+ "--root", str(tmpdir),
+ "--no-mypy", # mypy doesn't fix, so skip it
+ "--no-flake8", # flake8 doesn't fix, so skip it
+ "--black",
+ "--fix",
+ )
+ print(output)
+ assert filecmp.cmp(broken_py, fixed_py)
+
+
+@skip_old_python
+@pytest.mark.skipif(not which("flake8"), reason="flake8 is not installed.")
+@pytest.mark.skipif(not which("isort"), reason="isort is not installed.")
+@pytest.mark.skipif(not which("mypy"), reason="mypy is not installed.")
+@pytest.mark.skipif(not which("black"), reason="black is not installed.")
+def test_external_root(external_style_root):
+ """Ensure we can run in a separate root directory w/o configuration files."""
+ tmpdir, py_file = external_style_root
+
+ # make sure tools are finding issues with external root,
+ # not the real one.
+ output = style(
+ "--root-relative", "--black", "--root", str(tmpdir),
+ fail_on_error=False
+ )
+
+ # make sure it failed
+ assert style.returncode != 0
+
+ # isort error
+ assert "%s Imports are incorrectly sorted" % str(py_file) in output
+
+ # mypy error
+ assert 'lib/spack/spack/dummy.py:10: error: Name "Package" is not defined' in output
+
+ # black error
+ assert "--- lib/spack/spack/dummy.py" in output
+ assert "+++ lib/spack/spack/dummy.py" in output
+
+ # flake8 error
+ assert "lib/spack/spack/dummy.py:7: [F401] 'os' imported but unused" in output
@skip_old_python
@@ -138,7 +294,7 @@ def test_style_with_errors(flake8_package_with_errors):
root_relative = os.path.relpath(flake8_package_with_errors, spack.paths.prefix)
output = style("--root-relative", flake8_package_with_errors, fail_on_error=False)
assert root_relative in output
- assert style.returncode == 1
+ assert style.returncode != 0
assert "spack style found errors" in output
@@ -148,5 +304,5 @@ def test_style_with_errors(flake8_package_with_errors):
def test_style_with_black(flake8_package_with_errors):
output = style("--black", flake8_package_with_errors, fail_on_error=False)
assert "black found errors" in output
- assert style.returncode == 1
+ assert style.returncode != 0
assert "spack style found errors" in output
diff --git a/lib/spack/spack/test/data/style/broken.dummy b/lib/spack/spack/test/data/style/broken.dummy
new file mode 100644
index 0000000000..5044a2f476
--- /dev/null
+++ b/lib/spack/spack/test/data/style/broken.dummy
@@ -0,0 +1,12 @@
+# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+import sys
+import os
+
+def this_is_a_function():
+ """This is a docstring."""
+ def this_should_be_offset():
+ sys.stdout.write(os.name)
diff --git a/lib/spack/spack/test/data/style/fixed.py b/lib/spack/spack/test/data/style/fixed.py
new file mode 100644
index 0000000000..76eec009c6
--- /dev/null
+++ b/lib/spack/spack/test/data/style/fixed.py
@@ -0,0 +1,14 @@
+# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+import os
+import sys
+
+
+def this_is_a_function():
+ """This is a docstring."""
+
+ def this_should_be_offset():
+ sys.stdout.write(os.name)