format: allow spaces in format specifiers (#45487)

* format: allow spaces in format specifiers

Key-value pair format specifiers can now contain spaces in the key. This allows us to
add spaces to format strings that are *only* present when the attribute formatted is not
``None``. Instead of writing:

```
    {arch=architecture}
```

and special casing `arch=` like a sigil in `Spec.format()`, we can now write:

```
    { arch=architecture}
```

And the space is *only* printed when `architecture` is not `None`. This allows us to
remove the special case in `Spec.format()` for `arch=`.

Previously the only `key=` prefix allowed in format specifiers was `arch=`, but this PR
removes that requirement, and the `key=` part of a key-value specifier can be any name.
It does *not* have to correspond to the formatted attribute.

- [x] modify `SPEC_FORMAT_RE` to allow arbitrary keys in key-value pairs.
- [x] remove special case for `arch=` from `Spec.format()`.
- [x] modify format strings using `{arch=architecture}` to use `{ arch=architecture}`
- [x] add more tests for formatting

This PR saves other more complex attributes like compiler flags and their spacing for later.

Signed-off-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
Todd Gamblin 2024-08-01 17:20:43 -07:00 committed by GitHub
parent 65b530e7ec
commit 96ddbd5e17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 87 additions and 46 deletions

View File

@ -71,7 +71,7 @@
# TODO: Remove this in Spack 0.23 # TODO: Remove this in Spack 0.23
SHARED_PR_MIRROR_URL = "s3://spack-binaries-prs/shared_pr_mirror" SHARED_PR_MIRROR_URL = "s3://spack-binaries-prs/shared_pr_mirror"
JOB_NAME_FORMAT = ( JOB_NAME_FORMAT = (
"{name}{@version} {/hash:7} {%compiler.name}{@compiler.version}{arch=architecture}" "{name}{@version} {/hash:7} {%compiler.name}{@compiler.version}{ arch=architecture}"
) )
IS_WINDOWS = sys.platform == "win32" IS_WINDOWS = sys.platform == "win32"
spack_gpg = spack.main.SpackCommand("gpg") spack_gpg = spack.main.SpackCommand("gpg")

View File

@ -237,7 +237,7 @@ def ensure_single_spec_or_die(spec, matching_specs):
if len(matching_specs) <= 1: if len(matching_specs) <= 1:
return return
format_string = "{name}{@version}{%compiler.name}{@compiler.version}{arch=architecture}" format_string = "{name}{@version}{%compiler.name}{@compiler.version}{ arch=architecture}"
args = ["%s matches multiple packages." % spec, "Matching packages:"] args = ["%s matches multiple packages." % spec, "Matching packages:"]
args += [ args += [
colorize(" @K{%s} " % s.dag_hash(7)) + s.cformat(format_string) for s in matching_specs colorize(" @K{%s} " % s.dag_hash(7)) + s.cformat(format_string) for s in matching_specs

View File

@ -129,7 +129,7 @@
r"|" # or r"|" # or
# OPTION 2: an actual format string # OPTION 2: an actual format string
r"{" # non-escaped open brace { r"{" # non-escaped open brace {
r"([%@/]|arch=)?" # optional sigil (to print sigil in color) r"([%@/]|[\w ][\w -]*=)?" # optional sigil (or identifier or space) to print sigil in color
r"(?:\^([^}\.]+)\.)?" # optional ^depname. (to get attr from dependency) r"(?:\^([^}\.]+)\.)?" # optional ^depname. (to get attr from dependency)
# after the sigil or depname, we can have a hash expression or another attribute # after the sigil or depname, we can have a hash expression or another attribute
r"(?:" # one of r"(?:" # one of
@ -163,14 +163,14 @@
DEFAULT_FORMAT = ( DEFAULT_FORMAT = (
"{name}{@versions}" "{name}{@versions}"
"{%compiler.name}{@compiler.versions}{compiler_flags}" "{%compiler.name}{@compiler.versions}{compiler_flags}"
"{variants}{arch=architecture}{/abstract_hash}" "{variants}{ arch=architecture}{/abstract_hash}"
) )
#: Display format, which eliminates extra `@=` in the output, for readability. #: Display format, which eliminates extra `@=` in the output, for readability.
DISPLAY_FORMAT = ( DISPLAY_FORMAT = (
"{name}{@version}" "{name}{@version}"
"{%compiler.name}{@compiler.version}{compiler_flags}" "{%compiler.name}{@compiler.version}{compiler_flags}"
"{variants}{arch=architecture}{/abstract_hash}" "{variants}{ arch=architecture}{/abstract_hash}"
) )
#: Regular expression to pull spec contents out of clearsigned signature #: Regular expression to pull spec contents out of clearsigned signature
@ -1894,14 +1894,14 @@ def short_spec(self):
"""Returns a version of the spec with the dependencies hashed """Returns a version of the spec with the dependencies hashed
instead of completely enumerated.""" instead of completely enumerated."""
spec_format = "{name}{@version}{%compiler.name}{@compiler.version}" spec_format = "{name}{@version}{%compiler.name}{@compiler.version}"
spec_format += "{variants}{arch=architecture}{/hash:7}" spec_format += "{variants}{ arch=architecture}{/hash:7}"
return self.format(spec_format) return self.format(spec_format)
@property @property
def cshort_spec(self): def cshort_spec(self):
"""Returns an auto-colorized version of ``self.short_spec``.""" """Returns an auto-colorized version of ``self.short_spec``."""
spec_format = "{name}{@version}{%compiler.name}{@compiler.version}" spec_format = "{name}{@version}{%compiler.name}{@compiler.version}"
spec_format += "{variants}{arch=architecture}{/hash:7}" spec_format += "{variants}{ arch=architecture}{/hash:7}"
return self.cformat(spec_format) return self.cformat(spec_format)
@property @property
@ -4387,13 +4387,14 @@ def deps():
yield deps yield deps
def format(self, format_string: str = DEFAULT_FORMAT, color: Optional[bool] = False) -> str: def format(self, format_string: str = DEFAULT_FORMAT, color: Optional[bool] = False) -> str:
r"""Prints out particular pieces of a spec, depending on what is r"""Prints out attributes of a spec according to a format string.
in the format string.
Using the ``{attribute}`` syntax, any field of the spec can be Using an ``{attribute}`` format specifier, any field of the spec can be
selected. Those attributes can be recursive. For example, selected. Those attributes can be recursive. For example,
``s.format({compiler.version})`` will print the version of the ``s.format({compiler.version})`` will print the version of the compiler.
compiler.
If the attribute in a format specifier evaluates to ``None``, then the format
specifier will evaluate to the empty string, ``""``.
Commonly used attributes of the Spec for format strings include:: Commonly used attributes of the Spec for format strings include::
@ -4409,6 +4410,7 @@ def format(self, format_string: str = DEFAULT_FORMAT, color: Optional[bool] = Fa
architecture.os architecture.os
architecture.target architecture.target
prefix prefix
namespace
Some additional special-case properties can be added:: Some additional special-case properties can be added::
@ -4417,40 +4419,51 @@ def format(self, format_string: str = DEFAULT_FORMAT, color: Optional[bool] = Fa
spack_install The spack install directory spack_install The spack install directory
The ``^`` sigil can be used to access dependencies by name. The ``^`` sigil can be used to access dependencies by name.
``s.format({^mpi.name})`` will print the name of the MPI ``s.format({^mpi.name})`` will print the name of the MPI implementation in the
implementation in the spec. spec.
The ``@``, ``%``, ``arch=``, and ``/`` sigils The ``@``, ``%``, and ``/`` sigils can be used to include the sigil with the
can be used to include the sigil with the printed printed string. These sigils may only be used with the appropriate attributes,
string. These sigils may only be used with the appropriate listed below::
attributes, listed below::
@ ``{@version}``, ``{@compiler.version}`` @ ``{@version}``, ``{@compiler.version}``
% ``{%compiler}``, ``{%compiler.name}`` % ``{%compiler}``, ``{%compiler.name}``
arch= ``{arch=architecture}``
/ ``{/hash}``, ``{/hash:7}``, etc / ``{/hash}``, ``{/hash:7}``, etc
The ``@`` sigil may also be used for any other property named The ``@`` sigil may also be used for any other property named ``version``.
``version``. Sigils printed with the attribute string are only Sigils printed with the attribute string are only printed if the attribute
printed if the attribute string is non-empty, and are colored string is non-empty, and are colored according to the color of the attribute.
according to the color of the attribute.
Sigils are not used for printing variants. Variants listed by Variants listed by name naturally print with their sigil. For example,
name naturally print with their sigil. For example, ``spec.format('{variants.debug}')`` prints either ``+debug`` or ``~debug``
``spec.format('{variants.debug}')`` would print either depending on the name of the variant. Non-boolean variants print as
``+debug`` or ``~debug`` depending on the name of the ``name=value``. To print variant names or values independently, use
variant. Non-boolean variants print as ``name=value``. To
print variant names or values independently, use
``spec.format('{variants.<name>.name}')`` or ``spec.format('{variants.<name>.name}')`` or
``spec.format('{variants.<name>.value}')``. ``spec.format('{variants.<name>.value}')``.
Spec format strings use ``\`` as the escape character. Use There are a few attributes on specs that can be specified as key-value pairs
``\{`` and ``\}`` for literal braces, and ``\\`` for the that are *not* variants, e.g.: ``os``, ``arch``, ``architecture``, ``target``,
literal ``\`` character. ``namespace``, etc. You can format these with an optional ``key=`` prefix, e.g.
``{namespace=namespace}`` or ``{arch=architecture}``, etc. The ``key=`` prefix
will be colorized along with the value.
When formatting specs, key-value pairs are separated from preceding parts of the
spec by whitespace. To avoid printing extra whitespace when the formatted
attribute is not set, you can add whitespace to the key *inside* the braces of
the format string, e.g.:
{ namespace=namespace}
This evaluates to `` namespace=builtin`` if ``namespace`` is set to ``builtin``,
and to ``""`` if ``namespace`` is ``None``.
Spec format strings use ``\`` as the escape character. Use ``\{`` and ``\}`` for
literal braces, and ``\\`` for the literal ``\`` character.
Args: Args:
format_string: string containing the format to be expanded format_string: string containing the format to be expanded
color: True for colorized result; False for no color; None for auto color. color: True for colorized result; False for no color; None for auto color.
""" """
ensure_modern_format_string(format_string) ensure_modern_format_string(format_string)
@ -4504,10 +4517,6 @@ def format_attribute(match_object: Match) -> str:
raise SpecFormatSigilError(sig, "compilers", attribute) raise SpecFormatSigilError(sig, "compilers", attribute)
elif sig == "/" and attribute != "abstract_hash": elif sig == "/" and attribute != "abstract_hash":
raise SpecFormatSigilError(sig, "DAG hashes", attribute) raise SpecFormatSigilError(sig, "DAG hashes", attribute)
elif sig == "arch=":
if attribute not in ("architecture", "arch"):
raise SpecFormatSigilError(sig, "the architecture", attribute)
sig = " arch=" # include space as separator
# Iterate over components using getattr to get next element # Iterate over components using getattr to get next element
for idx, part in enumerate(parts): for idx, part in enumerate(parts):
@ -4552,15 +4561,19 @@ def format_attribute(match_object: Match) -> str:
# Set color codes for various attributes # Set color codes for various attributes
color = None color = None
if "variants" in parts: if "architecture" in parts:
color = VARIANT_COLOR
elif "architecture" in parts:
color = ARCHITECTURE_COLOR color = ARCHITECTURE_COLOR
elif "variants" in parts or sig.endswith("="):
color = VARIANT_COLOR
elif "compiler" in parts or "compiler_flags" in parts: elif "compiler" in parts or "compiler_flags" in parts:
color = COMPILER_COLOR color = COMPILER_COLOR
elif "version" in parts or "versions" in parts: elif "version" in parts or "versions" in parts:
color = VERSION_COLOR color = VERSION_COLOR
# return empty string if the value of the attribute is None.
if current is None:
return ""
# return colored output # return colored output
return safe_color(sig, str(current), color) return safe_color(sig, str(current), color)
@ -5523,7 +5536,7 @@ def __init__(self, spec):
class AmbiguousHashError(spack.error.SpecError): class AmbiguousHashError(spack.error.SpecError):
def __init__(self, msg, *specs): def __init__(self, msg, *specs):
spec_fmt = "{namespace}.{name}{@version}{%compiler}{compiler_flags}" spec_fmt = "{namespace}.{name}{@version}{%compiler}{compiler_flags}"
spec_fmt += "{variants}{arch=architecture}{/hash:7}" spec_fmt += "{variants}{ arch=architecture}{/hash:7}"
specs_str = "\n " + "\n ".join(spec.format(spec_fmt) for spec in specs) specs_str = "\n " + "\n ".join(spec.format(spec_fmt) for spec in specs)
super().__init__(msg + specs_str) super().__init__(msg + specs_str)

View File

@ -656,6 +656,7 @@ def test_spec_formatting(self, default_mock_concretization):
("{@VERSIONS}", "@", "versions", lambda spec: spec), ("{@VERSIONS}", "@", "versions", lambda spec: spec),
("{%compiler}", "%", "compiler", lambda spec: spec), ("{%compiler}", "%", "compiler", lambda spec: spec),
("{arch=architecture}", "arch=", "architecture", lambda spec: spec), ("{arch=architecture}", "arch=", "architecture", lambda spec: spec),
("{namespace=namespace}", "namespace=", "namespace", lambda spec: spec),
("{compiler.name}", "", "name", lambda spec: spec.compiler), ("{compiler.name}", "", "name", lambda spec: spec.compiler),
("{compiler.version}", "", "version", lambda spec: spec.compiler), ("{compiler.version}", "", "version", lambda spec: spec.compiler),
("{%compiler.name}", "%", "name", lambda spec: spec.compiler), ("{%compiler.name}", "%", "name", lambda spec: spec.compiler),
@ -706,13 +707,40 @@ def check_prop(check_spec, fmt_str, prop, getter):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fmt_str", "fmt_str",
[ [
"{@name}", "{name}",
"{@version.concrete}", "{version}",
"{%compiler.version}", "{@version}",
"{/hashd}", "{%compiler}",
"{arch=architecture.os}", "{namespace}",
"{ namespace=namespace}",
"{ namespace =namespace}",
"{ name space =namespace}",
"{arch}",
"{architecture}",
"{arch=architecture}",
"{ arch=architecture}",
"{ arch =architecture}",
], ],
) )
def test_spec_format_null_attributes(self, fmt_str):
"""Ensure that attributes format to empty strings when their values are null."""
spec = spack.spec.Spec()
assert spec.format(fmt_str) == ""
def test_spec_formatting_spaces_in_key(self, default_mock_concretization):
spec = default_mock_concretization("multivalue-variant cflags=-O2")
# test that spaces are preserved, if they come after some other text, otherwise
# they are trimmed.
# TODO: should we be trimming whitespace from formats? Probably not.
assert spec.format("x{ arch=architecture}") == f"x arch={spec.architecture}"
assert spec.format("x{ namespace=namespace}") == f"x namespace={spec.namespace}"
assert spec.format("x{ name space =namespace}") == f"x name space ={spec.namespace}"
assert spec.format("x{ os =os}") == f"x os ={spec.os}"
@pytest.mark.parametrize(
"fmt_str", ["{@name}", "{@version.concrete}", "{%compiler.version}", "{/hashd}"]
)
def test_spec_formatting_sigil_mismatches(self, default_mock_concretization, fmt_str): def test_spec_formatting_sigil_mismatches(self, default_mock_concretization, fmt_str):
spec = default_mock_concretization("multivalue-variant cflags=-O2") spec = default_mock_concretization("multivalue-variant cflags=-O2")