Source code for robot.model.tags

#  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 abc import ABC, abstractmethod
from typing import Iterable, Iterator, overload, Sequence

from robot.utils import Matcher, normalize, NormalizedDict


[docs] class Tags(Sequence[str]): __slots__ = ("_tags", "_reserved") def __init__(self, tags: Iterable[str] = ()): if isinstance(tags, Tags): self._tags, self._reserved = tags._tags, tags._reserved else: self._tags, self._reserved = self._init_tags(tags)
[docs] def robot(self, name: str) -> bool: """Check do tags contain a reserved tag in format `robot:<name>`. `tags.robot('<name>')` is same as `'robot:<name>' in tags`, but the former is considerably faster. """ return name in self._reserved
def _init_tags(self, tags) -> "tuple[tuple[str, ...], tuple[str, ...]]": if not tags: return (), () if isinstance(tags, str): tags = (tags,) return self._normalize(tags) def _normalize(self, tags): nd = NormalizedDict.fromkeys([str(t) for t in tags], ignore="_") if "" in nd: del nd[""] if "NONE" in nd: del nd["NONE"] reserved = [tag[6:] for tag in nd.normalized_keys if tag[:6] == "robot:"] return tuple(nd), tuple(reserved)
[docs] def add(self, tags: Iterable[str], remove_negated: bool = False): tags = tuple(Tags(tags)) if remove_negated: remove = [t[1:] for t in tags if t[0] == "-"] if remove: self.remove(remove) tags = [t for t in tags if t[0] != "-"] self.__init__(tuple(self) + tuple(tags))
[docs] def remove(self, tags: Iterable[str]): match = TagPatterns(tags).match self.__init__([t for t in self if not match(t)])
[docs] def match(self, tags: Iterable[str]) -> bool: return TagPatterns(tags).match(self)
def __contains__(self, tags: Iterable[str]) -> bool: return self.match(tags) def __len__(self) -> int: return len(self._tags) def __iter__(self) -> Iterator[str]: return iter(self._tags) def __str__(self) -> str: tags = ", ".join(self) return f"[{tags}]" def __repr__(self) -> str: return repr(list(self)) def __eq__(self, other: object) -> bool: if not isinstance(other, Iterable): return False if not isinstance(other, Tags): other = Tags(other) self_normalized = [normalize(tag, ignore="_") for tag in self] other_normalized = [normalize(tag, ignore="_") for tag in other] return sorted(self_normalized) == sorted(other_normalized) @overload def __getitem__(self, index: int) -> str: ... @overload def __getitem__(self, index: slice) -> "Tags": ... def __getitem__(self, index: "int|slice") -> "str|Tags": if isinstance(index, slice): return Tags(self._tags[index]) return self._tags[index] def __add__(self, other: Iterable[str]) -> "Tags": return Tags(tuple(self) + tuple(Tags(other)))
[docs] class TagPatterns(Sequence["TagPattern"]): def __init__(self, patterns: Iterable[str] = ()): self._patterns = tuple(TagPattern.from_string(p) for p in Tags(patterns)) @property def is_constant(self): return all(p.is_constant for p in self._patterns)
[docs] def match(self, tags: Iterable[str]) -> bool: if not self._patterns: return False tags = normalize_tags(tags) return any(p.match(tags) for p in self._patterns)
def __contains__(self, tag: str) -> bool: return self.match(tag) def __len__(self) -> int: return len(self._patterns) def __iter__(self) -> Iterator["TagPattern"]: return iter(self._patterns) def __getitem__(self, index: int) -> "TagPattern": return self._patterns[index] def __str__(self) -> str: patterns = ", ".join(str(pattern) for pattern in self) return f"[{patterns}]"
[docs] class TagPattern(ABC): is_constant = False
[docs] @classmethod def from_string(cls, pattern: str) -> "TagPattern": pattern = pattern.replace(" ", "") if "NOT" in pattern: must_match, *must_not_match = pattern.split("NOT") return NotTagPattern(must_match, must_not_match) if "OR" in pattern: return OrTagPattern(pattern.split("OR")) if "AND" in pattern or "&" in pattern: return AndTagPattern(pattern.replace("&", "AND").split("AND")) return SingleTagPattern(pattern)
[docs] @abstractmethod def match(self, tags: Iterable[str]) -> bool: raise NotImplementedError
@abstractmethod def __iter__(self) -> Iterator["TagPattern"]: raise NotImplementedError @abstractmethod def __str__(self) -> str: raise NotImplementedError
[docs] class SingleTagPattern(TagPattern): def __init__(self, pattern: str): # Normalization is handled here, not in Matcher, for performance reasons. # This way we can normalize tags only once. self._matcher = Matcher( normalize(pattern, ignore="_"), caseless=False, spaceless=False, ) @property def is_constant(self): pattern = self._matcher.pattern return not ("*" in pattern or "?" in pattern or "[" in pattern)
[docs] def match(self, tags: Iterable[str]) -> bool: tags = normalize_tags(tags) return self._matcher.match_any(tags)
def __iter__(self) -> Iterator["TagPattern"]: yield self def __str__(self) -> str: return self._matcher.pattern def __bool__(self) -> bool: return bool(self._matcher)
[docs] class AndTagPattern(TagPattern): def __init__(self, patterns: Iterable[str]): self._patterns = tuple(TagPattern.from_string(p) for p in patterns)
[docs] def match(self, tags: Iterable[str]) -> bool: tags = normalize_tags(tags) return all(p.match(tags) for p in self._patterns)
def __iter__(self) -> Iterator["TagPattern"]: return iter(self._patterns) def __str__(self) -> str: return " AND ".join(str(pattern) for pattern in self)
[docs] class OrTagPattern(TagPattern): def __init__(self, patterns: Iterable[str]): self._patterns = tuple(TagPattern.from_string(p) for p in patterns)
[docs] def match(self, tags: Iterable[str]) -> bool: tags = normalize_tags(tags) return any(p.match(tags) for p in self._patterns)
def __iter__(self) -> Iterator["TagPattern"]: return iter(self._patterns) def __str__(self) -> str: return " OR ".join(str(pattern) for pattern in self)
[docs] class NotTagPattern(TagPattern): def __init__(self, must_match: str, must_not_match: Iterable[str]): self._first = TagPattern.from_string(must_match) self._rest = OrTagPattern(must_not_match)
[docs] def match(self, tags: Iterable[str]) -> bool: tags = normalize_tags(tags) if self._first and not self._first.match(tags): return False return not self._rest.match(tags)
def __iter__(self) -> Iterator["TagPattern"]: yield self._first yield from self._rest def __str__(self) -> str: return " NOT ".join(str(pattern) for pattern in self).lstrip()
[docs] def normalize_tags(tags: Iterable[str]) -> Iterable[str]: """Performance optimization to normalize tags only once.""" if isinstance(tags, NormalizedTags): return tags if isinstance(tags, str): tags = [tags] return NormalizedTags([normalize(t, ignore="_") for t in tags])
[docs] class NormalizedTags(list): pass