Refactor to improve spec format speed (#43712)
				
					
				
			When looking at where we spend our time in solver setup, I noticed a fair bit of time is spent
in `Spec.format()`, and `Spec.format()` is a pretty old, slow, convoluted method.
This PR does a number of things:
- [x] Consolidate most of what was being done manually with a character loop and several
      regexes into a single regex.
- [x] Precompile regexes where we keep them 
- [x] Remove the `transform=` argument to `Spec.format()` which was only used in one 
      place in the code (modules) to uppercase env var names, but added a lot of complexity
- [x] Avoid escaping and colorizing specs unless necessary
- [x] Refactor a lot of the colorization logic to avoid unnecessary object construction
- [x] Add type hints and remove some spots in the code where we were using nonexistent
      arguments to `format()`.
- [x] Add trivial cases to `__str__` in `VariantMap` and `VersionList` to avoid sorting
- [x] Avoid calling `isinstance()` in the main loop of `Spec.format()`
- [x] Don't bother constructing a `string` representation for the result of `_prev_version`
      as it is only used for comparisons.
In my timings (on all the specs formatted in a solve of `hdf5`), this is over 2.67x faster than the 
original `format()`, and it seems to reduce setup time by around a second (for `hdf5`).
			
			
This commit is contained in:
		@@ -12,7 +12,7 @@
 | 
			
		||||
import traceback
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from sys import platform as _platform
 | 
			
		||||
from typing import NoReturn
 | 
			
		||||
from typing import Any, NoReturn
 | 
			
		||||
 | 
			
		||||
if _platform != "win32":
 | 
			
		||||
    import fcntl
 | 
			
		||||
@@ -158,21 +158,22 @@ def get_timestamp(force=False):
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def msg(message, *args, **kwargs):
 | 
			
		||||
def msg(message: Any, *args: Any, newline: bool = True) -> None:
 | 
			
		||||
    if not msg_enabled():
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if isinstance(message, Exception):
 | 
			
		||||
        message = "%s: %s" % (message.__class__.__name__, str(message))
 | 
			
		||||
        message = f"{message.__class__.__name__}: {message}"
 | 
			
		||||
    else:
 | 
			
		||||
        message = str(message)
 | 
			
		||||
 | 
			
		||||
    newline = kwargs.get("newline", True)
 | 
			
		||||
    st_text = ""
 | 
			
		||||
    if _stacktrace:
 | 
			
		||||
        st_text = process_stacktrace(2)
 | 
			
		||||
    if newline:
 | 
			
		||||
        cprint("@*b{%s==>} %s%s" % (st_text, get_timestamp(), cescape(_output_filter(message))))
 | 
			
		||||
    else:
 | 
			
		||||
        cwrite("@*b{%s==>} %s%s" % (st_text, get_timestamp(), cescape(_output_filter(message))))
 | 
			
		||||
 | 
			
		||||
    nl = "\n" if newline else ""
 | 
			
		||||
    cwrite(f"@*b{{{st_text}==>}} {get_timestamp()}{cescape(_output_filter(message))}{nl}")
 | 
			
		||||
 | 
			
		||||
    for arg in args:
 | 
			
		||||
        print(indent + _output_filter(str(arg)))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
from contextlib import contextmanager
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ColorParseError(Exception):
 | 
			
		||||
@@ -95,7 +96,7 @@ def __init__(self, message):
 | 
			
		||||
}  # white
 | 
			
		||||
 | 
			
		||||
# Regex to be used for color formatting
 | 
			
		||||
color_re = r"@(?:@|\.|([*_])?([a-zA-Z])?(?:{((?:[^}]|}})*)})?)"
 | 
			
		||||
COLOR_RE = re.compile(r"@(?:(@)|(\.)|([*_])?([a-zA-Z])?(?:{((?:[^}]|}})*)})?)")
 | 
			
		||||
 | 
			
		||||
# Mapping from color arguments to values for tty.set_color
 | 
			
		||||
color_when_values = {"always": True, "auto": None, "never": False}
 | 
			
		||||
