# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project
"""
Utilities related to formatting output or translating input.
"""
from __future__ import absolute_import
from string import Formatter
from rez.vendor.enum import Enum
from rez.vendor.version.requirement import Requirement
from rez.exceptions import PackageRequestError
from rez.vendor.six import six
from pprint import pformat
import os
import os.path
import re
import time
PACKAGE_NAME_REGSTR = r"[a-zA-Z_0-9](\.?[a-zA-Z0-9_]+)*"
PACKAGE_NAME_REGEX = re.compile(r"^%s\Z" % PACKAGE_NAME_REGSTR)
ENV_VAR_REGSTR = r'\$(\w+|\{[^}]*\})'
ENV_VAR_REGEX = re.compile(ENV_VAR_REGSTR)
FORMAT_VAR_REGSTR = "{(?P<var>.+?)}"
FORMAT_VAR_REGEX = re.compile(FORMAT_VAR_REGSTR)
# package names that are invalid because they may clash with reserved dir
# names in some package repos (eg filesystem)
#
invalid_package_names = (
"__pycache__",
)
[docs]def is_valid_package_name(name, raise_error=False):
"""Test the validity of a package name string.
Args:
name (str): Name to test.
raise_error (bool): If True, raise an exception on failure
Returns:
bool.
"""
is_valid = (
PACKAGE_NAME_REGEX.match(name)
and name not in invalid_package_names
)
if raise_error and not is_valid:
raise PackageRequestError("Not a valid package name: %r" % name)
return is_valid
[docs]class PackageRequest(Requirement):
"""A package request parser.
Valid requests include:
* Any standard request, eg 'foo-1.2.3', '!foo-1', etc
* "Ephemeral" request, eg '.foo-1.2.3'
Example:
>>> pr = PackageRequest("foo-1.3+")
>>> print(pr.name, pr.range)
foo 1.3+
"""
def __init__(self, s):
super(PackageRequest, self).__init__(s)
# detect ephemeral package
if s.startswith('.'):
self.ephemeral = True
is_valid_package_name(self.name[1:], True)
else:
self.ephemeral = False
is_valid_package_name(self.name, True)
[docs]def expand_abbreviations(txt, fields):
"""Expand abbreviations in a format string.
If an abbreviation does not match a field, or matches multiple fields, it
is left unchanged.
Example:
>>> fields = ("hey", "there", "dude")
>>> expand_abbreviations("hello {d}", fields)
'hello dude'
Args:
txt (str): Format string.
fields (list of str): Fields to expand to.
Returns:
Expanded string.
"""
def _expand(matchobj):
s = matchobj.group("var")
if s not in fields:
matches = [x for x in fields if x.startswith(s)]
if len(matches) == 1:
s = matches[0]
return "{%s}" % s
return re.sub(FORMAT_VAR_REGEX, _expand, txt)
[docs]def expandvars(text, environ=None):
"""Expand shell variables of form $var and ${var}.
Unknown variables are left unchanged.
Args:
text (str): String to expand.
environ (dict): Environ dict to use for expansions, defaults to
os.environ.
Returns:
The expanded string.
"""
if '$' not in text:
return text
i = 0
if environ is None:
environ = os.environ
while True:
m = ENV_VAR_REGEX.search(text, i)
if not m:
break
i, j = m.span(0)
name = m.group(1)
if name.startswith('{') and name.endswith('}'):
name = name[1:-1]
if name in environ:
tail = text[j:]
text = text[:i] + environ[name]
i = len(text)
text += tail
else:
i = j
return text
[docs]def indent(txt):
"""Indent the given text by 4 spaces."""
lines = ((" " + x) for x in txt.split('\n'))
return '\n'.join(lines)
[docs]def dict_to_attributes_code(dict_):
"""Given a nested dict, generate a python code equivalent.
Example:
>>> d = {'foo': 'bah', 'colors': {'red': 1, 'blue': 2}}
>>> print(dict_to_attributes_code(d))
foo = 'bah'
colors.red = 1
colors.blue = 2
Returns:
str.
"""
lines = []
for key, value in dict_.items():
if isinstance(value, dict):
txt = dict_to_attributes_code(value)
lines_ = txt.split('\n')
for line in lines_:
if not line.startswith(' '):
line = "%s.%s" % (key, line)
lines.append(line)
else:
value_txt = pformat(value)
if '\n' in value_txt:
lines.append("%s = \\" % key)
value_txt = indent(value_txt)
lines.extend(value_txt.split('\n'))
else:
line = "%s = %s" % (key, value_txt)
lines.append(line)
return '\n'.join(lines)
[docs]def columnise(rows, padding=2):
"""Print rows of entries in aligned columns."""
strs = []
maxwidths = {}
for row in rows:
for i, e in enumerate(row):
se = str(e)
nse = len(se)
w = maxwidths.get(i, -1)
if nse > w:
maxwidths[i] = nse
for row in rows:
s = ''
for i, e in enumerate(row):
se = str(e)
if i < len(row) - 1:
n = maxwidths[i] + padding - len(se)
se += ' ' * n
s += se
strs.append(s)
return strs
[docs]def print_colored_columns(printer, rows, padding=2):
"""Like `columnise`, but with colored rows.
Args:
printer (`colorize.Printer`): Printer object.
Note:
The last entry in each row is the row color, or None for no coloring.
"""
rows_ = [x[:-1] for x in rows]
colors = [x[-1] for x in rows]
for col, line in zip(colors, columnise(rows_, padding=padding)):
printer(line, col)
time_divs = (
(365 * 24 * 3600, "years", 10),
(30 * 24 * 3600, "months", 12),
(7 * 24 * 3600, "weeks", 5),
(24 * 3600, "days", 7),
(3600, "hours", 10),
(60, "minutes", 10),
(1, "seconds", 60))
[docs]def readable_time_duration(secs):
"""Convert number of seconds into human readable form, eg '3.2 hours'.
"""
return _readable_units(secs, time_divs, True)
memory_divs = (
(1024 * 1024 * 1024 * 1024, "Tb", 128),
(1024 * 1024 * 1024, "Gb", 64),
(1024 * 1024, "Mb", 32),
(1024, "Kb", 16),
(1, "bytes", 1024))
[docs]def readable_memory_size(bytes_):
"""Convert number of bytes into human readable form, eg '1.2 Kb'.
"""
return _readable_units(bytes_, memory_divs)
def _readable_units(value, divs, plural_aware=False):
if value == 0:
unit = divs[-1][1]
return "0 %s" % unit
neg = (value < 0)
if neg:
value = -value
for quantity, unit, threshold in divs:
if value >= quantity:
f = value / float(quantity)
rounding = 0 if f > threshold else 1
f = round(f, rounding)
f = int(f * 10) / 10.0
if plural_aware and f == 1.0:
unit = unit[:-1]
txt = "%g %s" % (f, unit)
break
if neg:
txt = '-' + txt
return txt
[docs]def get_epoch_time_from_str(s):
"""Convert a string into epoch time. Examples of valid strings:
1418350671 # already epoch time
-12s # 12 seconds ago
-5.4m # 5.4 minutes ago
"""
try:
return int(s)
except:
pass
try:
if s.startswith('-'):
chars = {'d': 24 * 60 * 60,
'h': 60 * 60,
'm': 60,
's': 1}
m = chars.get(s[-1])
if m:
n = float(s[1:-1])
secs = int(n * m)
now = int(time.time())
return max((now - secs), 0)
except:
pass
raise ValueError("'%s' is an unrecognised time format." % s)
positional_suffix = ("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")
[docs]def positional_number_string(n):
"""Print the position string equivalent of a positive integer. Examples:
0: zeroeth
1: first
2: second
14: 14th
21: 21st
"""
if n > 20:
suffix = positional_suffix[n % 10]
return "%d%s" % (n, suffix)
elif n > 3:
return "%dth" % n
elif n == 3:
return "third"
elif n == 2:
return "second"
elif n == 1:
return "first"
return "zeroeth"
# regex used to expand user; set here to avoid recompile on every call
EXPANDUSER_RE = re.compile(
r'(\A|\s|[{pathseps}])~([{seps}]|[{pathseps}]|\s|\Z)'.format(
seps=re.escape(''.join(set([os.sep + (getattr(os, 'altsep') or os.sep)]))),
pathseps=re.escape(''.join(set([os.pathsep + ';'])))
)
)
[docs]def expanduser(path):
"""Expand '~' to home directory in the given string.
Note that this function deliberately differs from the builtin
os.path.expanduser() on Linux systems, which expands strings such as
'~sclaus' to that user's homedir. This is problematic in rez because the
string '~packagename' may inadvertently convert to a homedir, if a package
happens to match a username.
"""
if '~' not in path:
return path
if os.name == "nt":
if 'HOME' in os.environ:
userhome = os.environ['HOME']
elif 'USERPROFILE' in os.environ:
userhome = os.environ['USERPROFILE']
elif 'HOMEPATH' in os.environ:
drive = os.environ.get('HOMEDRIVE', '')
userhome = os.path.join(drive, os.environ['HOMEPATH'])
else:
return path
else:
userhome = os.path.expanduser('~')
def _expanduser(path):
return EXPANDUSER_RE.sub(
lambda m: m.groups()[0] + userhome + m.groups()[1],
path)
# only replace '~' if it's at start of string or is preceeded by pathsep or
# ';' or whitespace; AND, is followed either by sep, pathsep, ';', ' ' or
# end-of-string.
#
return os.path.normpath(_expanduser(path))
[docs]def as_block_string(txt):
"""Return a string formatted as a python block comment string, like the one
you're currently reading. Special characters are escaped if necessary.
"""
import json
lines = []
for line in txt.split('\n'):
line_ = json.dumps(line, ensure_ascii=False)
line_ = line_[1:-1].rstrip() # drop double quotes
lines.append(line_)
return '"""\n%s\n"""' % '\n'.join(lines)
_header_br = '#' * 80
_header_br_minor = '-' * 80