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 typing information to all of pluggy #326

Closed
Closed
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
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,25 @@ template = "changelog/_template.rst"
directory = "trivial"
name = "Trivial/Internal Changes"
showcontent = true


[tool.mypy]
mypy_path = "src"
check_untyped_defs = true
disallow_any_expr = true
disallow_any_generics = true
disallow_any_unimported = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
ignore_missing_imports = true
implicit_reexport = false
no_implicit_optional = true
show_error_codes = true
strict_equality = true
strict_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dev =
testing =
pytest
pytest-benchmark

mypy
importlib-metadata>=0.12 # keep in sync
[devpi:upload]
formats=sdist.tgz,bdist_wheel
3 changes: 2 additions & 1 deletion src/pluggy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"HookimplMarker",
]

from ._manager import PluginManager, PluginValidationError
from ._errors import PluginValidationError
from ._manager import PluginManager
from ._callers import HookCallError
from ._hooks import HookspecMarker, HookimplMarker
131 changes: 87 additions & 44 deletions src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,101 @@
"""
import sys

from ._result import HookCallError, _Result, _raise_wrapfail
from ._result import (
HookCallError,
_Result,
WrapResult,
_raise_wrapfail,
EXCINFO,
SomeResult,
)
from typing import List, TYPE_CHECKING, Dict, cast, overload, Union, Callable
from typing_extensions import Literal

if TYPE_CHECKING:
from ._hooks import HookImpl

def _multicall(hook_name, hook_impls, caller_kwargs, firstresult):
HookImpls = List["HookImpl"]
HookArgs = Dict[str, object]
HookResultCallback = Callable[[SomeResult], SomeResult]
HookExecCallable = Callable[
[str, List["HookImpl"], Dict[str, object], bool],
Union[SomeResult, List[SomeResult]],
]


@overload
def _multicall(
hook_name: str,
hook_impls: HookImpls,
caller_kwargs: HookArgs,
firstresult: Literal[False],
) -> List[SomeResult]:
pass


@overload
def _multicall(
hook_name: str,
hook_impls: HookImpls,
caller_kwargs: HookArgs,
firstresult: Literal[True],
) -> SomeResult:
pass


def _multicall(
hook_name: str,
hook_impls: HookImpls,
caller_kwargs: HookArgs,
firstresult: bool,
) -> Union[List[SomeResult], SomeResult]:
"""Execute a call into multiple python functions/methods and return the
result(s).

``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
f"hook call must provide argument {argname!r}"
)

if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)

# run all wrapper post-yield blocks
for gen in reversed(teardowns):
excinfo = None, None, None # type: EXCINFO
teardowns: List[WrapResult] = []
try:
for hook_impl in reversed(hook_impls):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
f"hook call must provide argument {argname!r}"
)

if hook_impl.hookwrapper:
try:
gen = cast(WrapResult, hook_impl.function(*args))
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)

# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass

return outcome.get_result()
return outcome.get_result()
26 changes: 26 additions & 0 deletions src/pluggy/_entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import sys
from typing import Optional, List, cast

if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata
else:
import importlib_metadata


class DistFacade:
"""Emulate a pkg_resources Distribution"""

def __init__(self, dist: importlib_metadata.Distribution):
self._dist = dist

@property
def project_name(self) -> str:
return cast(str, self._dist.metadata["name"])

def __getattr__(
self, attr: str, default: Optional[object] = None
) -> Optional[object]:
return cast(Optional[object], getattr(self._dist, attr, default))

def __dir__(self) -> List[str]:
return sorted(dir(self._dist) + list(super().__dir__()))
12 changes: 12 additions & 0 deletions src/pluggy/_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class PluginValidationError(Exception):
"""plugin failed validation.

:param object plugin: the plugin which failed validation,
may be a module or an arbitrary object.
"""

plugin: object

def __init__(self, plugin: object, message: str):
self.plugin = plugin
super(Exception, self).__init__(message)
Loading