Robot Framework
embedded.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import re
17 
18 from robot.errors import DataError
19 from robot.utils import get_error_message, is_string
20 from robot.variables import VariableIterator
21 
22 
23 ENABLE_STRICT_ARGUMENT_VALIDATION = False
24 
25 
27 
28  def __init__(self, name=None, args=(), custom_patterns=None):
29  self.namename = name
30  self.argsargs = args
31  self.custom_patternscustom_patterns = custom_patterns
32 
33  @classmethod
34  def from_name(cls, name):
35  return EmbeddedArgumentParser().parse(name) if '${' in name else cls()
36 
37  def match(self, name):
38  return self.namename.match(name)
39 
40  def map(self, values):
41  self.validatevalidate(values)
42  return list(zip(self.argsargs, values))
43 
44  def validate(self, values):
45  # Validating that embedded args match custom regexps also if args are
46  # given as variables was initially implemented in RF 6.0. It needed
47  # to be reverted due to backwards incompatibility reasons but the plan
48  # is to enable it again in RF 7.0:
49  # https://github.com/robotframework/robotframework/issues/4069
50  #
51  # TODO: Emit deprecation warnings if patterns don't match in RF 6.1:
52  # https://github.com/robotframework/robotframework/issues/4524
53  #
54  # Because the plan is to add validation back, the code was not removed
55  # but the `ENABLE_STRICT_ARGUMENT_VALIDATION` guard was added instead.
56  # Enabling validation requires only removing the following two lines
57  # (along with this comment). If someone wants to enable strict validation
58  # already now, they set `ENABLE_STRICT_ARGUMENT_VALIDATION` to True
59  # before running tests.
60  if not ENABLE_STRICT_ARGUMENT_VALIDATION:
61  return
62  if not self.custom_patternscustom_patterns:
63  return
64  for arg, value in zip(self.argsargs, values):
65  if arg in self.custom_patternscustom_patterns and is_string(value):
66  pattern = self.custom_patternscustom_patterns[arg]
67  if not re.fullmatch(pattern, value):
68  raise ValueError(f"Embedded argument '{arg}' got value '{value}' "
69  f"that does not match custom pattern '{pattern}'.")
70 
71  def __bool__(self):
72  return self.namename is not None
73 
74 
76 
79  _regexp_extension = re.compile(r'(?<!\\)\‍(\?.+\‍)')
80 
83  _regexp_group_start = re.compile(r'(?<!\\)\‍((.*?)\‍)')
84 
87  _escaped_curly = re.compile(r'(\\+)([{}])')
88 
91  _regexp_group_escape = r'(?:\1)'
92 
95  _default_pattern = '.*?'
96 
99  _variable_pattern = r'\$\{[^\}]+\}'
100 
101  def parse(self, string):
102  args = []
103  custom_patterns = {}
104  name_regexp = ['^']
105  for before, variable, string in VariableIterator(string, identifiers='$'):
106  name, pattern, custom = self._get_name_and_pattern_get_name_and_pattern(variable[2:-1])
107  args.append(name)
108  if custom:
109  custom_patterns[name] = pattern
110  pattern = self._format_custom_regexp_format_custom_regexp(pattern)
111  name_regexp.extend([re.escape(before), f'({pattern})'])
112  name_regexp.extend([re.escape(string), '$'])
113  name = self._compile_regexp_compile_regexp(name_regexp) if args else None
114  return EmbeddedArguments(name, args, custom_patterns or None)
115 
116  def _get_name_and_pattern(self, name):
117  if ':' in name:
118  name, pattern = name.split(':', 1)
119  custom = True
120  else:
121  pattern = self._default_pattern_default_pattern
122  custom = False
123  return name, pattern, custom
124 
125  def _format_custom_regexp(self, pattern):
126  for formatter in (self._regexp_extensions_are_not_allowed_regexp_extensions_are_not_allowed,
127  self._make_groups_non_capturing_make_groups_non_capturing,
128  self._unescape_curly_braces_unescape_curly_braces,
129  self._escape_escapes_escape_escapes,
130  self._add_automatic_variable_pattern_add_automatic_variable_pattern):
131  pattern = formatter(pattern)
132  return pattern
133 
135  if self._regexp_extension_regexp_extension.search(pattern):
136  raise DataError('Regexp extensions are not allowed in embedded arguments.')
137  return pattern
138 
139  def _make_groups_non_capturing(self, pattern):
140  return self._regexp_group_start_regexp_group_start.sub(self._regexp_group_escape_regexp_group_escape, pattern)
141 
142  def _unescape_curly_braces(self, pattern):
143  # Users must escape possible lone curly braces in patters (e.g. `${x:\{}`)
144  # or otherwise the variable syntax is invalid.
145  def unescape(match):
146  backslashes = len(match.group(1))
147  return '\\' * (backslashes // 2 * 2) + match.group(2)
148  return self._escaped_curly_escaped_curly.sub(unescape, pattern)
149 
150  def _escape_escapes(self, pattern):
151  # When keywords are matched, embedded arguments have not yet been
152  # resolved which means possible escapes are still doubled. We thus
153  # need to double them in the pattern as well.
154  return pattern.replace(r'\\', r'\\\\')
155 
157  return f'{pattern}|{self._variable_pattern}'
158 
159  def _compile_regexp(self, pattern):
160  try:
161  return re.compile(''.join(pattern), re.IGNORECASE)
162  except Exception:
163  raise DataError("Compiling embedded arguments regexp failed: %s"
164  % get_error_message())
def __init__(self, name=None, args=(), custom_patterns=None)
Definition: embedded.py:28
def get_error_message()
Returns error message of the last occurred exception.
Definition: error.py:34