-
-
Notifications
You must be signed in to change notification settings - Fork 417
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
Support calling methods that doesn't match a rule name #1076
base: master
Are you sure you want to change the base?
Conversation
This is a creative approach. But I think the code will be much simpler if the decorator merely tagged the method, and the tree-builder used the tagging when scanning the class. Something like: def call_for(rule_name: str) -> Callable:
def _call_for(func: Callable) -> Callable:
func.__rule_name__ = rule_name
return func
return _call_for Then the visitors/transformers will only need to scan the instance and build a mapping. Maybe we'll need a few extra lines to make sure it plays nice with v_args (i.e. the attribute doesn't get removed), not sure, but either way it feels much simpler. |
Putting a marker attribute on the function could also work.
I guess the key question is when should the scan happen to build the mapping? The answer to this is likely related to: What is the tolerance for how frequently does this happen? What I have now is a single scan each time a subclass is created, likely the best that can done as it never needs to be done again after that. The other extreme, would be to not maintain a mapping and scan the class every time. The middle ground dynamic option I was thinking about was lazily building the mapping the first time |
Well, I can't say for sure if it's better to do it during It might seem nitpicky but every line we add, is a line we need to maintain. Every behavior we choose, needs to play nice with future changes. So keeping it simple is really crucial for the long run. (sure, putting it at the beginning of |
Btw, maybe we're over-engineering this. Maybe the only form of renaming we need to support is to add try:
return getattr(self, rule_name)
except AttributeError:
return getattr(self, rule_name + '_') And we memoize the lookup. Maybe a little too magical, actually, but definitely simpler than the alternatives. |
I would suggest allowing the user to define a method We could also consider to amend that with a default search that tries to find methods having an attribute set by some |
Since we're brainstorming here, why not just rstrip underscores from methods? Something like @rstrip_underscores
class Foo(Transformer):
def from_(self, args): # renamed to from
...
def not_a_keyword_(self, args): # also renamed, because why not
... |
My suggestion of providing a generic interface also has the benefit that it works even if the grammar rules are named very different than python identifiers, for example what might happen if you have ABNF compatible identifiers with hyphens instead of underscores. |
Or we can rename hyphens to underscores and lose nothing in the process :) |
and the spaces, and any other special characters that might appear in the name |
My preference would be to avoid a name coercion scheme where certain characters are collapsed into underscores. It can lead to whack-a-mole situation where the implementation initially seems sufficient, but then a user comes with an unexpected edge case. Additionally, it can cause situations where the grammar rule names are unique, but the converted names collide. In general, it seems like were attempting to figure out where we want to be in the triangle of:
The decorator approach is also generic and likely the best option for "nice API for the user to work with". In general, it also scores well on "low performance cost at runtime", but some existing implementation decisions hurt that some as well as making "easy for the framework to implement" a bit worse. I can take another shot at using a marker instead of the wrapper object using a cache instead of identifying everything at class creation. |
@plannigan I think that's a good analysis of the issue here. I'll just note that "nice API for the user to work with" is the main driver here, because otherwise everyone can do setattr(my_transformer, 'from', from_) And problem solved. So this is not really about adding new abilities, but about making it easy and idiomatic. (okay, it won't solve dashes or dollar signs in the rule name, and for that |
Actually, doesn't |
Use a marker on the function instead of a wrapper class Look up overrides only when needed and cache result
Was able to get a complete solution working that:
Open questions:
|
A working version that supports using a decorator to register a method to be called for a rule that does match the method name. It also supports things like a
A
inherits fromB
, which inherits fromTransformer
, and bothA
&B
define overrides.However, things get complicated with
merge_transformers()
. Since it will mutatebase_transformer
when given (explicitly documented behavior), copying over registered overrides needs to be done at the instance level instead of the class level. That in itself, is not a problem as an instance variable could be created in__init__()
. The issue is that, nothing in theTransformer
orVisitor
inheritance chain currently defines an__init__()
. This means that classes that do have an__init__()
, likely don't callsuper().__init__()
(example in test cases). This means the straightforward implementation would be a breaking change assuper().__init__()
would now be a requirement.This is Python, so we can do a dynamic non-straightforward implementation, but I wanted to get some thought before I headed down that path.
Addresses #1066