-
Notifications
You must be signed in to change notification settings - Fork 25
Allow varname to play with type annotation context #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
varname.py
Outdated
|
|
||
| exet = executing.Source.executing(frame) | ||
| qualname = exet.code_qualname() | ||
| module = inspect.getmodule(frame) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki
Is there a better way for this with executing? Or this is already the best way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine. The alternative is to compare module.__file__ and frame.f_code.co_filename but I'm not actually sure if they always match.
|
|
|
|
2: The problem is that when someone provides a decorated function, we receive something like a locally defined wrapper. There's no way to get to the AST of the original function. I guess leave it as just the string but explain the reasoning in the docs. Otherwise some people might just think it's a bad API and pass 5: I understand the doc, I don't think it's good behaviour. |
|
2: So, if I got all your points correctly,
5: Do you mean |
|
2: No, let's not allow passing a function. It's not worth the risk. I briefly thought we could use varname style magic to check that the argument name and value match, e.g. in |
|
5: If " |
|
For example, this script works right now: import varname
def foo1():
return foo2()
def foo2():
return foo3()
def foo3():
return varname.varname(caller=3)
x = foo1()
assert x == 'x'We'd want this to also work: import varname
def my_decorator(f):
def wrapper():
return f()
return wrapper
@my_decorator
def foo1():
return foo2()
@my_decorator
def foo2():
return foo3()
@my_decorator
def foo3():
return varname.varname(caller=3, ignore="my_decorator.<locals>.wrapper")
x = foo1()
assert x == 'x'The stacktrace looks like this: File "...", line 20, in <module>
x = foo1()
File "...", line 5, in wrapper
return f()
File "...", line 10, in foo1
return foo2()
File "...", line 5, in wrapper
return f()
File "...", line 14, in foo2
return foo3()
File "...", line 5, in wrapper
return f()
File "...", line 18, in foo3
return varname.varname(caller=3)It's an alternating mix of frames that should be File "...", line 20, in <module>
x = foo1()
File "...", line 10, in foo1
return foo2()
File "...", line 14, in foo2
return foo3()
File "...", line 18, in foo3
return varname.varname(caller=3)Now |
|
Decide not to check the uniqueness of qualnames from the same module. I thought about this uniqueness at the beginning because I didn't take into account the module. Users should take care of the duplicated qualnames from the same module themselves. |
How? |
varname.py
Outdated
| If `ignore` provided, this should be the stack where we start | ||
| to search the node that should not be ignored. | ||
| ignore: A list of qualnames or tuples of module and qualname that | ||
| Stacks are counted with the ignored ones being excluded. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is one stack, consisting of many frames.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is frame the right word? I am sort of confused. Are we ignoring frames or stacks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we are ignoring and skipping frames.
varname.py
Outdated
| # loot at next frame anyway at next iteration | ||
| frame_index += 1 | ||
| module = inspect.getmodule(frame) | ||
| exect = executing.Source.executing(frame) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is somewhat expensive, try to call it only when you need to get the node, i.e. when you return at the end. For just the qualname use executing.Source.for_frame(frame).code_qualname(frame.f_code). Even that should only be when you actually need the qualname.
| _debug('Skipping frame from varname', frame) | ||
| continue | ||
|
|
||
| if module and module.__name__ in sys.builtin_module_names: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you check if module, later you don't. What does it mean if module is falsy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just because module.__name__ will raise AttributeError if module is None by inspect.getmodule. However, it's fine for other cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you use module.__name__ again further down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, I should check it at L541 as well.
| # if asyncio specified, asyncio.runners, asyncio.events, etc | ||
| # should be all ignored | ||
| modnames = module.__name__.split('.')[:-1] | ||
| if any(sys.modules['.'.join(modnames[:i+1])] in ignore | ||
| for i, _ in enumerate(modnames)): | ||
|
|
||
| _debug('Ignored', frame) | ||
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about specifying (asyncio, 'function_qualname') where the function is actually in asyncio.runners? This prefix checking here only works for ignoring entire modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then I kind of have to scan all qualnames in asyncio.runners to verify function_qualname is from it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppose module.__name__ == 'foo.bar.spam' and exect.code_qualname() == 'func'. Then we need to check (foo.bar.spam, 'func') in ignore or (foo.bar, 'func') in ignore or (foo, 'func') in ignore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then what if there is a func in foo.bar or foo that one doesn't want to ignore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently your code will ignore any function in foo.bar.spam if I set ignore=[foo]. I'm not entirely sure if this is a good idea but I can see the advantage so let's assume we keep that.
Also currently your code will not ignore the function func in foo.bar.spam if I set ignore=[(foo, 'func')]. I think that's inconsistent and confusing. You have this nice prefix wildcard system for entire modules but not functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that if a pair (module and qualname) is received, one is ignoring an exact call (which can be located with the module (not the parent module) and qualname exactly). Otherwise, if a module is passed, I assume one wants to do some fuzzy ignoring (such as any calls from that module and submodules).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was actually thinking about ignoring all frames from python standard libraries, instead of just modules from sys.builtin_module_names, since developers barely have a chance to touch them. However, I didn't find a good way to list all standard libraries. Most of the solutions are trying to walk through the library path.
In this way, developers don't have to worried about calls from libraries, such as typing if varname retrieving gets type annotations involved, asyncio if the retrieving is in async context, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this is somehow too aggresive.
In such a case: class Foo:
...
class Foo:
...The first |
It might be. class Foo:
pass
x = Foo
class Foo:
pass
assert x != FooFor a more realistic example: class Foo:
@property
def x(self):
return 3
@x.setter
def x(self, value):
pass
assert Foo.x.fget.__qualname__ == Foo.x.fset.__qualname__ == "Foo.x"
assert Foo.x.fget != Foo.x.fset |
Makes sense. Then I will have to check the whole |
|
The problem is I'm still not sure what the best behaviour is if duplicate qualnames exist. You could just ignore all the functions with that qualname, sometimes that will be perfect and very convenient. But if people only wanted to ignore one of the functions they might be surprised. Or you could raise an exception and not allow it at all, but now some things can't be ignored. |
Here are the options:
I tend to have option 2. Because in most cases, developers do not intend to specify a qualname pointing to multiple calls to ignore. |
|
You could raise an error or show a warning by default, but allow passing something extra, either another argument to |
Sort of hesitating to add more arguments to Even there are chances to have qualnames referring to multiple calls, the chance to have all these calls involved along the varname retrieving is rare. I'd rather just show a warning, and leave it for the developers to solve it (simply wrap one of them and specify the qualname of the wrapper). |
Yes, no solution is great. This is all very theoretical and unlikely to matter, it's just interesting to think and talk about. You can ignore me if you want.
Not sure what you mean. The problem exists even if the qualname appears only once in the stack. There doesn't have to be several calls involved at once. The problem is that two functions with the same qualname might exist in a file and the user intends to ignore just one but the other will also get ignored if it gets involved. Now as I'm writing, maybe I get it - you're saying that probably the extra functions will never get involved, as in they'll never indirectly call varname?
Warnings can go unnoticed, and suppressing them can be annoying. The Zen says "In the face of ambiguity, refuse the temptation to guess". Based on that I lean a little towards raising an exception. If someone has a problem they can raise an issue and we can learn about a real use case. Then we can change the behaviour without breaking compatibility. |
Correct. I didn't mean those calls were to be involved in a single retrieving, but the chance is also rare for them to be involved in single retrieving individually and separately.
Convinced. |
|
This whole discussion is making me think that we might have it backwards. Instead a blacklist approach of ignoring and skipping frames, maybe it should be a whitelist approach, like "keep going until you reach one of these functions". Something like: def foo1():
return foo2()
def bar():
return foo2()
def foo2():
return foo3()
def foo3():
return varname(caller=[foo1, bar])
x = foo1()
assert x == 'x'But actually the more I think about it maybe not. |
|
I have thought about this way, too. The problem is that this disallows others to wrap more layers around those functions. And sometimes, it is difficult to predict how it will be wrapped inside Another sense is that, even though it is difficult to predict, the wrappers/intermediate calls are kind of grouped. For example, calls from Or if it's deeply wrapped by a group of functions (say from the same module), and one of them is the end. It could be a disaster to list all those wrappers except for the end call. I am actually also thinking that the name |
|
Maybe you were talking about allowing both Then it'll be a fight of which one is more intuitive and easier to use between |
|
I just didn't bother to invent a new name. Let's just leave caller as is and forget the whitelist system. |
…ck to _get_frame to avoid expense of converting each frame into an executing object.
|
@alexmojaki Merge this, for now, to continue working on the preparation of |
#31
Basically allow the following: