robotframework-nl 2.2.1__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-nl
3
- Version: 2.2.1
3
+ Version: 3.1.0
4
4
  Summary: robotnl is a proving ground to boost Robot framework closer to Natural Language.
5
5
  Author-email: Johan Foederer <github@famfoe.nl>
6
6
  License: BSD 3-Clause License
@@ -41,7 +41,7 @@ Classifier: Operating System :: OS Independent
41
41
  Requires-Python: >=3.8
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
- Requires-Dist: robotframework>=6.0
44
+ Requires-Dist: robotframework>=7.0
45
45
 
46
46
  # robotframeworkNL - the oneliner
47
47
  robotframeworkNL is a proving ground to boost Robot framework closer to Natural Language.
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "robotframework-nl"
10
- version = "2.2.1"
10
+ version = "3.1.0"
11
11
  description = "robotnl is a proving ground to boost Robot framework closer to Natural Language."
12
12
  readme = "README.md"
13
13
  authors = [{ name = "Johan Foederer", email = "github@famfoe.nl" }]
@@ -19,7 +19,7 @@ classifiers = [
19
19
  ]
20
20
  keywords = ["robotframework", "robot", "testing", "dsl"]
21
21
  dependencies = [
22
- "robotframework >= 6.0",
22
+ "robotframework >= 7.0",
23
23
  ]
24
24
  requires-python = ">=3.8"
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-nl
3
- Version: 2.2.1
3
+ Version: 3.1.0
4
4
  Summary: robotnl is a proving ground to boost Robot framework closer to Natural Language.
5
5
  Author-email: Johan Foederer <github@famfoe.nl>
6
6
  License: BSD 3-Clause License
@@ -41,7 +41,7 @@ Classifier: Operating System :: OS Independent
41
41
  Requires-Python: >=3.8
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
- Requires-Dist: robotframework>=6.0
44
+ Requires-Dist: robotframework>=7.0
45
45
 
46
46
  # robotframeworkNL - the oneliner
47
47
  robotframeworkNL is a proving ground to boost Robot framework closer to Natural Language.
