Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ and replaces them with `format:fix` and `format:check`.
## Feature

* #614: Replaced `path_filters` with `BaseConfig.add_to_excluded_python_paths` and `BaseConfig.excluded_python_paths`
* #626: Replaced `plugins` with `BaseConfig.plugins_for_nox_sessions`
2 changes: 1 addition & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Documentation of the Exasol-Toolbox

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

.. grid-item-card:: :octicon:`play` Github Actions
.. grid-item-card:: :octicon:`play` GitHub Actions
:link: github_actions
:link-type: ref

Expand Down
11 changes: 5 additions & 6 deletions doc/user_guide/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,15 @@ Plugin Registration

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.

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

.. code-block:: python

@dataclass(frozen=True)
class Config:
"""Project-specific configuration used by Nox infrastructure."""
# ... other configuration attributes ...
from exasol.toolbox.config import BaseConfig

plugins = [UpdateTemplates] # register the plugin
PROJECT_CONFIG = BaseConfig(
plugins_for_nox_sessions=(UpdateTemplates,), # register the plugin
)

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.

Expand Down
2 changes: 1 addition & 1 deletion doc/user_guide/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ example shown below.

.. note::

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

.. collapse:: noxconfig.py

Expand Down
71 changes: 71 additions & 0 deletions exasol/toolbox/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import inspect
from collections.abc import Callable
from typing import (
Annotated,
Any,
)

from pydantic import (
Expand All @@ -10,14 +13,72 @@
computed_field,
)

from exasol.toolbox.nox.plugin import (
METHODS_SPECIFIED_FOR_HOOKS,
PLUGIN_ATTR_NAME,
)
from exasol.toolbox.util.version import Version


def get_methods_with_hook_implementation(
plugin_class: type[Any],
) -> tuple[tuple[str, Callable], ...]:
"""
Get all methods from a plugin_class which were specified with a @hookimpl.
"""
return tuple(
(name, method)
for name, method in inspect.getmembers(plugin_class, inspect.isroutine)
if hasattr(method, PLUGIN_ATTR_NAME)
)


def filter_not_specified_methods(
methods: tuple[tuple[str, Callable], ...],
) -> tuple[str, ...]:
"""
Filter methods which were specified with a @hookimpl but where not specified
in `exasol.toolbox.nox.plugins.NoxTasks`.
"""
return tuple(name for name, _ in methods if name not in METHODS_SPECIFIED_FOR_HOOKS)


def validate_plugin_hook(plugin_class: type[Any]):
"""
Validate methods in a class for at least one pluggy @hookimpl marker and verifies
that this method is also specified in `exasol.toolbox.nox.plugins.NoxTasks`.
"""
methods_with_hook = get_methods_with_hook_implementation(plugin_class=plugin_class)

if len(methods_with_hook) == 0:
raise ValueError(
f"No methods in `{plugin_class.__name__}` were found to be decorated"
"with `@hookimpl`. The `@hookimpl` decorator indicates that this"
"will be used with pluggy and used in specific nox sessions."
"Without it, this class does not modify any nox sessions."
)

if not_specified_methods := filter_not_specified_methods(methods_with_hook):
raise ValueError(
f"{len(not_specified_methods)} method(s) were "
"decorated with `@hookimpl`, but these methods were not "
"specified in `exasol.toolbox.nox.plugins.NoxTasks`: "
f"{not_specified_methods}. The `@hookimpl` decorator indicates "
"that these methods will be used by pluggy to modify specific nox sessions."
"If the method was not previously specified, then no nox sessions will"
"be modified. The `@hookimpl` is only used by nox sessions provided by the"
"pyexasol-toolbox and not ones created for just your project."
)

return plugin_class


def valid_version_string(version_string: str) -> str:
Version.from_string(version_string)
return version_string


ValidPluginHook = Annotated[type[Any], AfterValidator(validate_plugin_hook)]
ValidVersionStr = Annotated[str, AfterValidator(valid_version_string)]

DEFAULT_EXCLUDED_PATHS = {
Expand Down Expand Up @@ -68,6 +129,16 @@ class BaseConfig(BaseModel):
`exasol.toolbox.config.DEFAULT_EXCLUDED_PATHS`.
""",
)
plugins_for_nox_sessions: tuple[ValidPluginHook, ...] = Field(
default=(),
description="""
This is used to provide hooks to extend one or more of the Nox sessions provided
by the python-toolbox. As described on the plugins pages:
- https://exasol.github.io/python-toolbox/main/user_guide/customization.html#plugins
- https://exasol.github.io/python-toolbox/main/developer_guide/plugins.html,
possible plugin options are defined in `exasol.toolbox.nox.plugins.NoxTasks`.
""",
)
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

@computed_field # type: ignore[misc]
Expand Down
25 changes: 24 additions & 1 deletion exasol/toolbox/nox/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import inspect

import pluggy

_PLUGIN_MARKER = "python-toolbox-nox"
PLUGIN_ATTR_NAME = f"{_PLUGIN_MARKER}_impl"
hookspec = pluggy.HookspecMarker("python-toolbox-nox")
hookimpl = pluggy.HookimplMarker("python-toolbox-nox")

Expand Down Expand Up @@ -105,6 +108,26 @@ def post_integration_tests_hook(self, session, config, context):
def plugin_manager(config) -> pluggy.PluginManager:
pm = pluggy.PluginManager(_PLUGIN_MARKER)
pm.add_hookspecs(NoxTasks)
for plugin in getattr(config, "plugins", []):
plugin_attribute = "plugins_for_nox_sessions"

if not hasattr(config, plugin_attribute):
raise AttributeError(
f"""`{plugin_attribute}` is not defined in the `PROJECT_CONFIG`.
To resolve this, check that the `PROJECT_CONFIG` in the `noxconfig.py`
file is an instance of `exasol.toolbox.config.BaseConfig`. The
`BaseConfig` sets many defaults. If the value for `{plugin_attribute}`
needs to differ in your project and is an input parameter (not
property), you can set it in the PROJECT_CONFIG statement.
"""
)

for plugin in getattr(config, plugin_attribute, ()):
pm.register(plugin())
return pm


METHODS_SPECIFIED_FOR_HOOKS = [
name
for name, method in inspect.getmembers(NoxTasks, inspect.isroutine)
if hasattr(method, f"{_PLUGIN_MARKER}_spec")
]
3 changes: 1 addition & 2 deletions noxconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

from collections.abc import Iterable
from pathlib import Path

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


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