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

kwarg injection does not work with decorated functions #688

Closed
deppe opened this issue Jul 22, 2022 · 5 comments · Fixed by #689
Closed

kwarg injection does not work with decorated functions #688

deppe opened this issue Jul 22, 2022 · 5 comments · Fixed by #689
Assignees
Labels
area:async area:sync bug Something isn't working
Milestone

Comments

@deppe
Copy link

deppe commented Jul 22, 2022

Reproducible in:

pip freeze | grep slack
python --version
sw_vers && uname -v # or `ver`

The slack_bolt version

slack-bolt==1.14.1
slack-sdk==3.17.2

Python runtime version

Python 3.9.13

OS info

Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal

Steps to reproduce:

  1. Create a decorator. This does nothing but call the wrapped function with *args, **kwargs. Example:
from functools import wraps
def my_decorator(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        f(*args, **kwargs)

    return wrap
  1. Use the decorator on a slack bolt listener. For example
    @app.event("app_mention")
    @my_decorator
    def handle_mentions(say: Say, body: dict[str, Any]) -> None:
        # do something with say, body variables

Expected result:

say and body should be properly injected into the handle_mentions listener

Actual result:

This throws the following error: TypeError: handle_mentions() missing 2 required positional arguments: 'say' and 'body'

Potential fix:

The slack bolt code currently uses inspect to get args like arg_names = inspect.getfullargspec(func).args. Instead, it could unwrap the func first: inspect.getfullargspec(inspect.unwrap(func)).args.

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.

@deppe deppe closed this as completed Jul 22, 2022
@deppe deppe reopened this Jul 22, 2022
@seratch seratch added bug Something isn't working area:async area:sync labels Jul 22, 2022
@seratch seratch added this to the 1.14.3 milestone Jul 22, 2022
@seratch seratch self-assigned this Jul 22, 2022
@seratch
Copy link
Member

seratch commented Jul 22, 2022

Hi @deppe, thanks for reporting this! We will resolve this issue shortly (I'm already working on the PR for it).

@ryanmsnyder
Copy link

ryanmsnyder commented Aug 16, 2022

Hi @seratch,

I'm implementing a custom decorator to authorize each user based on the team they belong to within our organization. The idea is to easily add a filter onto any event listener. In the below pseudo example, if the user that said "hi" isn't on team1 or team2, the bot won't respond with "hello" but will instead respond with a message saying the user doesn't have access to that keyword/feature, which is implemented in the @auth_user decorator (by accessing the message kwarg and extracting the user id).

@app.message("hi")
@auth_user(['team1', 'team2'])
async def message_hello(say):
   say("Hello!")

I was able to get this working but I'd like to abstract away a little more code in the custom @auth_user decorator by manually injecting the message keyword into the original function's signature using the inspect module similar to this solution. That way, I can access message within my decorator without having to manually pass it in each listener function as an argument. The injection works until inspect.getfullargspec(inspect.unwrap(func)).args is called, which strips away the message argument that I injected in the decorator.

Is there a way around this to where I can always access message without having to pass it into each function?

@seratch
Copy link
Member

seratch commented Aug 16, 2022

@ryanmsnyder

Is there a way around this to where I can always access message without having to pass it into each function?

Since Bolt's design philosophy requires your listener functions to have all the necessary arguments in the method signature, I don't think this is feasible.

Instead of implementing your own decorator, I would suggest using listener middleware / matchers. You can receive any arguments in the function and you can reuse it for any listeners: https://slack.dev/bolt-python/concepts#listener-middleware

@dimitarOnGithub
Copy link

Hi @seratch,

Apologies for commenting on an old, and closed, issue, but I didn't want to open an entire new one, considering my question related directly to your comment - has there been any consideration in the time since regarding custom decorators that abstract logic away from the listeners?

Changing the get_arg_names_of_callable to:

def get_arg_names_of_callable(func: Callable) -> List[str]:
    # Check if the func is decorated and if the decorator requires specific data injection
    # in the case of a decorator that handles *args and **kwargs, the length of inspect.getfullargspec(func).args is 0,
    # so we can defer to the unwrapped func
    if hasattr(func, '__wrapped__') and inspect.getfullargspec(func).args:
        return inspect.getfullargspec(func).args
    return inspect.getfullargspec(inspect.unwrap(func)).args

would retain the original fix made with #689 but also allow the function to inject arguments required by the decorator, instead of the listener function. I do, however, also realize this comes with a lot of assumptions and downsides (user has to handle arguments modifications, passing parameters properly to the listener, and a whole other can of worms) which might not be something you want to maintain in the long run.

@seratch
Copy link
Member

seratch commented Jun 19, 2024

@dimitarOnGithub Thanks for the suggestion. Indeed, your suggestion may be helpful for some scenarios. If you're happy to contribute to this project using your time, a pull request with sufficient unit test code that demonstrates the scenarios to cover by its change would be greatly appreciated! If we need to discuss a bit more details before making code changes, please feel free to write in here or create a new issue for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:async area:sync bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants