Edit: Actual 'Solution'
It's tedious but you can 'literalize' your functions by wrapping them up in an enum. To enable full control over docstrings and parameter specification you'll need a separate enum for each function. Tested with pyright>1.1.310.
from typing import TYPE_CHECKING, Literal
from enum import Enum, member
class _accepted_function1(Enum):
@staticmethod
def __call__(x: int): # specify your function signature here
"""My docstring"""
# write your code here
pass
if TYPE_CHECKING:
_ = __call__
else:
_ = member(__call__)
class _accepted_function2(Enum):
@staticmethod
def __call__(): # specify your function signature here
"""My docstring"""
# write your code here
pass
if TYPE_CHECKING:
_ = __call__
else:
_ = member(__call__)
accepted_function1 = _accepted_function1._
accepted_function2 = _accepted_function2._
def function_accepting_functions(foo: Literal[accepted_function1, accepted_function2]):
if foo is accepted_function1:
foo(3)
else:
foo()
function_accepting_functions(accepted_function1)
As far as I know it's not actually possible to do what you want here.
The problem is that functions just aren't types neither literals. They're objects that are created at runtime. A function is technically an instance of its signature, so you can only restrict the type on a signature basis. I can present you with three alternatives though.
1. Using an enum
I'd argue this is the most clean solution and surely the one I'd recommend. Although at least pyright kinda messes things up there.
Note you need the member(...)
call as functions aren't converted to enum members by default.
from enum import Enum, member
def accepted_function1():
pass
def accepted_function2():
pass
class AcceptedFunctions(Enum):
if TYPE_CHECKING:
accepted_function1 = accepted_function1
accepted_function2 = accepted_function2
else:
accepted_function1 = member(accepted_function1)
accepted_function2 = member(accepted_function2)
def function_accepting_functions(foo: AcceptedFunctions):
foo.value()
function_accepting_functions(AcceptedFunctions.accepted_function1)
You could use this approach to 'literalize' your functions. You need all your functions to have the same signature though (if they aren't the same signature you'll get a huge headache trying to overload __call__
correctly. I didn't manage to do that as you cannot have forward refs for enum literals)
from enum import Enum, member
def _accepted_function1():
pass
def _accepted_function2():
pass
class _AcceptedFunction(Enum):
if TYPE_CHECKING:
accepted_function1 = _accepted_function1
accepted_function2 = _accepted_function2
else:
accepted_function1 = member(_accepted_function1)
accepted_function2 = member(_accepted_function2)
def __call__(self) -> None:
return self.value()
accepted_function1 = _AcceptedFunction.accepted_function1
accepted_function2 = _AcceptedFunction.accepted_function2
def function_accepting_functions(foo: Literal[accepted_function1, accepted_function2]):
foo()
function_accepting_functions(accepted_function1)
2. Using a decorator and a custom function type
This solution just fools the type checker into treating your accepted_function*
as a different type. You'll loose access to any function attributes that way though. You can implement proxy-properties though if you wanna - check typeshed/types.pyi::FunctionType
to see properties that exist on functions - I've implemented closure for you but you could add all other attributes normal functions have add there as well.
from typing import Generic, TypeVar, ParamSpec, Any, Callable
from types import CellType
import sys
T = TypeVar("T")
P = ParamSpec("P")
class acceptable_function(Generic[P, T]):
def __init__(self, func: Callable[P, T]) -> None:
self.__func = func
def __call__(self, *args: P.args, **kwds: P.kwargs) -> T:
return self.__func(*args, **kwds)
@property
def __closure__(self) -> tuple[CellType, ...] | None:
return self.__func.__closure__
# you may add more properties that exist on functions...
@acceptable_function
def accepted_function1():
pass
@acceptable_function
def accepted_function2():
pass
def function_accepting_functions(foo: acceptable_function):
foo()
function_accepting_functions(accepted_function1)
3. Add some restrictions on the function signature
If you have control over the signatures of the accepted functions and they all have the same signature you can add a 'secret' parameter to ensure no other functions are passed.
from typing import Protocol
class __Helper:
pass
class AcceptedFunction(Protocol):
def __call__(self, *, __secret: __Helper = __Helper()) -> None:
...
def accepted_function1(*, __secret: __Helper = __Helper()):
pass
def accepted_function2(*, __secret: __Helper = __Helper()):
pass
def function_accepting_functions(foo: AcceptedFunction):
foo()
function_accepting_functions(accepted_function1)