Source code for robot.running.arguments.embedded

#  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 re
from typing import Any, Mapping, Sequence

from robot.errors import DataError
from robot.utils import get_error_message
from robot.variables import VariableMatches

from ..context import EXECUTION_CONTEXTS


[docs] class EmbeddedArguments: def __init__(self, name: re.Pattern, args: Sequence[str] = (), custom_patterns: 'Mapping[str, str]|None' = None): self.name = name self.args = tuple(args) self.custom_patterns = custom_patterns or None
[docs] @classmethod def from_name(cls, name: str) -> 'EmbeddedArguments|None': return EmbeddedArgumentParser().parse(name) if '${' in name else None
[docs] def match(self, name: str) -> 're.Match|None': return self.name.fullmatch(name)
[docs] def map(self, args: Sequence[Any]) -> 'list[tuple[str, Any]]': self.validate(args) return list(zip(self.args, args))
[docs] def validate(self, args: Sequence[Any]): """Validate that embedded args match custom regexps. Initial validation is done already when matching keywords, but this validation makes sure arguments match also if they are given as variables. Currently, argument not matching only causes a deprecation warning, but that will be changed to ``ValueError`` in RF 8.0: https://github.com/robotframework/robotframework/issues/4069 """ if not self.custom_patterns: return for name, value in zip(self.args, args): if name in self.custom_patterns and isinstance(value, str): pattern = self.custom_patterns[name] if not re.fullmatch(pattern, value): # TODO: Change to `raise ValueError(...)` in RF 8.0. context = EXECUTION_CONTEXTS.current context.warn(f"Embedded argument '{name}' got value {value!r} " f"that does not match custom pattern {pattern!r}. " f"The argument is still accepted, but this behavior " f"will change in Robot Framework 8.0.")
[docs] class EmbeddedArgumentParser: _regexp_extension = re.compile(r'(?<!\\)\(\?.+\)') _regexp_group_start = re.compile(r'(?<!\\)\((.*?)\)') _escaped_curly = re.compile(r'(\\+)([{}])') _regexp_group_escape = r'(?:\1)' _default_pattern = '.*?' _variable_pattern = r'\$\{[^\}]+\}'
[docs] def parse(self, string: str) -> 'EmbeddedArguments|None': name_parts = ['^'] args = [] custom_patterns = {} after = string for match in VariableMatches(string, identifiers='$'): arg, pattern, is_custom = self._get_name_and_pattern(match.base) args.append(arg) if is_custom: custom_patterns[arg] = pattern pattern = self._format_custom_regexp(pattern) name_parts.extend([re.escape(match.before), f'({pattern})']) after = match.after if not args: return None name_parts.extend([re.escape(after), '$']) name = self._compile_regexp(''.join(name_parts)) return EmbeddedArguments(name, args, custom_patterns)
def _get_name_and_pattern(self, name: str) -> 'tuple[str, str, bool]': if ':' in name: name, pattern = name.split(':', 1) custom = True else: pattern = self._default_pattern custom = False return name, pattern, custom def _format_custom_regexp(self, pattern: str) -> str: for formatter in (self._regexp_extensions_are_not_allowed, self._make_groups_non_capturing, self._unescape_curly_braces, self._escape_escapes, self._add_automatic_variable_pattern): pattern = formatter(pattern) return pattern def _regexp_extensions_are_not_allowed(self, pattern: str) -> str: if self._regexp_extension.search(pattern): raise DataError('Regexp extensions are not allowed in embedded arguments.') return pattern def _make_groups_non_capturing(self, pattern: str) -> str: return self._regexp_group_start.sub(self._regexp_group_escape, pattern) def _unescape_curly_braces(self, pattern: str) -> str: # Users must escape possible lone curly braces in patters (e.g. `${x:\{}`) # or otherwise the variable syntax is invalid. def unescape(match): backslashes = len(match.group(1)) return '\\' * (backslashes // 2 * 2) + match.group(2) return self._escaped_curly.sub(unescape, pattern) def _escape_escapes(self, pattern: str) -> str: # When keywords are matched, embedded arguments have not yet been # resolved which means possible escapes are still doubled. We thus # need to double them in the pattern as well. return pattern.replace(r'\\', r'\\\\') def _add_automatic_variable_pattern(self, pattern: str) -> str: return f'{pattern}|{self._variable_pattern}' def _compile_regexp(self, pattern: str) -> re.Pattern: try: return re.compile(''.join(pattern), re.IGNORECASE) except Exception: raise DataError(f"Compiling embedded arguments regexp failed: " f"{get_error_message()}")