Coder Social home page Coder Social logo

ibis's People

Contributors

dmulholl avatar otree-org avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ibis's Issues

Identifying the line number of an error

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?

Formatting numbers?

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!

Strict mode for undefined variables

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:

  • undefined variables: {{ bogus }}
  • undefined attributes: {{ real.bogus }}
  • undefined variables used in tags, especially {% 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.)

{{ item.0 }} lookups and |index filter

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.)

Objects with __call__ method have problem in templates

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 ().

Use "raise from" with caught exceptions

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?

Provide error message for empty block: {% %}

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

Certain variable names cannot be used: update, push, template, etc

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.

Support for non-English formatting

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)

Catch when a user forgets to call a method

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.)

Method calls inside expressions: {{ a.b().c }}

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.

Passing arguments to {% include %}

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 %}.

"Walrus operator" is not compatible with Python 3.7

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:

FileReloader doesn't see changes to included and inherited templates

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.