dmulholl / ibis Goto Github PK
View Code? Open in Web Editor NEWA template engine for people who enjoy the simpler things in life.
Home Page: http://www.dmulholl.com/docs/ibis/master/
License: The Unlicense
A template engine for people who enjoy the simpler things in life.
Home Page: http://www.dmulholl.com/docs/ibis/master/
License: The Unlicense
Hi, I just found this project and really like it! Clean code and good API design ๐ I'm thinking of using it in my project oTree: https://github.com/oTree-org/oTree
It would be really useful if exceptions reported which line in the template the error occurred on, so people could debug more easily. Currently this is what I get:
File ".\ibis_test.py", line 5, in <module>
template = ibis_loader('tpl.html')
File "C:\project\lib\site-packages\ibis\loaders.py", line 50, in __call__
return self.cache.setdefault(path, Template(file.read()))
File "C:\project\lib\site-packages\ibis\templates.py", line 24, in __init__
self.root = self._parse(Lexer(string))
File "C:\project\lib\site-packages\ibis\templates.py", line 54, in _parse
node = nodes.nodemap[token.tag](token)
File "C:\project\lib\site-packages\ibis\nodes.py", line 155, in __init__
self.process_token(token)
File "C:\project\lib\site-packages\ibis\nodes.py", line 394, in process_token
raise TemplateSyntaxError(msg) from None
ibis.errors.TemplateSyntaxError: malformed [if] tag: [if]
Having the file name and line number as attributes on the exception object (e.g. .lineno
) would help authors of other tools, e.g. to show an error page that displays the error in context. However, it seems Ibis does not currently track line numbers internally.
What do you think?
I'm trying to round a floating point number to two decimal places. For example:
a = 3.1415
Template(""" hello {{ "%.2f" % a }} """).render({"a": a})
Needless to say, that doesn't work. I've tried a few options, but I'm at loss. I feel this is simple enough that there must be a solution. What am I missing?
Thanks!
I've been using Ibis (markup as well as API for custom tags) and so far it's perfect!
I'm happy to contribute an implementation of a global strict mode for undefined variables, if you feel that fits well with this project.
Report errors such as:
{{ bogus }}
{{ real.bogus }}
{% if %}
& {% for %}
(In some situations it's useful to have undefined variables, but then I can check |undefined
, great.)
Here are the steps I see:
ibis.errors: add a subclass called StrictUndefined
that raises an error on all methods like __str__
, __bool__
, etc.
In IfNode.eval_condition()
, fix the catch-all except
(I see you already have a note about this)
In Context.resolve()
, change:
except (TypeError, AttributeError):
obj = Undefined()
To:
except (TypeError, AttributeError):
obj = StrictUndefined() if USE_STRICT else Undefined()
Jinja allows passing StrictUndefined as an arg to the Environment and Template constructor, but personally I would be satisfied with a toggle through global var like ibis.USE_STRICT = True
or env var.
I can do all the above by patching Ibis at runtime, but may be better to contribute upstream, so that it integrates well.
Another consideration is whether, as discussed previously, the line numbers of undefined variables would be reported.
(This also seems necessary for CallError, which is raised during method call exceptions.)
One commonly used feature in Django templates is number-based lookups, e.g. {{ item.0 }}
, {{ item.1 }}
. These are typically used for small tuples passed to the template, such as (value, label)
, (min, max)
, (current, total)
, etc.
Ibis has the |index filter, but it actually works inversely to Python's .index()
method:
arr = [1,2,3]
print(T('{{ arr|index(2) }}').render(arr=arr))
print(arr.index(2))
Outputs:
3
1
(I guess Python should have called the method indexof
to avoid the ambiguity.)
What do you think of supporting dot-lookup notation, such as ``{{ item.0 }}`?
I think this could be implemented by adding the following to Context.resolve:
if word.isdigit():
result = result[int(word)]
else:
...
(Possible errors include IndexError if the index is out of range, and TypeError if the item is not subscriptable.)
What do you think of having a "{% verbatim %}" tag like in Django? https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#verbatim
This would allow easier integration with various JS frameworks like Vue, Handlebars, etc.
I am using wtforms and tried laying out radio buttons as described here: https://wtforms.readthedocs.io/en/2.3.x/fields/#wtforms.fields.RadioField
Below is my code:
import wtforms
from ibis import Template
class MyForm(wtforms.Form):
radio = wtforms.RadioField(
choices=['a', 'b'],
)
res = Template("""
{% for subfield in form.radio %}
<tr>
<td>{{ subfield }}</td>
<td>{{ subfield.label }}</td>
</tr>
{% endfor %}
""").render(form=MyForm())
print(res)
The output is about 1000 lines of HTML that look like this:
<tr>
<td><</td>
<td></td>
</tr>
<tr>
<td>u</td>
<td></td>
</tr>
<tr>
<td>l</td>
<td></td>
</tr>
<tr>
<td> </td>
<td></td>
</tr>
...
The reason is that fields in wtforms define __call__
as well as __iter__
. __call__
renders the field as HTML, and __iter__
allows iteration over the field's members (e.g. individual radio buttons in a radio select widget). Since the object is callable, Ibis evaluates it first, turning it to a string. So when ForNode loops over it, it is actually iterating a string, character by character. (So, as you can see above, it is iterating over the first characters of a <ul>
tag.)
This problem does not occur in Django templates because Django checks for an attribute do_not_call_in_templates
before calling a function. WTForms fields set this attribute.
That is one solution. Another would be to tighten the conditions for calling a variable. For example, only call variables if they are functions or methods, but not regular objects with a __call__
method.
Another solution would be to not automatically call variables, but rather require an explicit ()
.
Please see ibis-project/ibis#3716 for details - it looks like https://pypi.org/project/ibis-framework/ installs under the name ibis
, which makes import ibis
ambiguous after both this template engine and Ibis-as-in-SQL are installed.
I think it would help to use exception chaining ("raise from") when raising TemplateError, so the original exception is accessible:
For example:
except Exception as exc:
msg = f"Error calling function '{self.varstr}' "
msg += f"in template '{self.token.template_id}', line {self.token.line_number}."
raise TemplateRenderingError(msg, self.token.template_id, self.token.line_number) from exc
Here is an example of using the chained exception:
try:
return HTMLResponse(ibis_loader(template_name).render(context))
except TemplateRenderingError as exc:
if exc.__cause__:
original_exc = exc.__cause__
raise type(original_exc)(
f'{original_exc} ({exc.template_id}, line {exc.line_number})'
)
raise
To produce:
TypeError: url_for() takes 2 positional arguments but 3 were given (app/Base.html, line 51)
What do you think?
t = Template("""{% %}""")
res = t.render({})
Traceback:
Traceback (most recent call last):
File ".\ibis_test.py", line 23, in <module>
""")
File "c:\otree\ibis\ibis\template.py", line 20, in __init__
self.root_node = compiler.compile(template_string, template_id)
File "c:\otree\ibis\ibis\compiler.py", line 18, in compile
return Parser(template_string, template_id).parse()
File "c:\otree\ibis\ibis\compiler.py", line 166, in parse
elif token.keyword in nodes.instruction_keywords:
File "c:\otree\ibis\ibis\compiler.py", line 35, in keyword
return self.text.split()[0]
IndexError: list index out of range
Would be nice if we raised a template syntax error. For example Django templates report:
TemplateSyntaxError: Empty block tag on line 10
res = T('{{ update }}').render(update='you have 1 new message')
print(res)
Output:
<bound method Context.update of <ibis.context.Context object at 0x02E838F0>>
As you can see, using any name that is also an attribute of the Context object results in that variable name getting shadowed.
Numbers, dates, times, etc. are written differently in different languages., e.g. a comma decimal separator (3,14
). How about some minimal hook to allow applications to format values according to the locale? Here is a suggestion. In ibis/__init__.py
, add some function that can be patched (just as the global loader can be patched):
def localize(value):
return str(value)
Then in PrintNode.wrender, instead of str(content)
we do:
content = ibis.localize(content)
For people migrating from Django templates, or a previous version of Ibis, a common problem would be templates that contain method calls without parentheses, e.g. {{ obj.some_method }}. It would be nice to catch this, especially since if you output it with {{ }}
, it will produce text with angle brackets, which will be interpreted by the browser as HTML tags and produce confusing output:
<bound method A.m of <__main__.A object at 0x019F3990>>
Inside template tags, it will often fail silently (e.g. {% if obj.my_method == 1 %}
will return false).
How about if we add this to _resolve_variable
, after if self.is_func_call:
:
elif isinstance(obj, types.MethodType):
msg = f"'{self.varstring}' is a method, so you must add parentheses, like '{self.varstring}()' "
msg += f"(template '{self.token.template_id}', line {self.token.line_number})."
raise errors.TemplateRenderingError(msg, self.token)
It requires import types
at the top. I checked with python -X importtime
and this doesn't add any overhead (I guess types
is already loaded with Python.)
Now that parentheses are required explicitly, {{ a.b.c }} no longer works when b
is a method. For example:
class Boss:
decision = 'accept'
b = Boss()
class Player:
def get_boss(self): return b
res = Template('{{ player.get_boss().decision }}').render(player=Player(), strict_mode=True)
Raises:
ibis.errors.UndefinedVariable: Cannot resolve the variable 'player.get_boss()' in template 'UNIDENTIFIED', line 1.
Often I want to include the same template in a page multiple times, but passing different arguments. This is a handy way to make reusable components. In Django it would be like this:
{% include 'includes/payment_table.html' with rows=participants_not_reviewed reviewable=True %}
...some content goes here...
{% include 'includes/payment_table.html' with rows=participants_reviewed reviewable=False %}
The best way I have found to do so in Ibis is like this:
{% with reviewable=True %}
{% with participants=participants_not_reviewed %}
{% include 'includes/payment_table.html' %}
{% endwith %}
{% endwith %}
...some content goes here...
{% with reviewable=False %}
{% with participants=participants_reviewed %}
{% include 'includes/payment_table.html' %}
{% endwith %}
{% endwith %}
It's not that bad, but what do you think of some friendlier syntax? Either the ability to pass arguments to {% include %} or pass multiple arguments to {% with %}.
in nodes.py line 181, there is an assignment expression:
if (token := self.token):
I wonder if this could be changed to a regular assignment, to keep compat with Python 3.7 and below?
token = self.token
if token:
If I'm using the FileReloader and change a template that I {% extend %}
from or {% include %}
, Ibis still shows the old file content. See here:
from pathlib import Path
from time import time
# import inspect
import ibis
from ibis.loaders import FileReloader
loader = FileReloader('templates')
ibis.loader = loader
def snippet_cache():
# load it once to cache
loader('main.html').render()
new_text = str(time())
changed_file = Path('templates/snippet.html')
changed_file.write_text(new_text)
actual = loader('main.html').render()
if not new_text in actual:
print('expected:', changed_file.read_text())
print('got', actual)
def base_cache():
# load it once to cache
loader('child.html').render()
new_text = str(time())
changed_file = Path('templates/base.html')
changed_file.write_text(new_text)
actual = loader('child.html').render()
if not new_text in actual:
print('expected:', changed_file.read_text())
print('got', actual)
snippet_cache()
base_cache()
Just want to bring it to your attention.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.