Last active
September 17, 2023 16:47
-
-
Save bswck/c46dda40698ebdd24d3050e0c03e5d78 to your computer and use it in GitHub Desktop.
Check whether the descriptor was called from the owner's method or from somewhere else.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (C) 2023, bswck | |
import inspect | |
from collections.abc import Callable | |
from types import MethodType | |
class Descriptor: | |
def __init__( | |
self, | |
stack_offset: int = 1, | |
inside_hook: Callable[[], None], | |
outside_hook: Callable[[], None], | |
) -> None: | |
self.stack_offset = stack_offset | |
self.inside_hook = inside_hook | |
self.outside_hook = outside_hook | |
def __set__(self, instance: object, value: object) -> None: | |
# Get the frame of inside the code that called this descriptor. | |
frame = inspect.stack()[self.stack_offset].frame | |
# Get the name of the code that contains that frame | |
# (if it's a function—what we look for, then it will be the function name). | |
function_name = frame.f_code.co_name | |
# Get the name of the module that contains the frame. | |
module = frame.f_globals["__name__"] | |
# Get the number of the line that called this descriptor. | |
lineno = frame.f_lineno | |
# Check if the function that is using this descriptor is an actual method | |
# of the instance. | |
try: | |
# Use object.__getattribute__() to avoid triggering | |
# __getattribute__, __getattr__, or any other descriptor's __get__. | |
function = object.__getattribute__(instance, function_name) | |
except AttributeError: | |
function = None | |
# Check if it's a bound method. | |
if isinstance(function, MethodType): | |
# Get the source code of the suspected method. | |
lines, start = inspect.getsourcelines(function) | |
stop = start + len(lines) | |
# Finally, check if the frame's line number is within the range of the | |
# method's source code. Also make sure we're in the same module | |
# as the method. | |
if module == function.__module__ and lineno in range(start, stop): | |
# Got it! We're inside the method. | |
self.inside_hook() | |
return | |
self.outside_hook() | |
# Check on some examples. | |
class Foo: | |
version = Descriptor( | |
lambda: print("inside a method of Foo"), | |
lambda: print("outside a method of Foo"), | |
) | |
def bar(self) -> None: | |
self.version = 5 | |
f = Foo() | |
f.bar() | |
f.version = 4 | |
def inside_outer_func() -> None: | |
f = Foo() | |
f.version = 4 | |
inside_outer_func() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment