Skip to content

Commit c76203e

Browse files
committed
Merge branch 'main' into feature/621_move_paths_into_baseconfig
2 parents 618a644 + a6c4575 commit c76203e

File tree

10 files changed

+312
-83
lines changed

10 files changed

+312
-83
lines changed

doc/changes/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ PROJECT_CONFIG = Config(
8181
## Feature
8282

8383
* #614: Replaced `path_filters` with `BaseConfig.add_to_excluded_python_paths` and `BaseConfig.excluded_python_paths`
84+
* #626: Replaced `plugins` with `BaseConfig.plugins_for_nox_sessions`
8485
* #621: Moved path specifications into `BaseConfig`
8586
* `root` is now `root_path`, which must be specified by the project
8687
* `source` is now covered by `project_name`, which must be specified by the project,

doc/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Documentation of the Exasol-Toolbox
2525

2626
CLI-Tools which are shipped as part of this project.
2727

28-
.. grid-item-card:: :octicon:`play` Github Actions
28+
.. grid-item-card:: :octicon:`play` GitHub Actions
2929
:link: github_actions
3030
:link-type: ref
3131

doc/user_guide/customization.rst

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,15 @@ Plugin Registration
6565

6666
Once the plugin class has been defined, it must be registered in the Nox configuration. This is done by adding the class to the `plugins` list within the `Config` data class.
6767

68-
In the Nox `Config` data class, you should amend the `plugins` list to include the new plugin:
68+
In the Nox `PROJECT_CONFIG`, you should amend the `plugins_for_nox_sessions` tuple to include the new plugin:
6969

7070
.. code-block:: python
7171
72-
@dataclass(frozen=True)
73-
class Config:
74-
"""Project-specific configuration used by Nox infrastructure."""
75-
# ... other configuration attributes ...
72+
from exasol.toolbox.config import BaseConfig
7673
77-
plugins = [UpdateTemplates] # register the plugin
74+
PROJECT_CONFIG = BaseConfig(
75+
plugins_for_nox_sessions=(UpdateTemplates,), # register the plugin
76+
)
7877
7978
When Nox runs, it will instantiate `UpdateTemplates` with no arguments and integrate the hooks defined by the plugin into the execution lifecycle. All registered plugins’ hooks are called at their designated points in the Nox workflow.
8079

doc/user_guide/getting_started.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ example shown below.
9191

9292
.. note::
9393

94-
For further details on plugins, see the customization section.
94+
For further details on plugins, see :ref:`plugins` in the Customization section.
9595

9696
.. collapse:: noxconfig.py
9797

exasol/toolbox/config.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import inspect
2+
from collections.abc import Callable
13
from pathlib import Path
24
from typing import (
35
Annotated,
6+
Any,
47
)
58

69
from pydantic import (
@@ -11,14 +14,72 @@
1114
computed_field,
1215
)
1316

17+
from exasol.toolbox.nox.plugin import (
18+
METHODS_SPECIFIED_FOR_HOOKS,
19+
PLUGIN_ATTR_NAME,
20+
)
1421
from exasol.toolbox.util.version import Version
1522

1623

24+
def get_methods_with_hook_implementation(
25+
plugin_class: type[Any],
26+
) -> tuple[tuple[str, Callable], ...]:
27+
"""
28+
Get all methods from a plugin_class which were specified with a @hookimpl.
29+
"""
30+
return tuple(
31+
(name, method)
32+
for name, method in inspect.getmembers(plugin_class, inspect.isroutine)
33+
if hasattr(method, PLUGIN_ATTR_NAME)
34+
)
35+
36+
37+
def filter_not_specified_methods(
38+
methods: tuple[tuple[str, Callable], ...],
39+
) -> tuple[str, ...]:
40+
"""
41+
Filter methods which were specified with a @hookimpl but where not specified
42+
in `exasol.toolbox.nox.plugins.NoxTasks`.
43+
"""
44+
return tuple(name for name, _ in methods if name not in METHODS_SPECIFIED_FOR_HOOKS)
45+
46+
47+
def validate_plugin_hook(plugin_class: type[Any]):
48+
"""
49+
Validate methods in a class for at least one pluggy @hookimpl marker and verifies
50+
that this method is also specified in `exasol.toolbox.nox.plugins.NoxTasks`.
51+
"""
52+
methods_with_hook = get_methods_with_hook_implementation(plugin_class=plugin_class)
53+
54+
if len(methods_with_hook) == 0:
55+
raise ValueError(
56+
f"No methods in `{plugin_class.__name__}` were found to be decorated"
57+
"with `@hookimpl`. The `@hookimpl` decorator indicates that this"
58+
"will be used with pluggy and used in specific nox sessions."
59+
"Without it, this class does not modify any nox sessions."
60+
)
61+
62+
if not_specified_methods := filter_not_specified_methods(methods_with_hook):
63+
raise ValueError(
64+
f"{len(not_specified_methods)} method(s) were "
65+
"decorated with `@hookimpl`, but these methods were not "
66+
"specified in `exasol.toolbox.nox.plugins.NoxTasks`: "
67+
f"{not_specified_methods}. The `@hookimpl` decorator indicates "
68+
"that these methods will be used by pluggy to modify specific nox sessions."
69+
"If the method was not previously specified, then no nox sessions will"
70+
"be modified. The `@hookimpl` is only used by nox sessions provided by the"
71+
"pyexasol-toolbox and not ones created for just your project."
72+
)
73+
74+
return plugin_class
75+
76+
1777
def valid_version_string(version_string: str) -> str:
1878
Version.from_string(version_string)
1979
return version_string
2080

2181

82+
ValidPluginHook = Annotated[type[Any], AfterValidator(validate_plugin_hook)]
2283
ValidVersionStr = Annotated[str, AfterValidator(valid_version_string)]
2384

2485
DEFAULT_EXCLUDED_PATHS = {
@@ -71,6 +132,16 @@ class BaseConfig(BaseModel):
71132
`exasol.toolbox.config.DEFAULT_EXCLUDED_PATHS`.
72133
""",
73134
)
135+
plugins_for_nox_sessions: tuple[ValidPluginHook, ...] = Field(
136+
default=(),
137+
description="""
138+
This is used to provide hooks to extend one or more of the Nox sessions provided
139+
by the python-toolbox. As described on the plugins pages:
140+
- https://exasol.github.io/python-toolbox/main/user_guide/customization.html#plugins
141+
- https://exasol.github.io/python-toolbox/main/developer_guide/plugins.html,
142+
possible plugin options are defined in `exasol.toolbox.nox.plugins.NoxTasks`.
143+
""",
144+
)
74145
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
75146

76147
@computed_field # type: ignore[misc]

exasol/toolbox/nox/plugin.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import inspect
2+
13
import pluggy
24

35
_PLUGIN_MARKER = "python-toolbox-nox"
6+
PLUGIN_ATTR_NAME = f"{_PLUGIN_MARKER}_impl"
47
hookspec = pluggy.HookspecMarker("python-toolbox-nox")
58
hookimpl = pluggy.HookimplMarker("python-toolbox-nox")
69

@@ -105,6 +108,26 @@ def post_integration_tests_hook(self, session, config, context):
105108
def plugin_manager(config) -> pluggy.PluginManager:
106109
pm = pluggy.PluginManager(_PLUGIN_MARKER)
107110
pm.add_hookspecs(NoxTasks)
108-
for plugin in getattr(config, "plugins", []):
111+
plugin_attribute = "plugins_for_nox_sessions"
112+
113+
if not hasattr(config, plugin_attribute):
114+
raise AttributeError(
115+
f"""`{plugin_attribute}` is not defined in the `PROJECT_CONFIG`.
116+
To resolve this, check that the `PROJECT_CONFIG` in the `noxconfig.py`
117+
file is an instance of `exasol.toolbox.config.BaseConfig`. The
118+
`BaseConfig` sets many defaults. If the value for `{plugin_attribute}`
119+
needs to differ in your project and is an input parameter (not
120+
property), you can set it in the PROJECT_CONFIG statement.
121+
"""
122+
)
123+
124+
for plugin in getattr(config, plugin_attribute, ()):
109125
pm.register(plugin())
110126
return pm
127+
128+
129+
METHODS_SPECIFIED_FOR_HOOKS = [
130+
name
131+
for name, method in inspect.getmembers(NoxTasks, inspect.isroutine)
132+
if hasattr(method, f"{_PLUGIN_MARKER}_spec")
133+
]

noxconfig.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from __future__ import annotations
44

5-
from collections.abc import Iterable
65
from pathlib import Path
76

7+
from pydantic import computed_field
8+
89
from exasol.toolbox.config import BaseConfig
910
from exasol.toolbox.nox.plugin import hookimpl
1011
from exasol.toolbox.tools.replace_version import update_github_yml
@@ -52,10 +53,16 @@ def prepare_release_add_files(self, session, config):
5253

5354

5455
class Config(BaseConfig):
55-
"""Project specific configuration used by nox infrastructure"""
56-
57-
importlinter: Path = ROOT_PATH / ".import_linter_config"
58-
plugins: Iterable[object] = (UpdateTemplates,)
56+
@computed_field # type: ignore[misc]
57+
@property
58+
def importlinter(self) -> Path:
59+
"""
60+
Path for the import lint configuration file.
61+
This is an experimental feature that requires further scrutiny. This will
62+
be done in:
63+
https://github.com/exasol/python-toolbox/issues/628
64+
"""
65+
return self.root_path / ".import_linter_config"
5966

6067

6168
PROJECT_CONFIG = Config(
@@ -74,4 +81,5 @@ class Config(BaseConfig):
7481
# The PTB does not have integration tests run with an Exasol DB,
7582
# so for running in the CI, we take the first element.
7683
exasol_versions=("7.1.30",),
84+
plugins_for_nox_sessions=(UpdateTemplates,),
7785
)

0 commit comments

Comments
 (0)