Full disclosure, I'm fairly new to Python, which is mostly a non-issue when it comes to day-to-day syntax; however, I have an issue that stems from using the descriptor protocol to bind a method from one class to another class while binding/locking the method to value of self
from the source class.
This is done primarily to maintain backwards compatibility with a syntax while adding some introducing a new API to the library I work on that's effectively syntactic sugar.
To avoid the verbosity of the original code, I've stripped down the example:
To start, I have a class that's meant to wrap a function and ensure it:
- is subscriptable (i.e. supports the following syntax:
func["some_key"]
) - is still callable and can produce the correct results
- has a bound method from another class that can be called to return that other class's result (e.g.
fn.some_method()
)
class SubscriptableFunctionWrapper:
def __init__(self, fn, decorator_name_variant_method):
self.fn = fn
self.name_variant = decorator_name_variant_method.__get__(self)
def __getitem__(self, keys):
# the implementation isn't important except for the fact that
# instances of SubscriptableFunctionWrapper should be subscriptable
# so we'll return a known constanct
return 42
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs)
Next, I have a class that's meant to decorate a function:
class Decorator:
def __init__(self, name = "", variant = "default"):
self.name = name
self.variant = variant
def __call__(self, fn):
if self.name == "":
self.name = fn.__name__
return SubscriptableFunctionWrapper(fn, self.name_variant)
def name_variant(self):
return (self.name, self.variant)
These two class combine such that the below code snippet is valid Python that produces the results I expect:
@Decorator()
def adds_two(a):
return a + 2
assert adds_two(2) == 4 # I expect adds_two to be callable and produce the correct result
assert adds_two["keys"] == 42 # I expect adds_two to be subscritable and return the constant
assert adds_two.name_variant() == ("adds_two", "default") # I expect adds_two to return the value of Decorator's name_variant method
This has been working on Python versions 3.7, 3.8, 3.9, and 3.10; however, it now no longer works with Python 3.11. I cannot find any documentation or Stack Overflow questions that explain why this syntax no longer works. I've been wholly unsuccessful in Google/ChatGPTing an explanation for this change in behavior.
I've reliably verified that the code snippets work in Python 3.7, 3.8, 3.9, 3.10 and not in 3.11. I Googled changes to the descriptor protocol in Python 3.11, but haven't identified anything that would point to a root cause for the change in the behavior.