pluginator 0.0.0__py3-none-any.whl
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.
- pluginator/__init__.py +16 -0
- pluginator/actions.py +92 -0
- pluginator/define.py +87 -0
- pluginator/pytest.py +339 -0
- pluginator/utils.py +8 -0
- pluginator-0.0.0.dist-info/METADATA +53 -0
- pluginator-0.0.0.dist-info/RECORD +9 -0
- pluginator-0.0.0.dist-info/WHEEL +5 -0
- pluginator-0.0.0.dist-info/top_level.txt +1 -0
pluginator/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from . import define
|
|
2
|
+
from .actions import Action, ActionContext
|
|
3
|
+
from .pytest import (
|
|
4
|
+
CommandLine,
|
|
5
|
+
install_pytest_plugins,
|
|
6
|
+
)
|
|
7
|
+
from .utils import call_context
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"define",
|
|
11
|
+
"Action",
|
|
12
|
+
"ActionContext",
|
|
13
|
+
"CommandLine",
|
|
14
|
+
"call_context",
|
|
15
|
+
"install_pytest_plugins",
|
|
16
|
+
]
|
pluginator/actions.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ActionContext:
|
|
6
|
+
def update(self, **kwargs):
|
|
7
|
+
for key, value in kwargs.items():
|
|
8
|
+
if not hasattr(self, key):
|
|
9
|
+
raise AttributeError(
|
|
10
|
+
f'Action context:{self.__class__.__name__} not contain attribute:"{key}"',
|
|
11
|
+
)
|
|
12
|
+
setattr(self, key, value)
|
|
13
|
+
|
|
14
|
+
def validate(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Action:
|
|
19
|
+
def __init__(self, name: str, module: str, *, enable: bool = True, default_config: dict | None = None):
|
|
20
|
+
self._name = name
|
|
21
|
+
self._module = module
|
|
22
|
+
self._enable = enable
|
|
23
|
+
self._config = default_config or {}
|
|
24
|
+
self._func = None
|
|
25
|
+
self._setup = None
|
|
26
|
+
|
|
27
|
+
def __call__(self, context: ActionContext) -> t.Any:
|
|
28
|
+
if self._enable:
|
|
29
|
+
context.validate()
|
|
30
|
+
return self._func(config=self._config, context=context)
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self._name
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def enabled(self) -> bool:
|
|
39
|
+
return self._enable
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def setup(self) -> t.Callable | None:
|
|
43
|
+
return self._setup
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def config(self) -> dict | None:
|
|
47
|
+
return self._config
|
|
48
|
+
|
|
49
|
+
def configure(self, action_config: dict):
|
|
50
|
+
if self._func:
|
|
51
|
+
raise RuntimeError("Action already initialized")
|
|
52
|
+
|
|
53
|
+
self._enable = action_config.get("enable", self._enable)
|
|
54
|
+
module = action_config.get("module", self._module)
|
|
55
|
+
|
|
56
|
+
self._config = {k: action_config.get("config", {}).get(k, v) for k, v in self._config.items()}
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
self._module = importlib.import_module(module)
|
|
60
|
+
except ImportError as e:
|
|
61
|
+
raise ImportError(f'Failed to import module "{module}": {e}') from e
|
|
62
|
+
|
|
63
|
+
func = getattr(self._module, "main", None)
|
|
64
|
+
if func is None:
|
|
65
|
+
raise AttributeError(f'Module "{self._module}" does not define a "main" func attribute')
|
|
66
|
+
|
|
67
|
+
if not isinstance(func, t.Callable):
|
|
68
|
+
raise TypeError(f'"{func}" should be a callable object, not {type(func)}')
|
|
69
|
+
|
|
70
|
+
self._func = func
|
|
71
|
+
|
|
72
|
+
self._setup = getattr(self._module, "setup", None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ActionManager:
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self._actions: dict[str, Action] = {}
|
|
78
|
+
|
|
79
|
+
def add_action(self, action: Action, plugin_config: dict | None = None) -> None:
|
|
80
|
+
if action.name in self._actions:
|
|
81
|
+
raise RuntimeError("Plugin action already exist")
|
|
82
|
+
|
|
83
|
+
action.configure(plugin_config.get("actions", {}).get(action.name, {}))
|
|
84
|
+
if action.enabled and action.setup:
|
|
85
|
+
action.setup(action.config)
|
|
86
|
+
|
|
87
|
+
self._actions[action.name] = action
|
|
88
|
+
|
|
89
|
+
def __call__(self, name: str, context: ActionContext) -> t.Any:
|
|
90
|
+
if action := self._actions.get(name):
|
|
91
|
+
return action(context)
|
|
92
|
+
raise RuntimeError(f'Plugin action "{name}" not found')
|
pluginator/define.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from .actions import Action
|
|
4
|
+
from .pytest import (
|
|
5
|
+
BasePlugin,
|
|
6
|
+
BasePluginMeta,
|
|
7
|
+
CommandLine,
|
|
8
|
+
PluginMeta,
|
|
9
|
+
PluginOption,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plugin(
|
|
14
|
+
name: str,
|
|
15
|
+
/,
|
|
16
|
+
*,
|
|
17
|
+
config: str | None = None,
|
|
18
|
+
default_config: dict | None = None,
|
|
19
|
+
deps: list[str] | None = None,
|
|
20
|
+
actions: list[Action] | None = None,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Decorator for creating a plugin class.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name: plugin name.
|
|
27
|
+
config: yaml config file path.
|
|
28
|
+
deps: list of dependencies names.
|
|
29
|
+
default_config: configuration by default.
|
|
30
|
+
actions: list of actions.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Decorator function that returns new plugin class.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def wrapper(cls: type) -> type[BasePlugin]:
|
|
37
|
+
meta = PluginMeta(
|
|
38
|
+
name=name, actions=actions, config_file=config, default_config=default_config, dependencies=deps
|
|
39
|
+
)
|
|
40
|
+
bases = (cls,) if BasePlugin in cls.__mro__ else (cls, BasePlugin)
|
|
41
|
+
|
|
42
|
+
return BasePluginMeta(cls.__name__, bases, {"__meta__": meta})
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def option(
|
|
48
|
+
opt_type: type,
|
|
49
|
+
/,
|
|
50
|
+
*,
|
|
51
|
+
strict: bool = True,
|
|
52
|
+
nullable: bool = False,
|
|
53
|
+
required: bool = False,
|
|
54
|
+
env_var: str | None = None,
|
|
55
|
+
default_from: str | None = None,
|
|
56
|
+
plugin_config_key: str | None = None,
|
|
57
|
+
command_line: CommandLine | None = None,
|
|
58
|
+
hook: t.Callable[[t.Any], t.Any] | None = None,
|
|
59
|
+
) -> PluginOption:
|
|
60
|
+
"""
|
|
61
|
+
Create a plugin option.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
opt_type: option type.
|
|
65
|
+
strict: strict type for type casting.
|
|
66
|
+
nullable: if set True value can be None
|
|
67
|
+
required: flag for required option.
|
|
68
|
+
env_var: environment variable name.
|
|
69
|
+
default_from: name of plugin class property for getting default value.
|
|
70
|
+
plugin_config_key: name of plugin config key for getting value.
|
|
71
|
+
command_line: command line object.
|
|
72
|
+
hook: hook function for prepare option value.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
New plugin option object.
|
|
76
|
+
"""
|
|
77
|
+
return PluginOption(
|
|
78
|
+
opt_type,
|
|
79
|
+
hook=hook,
|
|
80
|
+
strict=strict,
|
|
81
|
+
env_var=env_var,
|
|
82
|
+
nullable=nullable,
|
|
83
|
+
required=required,
|
|
84
|
+
command_line=command_line,
|
|
85
|
+
default_from=default_from,
|
|
86
|
+
plugin_config_key=plugin_config_key,
|
|
87
|
+
)
|
pluginator/pytest.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typing as t
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from _pytest.config import Config
|
|
9
|
+
from _pytest.config.argparsing import OptionGroup, Parser
|
|
10
|
+
from _pytest.main import Session
|
|
11
|
+
from qools.funcutils import called_once
|
|
12
|
+
|
|
13
|
+
from .actions import (
|
|
14
|
+
Action,
|
|
15
|
+
ActionContext,
|
|
16
|
+
ActionManager,
|
|
17
|
+
)
|
|
18
|
+
from .utils import call_context
|
|
19
|
+
|
|
20
|
+
PLUGINATOR_OPTIONS_ATTRIBUTE = "__pluginator_options__"
|
|
21
|
+
EXCLUDED_TYPES = (list, tuple, set, frozenset)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CommandLine:
|
|
25
|
+
"""
|
|
26
|
+
Define a command line settings for pytest.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, opt: str, /, *args, **kwargs):
|
|
30
|
+
self.opt = opt
|
|
31
|
+
|
|
32
|
+
self.args = args
|
|
33
|
+
self.kwargs = kwargs
|
|
34
|
+
|
|
35
|
+
def register_once(self, opt_type: type, parser: Parser, *, group: OptionGroup | None = None):
|
|
36
|
+
"""
|
|
37
|
+
Register the option once in pytest option parser.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
opt_type: The type of the option.
|
|
41
|
+
parser: The option parser instance.
|
|
42
|
+
group: The option group instance.
|
|
43
|
+
"""
|
|
44
|
+
pluginator_options = getattr(parser, PLUGINATOR_OPTIONS_ATTRIBUTE, [])
|
|
45
|
+
|
|
46
|
+
if self.opt not in pluginator_options:
|
|
47
|
+
if "group" in self.kwargs:
|
|
48
|
+
group = parser.getgroup(self.kwargs.pop("group"))
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
"type" not in self.kwargs
|
|
52
|
+
and self.kwargs.get("action") not in ("store_true", "store_false")
|
|
53
|
+
and opt_type not in EXCLUDED_TYPES
|
|
54
|
+
):
|
|
55
|
+
self.kwargs["type"] = opt_type
|
|
56
|
+
|
|
57
|
+
(group or parser).addoption(self.opt, *self.args, **self.kwargs)
|
|
58
|
+
pluginator_options.append(self.opt)
|
|
59
|
+
|
|
60
|
+
setattr(parser, PLUGINATOR_OPTIONS_ATTRIBUTE, pluginator_options)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PluginOption:
|
|
64
|
+
"""
|
|
65
|
+
A plugin option.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# pylint: disable=used-before-assignment
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
opt_type: type,
|
|
72
|
+
/,
|
|
73
|
+
*,
|
|
74
|
+
nullable: bool = False,
|
|
75
|
+
required: bool = False,
|
|
76
|
+
env_var: str | None = None,
|
|
77
|
+
default_from: str | None = None,
|
|
78
|
+
plugin_config_key: str | None = None,
|
|
79
|
+
command_line: CommandLine | None = None,
|
|
80
|
+
hook: t.Callable[[t.Any], t.Any] | None = None,
|
|
81
|
+
strict: bool = True,
|
|
82
|
+
):
|
|
83
|
+
self._type = opt_type
|
|
84
|
+
|
|
85
|
+
self._env_var = env_var
|
|
86
|
+
self._nullable = nullable
|
|
87
|
+
self._required = required
|
|
88
|
+
self._default_from = default_from
|
|
89
|
+
self._command_line = command_line
|
|
90
|
+
self._hook = hook
|
|
91
|
+
self._plugin_config_key = plugin_config_key
|
|
92
|
+
self._strict = strict
|
|
93
|
+
|
|
94
|
+
self._name: str | None = None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def type(self):
|
|
98
|
+
return self._type
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def command_line(self):
|
|
102
|
+
return self._command_line
|
|
103
|
+
|
|
104
|
+
def __set_name__(self, _, name: str):
|
|
105
|
+
self._name = name
|
|
106
|
+
|
|
107
|
+
def _prepare_value(self, value: t.Any) -> t.Any:
|
|
108
|
+
if callable(self._hook):
|
|
109
|
+
return self._type(self._hook(value)) if self._strict else self._hook(value)
|
|
110
|
+
return self._type(value) if self._strict else value
|
|
111
|
+
|
|
112
|
+
def __get__(self, instance: "BasePlugin", _):
|
|
113
|
+
if instance is None:
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
if self._plugin_config_key is not None:
|
|
117
|
+
plugin_config_value = instance.plugin_config.get(self._plugin_config_key, object)
|
|
118
|
+
|
|
119
|
+
if plugin_config_value is not object:
|
|
120
|
+
return self._prepare_value(plugin_config_value)
|
|
121
|
+
|
|
122
|
+
if self._env_var is not None:
|
|
123
|
+
env_var_value = os.getenv(self._env_var)
|
|
124
|
+
|
|
125
|
+
if env_var_value is not None:
|
|
126
|
+
return self._prepare_value(env_var_value)
|
|
127
|
+
|
|
128
|
+
if self._command_line is not None:
|
|
129
|
+
cli_opt_value = instance.pytest_config.getoption(self._command_line.opt)
|
|
130
|
+
|
|
131
|
+
if cli_opt_value is not None:
|
|
132
|
+
return self._prepare_value(cli_opt_value)
|
|
133
|
+
|
|
134
|
+
if self._default_from is not None:
|
|
135
|
+
default_from_value = getattr(instance, self._default_from)
|
|
136
|
+
|
|
137
|
+
if default_from_value is not None:
|
|
138
|
+
return self._prepare_value(default_from_value)
|
|
139
|
+
|
|
140
|
+
if self._required:
|
|
141
|
+
raise ValueError(f'{instance.__class__}: option "{self._name}" is required')
|
|
142
|
+
|
|
143
|
+
return None if self._nullable else self._type()
|
|
144
|
+
|
|
145
|
+
def init_command_line(self, parser: Parser, *, group: OptionGroup | None = None):
|
|
146
|
+
"""
|
|
147
|
+
Init command line settings for the option.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
parser: The option parser instance.
|
|
151
|
+
group: The option group instance.
|
|
152
|
+
"""
|
|
153
|
+
if self._command_line is not None:
|
|
154
|
+
self._command_line.register_once(self._type, parser, group=group)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class PluginMeta:
|
|
159
|
+
"""
|
|
160
|
+
Plugin meta information.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
name: str
|
|
164
|
+
actions: list[Action] = field(kw_only=True, default=None)
|
|
165
|
+
config_file: str | None = field(kw_only=True, default=None)
|
|
166
|
+
default_config: dict | None = field(kw_only=True, default=None)
|
|
167
|
+
dependencies: t.Iterable[str] | None = field(kw_only=True, default=None)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class BasePluginMeta(type):
|
|
171
|
+
def __new__(mcs, name, bases, attrs):
|
|
172
|
+
cls = type.__new__(mcs, name, bases, attrs)
|
|
173
|
+
|
|
174
|
+
if hasattr(cls, "pytest_addoption"):
|
|
175
|
+
cls.pytest_addoption = called_once(cls.pytest_addoption)
|
|
176
|
+
|
|
177
|
+
return cls
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class BasePlugin(metaclass=BasePluginMeta):
|
|
181
|
+
"""
|
|
182
|
+
Base plugin class.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
__meta__: PluginMeta
|
|
186
|
+
|
|
187
|
+
def __init__(self):
|
|
188
|
+
assert hasattr(self, "__meta__"), f'meta object does not defined for "{self.__class__.__name__}"'
|
|
189
|
+
|
|
190
|
+
self.__pytest_config: Config | None = None
|
|
191
|
+
self._actions: ActionManager = ActionManager()
|
|
192
|
+
|
|
193
|
+
if self.__meta__.actions is not None:
|
|
194
|
+
for action in self.__meta__.actions:
|
|
195
|
+
self._actions.add_action(action, self.plugin_config)
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
return f"<{self.__class__.__name__}: {self.meta.name}>"
|
|
199
|
+
|
|
200
|
+
def action(self, name: str, context: ActionContext, *, lazy: bool = False):
|
|
201
|
+
if lazy:
|
|
202
|
+
|
|
203
|
+
def wrapper(**kwargs):
|
|
204
|
+
ctx = deepcopy(context)
|
|
205
|
+
ctx.update(**kwargs)
|
|
206
|
+
return self._actions(name, ctx)
|
|
207
|
+
|
|
208
|
+
return wrapper
|
|
209
|
+
return self._actions(name, context)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def meta(self) -> PluginMeta:
|
|
213
|
+
return self.__meta__
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def pytest_config(self) -> Config:
|
|
217
|
+
if not self.__pytest_config:
|
|
218
|
+
raise AssertionError(
|
|
219
|
+
f'Config is not initialized yet in "{self.__class__.__name__}"',
|
|
220
|
+
)
|
|
221
|
+
return self.__pytest_config
|
|
222
|
+
|
|
223
|
+
@cached_property
|
|
224
|
+
def plugin_config(self) -> dict:
|
|
225
|
+
if self.meta.config_file is None:
|
|
226
|
+
return {}
|
|
227
|
+
|
|
228
|
+
default_config = self.meta.default_config or {}
|
|
229
|
+
|
|
230
|
+
if not os.path.exists(self.meta.config_file):
|
|
231
|
+
return default_config
|
|
232
|
+
|
|
233
|
+
with open(self.meta.config_file, encoding="utf-8") as f:
|
|
234
|
+
config_data = yaml.full_load(f)
|
|
235
|
+
|
|
236
|
+
if isinstance(config_data, dict):
|
|
237
|
+
return default_config | config_data
|
|
238
|
+
|
|
239
|
+
return default_config
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def plugin_options(self) -> t.Iterable[PluginOption]:
|
|
243
|
+
for attr in dir(self.__class__):
|
|
244
|
+
value = getattr(self.__class__, attr, None)
|
|
245
|
+
|
|
246
|
+
if isinstance(value, PluginOption):
|
|
247
|
+
yield value
|
|
248
|
+
|
|
249
|
+
def init_plugin_options(self, parser: Parser) -> None:
|
|
250
|
+
group = parser.getgroup(self.meta.name)
|
|
251
|
+
|
|
252
|
+
for plugin_option in self.plugin_options:
|
|
253
|
+
plugin_option.init_command_line(parser, group=group)
|
|
254
|
+
|
|
255
|
+
def init_pytest_config(self, config: Config) -> None:
|
|
256
|
+
self.__pytest_config = config
|
|
257
|
+
|
|
258
|
+
def install(self):
|
|
259
|
+
if not self.__pytest_config:
|
|
260
|
+
raise AssertionError(
|
|
261
|
+
f'Plugin "{self.__class__.__name__}" is not ready to install, '
|
|
262
|
+
f'please call to "init_options" and "init_config" before that',
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
name = self.__pytest_config.pluginmanager.register(self, name=self.meta.name)
|
|
266
|
+
self.__pytest_config.add_cleanup(lambda: self.__pytest_config.pluginmanager.unregister(name=name))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def install_pytest_plugins(
|
|
270
|
+
*plugins,
|
|
271
|
+
check_deps: bool = True, # TODO: remove flag for new minor version
|
|
272
|
+
context: dict[str, t.Any] | None = None,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Install pytest plugins in context.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
plugins: The plugins to install.
|
|
279
|
+
check_deps: Check dependencies if True else no check.
|
|
280
|
+
context: The context to install plugins in.
|
|
281
|
+
"""
|
|
282
|
+
context = call_context() if context is None else context
|
|
283
|
+
|
|
284
|
+
ctx_pytest_addoption = context.get("pytest_addoption", lambda parser: None)
|
|
285
|
+
ctx_pytest_configure = context.get("pytest_configure", lambda config: None)
|
|
286
|
+
ctx_pytest_collection_finish = context.get("pytest_collection_finish", lambda session: None)
|
|
287
|
+
|
|
288
|
+
failed_deps = []
|
|
289
|
+
plugin_name_to_failed_deps = {}
|
|
290
|
+
|
|
291
|
+
def pytest_addoption(parser: Parser):
|
|
292
|
+
ctx_pytest_addoption(parser)
|
|
293
|
+
|
|
294
|
+
for plugin in plugins:
|
|
295
|
+
plugin.init_plugin_options(parser)
|
|
296
|
+
|
|
297
|
+
def pytest_configure(config: Config):
|
|
298
|
+
ctx_pytest_configure(config)
|
|
299
|
+
|
|
300
|
+
for plugin in plugins:
|
|
301
|
+
plugin.init_pytest_config(config)
|
|
302
|
+
plugin.install()
|
|
303
|
+
|
|
304
|
+
configure_callback = getattr(plugin, "configure", None)
|
|
305
|
+
|
|
306
|
+
if configure_callback is not None:
|
|
307
|
+
configure_callback()
|
|
308
|
+
|
|
309
|
+
def pytest_collection_finish(session: Session):
|
|
310
|
+
ctx_pytest_collection_finish(session)
|
|
311
|
+
|
|
312
|
+
if check_deps:
|
|
313
|
+
for plugin in plugins:
|
|
314
|
+
for dep_name in plugin.__meta__.dependencies or []:
|
|
315
|
+
if not session.config.pluginmanager.get_plugin(dep_name):
|
|
316
|
+
failed_deps.append(dep_name)
|
|
317
|
+
|
|
318
|
+
if failed_deps:
|
|
319
|
+
plugin_name_to_failed_deps[plugin.meta.name] = failed_deps
|
|
320
|
+
|
|
321
|
+
if plugin_name_to_failed_deps:
|
|
322
|
+
fails = plugin_name_to_failed_deps.items()
|
|
323
|
+
message = 'Plugin "{}" needs "{}" dependencies, but does not installed'
|
|
324
|
+
|
|
325
|
+
raise AssertionError(*map(lambda i: message.format(i[0], ", ".join(i[1])), fails))
|
|
326
|
+
|
|
327
|
+
context["pytest_addoption"] = pytest_addoption
|
|
328
|
+
context["pytest_configure"] = pytest_configure
|
|
329
|
+
context["pytest_collection_finish"] = pytest_collection_finish
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
__all__ = [
|
|
333
|
+
"BasePlugin",
|
|
334
|
+
"PluginMeta",
|
|
335
|
+
"CommandLine",
|
|
336
|
+
"PluginOption",
|
|
337
|
+
"BasePluginMeta",
|
|
338
|
+
"install_pytest_plugins",
|
|
339
|
+
]
|
pluginator/utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pluginator
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Plugin management system
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: qools>=0.0.0
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-mock>=3.0; extra == "test"
|
|
21
|
+
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
22
|
+
Requires-Dist: pyhamcrest>=2.0; extra == "test"
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
25
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
27
|
+
|
|
28
|
+
# pluginator
|
|
29
|
+
|
|
30
|
+
Plugin management system
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pluginator
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pluginator import define
|
|
42
|
+
|
|
43
|
+
@define.plugin('my-plugin', config='config.yml')
|
|
44
|
+
class MyPlugin:
|
|
45
|
+
name = define.option(str, required=True, env_var='PLUGIN_NAME')
|
|
46
|
+
|
|
47
|
+
def configure(self):
|
|
48
|
+
print(f'Configured: {self.name}')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
Full documentation: https://qarium.github.io/pluginator/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pluginator/__init__.py,sha256=CMJv2uG2SZbbhyUwaFuAVtysBSxaqIc14_vo23xkQSo,298
|
|
2
|
+
pluginator/actions.py,sha256=ReiEwnnLYBz-RMUc7oNS4UZ3Ck9rtqLE5ATfMXAA_00,2865
|
|
3
|
+
pluginator/define.py,sha256=mweHrj9WDovj3fS_WUE9mlDB3XgMh-RsAqCj1Rpja9s,2253
|
|
4
|
+
pluginator/pytest.py,sha256=4PNurKW1kL69rWohv17ksShuRFd70aGdtAUS20pAlI0,10434
|
|
5
|
+
pluginator/utils.py,sha256=skuT5HKY6fSNiczD0GYbWmDJexmGQ2fsROhOgSEuYdA,164
|
|
6
|
+
pluginator-0.0.0.dist-info/METADATA,sha256=UU2RZukmKNtnZz541GOn3mRQJfnrernI8kEvKPDfcjY,1424
|
|
7
|
+
pluginator-0.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
pluginator-0.0.0.dist-info/top_level.txt,sha256=s-5LW15-ww9J184xm1Nq3gYSg3g0-0jj0KpnZuqB4dI,11
|
|
9
|
+
pluginator-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pluginator
|