# Copyright 2013-2022 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)
# Need this because of spack.util.string
from __future__ import absolute_import
import itertools
import re
import string
from six import StringIO
import spack.error
__all__ = [
'mod_to_class',
'spack_module_to_python_module',
'valid_module_name',
'valid_fully_qualified_module_name',
'validate_fully_qualified_module_name',
'validate_module_name',
'possible_spack_module_names',
'simplify_name',
'NamespaceTrie']
# Valid module names can contain '-' but can't start with it.
_valid_module_re = r'^\w[\w-]*$'
# Valid module names can contain '-' but can't start with it.
_valid_fully_qualified_module_re = r'^(\w[\w-]*)(\.\w[\w-]*)*$'
[docs]def mod_to_class(mod_name):
"""Convert a name from module style to class name style. Spack mostly
follows `PEP-8 <http://legacy.python.org/dev/peps/pep-0008/>`_:
* Module and package names use lowercase_with_underscores.
* Class names use the CapWords convention.
Regular source code follows these convetions. Spack is a bit
more liberal with its Package names and Compiler names:
* They can contain '-' as well as '_', but cannot start with '-'.
* They can start with numbers, e.g. "3proxy".
This function converts from the module convention to the class
convention by removing _ and - and converting surrounding
lowercase text to CapWords. If mod_name starts with a number,
the class name returned will be prepended with '_' to make a
valid Python identifier.
"""
validate_module_name(mod_name)
class_name = re.sub(r'[-_]+', '-', mod_name)
class_name = string.capwords(class_name, '-')
class_name = class_name.replace('-', '')
# If a class starts with a number, prefix it with Number_ to make it
# a valid Python class name.
if re.match(r'^[0-9]', class_name):
class_name = "_%s" % class_name
return class_name
[docs]def spack_module_to_python_module(mod_name):
"""Given a Spack module name, returns the name by which it can be
imported in Python.
"""
if re.match(r'[0-9]', mod_name):
mod_name = 'num' + mod_name
return mod_name.replace('-', '_')
[docs]def possible_spack_module_names(python_mod_name):
"""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 = []
for subs in itertools.product(*options):
s = list(parts)
s[1::2] = subs
results.append(''.join(s))
return results
[docs]def simplify_name(name):
"""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):
"""Return whether mod_name is valid for use in Spack."""
return bool(re.match(_valid_module_re, mod_name))
[docs]def valid_fully_qualified_module_name(mod_name):
"""Return whether mod_name is a valid namespaced module name."""
return bool(re.match(_valid_fully_qualified_module_re, mod_name))
[docs]def validate_module_name(mod_name):
"""Raise an exception if mod_name is not valid."""
if not valid_module_name(mod_name):
raise InvalidModuleNameError(mod_name)
[docs]def validate_fully_qualified_module_name(mod_name):
"""Raise an exception if mod_name is not a valid namespaced module name."""
if not valid_fully_qualified_module_name(mod_name):
raise InvalidFullyQualifiedModuleNameError(mod_name)
class InvalidModuleNameError(spack.error.SpackError):
"""Raised when we encounter a bad module name."""
def __init__(self, name):
super(InvalidModuleNameError, self).__init__(
"Invalid module name: " + name)
self.name = name
class InvalidFullyQualifiedModuleNameError(spack.error.SpackError):
"""Raised when we encounter a bad full package name."""
def __init__(self, name):
super(InvalidFullyQualifiedModuleNameError, self).__init__(
"Invalid fully qualified package name: " + name)
self.name = name
[docs]class NamespaceTrie(object):
[docs] class Element(object):
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 = StringIO()
self._str_helper(stream)
return stream.getvalue()