Skip to content

Instantly share code, notes, and snippets.

@bswck
Last active September 17, 2023 16:47
Show Gist options
  • Save bswck/c46dda40698ebdd24d3050e0c03e5d78 to your computer and use it in GitHub Desktop.
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.
# (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