2

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.

Erik Eppel
  • 21
  • 1
  • We ran into the same issue, and are unsuccessful at finding a good explanation as well. What we found however is that it'll work provided you instantiate your `fn` callable, accessing `some_method` on the instance `fn()` instead of the class `fn`. – edthrn Jul 11 '23 at 07:11

0 Answers0