@@ -0,0 +1 @@
1
+ robotframework>=7.0
@@ -0,0 +1,394 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2021, J. Foederer
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # 1. Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # 3. Neither the name of the copyright holder nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+
33
+ from robot.api import TypeInfo
34
+ from robot.libraries.BuiltIn import BuiltIn
35
+ from robot.running.arguments import TypeConverter
36
+ from robot.utils import is_list_like
37
+
38
+ from .inline_keywords import keyword
39
+
40
+ class CheckOperator:
41
+ """
42
+ This class defines a set of commonly used operators for use by 'Check that'
43
+ and other check keywords
44
+ """
45
+
46
+ ################################################################################################
47
+ # Generic operators that can work on basically any object type
48
+ def equals(self, lValue, rValue):
49
+ """Checks whether the left and right side are equal to each other [`=`]
50
+
51
+ Applies Robot type conversions when executing the check.
52
+ Examples:
53
+ | `Check that` | 7 | `=` | 7 |
54
+ | `Check that` | ${7} | `=` | 7.0 |
55
+ | `Check that` | _Two times_ | 6 | `equals` | 12 |
56
+ | `Check that` | text | `equals` | TeXT |
57
+ """
58
+ return OperatorProxy("==").basicOperator(lValue, rValue)
59
+
60
+ def is_less_than(self, lValue, rValue):
61
+ """Checks whether the left side `is less than` or smaller than the right side [`<`]
62
+
63
+ Applies Robot type conversions when executing the check.
64
+ Examples:
65
+ | `Check that` | 2 | `<` | 4 |
66
+ | `Check that` | 2 | `is less than` | 4 |
67
+ """
68
+ return OperatorProxy("<").basicOperator(lValue, rValue)
69
+
70
+ def is_greater_than(self, lValue, rValue):
71
+ """Checks whether the left side `is greater than` or larger than the right side [`>`]
72
+
73
+ Applies Robot type conversions when executing the check.
74
+ Examples:
75
+ | `Check that` | 4 | `>` | 2 |
76
+ | `Check that` | 4 | `is greater than` | 2 |
77
+ """
78
+ return OperatorProxy(">").basicOperator(lValue, rValue)
79
+
80
+ def is_less_than_or_equal_to(self, lValue, rValue):
81
+ """Checks whether the left side `is less than or equal to` the right side [`≤`]
82
+
83
+ Applies Robot type conversions when executing the check.
84
+ Examples:
85
+ | `Check that` | 2 | `≤` | 2 |
86
+ | `Check that` | 2 | `is less than or equal to` | 4 |
87
+ """
88
+ return OperatorProxy("<=").basicOperator(lValue, rValue)
89
+
90
+ def is_greater_than_or_equal_to(self, lValue, rValue):
91
+ """Checks whether the left side `is greater than or equal to` the right side [`≥`]
92
+
93
+ Applies Robot type conversions when executing the check.
94
+ Examples:
95
+ | `Check that` | 4 | `≥` | 4 |
96
+ | `Check that` | 4 | `is greater than or equal to` | 2 |
97
+ """
98
+ return OperatorProxy(">=").basicOperator(lValue, rValue)
99
+
100
+ def does_not_equal(self, lValue, rValue):
101
+ """Checks whether the left side `does not equal`, i.e. is different from the right side [`≠`]
102
+
103
+ Applies Robot type conversions when executing the check.
104
+ Examples:
105
+ | `Check that` | 7 | `≠` | 13 |
106
+ | `Check that` | 7 | `≠` | 7.01 |
107
+ | `Check that` | _Two times_ | 6 | `does not equal` | 13 |
108
+ | `Check that` | random text | `does not equal` | my text |
109
+ """
110
+ return OperatorProxy("!=").basicOperator(lValue, rValue)
111
+
112
+ ################################################################################################
113
+ # Operators that work on text items str() or Unicode()
114
+ def contains_text(self, baseString, subString):
115
+ """Performs a case insensitive check whether the right side is a substring of the left side
116
+
117
+ Examples:
118
+ | `Check that` | the time is now | `contains text` | me |
119
+ | `Check that` | the time is now | `contains text` | ME |
120
+ | `Check That` | Robotstraße | `contains text` | Strasse |
121
+ """
122
+ try:
123
+ return self.contains_exact_text(baseString.casefold(), subString.casefold())
124
+ except:
125
+ return False
126
+
127
+ def contains_exact_text(self, baseString, subString):
128
+ """Performs a case sensitive check whether the right side is a substring of the left side
129
+
130
+ Examples:
131
+ | `Check that` | the time is now | `contains text` | me |
132
+ | `Check That` | Robotstraße | `contains text` | straße |
133
+ """
134
+ try:
135
+ return subString in baseString
136
+ except:
137
+ return False
138
+
139
+ def matches_without_case_to(self, leftText, rightText):
140
+ """Performs a case insensitive check whether the left and right side texts are equal
141
+
142
+ Examples:
143
+ | `Check that` | the time is now | `matches without case to` | the time is now |
144
+ | `Check that` | the time is now | `matches without case to` | THE TIME IS NOW |
145
+ | `Check That` | Robotstraße | `matches without case to` | ROBOTstrasse |
146
+ """
147
+ try:
148
+ return self.matches_with_case_to(leftText.casefold(), rightText.casefold())
149
+ except:
150
+ return False
151
+
152
+ def matches_with_case_to(self, leftText, rightText):
153
+ """Performs a case sensitive check whether the left and right side texts are equal
154
+
155
+ Examples:
156
+ | `Check that` | the time is now | `matches with case to` | the time is now |
157
+ | `Check That` | Robotstraße | `matches with case to` | Robotstraße |
158
+ """
159
+ return leftText == rightText
160
+
161
+ def does_not_contain_text(self, baseString, subString):
162
+ """Performs a case insensitive check whether the right side is not a substring of the left side
163
+
164
+ Examples:
165
+ | `Check that` | random text | `does not contain text` | my text |
166
+ """
167
+ return not self.contains_text(baseString, subString)
168
+
169
+ def does_not_contain_exact_text(self, baseString, subString):
170
+ """Performs a case sensitive check whether the right side is not a substring of the left side
171
+
172
+ Examples:
173
+ | `Check that` | random text | `does not contain exact text` | my text |
174
+ | `Check that` | random text | `does not contain exact text` | RANDOM text |
175
+ """
176
+ return not self.contains_exact_text(baseString, subString)
177
+
178
+ def does_not_match_without_case_to(self, leftText, rightText):
179
+ """Performs a case insensitive check whether the left and right side texts are different
180
+
181
+ Examples:
182
+ | `Check that` | random text | `does not match without case to` | my text |
183
+ """
184
+ return not self.matches_without_case_to(leftText, rightText)
185
+
186
+ def does_not_match_with_case_to(self, leftText, rightText):
187
+ """Performs a case sensitive check whether the left and right side texts are different
188
+
189
+ Examples:
190
+ | `Check that` | random text | `does not match with case to` | my text |
191
+ | `Check that` | random text | `does not match with case to` | RANDOM text |
192
+ """
193
+ return not self.matches_with_case_to(leftText, rightText)
194
+
195
+ ################################################################################################
196
+ # Operators that work on lists or other sequences
197
+ def is_empty(self, sequence):
198
+ """Checks whether the sequence on the left does not contain any items.
199
+
200
+ Example:
201
+ | Take a new suitcase |
202
+ | `Check that` | suitcase | `is empty` |
203
+ _Assumes a 'box' type to be defined with associated action and observation keywords._
204
+ """
205
+ return len(sequence) == 0
206
+
207
+ @keyword("contains ${n} items")
208
+ def contains_n_items(self, n:int, sequence):
209
+ """Checks whether the sequence on the left contains ${n} items. Uses python's len-operator
210
+ to count the number of items.
211
+
212
+ Example:
213
+ | `Check precondition` | suitcase | `is empty` |
214
+ | Put toothbrush into suitcase |
215
+ | `Check that` | suitcase | `contains 1 item` |
216
+ | Put t-shirt into suitcase |
217
+ | `Check that` | suitcase | `contains 2 items` |
218
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
219
+ """
220
+ count = len(sequence)
221
+ BuiltIn().log(f"Counted {count} items")
222
+ return count == n
223
+
224
+ def contains_1_item(self, sequence):
225
+ return self.contains_n_items(1, sequence)
226
+ contains_1_item.__doc__ = contains_n_items.__doc__
227
+
228
+ def contains(self, sequence, part):
229
+ """Checks whether part is present in sequence. Uses Python's primitive in-operator.
230
+
231
+ Example:
232
+ | Put toothbrush into suitcase |
233
+ | Put t-shirt into suitcase |
234
+ | `Check that` | suitcase | `contains` | toothbrush |
235
+ | `Check that` | suitcase | `contains` | t-shirt |
236
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
237
+ """
238
+ return part in sequence
239
+
240
+ def does_not_contain(self, sequence, part):
241
+ """Checks whether part is present in sequence. Uses Python's primitive 'not in' operator.
242
+
243
+ Example:
244
+ | `Check precondition` | suitcase | `is empty` |
245
+ | Put toothbrush into suitcase |
246
+ | `Check that` | suitcase | `does not contain` | t-shirt |
247
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
248
+ """
249
+ return part not in sequence
250
+
251
+ def contains_item(self, sequence, part):
252
+ """Checks whether the right side item(s) is/are part of the sequence on the left side.
253
+
254
+ `Contains item` and the plural `Contains items` are aliases. The difference with `Contains`
255
+ is that these iterate over the sequence and applies *automatic Robot type conversion*
256
+ between elements when applicable. Duplicate items from the right side can match a single
257
+ item from the left side.
258
+
259
+ Example:
260
+ | Put toothbrush into suitcase |
261
+ | Put t-shirt into suitcase |
262
+ | `Check that` | suitcase | `contains item` | toothbrush |
263
+ | `Check that` | suitcase | `contains items` | t-shirt | toothbrush |
264
+ | `Check that` | suitcase's lock code | `contains items` | 7 | ${2} |
265
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
266
+ """
267
+ if not is_list_like(part):
268
+ part = [part]
269
+ for elem in part:
270
+ BuiltIn().log(f"Processing '{elem}' from right side")
271
+ for item in sequence:
272
+ if self.equals(elem, item):
273
+ BuiltIn().log("Matched")
274
+ break
275
+ BuiltIn().log("No match")
276
+ else:
277
+ BuiltIn().log(f"{elem} not present in left side list")
278
+ return False
279
+ return True
280
+
281
+ # Alias for plural form
282
+ contains_items = contains_item
283
+
284
+ def contains_exactly_the_items_from(self, sequence, sequence_right):
285
+ """
286
+ Checks whether the sequence on the right side contains all items of the left side and vice versa.
287
+ Iterates over sequence on the left and matches each element with a single element on the right.
288
+ The items can be in any order. Automatic Robot type conversion is applied when applicable.
289
+
290
+ Example:
291
+ | `Check precondition` | suitcase | `is empty` |
292
+ | Put toothbrush into suitcase |
293
+ | Put t-shirt into suitcase |
294
+ | `Check that` | suitcase | `contains exactly the items from` | toothbrush | t-shirt |
295
+ | `Check that` | suitcase | `contains exactly the items from` | t-shirt | toothbrush |
296
+ | `Check that` | my packing list | `contains exactly the items from` | toothbrush | t-shirt |
297
+ | `Check that` | suitcase | `contains exactly the items from` | my packing list |
298
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
299
+ """
300
+ if isinstance(sequence_right, str):
301
+ sequence_right = [sequence_right]
302
+ sequence_right = [*sequence_right]
303
+ for item in sequence:
304
+ BuiltIn().log(f"Processing '{item}' from left side list")
305
+ for i in range(len(sequence_right)):
306
+ if self.equals(item, sequence_right[i]):
307
+ sequence_right.pop(i)
308
+ break
309
+ else:
310
+ BuiltIn().log(f"Item '{item}' from left side is not found in the list on the right side")
311
+ return False
312
+ if len(sequence_right) > 0:
313
+ BuiltIn().log(f"Not all items from right side list are present: {sequence_right}")
314
+ return False
315
+
316
+ return True
317
+
318
+ def does_not_contain_item(self, sequence, part):
319
+ """
320
+ Checks whether the right side item is not part of the sequence on the left side.
321
+ Iterates over the sequence and applies automatic Robot type conversion where applicable.
322
+
323
+ No plural variant is available for this keyword due to ambiguity with List-like values. When
324
+ writing "[2, 4, 6] does not contain items [4, 5, 6]", one could expect a pass, because the
325
+ set [4, 5, 6] is not contained, but also a fail because item 4 *is* part of the left side set.
326
+
327
+ Example:
328
+ | `Check precondition` | suitcase | `is empty` |
329
+ | Put toothbrush into suitcase |
330
+ | `Check that` | suitcase | `does not contain item` | t-shirt |
331
+ _Assumes a 'suitcase' type to be defined with associated action and observation keywords._
332
+ """
333
+ if is_list_like(part):
334
+ raise TypeError("List-like items not accepted as right side value")
335
+ return not self.contains_item(sequence, part)
336
+
337
+ # Add operator keywords that do not comply to Python's identifier syntax
338
+ setattr(CheckOperator, "=", CheckOperator.equals)
339
+ setattr(CheckOperator, "<", CheckOperator.is_less_than)
340
+ setattr(CheckOperator, ">", CheckOperator.is_greater_than)
341
+ setattr(CheckOperator, "≤", CheckOperator.is_less_than_or_equal_to)
342
+ setattr(CheckOperator, "≥", CheckOperator.is_greater_than_or_equal_to)
343
+ setattr(CheckOperator, "≠", CheckOperator.does_not_equal)
344
+
345
+ class OperatorProxy:
346
+ """
347
+ Proxy class for mapping generic Robot comparison keywords to Python operators
348
+ """
349
+ def __init__(self, s_operator):
350
+ self.__s_Operator = s_operator
351
+
352
+ @staticmethod
353
+ def __typeCastRobotStringValue(leadingValue, otherValue, name):
354
+ """
355
+ When Robot files are parsed, arguments are always passed as string even when comparing
356
+ numbers or other objects. When a string argument is detected the other argument is
357
+ deemed leading. This function converts the other argument to the leading value's type
358
+ when possible. Otherwise the value is kept unchanged, inevitably leading to a mismatch
359
+ in comparison.
360
+
361
+ Precondition: otherValue is of types str and supports .casefold()
362
+ returns type casted otherValue
363
+ """
364
+ CastedOther = otherValue # By default leave untouched
365
+ converter = TypeConverter.converter_for(TypeInfo.from_type(type(leadingValue)))
366
+ if converter:
367
+ try:
368
+ CastedOther = converter.convert(otherValue, name)
369
+ except ValueError as err:
370
+ BuiltIn().log(err, level='DEBUG')
371
+ BuiltIn().log(f"Comparing as {converter.type_name} values")
372
+
373
+ if isinstance(CastedOther, str):
374
+ # By default compare as case insensitive Unicode. Note that it already was a string.
375
+ BuiltIn().log(f"Interpreting {name} '{otherValue}' as string (case insensitive)")
376
+ CastedOther = str(otherValue).casefold()
377
+
378
+ return CastedOther
379
+
380
+ def basicOperator(self, lvalue, rvalue):
381
+ # Local argument copies for assignment
382
+ lValue = lvalue
383
+ rValue = rvalue
384
+
385
+ if type(lvalue) is str:
386
+ lValue = OperatorProxy.__typeCastRobotStringValue(rvalue, lvalue, "left operand")
387
+
388
+ if type(rvalue) is str:
389
+ rValue = OperatorProxy.__typeCastRobotStringValue(lvalue, rvalue, "right operand")
390
+
391
+ if lValue is lvalue and rValue is rvalue:
392
+ BuiltIn().log("Comparing values as is")
393
+
394
+ return eval(f"lValue {self.__s_Operator} rValue")
@@ -38,6 +38,7 @@ except ImportError:
38
38
  tkinter = False
