# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import io
import itertools
import re
import string
from typing import List, Tuple
__all__ = [
"pkg_name_to_class_name",
"valid_module_name",
"possible_spack_module_names",
"simplify_name",
"NamespaceTrie",
]
#: see keyword.kwlist: https://github.com/python/cpython/blob/main/Lib/keyword.py
RESERVED_NAMES_ONLY_LOWERCASE = frozenset(
(
"and",
"as",
"assert",
"async",
"await",
"break",
"class",
"continue",
"def",
"del",
"elif",
"else",
"except",
"finally",
"for",
"from",
"global",
"if",
"import",
"in",
"is",
"lambda",
"nonlocal",
"not",
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
)
)
RESERVED_NAMES_LIST_MIXED_CASE = ("False", "None", "True")
# Valid module names can contain '-' but can't start with it.
_VALID_MODULE_RE_V1 = re.compile(r"^\w[\w-]*$")
_VALID_MODULE_RE_V2 = re.compile(r"^[a-z_][a-z0-9_]*$")
[docs]
def pkg_name_to_class_name(pkg_name: str):
"""Convert a Spack package name to a class name, based on
`PEP-8 <http://legacy.python.org/dev/peps/pep-0008/>`_:
* Module and package names use lowercase_with_underscores.
* Class names use the CapWords convention.
Not all package names are valid Python identifiers:
* They can contain ``-``, but cannot start with ``-``.
* They can start with numbers, e.g. ``3proxy``.
This function converts from the package name to the class convention by removing ``_`` and
``-``, and converting surrounding lowercase text to CapWords. If package name starts with a
number, the class name returned will be prepended with ``_`` to make a valid Python identifier.
"""
class_name = re.sub(r"[-_]+", "-", pkg_name)
class_name = string.capwords(class_name, "-")
class_name = class_name.replace("-", "")
# Ensure that the class name is a valid Python identifier
if re.match(r"^[0-9]", class_name) or class_name in RESERVED_NAMES_LIST_MIXED_CASE:
class_name = f"_{class_name}"
return class_name
def pkg_dir_to_pkg_name(dirname: str, package_api: Tuple[int, int]) -> str:
"""Translate a package dir (pkg_dir/package.py) to its corresponding package name"""
if package_api < (2, 0):
return dirname
return dirname.lstrip("_").replace("_", "-")
def pkg_name_to_pkg_dir(name: str, package_api: Tuple[int, int]) -> str:
"""Translate a package name to its corresponding package dir (pkg_dir/package.py)"""
if package_api < (2, 0):
return name
name = name.replace("-", "_")
if re.match(r"^[0-9]", name) or name in RESERVED_NAMES_ONLY_LOWERCASE:
name = f"_{name}"
return name
[docs]
def possible_spack_module_names(python_mod_name: str) -> List[str]:
"""Given a Python module name, return a list of all possible spack module
names that could correspond to it."""
mod_name = re.sub(r"^num(\d)", r"\1", python_mod_name)
parts = re.split(r"(_)", mod_name)
options = [["_", "-"]] * mod_name.count("_")
results: List[str] = []
for subs in itertools.product(*options):
s = list(parts)
s[1::2] = subs
results.append("".join(s))
return results
[docs]
def simplify_name(name: str) -> str:
"""Simplify package name to only lowercase, digits, and dashes.
Simplifies a name which may include uppercase letters, periods,
underscores, and pluses. In general, we want our package names to
only contain lowercase letters, digits, and dashes.
Args:
name (str): The original name of the package
Returns:
str: The new name of the package
"""
# Convert CamelCase to Dashed-Names
# e.g. ImageMagick -> Image-Magick
# e.g. SuiteSparse -> Suite-Sparse
# name = re.sub('([a-z])([A-Z])', r'\1-\2', name)
# Rename Intel downloads
# e.g. l_daal, l_ipp, l_mkl -> daal, ipp, mkl
if name.startswith("l_"):
name = name[2:]
# Convert UPPERCASE to lowercase
# e.g. SAMRAI -> samrai
name = name.lower()
# Replace '_' and '.' with '-'
# e.g. backports.ssl_match_hostname -> backports-ssl-match-hostname
name = name.replace("_", "-")
name = name.replace(".", "-")
# Replace "++" with "pp" and "+" with "-plus"
# e.g. gtk+ -> gtk-plus
# e.g. voro++ -> voropp
name = name.replace("++", "pp")
name = name.replace("+", "-plus")
# Simplify Lua package names
# We don't want "lua" to occur multiple times in the name
name = re.sub("^(lua)([^-])", r"\1-\2", name)
# Simplify Bio++ package names
name = re.sub("^(bpp)([^-])", r"\1-\2", name)
return name
[docs]
def valid_module_name(mod_name: str, package_api: Tuple[int, int]) -> bool:
"""Return whether mod_name is valid for use in Spack."""
if package_api < (2, 0):
return bool(_VALID_MODULE_RE_V1.match(mod_name))
elif not _VALID_MODULE_RE_V2.match(mod_name) or "__" in mod_name:
return False
elif mod_name.startswith("_"):
# it can only start with an underscore if followed by digit or reserved name
return mod_name[1:] in RESERVED_NAMES_ONLY_LOWERCASE or mod_name[1].isdigit()
else:
return mod_name not in RESERVED_NAMES_ONLY_LOWERCASE
[docs]
class NamespaceTrie:
[docs]
class Element:
def __init__(self, value):
self.value = value
def __init__(self, separator="."):
self._subspaces = {}
self._value = None
self._sep = separator
def __setitem__(self, namespace, value):
first, sep, rest = namespace.partition(self._sep)
if not first:
self._value = NamespaceTrie.Element(value)
return
if first not in self._subspaces:
self._subspaces[first] = NamespaceTrie()
self._subspaces[first][rest] = value
def _get_helper(self, namespace, full_name):
first, sep, rest = namespace.partition(self._sep)
if not first:
if not self._value:
raise KeyError("Can't find namespace '%s' in trie" % full_name)
return self._value.value
elif first not in self._subspaces:
raise KeyError("Can't find namespace '%s' in trie" % full_name)
else:
return self._subspaces[first]._get_helper(rest, full_name)
def __getitem__(self, namespace):
return self._get_helper(namespace, namespace)
[docs]
def is_prefix(self, namespace):
"""True if the namespace has a value, or if it's the prefix of one that
does."""
first, sep, rest = namespace.partition(self._sep)
if not first:
return True
elif first not in self._subspaces:
return False
else:
return self._subspaces[first].is_prefix(rest)
[docs]
def is_leaf(self, namespace):
"""True if this namespace has no children in the trie."""
first, sep, rest = namespace.partition(self._sep)
if not first:
return bool(self._subspaces)
elif first not in self._subspaces:
return False
else:
return self._subspaces[first].is_leaf(rest)
[docs]
def has_value(self, namespace):
"""True if there is a value set for the given namespace."""
first, sep, rest = namespace.partition(self._sep)
if not first:
return self._value is not None
elif first not in self._subspaces:
return False
else:
return self._subspaces[first].has_value(rest)
def __contains__(self, namespace):
"""Returns whether a value has been set for the namespace."""
return self.has_value(namespace)
def _str_helper(self, stream, level=0):
indent = level * " "
for name in sorted(self._subspaces):
stream.write(indent + name + "\n")
if self._value:
stream.write(indent + " " + repr(self._value.value))
stream.write(self._subspaces[name]._str_helper(stream, level + 1))
def __str__(self):
stream = io.StringIO()
self._str_helper(stream)
return stream.getvalue()