-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add support for the Environment to optionally return native types. #708
Conversation
tests/test_nativetypes.py
Outdated
try: | ||
native_tmpl.render() | ||
failed = False | ||
except UndefinedError: |
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.
Should use pytest.raises
.
jinja2/utils.py
Outdated
element. | ||
''' | ||
invals = [x for x in invals] | ||
if isinstance(invals, list): |
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 will always be True
because of the previous line.
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.
Fixed
jinja2/utils.py
Outdated
invals = invals[0] | ||
elif len(invals) > 1: | ||
# cast to unicode and join | ||
invals = u''.join([u'%s' % x for x in invals]) |
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.
Implicit encoding seems dangerous, should use text_type(x, 'utf8')
here.
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.
Fixed
jinja2/utils.py
Outdated
invals = invals[0] | ||
elif len(invals) > 1: | ||
# cast to unicode and join | ||
invals = u''.join([u'%s' % x for x in invals]) |
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.
Why do you convert multiple nodes to a string? Wouldn't you want to return the list?
It took me a while to figure out what this was actually doing. Definitely needs some docs, but looks interesting. The term "native" is a bit confusing, since I also associate it with discussion about what Python's How is Ansible using this? That is, when do users need access to the results of a render and don't have access to the original objects instead? Does this need to be in Jinja, as opposed to a separate package providing a different |
Hey @davidism I'm working on addressing your inline code comments at the moment. In terms of how Ansible will be using this... It's going to be transparent to our users, except that when they have chained template operations inside a playbook, types will be preserved all the way through. Here's a basic example:
The type for "tempres" is -always- unicode in Ansible right now. With this feature, it will be an integer. |
jinja2/utils.py
Outdated
elif len(invals) > 1: | ||
# cast to unicode and join | ||
try: | ||
invals = u''.join([text_type(x, 'utf8') for x in invals]) |
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.
Never mind, I messed up the review for this one. Should use text_type
, which is what runtime.to_string
is. But don't specify an encoding, since that's only relevant if the object is bytes
. Should remove the except Exception
block.
jinja2/utils.py
Outdated
invals = u''.join([text_type(x, 'utf8') for x in invals]) | ||
except Exception as e: | ||
pass | ||
return invals |
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.
Should this be return None
? An empty list doesn't make much sense.
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.
Also need a test case for this path.
jinja2/utils.py
Outdated
native, the list is artificial and we should return just the first | ||
element. | ||
''' | ||
invals = [x for x in invals] |
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 don't like building this list, seems wasteful of memory, although I guess there shouldn't be many nodes for the use case you're solving. Could change this to head = list(islice(invals, 2))
to test the length, then either return head[0]
or build a list from invals
in join
.
jinja2/environment.py
Outdated
self.native = native | ||
if self.native: | ||
self.code_generator_class = NativeCodeGenerator | ||
#import pytest; pytest.set_trace() |
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.
🐒
jinja2/environment.py
Outdated
@@ -335,6 +336,11 @@ def __init__(self, | |||
self.enable_async = enable_async | |||
self.is_async = self.enable_async and have_async_gen | |||
|
|||
self.native = native | |||
if self.native: | |||
self.code_generator_class = NativeCodeGenerator |
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 don't like this. I'd rather add a native_code_generator_class
class attribute and pick this one here. Otherwise there's no way of using a custom native code generator class without setting it after instantiating the environment (instead of setting it on the class level on a subclass)
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.
Or just make this a completely separate NativeEnvironment
, rather than an option on the base environment.
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.
Does that mean you'd prefer a subclassed Environment?
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, I think that would be more clear.
Not sure if this really its in the Jinja core. Any chance this would be possible fully on the Ansible side by using a custom Environment/CodeGenerator/Runtime (I made the last two overridable on the Environment level some time ago) |
tests/test_nativetypes.py
Outdated
native_env = Environment(native=True) | ||
native_tmpl = native_env.from_string("{% for x in listone %}{{ x }}{% endfor %}") | ||
result = native_tmpl.render(listone=['a', 'b', 'c', 'd']) | ||
assert isinstance(result, unicode) |
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.
text_type
not unicode
Possible def concat(nodes):
head = list(islice(nodes, 2))
if not head:
return None
if len(head) == 1:
out = head[0]
else:
out = u''.join([text_type(v) for v in nodes])
try:
return literal_eval(out)
except (ValueError, SyntaxError, MemoryError): # possibly RecursionError
return out |
@davidism that concat example breaks this test ...
The result is "cd", which I presume comes from having iterated over the first two values in the generator and not being able to seek backwards. |
|
@davidism @ThiefMaster I think I've addressed everything suggested up till now. Any further thoughts? |
Needs documentation and changelog. Can be moved to a separate module, like the sandboxed env. I still think this needs a clearer name than just "native". |
@davidism Would you want just the new code in environment.py moved to a separate file, or should I also move the new code in utils.py as well? |
All of it. |
@davidism all code is in a separate "nativetypes.py" file now. I honestly don't know what makes sense in terms of naming the classes/files. I chose "native" because it espoused how I think about it, but I realize that's not how everyone thinks. If you or the rest of the team want to decide and pick a name, I'll do the renames. For the docs, how much do you want in the docstrings versus in the "docs" directory? Is there a make script for the "docs" dir ... I didn't see anything obvious. How does the dev team build and examine docs prior to push? |
Install sphinx, cd to docs, |
@davidism first pass on webdocs is done. I wasn't sure where to put it in the TOC, so I placed last in the list, beneath tips n tricks. Here's a rendered example http://tannerjc.net/tmp/jinja/html/nativetypes.html |
docs/nativetypes.rst
Outdated
Native Python Types | ||
=================== | ||
|
||
The Jinja2 :class:`NativeEnvironment` class can be used instead of :class:`Environment` to override jinja's default behavior of returning only strings. This can be useful if you are using jinja outside the context of creating text files. |
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.
"Jinja", without 2, capitalized.
jinja2/nativetypes.py
Outdated
|
||
|
||
def native_concat(nodes): | ||
''' |
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.
Use double quotes, remove blank first line, for all docstrings.
jinja2/nativetypes.py
Outdated
|
||
class NativeCodeGenerator(CodeGenerator): | ||
''' | ||
A custom code generator, which avoids injecting to_string() calls around |
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.
``to_string()``
jinja2/nativetypes.py
Outdated
|
||
def visit_Output(self, node, frame): | ||
''' | ||
Slightly modified from the same method in CodeGenerator, |
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.
:class:CodeGenerator
(linked appropriately)
jinja2/nativetypes.py
Outdated
def visit_Output(self, node, frame): | ||
''' | ||
Slightly modified from the same method in CodeGenerator, | ||
so that to_string() is not inserted afer the yield keyword. |
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.
``to_string()``
tests/test_nativetypes.py
Outdated
|
||
def test_loops(self, env): | ||
|
||
# FIXME - is this what we want? |
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.
Obsolete comment?
tests/test_nativetypes.py
Outdated
|
||
def test_loop_look_alike(self, env): | ||
|
||
# FIXME - conflicts with test_loops |
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.
Obsolete comment?
tests/test_nativetypes.py
Outdated
|
||
:copyright: (c) 2017 by the Jinja Team. | ||
:license: BSD, see LICENSE for more details. | ||
""" |
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.
Remove header and encoding.
tests/test_nativetypes.py
Outdated
""" | ||
import pytest | ||
|
||
from jinja2 import Markup, Environment |
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.
Clean up unused imports.
tests/test_nativetypes.py
Outdated
|
||
|
||
@pytest.mark.test_tests | ||
class TestNativeTestsCase(object): |
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.
TestNativeEnvironment
docs/contents.rst.inc
Outdated
@@ -12,6 +12,7 @@ Jinja2 Documentation | |||
integration | |||
switching | |||
tricks | |||
nativetypes |
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 makes more sense below sandbox.
I'm still hesitant to add this to Jinja. It's such a standalone thing that it would make sense as a Jinja-NativeEnv package. Our philosophy is to avoid adding more features unless they're absolutely necessary, since we have limited resources and want to keep the packages focused on one obvious use case. "Render to Python types" is not an obvious use case, given the features in the rest of the library. Will you be willing to monitor this repository and keep this feature in sync with the rest of the code? Is there a plan within Ansible to provide maintenance for this? @untitaker @ThiefMaster can you help make a decision about this? |
Speaking from my POV, and not on behalf of @jctanner I had initially voiced concern about this living outside of jinja, as the likelihood for a change to crop up in jinja, that impacts this code could be pretty high. My initial recommendation was to get buy in from the authors of jinja, to keep this in mind, and perform external validation to ensure they were not breaking the code in Ansible to perform this, and potentially keeping us informed of such changes. Obviously, this route has some problems. Including in jinja helps us ensure that they code is functional. However at some level I do imagine we would need to vendor this code as well, to support older versions of jinja, such as those an OS packaging system would provide. We already have some of this problem currently. An example was the 2.9 release, where changes broke some things for our users, that we had to deal with it, because we aren't tightly integrated from a community perspective. I'm not necessarily recommending a particular solution, just voicing some concerns. |
We are going to have to "vendor" a copy of this code for older versions of jinja that our consumers might have, so yeah we have to keep ourselves in sync with upstream and do everything we can to keep the upstream functional. My eventual goal is to see if the jinja dev team would consider allowing me to break up CodeGenerator.visitOutput into a few smaller functions. If so, the nativetypes code can be drastically smaller and maintenance will be much simpler. I did not want to muddy the waters and confuse that refactor with this feature, so I didn't bring it up yet.
I fully understand that mentality. I'm still trying to build a list of useful examples of this feature that make sense outside ansible, but I also think that the community might have some input too. |
I think I'm generally in favour of adding that. In the past I used similar things. I will have a look at this. |
jinja2/nativetypes.py
Outdated
finalize = lambda x: text_type( | ||
self.environment.finalize(self.environment, x)) | ||
else: | ||
finalize = lambda x: text_type(self.environment.finalize(x)) |
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.
Should this text_type
be removed?
jinja2/nativetypes.py
Outdated
else: | ||
finalize = lambda x: text_type(self.environment.finalize(x)) | ||
else: | ||
finalize = text_type |
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.
Should this be replaced with a no-op?
jinja2/nativetypes.py
Outdated
close = 0 | ||
if frame.eval_ctx.volatile: | ||
self.write('(escape if context.eval_ctx.autoescape' | ||
' else to_string)(') |
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.
Should this be removed? Does autoescaping even make sense for native types?
jinja2/nativetypes.py
Outdated
close = 0 | ||
if frame.eval_ctx.volatile: | ||
self.write('(escape if context.eval_ctx.autoescape else' | ||
' to_string)(') |
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.
Should this be removed? Does autoescaping even make sense for native types?
jinja2/nativetypes.py
Outdated
getattr(func, 'evalcontextfunction', False): | ||
allow_constant_finalize = False | ||
elif getattr(func, 'environmentfunction', False): | ||
finalize = lambda x: text_type( |
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.
Should this text_type
be removed?
@davidism I removed all of the text_type refs and set the finalize noop per your suggestions. Tests still seem to pass for me. |
anything else I can do to help out here? |
It's been a while (sorry!) and there was some confusion over what we were waiting on here. https://botbot.me/freenode/pocoo/2017-10-31/?msg=92959650&page=3 Our conclusion was that adding |
@davidism thank you so much! You've made many ansible devs happy today! |
@jctanner I just released Jinja 2.10 with this included. I tried to ping you on Twitter but couldn't find your username. 😄 |
@davidism I do all my microblogging in github comments =) New downstream patch for ansible ansible/ansible#32738 |
Hello, |
@arodier we're going to merge ansible/ansible#32738 at the very beginning of the ansible 2.7 development cycle. |
Thank you! |
This works by having an alternate CodeGenerator that avoids doing to_string
after the yield statement and a new version of concat that handles the returned
generator with a bit more "intelligence".
Related to ansible/ansible#23943
We use jinja heavily in the ansible project. Although it seems to target a text based destination for the renderers, our users have a desire to preserve the types of their templated vars. We also do a lot of internal intercept and post-processing to preserve those types but it's hit or miss and never obvious to the end user what will work and what won't. Therefore, I'm trying to extend jinja beyond what it's original usecase might have been.
This is a first pass and I hope to drive discussion with it and shape it into something the rest of the jinja community is happy with.