39
39
 
40
40
  from robot.libraries.BuiltIn import BuiltIn
41
+ from robot.running import RUN_KW_REGISTER
41
42
  from robot.utils import timestr_to_secs, secs_to_timestr
42
43
  from .inline_keywords import is_keyword
43
44
 
@@ -77,6 +78,7 @@ class RobotChecks:
77
78
  except CheckFailed as failure:
78
79
  failure.ROBOT_CONTINUE_ON_FAILURE = False
79
80
  raise failure
81
+ RUN_KW_REGISTER.register_run_keyword('robotnl', check_precondition.__name__, args_to_process=0, deprecation_warning=False)
80
82
 
81
83
  def check_postcondition(self, *args):
82
84
  """
@@ -93,6 +95,7 @@ class RobotChecks:
93
95
  except CheckFailed as failure:
94
96
  failure.ROBOT_CONTINUE_ON_FAILURE = False
95
97
  raise failure
98
+ RUN_KW_REGISTER.register_run_keyword('robotnl', check_postcondition.__name__, args_to_process=0, deprecation_warning=False)
96
99
 
97
100
  def check_that(self, *args):
98
101
  """
@@ -113,13 +116,13 @@ class RobotChecks:
113
116
 
114
117
  Examples:
115
118
  | `Check that` | 3 | `=` | 3 |
