# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import os
import os.path
from collections.abc import Mapping
from pathlib import Path
from string import Template
from .charwidth import get_char_width
from .misc import seq2str2
from .unic import safe_str
MAX_ERROR_LINES = 40
MAX_ASSIGN_LENGTH = 200
_MAX_ERROR_LINE_LENGTH = 78
_ERROR_CUT_EXPLN = " [ Message content over the limit has been removed. ]"
[docs]
def cut_long_message(msg):
if MAX_ERROR_LINES is None:
return msg
lines = msg.splitlines()
lengths = [_get_virtual_line_length(line) for line in lines]
if sum(lengths) <= MAX_ERROR_LINES:
return msg
start = _prune_excess_lines(lines, lengths)
end = _prune_excess_lines(lines, lengths, from_end=True)
return "\n".join([*start, _ERROR_CUT_EXPLN, *end])
def _prune_excess_lines(lines, lengths, from_end=False):
if from_end:
lines.reverse()
lengths.reverse()
ret = []
total = 0
limit = MAX_ERROR_LINES // 2
for line, length in zip(lines[:limit], lengths[:limit]):
if total + length >= limit:
ret.append(_cut_long_line(line, total, from_end))
break
total += length
ret.append(line)
if from_end:
ret.reverse()
return ret
def _cut_long_line(line, used, from_end):
available_lines = MAX_ERROR_LINES // 2 - used
available_chars = available_lines * _MAX_ERROR_LINE_LENGTH - 3
if len(line) > available_chars:
if not from_end:
line = line[:available_chars] + "..."
else:
line = "..." + line[-available_chars:]
return line
def _get_virtual_line_length(line):
if not line:
return 1
lines, remainder = divmod(len(line), _MAX_ERROR_LINE_LENGTH)
return lines if not remainder else lines + 1
def _dict_to_str(d):
if not d:
return "{ }"
items = " | ".join(f"{safe_str(k)}={safe_str(d[k])}" for k in d)
return f"{{ {items} }}"
[docs]
def cut_assign_value(value):
if not isinstance(value, str):
value = safe_str(value)
if len(value) > MAX_ASSIGN_LENGTH:
value = value[:MAX_ASSIGN_LENGTH] + "..."
return value
[docs]
def get_console_length(text):
return sum(get_char_width(char) for char in text)
[docs]
def pad_console_length(text, width):
if width < 5:
width = 5
diff = get_console_length(text) - width
if diff > 0:
text = _lose_width(text, diff + 3) + "..."
return _pad_width(text, width)
def _pad_width(text, width):
more = width - get_console_length(text)
return text + " " * more
def _lose_width(text, diff):
lost = 0
while lost < diff:
lost += get_console_length(text[-1])
text = text[:-1]
return text
[docs]
def split_args_from_name_or_path(name):
"""Split arguments embedded to name or path like ``Example:arg1:arg2``.
The separator can be either colon ``:`` or semicolon ``;``. If both are used,
the first one is considered to be the separator.
"""
if os.path.exists(name):
return os.path.abspath(name), []
if isinstance(name, Path):
name = str(name)
index = _get_arg_separator_index_from_name_or_path(name)
if index == -1:
return name, []
args = name[index + 1 :].split(name[index])
name = name[:index]
if os.path.exists(name):
name = os.path.abspath(name)
return name, args
def _get_arg_separator_index_from_name_or_path(name):
colon_index = name.find(":")
# Handle absolute Windows paths
if colon_index == 1 and name[2:3] in ("/", "\\"):
colon_index = name.find(":", colon_index + 1)
semicolon_index = name.find(";")
if colon_index == -1:
return semicolon_index
if semicolon_index == -1:
return colon_index
return min(colon_index, semicolon_index)
[docs]
def getdoc(item):
return inspect.getdoc(item) or ""
[docs]
def getshortdoc(doc_or_item, linesep="\n"):
if not doc_or_item:
return ""
doc = doc_or_item if isinstance(doc_or_item, str) else getdoc(doc_or_item)
if not doc:
return ""
lines = []
for line in doc.splitlines():
if not line.strip():
break
lines.append(line)
return linesep.join(lines)
[docs]
def expand_variables(string, mapping=None):
"""Expands variables in the ``string`` based on the ``mapping``.
If ``mapping`` is not given, defaults to using ``os.environ``. Variable names
are limited to ASCII letters, numbers and underscores, and they cannot start
with a number.
This is similar to ``os.path.expandvars``, but there are certain differences:
- Only ``$NAME`` and ``${NAME}`` styles are supported, ``%NAME%`` is not.
- Non-existing variables cause a ``ValueError``.
- Escaping is possible by doubling the dollar sign like ``$$XXX``.
- Default values are supported with syntax ``${NAME=default}``.
New in Robot Framework 7.4.
"""
try:
return TemplateWithDefaults(string).substitute(mapping or os.environ)
except KeyError as err:
raise ValueError(f"Variable '{err.args[0]}' does not exist.")
[docs]
class TemplateWithDefaults(Template):
braceidpattern = r"(?a:[_a-z][_a-z0-9]*(=[^}]*)?)"
[docs]
def substitute(self, mapping=None, /, **kwds):
ns = NamespaceWithDefaults(mapping, **kwds)
return super().substitute(ns)
[docs]
def safe_substitute(self, mapping=None, /, **kwds):
ns = NamespaceWithDefaults(mapping, **kwds)
return super().safe_substitute(ns)
[docs]
class NamespaceWithDefaults(Mapping):
def __init__(self, data=None, /, **extra):
self.data = {**(data or {}), **extra}
def __getitem__(self, key):
if "=" in key:
key, default = key.split("=", 1)
return self.data.get(key, default)
return self.data[key]
def __iter__(self):
return iter(self.data)
def __len__(self):
return len(self.data)