icecream icon indicating copy to clipboard operation
icecream copied to clipboard

Using ic in decorator/wrappers; showing alias function and parameters instead of actual function and passed parameters.

Open CJC-ds opened this issue 3 years ago • 3 comments

When trying to decorate my functions with ic,

In[0]

from icecream import ic
def my_decorator(func, *args, **kwargs):
    def icecream_wrapper(*args, **kwargs):
        ic(func(*args))
    return icecream_wrapper

@my_decorator
def add_two_numbers(a,b):
    return (a+b)

add_two_numbers(1,2)

Out[0]

ic| func(*args): 3

From the above example, instead of printing ic| add_two_numbers(1,2): 3, it instead uses the aliases in the my_decorator function instead. Probably not an issue, just thought it could be nice to have. Any help or suggestions? Cheers :)

CJC-ds avatar Feb 26 '21 06:02 CJC-ds

Try https://github.com/alexmojaki/sorcery and this code:

import sorcery


@sorcery.spell
def print_call(frame_info, x):
    print(f"{frame_info.get_source(frame_info.call)}: {x!r}")
    return x


def my_decorator(func):
    @sorcery.no_spells
    def icecream_wrapper(*args, **kwargs):
        return print_call(func(*args))

    return icecream_wrapper


@my_decorator
def add_two_numbers(a, b):
    return (a + b)


add_two_numbers(1, 2)

An important part is @sorcery.no_spells which tells sorcery to skip icecream_wrapper when finding the calling frame. Unfortunately it's no longer documented properly.

alexmojaki avatar Feb 26 '21 07:02 alexmojaki

@alexmojaki any way to integrate the magic (terrible pun intended) of sorcery into icecream so @CJC-ds's decorator example above automagically works as intended?

gruns avatar Mar 08 '21 21:03 gruns

Yes, this is very simple to reimplement without depending on sorcery, it's just keeping track of a list of code objects to ignore and it looks out for those code objects when looking at frames in the stack before picking one to extract the ic() call from.

An alternative API could be something like ic(func(*args), ignore=[icecream_wrapper]).

There's a tricky API design problem though. If icecream_wrapper isn't actually what it seems, e.g. if it's been decorated and replaced with a wrapper function, you ignore the wrong code object without noticing.

@pwwang and I thought about this problem way too much:

  • https://github.com/pwwang/python-varname/issues/31
  • https://github.com/pwwang/python-varname/pull/32
  • https://github.com/pwwang/python-varname/issues/33
  • https://github.com/pwwang/python-varname/pull/39

There's a bunch of heuristics you can use to mitigate the problem. But there is no perfect solution.

I think for ic() and its scope limited to debugging, it's probably fine to take the small risk of ignoring the wrong thing.

You can also choose a number of frames to skip, like ic(func(*args), skip=1). But in complicated situations this can be tricky for the user to get right.

Or maybe you show everything going back a few frames, like ic(func(*args), depth=3):

ic | main(): add_two_numbers(1, 2): func(*args): 3

It's like a mini traceback!

alexmojaki avatar Mar 08 '21 22:03 alexmojaki