116
- | `Check that` | Two times | 6 | `equals` | 12 |
117
- | `Check that` | Two times | 5 | `≠` | Two times | 7 |
118
- | `Check that` | Earth exists |
119
+ | `Check that` | _Two times_ | 6 | `equals` | 12 |
120
+ | `Check that` | _Two times_ | 5 | `≠` | _Two times_ | 7 |
121
+ | `Check that` | _Earth exists_ |
119
122
 
120
123
  'Two times' in these examples is assumed to be defined as a Robot keyword that takes one
121
- argument and multiplies it by 2. Check that will pass if the evaluated result of Two times 6
122
- equals the fixed expected value 12.
124
+ argument and multiplies it by 2. `Check that` will pass if the evaluated result of _Two
125
+ times_ 6 equals the fixed expected value 12.
123
126
 
124
127
  *Adding time constraints*:\n
125
128
  Any check can be extended with an additional timing constraint by adding ``within``
@@ -127,15 +130,16 @@ class RobotChecks:
127
130
  the specified time has passed. In the latter case the test case will fail.
128
131
 
129
132
  Example with time constraint:
130
- | `Check that` | condition is true | within | 1 minute 30 seconds |
133
+ | `Check that` | _condition is true_ | within | 1 minute 30 seconds |
131
134
 
