Skip to content

Commit e85cd39

Browse files
committed
Move plugins to BaseConfig.plugins_for_nox_sessions and add tests
1 parent ec1846a commit e85cd39

File tree

8 files changed

+120
-11
lines changed

8 files changed

+120
-11
lines changed

doc/changes/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ and replaces them with `format:fix` and `format:check`.
1010
## Feature
1111

1212
* #614: Replaced `path_filters` with `BaseConfig.add_to_excluded_python_paths` and `BaseConfig.excluded_python_paths`
13+
* #626: Replaced `plugins` with `BaseConfig.plugins_for_nox_sessions`

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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import inspect
12
from typing import (
23
Annotated,
4+
Any,
35
)
46

57
from pydantic import (
@@ -10,14 +12,50 @@
1012
computed_field,
1113
)
1214

15+
from exasol.toolbox.nox.plugin import (
16+
METHODS_SPECIFIED_FOR_HOOKS,
17+
PLUGIN_ATTR_NAME,
18+
)
1319
from exasol.toolbox.util.version import Version
1420

1521

22+
def validate_plugin_hook(plugin_class: type[Any]):
23+
"""
24+
Checks methods in a class for at least one specific pluggy @hookimpl marker
25+
and verifies that this method is also specified in
26+
`exasol.toolbox.nox.plugins.NoxTasks`.
27+
"""
28+
has_hook_implementation = False
29+
not_specified_decorated_methods = []
30+
for name, method in inspect.getmembers(plugin_class, inspect.isroutine):
31+
if hasattr(method, PLUGIN_ATTR_NAME):
32+
has_hook_implementation = True
33+
if name not in METHODS_SPECIFIED_FOR_HOOKS:
34+
not_specified_decorated_methods.append(name)
35+
36+
if not has_hook_implementation:
37+
raise ValueError(
38+
f"No methods in `{plugin_class.__name__}` were found to be decorated"
39+
"with `@hookimpl`"
40+
)
41+
42+
if not_specified_decorated_methods:
43+
raise ValueError(
44+
f"{len(not_specified_decorated_methods)} method(s) were "
45+
"decorated with `@hookimpl`, but these methods were not "
46+
"specified in `exasol.toolbox.nox.plugins.NoxTasks`: "
47+
f"{not_specified_decorated_methods}"
48+
)
49+
50+
return plugin_class
51+
52+
1653
def valid_version_string(version_string: str) -> str:
1754
Version.from_string(version_string)
1855
return version_string
1956

2057

58+
ValidPluginHook = Annotated[type[object], AfterValidator(validate_plugin_hook)]
2159
ValidVersionStr = Annotated[str, AfterValidator(valid_version_string)]
2260

