-
-
Notifications
You must be signed in to change notification settings - Fork 661
Fixed rule decorator factory typing to help call-by-name call sites #21987
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
Changes from all commits
f3da27b
1b0889a
615f197
6b443ae
b12bca3
a8efa7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -10,7 +10,17 @@ | |||||
from dataclasses import dataclass | ||||||
from enum import Enum | ||||||
from types import FrameType, ModuleType | ||||||
from typing import Any, Protocol, TypeVar, Union, cast, get_type_hints, overload | ||||||
from typing import ( | ||||||
Any, | ||||||
NotRequired, | ||||||
Protocol, | ||||||
TypedDict, | ||||||
TypeVar, | ||||||
Unpack, | ||||||
cast, | ||||||
get_type_hints, | ||||||
overload, | ||||||
) | ||||||
|
||||||
from typing_extensions import ParamSpec | ||||||
|
||||||
|
@@ -49,7 +59,7 @@ class RuleType(Enum): | |||||
R = TypeVar("R") | ||||||
SyncRuleT = Callable[P, R] | ||||||
AsyncRuleT = Callable[P, Coroutine[Any, Any, R]] | ||||||
RuleDecorator = Callable[[Union[SyncRuleT, AsyncRuleT]], AsyncRuleT] | ||||||
RuleDecorator = Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT] | ||||||
|
||||||
|
||||||
def _rule_call_trampoline( | ||||||
|
@@ -188,7 +198,39 @@ def _ensure_type_annotation( | |||||
IMPLICIT_PRIVATE_RULE_DECORATOR_ARGUMENTS = {"rule_type", "cacheable"} | ||||||
|
||||||
|
||||||
def rule_decorator(func: SyncRuleT | AsyncRuleT, **kwargs) -> AsyncRuleT: | ||||||
class RuleDecoratorKwargs(TypedDict): | ||||||
"""Public-facing @rule kwargs used in the codebase.""" | ||||||
|
||||||
canonical_name: NotRequired[str] | ||||||
|
||||||
canonical_name_suffix: NotRequired[str] | ||||||
|
||||||
desc: NotRequired[str] | ||||||
"""The rule's description as it appears in stacktraces/debugging. For goal rules, defaults to the goal name.""" | ||||||
|
||||||
level: NotRequired[LogLevel] | ||||||
"""The logging level applied to this rule. Defaults to TRACE.""" | ||||||
|
||||||
_masked_types: NotRequired[Iterable[type[Any]]] | ||||||
"""Unstable. Internal Pants usage only.""" | ||||||
|
||||||
_param_type_overrides: NotRequired[dict[str, type[Any]]] | ||||||
"""Unstable. Internal Pants usage only.""" | ||||||
|
||||||
|
||||||
class _RuleDecoratorKwargs(RuleDecoratorKwargs): | ||||||
"""Internal/Implicit @rule kwargs (not for use outside rules.py)""" | ||||||
|
||||||
rule_type: RuleType | ||||||
"""The decorator used to declare the rule (see rules.py:_make_rule(...))""" | ||||||
|
||||||
cacheable: bool | ||||||
"""Whether the results of this rule should be cached. Typically true for rules, false for goal_rules (see rules.py:_make_rule(...))""" | ||||||
|
||||||
|
||||||
def rule_decorator( | ||||||
func: SyncRuleT | AsyncRuleT, **kwargs: Unpack[_RuleDecoratorKwargs] | ||||||
) -> AsyncRuleT: | ||||||
if not inspect.isfunction(func): | ||||||
raise ValueError("The @rule decorator expects to be placed on a function.") | ||||||
|
||||||
|
@@ -205,8 +247,8 @@ def rule_decorator(func: SyncRuleT | AsyncRuleT, **kwargs) -> AsyncRuleT: | |||||
f"`@rule`s and `@goal_rule`s only accept the following keyword arguments: {PUBLIC_RULE_DECORATOR_ARGUMENTS}" | ||||||
) | ||||||
|
||||||
rule_type: RuleType = kwargs["rule_type"] | ||||||
cacheable: bool = kwargs["cacheable"] | ||||||
rule_type = kwargs["rule_type"] | ||||||
cacheable = kwargs["cacheable"] | ||||||
masked_types: tuple[type, ...] = tuple(kwargs.get("_masked_types", ())) | ||||||
param_type_overrides: dict[str, type] = kwargs.get("_param_type_overrides", {}) | ||||||
|
||||||
|
@@ -257,7 +299,7 @@ def rule_decorator(func: SyncRuleT | AsyncRuleT, **kwargs) -> AsyncRuleT: | |||||
effective_desc = f"`{return_type.name}` goal" | ||||||
|
||||||
effective_level = kwargs.get("level", LogLevel.TRACE) | ||||||
if not isinstance(effective_level, LogLevel): | ||||||
if not isinstance(effective_level, LogLevel): # type: ignore[unused-ignore] | ||||||
raise ValueError( | ||||||
"Expected to receive a value of type LogLevel for the level " | ||||||
f"argument, but got: {effective_level}" | ||||||
|
@@ -343,18 +385,38 @@ def wrapper(*args): | |||||
return wrapper | ||||||
|
||||||
|
||||||
F = TypeVar("F", bound=Callable[..., Any | Coroutine[Any, Any, Any]]) | ||||||
|
||||||
|
||||||
@overload | ||||||
def rule(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]: ... | ||||||
def rule(**kwargs: Unpack[RuleDecoratorKwargs]) -> Callable[[F], F]: | ||||||
thejcannon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
"""Handles decorator factories of the form `@rule(foo=..., bar=...)` | ||||||
https://mypy.readthedocs.io/en/stable/generics.html#decorator-factories. | ||||||
|
||||||
Note: This needs to be the first rule, otherwise MyPy goes nuts | ||||||
""" | ||||||
... | ||||||
|
||||||
|
||||||
@overload | ||||||
def rule(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: ... | ||||||
def rule(_func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this won't work as-is - since those need to be given generics. Ends up being:
And earlier on, there is another type alias of: RuleDecorator = Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]
# needs to be
RuleDecorator = Callable[[SyncRuleT[P, R] | AsyncRuleT[P, R]], AsyncRuleT[P, R]] But at usage, it should be So, I can do all that - but I feel like we're losing the plot with the Russian dolls of generic aliases - and I'd also bet that's how some of these type errors propagated in the first place. None of the uses of SyncRuleT and AsyncRuleT are correctly typed - which is why pyright loses its mind in that file. Take the Finally, at call-usage, when I want to get information about what the typings are - my options are: # Typealiases - which hide what happens:
def rule(_func: AsyncRuleT[P@rule, R@rule]) -> AsyncRuleT[P@rule, R@rule]: ...
# More verbose, but more precise
def rule(_func: (**P@rule) -> Coroutine[Any, Any, R@rule]) -> ((**P@rule) -> Coroutine[Any, Any, R@rule]): ... Nine of ten times, I'd prefer more concise - but the decorator in this case, is genuinely more complex - so it's nice to actually see what's going on, versus 2-3 indirections to get to the meaning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. Please disregard my suggestion then. Btw: I've never seen There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's not in the type hints, that's in the generated code from pyright - but essentially, if you use ParamSpec, you get access to stuff like I read the PEP, but don't recall the terminology they use to describe that. |
||||||
"""Handles bare @rule decorators on async functions. | ||||||
|
||||||
Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `MultiGet`/`concurrently` use | ||||||
coroutines directly. | ||||||
""" | ||||||
... | ||||||
|
||||||
|
||||||
@overload | ||||||
def rule( | ||||||
*args, func: None = None, **kwargs: Any | ||||||
) -> Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]: ... | ||||||
def rule(_func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"""Handles bare @rule decorators on non-async functions It's debatable whether we should even | ||||||
have non-async @rule functions, but keeping this to not break the world for plugin authors. | ||||||
|
||||||
Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `MultiGet`/`concurrently` use | ||||||
coroutines directly. | ||||||
""" | ||||||
... | ||||||
|
||||||
|
||||||
def rule(*args, **kwargs): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically you can spec this and it won't affect caller typing, but it will itself be internally type-checked. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about it, but my plan was to do that as part of the rules internals re-factor. I didn't want to obscure this PR any more than it needed to be (because the internal re-factor will touch like 150 lines). |
||||||
|
Uh oh!
There was an error while loading. Please reload this page.