132
135
  Elevator example:
133
- | `Check that` | elevator doors are closed |
134
- | Request elevator at floor | 3 |
135
- | `Check that` | elevator floor | `equals` | 3 | within | 20 seconds |
136
- | `Check that` | offset to floor level in mm | `≤` | 5 | within | 3 seconds |
136
+ | `Check that` | _elevator doors are closed_ |
137
+ | _Request elevator at floor_ | 3 |
138
+ | `Check that` | _elevator floor_ | `equals` | 3 | within | 20 seconds |
139
+ | `Check that` | _offset to floor level in mm_ | `≤` | 5 | within | 3 seconds |
137
140
  """
138
141
  return RobotChecks.__execute_check("Requirement", *args)
142
+ RUN_KW_REGISTER.register_run_keyword('robotnl', check_that.__name__, args_to_process=0, deprecation_warning=False)
139
143
 
140
144
  def check_manual(self, checkRequestText=""):
141
145
  """
@@ -32,8 +32,7 @@
32
32
 
33
33
  from robot.libraries.BuiltIn import BuiltIn
34
34
  from robot.api.deco import keyword as robot_keyword
35
- from robot.api import logger
36
- from robot.utils import type_repr
35
+ from robot.utils import type_name
37
36
  from robot.running.arguments import TypeConverter
