Search code examples
pythonsuperinternalsyspython-3.7

Detect, from a frame, which class a method is associated with


I'm writing an incredibly hackish piece of not-quite-production code in Python, and I need some way of detecting whether a _XYZ__foo attribute access was called from a method defined on a class called /_*XYZ/. It's not quite as easy as that, though, since I need to detect the original method access in case anything has overridden __getattribute__ and called super().

I'm bad at explaining, so... the rules are similar to Java's private, except I want to prevent cheating. (Yes, I know that this is counter to the Python philosophy; bear with me here.)

My current plan of attack is:

  1. Use re.compile('_(?P<class>.*?)__(?P<name>.*)') to detect the name of the class (with preceding _s stripped).
  2. Climb the super chain with sys._getframe(n) to find out where the attribute access was.
  3. Detect what class it was on... somehow. I'm stuck here.

I might be able to do this by emulating super's walking of the MRO, but I'd much rather rely on detection because checking what's been called by super and what's been called by user functions is hard.

So, to my actual question. Given a frame, how can I detect which class a method is associated with? If I had access to the function object I could do f.__qualname__[:-1-len(f.__name__)], but I don't (or, at least, I don't think I do). As is, I have no idea how to do this!

Here's a simple example that demonstrates what I want to do:

import sys
import re
import itertools
import builtins
from builtins import __build_class__

def build_class(func, name, *bases, metaclass=None, **kwds):
    if bases[-1] is object:
        bases = bases[:-1]
    bases += HackishClass, object
    if metaclass is None:
        return __build_class__(func, name, *bases, **kwds)
    return __build_class__(func, name, *bases, metaclass=metaclass, **kwds)

private_regex = re.compile('_(?P<class>.*?)__(?P<name>.*)')
class HackishClass:
    __slots__ = ()
    def __getattribute__(self, key):
        match = private_regex.match(key)
        if match is not None:
            for depth in itertools.count(1):
                frame = sys._getframe(depth)
                if ...:  # snip
                    # Check for the original attribute access here.
                    break
            class_name = ...  # HERE! MAGIC GOES HERE!
            if class_name != match['class']:
                raise AttributeError("This is private! Keep out.")
        return super().__getattribute__(key)

builtins.__build_class__ = build_class

Solution

  • As far as I know, there's no way to obtain the method where the attribute access occurred directly from a frame object. We can, however, obtain that method's code object. We can then search the object's MRO until we find the method to which that code object belongs.

    private_regex = re.compile('_(?P<class>.*?)__(?P<name>.*)')
    class HackishClass:
        __slots__ = ()
    
        def __getattribute__(self, key):
            match = private_regex.match(key)
            if match is None:
                # not a private attribute, no problem
                return super().__getattribute__(key)
    
            # obtain the code object of the calling function
            calling_codeobj = inspect.currentframe().f_back.f_code
    
            # iterate the MRO until we find a class with the name from `key`
            classname = match.group('class')
            for cls in type(self).mro():
                if cls.__name__ != classname:
                    continue
    
                # check if the code object belongs to a method defined in this class
                for thing in vars(cls).values():
                    if getattr(thing, '__code__', None) is calling_codeobj:
                        # found it! allow the attribute access
                        return super().__getattribute__(key)
    
            raise AttributeError("This is private! Keep out.")
    

    A small demonstration:

    class Foo:
        def __init__(self):
            self.__foo = 5
            print(self.__foo)
    
    f = Foo()           # prints 5
    print(f._Foo__foo)  # throws AttributeError