Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add force_not_a_hook() and unit test #447

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion src/pluggy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"HookspecMarker",
"HookimplMarker",
"Result",
"force_not_a_hook",
]

from ._manager import PluginManager, PluginValidationError
from ._manager import PluginManager, PluginValidationError, force_not_a_hook
from ._result import HookCallError, Result
from ._hooks import (
HookspecMarker,
Expand Down
47 changes: 46 additions & 1 deletion src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Iterable
from typing import Mapping
from typing import Sequence
from typing import TypeVar

from . import _tracing
from ._callers import _multicall
Expand Down Expand Up @@ -72,6 +73,32 @@
return sorted(dir(self._dist) + ["_dist", "project_name"])


_T = TypeVar("_T")


_pluggy_hide_attr_name = "_pluggy_hide_mark"


def force_not_a_hook(obj: _T) -> _T:
"""
Use this to mark a function or method as *hidden* from discovery as a plugin hook.

This is useful in rare cases. Use it where hooks are discovered by prefix name but some objects exist with
matching names which are _not_ to be considered as hook implementations.

e.g. When using _pytest_, use this marker to mark a function that has a name starting with 'pytest_' but which
is not intended to be picked up as a hook implementation.

>>> @force_not_a_hook

Check warning on line 92 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L92

Added line #L92 was not covered by tests
... def pytest_some_function(arg1):
... # For some reason, we needed to name this function with `pytest_` prefix, but we don't want it treated as a
... # hook
"""

Check warning on line 96 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L94-L96

Added lines #L94 - L96 were not covered by tests
assert not hasattr(obj, _pluggy_hide_attr_name)
setattr(obj, _pluggy_hide_attr_name, True)
return obj


class PluginManager:
"""Core class which manages registration of plugin objects and 1:N hook
calling.
Expand Down Expand Up @@ -148,7 +175,7 @@
self._name2plugin[plugin_name] = plugin

# register matching hook implementations of the plugin
for name in dir(plugin):
for name in self._find_plugin_attrs(plugin):
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
if hookimpl_opts is not None:
normalize_hookimpl_opts(hookimpl_opts)
Expand All @@ -165,6 +192,24 @@
hook._add_hookimpl(hookimpl)
return plugin_name

def _find_plugin_attrs(self, plugin: _Namespace) -> Iterable[str]:
"""
Override this method to customize the way we select the attribute names from an object to inspect as potential
hook implementations.

The results from this method will run through `parse_hookimpl_opts` which may do additional filtering.
"""
for name in dir(plugin): # , method in inspect.getmembers(plugin):
try:
method = getattr(plugin, name, None)
pluggy_hide_mark = getattr(method, _pluggy_hide_attr_name, None)
except Exception:
# Ignore all kinds of exceptions. Pluggy has to be very exception-tolerant during plugin registration
continue

Check warning on line 209 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L209

Added line #L209 was not covered by tests
if pluggy_hide_mark is not True:
yield name

def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None:
"""Try to obtain a hook implementation from an item with the given name
in the given plugin which is being searched for hook impls.
Expand Down
21 changes: 21 additions & 0 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pytest

from pluggy import force_not_a_hook
from pluggy import HookCallError
from pluggy import HookimplMarker
from pluggy import HookspecMarker
Expand Down Expand Up @@ -211,6 +212,26 @@
assert len(hookcallers) == 1


def test_register_force_not_hook(pm: PluginManager) -> None:
class Hooks:
@hookspec
def he_method1(self):
pass

Check warning on line 219 in testing/test_pluginmanager.py

View check run for this annotation

Codecov / codecov/patch

testing/test_pluginmanager.py#L219

Added line #L219 was not covered by tests

pm.add_hookspecs(Hooks)

class Plugin:
@force_not_a_hook
@hookimpl
def he_method1(self):
return 1

Check warning on line 227 in testing/test_pluginmanager.py

View check run for this annotation

Codecov / codecov/patch

testing/test_pluginmanager.py#L227

Added line #L227 was not covered by tests

pm.register(Plugin())
hc = pm.hook
out = hc.he_method1()
assert out == []


def test_register_historic(pm: PluginManager) -> None:
class Hooks:
@hookspec(historic=True)
Expand Down