|
| 1 | +import inspect |
| 2 | +from collections.abc import Callable |
1 | 3 | from typing import ( |
2 | 4 | Annotated, |
| 5 | + Any, |
3 | 6 | ) |
4 | 7 |
|
5 | 8 | from pydantic import ( |
|
10 | 13 | computed_field, |
11 | 14 | ) |
12 | 15 |
|
| 16 | +from exasol.toolbox.nox.plugin import ( |
| 17 | + METHODS_SPECIFIED_FOR_HOOKS, |
| 18 | + PLUGIN_ATTR_NAME, |
| 19 | +) |
13 | 20 | from exasol.toolbox.util.version import Version |
14 | 21 |
|
15 | 22 |
|
| 23 | +def get_methods_with_hook_implementation( |
| 24 | + plugin_class: type[Any], |
| 25 | +) -> tuple[tuple[str, Callable], ...]: |
| 26 | + """ |
| 27 | + Get all methods from a plugin_class which were specified with a @hookimpl. |
| 28 | + """ |
| 29 | + return tuple( |
| 30 | + (name, method) |
| 31 | + for name, method in inspect.getmembers(plugin_class, inspect.isroutine) |
| 32 | + if hasattr(method, PLUGIN_ATTR_NAME) |
| 33 | + ) |
| 34 | + |
| 35 | + |
| 36 | +def filter_not_specified_methods( |
| 37 | + methods: tuple[tuple[str, Callable], ...], |
| 38 | +) -> tuple[str, ...]: |
| 39 | + """ |
| 40 | + Filter methods which were specified with a @hookimpl but where not specified |
| 41 | + in `exasol.toolbox.nox.plugins.NoxTasks`. |
| 42 | + """ |
| 43 | + return tuple(name for name, _ in methods if name not in METHODS_SPECIFIED_FOR_HOOKS) |
| 44 | + |
| 45 | + |
| 46 | +def validate_plugin_hook(plugin_class: type[Any]): |
| 47 | + """ |
| 48 | + Validate methods in a class for at least one pluggy @hookimpl marker and verifies |
| 49 | + that this method is also specified in `exasol.toolbox.nox.plugins.NoxTasks`. |
| 50 | + """ |
| 51 | + methods_with_hook = get_methods_with_hook_implementation(plugin_class=plugin_class) |
| 52 | + |
| 53 | + if len(methods_with_hook) == 0: |
| 54 | + raise ValueError( |
| 55 | + f"No methods in `{plugin_class.__name__}` were found to be decorated" |
| 56 | + "with `@hookimpl`. The `@hookimpl` decorator indicates that this" |
| 57 | + "will be used with pluggy and used in specific nox sessions." |
| 58 | + "Without it, this class does not modify any nox sessions." |
| 59 | + ) |
| 60 | + |
| 61 | + if not_specified_methods := filter_not_specified_methods(methods_with_hook): |
| 62 | + raise ValueError( |
| 63 | + f"{len(not_specified_methods)} method(s) were " |
| 64 | + "decorated with `@hookimpl`, but these methods were not " |
| 65 | + "specified in `exasol.toolbox.nox.plugins.NoxTasks`: " |
| 66 | + f"{not_specified_methods}. The `@hookimpl` decorator indicates " |
| 67 | + "that these methods will be used by pluggy to modify specific nox sessions." |
| 68 | + "If the method was not previously specified, then no nox sessions will" |
| 69 | + "be modified. The `@hookimpl` is only used by nox sessions provided by the" |
| 70 | + "pyexasol-toolbox and not ones created for just your project." |
| 71 | + ) |
| 72 | + |
| 73 | + return plugin_class |
| 74 | + |
| 75 | + |
16 | 76 | def valid_version_string(version_string: str) -> str: |
17 | 77 | Version.from_string(version_string) |
18 | 78 | return version_string |
19 | 79 |
|
20 | 80 |
|
| 81 | +ValidPluginHook = Annotated[type[Any], AfterValidator(validate_plugin_hook)] |
21 | 82 | ValidVersionStr = Annotated[str, AfterValidator(valid_version_string)] |
22 | 83 |
|
23 | 84 | DEFAULT_EXCLUDED_PATHS = { |
@@ -68,6 +129,16 @@ class BaseConfig(BaseModel): |
68 | 129 | `exasol.toolbox.config.DEFAULT_EXCLUDED_PATHS`. |
69 | 130 | """, |
70 | 131 | ) |
| 132 | + plugins_for_nox_sessions: tuple[ValidPluginHook, ...] = Field( |
| 133 | + default=(), |
| 134 | + description=""" |
| 135 | + This is used to provide hooks to extend one or more of the Nox sessions provided |
| 136 | + by the python-toolbox. As described on the plugins pages: |
| 137 | + - https://exasol.github.io/python-toolbox/main/user_guide/customization.html#plugins |
| 138 | + - https://exasol.github.io/python-toolbox/main/developer_guide/plugins.html, |
| 139 | + possible plugin options are defined in `exasol.toolbox.nox.plugins.NoxTasks`. |
| 140 | + """, |
| 141 | + ) |
71 | 142 | model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) |
72 | 143 |
|
73 | 144 | @computed_field # type: ignore[misc] |
|
0 commit comments