Open Quelklef opened 4 years ago
I don't think a perfect solution is possible.
This is because decorators can only appear before function and class definitions, which will always evaluate to a function and a class, respectively. On the contrary, our goal here is for templates to evaluate to a string.
I see a few possible options to support decorators, but none of them are pretty.
Option one is to accept that we won't be able to evaluate to a string. Instead, compile down into class syntax and evaluate to a proxy to a string. Something like this:
@italicize
[unplate.begin(my_template)] @ r"""
I may be {{ adj1 }},
but at least I'm not {{ adj2 }}!
""" [unplate.end]
# compiles to
@italicize
class my_template(unplate.Template): lines = []; \
lines.extend(["I may be ", str(adj1), ",\n"]); \
lines.extend(["but at least I'm not ", str(adj2), "!\n"]); \
text = ''.join(lines)
# where unplate has defined
class Template(str):
def __init__(self):
self.text = type(self).text
def __str__(self):
return self.text
def __getattr__(self, name):
return getattr(self.text, name)
Pros:
Cons:
unplate.Template
. Further, this class adds no semantic value or functionality.Alternatively, we could compile templates down into functions and require that they be lead with an unplate.decorated
decorator, like so:
@italicize
@unplate.decorated
[unplate.begin(my_template)] @ r"""
I may be {{ adj1 }},
but at least I'm not {{ adj2 }}!
""" [unplate.end]
# compiles to
@italicize
@unplate.decorated
def my_template(lines=[]):
lines.extend(["I may be ", str(adj1), ",\n"])
lines.extend(["but at least I'm not ", str(adj2), "!\n"])
return ''.join(lines)
# where unplate has defined
def decorated(func):
return func()
When the template builder is undecorated, it would compile down as usual--not into a function.
Pros:
Cons:
In a similar vein to https://github.com/Quelklef/unplate/issues/2, move the responsibility of executing compiled unplate code to unplate, and then catch errors and dynamically modify the line numbers (and perhaps even the column numbers!) of these errors to match the pre-compiled source.
@italicize
[unplate.begin(my_template)] @ r"""
I may be {{ adj1 }},
but at least I'm not {{ adj2 }}!
""" [unplate.end]
# compiles to
@italicize
@unplate.decorated
def my_template(lines=[]):
lines.extend(["I may be ", str(adj1), ",\n"])
lines.extend(["but at least I'm not ", str(adj2), "!\n"])
return ''.join(lines)
# where unplate has defined
def decorated(func):
return func()
Pros:
Cons:
I feel that option three is reasonable. The fact that Unplate compiles back into Python internally should be an implementation detail. The top-level API should be changed from
exec(unplate.compile(__file__, globals(), locals())
to
unplate.main(__file__, globals(), locals())
or something similar, and then option 3 should be followed.
Allow for
The difficulty in doing this is preserving line numbers in the compiled output. Otherwise, the above could compile into something like
where
unplate.call
simply calls the function it wraps. But this is one line too long--the@unplate.call
line will bump all lines afterwards down one.