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

(Somehow) Allow autocomplete methods to accept arguments #2668

Open
DefiDebauchery opened this issue Dec 12, 2024 · 4 comments · May be fixed by #2669
Open

(Somehow) Allow autocomplete methods to accept arguments #2668

DefiDebauchery opened this issue Dec 12, 2024 · 4 comments · May be fixed by #2669
Labels
feature request New feature request

Comments

@DefiDebauchery
Copy link
Contributor

DefiDebauchery commented Dec 12, 2024

What is the feature request for?

The core library

The Problem

This stream of consciousness delves into parts of python I'm not fully familiar with, and is fairly contrived without testing, so apologies in advance for typos or incorrect concepts.

With autocompletes, I thought it might be useful to allow arguments to the autocomplete itself, which could generalize related searches with different parameters without cluttering the application with one-shot methods.

Let's assume the following basic example:

import discord
from discord.ext import commands

class Pets(commands.Cog):
	async def pet_autocomplete(self, ctx: discord.AutocompleteContext) -> list[discord.OptionChoice]:
        search_term = ctx.value.casefold()
        return [
            discord.OptionChoice(name=animal['name'], value=animal['id'])
            for animal in self.pet_datasource if search_term in animal['name'].casefold()
        ]

    @commands.slash_command(description='Get pet info')
    async def pet_by_name(
		self,
		ctx: discord.ApplicationContext,
		pet: discord.Option(str, "Target Pet", autocomplete=pet_autocomplete)
	) -> None:
		# ... This part doesn't matter

Now say we want to have commands that specifically only concern dogs (without adding a secondary option). Granted, we could always create a separate autocomplete method, referencing that in our new slash command:

	async def dog_autocomplete(self, ctx: discord.AutocompleteContext) -> list[OptionChoice]:
		search_term = ctx.value.casefold()
		return [
            discord.OptionChoice(name=animal['name'], value=animal['id'])
            for animal in self.pet_datasource if search_term in animal['name'].casefold()
			and animal['type'] == 'dog'
        ]

	@commands.slash_command(description="Who's a good dog?")
	async def vote_for_dog(
		self,
		ctx: discord.ApplicationContext,
		dog: discord.Option(str, "Dog's Name", autocomplete=dog_autocomplete)
	) -> None:
		# ...

And of course, we could move the logic of the autocomplete to a generalized search function and have the AC make the . But it would be even more cool for the autocomplete method itself to be more generic, taking in arguments from the autocomplete= arg in discord.Option. This would allow us to have something like

	async def pet_autocomplete(self, ctx: discord.AutocompleteContext, pet_type: str = None) -> list[OptionChoice]:
		search_term = ctx.value.casefold()
		return [
            discord.OptionChoice(name=animal['name'], value=animal['id'])
            for animal in self.pet_datasource if search_term in animal['name'].casefold()
			and (animal['type'] == pet_type if pet_type else True)
		]

The first hurdle is the autocomplete handler itself. It requires an AutocompleteContext, so of course we cannot execute the callback methods directly.

