-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy path__init__.py
More file actions
249 lines (187 loc) · 7.28 KB
/
__init__.py
File metadata and controls
249 lines (187 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
"""Contains the main code for the markers plugin."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from typing import Any
import click
from attrs import define
from rich.table import Table
from _pytask.click import ColoredCommand
from _pytask.console import console
from _pytask.dag_utils import task_and_preceding_tasks
from _pytask.exceptions import ConfigurationError
from _pytask.mark.expression import Expression
from _pytask.mark.expression import ParseError
from _pytask.mark.structures import MARK_GEN
from _pytask.mark.structures import Mark
from _pytask.mark.structures import MarkDecorator
from _pytask.mark.structures import MarkGenerator
from _pytask.outcomes import ExitCode
from _pytask.pluginmanager import hookimpl
from _pytask.pluginmanager import storage
from _pytask.session import Session
from _pytask.shared import parse_markers
if TYPE_CHECKING:
from collections.abc import Set as AbstractSet
from typing import NoReturn
import networkx as nx
from _pytask.node_protocols import PTask
__all__ = [
"MARK_GEN",
"Expression",
"Mark",
"MarkDecorator",
"MarkGenerator",
"ParseError",
"select_by_after_keyword",
"select_by_keyword",
"select_by_mark",
"select_tasks_by_marks_and_expressions",
]
@click.command(cls=ColoredCommand)
def markers(**raw_config: Any) -> NoReturn:
"""Show all registered markers."""
raw_config["command"] = "markers"
pm = storage.get()
try:
config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config)
session = Session.from_config(config)
except (ConfigurationError, Exception): # noqa: BLE001 # pragma: no cover
console.print_exception()
session = Session(exit_code=ExitCode.CONFIGURATION_FAILED)
else:
table = Table("Marker", "Description", leading=1)
for name, description in config["markers"].items():
table.add_row(f"pytask.mark.{name}", description)
console.print(table)
session.hook.pytask_unconfigure(session=session)
sys.exit(session.exit_code)
@hookimpl
def pytask_extend_command_line_interface(cli: click.Group) -> None:
"""Add marker related options."""
cli.add_command(markers)
additional_build_parameters = [
click.Option(
["--strict-markers"],
is_flag=True,
help="Raise errors for unknown markers.",
default=False,
),
click.Option(
["-m", "marker_expression"],
metavar="MARKER_EXPRESSION",
type=str,
help="Select tasks via marker expressions.",
),
click.Option(
["-k", "expression"],
metavar="EXPRESSION",
type=str,
help="Select tasks via expressions on task ids.",
),
]
for command in ("build", "clean", "collect"):
cli.commands[command].params.extend(additional_build_parameters)
@hookimpl
def pytask_parse_config(config: dict[str, Any]) -> None:
"""Parse marker related options."""
MARK_GEN.config = config
@hookimpl
def pytask_post_parse(config: dict[str, Any]) -> None:
config["markers"] = parse_markers(config["markers"])
@define(slots=True)
class KeywordMatcher:
"""A matcher for keywords.
Given a list of names, matches any substring of one of these names. The string
inclusion check is case-insensitive.
Will match on the name of the task, including the names of its parents. Only matches
names of items which are either a :class:`Class` or :class:`Function`.
Additionally, matches on names in the 'extra_keyword_matches' set of any task, as
well as names directly assigned to test functions.
"""
_names: AbstractSet[str]
@classmethod
def from_task(cls, task: PTask) -> KeywordMatcher:
mapped_names = {task.name}
# Add the names attached to the current function through direct assignment.
mapped_names.update(task.function.__dict__)
# Add the markers to the keywords as we no longer handle them correctly.
mapped_names.update(mark.name for mark in task.markers)
return cls(mapped_names)
def __call__(self, subname: str) -> bool:
subname = subname.lower()
names = (name.lower() for name in self._names)
return any(subname in name for name in names)
def select_by_keyword(session: Session, dag: nx.DiGraph) -> set[str] | None:
"""Deselect tests by keywords."""
keywordexpr = session.config["expression"]
if not keywordexpr:
return None
try:
expression = Expression.compile_(keywordexpr)
except ParseError as e:
msg = f"Wrong expression passed to '-k': {keywordexpr}: {e}"
raise ValueError(msg) from None
remaining: set[str] = set()
for task in session.tasks:
if keywordexpr and expression.evaluate(KeywordMatcher.from_task(task)):
remaining.update(task_and_preceding_tasks(task.signature, dag))
return remaining
def select_by_after_keyword(session: Session, after: str) -> set[str]:
"""Select tasks defined by the after keyword."""
try:
expression = Expression.compile_(after)
except ParseError as e:
msg = f"Wrong expression passed to 'after': {after}: {e}"
raise ValueError(msg) from None
ancestors: set[str] = set()
for task in session.tasks:
if after and expression.evaluate(KeywordMatcher.from_task(task)):
ancestors.add(task.signature)
return ancestors
@define(slots=True)
class MarkMatcher:
"""A matcher for markers which are present.
Tries to match on any marker names, attached to the given task.
"""
own_mark_names: set[str]
@classmethod
def from_task(cls, task: PTask) -> MarkMatcher:
mark_names = {mark.name for mark in task.markers}
return cls(mark_names)
def __call__(self, name: str) -> bool:
return name in self.own_mark_names
def select_by_mark(session: Session, dag: nx.DiGraph) -> set[str] | None:
"""Deselect tests by marks."""
matchexpr = session.config["marker_expression"]
if not matchexpr:
return None
try:
expression = Expression.compile_(matchexpr)
except ParseError as e:
msg = f"Wrong expression passed to '-m': {matchexpr}: {e}"
raise ValueError(msg) from None
remaining: set[str] = set()
for task in session.tasks:
if expression.evaluate(MarkMatcher.from_task(task)):
remaining.update(task_and_preceding_tasks(task.signature, dag))
return remaining
def _deselect_others_with_mark(
session: Session, remaining: set[str], mark: Mark
) -> None:
"""Deselect tasks."""
for task in session.tasks:
if task.signature not in remaining:
task.markers.append(mark)
def select_tasks_by_marks_and_expressions(session: Session, dag: nx.DiGraph) -> None:
"""Modify the tasks which are executed with expressions and markers."""
remaining = select_by_keyword(session, dag)
if remaining is not None:
_deselect_others_with_mark(
session, remaining, Mark("skip", (), {"reason": "Deselected by keyword."})
)
remaining = select_by_mark(session, dag)
if remaining is not None:
_deselect_others_with_mark(
session, remaining, Mark("skip", (), {"reason": "Deselected by mark."})
)