pwwang/python-varname

Explore adding an `ignore` decorator to ignore the intermediate calls for varname

pwwang opened this issue · 13 comments

I assume you mean allowing both Foo() and Foo[int]() which would require different values for caller. I think this is a good example of how trying to specify a value for caller is often difficult or impossible. An alternative strategy is to mark certain functions as functions that varname should 'ignore', as in "I never want the varname from here". So you might write something like:

import typing
import varname

varname.ignore(typing._GenericAlias.__call__)

And then when varname is looking for the caller, it skips frames with the code object from there.

This gets more complicated when the function has been decorated and you don't have easy access to the original. In that case one possibility is to pass the qualified function name as a string, maybe something like:

import typing
import varname

varname.ignore(typing, "_GenericAlias.__call__")

executing can usually get the qualified name from a code object, although now of course many caveats apply like source code being available and the function name being unique.

Originally posted by @alexmojaki in #31 (comment)

@alexmojaki I am opening a new issue, as this could be a more general functionality.

I am also thinking that this should be function-specific, to make sure ignoring some calls don't affect other varname wrappers if we have many.

By varname wrappers I mean the functions directly call varname to fetch the variable names.

I guess the best practice should be something like this:

@varname.inject_varname
def func():
  return __varname__

@func.ignore
def func2():
  return func()

To extend that varname.inject_varname to function level, we could probably do this:

def inject_varname(func):

    # get the variable name here, also based on func.ignore
    var_name = 'somevar'
    def wrapper():
        func.__globals__['__varname__'] = var_name
        try:
          return func()
        finally:
          del func.__globals__['__varname__']

    return wrapper

@inject_varname
def func():
    return __varname__

foo = func() # 'foo'

Not sure about the caveats when injecting __varname__ to globals, even though it's removed finally.
One of them I can think of is that it is not thread-safe.

  1. I don't think this code makes sense, you can't calculate var_name = 'somevar' at function decoration time.
  2. Why do you even want this inject_varname? What's wrong with the regular varname() function?
  1. I was wrong. The retrieval should happen inside the wrapper
  2. I was thinking we need some kind of function-specific ignoring calls. A decorator might be a good solution to do that.

Ah, so the point is that you want something less global like @func.ignore? I hadn't noticed that, I thought it was basically just @ignore.

For that I suggest selecting things to ignore when varname() is called, e.g. varname(ignore=...).

Thought about that as well. Swinging between those two ways, or to have both of them?

Keep the API simple and consistent. inject_varname sounds like more confusion.

Yap, let's go for varname(ignore=...) way.

Then the uniqueness thing is still a problem. So we just leave it for the developers to take care of, or we should consider that (even though I am not sure if it is feasible).

You can check for uniqueness, so things shouldn't silently go wrong:

import executing
import __main__

def ignore(module, qualname):
    source = executing.Source.for_filename(module.__file__, module.__dict__)
    assert list(source._qualnames.values()).count(qualname) == 1


class A:
    def foo(self):
        pass

class B:
    def foo(self):
        pass

class B:
    def foo(self):
        pass


ignore(__main__, "A.foo")
ignore(__main__, "B.foo")  # fails

Bingo for introducing filename of the module to make it unique. That's clever.

Closing via v0.5.6