From 1d8351addaad65d64b51fd606e02ae12029bec5f Mon Sep 17 00:00:00 2001 From: Nico Bollen Date: Mon, 21 Oct 2019 20:34:35 +0200 Subject: [PATCH 01/15] also allow byte like object to get item by index --- .../builtin/run_keyword_with_errors.robot | 2 +- .../variables/dict_variable_items.robot | 7 +++-- .../variables/list_variable_items.robot | 29 ++++++++++--------- .../variables/nested_item_access.robot | 8 ++--- atest/testdata/variables/scalar_lists.py | 1 + src/robot/utils/__init__.py | 3 +- src/robot/utils/robottypes.py | 4 +-- src/robot/utils/robottypes2.py | 4 +++ src/robot/utils/robottypes3.py | 4 +++ src/robot/variables/replacer.py | 20 ++++++------- 10 files changed, 47 insertions(+), 35 deletions(-) diff --git a/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot b/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot index 832d0d033de..8cb24c42e13 100644 --- a/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot +++ b/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot @@ -194,7 +194,7 @@ Expect Error When Access To List Variable Nonexisting Index Syntax 1 Expect Error When Access To List Variable Nonexisting Index Syntax 2 Run Keyword And Expect Error - ... List '\@{list}' has no item in index 2. + ... Iterable '\@{list}' has no item in index 2. ... Access To List Variable Nonexisting Index Syntax 2 Expect Error When Access To Dictionary Nonexisting Key Syntax 1 diff --git a/atest/testdata/variables/dict_variable_items.robot b/atest/testdata/variables/dict_variable_items.robot index 3f460ece1a6..47bf778da31 100644 --- a/atest/testdata/variables/dict_variable_items.robot +++ b/atest/testdata/variables/dict_variable_items.robot @@ -3,6 +3,7 @@ Library Collections Library XML *** Variables *** +${INT} ${15} &{DICT} A=1 B=2 C=3 ${1}=${2} 3=4 ${NONE}=${NONE} = ${SPACE}=${SPACE} &{SQUARES} [=open ]=close []=both [x[y]=mixed ${A} A @@ -99,9 +100,9 @@ Non-existing index variable Non-dict variable [Documentation] FAIL - ... Variable '\${INVALID}' is string, not list or dictionary, \ - ... and thus accessing item '${nonex}' from it is not possible. - Log ${INVALID}[${nonex}] + ... Variable '\${INT}' is integer, which is not iterable, \ + ... and thus accessing item '0' from it is not possible. + Log ${INT}[0] Sanity check @{items} = Create List diff --git a/atest/testdata/variables/list_variable_items.robot b/atest/testdata/variables/list_variable_items.robot index ee7d566da1a..a4af18cec8a 100644 --- a/atest/testdata/variables/list_variable_items.robot +++ b/atest/testdata/variables/list_variable_items.robot @@ -1,4 +1,5 @@ *** Variables *** +${INT} ${15} @{LIST} A B C D E F G H I J K @{NUMBERS} 1 2 3 &{MAP} first=0 last=-1 @@ -44,43 +45,43 @@ Slicing with variable ... ${LIST[1:]} Invalid index - [Documentation] FAIL List '\${LIST}' has no item in index 12. + [Documentation] FAIL Iterable '\${LIST}' has no item in index 12. Log ${LIST}[12] Invalid index using variable - [Documentation] FAIL List '\${LIST}' has no item in index 13. + [Documentation] FAIL Iterable '\${LIST}' has no item in index 13. Log ${LIST}[${ONE}${3}] Non-int index - [Documentation] FAIL List '\${LIST}' used with invalid index 'invalid'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'invalid'. Log ${LIST}[invalid] Non-int index using variable 1 - [Documentation] FAIL List '\${LIST}' used with invalid index 'xxx'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'xxx'. Log ${LIST}[${INVALID}] Non-int index using variable 2 - [Documentation] FAIL List '\${LIST}' used with invalid index '1.1'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1.1'. Log ${LIST}[${1.1}] Empty index - [Documentation] FAIL List '\${LIST}' used with invalid index ''. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index ''. Log ${LIST}[] Invalid slice - [Documentation] FAIL List '\${LIST}' used with invalid index '1:2:3:4'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:2:3:4'. Log ${LIST}[1:2:3:4] Non-int slice index 1 - [Documentation] FAIL List '\${LIST}' used with invalid index 'ooops:'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'ooops:'. Log ${LIST}[ooops:] Non-int slice index 2 - [Documentation] FAIL List '\${LIST}' used with invalid index '1:ooops'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:ooops'. Log ${LIST}[1:ooops] Non-int slice index 3 - [Documentation] FAIL List '\${LIST}' used with invalid index '1:2:ooops'. + [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:2:ooops'. Log ${LIST}[1:2:ooops] Non-existing variable @@ -93,19 +94,19 @@ Non-existing index variable Non-list variable [Documentation] FAIL - ... Variable '\${INVALID}' is string, not list or dictionary, \ + ... Variable '\${INT}' is integer, which is not iterable, \ ... and thus accessing item '0' from it is not possible. - Log ${INVALID}[0] + Log ${INT}[0] Old syntax with `@` still works but is deprecated [Documentation] `\${list}[1]` and `\@{list}[1]` work same way still. ... In the future latter is deprecated and changed. - ... FAIL List '\@{LIST}' has no item in index 99. + ... FAIL Iterable '\@{LIST}' has no item in index 99. Should Be Equal @{LIST}[0] A Should Be Equal @{LIST}[${-1}] K Log @{LIST}[99] Old syntax with `@` doesn't support new slicing syntax [Documentation] Slicing support should be added in RF 3.3 when `@{list}[index]` changes. - ... FAIL List '\@{LIST}' used with invalid index '1:'. + ... FAIL Iterable '\@{LIST}' used with invalid index '1:'. Log @{LIST}[1:] diff --git a/atest/testdata/variables/nested_item_access.robot b/atest/testdata/variables/nested_item_access.robot index f60ff81029a..7f84b70170d 100644 --- a/atest/testdata/variables/nested_item_access.robot +++ b/atest/testdata/variables/nested_item_access.robot @@ -28,7 +28,7 @@ Nested access with slicing ${LIST}[1:-1][-1][-2:1:-2][0][0] ${3} Non-existing nested list item - [Documentation] FAIL List '\${LIST}[1][2]' has no item in index 666. + [Documentation] FAIL Iterable '\${LIST}[1][2]' has no item in index 666. ${LIST}[1][2][666] whatever Non-existing nested dict item @@ -36,7 +36,7 @@ Non-existing nested dict item ${DICT}[x][y][nonex] whatever Invalid nested list access - [Documentation] FAIL List '\${LIST}[1][2]' used with invalid index 'inv'. + [Documentation] FAIL Iterable '\${LIST}[1][2]' used with invalid index 'inv'. ${LIST}[1][2][inv] whatever Invalid nested dict access @@ -45,9 +45,9 @@ Invalid nested dict access Nested access with non-list/dict [Documentation] FAIL - ... Variable '\${DICT}[key][key]' is string, not list or dictionary, \ + ... Variable '\${DICT}[\${1}][\${2}]' is integer, which is not iterable, \ ... and thus accessing item '0' from it is not possible. - ${DICT}[key][key][0] whatever + ${DICT}[${1}][${2}][0] whatever Escape nested ${LIST}[-1]\[0] third[0] diff --git a/atest/testdata/variables/scalar_lists.py b/atest/testdata/variables/scalar_lists.py index 48968311cd4..0227aa9a891 100644 --- a/atest/testdata/variables/scalar_lists.py +++ b/atest/testdata/variables/scalar_lists.py @@ -4,6 +4,7 @@ class _Extended: list = LIST string = 'not a list' + int = 15 def __getitem__(self, item): return LIST diff --git a/src/robot/utils/__init__.py b/src/robot/utils/__init__.py index 3daa4e617d5..ada1daa55a2 100644 --- a/src/robot/utils/__init__.py +++ b/src/robot/utils/__init__.py @@ -66,7 +66,8 @@ parse_time) from .robottypes import (FALSE_STRINGS, TRUE_STRINGS, is_bytes, is_dict_like, is_falsy, is_integer, is_list_like, is_number, - is_string, is_truthy, is_unicode, type_name, unicode) + is_string, is_truthy, is_unicode, type_name, unicode, + is_iterable) from .setter import setter, SetterAwareType from .sortable import Sortable from .text import (cut_long_message, format_assign_message, diff --git a/src/robot/utils/robottypes.py b/src/robot/utils/robottypes.py index 536dedd5a59..7148830e18b 100644 --- a/src/robot/utils/robottypes.py +++ b/src/robot/utils/robottypes.py @@ -18,12 +18,12 @@ if PY2: from .robottypes2 import (is_bytes, is_dict_like, is_integer, is_list_like, - is_number, is_string, is_unicode, type_name) + is_number, is_string, is_unicode, type_name, is_iterable) unicode = unicode else: from .robottypes3 import (is_bytes, is_dict_like, is_integer, is_list_like, - is_number, is_string, is_unicode, type_name) + is_number, is_string, is_unicode, type_name, is_iterable) unicode = str diff --git a/src/robot/utils/robottypes2.py b/src/robot/utils/robottypes2.py index 8c3482e90f3..509f8c50031 100644 --- a/src/robot/utils/robottypes2.py +++ b/src/robot/utils/robottypes2.py @@ -52,6 +52,10 @@ def is_list_like(item): if isinstance(item, (str, unicode, bytes, bytearray, UserString, String, file)): return False + return is_iterable(item) + + +def is_iterable(item): try: iter(item) except RERAISED_EXCEPTIONS: diff --git a/src/robot/utils/robottypes3.py b/src/robot/utils/robottypes3.py index d02155ab4ef..1fc635abb5a 100644 --- a/src/robot/utils/robottypes3.py +++ b/src/robot/utils/robottypes3.py @@ -43,6 +43,10 @@ def is_unicode(item): def is_list_like(item): if isinstance(item, (str, bytes, bytearray, UserString, IOBase)): return False + return is_iterable(item) + + +def is_iterable(item): try: iter(item) except RERAISED_EXCEPTIONS: diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index 5a5ec9d3cfd..69769b0f4de 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -15,8 +15,8 @@ from robot.errors import DataError, VariableError from robot.output import librarylogger as logger -from robot.utils import (escape, is_dict_like, is_list_like, type_name, - unescape, unic) +from robot.utils import (escape, is_dict_like, is_list_like, is_iterable, + type_name, unescape, unic) from .search import search_variable, VariableMatch @@ -139,31 +139,31 @@ def _get_variable_item(self, match, value): for item in match.items: if is_dict_like(value): value = self._get_dict_variable_item(name, value, item) - elif is_list_like(value): - value = self._get_list_variable_item(name, value, item) + elif is_iterable(value): + value = self._get_iterable_variable_item(name, value, item) else: raise VariableError( - "Variable '%s' is %s, not list or dictionary, and thus " + "Variable '%s' is %s, which is not iterable, and thus " "accessing item '%s' from it is not possible." % (name, type_name(value), item) ) name = '%s[%s]' % (name, item) return value - def _get_list_variable_item(self, name, variable, index): + def _get_iterable_variable_item(self, name, variable, index): index = self.replace_string(index) try: - index = self._parse_list_variable_index(index, name[0] == '$') + index = self._parse_iterable_variable_index(index, name[0] == '$') except ValueError: - raise VariableError("List '%s' used with invalid index '%s'." + raise VariableError("Iterable '%s' used with invalid index '%s'." % (name, index)) try: return variable[index] except IndexError: - raise VariableError("List '%s' has no item in index %d." + raise VariableError("Iterable '%s' has no item in index %d." % (name, index)) - def _parse_list_variable_index(self, index, support_slice=True): + def _parse_iterable_variable_index(self, index, support_slice=True): if ':' not in index: return int(index) if index.count(':') > 2 or not support_slice: From f47f4a582979601e29927d92216e916c2d190c27 Mon Sep 17 00:00:00 2001 From: Nico Bollen Date: Wed, 23 Oct 2019 21:41:31 +0200 Subject: [PATCH 02/15] review comment: update error message --- atest/testdata/variables/dict_variable_items.robot | 2 +- atest/testdata/variables/list_variable_items.robot | 2 +- atest/testdata/variables/nested_item_access.robot | 2 +- src/robot/variables/replacer.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/atest/testdata/variables/dict_variable_items.robot b/atest/testdata/variables/dict_variable_items.robot index 47bf778da31..137a2ff2ffd 100644 --- a/atest/testdata/variables/dict_variable_items.robot +++ b/atest/testdata/variables/dict_variable_items.robot @@ -100,7 +100,7 @@ Non-existing index variable Non-dict variable [Documentation] FAIL - ... Variable '\${INT}' is integer, which is not iterable, \ + ... Variable '\${INT}' is integer, not a dictionary or iterable, \ ... and thus accessing item '0' from it is not possible. Log ${INT}[0] diff --git a/atest/testdata/variables/list_variable_items.robot b/atest/testdata/variables/list_variable_items.robot index a4af18cec8a..7a1cccd4b6c 100644 --- a/atest/testdata/variables/list_variable_items.robot +++ b/atest/testdata/variables/list_variable_items.robot @@ -94,7 +94,7 @@ Non-existing index variable Non-list variable [Documentation] FAIL - ... Variable '\${INT}' is integer, which is not iterable, \ + ... Variable '\${INT}' is integer, not a dictionary or iterable, \ ... and thus accessing item '0' from it is not possible. Log ${INT}[0] diff --git a/atest/testdata/variables/nested_item_access.robot b/atest/testdata/variables/nested_item_access.robot index 7f84b70170d..788966d07d4 100644 --- a/atest/testdata/variables/nested_item_access.robot +++ b/atest/testdata/variables/nested_item_access.robot @@ -45,7 +45,7 @@ Invalid nested dict access Nested access with non-list/dict [Documentation] FAIL - ... Variable '\${DICT}[\${1}][\${2}]' is integer, which is not iterable, \ + ... Variable '\${DICT}[\${1}][\${2}]' is integer, not a dictionary or iterable, \ ... and thus accessing item '0' from it is not possible. ${DICT}[${1}][${2}][0] whatever diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index 69769b0f4de..7f6cf5df204 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -143,7 +143,7 @@ def _get_variable_item(self, match, value): value = self._get_iterable_variable_item(name, value, item) else: raise VariableError( - "Variable '%s' is %s, which is not iterable, and thus " + "Variable '%s' is %s, not a dictionary or iterable, and thus " "accessing item '%s' from it is not possible." % (name, type_name(value), item) ) From bd79d31c5fd7aadfdf0120199b6d53b13f6b07e1 Mon Sep 17 00:00:00 2001 From: Bollen Nico Date: Wed, 23 Oct 2019 21:46:12 +0200 Subject: [PATCH 03/15] Update scalar_lists.py Revert change --- atest/testdata/variables/scalar_lists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/atest/testdata/variables/scalar_lists.py b/atest/testdata/variables/scalar_lists.py index 0227aa9a891..48968311cd4 100644 --- a/atest/testdata/variables/scalar_lists.py +++ b/atest/testdata/variables/scalar_lists.py @@ -4,7 +4,6 @@ class _Extended: list = LIST string = 'not a list' - int = 15 def __getitem__(self, item): return LIST From 6f34e92f68f146823611983284b198fd4d2162f9 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Mon, 30 Dec 2019 17:37:05 +0100 Subject: [PATCH 04/15] Use presence or lack of __getitem__ method to determine whether a variable is subscriptable or not --- src/robot/utils/__init__.py | 5 +++-- src/robot/utils/robottypes.py | 12 ++++++++---- src/robot/utils/robottypes2.py | 10 +++++----- src/robot/utils/robottypes3.py | 10 +++++----- src/robot/variables/replacer.py | 33 ++++++++++++++++++--------------- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/robot/utils/__init__.py b/src/robot/utils/__init__.py index 59c790bf23d..c7d771033ae 100644 --- a/src/robot/utils/__init__.py +++ b/src/robot/utils/__init__.py @@ -67,8 +67,9 @@ parse_time) from .robottypes import (FALSE_STRINGS, Mapping, MutableMapping, TRUE_STRINGS, is_bytes, is_dict_like, is_falsy, is_integer, - is_list_like, is_number, is_string, is_truthy, - is_unicode, type_name, unicode, is_iterable) + is_list_like, is_number, is_sequence, + is_subscriptable, is_string, is_truthy, is_unicode, + type_name, unicode) from .setter import setter, SetterAwareType from .sortable import Sortable from .text import (cut_long_message, format_assign_message, diff --git a/src/robot/utils/robottypes.py b/src/robot/utils/robottypes.py index 899f5e67d04..9891e7fa795 100644 --- a/src/robot/utils/robottypes.py +++ b/src/robot/utils/robottypes.py @@ -17,14 +17,14 @@ if PY2: - from .robottypes2 import (is_bytes, is_dict_like, is_integer, is_iterable, - is_list_like, is_number, is_pathlike, is_string, + from .robottypes2 import (is_bytes, is_dict_like, is_integer, is_list_like, + is_number, is_pathlike, is_sequence, is_string, is_unicode, type_name, Mapping, MutableMapping) unicode = unicode else: - from .robottypes3 import (is_bytes, is_dict_like, is_integer, is_iterable, - is_list_like, is_number, is_pathlike, is_string, + from .robottypes3 import (is_bytes, is_dict_like, is_integer, is_list_like, + is_number, is_pathlike, is_sequence, is_string, is_unicode, type_name, Mapping, MutableMapping) unicode = str @@ -57,3 +57,7 @@ def is_truthy(item): def is_falsy(item): """Opposite of :func:`is_truthy`.""" return not is_truthy(item) + + +def is_subscriptable(item): + return hasattr(item, '__getitem__') diff --git a/src/robot/utils/robottypes2.py b/src/robot/utils/robottypes2.py index 841706de9f7..903058d34f0 100644 --- a/src/robot/utils/robottypes2.py +++ b/src/robot/utils/robottypes2.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import Mapping, MutableMapping +from collections import Mapping, MutableMapping, Sequence from UserDict import UserDict from UserString import UserString from types import ClassType, NoneType @@ -56,10 +56,6 @@ def is_list_like(item): if isinstance(item, (str, unicode, bytes, bytearray, UserString, String, file)): return False - return is_iterable(item) - - -def is_iterable(item): try: iter(item) except RERAISED_EXCEPTIONS: @@ -70,6 +66,10 @@ def is_iterable(item): return True +def is_sequence(item): + return isinstance(item, Sequence) + + def is_dict_like(item): return isinstance(item, (Mapping, UserDict)) diff --git a/src/robot/utils/robottypes3.py b/src/robot/utils/robottypes3.py index f9bb6b5f8c4..431eba4f719 100644 --- a/src/robot/utils/robottypes3.py +++ b/src/robot/utils/robottypes3.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Mapping, MutableMapping +from collections.abc import Mapping, MutableMapping, Sequence from collections import UserString from io import IOBase @@ -53,10 +53,6 @@ def is_pathlike(item): def is_list_like(item): if isinstance(item, (str, bytes, bytearray, UserString, IOBase)): return False - return is_iterable(item) - - -def is_iterable(item): try: iter(item) except RERAISED_EXCEPTIONS: @@ -67,6 +63,10 @@ def is_iterable(item): return True +def is_sequence(item): + return isinstance(item, Sequence) + + def is_dict_like(item): return isinstance(item, Mapping) diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index 7f6cf5df204..d3976e5431d 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -15,7 +15,7 @@ from robot.errors import DataError, VariableError from robot.output import librarylogger as logger -from robot.utils import (escape, is_dict_like, is_list_like, is_iterable, +from robot.utils import (escape, is_list_like, is_sequence, is_subscriptable, type_name, unescape, unic) from .search import search_variable, VariableMatch @@ -137,46 +137,49 @@ def _get_variable_item(self, match, value): logger.warn("Accessing variable items using '%s' syntax " "is deprecated. Use '$%s' instead." % (var, var[1:])) for item in match.items: - if is_dict_like(value): - value = self._get_dict_variable_item(name, value, item) - elif is_iterable(value): - value = self._get_iterable_variable_item(name, value, item) + if is_sequence(value): + value = self._get_sequence_variable_item(name, value, item) + elif is_subscriptable(value): + value = self._get_subscriptable_variable_item(name, value, item) else: raise VariableError( - "Variable '%s' is %s, not a dictionary or iterable, and thus " + "Variable '%s' is %s, not subscriptable, and thus " "accessing item '%s' from it is not possible." % (name, type_name(value), item) ) name = '%s[%s]' % (name, item) return value - def _get_iterable_variable_item(self, name, variable, index): + def _get_sequence_variable_item(self, name, variable, index): index = self.replace_string(index) try: - index = self._parse_iterable_variable_index(index, name[0] == '$') + index = self._parse_sequence_variable_index(index, name[0] == '$') except ValueError: - raise VariableError("Iterable '%s' used with invalid index '%s'." + raise VariableError("Sequence '%s' used with invalid index '%s'." % (name, index)) try: return variable[index] except IndexError: - raise VariableError("Iterable '%s' has no item in index %d." + raise VariableError("Sequence '%s' has no item in index %d." % (name, index)) - def _parse_iterable_variable_index(self, index, support_slice=True): + def _parse_sequence_variable_index(self, index, support_slice=True): if ':' not in index: return int(index) if index.count(':') > 2 or not support_slice: raise ValueError return slice(*[int(i) if i else None for i in index.split(':')]) - def _get_dict_variable_item(self, name, variable, key): - key = self.replace_scalar(key) + def _get_subscriptable_variable_item(self, name, variable, key): + if not isinstance(key, int): + key = self.replace_scalar(key) try: return variable[key] except KeyError: - raise VariableError("Dictionary '%s' has no key '%s'." + raise VariableError("Subscriptable '%s' has no key '%s'." % (name, key)) except TypeError as err: - raise VariableError("Dictionary '%s' used with invalid key: %s" + if not isinstance(key, int): + return self._get_subscriptable_variable_item(name, variable, int(key)) + raise VariableError("Subscriptable '%s' used with invalid key: %s" % (name, err)) From 13b64ee5b118a68c59303da438709f36747d4669 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Mon, 30 Dec 2019 17:39:21 +0100 Subject: [PATCH 05/15] Change Iterable to Sequence and 'dictionary or iterable' to 'subscriptable' --- .../variables/list_variable_items.robot | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/atest/testdata/variables/list_variable_items.robot b/atest/testdata/variables/list_variable_items.robot index 7a1cccd4b6c..181f1020821 100644 --- a/atest/testdata/variables/list_variable_items.robot +++ b/atest/testdata/variables/list_variable_items.robot @@ -45,43 +45,43 @@ Slicing with variable ... ${LIST[1:]} Invalid index - [Documentation] FAIL Iterable '\${LIST}' has no item in index 12. + [Documentation] FAIL Sequence '\${LIST}' has no item in index 12. Log ${LIST}[12] Invalid index using variable - [Documentation] FAIL Iterable '\${LIST}' has no item in index 13. + [Documentation] FAIL Sequence '\${LIST}' has no item in index 13. Log ${LIST}[${ONE}${3}] Non-int index - [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'invalid'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'invalid'. Log ${LIST}[invalid] Non-int index using variable 1 - [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'xxx'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'xxx'. Log ${LIST}[${INVALID}] Non-int index using variable 2 - [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1.1'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1.1'. Log ${LIST}[${1.1}] Empty index - [Documentation] FAIL Iterable '\${LIST}' used with invalid index ''. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index ''. Log ${LIST}[] Invalid slice - [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:2:3:4'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:2:3:4'. Log ${LIST}[1:2:3:4] Non-int slice index 1 - [Documentation] FAIL Iterable '\${LIST}' used with invalid index 'ooops:'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'ooops:'. Log ${LIST}[ooops:] Non-int slice index 2 - [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:ooops'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:ooops'. Log ${LIST}[1:ooops] Non-int slice index 3 - [Documentation] FAIL Iterable '\${LIST}' used with invalid index '1:2:ooops'. + [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:2:ooops'. Log ${LIST}[1:2:ooops] Non-existing variable @@ -94,19 +94,19 @@ Non-existing index variable Non-list variable [Documentation] FAIL - ... Variable '\${INT}' is integer, not a dictionary or iterable, \ + ... Variable '\${INT}' is integer, not subscriptable, \ ... and thus accessing item '0' from it is not possible. Log ${INT}[0] Old syntax with `@` still works but is deprecated [Documentation] `\${list}[1]` and `\@{list}[1]` work same way still. ... In the future latter is deprecated and changed. - ... FAIL Iterable '\@{LIST}' has no item in index 99. + ... FAIL Sequence '\@{LIST}' has no item in index 99. Should Be Equal @{LIST}[0] A Should Be Equal @{LIST}[${-1}] K Log @{LIST}[99] Old syntax with `@` doesn't support new slicing syntax [Documentation] Slicing support should be added in RF 3.3 when `@{list}[index]` changes. - ... FAIL Iterable '\@{LIST}' used with invalid index '1:'. + ... FAIL Sequence '\@{LIST}' used with invalid index '1:'. Log @{LIST}[1:] From 24072b392414267cd71e9f3b1d127b9aae741dce Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Mon, 30 Dec 2019 23:46:28 +0100 Subject: [PATCH 06/15] Fix no-member: `self.item` to `self.items` in `__unicode__` --- src/robot/variables/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robot/variables/search.py b/src/robot/variables/search.py index bb02fca47a4..953a4a9a76a 100644 --- a/src/robot/variables/search.py +++ b/src/robot/variables/search.py @@ -83,7 +83,7 @@ def __nonzero__(self): def __unicode__(self): if not self: return '' - items = ''.join('[%s]' % i for i in self.item) if self.items else '' + items = ''.join('[%s]' % i for i in self.items) if self.items else '' return '%s{%s}%s' % (self.identifier, self.base, items) From 3a4570b1e43025429ed4c42de8d4a28ed3e97b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20Kl=C3=A4rck?= Date: Tue, 31 Dec 2019 01:11:00 +0200 Subject: [PATCH 07/15] Enhance lexer tests --- utest/parsing/test_lexer.py | 105 +++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/utest/parsing/test_lexer.py b/utest/parsing/test_lexer.py index 5e3e9d31747..5b6d05a4e4d 100644 --- a/utest/parsing/test_lexer.py +++ b/utest/parsing/test_lexer.py @@ -10,17 +10,107 @@ T = Token - -def assert_tokens(tokens, expected): - assert_equal(len(tokens), len(expected)) +def assert_tokens(source, expected, get_tokens=get_tokens, data_only=False): + tokens = list(get_tokens(source, data_only)) + assert_equal(len(tokens), len(expected), + 'Expected %d tokens:\n%s\n\nGot %d tokens:\n%s' + % (len(expected), expected, len(tokens), tokens), + values=False) for act, exp in zip(tokens, expected): exp = Token(*exp) assert_equal(act.type, exp.type) - assert_equal(act.value, exp.value) + assert_equal(act.value, exp.value, formatter=repr) assert_equal(act.lineno, exp.lineno) assert_equal(act.columnno, exp.columnno) +class TestName(unittest.TestCase): + + def test_name_on_own_row(self): + self._verify('My Name', + [(T.NAME, 'My Name', 2, 1), (T.EOS, '', 2, 8)]) + self._verify('My Name ', + [(T.NAME, 'My Name', 2, 1), (T.EOL, ' ', 2, 8), (T.EOS, '', 2, 12)]) + self._verify('My Name\n Keyword', + [(T.NAME, 'My Name', 2, 1), (T.EOL, '\n', 2, 8), (T.EOS, '', 2, 9), + (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOS, '', 3, 12)]) + self._verify('My Name \n Keyword', + [(T.NAME, 'My Name', 2, 1), (T.EOL, ' \n', 2, 8), (T.EOS, '', 2, 11), + (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOS, '', 3, 12)]) + + def test_name_and_keyword_on_same_row(self): + self._verify('Name Keyword', + [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), + (T.KEYWORD, 'Keyword', 2, 9), (T.EOS, '', 2, 16)]) + self._verify('N K A', + [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), + (T.KEYWORD, 'K', 2, 4), (T.SEPARATOR, ' ', 2, 5), + (T.ARGUMENT, 'A', 2, 7), (T.EOS, '', 2, 8)]) + self._verify('N ${v}= K', + [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), + (T.ASSIGN, '${v}=', 2, 4), (T.SEPARATOR, ' ', 2, 9), + (T.KEYWORD, 'K', 2, 11), (T.EOS, '', 2, 12)]) + + def test_name_and_setting_on_same_row(self): + self._verify('Name [Documentation] The doc.', + [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), + (T.DOCUMENTATION, '[Documentation]', 2, 9), (T.SEPARATOR, ' ', 2, 24), + (T.ARGUMENT, 'The doc.', 2, 28), (T.EOS, '', 2, 36)]) + + def _verify(self, data, tokens): + assert_tokens('*** Test Cases ***\n' + data, + [(T.TESTCASE_HEADER, '*** Test Cases ***', 1, 1), + (T.EOL, '\n', 1, 19), + (T.EOS, '', 1, 20)] + tokens) + assert_tokens('*** Keywords ***\n' + data, + [(T.KEYWORD_HEADER, '*** Keywords ***', 1, 1), + (T.EOL, '\n', 1, 17), + (T.EOS, '', 1, 18)] + tokens, + get_tokens=get_resource_tokens) + + +class TestNameWithPipes(unittest.TestCase): + + def test_name_on_own_row(self): + self._verify('| My Name', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.EOS, '', 2, 10)]) + self._verify('| My Name |', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.SEPARATOR, ' |', 2, 10), (T.EOS, '', 2, 12)]) + self._verify('| My Name | ', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.SEPARATOR, ' |', 2, 10), (T.EOL, ' ', 2, 12), (T.EOS, '', 2, 13)]) + + def test_name_and_keyword_on_same_row(self): + self._verify('| Name | Keyword', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), + (T.KEYWORD, 'Keyword', 2, 10), (T.EOS, '', 2, 17)]) + self._verify('| N | K | A |', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 3), (T.SEPARATOR, ' | ', 2, 4), (T.EOS, '', 2, 7), + (T.KEYWORD, 'K', 2, 7), (T.SEPARATOR, ' | ', 2, 8), + (T.ARGUMENT, 'A', 2, 11), (T.SEPARATOR, ' |', 2, 12), (T.EOS, '', 2, 14)]) + self._verify('| N | ${v} = | K ', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 6), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 12), + (T.ASSIGN, '${v} =', 2, 12), (T.SEPARATOR, ' | ', 2, 18), + (T.KEYWORD, 'K', 2, 27), (T.EOL, ' ', 2, 28), (T.EOS, '', 2, 32)]) + + def test_name_and_setting_on_same_row(self): + self._verify('| Name | [Documentation] | The doc.', + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), + (T.DOCUMENTATION, '[Documentation]', 2, 10), (T.SEPARATOR, ' | ', 2, 25), + (T.ARGUMENT, 'The doc.', 2, 28), (T.EOS, '', 2, 36)]) + + def _verify(self, data, tokens): + assert_tokens('*** Test Cases ***\n' + data, + [(T.TESTCASE_HEADER, '*** Test Cases ***', 1, 1), + (T.EOL, '\n', 1, 19), + (T.EOS, '', 1, 20)] + tokens) + assert_tokens('*** Keywords ***\n' + data, + [(T.KEYWORD_HEADER, '*** Keywords ***', 1, 1), + (T.EOL, '\n', 1, 17), + (T.EOS, '', 1, 18)] + tokens, + get_tokens=get_resource_tokens) + + + class SourceFormatsTestBase(unittest.TestCase): data = None tokens = None @@ -104,9 +194,8 @@ def test_string(self): self._verify(self.data, data_only=True) def _verify(self, source, data_only=False): - tokens = get_tokens(source, data_only) expected = self.data_tokens if data_only else self.tokens - assert_tokens(list(tokens), expected) + assert_tokens(source, expected, data_only=data_only) class TestGetResourceTokensSourceFormats(SourceFormatsTestBase): @@ -172,9 +261,9 @@ def test_string(self): self._verify(self.data, data_only=True) def _verify(self, source, data_only=False): - tokens = get_resource_tokens(source, data_only) expected = self.data_tokens if data_only else self.tokens - assert_tokens(list(tokens), expected) + assert_tokens(source, expected, get_tokens=get_resource_tokens, + data_only=data_only) if __name__ == '__main__': From 72b22de5a99e6bef59dfa8f36baef4c9d7a59dc7 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 00:07:17 +0100 Subject: [PATCH 08/15] Update tests to changes in #3350 --- .../builtin/run_keyword_with_errors.robot | 4 ++-- atest/testdata/variables/dict_variable_items.robot | 14 +++++++------- atest/testdata/variables/list_variable_items.robot | 2 +- atest/testdata/variables/nested_item_access.robot | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot b/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot index 4984ce13026..c3d71262946 100644 --- a/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot +++ b/atest/testdata/standard_libraries/builtin/run_keyword_with_errors.robot @@ -194,7 +194,7 @@ Expect Error When Access To List Variable Nonexisting Index Syntax 1 Expect Error When Access To List Variable Nonexisting Index Syntax 2 Run Keyword And Expect Error - ... Iterable '\${list}' has no item in index 2. + ... Sequence '\${list}' has no item in index 2. ... Access To List Variable Nonexisting Index Syntax 2 Expect Error When Access To Dictionary Nonexisting Key Syntax 1 @@ -204,7 +204,7 @@ Expect Error When Access To Dictionary Nonexisting Key Syntax 1 Expect Error When Access To Dictionary Nonexisting Key Syntax 2 Run Keyword And Expect Error - ... Dictionary '\${dict}' has no key 'c'. + ... Subscriptable '\${dict}' has no key 'c'. ... Access To Dictionary Variable Nonexisting Key Syntax 2 Expect Error With Explicit GLOB diff --git a/atest/testdata/variables/dict_variable_items.robot b/atest/testdata/variables/dict_variable_items.robot index 137a2ff2ffd..7d783361234 100644 --- a/atest/testdata/variables/dict_variable_items.robot +++ b/atest/testdata/variables/dict_variable_items.robot @@ -71,23 +71,23 @@ List-like values are not manipulated Should Be Equal ${dict}[tuple] ${tuple} Integer key cannot be accessed as string - [Documentation] FAIL Dictionary '\${DICT}' has no key '1'. + [Documentation] FAIL Subscriptable '\${DICT}' has no key '1'. Log ${DICT}[1] String key cannot be accessed as integer - [Documentation] FAIL Dictionary '\${DICT}' has no key '3'. + [Documentation] FAIL Subscriptable '\${DICT}' has no key '3'. Log ${DICT}[${3}] Invalid key - [Documentation] FAIL Dictionary '\${DICT}' has no key 'nonex'. + [Documentation] FAIL Subscriptable '\${DICT}' has no key 'nonex'. Log ${DICT}[nonex] Invalid key using variable - [Documentation] FAIL Dictionary '\${DICT}' has no key 'xxx'. + [Documentation] FAIL Subscriptable '\${DICT}' has no key 'xxx'. Log ${DICT}[${INVALID}] Non-hashable key - [Documentation] FAIL STARTS: Dictionary '\${DICT}' used with invalid key: + [Documentation] FAIL STARTS: Subscriptable '\${DICT}' used with invalid key: Log ${DICT}[@{DICT}] Non-existing variable @@ -100,7 +100,7 @@ Non-existing index variable Non-dict variable [Documentation] FAIL - ... Variable '\${INT}' is integer, not a dictionary or iterable, \ + ... Variable '\${INT}' is integer, which is not subscriptable, \ ... and thus accessing item '0' from it is not possible. Log ${INT}[0] @@ -113,7 +113,7 @@ Sanity check Should Be Equal ${items} A: 1, B: 2, C: 3, 1: 2, 3: 4, None: None, : , ${SPACE}: ${SPACE} Old syntax with `&` still works but is deprecated - [Documentation] FAIL Dictionary '\&{DICT}' has no key 'nonex'. + [Documentation] FAIL Subscriptable '\&{DICT}' has no key 'nonex'. Should Be Equal &{DICT}[A] 1 Should Be Equal &{DICT}[${1}] ${2} Log &{DICT}[nonex] diff --git a/atest/testdata/variables/list_variable_items.robot b/atest/testdata/variables/list_variable_items.robot index 181f1020821..ffc8ab9e6ac 100644 --- a/atest/testdata/variables/list_variable_items.robot +++ b/atest/testdata/variables/list_variable_items.robot @@ -94,7 +94,7 @@ Non-existing index variable Non-list variable [Documentation] FAIL - ... Variable '\${INT}' is integer, not subscriptable, \ + ... Variable '\${INT}' is integer, which is not subscriptable, \ ... and thus accessing item '0' from it is not possible. Log ${INT}[0] diff --git a/atest/testdata/variables/nested_item_access.robot b/atest/testdata/variables/nested_item_access.robot index 788966d07d4..70dda95d297 100644 --- a/atest/testdata/variables/nested_item_access.robot +++ b/atest/testdata/variables/nested_item_access.robot @@ -28,24 +28,24 @@ Nested access with slicing ${LIST}[1:-1][-1][-2:1:-2][0][0] ${3} Non-existing nested list item - [Documentation] FAIL Iterable '\${LIST}[1][2]' has no item in index 666. + [Documentation] FAIL Sequence '\${LIST}[1][2]' has no item in index 666. ${LIST}[1][2][666] whatever Non-existing nested dict item - [Documentation] FAIL Dictionary '\${DICT}[x][y]' has no key 'nonex'. + [Documentation] FAIL Subscriptable '\${DICT}[x][y]' has no key 'nonex'. ${DICT}[x][y][nonex] whatever Invalid nested list access - [Documentation] FAIL Iterable '\${LIST}[1][2]' used with invalid index 'inv'. + [Documentation] FAIL Sequence '\${LIST}[1][2]' used with invalid index 'inv'. ${LIST}[1][2][inv] whatever Invalid nested dict access - [Documentation] FAIL STARTS: Dictionary '\${DICT}[key]' used with invalid key: + [Documentation] FAIL STARTS: Subscriptable '\${DICT}[key]' used with invalid key: ${DICT}[key][${DICT}] whatever Nested access with non-list/dict [Documentation] FAIL - ... Variable '\${DICT}[\${1}][\${2}]' is integer, not a dictionary or iterable, \ + ... Variable '\${DICT}[\${1}][\${2}]' is integer, which is not subscriptable, \ ... and thus accessing item '0' from it is not possible. ${DICT}[${1}][${2}][0] whatever From 70728d3a6d1b37caa9f14cb76f50fcbbf62d0909 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 00:28:56 +0100 Subject: [PATCH 09/15] Catch all exceptions when the attempt to cast key to int fails --- src/robot/variables/replacer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index d3976e5431d..72bee48d037 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -143,8 +143,8 @@ def _get_variable_item(self, match, value): value = self._get_subscriptable_variable_item(name, value, item) else: raise VariableError( - "Variable '%s' is %s, not subscriptable, and thus " - "accessing item '%s' from it is not possible." + "Variable '%s' is %s, which is not subscriptable, and " + "thus accessing item '%s' from it is not possible." % (name, type_name(value), item) ) name = '%s[%s]' % (name, item) @@ -180,6 +180,12 @@ def _get_subscriptable_variable_item(self, name, variable, key): % (name, key)) except TypeError as err: if not isinstance(key, int): - return self._get_subscriptable_variable_item(name, variable, int(key)) + try: + key = int(key) + except: + pass + else: + return self._get_subscriptable_variable_item( + name, variable, key) raise VariableError("Subscriptable '%s' used with invalid key: %s" % (name, err)) From f75c073da212ea49454506145717151cc37ff430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20Kl=C3=A4rck?= Date: Tue, 31 Dec 2019 01:43:42 +0200 Subject: [PATCH 10/15] Lexer: Yield EOL also at the end of input --- src/robot/parsing/lexer/readers.py | 11 +++++++---- src/robot/parsing/lexer/tokens.py | 11 +++++++++++ utest/parsing/test_lexer.py | 26 +++++++++++++------------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/robot/parsing/lexer/readers.py b/src/robot/parsing/lexer/readers.py index 1df7415dca4..5b3fcb1b3e5 100644 --- a/src/robot/parsing/lexer/readers.py +++ b/src/robot/parsing/lexer/readers.py @@ -21,7 +21,7 @@ from .context import TestCaseFileContext, ResourceFileContext from .lexers import FileLexer from .splitter import Splitter -from .tokens import EOS, Token +from .tokens import EOL, EOS, Token def get_tokens(source, data_only=False): @@ -142,12 +142,15 @@ def _split_trailing_comment_and_empty_lines(self, statement): def _split_to_lines(self, statement): current = [] - for tok in statement: - current.append(tok) - if tok.type == tok.EOL: + eol = Token.EOL + for token in statement: + current.append(token) + if token.type == eol: yield current current = [] if current: + if current[-1].type != eol: + current.append(EOL.from_token(current[-1])) yield current diff --git a/src/robot/parsing/lexer/tokens.py b/src/robot/parsing/lexer/tokens.py index b2b48a9046a..c308a598a71 100644 --- a/src/robot/parsing/lexer/tokens.py +++ b/src/robot/parsing/lexer/tokens.py @@ -118,6 +118,17 @@ def __repr__(self): self.lineno, self.columnno) +class EOL(Token): + __slots__ = [] + + def __init__(self, value='', lineno=-1, columnno=-1): + Token.__init__(self, Token.EOL, value, lineno, columnno) + + @classmethod + def from_token(cls, token): + return EOL('', token.lineno, token.columnno + len(token.value)) + + class EOS(Token): __slots__ = [] diff --git a/utest/parsing/test_lexer.py b/utest/parsing/test_lexer.py index 5b6d05a4e4d..7e269f84785 100644 --- a/utest/parsing/test_lexer.py +++ b/utest/parsing/test_lexer.py @@ -28,34 +28,34 @@ class TestName(unittest.TestCase): def test_name_on_own_row(self): self._verify('My Name', - [(T.NAME, 'My Name', 2, 1), (T.EOS, '', 2, 8)]) + [(T.NAME, 'My Name', 2, 1), (T.EOL, '', 2, 8), (T.EOS, '', 2, 8)]) self._verify('My Name ', [(T.NAME, 'My Name', 2, 1), (T.EOL, ' ', 2, 8), (T.EOS, '', 2, 12)]) self._verify('My Name\n Keyword', [(T.NAME, 'My Name', 2, 1), (T.EOL, '\n', 2, 8), (T.EOS, '', 2, 9), - (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOS, '', 3, 12)]) + (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOL, '', 3, 12), (T.EOS, '', 3, 12)]) self._verify('My Name \n Keyword', [(T.NAME, 'My Name', 2, 1), (T.EOL, ' \n', 2, 8), (T.EOS, '', 2, 11), - (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOS, '', 3, 12)]) + (T.SEPARATOR, ' ', 3, 1), (T.KEYWORD, 'Keyword', 3, 5), (T.EOL, '', 3, 12), (T.EOS, '', 3, 12)]) def test_name_and_keyword_on_same_row(self): self._verify('Name Keyword', [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), - (T.KEYWORD, 'Keyword', 2, 9), (T.EOS, '', 2, 16)]) + (T.KEYWORD, 'Keyword', 2, 9), (T.EOL, '', 2, 16), (T.EOS, '', 2, 16)]) self._verify('N K A', [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), (T.KEYWORD, 'K', 2, 4), (T.SEPARATOR, ' ', 2, 5), - (T.ARGUMENT, 'A', 2, 7), (T.EOS, '', 2, 8)]) + (T.ARGUMENT, 'A', 2, 7), (T.EOL, '', 2, 8), (T.EOS, '', 2, 8)]) self._verify('N ${v}= K', [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), (T.ASSIGN, '${v}=', 2, 4), (T.SEPARATOR, ' ', 2, 9), - (T.KEYWORD, 'K', 2, 11), (T.EOS, '', 2, 12)]) + (T.KEYWORD, 'K', 2, 11), (T.EOL, '', 2, 12), (T.EOS, '', 2, 12)]) def test_name_and_setting_on_same_row(self): self._verify('Name [Documentation] The doc.', [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), (T.DOCUMENTATION, '[Documentation]', 2, 9), (T.SEPARATOR, ' ', 2, 24), - (T.ARGUMENT, 'The doc.', 2, 28), (T.EOS, '', 2, 36)]) + (T.ARGUMENT, 'The doc.', 2, 28), (T.EOL, '', 2, 36), (T.EOS, '', 2, 36)]) def _verify(self, data, tokens): assert_tokens('*** Test Cases ***\n' + data, @@ -73,20 +73,20 @@ class TestNameWithPipes(unittest.TestCase): def test_name_on_own_row(self): self._verify('| My Name', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.EOS, '', 2, 10)]) + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.EOL, '', 2, 10), (T.EOS, '', 2, 10)]) self._verify('| My Name |', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.SEPARATOR, ' |', 2, 10), (T.EOS, '', 2, 12)]) + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.SEPARATOR, ' |', 2, 10), (T.EOL, '', 2, 12), (T.EOS, '', 2, 12)]) self._verify('| My Name | ', [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'My Name', 2, 3), (T.SEPARATOR, ' |', 2, 10), (T.EOL, ' ', 2, 12), (T.EOS, '', 2, 13)]) def test_name_and_keyword_on_same_row(self): self._verify('| Name | Keyword', [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), - (T.KEYWORD, 'Keyword', 2, 10), (T.EOS, '', 2, 17)]) - self._verify('| N | K | A |', + (T.KEYWORD, 'Keyword', 2, 10), (T.EOL, '', 2, 17), (T.EOS, '', 2, 17)]) + self._verify('| N | K | A |\n', [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 3), (T.SEPARATOR, ' | ', 2, 4), (T.EOS, '', 2, 7), (T.KEYWORD, 'K', 2, 7), (T.SEPARATOR, ' | ', 2, 8), - (T.ARGUMENT, 'A', 2, 11), (T.SEPARATOR, ' |', 2, 12), (T.EOS, '', 2, 14)]) + (T.ARGUMENT, 'A', 2, 11), (T.SEPARATOR, ' |', 2, 12), (T.EOL, '\n', 2, 14), (T.EOS, '', 2, 15)]) self._verify('| N | ${v} = | K ', [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 6), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 12), (T.ASSIGN, '${v} =', 2, 12), (T.SEPARATOR, ' | ', 2, 18), @@ -96,7 +96,7 @@ def test_name_and_setting_on_same_row(self): self._verify('| Name | [Documentation] | The doc.', [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), (T.DOCUMENTATION, '[Documentation]', 2, 10), (T.SEPARATOR, ' | ', 2, 25), - (T.ARGUMENT, 'The doc.', 2, 28), (T.EOS, '', 2, 36)]) + (T.ARGUMENT, 'The doc.', 2, 28), (T.EOL, '', 2, 36), (T.EOS, '', 2, 36)]) def _verify(self, data, tokens): assert_tokens('*** Test Cases ***\n' + data, From d861fb9764685027ae972bf4a321e949a381b4db Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 12:57:25 +0100 Subject: [PATCH 11/15] Extend error messages in case of invalid subscript and update atests accordingly --- .../robot/variables/list_variable_items.robot | 2 +- .../robot/variables/nested_item_access.robot | 2 +- .../variables/dict_variable_items.robot | 5 +- .../variables/list_variable_items.robot | 48 ++++++++++++++----- .../variables/nested_item_access.robot | 17 +++++-- src/robot/variables/replacer.py | 11 +++-- 6 files changed, 61 insertions(+), 24 deletions(-) diff --git a/atest/robot/variables/list_variable_items.robot b/atest/robot/variables/list_variable_items.robot index 715fb77695e..ccc90c4468e 100644 --- a/atest/robot/variables/list_variable_items.robot +++ b/atest/robot/variables/list_variable_items.robot @@ -48,7 +48,7 @@ Non-existing variable Non-existing index variable Check Test Case ${TESTNAME} -Non-list variable +Non-subscriptable variable Check Test Case ${TESTNAME} Old syntax with `@` still works but is deprecated diff --git a/atest/robot/variables/nested_item_access.robot b/atest/robot/variables/nested_item_access.robot index ee991e97409..6c20ef62714 100644 --- a/atest/robot/variables/nested_item_access.robot +++ b/atest/robot/variables/nested_item_access.robot @@ -27,7 +27,7 @@ Invalid nested list access Invalid nested dict access Check Test Case ${TESTNAME} -Nested access with non-list/dict +Nested access with non-subscriptable Check Test Case ${TESTNAME} Escape nested diff --git a/atest/testdata/variables/dict_variable_items.robot b/atest/testdata/variables/dict_variable_items.robot index 7d783361234..ecc5064108c 100644 --- a/atest/testdata/variables/dict_variable_items.robot +++ b/atest/testdata/variables/dict_variable_items.robot @@ -100,8 +100,9 @@ Non-existing index variable Non-dict variable [Documentation] FAIL - ... Variable '\${INT}' is integer, which is not subscriptable, \ - ... and thus accessing item '0' from it is not possible. + ... Variable '\${INT}' is integer, which is not subscriptable, and thus \ + ... accessing item '0' from it is not possible. To use '[0]' as a \ + ... literal value, it needs to be escaped like '\\[0]'. Log ${INT}[0] Sanity check diff --git a/atest/testdata/variables/list_variable_items.robot b/atest/testdata/variables/list_variable_items.robot index ffc8ab9e6ac..d1ccb50a910 100644 --- a/atest/testdata/variables/list_variable_items.robot +++ b/atest/testdata/variables/list_variable_items.robot @@ -53,35 +53,56 @@ Invalid index using variable Log ${LIST}[${ONE}${3}] Non-int index - [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'invalid'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index 'invalid'. To use \ + ... '[invalid]' as a literal value, it needs to be escaped like \ + ... '\\[invalid]'. Log ${LIST}[invalid] Non-int index using variable 1 - [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'xxx'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index 'xxx'. To use \ + ... '[xxx]' as a literal value, it needs to be escaped like '\\[xxx]'. Log ${LIST}[${INVALID}] Non-int index using variable 2 - [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1.1'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index '1.1'. To use \ + ... '[1.1]' as a literal value, it needs to be escaped like '\\[1.1]'. Log ${LIST}[${1.1}] Empty index - [Documentation] FAIL Sequence '\${LIST}' used with invalid index ''. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index ''. To use \ + ... '[]' as a literal value, it needs to be escaped like '\\[]'. Log ${LIST}[] Invalid slice - [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:2:3:4'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index '1:2:3:4'. To use \ + ... '[1:2:3:4]' as a literal value, it needs to be escaped like \ + ... '\\[1:2:3:4]'. Log ${LIST}[1:2:3:4] Non-int slice index 1 - [Documentation] FAIL Sequence '\${LIST}' used with invalid index 'ooops:'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index 'ooops:'. To use \ + ... '[ooops:]' as a literal value, it needs to be escaped like \ + ... '\\[ooops:]'. Log ${LIST}[ooops:] Non-int slice index 2 - [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:ooops'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index '1:ooops'. To use \ + ... '[1:ooops]' as a literal value, it needs to be escaped like \ + ... '\\[1:ooops]'. Log ${LIST}[1:ooops] Non-int slice index 3 - [Documentation] FAIL Sequence '\${LIST}' used with invalid index '1:2:ooops'. + [Documentation] FAIL \ + ... Sequence '\${LIST}' used with invalid index '1:2:ooops'. To use \ + ... '[1:2:ooops]' as a literal value, it needs to be escaped like \ + ... '\\[1:2:ooops]'. Log ${LIST}[1:2:ooops] Non-existing variable @@ -92,10 +113,11 @@ Non-existing index variable [Documentation] FAIL Variable '\${nonex index}' not found. Log ${LIST}[${nonex index}] -Non-list variable +Non-subscriptable variable [Documentation] FAIL - ... Variable '\${INT}' is integer, which is not subscriptable, \ - ... and thus accessing item '0' from it is not possible. + ... Variable '\${INT}' is integer, which is not subscriptable, and thus \ + ... accessing item '0' from it is not possible. To use '[0]' as a \ + ... literal value, it needs to be escaped like '\\[0]'. Log ${INT}[0] Old syntax with `@` still works but is deprecated @@ -108,5 +130,7 @@ Old syntax with `@` still works but is deprecated Old syntax with `@` doesn't support new slicing syntax [Documentation] Slicing support should be added in RF 3.3 when `@{list}[index]` changes. - ... FAIL Sequence '\@{LIST}' used with invalid index '1:'. + ... FAIL Sequence '\@{LIST}' used with invalid index '1:'. \ + ... To use '[1:]' as a literal value, it needs to be \ + ... escaped like '\\[1:]'. Log @{LIST}[1:] diff --git a/atest/testdata/variables/nested_item_access.robot b/atest/testdata/variables/nested_item_access.robot index 70dda95d297..662659d6bac 100644 --- a/atest/testdata/variables/nested_item_access.robot +++ b/atest/testdata/variables/nested_item_access.robot @@ -6,6 +6,7 @@ Test Template Should Be Equal ${LIST} [['item'], [1, 2, (3, [4]), 5], 'third'] ${DICT} {'key': {'key': 'value'}, 1: {2: 3}, 'x': {'y': {'z': ''}}} ${MIXED} {'x': [(1, {'y': {'z': ['foo', 'bar', {'': [42]}]}})]} +${STRING} Robot42 *** Test Cases *** Nested list access @@ -36,23 +37,31 @@ Non-existing nested dict item ${DICT}[x][y][nonex] whatever Invalid nested list access - [Documentation] FAIL Sequence '\${LIST}[1][2]' used with invalid index 'inv'. + [Documentation] FAIL + ... Sequence '\${LIST}[1][2]' used with invalid index 'inv'. To use \ + ... '[inv]' as a literal value, it needs to be escaped like '\\[inv]'. ${LIST}[1][2][inv] whatever Invalid nested dict access [Documentation] FAIL STARTS: Subscriptable '\${DICT}[key]' used with invalid key: ${DICT}[key][${DICT}] whatever -Nested access with non-list/dict +Invalid nested string access + [Documentation] FAIL Sequence '\${STRING}[1]' used with invalid index 'inv'. + ${LIST}[1][inv] whatever + +Nested access with non-subscriptable [Documentation] FAIL - ... Variable '\${DICT}[\${1}][\${2}]' is integer, which is not subscriptable, \ - ... and thus accessing item '0' from it is not possible. + ... Variable '\${DICT}[\${1}][\${2}]' is integer, which is not \ + ... subscriptable, and thus accessing item '0' from it is not possible. \ + ... To use '[0]' as a literal value, it needs to be escaped like '\\[0]'. ${DICT}[${1}][${2}][0] whatever Escape nested ${LIST}[-1]\[0] third[0] ${DICT}[key][key]\[key] value[key] ${DICT}[key]\[key][key] {'key': 'value'}[key][key] + ${STRING}[0]\[-1] R[-1] Nested access doesn't support old `@` and `&` syntax @{LIST}[0][0] ['item'][0] diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index 72bee48d037..712b8d18f68 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -144,8 +144,9 @@ def _get_variable_item(self, match, value): else: raise VariableError( "Variable '%s' is %s, which is not subscriptable, and " - "thus accessing item '%s' from it is not possible." - % (name, type_name(value), item) + "thus accessing item '%s' from it is not possible. To use " + "'[%s]' as a literal value, it needs to be escaped like " + "'\\[%s]'." % (name, type_name(value), item, item, item) ) name = '%s[%s]' % (name, item) return value @@ -155,8 +156,10 @@ def _get_sequence_variable_item(self, name, variable, index): try: index = self._parse_sequence_variable_index(index, name[0] == '$') except ValueError: - raise VariableError("Sequence '%s' used with invalid index '%s'." - % (name, index)) + raise VariableError("Sequence '%s' used with invalid index '%s'. " + "To use '[%s]' as a literal value, it needs " + "to be escaped like '\\[%s]'." + % (name, index, index, index)) try: return variable[index] except IndexError: From 883e6ed3b8eaba602ddb9940c7c3611345b819a6 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 14:17:16 +0100 Subject: [PATCH 12/15] Add utests for changes in #3350 --- utest/variables/test_variables.py | 50 ++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/utest/variables/test_variables.py b/utest/variables/test_variables.py index 09edf916696..78dda635d07 100644 --- a/utest/variables/test_variables.py +++ b/utest/variables/test_variables.py @@ -19,6 +19,8 @@ class PythonObject: def __init__(self, a, b): self.a = a self.b = b + def __getitem__(self, index): + return (self.a, self.b)[index] def __str__(self): return '(%s, %s)' % (self.a, self.b) __repr__ = __str__ @@ -222,7 +224,7 @@ def test_math_with_internal_vars_does_not_work_if_first_var_is_float(self): assert_raises(VariableError, self.varz.replace_scalar, '${${1.1} * ${2}}') assert_raises(VariableError, self.varz.replace_scalar, '${${1.1}/${2}}') - def test_list_variable_as_scalar(self): + def var_variable_as_scalar(self): self.varz['@{name}'] = exp = ['spam', 'eggs'] assert_equal(self.varz.replace_scalar('${name}'), exp) assert_equal(self.varz.replace_list(['${name}', 42]), [exp, 42]) @@ -267,6 +269,52 @@ def test_ignore_error(self): assert_equal(v.replace_list(['${x}'+item+'${x}', '@{NON}'], ignore_errors=True), ['x' + item + x_at_end, '@{NON}']) + def test_sequence_subscript(self): + sequences = ( + [42, 'my', 'name'], + (42, ['foo', 'bar'], 'name'), + 'abcDEF123#@$', + b'abcDEF123#@$', + bytearray(b'abcDEF123#@$'), + ) + for var in sequences: + self.varz['${var}'] = var + assert_equal(self.varz.replace_scalar('${var}[0]'), var[0]) + assert_equal(self.varz.replace_scalar('${var}[-2]'), var[-2]) + assert_equal(self.varz.replace_scalar('${var}[::2]'), var[::2]) + assert_equal(self.varz.replace_scalar('${var}[1::2]'), var[1::2]) + assert_equal(self.varz.replace_scalar('${var}[1:-3:2]'), var[1:-3:2]) + assert_raises(VariableError, self.varz.replace_scalar, '${var}[0][1]') + + def test_dict_subscript(self): + a_key = (42, b'key') + var = {'foo': 'bar', 42: [4, 2], 'name': b'my-name', a_key: {4: 2}} + self.varz['${a_key}'] = a_key + self.varz['${var}'] = var + assert_equal(self.varz.replace_scalar('${var}[foo][-1]'), var['foo'][-1]) + assert_equal(self.varz.replace_scalar('${var}[${42}][-1]'), var[42][-1]) + assert_equal(self.varz.replace_scalar('${var}[name][:3]'), var['name'][:3]) + assert_equal(self.varz.replace_scalar('${var}[${a_key}][${4}]'), var[a_key][4]) + assert_raises(VariableError, self.varz.replace_scalar, '${var}[1]') + assert_raises(VariableError, self.varz.replace_scalar, '${var}[42:]') + assert_raises(VariableError, self.varz.replace_scalar, '${var}[nonex]') + + def test_custom_class_subscript(self): + # the two class attributes are accessible via indices 0 and 1 + bytes_key = b'my' + var = PythonObject([1, 2, 3, 4, 5], {bytes_key: 'myname'}) + self.varz['${bytes_key}'] = bytes_key + self.varz['${var}'] = var + assert_equal(self.varz.replace_scalar('${var}[${0}][2::2]'), [3, 5]) + assert_equal(self.varz.replace_scalar('${var}[0][2::2]'), [3, 5]) + assert_equal(self.varz.replace_scalar('${var}[1][${bytes_key}][2:]'), 'name') + assert_equal(self.varz.replace_scalar('${var}\\[1]'), str(var) + '[1]') + assert_raises(IndexError, self.varz.replace_scalar, '${var}[2]') + assert_raises(VariableError, self.varz.replace_scalar, '${var}[${bytes_key}]') + + def test_non_subscriptable(self): + assert_raises(VariableError, self.varz.replace_scalar, '${1}[1]') + if __name__ == '__main__': unittest.main() From 38981bca8593c071bf72d01e33ecc1f05c5058d0 Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 15:14:29 +0100 Subject: [PATCH 13/15] Add support for slicing for non-sequence subscriptables --- src/robot/variables/replacer.py | 7 ++++--- utest/variables/test_variables.py | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py index 712b8d18f68..bc71994271d 100644 --- a/src/robot/variables/replacer.py +++ b/src/robot/variables/replacer.py @@ -174,7 +174,7 @@ def _parse_sequence_variable_index(self, index, support_slice=True): return slice(*[int(i) if i else None for i in index.split(':')]) def _get_subscriptable_variable_item(self, name, variable, key): - if not isinstance(key, int): + if not isinstance(key, (int, slice)): key = self.replace_scalar(key) try: return variable[key] @@ -182,9 +182,10 @@ def _get_subscriptable_variable_item(self, name, variable, key): raise VariableError("Subscriptable '%s' has no key '%s'." % (name, key)) except TypeError as err: - if not isinstance(key, int): + if not isinstance(key, (int, slice)): try: - key = int(key) + key = self._parse_sequence_variable_index( + key, name[0] == '$') except: pass else: diff --git a/utest/variables/test_variables.py b/utest/variables/test_variables.py index 78dda635d07..b8bd8b031b8 100644 --- a/utest/variables/test_variables.py +++ b/utest/variables/test_variables.py @@ -301,6 +301,7 @@ def test_dict_subscript(self): def test_custom_class_subscript(self): # the two class attributes are accessible via indices 0 and 1 + # slicing should be supported here as well bytes_key = b'my' var = PythonObject([1, 2, 3, 4, 5], {bytes_key: 'myname'}) self.varz['${bytes_key}'] = bytes_key @@ -309,7 +310,11 @@ def test_custom_class_subscript(self): assert_equal(self.varz.replace_scalar('${var}[0][2::2]'), [3, 5]) assert_equal(self.varz.replace_scalar('${var}[1][${bytes_key}][2:]'), 'name') assert_equal(self.varz.replace_scalar('${var}\\[1]'), str(var) + '[1]') - assert_raises(IndexError, self.varz.replace_scalar, '${var}[2]') + assert_equal(self.varz.replace_scalar('${var}[:][0][4]'), var[:][0][4]) + assert_equal(self.varz.replace_scalar('${var}[:-2]'), var[:-2]) + assert_equal(self.varz.replace_scalar('${var}[:7:-2]'), var[:7:-2]) + assert_equal(self.varz.replace_scalar('${var}[2::]'), ()) + assert_raises(IndexError, self.varz.replace_scalar, '${var}[${2}]') assert_raises(VariableError, self.varz.replace_scalar, '${var}[${bytes_key}]') def test_non_subscriptable(self): From 22929a712eea67905b00bd16f3ce026525403e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20Kl=C3=A4rck?= Date: Tue, 31 Dec 2019 18:50:01 +0200 Subject: [PATCH 14/15] Lexer: Fine tune lexing name statement. If name has other content on same line, yield EOS ending the name statement right after the name token, not after separator. Also, happy new year! --- src/robot/parsing/lexer/readers.py | 18 +++++++++++++----- utest/parsing/test_lexer.py | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/robot/parsing/lexer/readers.py b/src/robot/parsing/lexer/readers.py index 5b3fcb1b3e5..0f88a705f7d 100644 --- a/src/robot/parsing/lexer/readers.py +++ b/src/robot/parsing/lexer/readers.py @@ -75,19 +75,27 @@ def get_tokens(self): self._split_trailing_comment_and_empty_lines(s) for s in statements ) - # Setting local variables, including 'type' below, is performance - # optimization to avoid unnecessary lookups and attribute access. + # Setting local variables is performance optimization to avoid + # unnecessary lookups and attribute access. name_type = Token.NAME - separator_or_eol_type = (Token.EOL, Token.SEPARATOR) + separator_type = Token.SEPARATOR + eol_type = Token.EOL for statement in statements: name_seen = False + separator_after_name = None prev_token = None for token in statement: type = token.type # Performance optimization. if type in ignore: continue - if name_seen and type not in separator_or_eol_type: - yield EOS.from_token(prev_token) + if name_seen: + if type == separator_type: + separator_after_name = token + continue + if type != eol_type: + yield EOS.from_token(prev_token) + if separator_after_name: + yield separator_after_name name_seen = False if type == name_type: name_seen = True diff --git a/utest/parsing/test_lexer.py b/utest/parsing/test_lexer.py index 7e269f84785..6ddeeee9c42 100644 --- a/utest/parsing/test_lexer.py +++ b/utest/parsing/test_lexer.py @@ -40,20 +40,20 @@ def test_name_on_own_row(self): def test_name_and_keyword_on_same_row(self): self._verify('Name Keyword', - [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), + [(T.NAME, 'Name', 2, 1), (T.EOS, '', 2, 5), (T.SEPARATOR, ' ', 2, 5), (T.KEYWORD, 'Keyword', 2, 9), (T.EOL, '', 2, 16), (T.EOS, '', 2, 16)]) self._verify('N K A', - [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), + [(T.NAME, 'N', 2, 1), (T.EOS, '', 2, 2), (T.SEPARATOR, ' ', 2, 2), (T.KEYWORD, 'K', 2, 4), (T.SEPARATOR, ' ', 2, 5), (T.ARGUMENT, 'A', 2, 7), (T.EOL, '', 2, 8), (T.EOS, '', 2, 8)]) self._verify('N ${v}= K', - [(T.NAME, 'N', 2, 1), (T.SEPARATOR, ' ', 2, 2), (T.EOS, '', 2, 4), + [(T.NAME, 'N', 2, 1), (T.EOS, '', 2, 2), (T.SEPARATOR, ' ', 2, 2), (T.ASSIGN, '${v}=', 2, 4), (T.SEPARATOR, ' ', 2, 9), (T.KEYWORD, 'K', 2, 11), (T.EOL, '', 2, 12), (T.EOS, '', 2, 12)]) def test_name_and_setting_on_same_row(self): self._verify('Name [Documentation] The doc.', - [(T.NAME, 'Name', 2, 1), (T.SEPARATOR, ' ', 2, 5), (T.EOS, '', 2, 9), + [(T.NAME, 'Name', 2, 1), (T.EOS, '', 2, 5), (T.SEPARATOR, ' ', 2, 5), (T.DOCUMENTATION, '[Documentation]', 2, 9), (T.SEPARATOR, ' ', 2, 24), (T.ARGUMENT, 'The doc.', 2, 28), (T.EOL, '', 2, 36), (T.EOS, '', 2, 36)]) @@ -81,20 +81,20 @@ def test_name_on_own_row(self): def test_name_and_keyword_on_same_row(self): self._verify('| Name | Keyword', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), - (T.KEYWORD, 'Keyword', 2, 10), (T.EOL, '', 2, 17), (T.EOS, '', 2, 17)]) + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.EOS, '', 2, 7), + (T.SEPARATOR, ' | ', 2, 7), (T.KEYWORD, 'Keyword', 2, 10), (T.EOL, '', 2, 17), (T.EOS, '', 2, 17)]) self._verify('| N | K | A |\n', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 3), (T.SEPARATOR, ' | ', 2, 4), (T.EOS, '', 2, 7), - (T.KEYWORD, 'K', 2, 7), (T.SEPARATOR, ' | ', 2, 8), + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 3), (T.EOS, '', 2, 4), + (T.SEPARATOR, ' | ', 2, 4), (T.KEYWORD, 'K', 2, 7), (T.SEPARATOR, ' | ', 2, 8), (T.ARGUMENT, 'A', 2, 11), (T.SEPARATOR, ' |', 2, 12), (T.EOL, '\n', 2, 14), (T.EOS, '', 2, 15)]) self._verify('| N | ${v} = | K ', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 6), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 12), - (T.ASSIGN, '${v} =', 2, 12), (T.SEPARATOR, ' | ', 2, 18), + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'N', 2, 6), (T.EOS, '', 2, 7), + (T.SEPARATOR, ' | ', 2, 7), (T.ASSIGN, '${v} =', 2, 12), (T.SEPARATOR, ' | ', 2, 18), (T.KEYWORD, 'K', 2, 27), (T.EOL, ' ', 2, 28), (T.EOS, '', 2, 32)]) def test_name_and_setting_on_same_row(self): self._verify('| Name | [Documentation] | The doc.', - [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.SEPARATOR, ' | ', 2, 7), (T.EOS, '', 2, 10), + [(T.SEPARATOR, '| ', 2, 1), (T.NAME, 'Name', 2, 3), (T.EOS, '', 2, 7), (T.SEPARATOR, ' | ', 2, 7), (T.DOCUMENTATION, '[Documentation]', 2, 10), (T.SEPARATOR, ' | ', 2, 25), (T.ARGUMENT, 'The doc.', 2, 28), (T.EOL, '', 2, 36), (T.EOS, '', 2, 36)]) @@ -222,8 +222,8 @@ class TestGetResourceTokensSourceFormats(SourceFormatsTestBase): (T.EOL, '\n', 4, 16), (T.EOS, '', 4, 17), (T.NAME, 'NOOP', 5, 1), + (T.EOS, '', 5, 5), (T.SEPARATOR, ' ', 5, 5), - (T.EOS, '', 5, 9), (T.KEYWORD, 'No Operation', 5, 9), (T.EOL, '\n', 5, 21), (T.EOS, '', 5, 22) From 96108c22361228619dc6f865d99e2735118ed98c Mon Sep 17 00:00:00 2001 From: Jasper Craeghs Date: Tue, 31 Dec 2019 17:59:51 +0100 Subject: [PATCH 15/15] Extend documentation about accessing items of subscriptable variables --- .../src/CreatingTestData/Variables.rst | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/doc/userguide/src/CreatingTestData/Variables.rst b/doc/userguide/src/CreatingTestData/Variables.rst index f8bb1fb3538..237db168486 100644 --- a/doc/userguide/src/CreatingTestData/Variables.rst +++ b/doc/userguide/src/CreatingTestData/Variables.rst @@ -284,62 +284,67 @@ are imports, setups and teardowns where dictionaries can be used as arguments. Accessing list and dictionary items ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It is possible to access items of lists and dictionaries using special -syntax `${var}[item]`. Accessing items is an old feature, but prior to -Robot Framework 3.1 the syntax was `@{var}[item]` with lists and -`&{var}[item]` with dictionaries. The old syntax was deprecated in +It is possible to access items of subscriptable variables, e.g. lists and +dictionaries, using special syntax `${var}[item]`. Accessing items is an old +feature, but prior to Robot Framework 3.1 the syntax was `@{var}[item]` with +lists and `&{var}[item]` with dictionaries. The old syntax was deprecated in Robot Framework 3.2 and will not be supported in the future. -Accessing list items -'''''''''''''''''''' +.. _sequence items: + +Accessing sequence items +'''''''''''''''''''''''' -It is possible to access a certain item of a list variable with the syntax -`${var}[index]`, where `index` is the index of the selected value. Indices -start from zero, negative indices can be used to access items from the end, -and trying to access an item with too large an index causes an error. -Indices are automatically converted to integers, and it is also possible to -use variables as indices. List items accessed in this manner can be used -similarly as scalar variables. +It is possible to access a certain item of a `sequence`__ variable (e.g. list, +string and bytes) with the syntax `${var}[index]`, where `index` is the index of +the selected value. Indices start from zero, negative indices can be used to +access items from the end, and trying to access an item with too large an index +causes an error. Indices are automatically converted to integers, and it is also +possible to use variables as indices. Sequence items accessed in this manner can +be used similarly as scalar variables. .. sourcecode:: robotframework *** Test Cases *** - List variable item + Sequence variable item Login ${USER}[0] ${USER}[1] Title Should Be Welcome ${USER}[0]! Negative index - Log ${LIST}[-1] + Log ${SEQUENCE}[-1] Index defined as variable - Log ${LIST}[${INDEX}] + Log ${SEQUENCE}[${INDEX}] -List item access supports also the `same "slice" functionality as Python`__ +Sequence item access supports also the `same "slice" functionality as Python`__ with syntax like `${var}[1:]`. With this syntax you do not get a single -item but a slice of the original list. Same way as with Python you can +item but a slice of the original sequence. Same way as with Python you can specify the start index, the end index, and the step: .. sourcecode:: robotframework *** Test Cases *** Start index - Keyword ${LIST}[1:] + Keyword ${SEQUENCE}[1:] End index - Keyword ${LIST}[:4] + Keyword ${SEQUENCE}[:4] Start and end - Keyword ${LIST}[2:-1] + Keyword ${SEQUENCE}[2:-1] Step - Keyword ${LIST}[::2] - Keyword ${LIST}[1:-1:10] + Keyword ${SEQUENCE}[::2] + Keyword ${SEQUENCE}[1:-1:10] .. note:: The slice syntax is new in Robot Framework 3.1 and does not work with the old `@{var}[index]` syntax. +__ https://docs.python.org/3/glossary.html#term-sequence __ https://docs.python.org/glossary.html#term-slice +.. _dictionary items: + Accessing individual dictionary items ''''''''''''''''''''''''''''''''''''' @@ -367,10 +372,20 @@ for more details about this syntax. Login ${USER.name} ${USER.password} Title Should Be Welcome ${USER.name}! +Accessing items of a custom object +'''''''''''''''''''''''''''''''''' + +It is possible to access items of an object of a class that implements the +`__getitem__()`__ method. Depending on the implementation by the class, it is +handled the same as accessing either `sequence items`_ or `dictionary items`_ +as explained in the two subsections above. + +__ https://docs.python.org/3/reference/datamodel.html#object.__getitem__ + Nested item access '''''''''''''''''' -Also nested list and dictionary structures can be accessed using the same +Also nested subscriptable variables can be accessed using the same item access syntax like `${var}[item1][item2]`. This is especially useful when working with JSON data often returned by REST services. For example, if a variable `${DATA}` contains `[{'id': 1, 'name': 'Robot'},