# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from functools import total_ordering
from typing import (Any, Iterable, Iterator, MutableSequence, overload, TYPE_CHECKING,
Type, TypeVar)
from robot.utils import copy_signature, KnownAtRuntime, type_name
from .modelobject import DataDict
if TYPE_CHECKING:
from .visitor import SuiteVisitor
T = TypeVar('T')
Self = TypeVar('Self', bound='ItemList')
[docs]@total_ordering
class ItemList(MutableSequence[T]):
"""List of items of a certain enforced type.
New items can be created using the :meth:`create` method and existing items
added using the common list methods like :meth:`append` or :meth:`insert`.
In addition to the common type, items can have certain common and
automatically assigned attributes.
Starting from Robot Framework 6.1, items can be added as dictionaries and
actual items are generated based on them automatically. If the type has
a ``from_dict`` class method, it is used, and otherwise dictionary data is
passed to the type as keyword arguments.
"""
__slots__ = ['_item_class', '_common_attrs', '_items']
# TypeVar T needs to be applied to a variable to be compatible with @copy_signature
item_type: Type[T] = KnownAtRuntime
def __init__(self, item_class: Type[T],
common_attrs: 'dict[str, Any]|None' = None,
items: 'Iterable[T|DataDict]' = ()):
self._item_class = item_class
self._common_attrs = common_attrs
self._items: 'list[T]' = []
if items:
self.extend(items)
[docs] @copy_signature(item_type)
def create(self, *args, **kwargs) -> T:
"""Create a new item using the provided arguments."""
return self.append(self._item_class(*args, **kwargs))
[docs] def append(self, item: 'T|DataDict') -> T:
item = self._check_type_and_set_attrs(item)
self._items.append(item)
return item
def _check_type_and_set_attrs(self, item: 'T|DataDict') -> T:
if not isinstance(item, self._item_class):
if isinstance(item, dict):
item = self._item_from_dict(item)
else:
raise TypeError(f'Only {type_name(self._item_class)} objects '
f'accepted, got {type_name(item)}.')
if self._common_attrs:
for attr, value in self._common_attrs.items():
setattr(item, attr, value)
return item
def _item_from_dict(self, data: DataDict) -> T:
if hasattr(self._item_class, 'from_dict'):
return self._item_class.from_dict(data) # type: ignore
return self._item_class(**data)
[docs] def extend(self, items: 'Iterable[T|DataDict]'):
self._items.extend(self._check_type_and_set_attrs(i) for i in items)
[docs] def insert(self, index: int, item: 'T|DataDict'):
item = self._check_type_and_set_attrs(item)
self._items.insert(index, item)
[docs] def index(self, item: T, *start_and_end) -> int:
return self._items.index(item, *start_and_end)
[docs] def clear(self):
self._items = []
[docs] def visit(self, visitor: 'SuiteVisitor'):
for item in self:
item.visit(visitor) # type: ignore
def __iter__(self) -> Iterator[T]:
index = 0
while index < len(self._items):
yield self._items[index]
index += 1
@overload
def __getitem__(self, index: int) -> T:
...
@overload
def __getitem__(self: Self, index: slice) -> Self:
...
def __getitem__(self, index):
if isinstance(index, slice):
return self._create_new_from(self._items[index])
return self._items[index]
def _create_new_from(self: Self, items: Iterable[T]) -> Self:
# Cannot pass common_attrs directly to new object because all
# subclasses don't have compatible __init__.
new = type(self)(self._item_class)
new._common_attrs = self._common_attrs
new.extend(items)
return new
@overload
def __setitem__(self, index: int, item: 'T|DataDict'):
...
@overload
def __setitem__(self, index: slice, item: 'Iterable[T|DataDict]'):
...
def __setitem__(self, index, item):
if isinstance(index, slice):
self._items[index] = [self._check_type_and_set_attrs(i) for i in item]
else:
self._items[index] = self._check_type_and_set_attrs(item)
def __delitem__(self, index: 'int|slice'):
del self._items[index]
def __contains__(self, item: object) -> bool:
return item in self._items
def __len__(self) -> int:
return len(self._items)
def __str__(self) -> str:
return str(list(self))
def __repr__(self) -> str:
class_name = type(self).__name__
item_name = self._item_class.__name__
return f'{class_name}(item_class={item_name}, items={self._items})'
[docs] def count(self, item: T) -> int:
return self._items.count(item)
[docs] def sort(self, **config):
self._items.sort(**config)
[docs] def reverse(self):
self._items.reverse()
def __reversed__(self) -> Iterator[T]:
index = 0
while index < len(self._items):
yield self._items[len(self._items) - index - 1]
index += 1
def __eq__(self, other: object) -> bool:
return (isinstance(other, ItemList)
and self._is_compatible(other)
and self._items == other._items)
def _is_compatible(self, other) -> bool:
return (self._item_class is other._item_class
and self._common_attrs == other._common_attrs)
def __lt__(self, other: 'ItemList[T]') -> bool:
if not isinstance(other, ItemList):
raise TypeError(f'Cannot order ItemList and {type_name(other)}.')
if not self._is_compatible(other):
raise TypeError('Cannot order incompatible ItemLists.')
return self._items < other._items
def __add__(self: Self, other: 'ItemList[T]') -> Self:
if not isinstance(other, ItemList):
raise TypeError(f'Cannot add ItemList and {type_name(other)}.')
if not self._is_compatible(other):
raise TypeError('Cannot add incompatible ItemLists.')
return self._create_new_from(self._items + other._items)
def __iadd__(self: Self, other: Iterable[T]) -> Self:
if isinstance(other, ItemList) and not self._is_compatible(other):
raise TypeError('Cannot add incompatible ItemLists.')
self.extend(other)
return self
def __mul__(self: Self, count: int) -> Self:
return self._create_new_from(self._items * count)
def __imul__(self: Self, count: int) -> Self:
self._items *= count
return self
def __rmul__(self: Self, count: int) -> Self:
return self * count
[docs] def to_dicts(self) -> 'list[DataDict]':
"""Return list of items converted to dictionaries.
Items are converted to dictionaries using the ``to_dict`` method, if
they have it, or the built-in ``vars()``.
New in Robot Framework 6.1.
"""
if not hasattr(self._item_class, 'to_dict'):
return [vars(item) for item in self]
return [item.to_dict() for item in self] # type: ignore