# 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 Any, Iterable, Iterator, overload, Sequence
from robot.utils import normalize, NormalizedDict, Matcher
[docs]class TagPatterns(Sequence['TagPattern']):
def __init__(self, patterns: Iterable[str]):
self._patterns = tuple(TagPattern.from_string(p) for p in Tags(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):
[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)
[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)
return ((self._first.match(tags) or not self._first)
and 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()