38
37
 
39
38
  from functools import wraps
@@ -81,7 +80,6 @@ class InlineKeyword(Generic[TypeVar('T')]):
81
80
  Inline keywords are keywords that are used as argument to other keywords. The keyword will be
82
81
  evaluated and its return value used as the actual argument.
83
82
  """
84
- _name = 'inline keyword'
85
83
 
86
84
 
87
85
  # work around for Libdoc
@@ -97,24 +95,21 @@ class InlineKeywordConverter(TypeConverter):
97
95
  type = InlineKeyword
98
96
  type_name = 'InlineKeyword'
99
97
 
100
- def __init__(self, used_type, custom_converters=None, languages=None):
101
- super().__init__(used_type, custom_converters, languages)
102
- types = self._get_nested_types(used_type, expected_count=1)
103
- if not types:
104
- self.converter = None
105
- else:
106
- self.type_name = type_repr(used_type)
107
- self.converter = self.converter_for(types[0], custom_converters, languages)
98
+ def __init__(self, type_info, custom_converters=None, languages=None):
99
+ super().__init__(type_info, custom_converters, languages)
100
+ self.converter = self.converter_for(self.type_info.nested[0])
101
+ self.type_name = f"keyword returning {type_name(self.type_info.nested[0].type)}"
108
102
 
109
- def _convert(self, value, explicit_type=True):
103
+ def _convert(self, value):
110
104
  if not is_keyword(value):
111
105
  raise ValueError
112
106
  BuiltIn().log(f"Evaluating argument as keyword [{value}]")
113
107
  result = BuiltIn().run_keyword(value)
114
108
  BuiltIn().log(f"{value} → {result}")
115
109
  if self.converter:
110
+ # Convert the return type of the keyword to the expected type
116
111
  try:
117
- result = self.converter.convert(f"result of: {value}", result, explicit_type)
112
+ result = self.converter.convert(result, f"result of: {value}")
118
113
  except ValueError as err:
119
114
  BuiltIn().log(f"Incorrect keyword return type: {err}")
120
115
  raise
@@ -0,0 +1 @@
1
+ VERSION = '3.1.0'
@@ -1 +0,0 @@
1
- robotframework>=6.0
@@ -1,227 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # BSD 3-Clause License
4
- #
5
- # Copyright (c) 2021, J. Foederer
6
- # All rights reserved.
7
- #
8
- # Redistribution and use in source and binary forms, with or without
9
- # modification, are permitted provided that the following conditions are met:
10
- #
11
- # 1. Redistributions of source code must retain the above copyright notice, this
12
- # list of conditions and the following disclaimer.
13
- #
14
- # 2. Redistributions in binary form must reproduce the above copyright notice,
15
- # this list of conditions and the following disclaimer in the documentation
16
- # and/or other materials provided with the distribution.
17
- #
18
- # 3. Neither the name of the copyright holder nor the names of its
19
- # contributors may be used to endorse or promote products derived from
20
- # this software without specific prior written permission.
21
- #
22
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
- # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
- # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
- # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
- # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
- # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
-
33
- from robot.libraries.BuiltIn import BuiltIn
34
- from robot.running.arguments import TypeConverter
35
-
36
-
37
- class CheckOperator:
38
- """
39
- This class defines a set of commonly used operators for use by 'Check that'
40
- and other check keywords
41
- """
42
-
43
- ################################################################################################
44
- # Generic operators that can work on basically any object type
45
- def equals(self, lValue, rValue):
46
- """Checks whether the left and right side are equal to each other [=]"""
47
- return OperatorProxy("==").basicOperator(lValue, rValue)
48
-
49
- def is_less_than(self, lValue, rValue):
50
- """Checks whether the left side is less than or smaller than the right side [<]"""
51
- return OperatorProxy("<").basicOperator(lValue, rValue)
52
-
53
- def is_greater_than(self, lValue, rValue):
54
- """Checks whether the left side is greater than or larger than the right side [>]"""
55
- return OperatorProxy(">").basicOperator(lValue, rValue)
56
-
57
- def is_less_than_or_equal_to(self, lValue, rValue):
58
- """Checks whether the left side is less than or equal to the right side [≤]"""
59
- return OperatorProxy("<=").basicOperator(lValue, rValue)
60
-
61
- def is_greater_than_or_equal_to(self, lValue, rValue):
62
- """Checks whether the left side is greater than or equal to the right side [≥]"""
63
- return OperatorProxy(">=").basicOperator(lValue, rValue)
64
-
65
- def does_not_equal(self, lValue, rValue):
66
- """Checks whether the left side is different from the right side [≠]"""
67
- return OperatorProxy("!=").basicOperator(lValue, rValue)
68
-
69
- ################################################################################################
70
- # Operators that work on text items str() or Unicode()
71
- def contains_text(self, baseString, subString):
72
- """Performs a case insensitive check whether the right side is a substring of the left side"""
73
- try:
74
- return self.contains_exact_text(baseString.lower(), subString.lower())
75
- except:
76
- return False
77
-
78
- def contains_exact_text(self, baseString, subString):
79
- """Performs a case sensitive check whether the right side is a substring of the left side"""
80
- try:
81
- return subString in baseString
82
- except:
83
- return False
84
-
85
- def matches_without_case_to(self, leftText, rightText):
86
- """Performs a case insensitive check whether the left and right side texts are equal"""
87
- try:
88
- return self.matches_with_case_to(leftText.lower(), rightText.lower())
89
- except:
90
- return False
91
-
92
- def matches_with_case_to(self, leftText, rightText):
93
- """Performs a case sensitive check whether the left and right side texts are equal"""
94
- return leftText == rightText
95
-
96
- def does_not_contain_text(self, baseString, subString):
97
- """Performs a case insensitive check whether the right side is not a substring of the left side"""
98
- return not self.contains_text(baseString, subString)
99
-
100
- def does_not_contain_exact_text(self, baseString, subString):
101
- """Performs a case sensitive check whether the right side is not a substring of the left side"""
102
- return not self.contains_exact_text(baseString, subString)
103
-
104
- def does_not_match_without_case_to(self, leftText, rightText):
105
- """Performs a case insensitive check whether the left and right side texts are different"""
106
- return not self.matches_without_case_to(leftText, rightText)
107
-
108
- def does_not_match_with_case_to(self, leftText, rightText):
109
- """Performs a case sensitive check whether the left and right side texts are different"""
110
- return not self.matches_with_case_to(leftText, rightText)
111
-
112
- ################################################################################################
113
- # Operators that work on lists or other sequences
114
- def is_empty(self, iterable):
115
- return len(iterable) == 0
116
-
117
- def contains(self, iterable, part):
118
- """Checks whether part is present in iterable. Uses primitive 'in' operator."""
119
- return part in iterable
120
-
121
- def does_not_contain(self, iterable, part):
122
- """Checks whether part is present in iterable. Uses primitive 'not in' operator."""
123
- return part not in iterable
124
-
125
- def contains_item(self, sequence, part):
126
- """
127
- Checks whether the right side item is part of the sequence on the left side.
128
- Iterates over the sequence to apply automatic Robot type conversion where applicable.
129
- """
130
- for item in sequence:
131
- BuiltIn().log(f"Processing '{item}' from list")
132
- if self.equals(part, item):
133
- BuiltIn().log("Matched")
134
- return True
135
- BuiltIn().log("No match")
136
- return False
137
-
138
- def contains_exactly_the_items_from(self, sequence, sequence_right):
139
- """
140
- Checks whether the sequence on the right side contains all items of the left side and vice versa.
141
- Iterates over sequence on the left and matches each element with a single element on the right.
142
- """
143
- if isinstance(sequence_right, str):
144
- sequence_right = [sequence_right]
145
- sequence_right = [*sequence_right]
146
- for item in sequence:
147
- item_found_in_parts = False
148
- BuiltIn().log(f"Processing '{item}' from left side list")
149
- for i in range(len(sequence_right)):
150
- if self.equals(item, sequence_right[i]):
151
- sequence_right.pop(i)
152
- item_found_in_parts = True
153
- break
154
- if not item_found_in_parts:
155
- BuiltIn().log(f"Item '{item}' from left side is not found in the list on the right side")
156
- return False
157
- if len(sequence_right) > 0:
158
- BuiltIn().log(f"Not all items from right side list are present: {sequence_right}")
159
- return False
160
-
161
- return True
162
-
163
- def does_not_contain_item(self, sequence, part):
164
- """
165
- Checks whether the right side item is not part of the sequence on the left side.
166
- Iterates over the sequence to apply automatic Robot type conversion where applicable.
167
- """
168
- return not self.contains_item(sequence, part)
169
-
170
- # Add operator keywords that do not comply to Python's identifier syntax
171
- setattr(CheckOperator, "=", CheckOperator.equals)
172
- setattr(CheckOperator, "<", CheckOperator.is_less_than)
173
- setattr(CheckOperator, ">", CheckOperator.is_greater_than)
174
- setattr(CheckOperator, "≤", CheckOperator.is_less_than_or_equal_to)
175
- setattr(CheckOperator, "≥", CheckOperator.is_greater_than_or_equal_to)
176
- setattr(CheckOperator, "≠", CheckOperator.does_not_equal)
177
-
178
- class OperatorProxy:
179
- """
180
- Proxy class for mapping generic Robot comparison keywords to Python operators
181
- """
182
- def __init__(self, s_operator):
183
- self.__s_Operator = s_operator
184
-
185
- @staticmethod
186
- def __typeCastRobotStringValue(leadingValue, otherValue, name):
187
- """
188
- When Robot files are parsed, arguments are always passed as string even when comparing
189
- numbers or other objects. When a string argument is detected the other argument is
190
- deemed leading. This function converts the other argument to the leading value's type
191
- when possible. Otherwise the value is kept unchanged, inevitably leading to a mismatch
192
- in comparison.
193
-
194
- Precondition: otherValue is of types str and supports .casefold()
195
- returns type casted otherValue
196
- """
197
- CastedOther = otherValue # By default leave untouched
198
- converter = TypeConverter.converter_for(type(leadingValue))
199
- if converter:
200
- try:
201
- CastedOther = converter.convert(name, otherValue, explicit_type=False)
202
- except ValueError as err:
203
- BuiltIn().log(err, level='DEBUG')
204
- BuiltIn().log(f"Comparing as {converter.type_name} values")
205
-
206
- if isinstance(CastedOther, str):
207
- # By default compare as case insensitive Unicode. Note that it already was a string.
208
- BuiltIn().log(f"Interpreting {name} '{otherValue}' as string (case insensitive)")
209
- CastedOther = str(otherValue).casefold()
210
-
211
- return CastedOther
212
-
213
- def basicOperator(self, lvalue, rvalue):
214
- # Local argument copies for assignment
215
- lValue = lvalue
216
- rValue = rvalue
217
-
218
- if type(lvalue) is str:
219
- lValue = OperatorProxy.__typeCastRobotStringValue(rvalue, lvalue, "left operand")
220
-
221
- if type(rvalue) is str:
222
- rValue = OperatorProxy.__typeCastRobotStringValue(lvalue, rvalue, "right operand")
223
-
224
- if lValue is lvalue and rValue is rvalue:
225
- BuiltIn().log("Comparing values as is")
226
-
227
- return eval(f"lValue {self.__s_Operator} rValue")
@@ -1 +0,0 @@
1
- VERSION = '2.2.1'