robotcode-modifiers 2.6.2__tar.gz → 2.8.0.dev548__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.
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/.gitignore +4 -1
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/PKG-INFO +3 -2
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/pyproject.toml +1 -0
- robotcode_modifiers-2.8.0.dev548/src/robotcode/modifiers/__init__.py +4 -0
- robotcode_modifiers-2.8.0.dev548/src/robotcode/modifiers/__version__.py +1 -0
- robotcode_modifiers-2.8.0.dev548/src/robotcode/modifiers/metadata_modifiers.py +159 -0
- robotcode_modifiers-2.6.2/src/robotcode/modifiers/__init__.py +0 -3
- robotcode_modifiers-2.6.2/src/robotcode/modifiers/__version__.py +0 -1
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/README.md +0 -0
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/src/robotcode/modifiers/longname_modifiers.py +0 -0
- {robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/src/robotcode/modifiers/py.typed +0 -0
|
@@ -279,6 +279,9 @@ dist
|
|
|
279
279
|
# vuepress build output
|
|
280
280
|
.vuepress/dist
|
|
281
281
|
|
|
282
|
+
# Astro (documentation site)
|
|
283
|
+
docs/.astro/
|
|
284
|
+
|
|
282
285
|
# Serverless directories
|
|
283
286
|
.serverless/
|
|
284
287
|
|
|
@@ -339,7 +342,7 @@ bundled/libs
|
|
|
339
342
|
# .agents
|
|
340
343
|
.agents/
|
|
341
344
|
skills-lock.json
|
|
342
|
-
|
|
345
|
+
#.claude
|
|
343
346
|
|
|
344
347
|
# sarif files
|
|
345
348
|
/**/*.sarif.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: robotcode-modifiers
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0-dev.548
|
|
4
4
|
Summary: Some Robot Framework Modifiers for RobotCode
|
|
5
5
|
Project-URL: Homepage, https://robotcode.io
|
|
6
6
|
Project-URL: Donate, https://opencollective.com/robotcode
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
24
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
24
25
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
25
26
|
Classifier: Topic :: Utilities
|
|
@@ -18,6 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
|
20
20
|
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Programming Language :: Python :: 3.15",
|
|
21
22
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
22
23
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
23
24
|
"Operating System :: OS Independent",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.8.0-dev.548"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Select tests and tasks by their own metadata (Robot Framework 7.5+).
|
|
2
|
+
|
|
3
|
+
A test is matched by its entries, `Name:Line` for every line of every
|
|
4
|
+
metadata value. A pattern combines terms `NAME:VALUE` with `AND`, `OR` and
|
|
5
|
+
`NOT` like a tag pattern, but an operator is only a word of its own, so
|
|
6
|
+
upper-case values such as `CORE-123` or `NORBERT` stay literal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Callable, List, Mapping, Optional, Sequence, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from robot.api import SuiteVisitor
|
|
13
|
+
from robot.errors import DataError
|
|
14
|
+
from robot.model import TestCase, TestSuite
|
|
15
|
+
from robot.utils import Matcher, normalize
|
|
16
|
+
from robot.version import VERSION
|
|
17
|
+
|
|
18
|
+
# `TestCase.metadata` exists since Robot Framework 7.5. This package must stay
|
|
19
|
+
# importable with plain Robot Framework, so it reads the version itself.
|
|
20
|
+
if tuple(int(part) for part in re.findall(r"\d+", VERSION)[:2]) >= (7, 5):
|
|
21
|
+
|
|
22
|
+
def _test_metadata(test: TestCase) -> Mapping[str, str]:
|
|
23
|
+
return test.metadata # type: ignore[attr-defined, no-any-return, unused-ignore]
|
|
24
|
+
|
|
25
|
+
else:
|
|
26
|
+
|
|
27
|
+
def _test_metadata(test: TestCase) -> Mapping[str, str]:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_Predicate = Callable[[Sequence[str]], bool]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def metadata_entries(metadata: Mapping[str, str]) -> List[Tuple[str, str]]:
|
|
35
|
+
"""The entries of a test's or suite's metadata as `(name, line)` pairs.
|
|
36
|
+
|
|
37
|
+
Every non-empty line of a value is an entry of its own, a value without
|
|
38
|
+
such a line gives one entry with an empty line. Cells of one `Metadata`
|
|
39
|
+
row are not split: Robot joins them with the separator of the source, so
|
|
40
|
+
they cannot be told apart from spaces inside a value. An item without a
|
|
41
|
+
name (a `Metadata` setting with nothing after it) is no metadata.
|
|
42
|
+
"""
|
|
43
|
+
entries: List[Tuple[str, str]] = []
|
|
44
|
+
for name, value in metadata.items():
|
|
45
|
+
if not name:
|
|
46
|
+
continue
|
|
47
|
+
lines = [line.strip() for line in value.splitlines() if line.strip()]
|
|
48
|
+
entries.extend((name, line) for line in lines or [""])
|
|
49
|
+
return entries
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalized_entries(metadata: Mapping[str, str]) -> List[str]:
|
|
53
|
+
return [normalize(f"{name}:{line}", ignore="_") for name, line in metadata_entries(metadata)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _split(words: List[str], operator: str) -> List[List[str]]:
|
|
57
|
+
parts: List[List[str]] = [[]]
|
|
58
|
+
for word in words:
|
|
59
|
+
if word == operator:
|
|
60
|
+
parts.append([])
|
|
61
|
+
else:
|
|
62
|
+
parts[-1].append(word)
|
|
63
|
+
return parts
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MetadataPattern:
|
|
67
|
+
"""A metadata pattern such as `Issue:4409 AND Author:Hans*`.
|
|
68
|
+
|
|
69
|
+
Terms match like tag patterns (`*`, `?`, `[…]`; case, spaces and
|
|
70
|
+
underscores ignored). The operators bind as in Robot's tag patterns:
|
|
71
|
+
`AND` before `OR`, `NOT` last. Raises `DataError` for an invalid pattern.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, pattern: str) -> None:
|
|
75
|
+
self.pattern = pattern
|
|
76
|
+
words = pattern.split()
|
|
77
|
+
if not words:
|
|
78
|
+
raise self._invalid("it is empty")
|
|
79
|
+
must_match, *must_not_match = _split(words, "NOT")
|
|
80
|
+
# Only a leading `NOT` leaves the first part empty: then every test
|
|
81
|
+
# that matches none of the other parts is selected.
|
|
82
|
+
self._must_match = self._any_of(must_match) if must_match else None
|
|
83
|
+
self._must_not_match = [self._any_of(part) for part in must_not_match]
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return self.pattern
|
|
87
|
+
|
|
88
|
+
def match(self, metadata: Mapping[str, str]) -> bool:
|
|
89
|
+
return self._match_normalized(_normalized_entries(metadata))
|
|
90
|
+
|
|
91
|
+
def _match_normalized(self, entries: Sequence[str]) -> bool:
|
|
92
|
+
if self._must_match is not None and not self._must_match(entries):
|
|
93
|
+
return False
|
|
94
|
+
return not any(part(entries) for part in self._must_not_match)
|
|
95
|
+
|
|
96
|
+
def _any_of(self, words: List[str]) -> _Predicate:
|
|
97
|
+
alternatives = [
|
|
98
|
+
[self._term(term) for term in _split(alternative, "AND")] for alternative in _split(words, "OR")
|
|
99
|
+
]
|
|
100
|
+
return lambda entries: any(all(term(entries) for term in terms) for terms in alternatives)
|
|
101
|
+
|
|
102
|
+
def _term(self, words: List[str]) -> _Predicate:
|
|
103
|
+
if not words:
|
|
104
|
+
raise self._invalid("an operator has no term on one of its sides")
|
|
105
|
+
term = " ".join(words)
|
|
106
|
+
if ":" not in term:
|
|
107
|
+
raise self._invalid(f"term '{term}' is not in the format NAME:VALUE")
|
|
108
|
+
# Normalised here, like Robot's own tag patterns do, so the matcher's
|
|
109
|
+
# own normalisation is a no-op.
|
|
110
|
+
matcher = Matcher(normalize(term, ignore="_"), caseless=False, spaceless=False)
|
|
111
|
+
return lambda entries: bool(matcher.match_any(entries))
|
|
112
|
+
|
|
113
|
+
def _invalid(self, reason: str) -> DataError:
|
|
114
|
+
return DataError(f"Invalid metadata pattern '{self.pattern}': {reason}.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _ByTestMetadataBase(SuiteVisitor):
|
|
118
|
+
def __init__(self, *patterns: Union[str, MetadataPattern]) -> None:
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.patterns: List[MetadataPattern] = []
|
|
121
|
+
self._error: Optional[DataError] = None
|
|
122
|
+
try:
|
|
123
|
+
self.patterns = [p if isinstance(p, MetadataPattern) else MetadataPattern(p) for p in patterns]
|
|
124
|
+
except DataError as error:
|
|
125
|
+
# Robot skips a modifier whose constructor fails and runs every
|
|
126
|
+
# test. An invalid pattern must never widen a selection, so the
|
|
127
|
+
# error is raised on the first suite, after removing its tests.
|
|
128
|
+
self._error = error
|
|
129
|
+
|
|
130
|
+
def start_suite(self, suite: TestSuite) -> None:
|
|
131
|
+
if self._error is not None:
|
|
132
|
+
suite.tests = []
|
|
133
|
+
suite.suites = []
|
|
134
|
+
raise self._error
|
|
135
|
+
suite.tests = [test for test in suite.tests if self._keep(test)]
|
|
136
|
+
|
|
137
|
+
def end_suite(self, suite: TestSuite) -> None:
|
|
138
|
+
suite.suites = [s for s in suite.suites if s.test_count > 0]
|
|
139
|
+
|
|
140
|
+
def _matches(self, test: TestCase) -> bool:
|
|
141
|
+
entries = _normalized_entries(_test_metadata(test))
|
|
142
|
+
return any(pattern._match_normalized(entries) for pattern in self.patterns)
|
|
143
|
+
|
|
144
|
+
def _keep(self, test: TestCase) -> bool:
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ByTestMetadata(_ByTestMetadataBase):
|
|
149
|
+
"""Keeps the tests and tasks whose metadata matches one of the patterns."""
|
|
150
|
+
|
|
151
|
+
def _keep(self, test: TestCase) -> bool:
|
|
152
|
+
return self._matches(test)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ExcludedByTestMetadata(_ByTestMetadataBase):
|
|
156
|
+
"""Removes the tests and tasks whose metadata matches one of the patterns."""
|
|
157
|
+
|
|
158
|
+
def _keep(self, test: TestCase) -> bool:
|
|
159
|
+
return not self._matches(test)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.6.2"
|
|
File without changes
|
|
File without changes
|
{robotcode_modifiers-2.6.2 → robotcode_modifiers-2.8.0.dev548}/src/robotcode/modifiers/py.typed
RENAMED
|
File without changes
|