Two ideas I immediately had were functools.partial and lambdas. Neither of these work, and I suspect for the same reason. First, a visual on how these might look:

	@commands.slash_command(description="Who's a good dog?")
	async def vote_for_dog(
		self,
		ctx: discord.ApplicationContext,
		dog: discord.Option(str, "Dog's Name", autocomplete=functools.partial(dog_autocomplete, pet_type='dog')
	) -> None:
		# ...

	# -- or -- #

	@commands.slash_command(description="Who's a good dog?")
	async def vote_for_dog(
		self,
		ctx: discord.ApplicationContext,
		dog: discord.Option(str, "Dog's Name", autocomplete=lambda self, *args: self.dog_autocomplete(*args, pet_type='dog')
	) -> None:
		# ...

As an aside: The lambda syntax came after a few different iterations. I always thought it was interesting that my autocomplete argument was the bare def. And once I put it in the lambda, it lost all context and complained that dog_autocomplete was an unresolved reference. I also couldn't immediately reference self.dog_autocomplete because self was unresolved -- passing that into the lambda solved all the reference errors.

Anyway, both approaches yielded the same outcome: dog_autocomplete was never awaited

However, when I removed async from my autocomplete method(s), the lambda approach worked! Unfortunately, in my production context, this isn't viable, but is at least some bit of progress. There are workarounds to 'async lambdas', but I think there's a valid use case in having this Just Work within Pycord.

If anyone smarter than me could take a look, it'd simplify my classes quite a bit to have something like this available!

The Ideal Solution

Easily allow argument passing to autocomplete method references within Option()s, preferably without the complex syntax of lambdas.

@DefiDebauchery DefiDebauchery added the feature request New feature request label Dec 12, 2024
@Soheab
Copy link
Contributor

Soheab commented Dec 12, 2024

If I understood you correctly, you want something like the following:

def get_autocomplete(pet_type: str) -> Callable[[AutocompleteContext], Coroutine[Any, Any, list[OptionChoice]]]:
    async def actual_autocomplete(ctx: AutocompleteContext) -> list[OptionChoice]:
        print(pet_type)
        ... # handle

  return actual_autocomplete


option: discord.Option(..., autocomplete=get_autocomplete("dog"))

A function that takes the needed params and then returns the actual callback for the library to call.

You can access the command's cog via ctx.cog if needed.

Or ideally, you should rely on the used option's name, which you can get via ctx.focused.name.

@DefiDebauchery
Copy link
Contributor Author

DefiDebauchery commented Dec 12, 2024

Very cool, thank you so much for the response and the guidance. This does seem to work!

Only one minor issue, but while I can reference self in the inner method, I cannot include it in the wrapper:

class Pets:
	def get_autocomplete(self, pet_type: str) -> Callable[[AutocompleteContext], Coroutine[Any, Any, list[OptionChoice]]]:
    	async def actual_autocomplete(self, ctx: AutocompleteContext) -> list[OptionChoice]:
        	print(pet_type)
        	... # handle
        	
		return actual_autocomplete
	  
	@commands.slash_command(description="Who's a good dog?")
	async def vote_for_dog(
		self,
		ctx: discord.ApplicationContext,
		dog: discord.Option(str, "Dog's Name", autocomplete=get_autocomplete('dog')
	) -> None:
		# ...

> TypeError: Pets.get_autocomplete() missing 1 required positional argument: 'self'

Of course, omitting it causes the IDE to whine, and it cannot be added to the autocomplete= invocation. Anything I can do to bridge the gap, or just # noqa and be on my way?

Edit: Or am I completely braindead and this is a @staticmethod? I always think of those to be methods that should return something outside of a instantiated class context.

@Paillat-dev
Copy link
Contributor

I will make a pr that allows for partial to be used too

@Soheab
Copy link
Contributor

Soheab commented Dec 12, 2024

Very cool, thank you so much for the response and the guidance. This does seem to work!

Only one minor issue, but while I can reference self in the inner method, I cannot include it in the wrapper:

class Pets:
	def get_autocomplete(self, pet_type: str) -> Callable[[AutocompleteContext], Coroutine[Any, Any, list[OptionChoice]]]:
    	async def actual_autocomplete(self, ctx: AutocompleteContext) -> list[OptionChoice]:
        	print(pet_type)
        	... # handle
        	
		return actual_autocomplete
	  
	@commands.slash_command(description="Who's a good dog?")
	async def vote_for_dog(
		self,
		ctx: discord.ApplicationContext,
		dog: discord.Option(str, "Dog's Name", autocomplete=get_autocomplete('dog')
	) -> None:
		# ...

> TypeError: Pets.get_autocomplete() missing 1 required positional argument: 'self'

Of course, omitting it causes the IDE to whine, and it cannot be added to the autocomplete= invocation. Anything I can do to bridge the gap, or just # noqa and be on my way?

Edit: Or am I completely braindead and this is a @staticmethod? I always think of those to be methods that should return something outside of a instantiated class context.

The function I suggested should indeed be outside the class or a @staticmethod since you cannot access self like that (that = autocomplete=self.get_autocomplete(...)). That's why I suggested using ctx.cog if you need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature request
Projects
None yet
3 participants