2361
DEFAULT_EXCLUDED_PATHS = {
@@ -68,6 +106,16 @@ class BaseConfig(BaseModel):
68106
`exasol.toolbox.config.DEFAULT_EXCLUDED_PATHS`.
69107
""",
70108
)
109+
plugins_for_nox_sessions: tuple[ValidPluginHook, ...] = Field(
110+
default=(),
111+
description="""
112+
This is used to provide hooks to extend one or more of the Nox sessions provided
113+
by the python-toolbox. As described on the plugins pages:
114+
- https://exasol.github.io/python-toolbox/main/user_guide/customization.html#plugins
115+
- https://exasol.github.io/python-toolbox/main/developer_guide/plugins.html,
116+
possible plugin options are defined in `exasol.toolbox.nox.plugins.NoxTasks`.
117+
""",
118+
)
71119
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
72120

73121
@computed_field # type: ignore[misc]

exasol/toolbox/nox/plugin.py

Lines changed: 22 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,24 @@ 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+
"in the noxconfig.py file, the class Config should inherit "
116+
"from `exasol.toolbox.config.BaseConfig`. This is used to "
117+
f"set the default `{plugin_attribute}`. If the allowed "
118+
f"`{plugin_attribute} needs to differ in your project and is an "
119+
"input parameter (not property), you can set it in the PROJECT_CONFIG statement."
120+
)
121+
122+
for plugin in getattr(config, plugin_attribute, ()):
109123
pm.register(plugin())
110124
return pm
125+
126+
127+
METHODS_SPECIFIED_FOR_HOOKS = [
128+
name
129+
for name, method in inspect.getmembers(NoxTasks, inspect.isroutine)
130+
if hasattr(method, f"{_PLUGIN_MARKER}_spec")
131+
]

noxconfig.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

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

87
from exasol.toolbox.config import BaseConfig
@@ -56,7 +55,6 @@ class Config(BaseConfig):
5655
source: Path = Path("exasol/toolbox")
5756
importlinter: Path = Path(__file__).parent / ".import_linter_config"
5857
version_file: Path = Path(__file__).parent / "exasol" / "toolbox" / "version.py"
59-
plugins: Iterable[object] = (UpdateTemplates,)
6058

6159

6260
PROJECT_CONFIG = Config(
@@ -73,4 +71,5 @@ class Config(BaseConfig):
7371
# The PTB does not have integration tests run with an Exasol DB,
7472
# so for running in the CI, we take the first element.
7573
exasol_versions=(BaseConfig().exasol_versions[0],),
74+
plugins_for_nox_sessions=(UpdateTemplates,),
7675
)

project-template/{{cookiecutter.repo_name}}/noxconfig.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@ class Config(BaseConfig):
1616
/ "{{cookiecutter.package_name}}"
1717
/ "version.py"
1818
)
19-
plugins: Iterable[object] = ()
2019

2120
PROJECT_CONFIG = Config()

test/unit/config_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
BaseConfig,
77
valid_version_string,
88
)
9+
from exasol.toolbox.nox.plugin import hookimpl
10+
from exasol.toolbox.util.version import Version
911

1012

1113
class TestBaseConfig:
@@ -86,3 +88,43 @@ def test_pyupgrade_argument(minimum_python_version):
8688
def test_excluded_python_paths(add_to_excluded_python_paths, expected):
8789
conf = BaseConfig(add_to_excluded_python_paths=add_to_excluded_python_paths)
8890
assert sorted(conf.excluded_python_paths) == sorted(expected)
91+
92+
93+
class WithHook:
94+
@hookimpl
95+
def prepare_release_update_version(self, session, config, version: Version) -> None:
96+
print("This is a simple, silly hook.")
97+
98+
99+
class WithNotSpecifiedHook:
100+
@hookimpl
101+
def not_specified_anywhere(self, session, config, version: Version) -> None:
102+
print("This is not a properly prepared hook.")
103+
104+
105+
class WithoutHook:
106+
def prepare_release_update_version(self, session, config, version: Version) -> None:
107+
print("This is not a properly prepared hook.")
108+
109+
110+
class TestPlugins:
111+
@staticmethod
112+
def test_works_when_empty():
113+
BaseConfig(plugins_for_nox_sessions=())
114+
115+
@staticmethod
116+
def test_works_for_hook(capsys):
117+
BaseConfig(plugins_for_nox_sessions=(WithHook,))
118+
119+
@staticmethod
120+
def test_raises_exception_method_with_hook_not_specified():
121+
with pytest.raises(ValidationError) as ex:
122+
BaseConfig(plugins_for_nox_sessions=(WithNotSpecifiedHook,))
123+
assert "1 method(s) were decorated with `@hookimpl`, but" in str(ex.value)
124+
assert "['not_specified_anywhere']" in str(ex.value)
125+
126+
@staticmethod
127+
def test_raises_exception_without_hook():
128+
with pytest.raises(ValidationError) as ex:
129+
BaseConfig(plugins_for_nox_sessions=(WithoutHook,))
130+
assert "No methods in `WithoutHook`" in str(ex.value)

0 commit comments

Comments
 (0)