@@ -203,77 +204,64 @@ def color_when(value):
 | 
			
		||||
    set_color_when(old_value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class match_to_ansi:
 | 
			
		||||
    def __init__(self, color=True, enclose=False, zsh=False):
 | 
			
		||||
        self.color = _color_when_value(color)
 | 
			
		||||
        self.enclose = enclose
 | 
			
		||||
        self.zsh = zsh
 | 
			
		||||
 | 
			
		||||
    def escape(self, s):
 | 
			
		||||
        """Returns a TTY escape sequence for a color"""
 | 
			
		||||
        if self.color:
 | 
			
		||||
            if self.zsh:
 | 
			
		||||
                result = rf"\e[0;{s}m"
 | 
			
		||||
            else:
 | 
			
		||||
                result = f"\033[{s}m"
 | 
			
		||||
 | 
			
		||||
            if self.enclose:
 | 
			
		||||
                result = rf"\[{result}\]"
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
def _escape(s: str, color: bool, enclose: bool, zsh: bool) -> str:
 | 
			
		||||
    """Returns a TTY escape sequence for a color"""
 | 
			
		||||
    if color:
 | 
			
		||||
        if zsh:
 | 
			
		||||
            result = rf"\e[0;{s}m"
 | 
			
		||||
        else:
 | 
			
		||||
            return ""
 | 
			
		||||
            result = f"\033[{s}m"
 | 
			
		||||
 | 
			
		||||
    def __call__(self, match):
 | 
			
		||||
        """Convert a match object generated by ``color_re`` into an ansi
 | 
			
		||||
        color code. This can be used as a handler in ``re.sub``.
 | 
			
		||||
        """
 | 
			
		||||
        style, color, text = match.groups()
 | 
			
		||||
        m = match.group(0)
 | 
			
		||||
        if enclose:
 | 
			
		||||
            result = rf"\[{result}\]"
 | 
			
		||||
 | 
			
		||||
        if m == "@@":
 | 
			
		||||
            return "@"
 | 
			
		||||
        elif m == "@.":
 | 
			
		||||
            return self.escape(0)
 | 
			
		||||
        elif m == "@":
 | 
			
		||||
            raise ColorParseError("Incomplete color format: '%s' in %s" % (m, match.string))
 | 
			
		||||
 | 
			
		||||
        string = styles[style]
 | 
			
		||||
        if color:
 | 
			
		||||
            if color not in colors:
 | 
			
		||||
                raise ColorParseError(
 | 
			
		||||
                    "Invalid color specifier: '%s' in '%s'" % (color, match.string)
 | 
			
		||||
                )
 | 
			
		||||
            string += ";" + str(colors[color])
 | 
			
		||||
 | 
			
		||||
        colored_text = ""
 | 
			
		||||
        if text:
 | 
			
		||||
            colored_text = text + self.escape(0)
 | 
			
		||||
 | 
			
		||||
        return self.escape(string) + colored_text
 | 
			
		||||
        return result
 | 
			
		||||
    else:
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def colorize(string, **kwargs):
 | 
			
		||||
def colorize(
 | 
			
		||||
    string: str, color: Optional[bool] = None, enclose: bool = False, zsh: bool = False
 | 
			
		||||
) -> str:
 | 
			
		||||
    """Replace all color expressions in a string with ANSI control codes.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        string (str): The string to replace
 | 
			
		||||
        string: The string to replace
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        str: The filtered string
 | 
			
		||||
        The filtered string
 | 
			
		||||
 | 
			
		||||
    Keyword Arguments:
 | 
			
		||||
        color (bool): If False, output will be plain text without control
 | 
			
		||||
            codes, for output to non-console devices.
 | 
			
		||||
        enclose (bool): If True, enclose ansi color sequences with
 | 
			
		||||
        color: If False, output will be plain text without control codes, for output to
 | 
			
		||||
            non-console devices (default: automatically choose color or not)
 | 
			
		||||
        enclose: If True, enclose ansi color sequences with
 | 
			
		||||
            square brackets to prevent misestimation of terminal width.
 | 
			
		||||
        zsh (bool): If True, use zsh ansi codes instead of bash ones (for variables like PS1)
 | 
			
		||||
        zsh: If True, use zsh ansi codes instead of bash ones (for variables like PS1)
 | 
			
		||||
    """
 | 
			
		||||
    color = _color_when_value(kwargs.get("color", get_color_when()))
 | 
			
		||||
    zsh = kwargs.get("zsh", False)
 | 
			
		||||
    string = re.sub(color_re, match_to_ansi(color, kwargs.get("enclose")), string, zsh)
 | 
			
		||||
    string = string.replace("}}", "}")
 | 
			
		||||
    return string
 | 
			
		||||
    color = color if color is not None else get_color_when()
 | 
			
		||||
 | 
			
		||||
    def match_to_ansi(match):
 | 
			
		||||
        """Convert a match object generated by ``COLOR_RE`` into an ansi
 | 
			
		||||
        color code. This can be used as a handler in ``re.sub``.
 | 
			
		||||
        """
 | 
			
		||||
        escaped_at, dot, style, color_code, text = match.groups()
 | 
			
		||||
 | 
			
		||||
        if escaped_at:
 | 
			
		||||
            return "@"
 | 
			
		||||
        elif dot:
 | 
			
		||||
            return _escape(0, color, enclose, zsh)
 | 
			
		||||
        elif not (style or color_code):
 | 
			
		||||
            raise ColorParseError(
 | 
			
		||||
                f"Incomplete color format: '{match.group(0)}' in '{match.string}'"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        ansi_code = _escape(f"{styles[style]};{colors.get(color_code, '')}", color, enclose, zsh)
 | 
			
		||||
        if text:
 | 
			
		||||
            return f"{ansi_code}{text}{_escape(0, color, enclose, zsh)}"
 | 
			
		||||
        else:
 | 
			
		||||
            return ansi_code
 | 
			
		||||
 | 
			
		||||
    return COLOR_RE.sub(match_to_ansi, string).replace("}}", "}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clen(string):
 | 
			
		||||
@@ -305,7 +293,7 @@ def cprint(string, stream=None, color=None):
 | 
			
		||||
    cwrite(string + "\n", stream, color)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cescape(string):
 | 
			
		||||
def cescape(string: str) -> str:
 | 
			
		||||
    """Escapes special characters needed for color codes.
 | 
			
		||||
 | 
			
		||||
    Replaces the following symbols with their equivalent literal forms:
 | 
			
		||||
@@ -321,10 +309,7 @@ def cescape(string):
 | 
			
		||||
    Returns:
 | 
			
		||||
        (str): the string with color codes escaped
 | 
			
		||||
    """
 | 
			
		||||
    string = str(string)
 | 
			
		||||
    string = string.replace("@", "@@")
 | 
			
		||||
    string = string.replace("}", "}}")
 | 
			
		||||
    return string
 | 
			
		||||
    return string.replace("@", "@@").replace("}", "}}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ColorStream:
 | 
			
		||||
 
 | 
			
		||||
@@ -263,8 +263,8 @@ def _fmt_name_and_default(variant):
 | 
			
		||||
    return color.colorize(f"@c{{{variant.name}}} @C{{[{_fmt_value(variant.default)}]}}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _fmt_when(when, indent):
 | 
			
		||||
    return color.colorize(f"{indent * ' '}@B{{when}} {color.cescape(when)}")
 | 
			
		||||
def _fmt_when(when: "spack.spec.Spec", indent: int):
 | 
			
		||||
    return color.colorize(f"{indent * ' '}@B{{when}} {color.cescape(str(when))}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _fmt_variant_description(variant, width, indent):
 | 
			
		||||
@@ -441,7 +441,7 @@ def get_url(version):
 | 
			
		||||
                return "No URL"
 | 
			
		||||
 | 
			
		||||
        url = get_url(preferred) if pkg.has_code else ""
 | 
			
		||||
        line = version("    {0}".format(pad(preferred))) + color.cescape(url)
 | 
			
		||||
        line = version("    {0}".format(pad(preferred))) + color.cescape(str(url))
 | 
			
		||||
        color.cwrite(line)
 | 
			
		||||
 | 
			
		||||
        print()
 | 
			
		||||
@@ -464,7 +464,7 @@ def get_url(version):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            for v, url in vers:
 | 
			
		||||
                line = version("    {0}".format(pad(v))) + color.cescape(url)
 | 
			
		||||
                line = version("    {0}".format(pad(v))) + color.cescape(str(url))
 | 
			
		||||
                color.cprint(line)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -475,10 +475,7 @@ def print_virtuals(pkg, args):
 | 
			
		||||
    color.cprint(section_title("Virtual Packages: "))
 | 
			
		||||
    if pkg.provided:
 | 
			
		||||
        for when, specs in reversed(sorted(pkg.provided.items())):
 | 
			
		||||
            line = "    %s provides %s" % (
 | 
			
		||||
                when.colorized(),
 | 
			
		||||
                ", ".join(s.colorized() for s in specs),
 | 
			
		||||
            )
 | 
			
		||||
            line = "    %s provides %s" % (when.cformat(), ", ".join(s.cformat() for s in specs))
 | 
			
		||||
            print(line)
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
@@ -497,7 +494,9 @@ def print_licenses(pkg, args):
 | 
			
		||||
        pad = padder(pkg.licenses, 4)
 | 
			
		||||
        for when_spec in pkg.licenses:
 | 
			
		||||
            license_identifier = pkg.licenses[when_spec]
 | 
			
		||||
            line = license("    {0}".format(pad(license_identifier))) + color.cescape(when_spec)
 | 
			
		||||
            line = license("    {0}".format(pad(license_identifier))) + color.cescape(
 | 
			
		||||
                str(when_spec)
 | 
			
		||||
            )
 | 
			
		||||
            color.cprint(line)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,17 @@ def configuration(module_set_name):
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_FORMAT_STRING_RE = re.compile(r"({[^}]*})")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _format_env_var_name(spec, var_name_fmt):
 | 
			
		||||
    """Format the variable name, but uppercase any formatted fields."""
 | 
			
		||||
    fmt_parts = _FORMAT_STRING_RE.split(var_name_fmt)
 | 
			
		||||
    return "".join(
 | 
			
		||||
        spec.format(part).upper() if _FORMAT_STRING_RE.match(part) else part for part in fmt_parts
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_tokens_are_valid(format_string, message):
 | 
			
		||||
    """Checks that the tokens used in the format string are valid in
 | 
			
		||||
    the context of module file and environment variable naming.
 | 
			
		||||
@@ -737,20 +748,12 @@ def environment_modifications(self):
 | 
			
		||||
        exclude = self.conf.exclude_env_vars
 | 
			
		||||
 | 
			
		||||
        # We may have tokens to substitute in environment commands
 | 
			
		||||
 | 
			
		||||
        # Prepare a suitable transformation dictionary for the names
 | 
			
		||||
        # of the environment variables. This means turn the valid
 | 
			
		||||
        # tokens uppercase.
 | 
			
		||||
        transform = {}
 | 
			
		||||
        for token in _valid_tokens:
 | 
			
		||||
            transform[token] = lambda s, string: str.upper(string)
 | 
			
		||||
 | 
			
		||||
        for x in env:
 | 
			
		||||
            # Ensure all the tokens are valid in this context
 | 
			
		||||
            msg = "some tokens cannot be expanded in an environment variable name"
 | 
			
		||||
 | 
			
		||||
            _check_tokens_are_valid(x.name, message=msg)
 | 
			
		||||
            # Transform them
 | 
			
		||||
            x.name = self.spec.format(x.name, transform=transform)
 | 
			
		||||
            x.name = _format_env_var_name(self.spec, x.name)
 | 
			
		||||
            if self.modification_needs_formatting(x):
 | 
			
		||||
                try:
 | 
			
		||||
                    # Not every command has a value
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,6 @@
 | 
			
		||||
import collections
 | 
			
		||||
import collections.abc
 | 
			
		||||
import enum
 | 
			
		||||
import io
 | 
			
		||||
import itertools
 | 
			
		||||
import os
 | 
			
		||||
import pathlib
 | 
			
		||||
@@ -59,7 +58,7 @@
 | 
			
		||||
import re
 | 
			
		||||
import socket
 | 
			
		||||
import warnings
 | 
			
		||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
 | 
			
		||||
from typing import Any, Callable, Dict, List, Match, Optional, Set, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import llnl.path
 | 
			
		||||
import llnl.string
 | 
			
		||||
@@ -121,36 +120,44 @@
 | 
			
		||||
    "SpecDeprecatedError",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SPEC_FORMAT_RE = re.compile(
 | 
			
		||||
    r"(?:"  # this is one big or, with matches ordered by priority
 | 
			
		||||
    # OPTION 1: escaped character (needs to be first to catch opening \{)
 | 
			
		||||
    # Note that an unterminated \ at the end of a string is left untouched
 | 
			
		||||
    r"(?:\\(.))"
 | 
			
		||||
    r"|"  # or
 | 
			
		||||
    # OPTION 2: an actual format string
 | 
			
		||||
    r"{"  # non-escaped open brace {
 | 
			
		||||
    r"([%@/]|arch=)?"  # optional sigil (to print sigil in color)
 | 
			
		||||
    r"(?:\^([^}\.]+)\.)?"  # optional ^depname. (to get attr from dependency)
 | 
			
		||||
    # after the sigil or depname, we can have a hash expression or another attribute
 | 
			
		||||
    r"(?:"  # one of
 | 
			
		||||
    r"(hash\b)(?:\:(\d+))?"  # hash followed by :<optional length>
 | 
			
		||||
    r"|"  # or
 | 
			
		||||
    r"([^}]*)"  # another attribute to format
 | 
			
		||||
    r")"  # end one of
 | 
			
		||||
    r"(})?"  # finish format string with non-escaped close brace }, or missing if not present
 | 
			
		||||
    r"|"
 | 
			
		||||
    # OPTION 3: mismatched close brace (option 2 would consume a matched open brace)
 | 
			
		||||
    r"(})"  # brace
 | 
			
		||||
    r")",
 | 
			
		||||
    re.IGNORECASE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
#: Valid pattern for an identifier in Spack
 | 
			
		||||
 | 
			
		||||
IDENTIFIER_RE = r"\w[\w-]*"
 | 
			
		||||
 | 
			
		||||
# Coloring of specs when using color output. Fields are printed with
 | 
			
		||||
# different colors to enhance readability.
 | 
			
		||||
# See llnl.util.tty.color for descriptions of the color codes.
 | 
			
		||||
COMPILER_COLOR = "@g"  #: color for highlighting compilers
 | 
			
		||||
VERSION_COLOR = "@c"  #: color for highlighting versions
 | 
			
		||||
ARCHITECTURE_COLOR = "@m"  #: color for highlighting architectures
 | 
			
		||||
ENABLED_VARIANT_COLOR = "@B"  #: color for highlighting enabled variants
 | 
			
		||||
DISABLED_VARIANT_COLOR = "r"  #: color for highlighting disabled varaints
 | 
			
		||||
DEPENDENCY_COLOR = "@."  #: color for highlighting dependencies
 | 
			
		||||
VARIANT_COLOR = "@B"  #: color for highlighting variants
 | 
			
		||||
HASH_COLOR = "@K"  #: color for highlighting package hashes
 | 
			
		||||
 | 
			
		||||
#: This map determines the coloring of specs when using color output.
 | 
			
		||||
#: We make the fields different colors to enhance readability.
 | 
			
		||||
#: See llnl.util.tty.color for descriptions of the color codes.
 | 
			
		||||
COLOR_FORMATS = {
 | 
			
		||||
    "%": COMPILER_COLOR,
 | 
			
		||||
    "@": VERSION_COLOR,
 | 
			
		||||
    "=": ARCHITECTURE_COLOR,
 | 
			
		||||
    "+": ENABLED_VARIANT_COLOR,
 | 
			
		||||
    "~": DISABLED_VARIANT_COLOR,
 | 
			
		||||
    "^": DEPENDENCY_COLOR,
 | 
			
		||||
    "#": HASH_COLOR,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#: Regex used for splitting by spec field separators.
 | 
			
		||||
#: These need to be escaped to avoid metacharacters in
 | 
			
		||||
#: ``COLOR_FORMATS.keys()``.
 | 
			
		||||
_SEPARATORS = "[\\%s]" % "\\".join(COLOR_FORMATS.keys())
 | 
			
		||||
 | 
			
		||||
#: Default format for Spec.format(). This format can be round-tripped, so that:
 | 
			
		||||
#:     Spec(Spec("string").format()) == Spec("string)"
 | 
			
		||||
DEFAULT_FORMAT = (
 | 
			
		||||
@@ -193,26 +200,7 @@ class InstallStatus(enum.Enum):
 | 
			
		||||
    missing = "@r{[-]}  "
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def colorize_spec(spec):
 | 
			
		||||
    """Returns a spec colorized according to the colors specified in
 | 
			
		||||
    COLOR_FORMATS."""
 | 
			
		||||
 | 
			
		||||
    class insert_color:
 | 
			
		||||
        def __init__(self):
 | 
			
		||||
            self.last = None
 | 
			
		||||
 | 
			
		||||
        def __call__(self, match):
 | 
			
		||||
            # ignore compiler versions (color same as compiler)
 | 
			
		||||
            sep = match.group(0)
 | 
			
		||||
            if self.last == "%" and sep == "@":
 | 
			
		||||
                return clr.cescape(sep)
 | 
			
		||||
            self.last = sep
 | 
			
		||||
 | 
			
		||||
            return "%s%s" % (COLOR_FORMATS[sep], clr.cescape(sep))
 | 
			
		||||
 | 
			
		||||
    return clr.colorize(re.sub(_SEPARATORS, insert_color(), str(spec)) + "@.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# regexes used in spec formatting
 | 
			
		||||
OLD_STYLE_FMT_RE = re.compile(r"\${[A-Z]+}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -4295,10 +4283,7 @@ def deps():
 | 
			
		||||
 | 
			
		||||
        yield deps
 | 
			
		||||
 | 
			
		||||
    def colorized(self):
 | 
			
		||||
        return colorize_spec(self)
 | 
			
		||||
 | 
			
		||||
    def format(self, format_string=DEFAULT_FORMAT, **kwargs):
 | 
			
		||||
    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
 | 
			
		||||
        in the format string.
 | 
			
		||||
 | 
			
		||||
@@ -4361,79 +4346,65 @@ def format(self, format_string=DEFAULT_FORMAT, **kwargs):
 | 
			
		||||
        literal ``\`` character.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            format_string (str): string containing the format to be expanded
 | 
			
		||||
 | 
			
		||||
        Keyword Args:
 | 
			
		||||
            color (bool): True if returned string is colored
 | 
			
		||||
            transform (dict): maps full-string formats to a callable \
 | 
			
		||||
                that accepts a string and returns another one
 | 
			
		||||
 | 
			
		||||
            format_string: string containing the format to be expanded
 | 
			
		||||
            color: True for colorized result; False for no color; None for auto color.
 | 
			
		||||
        """
 | 
			
		||||
        ensure_modern_format_string(format_string)
 | 
			
		||||
        color = kwargs.get("color", False)
 | 
			
		||||
        transform = kwargs.get("transform", {})
 | 
			
		||||
 | 
			
		||||
        out = io.StringIO()
 | 
			
		||||
        def safe_color(sigil: str, string: str, color_fmt: Optional[str]) -> str:
 | 
			
		||||
            # avoid colorizing if there is no color or the string is empty
 | 
			
		||||
            if (color is False) or not color_fmt or not string:
 | 
			
		||||
                return sigil + string
 | 
			
		||||
            # escape and add the sigil here to avoid multiple concatenations
 | 
			
		||||
            if sigil == "@":
 | 
			
		||||
                sigil = "@@"
 | 
			
		||||
            return clr.colorize(f"{color_fmt}{sigil}{clr.cescape(string)}@.", color=color)
 | 
			
		||||
 | 
			
		||||
        def write(s, c=None):
 | 
			
		||||
            f = clr.cescape(s)
 | 
			
		||||
            if c is not None:
 | 
			
		||||
                f = COLOR_FORMATS[c] + f + "@."
 | 
			
		||||
            clr.cwrite(f, stream=out, color=color)
 | 
			
		||||
        def format_attribute(match_object: Match) -> str:
 | 
			
		||||
            (esc, sig, dep, hash, hash_len, attribute, close_brace, unmatched_close_brace) = (
 | 
			
		||||
                match_object.groups()
 | 
			
		||||
            )
 | 
			
		||||
            if esc:
 | 
			
		||||
                return esc
 | 
			
		||||
            elif unmatched_close_brace:
 | 
			
		||||
                raise SpecFormatStringError(f"Unmatched close brace: '{format_string}'")
 | 
			
		||||
            elif not close_brace:
 | 
			
		||||
                raise SpecFormatStringError(f"Missing close brace: '{format_string}'")
 | 
			
		||||
 | 
			
		||||
        def write_attribute(spec, attribute, color):
 | 
			
		||||
            attribute = attribute.lower()
 | 
			
		||||
            current = self if dep is None else self[dep]
 | 
			
		||||
 | 
			
		||||
            sig = ""
 | 
			
		||||
            if attribute.startswith(("@", "%", "/")):
 | 
			
		||||
                # color sigils that are inside braces
 | 
			
		||||
                sig = attribute[0]
 | 
			
		||||
                attribute = attribute[1:]
 | 
			
		||||
            elif attribute.startswith("arch="):
 | 
			
		||||
                sig = " arch="  # include space as separator
 | 
			
		||||
                attribute = attribute[5:]
 | 
			
		||||
 | 
			
		||||
            current = spec
 | 
			
		||||
            if attribute.startswith("^"):
 | 
			
		||||
                attribute = attribute[1:]
 | 
			
		||||
                dep, attribute = attribute.split(".", 1)
 | 
			
		||||
                current = self[dep]
 | 
			
		||||
            # Hash attributes can return early.
 | 
			
		||||
            # NOTE: we currently treat abstract_hash like an attribute and ignore
 | 
			
		||||
            # any length associated with it. We may want to change that.
 | 
			
		||||
            if hash:
 | 
			
		||||
                if sig and sig != "/":
 | 
			
		||||
                    raise SpecFormatSigilError(sig, "DAG hashes", hash)
 | 
			
		||||
                try:
 | 
			
		||||
                    length = int(hash_len) if hash_len else None
 | 
			
		||||
                except ValueError:
 | 
			
		||||
                    raise SpecFormatStringError(f"Invalid hash length: '{hash_len}'")
 | 
			
		||||
                return safe_color(sig or "", current.dag_hash(length), HASH_COLOR)
 | 
			
		||||
 | 
			
		||||
            if attribute == "":
 | 
			
		||||
                raise SpecFormatStringError("Format string attributes must be non-empty")
 | 
			
		||||
 | 
			
		||||
            attribute = attribute.lower()
 | 
			
		||||
            parts = attribute.split(".")
 | 
			
		||||
            assert parts
 | 
			
		||||
 | 
			
		||||
            # check that the sigil is valid for the attribute.
 | 
			
		||||
            if sig == "@" and parts[-1] not in ("versions", "version"):
 | 
			
		||||
            if not sig:
 | 
			
		||||
                sig = ""
 | 
			
		||||
            elif sig == "@" and parts[-1] not in ("versions", "version"):
 | 
			
		||||
                raise SpecFormatSigilError(sig, "versions", attribute)
 | 
			
		||||
            elif sig == "%" and attribute not in ("compiler", "compiler.name"):
 | 
			
		||||
                raise SpecFormatSigilError(sig, "compilers", attribute)
 | 
			
		||||
            elif sig == "/" and not re.match(r"(abstract_)?hash(:\d+)?$", attribute):
 | 
			
		||||
            elif sig == "/" and attribute != "abstract_hash":
 | 
			
		||||
                raise SpecFormatSigilError(sig, "DAG hashes", attribute)
 | 
			
		||||
            elif sig == " arch=" and attribute not in ("architecture", "arch"):
 | 
			
		||||
                raise SpecFormatSigilError(sig, "the architecture", attribute)
 | 
			
		||||
 | 
			
		||||
            # find the morph function for our attribute
 | 
			
		||||
            morph = transform.get(attribute, lambda s, x: x)
 | 
			
		||||
 | 
			
		||||
            # Special cases for non-spec attributes and hashes.
 | 
			
		||||
            # These must be the only non-dep component of the format attribute
 | 
			
		||||
            if attribute == "spack_root":
 | 
			
		||||
                write(morph(spec, spack.paths.spack_root))
 | 
			
		||||
                return
 | 
			
		||||
            elif attribute == "spack_install":
 | 
			
		||||
                write(morph(spec, spack.store.STORE.layout.root))
 | 
			
		||||
                return
 | 
			
		||||
            elif re.match(r"hash(:\d)?", attribute):
 | 
			
		||||
                col = "#"
 | 
			
		||||
                if ":" in attribute:
 | 
			
		||||
                    _, length = attribute.split(":")
 | 
			
		||||
                    write(sig + morph(spec, current.dag_hash(int(length))), col)
 | 
			
		||||
                else:
 | 
			
		||||
                    write(sig + morph(spec, current.dag_hash()), col)
 | 
			
		||||
                return
 | 
			
		||||
            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
 | 
			
		||||
            for idx, part in enumerate(parts):
 | 
			
		||||
@@ -4442,7 +4413,7 @@ def write_attribute(spec, attribute, color):
 | 
			
		||||
                if part.startswith("_"):
 | 
			
		||||
                    raise SpecFormatStringError("Attempted to format private attribute")
 | 
			
		||||
                else:
 | 
			
		||||
                    if isinstance(current, vt.VariantMap):
 | 
			
		||||
                    if part == "variants" and isinstance(current, vt.VariantMap):
 | 
			
		||||
                        # subscript instead of getattr for variant names
 | 
			
		||||
                        current = current[part]
 | 
			
		||||
                    else:
 | 
			
		||||
@@ -4466,62 +4437,31 @@ def write_attribute(spec, attribute, color):
 | 
			
		||||
                            raise SpecFormatStringError(m)
 | 
			
		||||
                        if isinstance(current, vn.VersionList):
 | 
			
		||||
                            if current == vn.any_version:
 | 
			
		||||
                                # We don't print empty version lists
 | 
			
		||||
                                return
 | 
			
		||||
                                # don't print empty version lists
 | 
			
		||||
                                return ""
 | 
			
		||||
 | 
			
		||||
                    if callable(current):
 | 
			
		||||
                        raise SpecFormatStringError("Attempted to format callable object")
 | 
			
		||||
 | 
			
		||||
                    if current is None:
 | 
			
		||||
                        # We're not printing anything
 | 
			
		||||
                        return
 | 
			
		||||
                        # not printing anything
 | 
			
		||||
                        return ""
 | 
			
		||||
 | 
			
		||||
            # Set color codes for various attributes
 | 
			
		||||
            col = None
 | 
			
		||||
            color = None
 | 
			
		||||
            if "variants" in parts:
 | 
			
		||||
                col = "+"
 | 
			
		||||
                color = VARIANT_COLOR
 | 
			
		||||
            elif "architecture" in parts:
 | 
			
		||||
                col = "="
 | 
			
		||||
                color = ARCHITECTURE_COLOR
 | 
			
		||||
            elif "compiler" in parts or "compiler_flags" in parts:
 | 
			
		||||
                col = "%"
 | 
			
		||||
                color = COMPILER_COLOR
 | 
			
		||||
            elif "version" in parts or "versions" in parts:
 | 
			
		||||
                col = "@"
 | 
			
		||||
                color = VERSION_COLOR
 | 
			
		||||
 | 
			
		||||
            # Finally, write the output
 | 
			
		||||
            write(sig + morph(spec, str(current)), col)
 | 
			
		||||
            # return colored output
 | 
			
		||||
            return safe_color(sig, str(current), color)
 | 
			
		||||
 | 
			
		||||
        attribute = ""
 | 
			
		||||
        in_attribute = False
 | 
			
		||||
        escape = False
 | 
			
		||||
 | 
			
		||||
        for c in format_string:
 | 
			
		||||
            if escape:
 | 
			
		||||
                out.write(c)
 | 
			
		||||
                escape = False
 | 
			
		||||
            elif c == "\\":
 | 
			
		||||
                escape = True
 | 
			
		||||
            elif in_attribute:
 | 
			
		||||
                if c == "}":
 | 
			
		||||
                    write_attribute(self, attribute, color)
 | 
			
		||||
                    attribute = ""
 | 
			
		||||
                    in_attribute = False
 | 
			
		||||
                else:
 | 
			
		||||
                    attribute += c
 | 
			
		||||
            else:
 | 
			
		||||
                if c == "}":
 | 
			
		||||
                    raise SpecFormatStringError(
 | 
			
		||||
                        "Encountered closing } before opening { in %s" % format_string
 | 
			
		||||
                    )
 | 
			
		||||
                elif c == "{":
 | 
			
		||||
                    in_attribute = True
 | 
			
		||||
                else:
 | 
			
		||||
                    out.write(c)
 | 
			
		||||
        if in_attribute:
 | 
			
		||||
            raise SpecFormatStringError(
 | 
			
		||||
                "Format string terminated while reading attribute." "Missing terminating }."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        formatted_spec = out.getvalue()
 | 
			
		||||
        return formatted_spec.strip()
 | 
			
		||||
        return SPEC_FORMAT_RE.sub(format_attribute, format_string).strip()
 | 
			
		||||
 | 
			
		||||
    def cformat(self, *args, **kwargs):
 | 
			
		||||
        """Same as format, but color defaults to auto instead of False."""
 | 
			
		||||
@@ -4529,6 +4469,16 @@ def cformat(self, *args, **kwargs):
 | 
			
		||||
        kwargs.setdefault("color", None)
 | 
			
		||||
        return self.format(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def spack_root(self):
 | 
			
		||||
        """Special field for using ``{spack_root}`` in Spec.format()."""
 | 
			
		||||
        return spack.paths.spack_root
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def spack_install(self):
 | 
			
		||||
        """Special field for using ``{spack_install}`` in Spec.format()."""
 | 
			
		||||
        return spack.store.STORE.layout.root
 | 
			
		||||
 | 
			
		||||
    def format_path(
 | 
			
		||||
        # self, format_string: str, _path_ctor: Optional[pathlib.PurePath] = None
 | 
			
		||||
        self,
 | 
			
		||||
@@ -4554,14 +4504,21 @@ def format_path(
 | 
			
		||||
 | 
			
		||||
        path_ctor = _path_ctor or pathlib.PurePath
 | 
			
		||||
        format_string_as_path = path_ctor(format_string)
 | 
			
		||||
        if format_string_as_path.is_absolute():
 | 
			
		||||
        if format_string_as_path.is_absolute() or (
 | 
			
		||||
            # Paths that begin with a single "\" on windows are relative, but we still
 | 
			
		||||
            # want to preserve the initial "\\" to be consistent with PureWindowsPath.
 | 
			
		||||
            # Ensure that this '\' is not passed to polite_filename() so it's not converted to '_'
 | 
			
		||||
            (os.name == "nt" or path_ctor == pathlib.PureWindowsPath)
 | 
			
		||||
            and format_string_as_path.parts[0] == "\\"
 | 
			
		||||
        ):
 | 
			
		||||
            output_path_components = [format_string_as_path.parts[0]]
 | 
			
		||||
            input_path_components = list(format_string_as_path.parts[1:])
 | 
			
		||||
        else:
 | 
			
		||||
            output_path_components = []
 | 
			
		||||
            input_path_components = list(format_string_as_path.parts)
 | 
			
		||||
 | 
			
		||||
        output_path_components += [
 | 
			
		||||
            fs.polite_filename(self.format(x)) for x in input_path_components
 | 
			
		||||
            fs.polite_filename(self.format(part)) for part in input_path_components
 | 
			
		||||
        ]
 | 
			
		||||
        return str(path_ctor(*output_path_components))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -390,11 +390,11 @@ def test_built_spec_cache(mirror_dir):
 | 
			
		||||
        assert any([r["spec"] == s for r in results])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fake_dag_hash(spec):
 | 
			
		||||
def fake_dag_hash(spec, length=None):
 | 
			
		||||
    # Generate an arbitrary hash that is intended to be different than
 | 
			
		||||
    # whatever a Spec reported before (to test actions that trigger when
 | 
			
		||||
    # the hash changes)
 | 
			
		||||
    return "tal4c7h4z0gqmixb1eqa92mjoybxn5l6"
 | 
			
		||||
    return "tal4c7h4z0gqmixb1eqa92mjoybxn5l6"[:length]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.usefixtures(
 | 
			
		||||
 
 | 
			
		||||
@@ -1276,7 +1276,7 @@ def test_user_config_path_is_default_when_env_var_is_empty(working_env):
 | 
			
		||||
 | 
			
		||||
def test_default_install_tree(monkeypatch, default_config):
 | 
			
		||||
    s = spack.spec.Spec("nonexistent@x.y.z %none@a.b.c arch=foo-bar-baz")
 | 
			
		||||
    monkeypatch.setattr(s, "dag_hash", lambda: "abc123")
 | 
			
		||||
    monkeypatch.setattr(s, "dag_hash", lambda length: "abc123")
 | 
			
		||||
    _, _, projections = spack.store.parse_install_tree(spack.config.get("config"))
 | 
			
		||||
    assert s.format(projections["all"]) == "foo-bar-baz/none-a.b.c/nonexistent-x.y.z-abc123"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -146,9 +146,6 @@ def test_autoload_all(self, modulefile_content, module_configuration):
 | 
			
		||||
 | 
			
		||||
        assert len([x for x in content if "depends_on(" in x]) == 5
 | 
			
		||||
 | 
			
		||||
    @pytest.mark.skipif(
 | 
			
		||||
        str(archspec.cpu.host().family) != "x86_64", reason="test data is specific for x86_64"
 | 
			
		||||
    )
 | 
			
		||||
    def test_alter_environment(self, modulefile_content, module_configuration):
 | 
			
		||||
        """Tests modifications to run-time environment."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,9 +114,6 @@ def test_prerequisites_all(
 | 
			
		||||
 | 
			
		||||
        assert len([x for x in content if "prereq" in x]) == 5
 | 
			
		||||
 | 
			
		||||
    @pytest.mark.skipif(
 | 
			
		||||
        str(archspec.cpu.host().family) != "x86_64", reason="test data is specific for x86_64"
 | 
			
		||||
    )
 | 
			
		||||
    def test_alter_environment(self, modulefile_content, module_configuration):
 | 
			
		||||
        """Tests modifications to run-time environment."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -703,22 +703,25 @@ def check_prop(check_spec, fmt_str, prop, getter):
 | 
			
		||||
            actual = spec.format(named_str)
 | 
			
		||||
            assert expected == actual
 | 
			
		||||
 | 
			
		||||
    def test_spec_formatting_escapes(self, default_mock_concretization):
 | 
			
		||||
        spec = default_mock_concretization("multivalue-variant cflags=-O2")
 | 
			
		||||
 | 
			
		||||
        sigil_mismatches = [
 | 
			
		||||
    @pytest.mark.parametrize(
 | 
			
		||||
        "fmt_str",
 | 
			
		||||
        [
 | 
			
		||||
            "{@name}",
 | 
			
		||||
            "{@version.concrete}",
 | 
			
		||||
            "{%compiler.version}",
 | 
			
		||||
            "{/hashd}",
 | 
			
		||||
            "{arch=architecture.os}",
 | 
			
		||||
        ]
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    def test_spec_formatting_sigil_mismatches(self, default_mock_concretization, fmt_str):
 | 
			
		||||
        spec = default_mock_concretization("multivalue-variant cflags=-O2")
 | 
			
		||||
 | 
			
		||||
        for fmt_str in sigil_mismatches:
 | 
			
		||||
            with pytest.raises(SpecFormatSigilError):
 | 
			
		||||
                spec.format(fmt_str)
 | 
			
		||||
        with pytest.raises(SpecFormatSigilError):
 | 
			
		||||
            spec.format(fmt_str)
 | 
			
		||||
 | 
			
		||||
        bad_formats = [
 | 
			
		||||
    @pytest.mark.parametrize(
 | 
			
		||||
        "fmt_str",
 | 
			
		||||
        [
 | 
			
		||||
            r"{}",
 | 
			
		||||
            r"name}",
 | 
			
		||||
            r"\{name}",
 | 
			
		||||
@@ -728,11 +731,12 @@ def test_spec_formatting_escapes(self, default_mock_concretization):
 | 
			
		||||
            r"{dag_hash}",
 | 
			
		||||
            r"{foo}",
 | 
			
		||||
            r"{+variants.debug}",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for fmt_str in bad_formats:
 | 
			
		||||
            with pytest.raises(SpecFormatStringError):
 | 
			
		||||
                spec.format(fmt_str)
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    def test_spec_formatting_bad_formats(self, default_mock_concretization, fmt_str):
 | 
			
		||||
        spec = default_mock_concretization("multivalue-variant cflags=-O2")
 | 
			
		||||
        with pytest.raises(SpecFormatStringError):
 | 
			
		||||
            spec.format(fmt_str)
 | 
			
		||||
 | 
			
		||||
    def test_combination_of_wildcard_or_none(self):
 | 
			
		||||
        # Test that using 'none' and another value raises
 | 
			
		||||
@@ -1138,12 +1142,12 @@ def _check_spec_format_path(spec_str, format_str, expected, path_ctor=None):
 | 
			
		||||
            r"\\hostname\sharename\{name}\{version}",
 | 
			
		||||
            r"\\hostname\sharename\git-test\git.foo_bar",
 | 
			
		||||
        ),
 | 
			
		||||
        # Windows doesn't attribute any significance to a leading
 | 
			
		||||
        # "/" so it is discarded
 | 
			
		||||
        # leading '/' is preserved on windows but converted to '\'
 | 
			
		||||
        # note that it's still not "absolute" -- absolute windows paths start with a drive.
 | 
			
		||||
        (
 | 
			
		||||
            "git-test@git.foo/bar",
 | 
			
		||||
            r"/installroot/{name}/{version}",
 | 
			
		||||
            r"installroot\git-test\git.foo_bar",
 | 
			
		||||
            r"\installroot\git-test\git.foo_bar",
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -638,6 +638,9 @@ def copy(self):
 | 
			
		||||
        return clone
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if not self:
 | 
			
		||||
            return ""
 | 
			
		||||
 | 
			
		||||
        # print keys in order
 | 
			
		||||
        sorted_keys = sorted(self.keys())
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -146,13 +146,11 @@ def from_string(string: str):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def typemin():
 | 
			
		||||
        return StandardVersion("", ((), (ALPHA,)), ("",))
 | 
			
		||||
        return _STANDARD_VERSION_TYPEMIN
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def typemax():
 | 
			
		||||
        return StandardVersion(
 | 
			
		||||
            "infinity", ((VersionStrComponent(len(infinity_versions)),), (FINAL,)), ("",)
 | 
			
		||||
        )
 | 
			
		||||
        return _STANDARD_VERSION_TYPEMAX
 | 
			
		||||
 | 
			
		||||
    def __bool__(self):
 | 
			
		||||
        return True
 | 
			
		||||
@@ -390,6 +388,13 @@ def up_to(self, index):
 | 
			
		||||
        return self[:index]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_STANDARD_VERSION_TYPEMIN = StandardVersion("", ((), (ALPHA,)), ("",))
 | 
			
		||||
 | 
			
		||||
_STANDARD_VERSION_TYPEMAX = StandardVersion(
 | 
			
		||||
    "infinity", ((VersionStrComponent(len(infinity_versions)),), (FINAL,)), ("",)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GitVersion(ConcreteVersion):
 | 
			
		||||
    """Class to represent versions interpreted from git refs.
 | 
			
		||||
 | 
			
		||||
@@ -1019,6 +1024,9 @@ def __hash__(self):
 | 
			
		||||
        return hash(tuple(self.versions))
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if not self.versions:
 | 
			
		||||
            return ""
 | 
			
		||||
 | 
			
		||||
        return ",".join(
 | 
			
		||||
            f"={v}" if isinstance(v, StandardVersion) else str(v) for v in self.versions
 | 
			
		||||
        )
 | 
			
		||||
@@ -1127,7 +1135,9 @@ def _prev_version(v: StandardVersion) -> StandardVersion:
 | 
			
		||||
    components[1::2] = separators[: len(release)]
 | 
			
		||||
    if prerelease_type != FINAL:
 | 
			
		||||
        components.extend((PRERELEASE_TO_STRING[prerelease_type], *prerelease[1:]))
 | 
			
		||||
    return StandardVersion("".join(str(c) for c in components), (release, prerelease), separators)
 | 
			
		||||
 | 
			
		||||
    # this is only used for comparison functions, so don't bother making a string
 | 
			
		||||
    return StandardVersion(None, (release, prerelease), separators)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def Version(string: Union[str, int]) -> Union[GitVersion, StandardVersion]:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user