### Rationale While working on #29549, I noticed a lot of inconsistencies in our argparse help messages. This is important for fish where these help messages end up as descriptions in the tab completion menu. See https://github.com/spack/spack/pull/29549#issuecomment-1627596477 for some examples of longer or more stylized help messages. ### Implementation This PR makes the following changes: - [x] help messages start with a lowercase letter. - [x] Help messages do not end with a period - [x] the first line of a help message is short and simple longer text is separated by an empty line - [x] "help messages do not use triple quotes" """(except docstrings)""" - [x] Parentheses not needed for string concatenation inside function call - [x] Remove "..." "..." string concatenation leftover from black reformatting - [x] Remove Sphinx argument docs from help messages The first 2 choices aren't very controversial, and are designed to match the syntax of the `--help` flag automatically added by argparse. The 3rd choice is more up for debate, and is designed to match our package/module docstrings. The 4th choice is designed to avoid excessive newline characters and indentation. We may actually want to go even further and disallow docstrings altogether. ### Alternatives Choice 3 in particular has a lot of alternatives. My goal is solely to ensure that fish tab completion looks reasonable. Alternatives include: 1. Get rid of long help messages, only allow short simple messages 2. Move longer help messages to epilog 3. Separate by 2 newline characters instead of 1 4. Separate by period instead of newline. First sentence goes into tab completion description The number of commands with long help text is actually rather small, and is mostly relegated to `spack ci` and `spack buildcache`. So 1 isn't actually as ridiculous as it sounds. Let me know if there are any other standardizations or alternatives you would like to suggest.
220 lines
6.6 KiB
Python
220 lines
6.6 KiB
Python
# Copyright 2013-2023 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 llnl.util.tty as tty
|
|
from llnl.util.tty.color import cprint, get_color_when
|
|
|
|
import spack.cmd
|
|
import spack.cmd.common.arguments as arguments
|
|
import spack.environment as ev
|
|
import spack.solver.asp as asp
|
|
import spack.util.environment
|
|
import spack.util.spack_json as sjson
|
|
|
|
description = "compare two specs"
|
|
section = "basic"
|
|
level = "long"
|
|
|
|
|
|
def setup_parser(subparser):
|
|
arguments.add_common_arguments(subparser, ["specs"])
|
|
|
|
subparser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
default=False,
|
|
dest="dump_json",
|
|
help="dump json output instead of pretty printing",
|
|
)
|
|
subparser.add_argument(
|
|
"--first",
|
|
action="store_true",
|
|
default=False,
|
|
dest="load_first",
|
|
help="load the first match if multiple packages match the spec",
|
|
)
|
|
subparser.add_argument(
|
|
"-a",
|
|
"--attribute",
|
|
action="append",
|
|
help="select the attributes to show (defaults to all)",
|
|
)
|
|
|
|
|
|
def shift(asp_function):
|
|
"""Transforms ``attr("foo", "bar")`` into ``foo("bar")``."""
|
|
if not asp_function.args:
|
|
raise ValueError(f"Can't shift ASP function with no arguments: {str(asp_function)}")
|
|
first, *rest = asp_function.args
|
|
return asp.AspFunction(first, rest)
|
|
|
|
|
|
def compare_specs(a, b, to_string=False, color=None):
|
|
"""
|
|
Generate a comparison, including diffs (for each side) and an intersection.
|
|
|
|
We can either print the result to the console, or parse
|
|
into a json object for the user to save. We return an object that shows
|
|
the differences, intersection, and names for a pair of specs a and b.
|
|
|
|
Arguments:
|
|
a (spack.spec.Spec): the first spec to compare
|
|
b (spack.spec.Spec): the second spec to compare
|
|
a_name (str): the name of spec a
|
|
b_name (str): the name of spec b
|
|
to_string (bool): return an object that can be json dumped
|
|
color (bool): whether to format the names for the console
|
|
"""
|
|
if color is None:
|
|
color = get_color_when()
|
|
|
|
# Prepare a solver setup to parse differences
|
|
setup = asp.SpackSolverSetup()
|
|
|
|
# get facts for specs, making sure to include build dependencies of concrete
|
|
# specs and to descend into dependency hashes so we include all facts.
|
|
a_facts = set(
|
|
shift(func)
|
|
for func in setup.spec_clauses(a, body=True, expand_hashes=True, concrete_build_deps=True)
|
|
if func.name == "attr"
|
|
)
|
|
b_facts = set(
|
|
shift(func)
|
|
for func in setup.spec_clauses(b, body=True, expand_hashes=True, concrete_build_deps=True)
|
|
if func.name == "attr"
|
|
)
|
|
|
|
# We want to present them to the user as simple key: values
|
|
intersect = sorted(a_facts.intersection(b_facts))
|
|
spec1_not_spec2 = sorted(a_facts.difference(b_facts))
|
|
spec2_not_spec1 = sorted(b_facts.difference(a_facts))
|
|
|
|
# Format the spec names to be colored
|
|
fmt = "{name}{@version}{/hash}"
|
|
a_name = a.format(fmt, color=color)
|
|
b_name = b.format(fmt, color=color)
|
|
|
|
# We want to show what is the same, and then difference for each
|
|
return {
|
|
"intersect": flatten(intersect) if to_string else intersect,
|
|
"a_not_b": flatten(spec1_not_spec2) if to_string else spec1_not_spec2,
|
|
"b_not_a": flatten(spec2_not_spec1) if to_string else spec2_not_spec1,
|
|
"a_name": a_name,
|
|
"b_name": b_name,
|
|
}
|
|
|
|
|
|
def flatten(functions):
|
|
"""
|
|
Given a list of ASP functions, convert into a list of key: value tuples.
|
|
|
|
We are squashing whatever is after the first index into one string for
|
|
easier parsing in the interface
|
|
"""
|
|
updated = []
|
|
for fun in functions:
|
|
updated.append([fun.name, " ".join(str(a) for a in fun.args)])
|
|
return updated
|
|
|
|
|
|
def print_difference(c, attributes="all", out=None):
|
|
"""
|
|
Print the difference.
|
|
|
|
Given a diffset for A and a diffset for B, print red/green diffs to show
|
|
the differences.
|
|
"""
|
|
# Default to standard out unless another stream is provided
|
|
out = out or sys.stdout
|
|
|
|
A = c["b_not_a"]
|
|
B = c["a_not_b"]
|
|
|
|
cprint("@R{--- %s}" % c["a_name"]) # bright red
|
|
cprint("@G{+++ %s}" % c["b_name"]) # bright green
|
|
|
|
# Cut out early if we don't have any differences!
|
|
if not A and not B:
|
|
print("No differences\n")
|
|
return
|
|
|
|
def group_by_type(diffset):
|
|
grouped = {}
|
|
for entry in diffset:
|
|
if entry[0] not in grouped:
|
|
grouped[entry[0]] = []
|
|
grouped[entry[0]].append(entry[1])
|
|
|
|
# Sort by second value to make comparison slightly closer
|
|
for key, values in grouped.items():
|
|
values.sort()
|
|
return grouped
|
|
|
|
A = group_by_type(A)
|
|
B = group_by_type(B)
|
|
|
|
# print a directionally relevant diff
|
|
keys = list(A) + list(B)
|
|
|
|
category = None
|
|
for key in keys:
|
|
if "all" not in attributes and key not in attributes:
|
|
continue
|
|
|
|
# Write the attribute, B is subtraction A is addition
|
|
subtraction = [] if key not in B else B[key]
|
|
addition = [] if key not in A else A[key]
|
|
|
|
# Bail out early if we don't have any entries
|
|
if not subtraction and not addition:
|
|
continue
|
|
|
|
# If we have a new category, create a new section
|
|
if category != key:
|
|
category = key
|
|
|
|
# print category in bold, colorized
|
|
cprint("@*b{@@ %s @@}" % category) # bold blue
|
|
|
|
# Print subtractions first
|
|
while subtraction:
|
|
cprint("@R{- %s}" % subtraction.pop(0)) # bright red
|
|
if addition:
|
|
cprint("@G{+ %s}" % addition.pop(0)) # bright green
|
|
|
|
# Any additions left?
|
|
while addition:
|
|
cprint("@G{+ %s}" % addition.pop(0))
|
|
|
|
|
|
def diff(parser, args):
|
|
env = ev.active_environment()
|
|
|
|
if len(args.specs) != 2:
|
|
tty.die("You must provide two specs to diff.")
|
|
|
|
specs = []
|
|
for spec in spack.cmd.parse_specs(args.specs):
|
|
if spec.concrete:
|
|
specs.append(spec)
|
|
else:
|
|
specs.append(spack.cmd.disambiguate_spec(spec, env, first=args.load_first))
|
|
|
|
# Calculate the comparison (c)
|
|
color = False if args.dump_json else get_color_when()
|
|
c = compare_specs(specs[0], specs[1], to_string=True, color=color)
|
|
|
|
# Default to all attributes
|
|
attributes = args.attribute or ["all"]
|
|
|
|
if args.dump_json:
|
|
print(sjson.dump(c))
|
|
else:
|
|
tty.warn("This interface is subject to change.\n")
|
|
print_difference(c, attributes)
|